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

file backends/apk.c

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

#include <archive.h>
#include <archive_entry.h>
#include <dirent.h>
#include <errno.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <stdint.h>
#include <strings.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <zlib.h>

int holy_apk_gzip_parts(const char *snapshot, FILE *parts[3],
                        char digests[3][65], uint64_t max_expanded)
{
    unsigned char in[65536], out[65536], digest[32];
    struct stat st;
    off_t offset = 0;
    int fd = open(snapshot, O_RDONLY | O_CLOEXEC), count = 0, ok = 0;
    if (fd < 0 || fstat(fd, &st) || st.st_size <= 0) goto done;
    while (offset < st.st_size && count < 3) {
        z_stream z = {0};
        EVP_MD_CTX *hash = EVP_MD_CTX_new();
        unsigned length = 0;
        uint64_t expanded = 0;
        off_t begin = offset;
        int status = Z_OK;
        parts[count] = tmpfile();
        if (!parts[count] || !hash || EVP_DigestInit_ex(hash, EVP_sha256(), NULL) != 1 ||
            inflateInit2(&z, 15 + 16) != Z_OK) {
            EVP_MD_CTX_free(hash); goto done;
        }
        while (status == Z_OK) {
            ssize_t got = pread(fd, in, sizeof in, offset);
            size_t used;
            if (got <= 0) break;
            z.next_in = in; z.avail_in = (uInt)got;
            while (z.avail_in && status == Z_OK) {
                uInt before = z.avail_in;
                z.next_out = out; z.avail_out = sizeof out;
                status = inflate(&z, Z_NO_FLUSH);
                used = before - z.avail_in;
                if (status != Z_OK && status != Z_STREAM_END) break;
                if (fwrite(in + (got - before), 1, used, parts[count]) != used ||
                    EVP_DigestUpdate(hash, in + (got - before), used) != 1) {
                    status = Z_ERRNO; break;
                }
                offset += (off_t)used;
                expanded += sizeof out - z.avail_out;
                if (expanded > max_expanded ||
                    (!used && z.avail_out == sizeof out)) { status = Z_DATA_ERROR; break; }
            }
        }
        inflateEnd(&z);
        if (status != Z_STREAM_END || offset == begin ||
            EVP_DigestFinal_ex(hash, digest, &length) != 1 || length != 32 ||
            fflush(parts[count])) { EVP_MD_CTX_free(hash); goto done; }
        EVP_MD_CTX_free(hash);
        {
            size_t i;
            for (i = 0; i < 32; ++i) snprintf(digests[count] + i * 2, 3, "%02x", digest[i]);
        }
        ++count;
    }
    ok = offset == st.st_size && count > 0;
done:
    if (fd >= 0) close(fd);
    if (!ok) { size_t i; for (i = 0; i < 3; ++i) { if (parts[i]) fclose(parts[i]); parts[i] = NULL; } }
    return ok ? count : 0;
}

struct apk_index_entry {
    char *name, *version, *arch, *checksum, *depends, *provides;
    unsigned long long size;
    unsigned mask;
};

struct apk_index {
    struct apk_index_entry *entries;
    size_t count, capacity;
};

struct apk_soname_row {
    char *soname;
    const struct apk_index_entry *package;
};

static int digit(char c) { return c >= '0' && c <= '9'; }

static int package_name(const char *name)
{
    const unsigned char *p = (const unsigned char *)name;
    if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
          (*p >= '0' && *p <= '9'))) return 0;
    for (; *p; ++p)
        if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
              (*p >= '0' && *p <= '9') || *p == '.' || *p == '_' ||
              *p == '+' || *p == '-')) return 0;
    return 1;
}

static void quote(FILE *out, const char *value)
{
    const unsigned char *p = (const unsigned char *)value;
    fputc('"', out);
    for (; *p; ++p)
        if (*p == '"' || *p == '\\') fprintf(out, "\\%c", *p);
        else if (*p < 32 || *p >= 127) fprintf(out, "\\x%02x", *p);
        else fputc(*p, out);
    fputc('"', out);
}

static void clear_index(struct apk_index *index)
{
    size_t i;
    for (i = 0; i < index->count; ++i) {
        struct apk_index_entry *e = &index->entries[i];
        free(e->name); free(e->version); free(e->arch); free(e->checksum);
        free(e->depends); free(e->provides);
    }
    free(index->entries);
}

static int entry_order(const void *left, const void *right)
{
    const struct apk_index_entry *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 hash_fd(int fd, char output[65]);

static int soname_order(const void *left, const void *right)
{
    const struct apk_soname_row *a = left, *b = right;
    int order = strcmp(a->soname, b->soname);
    if (!order) order = entry_order(a->package, b->package);
    return order;
}

static int apk_sonames(const struct apk_index *index, int dir, char digest[65])
{
    struct apk_soname_row *rows = NULL;
    size_t count = 0, capacity = 0, i;
    FILE *out = NULL;
    int fd = -1, ok = 0;
    for (i = 0; i < index->count; ++i) {
        const struct apk_index_entry *package = &index->entries[i];
        const char *part = package->provides;
        if (!part) continue;
        while (*part) {
            const char *end, *version;
            size_t length;
            struct apk_soname_row *grown;
            while (*part == ' ' || *part == '\t') ++part;
            if (!*part) break;
            end = part + strcspn(part, " \t");
            if (end - part < 4 || strncmp(part, "so:", 3)) { part = end; continue; }
            version = memchr(part + 3, '=', (size_t)(end - part - 3));
            length = (size_t)((version ? version : end) - part - 3);
            if (!length || length > 255) { part = end; continue; }
            if (count == 1000000) goto done;
            if (count == capacity) {
                size_t next = capacity ? capacity * 2 : 256;
                if (next > 1000000) next = 1000000;
                grown = realloc(rows, next * sizeof *rows);
                if (!grown) goto done;
                rows = grown; capacity = next;
            }
            rows[count].soname = strndup(part + 3, length);
            if (!rows[count].soname) goto done;
            rows[count].package = package;
            ++count;
            part = end;
        }
    }
    qsort(rows, count, sizeof *rows, soname_order);
    fd = openat(dir, "sonames", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
    if (fd < 0 || !(out = fdopen(fd, "w"))) goto done;
    fd = -1;
    fputs("format holy-apk-sonames-1\n", out);
    for (i = 0; i < count; ++i) {
        const struct apk_index_entry *package = rows[i].package;
        if (i && !soname_order(&rows[i - 1], &rows[i])) continue;
        fputs("soname ", out); quote(out, rows[i].soname); fputc(' ', out);
        quote(out, package->name); fputc(' ', out);
        quote(out, package->version); fputc(' ', out);
        quote(out, package->arch); fputc('\n', out);
    }
    {
        int failed = ferror(out);
        if (fflush(out) || fsync(fileno(out))) failed = 1;
        if (fclose(out)) failed = 1;
        out = NULL;
        if (failed) goto done;
    }
    fd = openat(dir, "sonames", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (fd < 0 || !hash_fd(fd, digest)) goto done;
    ok = 1;
done:
    if (out) fclose(out);
    if (fd >= 0) close(fd);
    for (i = 0; i < count; ++i) free(rows[i].soname);
    free(rows);
    return ok;
}

static int parse_index(char *data, size_t size, struct apk_index *index)
{
    size_t start = 0, line = 0;
    int new_record = 1;
    while (start < size) {
        char *end = memchr(data + start, '\n', size - start);
        size_t length = end ? (size_t)(end - data - start) : size - start;
        struct apk_index_entry *e;
        char code, *value, **slot = NULL;
        unsigned bit = 0;
        if (++line > 2000000 || length > 65536 || memchr(data + start, 0, length)) return 0;
        data[start + length] = 0;
        if (!length) { new_record = 1; start += length + (end != NULL); continue; }
        if (length < 3 || data[start + 1] != ':' || index->count > 100000) return 0;
        code = data[start]; value = data + start + 2;
        if (!*value || strchr(value, '\r')) return 0;
        if (new_record) {
            if (index->count == index->capacity) {
                size_t capacity = index->capacity ? index->capacity * 2 : 256;
                void *grown;
                if (capacity > 100000) capacity = 100000;
                if (capacity == index->capacity) return 0;
                grown = realloc(index->entries, capacity * sizeof *index->entries);
                if (!grown) return 0;
                index->entries = grown; index->capacity = capacity;
            }
            e = &index->entries[index->count++]; memset(e, 0, sizeof *e);
            new_record = 0;
        }
        e = &index->entries[index->count - 1];
        switch (code) {
        case 'P': slot = &e->name; bit = 1; break;
        case 'V': slot = &e->version; bit = 2; break;
        case 'A': slot = &e->arch; bit = 4; break;
        case 'C': slot = &e->checksum; bit = 8; break;
        case 'S': bit = 16; break;
        case 'D': slot = &e->depends; bit = 32; break;
        case 'p': slot = &e->provides; bit = 64; break;
        default: break;
        }
        if (bit && (e->mask & bit)) return 0;
        e->mask |= bit;
        if (slot) { *slot = strdup(value); if (!*slot) return 0; }
        if (code == 'S') {
            const char *p = value;
            for (; *p; ++p) {
                if (!digit(*p) || e->size > (1000000000000ULL - (unsigned)(*p - '0')) / 10) return 0;
                e->size = e->size * 10 + (unsigned)(*p - '0');
            }
        }
        start += length + (end != NULL);
    }
    if (!index->count) return 0;
    for (start = 0; start < index->count; ++start) {
        struct apk_index_entry *e = &index->entries[start];
        if ((e->mask & 31) != 31 || !package_name(e->name) ||
            !e->version[0] || !e->arch[0] || strlen(e->checksum) > 128) return 0;
    }
    qsort(index->entries, index->count, sizeof *index->entries, entry_order);
    for (start = 1; start < index->count; ++start)
        if (!entry_order(&index->entries[start - 1], &index->entries[start])) return 0;
    return 1;
}

static int read_index_tar(FILE *part, int signature, char **data, size_t *size)
{
    struct archive *ar = archive_read_new();
    struct archive_entry *entry;
    char descriptor[64];
    int status, ok = 0, seen = 0;
    if (!ar) return 0;
    snprintf(descriptor, sizeof descriptor, "/proc/self/fd/%d", fileno(part));
    if (archive_read_support_filter_gzip(ar) != ARCHIVE_OK ||
        archive_read_support_format_tar(ar) != ARCHIVE_OK ||
        archive_read_open_filename(ar, descriptor, 65536) != ARCHIVE_OK) goto done;
    while ((status = archive_read_next_header(ar, &entry)) == ARCHIVE_OK) {
        const char *name = archive_entry_pathname(entry);
        long long length = archive_entry_size(entry);
        if (!name || length < 0 || archive_entry_filetype(entry) != AE_IFREG ||
            archive_entry_hardlink(entry) || archive_entry_symlink(entry)) goto done;
        if (signature) {
            if (strncmp(name, ".SIGN.", 6) || length > 4096 ||
                archive_read_data_skip(ar) != ARCHIVE_OK) goto done;
            ++seen;
        } else if (!strcmp(name, "APKINDEX")) {
            size_t used = 0;
            if (seen++ || length > 32LL * 1024 * 1024) goto done;
            *data = malloc((size_t)length + 1);
            if (!*data) goto done;
            while (used < (size_t)length) {
                la_ssize_t got = archive_read_data(ar, *data + used, (size_t)length - used);
                if (got <= 0) goto done;
                used += (size_t)got;
            }
            (*data)[used] = 0; *size = used;
        } else if (!strcmp(name, "DESCRIPTION") && length <= 1024 * 1024) {
            if (archive_read_data_skip(ar) != ARCHIVE_OK) goto done;
        } else goto done;
    }
    ok = status == ARCHIVE_EOF && seen > 0;
done:
    archive_read_free(ar);
    return ok;
}

int holy_apk_verify_signature(FILE *signature, FILE *payload,
                              const char *public_key, const char *keyname,
                              char algorithm[16])
{
    struct archive *archive = archive_read_new();
    struct archive_entry *entry;
    EVP_PKEY *key = NULL;
    EVP_MD_CTX *context = NULL;
    FILE *keyfile = NULL;
    const EVP_MD *method = NULL;
    unsigned char signature_bytes[4096], buffer[65536];
    char descriptor[64];
    size_t got;
    int status, found = 0, ok = 0;
    if (!archive) return 0;
    if (!*keyname || strlen(keyname) > 255) goto done;
    keyfile = fopen(public_key, "r");
    if (!keyfile) goto done;
    key = PEM_read_PUBKEY(keyfile, NULL, NULL, NULL);
    if (!key || EVP_PKEY_base_id(key) != EVP_PKEY_RSA) goto done;
    snprintf(descriptor, sizeof descriptor, "/proc/self/fd/%d", fileno(signature));
    if (archive_read_support_filter_gzip(archive) != ARCHIVE_OK ||
        archive_read_support_format_tar(archive) != ARCHIVE_OK ||
        archive_read_open_filename(archive, descriptor, 65536) != ARCHIVE_OK) goto done;
    while ((status = archive_read_next_header(archive, &entry)) == ARCHIVE_OK) {
        const char *name = archive_entry_pathname(entry), *tail;
        la_int64_t size = archive_entry_size(entry);
        if (!name || strncmp(name, ".SIGN.", 6) || size < 0 || size > 4096 ||
            archive_entry_filetype(entry) != AE_IFREG || archive_entry_hardlink(entry) ||
            archive_entry_symlink(entry)) goto done;
        tail = name + 6;
        if (!strncmp(tail, "RSA256.", 7) && !strcmp(tail + 7, keyname)) {
            method = EVP_sha256(); strcpy(algorithm, "rsa-sha256");
        } else if (!strncmp(tail, "RSA512.", 7) && !strcmp(tail + 7, keyname)) {
            method = EVP_sha512(); strcpy(algorithm, "rsa-sha512");
        } else if (!strncmp(tail, "RSA.", 4) && !strcmp(tail + 4, keyname)) {
            method = EVP_sha1(); strcpy(algorithm, "rsa-sha1");
        } else {
            if (archive_read_data_skip(archive) != ARCHIVE_OK) goto done;
            continue;
        }
        if (found++ || !size) goto done;
        {
            size_t used = 0;
            while (used < (size_t)size) {
                la_ssize_t read_size = archive_read_data(archive, signature_bytes + used,
                                                           (size_t)size - used);
                if (read_size <= 0) goto done;
                used += (size_t)read_size;
            }
        }
        context = EVP_MD_CTX_new();
        if (!context || EVP_DigestVerifyInit(context, NULL, method, NULL, key) != 1 ||
            fseek(payload, 0, SEEK_SET)) goto done;
        while ((got = fread(buffer, 1, sizeof buffer, payload)) != 0)
            if (EVP_DigestVerifyUpdate(context, buffer, got) != 1) goto done;
        if (ferror(payload) || EVP_DigestVerifyFinal(context, signature_bytes,
                                                     (size_t)size) != 1) goto done;
    }
    ok = status == ARCHIVE_EOF && found == 1;
done:
    if (keyfile) fclose(keyfile);
    if (context) EVP_MD_CTX_free(context);
    if (key) EVP_PKEY_free(key);
    archive_read_free(archive);
    return ok;
}

int holy_apk_key_fingerprint(const char *public_key, char digest[65])
{
    EVP_PKEY *key = NULL;
    FILE *file = NULL;
    unsigned char *der = NULL, *cursor, bytes[32];
    unsigned length;
    int size, result = 0;
    size_t i;
    if (!public_key || !digest) return 0;
    file = fopen(public_key, "r");
    if (!file) goto done;
    key = PEM_read_PUBKEY(file, NULL, NULL, NULL);
    if (!key || EVP_PKEY_base_id(key) != EVP_PKEY_RSA) goto done;
    size = i2d_PUBKEY(key, NULL);
    if (size <= 0 || size > 8192 || !(der = malloc((size_t)size))) goto done;
    cursor = der;
    if (i2d_PUBKEY(key, &cursor) != size ||
        EVP_Digest(der, (size_t)size, bytes, &length, EVP_sha256(), NULL) != 1 ||
        length != 32) goto done;
    for (i = 0; i < 32; ++i) snprintf(digest + i * 2, 3, "%02x", bytes[i]);
    result = 1;
done:
    if (file) fclose(file);
    if (key) EVP_PKEY_free(key);
    free(der);
    return result;
}

static int verify_index_key(const char *input, const char *public_key,
                            const char *keyname, char algorithm[16])
{
    FILE *parts[3] = {0};
    struct stat st;
    char digests[3][65] = {{0}}, *snapshot = NULL, *bytes = NULL;
    size_t size = 0, i;
    int fd = -1, count, result = 4;
    if (!input || !public_key || !*public_key || !keyname) return 2;
    fd = open(input, 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 > 64LL * 1024 * 1024) { result = 6; goto done; }
    snapshot = holy_stage_fd(fd, "holy-apk-verify");
    if (!snapshot) { result = 1; goto done; }
    count = holy_apk_gzip_parts(snapshot, parts, digests, 64ULL * 1024 * 1024);
    if (count != 2 || !read_index_tar(parts[0], 1, &bytes, &size) ||
        !read_index_tar(parts[1], 0, &bytes, &size) ||
        !holy_apk_verify_signature(parts[0], parts[1], public_key, keyname,
                                 algorithm)) goto done;
    result = 0;
done:
    for (i = 0; i < 3; ++i) if (parts[i]) fclose(parts[i]);
    if (fd >= 0) close(fd);
    if (snapshot) { unlink(snapshot); free(snapshot); }
    free(bytes);
    return result;
}

int holy_apk_verify_index(const char *input, const char *public_key)
{
    char algorithm[16] = {0};
    const char *keyname;
    int result;
    if (!public_key) return 2;
    keyname = strrchr(public_key, '/');
    keyname = keyname ? keyname + 1 : public_key;
    result = verify_index_key(input, public_key, keyname, algorithm);
    if (result) fprintf(stderr, "holypkg: APK index signature verification failed (status %d)\n", result);
    else printf("verified APKINDEX %s\n", algorithm);
    return result;
}

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

static int hash_file(const char *path, char output[65])
{
    int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC), ok;
    if (fd < 0) return 0;
    ok = hash_fd(fd, output);
    close(fd);
    return ok;
}

static int copy_original(const char *path, int output)
{
    int source = open(path, O_RDONLY | O_CLOEXEC);
    int target = openat(output, "original", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
    char buffer[65536];
    ssize_t got;
    int ok = 0;
    if (source < 0 || target < 0) goto done;
    while ((got = read(source, buffer, sizeof buffer)) != 0) {
        size_t used = 0;
        if (got < 0 && errno == EINTR) continue;
        if (got < 0) goto done;
        while (used < (size_t)got) {
            ssize_t n = write(target, buffer + used, (size_t)got - used);
            if (n < 0 && errno == EINTR) continue;
            if (n <= 0) goto done;
            used += (size_t)n;
        }
    }
    ok = !fsync(target);
done:
    if (source >= 0) close(source);
    if (target >= 0) close(target);
    return ok;
}

static int base_url(const char *url)
{
    const char *host, *end;
    const unsigned char *p;
    if (!strncmp(url, "https://", 8)) host = url + 8;
    else if (!strncmp(url, "http://", 7)) host = url + 7;
    else return 0;
    end = strchr(host, '/');
    if (!end || end == host || url[strlen(url) - 1] != '/' ||
        memchr(host, '@', (size_t)(end - host)) || strchr(url, '?') || strchr(url, '#')) return 0;
    for (p = (const unsigned char *)url; *p; ++p) if (*p <= 32 || *p == 127) return 0;
    return 1;
}

static int apk_index_bound(const char *input, const char *source, const char *base,
                           const char *output, const char *source_id, const char *repo,
                           const char *verification, const char *key_hash)
{
    struct apk_index index = {0};
    struct stat st;
    FILE *parts[3] = {0}, *catalog = NULL, *record = NULL;
    char digests[3][65] = {{0}}, digest[65], catalog_digest[65], sonames_digest[65];
    char *snapshot = NULL, *bytes = NULL;
    size_t size = 0, i;
    int input_fd = -1, output_fd = -1, count, result = 1;
    if (!input || !source || !*source || !strcmp(source, "local") ||
        !base || !base_url(base) || !output) return 2;
    for (i = 0; source[i]; ++i)
        if ((unsigned char)source[i] <= 32 || source[i] == ':' || source[i] == '/' || source[i] == '@') return 2;
    input_fd = open(input, O_RDONLY | O_NONBLOCK | O_CLOEXEC);
    if (input_fd < 0 || fstat(input_fd, &st) || !S_ISREG(st.st_mode) ||
        st.st_size < 0 || st.st_size > 64LL * 1024 * 1024) { result = 6; goto done; }
    snapshot = holy_stage_fd(input_fd, "holy-apk-index");
    if (!snapshot || !hash_file(snapshot, digest) || mkdir(output, 0700)) goto done;
    output_fd = open(output, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (output_fd < 0 || fstat(output_fd, &st) || st.st_uid != geteuid() ||
        (st.st_mode & 0777) != 0700 || !copy_original(snapshot, output_fd)) goto done;
    result = 2;
    count = holy_apk_gzip_parts(snapshot, parts, digests, 64ULL * 1024 * 1024);
    if (count < 1 || count > 2 ||
        (count == 2 && !read_index_tar(parts[0], 1, &bytes, &size)) ||
        !read_index_tar(parts[count - 1], 0, &bytes, &size) ||
        !parse_index(bytes, size, &index)) goto done;
    result = 1;
    {
        int fd = openat(output_fd, "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-apk-catalog-1\n", catalog);
    for (i = 0; i < index.count; ++i) {
        const struct apk_index_entry *e = &index.entries[i];
        fputs("package ", catalog); quote(catalog, e->name); fputc(' ', catalog);
        quote(catalog, e->version); fputc(' ', catalog);
        quote(catalog, e->arch); fputc(' ', catalog);
        quote(catalog, e->checksum); fprintf(catalog, " %llu ", e->size);
        quote(catalog, e->depends ? e->depends : "-"); fputc(' ', catalog);
        quote(catalog, e->provides ? e->provides : "-"); fputc('\n', catalog);
    }
    {
        int failed = ferror(catalog);
        if (fflush(catalog) || fsync(fileno(catalog))) failed = 1;
        if (fclose(catalog)) failed = 1;
        catalog = NULL;
        if (failed) goto done;
    }
    {
        int fd = openat(output_fd, "catalog", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
        int ok = fd >= 0 && hash_fd(fd, catalog_digest);
        if (fd >= 0) close(fd);
        if (!ok) goto done;
    }
    if (!apk_sonames(&index, output_fd, sonames_digest)) goto done;
    {
        int fd = openat(output_fd, "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; }
    }
    fputs("format holy-apk-index-record-1\nsource-name ", record); quote(record, source);
    fputs("\nbase-url ", record); quote(record, base);
    if (source_id) { fprintf(record, "\nsource-id %s\nrepo ", source_id); quote(record, repo); }
    fprintf(record, "\noriginal-sha256 %s\ncatalog-sha256 %s\nsonames-sha256 %s\nverification %s\n",
            digest, catalog_digest, sonames_digest,
            verification ? verification : "unverified");
    if (key_hash) fprintf(record, "public-key-sha256 %s\n", key_hash);
    fprintf(record, "package-coverage complete\nfile-coverage unavailable\npackages %zu\nstate complete\n",
            index.count);
    {
        int failed = ferror(record);
        if (fflush(record) || fsync(fileno(record))) failed = 1;
        if (fclose(record)) failed = 1;
        record = NULL;
        if (failed) goto done;
    }
    if (fsync(output_fd)) goto done;
    printf("indexed %zu packages original %s\n", index.count, digest);
    result = 0;
done:
    if (result) fprintf(stderr, "holypkg: APK index incomplete (status %d)\n", result);
    if (catalog) fclose(catalog);
    if (record) fclose(record);
    for (i = 0; i < 3; ++i) if (parts[i]) fclose(parts[i]);
    if (output_fd >= 0) close(output_fd);
    if (input_fd >= 0) close(input_fd);
    if (snapshot) { unlink(snapshot); free(snapshot); }
    free(bytes); clear_index(&index);
    return result;
}

int holy_apk_index(const char *input, const char *source, const char *base,
                   const char *output)
{
    return apk_index_bound(input, source, base, output, NULL, NULL, NULL, NULL);
}

static int query_sonames(int dir, const char *expected, const char *soname,
                         const char *arch, char **name, char **version)
{
    char actual[65], *line = NULL;
    size_t capacity = 0, matches = 0;
    struct stat st;
    FILE *file = NULL;
    int fd = openat(dir, "sonames", O_RDONLY | O_NOFOLLOW | O_CLOEXEC), result = 2;
    if (fd < 0) { result = 6; goto done; }
    if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size < 25 ||
        st.st_size > 64LL * 1024 * 1024 || !hash_fd(fd, actual) ||
        strcmp(actual, expected)) goto done;
    file = fdopen(fd, "r");
    if (!file) goto done;
    fd = -1;
    if (getline(&line, &capacity, file) < 0 ||
        strcmp(line, "format holy-apk-sonames-1\n")) goto done;
    while (1) {
        char **v = NULL, *error = NULL;
        size_t n = 0;
        ssize_t got = getline(&line, &capacity, file);
        int order;
        if (got < 0) break;
        if (got > 65536 || !holy_lex(line, (size_t)got, &v, &n,
                                     "APK SONAME index", 0, &error) ||
            n != 5 || strcmp(v[0], "soname")) {
            free(error); holy_tokens_free(v, n); goto done;
        }
        order = strcmp(v[1], soname);
        if (!order && (!arch || !strcmp(v[4], arch))) {
            if (arch) {
                fprintf(stderr, "holypkg: APK candidate %s %s %s for %s\n",
                        v[2], v[3], v[4], soname);
                if (!matches) {
                    *name = strdup(v[2]); *version = strdup(v[3]);
                    if (!*name || !*version) {
                        free(error); holy_tokens_free(v, n); result = 1; goto done;
                    }
                }
            } else printf("%s %s %s index-hint\n", v[2], v[3], v[4]);
            ++matches;
        }
        free(error); holy_tokens_free(v, n);
        if (order > 0) break;
    }
    if (ferror(file)) goto done;
    result = arch && matches > 1 ? 3 : matches ? 0 : 4;
    if (result == 3)
        fputs("holypkg: select an APK candidate by NAME VERSION ARCH with apk fetch\n", stderr);
done:
    if (result && arch) { free(*name); free(*version); *name = *version = NULL; }
    if (file) fclose(file);
    if (fd >= 0) close(fd);
    free(line);
    return result;
}

int holy_apk_query(const char *directory, const char *query, int mode)
{
    char *line = NULL;
    char expected[65] = {0}, sonames_digest[65] = {0}, actual[65];
    FILE *catalog = NULL, *record = NULL;
    struct stat st;
    size_t capacity = 0, matches = 0;
    int dir = -1, fd, result = 2;
    if (!directory || !query || !*query || mode < 0 || mode > 2 ||
        (mode == 2 && (strchr(query, '/') || strlen(query) > 255 ||
                       !package_name(query)))) return 2;
    dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir < 0) { result = 6; goto done; }
    fd = openat(dir, "conversion", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (fd < 0) { result = 6; goto done; }
    record = fdopen(fd, "r");
    if (!record) { close(fd); goto done; }
    if (!record || fstat(fileno(record), &st) || !S_ISREG(st.st_mode) || st.st_size > 4096 ||
        getline(&line, &capacity, record) < 0 ||
        strcmp(line, "format holy-apk-index-record-1\n")) goto done;
    {
        int complete = 0;
        ssize_t got;
        while ((got = getline(&line, &capacity, record)) > 0) {
            if (got > 4096) goto done;
            if (!strncmp(line, "catalog-sha256 ", 15) && got == 80) {
                memcpy(expected, line + 15, 64); expected[64] = 0;
                if (strspn(expected, "0123456789abcdef") != 64) goto done;
            }
            if (!strncmp(line, "sonames-sha256 ", 15) && got == 80) {
                memcpy(sonames_digest, line + 15, 64); sonames_digest[64] = 0;
                if (strspn(sonames_digest, "0123456789abcdef") != 64) goto done;
            }
            if (!strcmp(line, "state complete\n")) complete = 1;
        }
        if (!complete || !expected[0] || (mode == 2 && !sonames_digest[0])) {
            result = 6; goto done;
        }
    }
    fd = openat(dir, "catalog", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (fd < 0) { result = 6; goto done; }
    catalog = fdopen(fd, "r");
    if (!catalog) { close(fd); goto done; }
    if (!catalog || fstat(fileno(catalog), &st) || !S_ISREG(st.st_mode) ||
        st.st_size < 27 || st.st_size > 64LL * 1024 * 1024 ||
        !hash_fd(fileno(catalog), actual) || strcmp(expected, actual) ||
        getline(&line, &capacity, catalog) < 0 ||
        strcmp(line, "format holy-apk-catalog-1\n")) goto done;
    if (mode == 2) {
        result = query_sonames(dir, sonames_digest, query, NULL, NULL, NULL);
        goto done;
    }
    while (1) {
        char **v = NULL, *error = NULL;
        size_t n = 0;
        ssize_t got = getline(&line, &capacity, catalog);
        if (got < 0) break;
        if (got > 65536 || !holy_lex(line, (size_t)got, &v, &n,
                                     "APK catalog", 0, &error) ||
            n != 8 || strcmp(v[0], "package")) {
            free(error); holy_tokens_free(v, n); goto done;
        }
        if ((mode == 1 && !strcmp(v[1], query)) ||
            (mode == 0 && strstr(v[1], query))) {
            ++matches;
            if (mode == 1) {
                printf("package %s\nversion %s\narch %s\nchecksum %s\nsize %s\ndepend %s\nprovides %s\n",
                       v[1], v[2], v[3], v[4], v[5], v[6], v[7]);
            } else printf("%s %s %s\n", v[1], v[2], v[3]);
        }
        free(error); holy_tokens_free(v, n);
    }
    if (ferror(catalog)) goto done;
    result = matches ? mode == 1 && matches > 1 ? 3 : 0 : 4;
done:
    if (result == 2) fputs("holypkg: malformed APK catalog\n", stderr);
    if (catalog) fclose(catalog);
    if (record) fclose(record);
    if (dir >= 0) close(dir);
    free(line);
    return result;
}

struct apk_selection {
    char *source, *base, *checksum, *repo;
    char source_id[65];
    char verification[16], key_hash[65];
    unsigned long long size;
    char index_hash[65], catalog_hash[65], sonames_hash[65];
};

static void free_selection(struct apk_selection *selection)
{
    free(selection->source); free(selection->base); free(selection->checksum);
    free(selection->repo);
}

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

static int select_package(const char *directory, const char *name,
                          const char *version, const char *arch,
                          struct apk_selection *selection)
{
    FILE *file = NULL;
    char *line = NULL, *snapshot = NULL;
    size_t capacity = 0, matches = 0;
    struct stat st;
    char actual[65];
    int dir = -1, fd = -1, result = 2;
    dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir < 0) { result = 6; goto done; }
    fd = openat(dir, "conversion", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size > 4096) {
        result = 6; goto done;
    }
    file = fdopen(fd, "r");
    if (!file) goto done;
    fd = -1;
    while (1) {
        char **v = NULL, *error = NULL;
        size_t n = 0;
        ssize_t got = getline(&line, &capacity, file);
        if (got < 0) break;
        if (got > 4096 || !holy_lex(line, (size_t)got, &v, &n,
                                     "APK conversion", 0, &error)) {
            free(error); holy_tokens_free(v, n); goto done;
        }
        if (n == 2 && !strcmp(v[0], "source-name")) {
            if (selection->source) { holy_tokens_free(v, n); goto done; }
            selection->source = strdup(v[1]);
        } else if (n == 2 && !strcmp(v[0], "base-url")) {
            if (selection->base) { holy_tokens_free(v, n); goto done; }
            selection->base = strdup(v[1]);
        } else if (n == 2 && !strcmp(v[0], "repo")) {
            if (selection->repo) { holy_tokens_free(v, n); goto done; }
            selection->repo = strdup(v[1]);
        } else if (n == 2 && !strcmp(v[0], "source-id")) {
            if (selection->source_id[0] || !hex_digest(v[1])) {
                holy_tokens_free(v, n); goto done;
            }
            memcpy(selection->source_id, v[1], 65);
        } else if (n == 2 && !strcmp(v[0], "verification")) {
            if (selection->verification[0] || strlen(v[1]) >= sizeof selection->verification) {
                holy_tokens_free(v, n); goto done;
            }
            strcpy(selection->verification, v[1]);
        } else if (n == 2 && !strcmp(v[0], "public-key-sha256")) {
            if (selection->key_hash[0] || !hex_digest(v[1])) {
                holy_tokens_free(v, n); goto done;
            }
            memcpy(selection->key_hash, v[1], 65);
        } else if (n == 2 && !strcmp(v[0], "original-sha256") && hex_digest(v[1])) {
            memcpy(selection->index_hash, v[1], 65);
        } else if (n == 2 && !strcmp(v[0], "catalog-sha256") && hex_digest(v[1])) {
            memcpy(selection->catalog_hash, v[1], 65);
        } else if (n == 2 && !strcmp(v[0], "sonames-sha256") && hex_digest(v[1])) {
            memcpy(selection->sonames_hash, v[1], 65);
        } else if (n == 2 && !strcmp(v[0], "state") && !strcmp(v[1], "complete")) {
            ++matches;
        }
        free(error); holy_tokens_free(v, n);
    }
    if (ferror(file) || matches != 1 || !selection->source || !selection->base ||
        !selection->index_hash[0] || !selection->catalog_hash[0] ||
        !base_url(selection->base) || (!!selection->repo != !!selection->source_id[0]) ||
        (!strcmp(selection->verification, "unverified") && selection->key_hash[0]) ||
        (strcmp(selection->verification, "unverified") &&
         strcmp(selection->verification, "rsa-sha1") &&
         strcmp(selection->verification, "rsa-sha256") &&
         strcmp(selection->verification, "rsa-sha512")) ||
        (strcmp(selection->verification, "unverified") && !selection->key_hash[0])) goto done;
    fclose(file); file = NULL;
    fd = openat(dir, "original", 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, actual) ||
        strcmp(actual, selection->index_hash)) goto done;
    close(fd); fd = -1;
    fd = openat(dir, "catalog", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) ||
        st.st_size > 64LL * 1024 * 1024) goto done;
    snapshot = holy_stage_fd(fd, "holy-apk-catalog");
    close(fd); fd = -1;
    if (!snapshot || !hash_file(snapshot, actual) || strcmp(actual, selection->catalog_hash)) goto done;
    file = fopen(snapshot, "r");
    if (!file || getline(&line, &capacity, file) < 0 ||
        strcmp(line, "format holy-apk-catalog-1\n")) goto done;
    matches = 0;
    while (1) {
        char **v = NULL, *error = NULL;
        size_t n = 0;
        ssize_t got = getline(&line, &capacity, file);
        if (got < 0) break;
        if (got > 65536 || !holy_lex(line, (size_t)got, &v, &n,
                                     "APK catalog", 0, &error) ||
            n != 8 || strcmp(v[0], "package")) {
            free(error); holy_tokens_free(v, n); goto done;
        }
        if (name && !strcmp(v[1], name) && !strcmp(v[2], version) && !strcmp(v[3], arch)) {
            char *end;
            if (++matches > 1) { free(error); holy_tokens_free(v, n); goto done; }
            selection->checksum = strdup(v[4]);
            errno = 0;
            selection->size = strtoull(v[5], &end, 10);
            if (!selection->checksum || errno || *end || !*v[5]) {
                free(error); holy_tokens_free(v, n); goto done;
            }
        }
        free(error); holy_tokens_free(v, n);
    }
    if (ferror(file)) goto done;
    result = !name || matches == 1 ? 0 : 4;
done:
    if (file) fclose(file);
    if (fd >= 0) close(fd);
    if (dir >= 0) close(dir);
    if (snapshot) { unlink(snapshot); free(snapshot); }
    free(line);
    return result;
}

static int select_soname_candidate(const char *catalog, const char *soname,
                                    const char *arch, char **name, char **version)
{
    struct apk_selection selection = {0};
    int dir = -1, result;
    *name = NULL; *version = NULL;
    if (!catalog || !soname || !arch || !*soname || strchr(soname, '/') ||
        strlen(soname) > 255 || !package_name(soname) || !package_name(arch)) return 2;
    result = select_package(catalog, NULL, NULL, NULL, &selection);
    if (result) goto done;
    if (!selection.sonames_hash[0]) { result = 6; goto done; }
    dir = open(catalog, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir < 0) { result = 6; goto done; }
    result = query_sonames(dir, selection.sonames_hash, soname,
                           arch, name, version);
done:
    if (dir >= 0) close(dir);
    free_selection(&selection);
    return result;
}

int holy_apk_fetch_provider(const char *catalog, const char *soname, const char *arch,
                            const char *output, const char *sha256,
                            const char *ca_file, const char *root,
                            const char *source_alias, const char *public_key)
{
    char *name = NULL, *version = NULL;
    int result = select_soname_candidate(catalog, soname, arch, &name, &version);
    if (!result) result = holy_apk_fetch(catalog, name, version, arch, output,
                                         sha256, ca_file, root, source_alias,
                                         public_key, 1, soname, NULL);
    free(name); free(version);
    return result;
}

static int binding_name(const char *id, const char *repo, char name[130])
{
    unsigned char digest[32];
    unsigned size;
    size_t i;
    if (!hex_digest(id) || EVP_Digest(repo, strlen(repo), digest, &size,
                                      EVP_sha256(), NULL) != 1 || size != 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 binding_directory(int database, int create)
{
    struct stat st;
    int dir;
    if (create && mkdirat(database, "apk-catalogs", 0700) && errno != EEXIST) return -1;
    dir = openat(database, "apk-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_uid != 0) || (st.st_mode & 0022)) {
        close(dir); return -1;
    }
    return dir;
}

int holy_apk_bind(const char *root, const char *source, const char *repo,
                  const char *catalog, const char *accepted,
                  const char *public_key)
{
    struct apk_selection selected = {0};
    char id[65], source_key[65], name[130], temporary[43] = {0};
    char checked_key[65], algorithm[16] = {0};
    char *base = NULL, *trust = NULL, *path = NULL, *root_path = NULL, *record = NULL;
    char *original = NULL, *key_snapshot = NULL;
    unsigned long long generation;
    size_t size = 0, used = 0;
    FILE *stream = NULL;
    int database = -1, dir = -1, fd = -1, result;
    if (!root || !source || !repo || !catalog || (accepted && !hex_digest(accepted))) return 2;
    result = holy_source_apk_repo(root, source, repo, id, &base, &trust, source_key);
    if (result) goto done;
    path = realpath(catalog, NULL);
    if (!path) { result = 6; goto done; }
    result = select_package(path, NULL, NULL, NULL, &selected);
    if (result || strcmp(selected.source_id, id) ||
        strcmp(selected.repo, repo) || strcmp(selected.base, base)) {
        result = 6; goto done;
    }
    if (source_key[0] ? strcmp(selected.key_hash, source_key) ||
                        !strcmp(selected.verification, "unverified") :
                        !strcmp(trust, "require")) { result = 6; goto done; }
    if (source_key[0]) {
        const char *keyname = public_key ? strrchr(public_key, '/') : NULL;
        keyname = keyname ? keyname + 1 : public_key;
        key_snapshot = public_key ? holy_stage_local(public_key, "holy-apk-key") : NULL;
        if (!key_snapshot || !holy_apk_key_fingerprint(key_snapshot, checked_key) ||
            strcmp(checked_key, source_key)) { result = 6; goto done; }
        original = malloc(strlen(path) + 10);
        if (!original) { result = 1; goto done; }
        sprintf(original, "%s/original", path);
        result = verify_index_key(original, key_snapshot, keyname, algorithm);
        if (result || strcmp(algorithm, selected.verification)) { result = 4; goto done; }
    } else if (public_key) { result = 2; goto done; }
    if (accepted && strcmp(accepted, selected.index_hash)) { result = 4; goto done; }
    if (!accepted && !source_key[0] && strcmp(trust, "ignore")) {
        fprintf(stderr, "holypkg: decision-required unsigned APK index source=%s repo=%s sha256=%s; --accept-unsigned %s confirms binding\n",
                id, repo, selected.index_hash, selected.index_hash);
        result = 3; goto done;
    }
    if (!binding_name(id, repo, name)) { result = 2; goto done; }
    root_path = realpath(root, NULL);
    if (!root_path) { result = 6; goto done; }
    stream = open_memstream(&record, &size);
    if (!stream) { result = 1; goto done; }
    fprintf(stream, "format holy-apk-binding-2\nsource-id %s\nrepo ", id);
    quote(stream, repo);
    fprintf(stream, "\nindex-sha256 %s\ncatalog-sha256 %s\nverification %s\npublic-key-sha256 %s\n%s ",
            selected.index_hash, selected.catalog_hash, selected.verification,
            selected.key_hash[0] ? selected.key_hash : "-",
            strcmp(root_path, "/") && !strncmp(path, root_path, strlen(root_path)) &&
            path[strlen(root_path)] == '/' ? "root-path" : "path");
    quote(stream, strcmp(root_path, "/") && !strncmp(path, root_path, strlen(root_path)) &&
          path[strlen(root_path)] == '/' ? path + strlen(root_path) : path);
    fputc('\n', stream);
    {
        int failed = ferror(stream);
        if (fclose(stream)) failed = 1;
        stream = NULL;
        if (failed || size > 4096) { result = 1; goto done; }
    }
    database = holy_state_lock(root, 1, &generation, &result);
    if (database < 0) goto done;
    dir = binding_directory(database, 1);
    if (dir < 0) { result = 1; goto done; }
    fd = holy_temporary_at(dir, temporary);
    if (fd < 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;
    printf("apk-catalog-bound %s repo %s index %s\n", id, repo, selected.index_hash);
done:
    if (result) fprintf(stderr, "holypkg: APK catalog binding failed (status %d)\n", result);
    if (stream) fclose(stream);
    if (fd >= 0) close(fd);
    if (dir >= 0) {
        if (temporary[0]) unlinkat(dir, temporary, 0);
        close(dir);
    }
    if (database >= 0) close(database);
    free(base); free(trust); free(path); free(root_path); free(record); free(original);
    if (key_snapshot) { unlink(key_snapshot); free(key_snapshot); }
    free_selection(&selected);
    return result;
}

static char *binding_field(FILE *file, const char *key)
{
    char *line = NULL, **v = NULL, *error = NULL, *value = NULL;
    size_t capacity = 0, count = 0;
    ssize_t got = getline(&line, &capacity, file);
    if (got > 0 && got <= 4096 &&
        holy_lex(line, (size_t)got, &v, &count, "APK binding", 0, &error) &&
        count == 2 && !strcmp(v[0], key)) value = strdup(v[1]);
    holy_tokens_free(v, count); free(error); free(line);
    return value;
}

int holy_apk_catalog_path(const char *root, const char *source, const char *repo,
                          char **catalog)
{
    struct apk_selection selected = {0};
    struct stat st;
    char id[65], source_key[65], name[130], *base = NULL, *trust = NULL;
    char *saved_id = NULL, *saved_repo = NULL, *index = NULL, *hash = NULL;
    char *verification = NULL, *saved_key = NULL;
    char *path = NULL, *root_path = NULL, *joined = NULL, *key = NULL;
    unsigned long long generation;
    FILE *file = NULL;
    int database = -1, dir = -1, fd = -1, result, root_relative = 0;
    *catalog = NULL;
    result = holy_source_apk_repo(root, source, repo, id, &base, &trust, source_key);
    if (result) goto done;
    if ((!source_key[0] && !strcmp(trust, "require")) ||
        !binding_name(id, repo, name)) {
        result = 6; goto done;
    }
    database = holy_state_lock(root, 0, &generation, &result);
    if (database < 0) goto done;
    dir = binding_directory(database, 0);
    if (dir < 0) { result = 6; goto done; }
    fd = openat(dir, name, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size > 4096 ||
        (st.st_mode & 0022) || (st.st_uid != 0 && st.st_uid != geteuid())) {
        result = 6; goto done;
    }
    file = fdopen(fd, "r");
    if (!file) { result = 1; goto done; }
    fd = -1;
    key = binding_field(file, "format");
    saved_id = binding_field(file, "source-id");
    saved_repo = binding_field(file, "repo");
    index = binding_field(file, "index-sha256");
    hash = binding_field(file, "catalog-sha256");
    verification = binding_field(file, "verification");
    saved_key = binding_field(file, "public-key-sha256");
    if (!key || strcmp(key, "holy-apk-binding-2") || !saved_id || strcmp(saved_id, id) ||
        !saved_repo || strcmp(saved_repo, repo) || !index || !hex_digest(index) ||
        !hash || !hex_digest(hash) || !verification || !saved_key ||
        (strcmp(saved_key, "-") && !hex_digest(saved_key))) { result = 6; goto done; }
    {
        char *line = NULL, **v = NULL, *error = NULL;
        size_t capacity = 0, n = 0;
        ssize_t got = getline(&line, &capacity, file);
        if (got <= 0 || got > 4096 ||
            !holy_lex(line, (size_t)got, &v, &n, "APK binding", 0, &error) ||
            n != 2 || (strcmp(v[0], "path") && strcmp(v[0], "root-path")) ||
            v[1][0] != '/' || fgetc(file) != EOF) result = 6;
        else {
            path = strdup(v[1]);
            if (!path) result = 1;
            root_relative = !strcmp(v[0], "root-path");
            if (root_relative) root_path = realpath(root, NULL);
        }
        holy_tokens_free(v, n); free(error); free(line);
        if (result) goto done;
    }
    if (root_relative && !root_path) { result = 6; goto done; }
    if (root_path) {
        size_t a = strlen(root_path), b = strlen(path);
        if (a > (size_t)-1 - b - 1) { result = 1; goto done; }
        joined = malloc(a + b + 1);
        if (!joined) { result = 1; goto done; }
        if (!strcmp(root_path, "/")) memcpy(joined, path, b + 1);
        else { memcpy(joined, root_path, a); memcpy(joined + a, path, b + 1); }
        free(path); path = realpath(joined, NULL);
        if (!path || (strcmp(root_path, "/") &&
                      (strncmp(path, root_path, a) || path[a] != '/'))) {
            result = 6; goto done;
        }
    } else {
        char *resolved = realpath(path, NULL);
        free(path); path = resolved;
        if (!path) { result = 6; goto done; }
    }
    result = select_package(path, NULL, NULL, NULL, &selected);
    if (result || strcmp(selected.source_id, id) ||
        strcmp(selected.repo, repo) || strcmp(selected.base, base) ||
        strcmp(selected.index_hash, index) || strcmp(selected.catalog_hash, hash) ||
        strcmp(selected.verification, verification) ||
        strcmp(selected.key_hash[0] ? selected.key_hash : "-", saved_key) ||
        (source_key[0] && (strcmp(selected.key_hash, source_key) ||
                           !strcmp(selected.verification, "unverified")))) {
        result = 6; goto done;
    }
    *catalog = path; path = NULL; result = 0;
done:
    if (result) fprintf(stderr, "holypkg: bound APK catalog unavailable (status %d)\n", result);
    if (file) fclose(file);
    if (fd >= 0) close(fd);
    if (dir >= 0) close(dir);
    if (database >= 0) close(database);
    free(base); free(trust); free(saved_id); free(saved_repo);
    free(index); free(hash); free(verification); free(saved_key);
    free(path); free(root_path); free(joined); free(key);
    free_selection(&selected);
    return result;
}

static int check_q1(FILE *control, const char *checksum)
{
    unsigned char expected[24], actual[EVP_MAX_MD_SIZE], buffer[65536];
    EVP_MD_CTX *hash = EVP_MD_CTX_new();
    unsigned length = 0;
    size_t got;
    int decoded, ok = 0;
    if (!hash) return 0;
    if (strlen(checksum) != 30 || strncmp(checksum, "Q1", 2) ||
        checksum[29] != '=') goto done;
    decoded = EVP_DecodeBlock(expected, (const unsigned char *)checksum + 2, 28);
    if (decoded != 21 || fseek(control, 0, SEEK_SET) ||
        EVP_DigestInit_ex(hash, EVP_sha1(), NULL) != 1) goto done;
    while ((got = fread(buffer, 1, sizeof buffer, control)) != 0)
        if (EVP_DigestUpdate(hash, buffer, got) != 1) goto done;
    ok = !ferror(control) && EVP_DigestFinal_ex(hash, actual, &length) == 1 &&
         length == 20 && !memcmp(actual, expected, 20);
done:
    EVP_MD_CTX_free(hash);
    return ok;
}

static int control_identity(FILE *control, const char *name,
                            const char *version, const char *arch,
                            const char *datahash, int require_datahash)
{
    struct archive *reader = archive_read_new();
    struct archive_entry *entry;
    char descriptor[64], *bytes = NULL, *line, *save = NULL;
    unsigned seen = 0;
    int result = 0, status;
    if (!reader) return 0;
    snprintf(descriptor, sizeof descriptor, "/proc/self/fd/%d", fileno(control));
    if (archive_read_support_filter_all(reader) != ARCHIVE_OK ||
        archive_read_support_format_tar(reader) != ARCHIVE_OK ||
        archive_read_open_filename(reader, descriptor, 65536) != ARCHIVE_OK) goto done;
    while ((status = archive_read_next_header(reader, &entry)) == ARCHIVE_OK) {
        const char *path = archive_entry_pathname(entry);
        if (path && (!strcmp(path, ".PKGINFO") || !strcmp(path, "./.PKGINFO"))) {
            la_int64_t size = archive_entry_size(entry);
            if (seen || size < 0 || size > 1024 * 1024 ||
                archive_entry_filetype(entry) != AE_IFREG) goto done;
            bytes = malloc((size_t)size + 1);
            if (!bytes) goto done;
            {
                size_t used = 0;
                while (used < (size_t)size) {
                    la_ssize_t got = archive_read_data(reader, bytes + used, (size_t)size - used);
                    if (got <= 0) goto done;
                    used += (size_t)got;
                }
            }
            if (memchr(bytes, 0, (size_t)size)) goto done;
            bytes[size] = 0;
            seen = 1;
        } else if (archive_read_data_skip(reader) != ARCHIVE_OK) goto done;
    }
    if (status != ARCHIVE_EOF || !seen) goto done;
    {
        unsigned fields = 0;
        for (line = strtok_r(bytes, "\n", &save); line; line = strtok_r(NULL, "\n", &save)) {
            const char *value;
            size_t length = strlen(line);
            if (length && line[length - 1] == '\r') line[length - 1] = 0;
            if (!strncmp(line, "pkgname = ", 10)) { value = line + 10; if (fields & 1 || strcmp(value, name)) goto done; fields |= 1; }
            else if (!strncmp(line, "pkgver = ", 9)) { value = line + 9; if (fields & 2 || strcmp(value, version)) goto done; fields |= 2; }
            else if (!strncmp(line, "arch = ", 7)) { value = line + 7; if (fields & 4 || strcmp(value, arch)) goto done; fields |= 4; }
            else if (!strncmp(line, "datahash = ", 11)) { value = line + 11; if (fields & 8 || strcasecmp(value, datahash)) goto done; fields |= 8; }
        }
        result = (fields & 7) == 7 && (!require_datahash || (fields & 8));
    }
done:
    free(bytes);
    archive_read_free(reader);
    return result;
}

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

struct required_path { const char *path; int found; };

static int imported_path(void *opaque, const struct holy_manifest_entry *entry)
{
    struct required_path *required = opaque;
    if (!entry->directory && !strcmp(entry->path, required->path)) required->found = 1;
    return 1;
}

static int safe_required_path(const char *path)
{
    const char *part, *end;
    if (!path || path[0] != '/' || !path[1] || strlen(path) > 1024 ||
        path[strlen(path) - 1] == '/') return 0;
    for (part = path + 1; *part; part = end + 1) {
        size_t i, length;
        end = strchr(part, '/');
        length = end ? (size_t)(end - part) : strlen(part);
        if (!length || (length == 1 && part[0] == '.') ||
            (length == 2 && part[0] == '.' && part[1] == '.')) return 0;
        for (i = 0; i < length; ++i)
            if ((unsigned char)part[i] < 32 || (unsigned char)part[i] == 127) return 0;
        if (!end) break;
    }
    return 1;
}

static int imported_claim(const char *directory, const char *name, const char *version,
                          const char *arch, const char *soname, const char *required_file)
{
    DIR *dir = opendir(directory);
    struct dirent *entry;
    size_t count = 0;
    int found = 0, ok = 0;
    struct required_path required = {required_file ? required_file + 1 : NULL, 0};
    if (!dir) return 0;
    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) || strcmp(identity.version, version) ||
            strcmp(identity.release, "1") ||
            !imported_arch_matches(arch, identity.arch) ||
            (soname && !holy_provides_match(path, "soname", soname, &matched)) ||
            (required_file && !holy_verify_visit(path, imported_path, &required))) {
            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) &&
         (!required_file || required.found);
done:
    closedir(dir);
    return ok;
}

int holy_apk_fetch(const char *catalog, const char *name, const char *version,
                   const char *arch, const char *output, const char *sha256,
                   const char *ca_file, const char *root, const char *source_alias,
                   const char *public_key, int import, const char *required_soname,
                   const char *required_file)
{
    struct apk_selection selection = {0};
    FILE *parts[3] = {0}, *receipt = NULL;
    struct stat st;
    char digests[3][65] = {{0}}, digest[65] = {0}, template[] = "/tmp/holy-apk-fetch-XXXXXX";
    char *url = NULL, *filename = NULL, *downloaded = NULL, *converted = NULL;
    char *registered_base = NULL, *registered_trust = NULL;
    char *bound_catalog = NULL, *canonical_catalog = NULL;
    char *key_snapshot = NULL;
    char registered_id[65] = {0}, registered_key[65] = {0}, checked_key[65];
    char package_verification[16] = "unverified";
    int dir = -1, temp = 0, count, result = 1, i;
    size_t length;
    if (!catalog || !name || !version || !arch || !output ||
        (source_alias && !root) || (public_key && !root) ||
        !package_name(name) || !package_name(version) || !package_name(arch) ||
        (required_soname && (!import || strlen(required_soname) > 255 ||
                             !package_name(required_soname))) ||
        (required_file && (!import || !safe_required_path(required_file))) ||
        (sha256 && !hex_digest(sha256))) return 2;
    result = select_package(catalog, name, version, arch, &selection);
    if (result) goto done;
    if (root) {
        if (!selection.source_id[0]) { result = 2; goto done; }
        result = holy_source_apk_repo(root, source_alias ? source_alias : selection.source,
                                      selection.repo,
                                      registered_id, &registered_base, &registered_trust,
                                      registered_key);
        if (result) goto done;
        if (strcmp(registered_id, selection.source_id) ||
            strcmp(registered_base, selection.base) ||
            (registered_key[0] && (strcmp(registered_key, selection.key_hash) ||
                                   !strcmp(selection.verification, "unverified"))) ||
            (!registered_key[0] && !strcmp(registered_trust, "require"))) {
            result = 6; goto done;
        }
        if (registered_key[0]) {
            if (!public_key || !(key_snapshot = holy_stage_local(public_key, "holy-apk-key")) ||
                !holy_apk_key_fingerprint(key_snapshot, checked_key) ||
                strcmp(checked_key, registered_key)) { result = 6; goto done; }
        } else if (public_key) { result = 2; goto done; }
        result = holy_apk_catalog_path(root,
                                       source_alias ? source_alias : selection.source,
                                       selection.repo, &bound_catalog);
        canonical_catalog = realpath(catalog, NULL);
        if (result || !canonical_catalog || strcmp(canonical_catalog, bound_catalog)) {
            result = 6; goto done;
        }
    }
    if (strncmp(selection.base, "https://", 8)) { result = 6; goto done; }
    length = strlen(name) + strlen(version) + 6;
    filename = malloc(length);
    if (!filename) { result = 1; goto done; }
    snprintf(filename, length, "%s-%s.apk", name, version);
    url = holy_fetch_child_url(selection.base, filename);
    if (!url) { result = 2; goto done; }
    if (!mkdtemp(template)) { result = 1; goto done; }
    temp = 1;
    result = holy_fetch_https_foreign(url, template, ca_file, digest);
    if (result) goto done;
    if (sha256 && strcmp(sha256, digest)) { result = 4; goto done; }
    downloaded = malloc(strlen(template) + 66);
    if (!downloaded) { result = 1; goto done; }
    sprintf(downloaded, "%s/%s", template, digest);
    if (stat(downloaded, &st) || !S_ISREG(st.st_mode) ||
        (unsigned long long)st.st_size != selection.size) { result = 4; goto done; }
    count = holy_apk_gzip_parts(downloaded, parts, digests, 4ULL * 1024 * 1024 * 1024);
    if (count < 2 || count > 3 || !check_q1(parts[count - 2], selection.checksum) ||
        !control_identity(parts[count - 2], name, version, arch, digests[count - 1],
                          strcmp(selection.verification, "unverified") != 0)) {
        result = 4; goto done;
    }
    if (registered_key[0] && root) {
        const char *keyname = strrchr(public_key, '/');
        keyname = keyname ? keyname + 1 : public_key;
        if (count != 3 || !holy_apk_verify_signature(parts[0], parts[1],
                                                     key_snapshot, keyname,
                                                     package_verification)) {
            result = 4; goto done;
        }
    }
    if (root) {
        char refreshed_id[65];
        char refreshed_key[65];
        char *refreshed_base = NULL, *refreshed_trust = NULL;
        char *refreshed_catalog = NULL;
        result = holy_source_apk_repo(root, source_alias ? source_alias : selection.source,
                                      selection.repo,
                                      refreshed_id, &refreshed_base, &refreshed_trust,
                                      refreshed_key);
        if (!result && (strcmp(refreshed_id, registered_id) ||
                        strcmp(refreshed_base, registered_base) ||
                        strcmp(refreshed_trust, registered_trust) ||
                        strcmp(refreshed_key, registered_key))) result = 3;
        free(refreshed_base); free(refreshed_trust);
        if (result) goto done;
        result = holy_apk_catalog_path(root,
                                       source_alias ? source_alias : selection.source,
                                       selection.repo, &refreshed_catalog);
        if (!result && strcmp(refreshed_catalog, canonical_catalog)) result = 3;
        free(refreshed_catalog);
        if (result) goto done;
    }
    result = 1;
    if (mkdir(output, 0700)) goto done;
    dir = open(output, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir < 0 || !copy_original(downloaded, dir)) goto done;
    if (import) {
        char *original = malloc(strlen(output) + sizeof "/original");
        converted = malloc(strlen(output) + sizeof "/converted");
        if (!original || !converted) {
            free(original); result = 1; goto done;
        }
        sprintf(original, "%s/original", output);
        sprintf(converted, "%s/converted", output);
        result = holy_import_apk_verified(original, selection.source, converted,
                                          registered_key[0] ? public_key : NULL,
                                          digest, registered_key[0] ? registered_key : NULL,
                                          selection.index_hash, url);
        free(original);
        if (result) goto done;
        if (!imported_claim(converted, name, version, arch, required_soname,
                            required_file)) {
            if (required_soname)
                fprintf(stderr, "holypkg: APK payload does not provide SONAME %s\n", required_soname);
            if (required_file)
                fprintf(stderr, "holypkg: APK payload does not contain %s\n", required_file);
            result = 4; goto done;
        }
    }
    {
        int fd = openat(dir, "selection", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
        if (fd < 0) goto done;
        receipt = fdopen(fd, "w");
        if (!receipt) { close(fd); goto done; }
    }
    fputs("format holy-apk-selection-1\nsource-name ", receipt); quote(receipt, selection.source);
    fputs("\nbase-url ", receipt); quote(receipt, selection.base);
    fputs("\nurl ", receipt); quote(receipt, url);
    fputs("\nname ", receipt); quote(receipt, name);
    fputs("\nversion ", receipt); quote(receipt, version);
    fputs("\narch ", receipt); quote(receipt, arch);
    if (selection.source_id[0]) {
        fprintf(receipt, "\nsource-id %s\nrepo ", selection.source_id);
        quote(receipt, selection.repo);
        fprintf(receipt, "\nsource-binding %s", root ? "checked" : "unchecked");
        if (root) {
            fputs("\nactive-source-name ", receipt);
            quote(receipt, source_alias ? source_alias : selection.source);
        }
    }
    fprintf(receipt, "\nindex-sha256 %s\ncatalog-sha256 %s\nindex-verification %s\n",
            selection.index_hash, selection.catalog_hash, selection.verification);
    if (selection.key_hash[0]) fprintf(receipt, "index-key-sha256 %s\n", selection.key_hash);
    fprintf(receipt, "original-sha256 %s\ncontrol-checksum %s\nverification %s\n",
            digest, selection.checksum, package_verification);
    if (registered_key[0] && root)
        fprintf(receipt, "package-key-sha256 %s\n", registered_key);
    if (required_soname) fprintf(receipt, "required-soname %s\nsoname-provider verified-payload\n", required_soname);
    if (required_file) {
        fputs("required-file ", receipt); quote(receipt, required_file);
        fputs("\nfile-provider verified-payload\n", receipt);
    }
    fprintf(receipt, "imported %s\n", import ? "yes" : "no");
    fputs("state complete\n", receipt);
    {
        int failed = ferror(receipt);
        if (fflush(receipt) || fsync(fileno(receipt))) failed = 1;
        if (fclose(receipt)) failed = 1;
        receipt = NULL;
        if (failed || fsync(dir)) goto done;
    }
    result = 0;
done:
    if (result) fprintf(stderr, "holypkg: APK fetch failed (status %d)\n", result);
    if (receipt) fclose(receipt);
    if (dir >= 0) close(dir);
    for (i = 0; i < 3; ++i) if (parts[i]) fclose(parts[i]);
    if (downloaded) unlink(downloaded);
    if (temp) rmdir(template);
    free(downloaded); free(url); free(filename); free(converted);
    free(registered_base); free(registered_trust);
    free(bound_catalog); free(canonical_catalog);
    if (key_snapshot) { unlink(key_snapshot); free(key_snapshot); }
    free_selection(&selection);
    return result;
}

int holy_apk_sync(const char *root, const char *source, const char *repo,
                  const char *output, const char *sha256,
                  const char *accept_unsigned, const char *ca_file,
                  const char *public_key)
{
    char id[65], current_id[65], source_key[65], current_key[65];
    char supplied_key[65] = {0}, digest[65] = {0}, algorithm[16] = {0};
    char template[] = "/tmp/holy-apk-sync-XXXXXX";
    char *base = NULL, *trust = NULL, *current_base = NULL, *current_trust = NULL;
    char *url = NULL, *downloaded = NULL, *key_snapshot = NULL;
    struct stat st;
    int temp = 0, result;
    if (!root || !source || !repo || !output || !*output ||
        (sha256 && !hex_digest(sha256)) ||
        (accept_unsigned && !hex_digest(accept_unsigned)) ||
        (sha256 && accept_unsigned)) return 2;
    if (lstat(output, &st) == 0 || errno != ENOENT) return 2;
    result = holy_source_apk_repo(root, source, repo, id, &base, &trust, source_key);
    if (result) goto done;
    if (source_key[0] && public_key)
        key_snapshot = holy_stage_local(public_key, "holy-apk-key");
    if ((source_key[0] && (!key_snapshot ||
                           !holy_apk_key_fingerprint(key_snapshot, supplied_key))) ||
        (public_key && !source_key[0]) ||
        (!source_key[0] && !strcmp(trust, "require"))) {
        fputs("holypkg: APK source needs its configured RSA public key\n", stderr);
        result = 6; goto done;
    }
    if (source_key[0] && strcmp(source_key, supplied_key)) { result = 4; goto done; }
    if (source_key[0] && accept_unsigned) { result = 2; goto done; }
    url = holy_fetch_child_url(base, "APKINDEX.tar.gz");
    if (!url || !mkdtemp(template)) { result = 1; goto done; }
    temp = 1;
    result = holy_fetch_https_foreign(url, template, ca_file, digest);
    if (result) goto done;
    if (sha256 && strcmp(sha256, digest)) { result = 4; goto done; }
    if (accept_unsigned && strcmp(accept_unsigned, digest)) { result = 4; goto done; }
    downloaded = malloc(strlen(template) + 66);
    if (!downloaded) { result = 1; goto done; }
    sprintf(downloaded, "%s/%s", template, digest);
    if (source_key[0]) {
        const char *keyname = strrchr(public_key, '/');
        keyname = keyname ? keyname + 1 : public_key;
        result = verify_index_key(downloaded, key_snapshot, keyname, algorithm);
        if (result) goto done;
    }
    if (!source_key[0] && !sha256 && !accept_unsigned && strcmp(trust, "ignore")) {
        fprintf(stderr, "holypkg: decision-required unsigned APK index source=%s repo=%s sha256=%s; --accept-unsigned %s confirms this generation\n",
                id, repo, digest, digest);
        result = 3; goto done;
    }
    result = holy_source_apk_repo(root, source, repo, current_id,
                                  &current_base, &current_trust, current_key);
    if (result) goto done;
    if (strcmp(id, current_id) || strcmp(base, current_base) ||
        strcmp(trust, current_trust) || strcmp(source_key, current_key)) {
        result = 3; goto done;
    }
    result = apk_index_bound(downloaded, source, base, output, id, repo,
                             source_key[0] ? algorithm : NULL,
                             source_key[0] ? source_key : NULL);
    if (!result) result = holy_apk_bind(root, source, repo, output,
                                        source_key[0] ? NULL : digest,
                                        source_key[0] ? public_key : NULL);
done:
    if (result && result != 3)
        fprintf(stderr, "holypkg: APK source sync failed (status %d)\n", result);
    if (downloaded) unlink(downloaded);
    if (temp) {
        char path[sizeof template + 65];
        if (digest[0]) { snprintf(path, sizeof path, "%s/%s", template, digest); unlink(path); }
        rmdir(template);
    }
    free(downloaded); free(url); free(base); free(trust);
    if (key_snapshot) { unlink(key_snapshot); free(key_snapshot); }
    free(current_base); free(current_trust);
    return result;
}