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

file src/up.c

#define _XOPEN_SOURCE 700
#include "up.h"
#include "cache.h"
#include "config.h"
#include "package.h"
#include "repo.h"
#include "source.h"
#include "stage.h"
#include "state.h"
#include "../backends/pacman.h"
#include "../backends/deb-version.h"
#include "../backends/apk-version.h"
#include "../backends/xbps-version.h"
#include "version.h"

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

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

static int digest_bytes(const void *data, size_t size, char output[65])
{
    unsigned char bytes[32];
    unsigned int count;
    size_t i;
    if (EVP_Digest(data, size, bytes, &count, EVP_sha256(), NULL) != 1 || count != 32)
        return 0;
    for (i = 0; i < 32; ++i) snprintf(output + 2 * i, 3, "%02x", bytes[i]);
    output[64] = 0;
    return 1;
}

static void quoted(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 int same_slot(const struct holy_package_identity *old,
                     const struct holy_package_identity *candidate)
{
    return !strcmp(old->name, candidate->name) &&
           !strcmp(old->os, candidate->os) &&
           !strcmp(old->arch, candidate->arch) &&
           !strcmp(old->libc, candidate->libc);
}

static int version_order(const struct holy_package_identity *a,
                         const struct holy_package_identity *b, int *order)
{
    char *left = NULL, *right = NULL;
    size_t a_length, b_length;
    int ok = 0;
    if (!a->version_family || !b->version_family ||
        strcmp(a->version_family, b->version_family)) return 0;
    if (!strcmp(a->version_family, "deb")) {
        if (!holy_deb_version_compare(a->version, b->version, order)) return 0;
        return *order || holy_deb_version_compare(a->release, b->release, order);
    }
    if (!strcmp(a->version_family, "holy")) {
        if (!holy_version_compare(a->version, b->version, order)) return 0;
        return *order || holy_version_compare(a->release, b->release, order);
    }
    if (!strcmp(a->version_family, "apk")) {
        if (!holy_apk_version_compare(a->version, b->version, order)) return 0;
        return *order || holy_version_compare(a->release, b->release, order);
    }
    if (!strcmp(a->version_family, "xbps")) {
        a_length = strlen(a->version) + strlen(a->release);
        b_length = strlen(b->version) + strlen(b->release);
        if (a_length > (size_t)-1 - 2 || b_length > (size_t)-1 - 2) return 0;
        left = malloc(a_length + 2);
        right = malloc(b_length + 2);
        if (left && right) {
            snprintf(left, a_length + 2, "%s_%s", a->version, a->release);
            snprintf(right, b_length + 2, "%s_%s", b->version, b->release);
            ok = holy_xbps_version_compare(left, right, order);
        }
        free(left); free(right);
        return ok;
    }
    if (strcmp(a->version_family, "pacman")) return 0;
    a_length = strlen(a->version) + strlen(a->release);
    b_length = strlen(b->version) + strlen(b->release);
    if (a_length > (size_t)-1 - 2 || b_length > (size_t)-1 - 2) return 0;
    left = malloc(a_length + 2);
    right = malloc(b_length + 2);
    if (left && right) {
        snprintf(left, a_length + 2, "%s-%s", a->version, a->release);
        snprintf(right, b_length + 2, "%s-%s", b->version, b->release);
        ok = holy_pacman_version_compare(left, right, order);
    }
    free(left); free(right);
    return ok;
}

static int write_plan(const char *path, const char *data, size_t size)
{
    char *parent = strdup(path), *slash;
    size_t used = 0;
    int fd = -1, dir = -1, ok = 0;
    if (!parent) return 0;
    slash = strrchr(parent, '/');
    if (slash) {
        if (slash == parent) slash[1] = 0;
        else *slash = 0;
    } else { free(parent); parent = strdup("."); if (!parent) return 0; }
    dir = open(parent, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir < 0) goto done;
    fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
    if (fd < 0) goto done;
    while (used < size) {
        ssize_t n = write(fd, data + used, size - used);
        if (n < 0 && errno == EINTR) continue;
        if (n <= 0) goto done;
        used += (size_t)n;
    }
    if (fsync(fd) || fsync(dir)) goto done;
    ok = 1;
done:
    if (fd >= 0) { close(fd); if (!ok) unlink(path); }
    if (dir >= 0) close(dir);
    free(parent);
    return ok;
}

int holy_up_command(int argc, char **argv)
{
    const char *separator, *root = "/", *catalog = NULL, *output = NULL;
    const char *choice = NULL, *arch = NULL, *libc = NULL;
    const char *accepted_arch = NULL, *accepted_privileged = NULL;
    char *alias = NULL, *bound = NULL, *canonical = NULL, *old_snapshot = NULL;
    char *inner = NULL, *plan = NULL, *temporary_dir = NULL, *temporary_plan = NULL;
    struct holy_package_identity old = {0}, *selected = NULL;
    struct holy_repo_slot_list candidates = {0};
    struct holy_repo_set staged = {0};
    char source_id[65], actual_id[65], old_hash[65], inner_hash[65], plan_hash[65], answer[16];
    size_t i, size = 0, compatible = 0, ambiguous = 0;
    int dir = -1, result = 2, prepared = 0, yes = 0, noninteractive = 0;
    int root_seen = 0, old_present = 0, plan_written = 0;
    FILE *stream = NULL;
    if (argc < 3 || !(separator = strchr(argv[2], ':')) ||
        separator == argv[2] || !separator[1] || strchr(separator + 1, ':')) goto done;
    alias = malloc((size_t)(separator - argv[2]) + 1);
    if (!alias) { result = 1; goto done; }
    memcpy(alias, argv[2], (size_t)(separator - argv[2]));
    alias[separator - argv[2]] = 0;
    if (!strcmp(alias, "local")) goto done;
    for (i = 3; i < (size_t)argc; ++i) {
        if (!strcmp(argv[i], "--prepare") && !prepared) prepared = 1;
        else if (!strcmp(argv[i], "--output") && !output && i + 1 < (size_t)argc)
            output = argv[++i];
        else if (!strcmp(argv[i], "--catalog") && !catalog && i + 1 < (size_t)argc)
            catalog = argv[++i];
        else if (!strcmp(argv[i], "--choose") && !choice && i + 1 < (size_t)argc)
            choice = argv[++i];
        else if (!strcmp(argv[i], "--arch") && !arch && i + 1 < (size_t)argc)
            arch = argv[++i];
        else if (!strcmp(argv[i], "--libc") && !libc && i + 1 < (size_t)argc)
            libc = argv[++i];
        else if (!strcmp(argv[i], "--accept-arch") && !accepted_arch && i + 1 < (size_t)argc)
            accepted_arch = argv[++i];
        else if (!strcmp(argv[i], "--accept-privileged") && !accepted_privileged && i + 1 < (size_t)argc)
            accepted_privileged = argv[++i];
        else if (!strcmp(argv[i], "--yes") && !yes) yes = 1;
        else if (!strcmp(argv[i], "--noninteractive") && !noninteractive) noninteractive = 1;
        else if (!strcmp(argv[i], "--root") && !root_seen && i + 1 < (size_t)argc) {
            root = argv[++i]; root_seen = 1;
        } else goto done;
    }
    if ((prepared && yes) ||
        (output && !*output) || !*root ||
        (catalog && !*catalog) || (arch && !*arch) || (libc && !*libc) ||
        (choice && !digest_valid(choice)) ||
        (accepted_arch && !digest_valid(accepted_arch)) ||
        (accepted_privileged && !digest_valid(accepted_privileged))) goto done;
    result = holy_source_active_id(root, alias, source_id);
    if (result) goto done;
    result = holy_state_find_slot(root, source_id, separator + 1, arch, libc, old_hash);
    if (result) goto done;
    old_snapshot = holy_cache_snapshot(old_hash, root);
    if (!old_snapshot || !holy_package_identity(old_snapshot, &old)) {
        fprintf(stderr, "holypkg: previous artifact unavailable for downgrade %s\n", old_hash);
        result = 6; goto done;
    }
    if (!catalog) {
        result = holy_source_catalog_path_fast(root, alias, &bound);
        if (result) goto done;
        catalog = bound;
    }
    canonical = realpath(catalog, NULL);
    dir = canonical ? open(canonical, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC) : -1;
    if (dir < 0 || flock(dir, LOCK_SH)) { result = 6; goto done; }
    result = holy_source_catalog(root, alias, canonical, actual_id);
    if (result || strcmp(source_id, actual_id)) { result = 3; goto done; }
    result = holy_repo_slot_candidates(canonical, &old, &candidates);
    if (result) {
        if (result == 6)
            fprintf(stderr, "holypkg: source package slot changed or disappeared\n");
        goto done;
    }
    for (i = 0; i < candidates.count; ++i) {
        struct holy_package_identity *candidate = &candidates.items[i];
        int order = 0;
        if (!same_slot(&old, candidate)) { result = 1; goto done; }
        ++compatible;
        if (!strcmp(candidate->digest, old_hash)) {
            old_present = 1;
            continue;
        }
        printf("update-candidate %s version %s release %s comparator %s\n",
               candidate->digest, candidate->version, candidate->release,
               candidate->version_family ? candidate->version_family : "unknown");
        if (choice) {
            if (!strcmp(choice, candidate->digest)) {
                selected = candidate;
            }
            continue;
        }
        if (!version_order(candidate, &old, &order)) {
            ambiguous = 1;
            continue;
        }
        if (order <= 0) continue;
        if (!selected) { selected = candidate; continue; }
        if (!version_order(candidate, selected, &order) || !order) ambiguous = 1;
        if (order > 0) selected = candidate;
    }
    if (!compatible) {
        fprintf(stderr, "holypkg: source package slot changed or disappeared\n");
        result = 6; goto done;
    }
    if (choice && !selected) { result = 6; goto done; }
    if (!choice && ambiguous) {
        fprintf(stderr, "holypkg: decision-required update candidate; use --choose SHA256\n");
        result = 3; goto done;
    }
    if (!selected) {
        if (!old_present) {
            fprintf(stderr, "holypkg: installed artifact absent from source and no newer candidate\n");
            result = 6; goto done;
        }
        printf("up-to-date %s %s\n", source_id, old_hash);
        result = 0; goto done;
    }
    if ((accepted_arch && strcmp(accepted_arch, selected->digest)) ||
        (accepted_privileged && strcmp(accepted_privileged, selected->digest))) {
        result = 2; goto done;
    }
    result = holy_repo_stage_slot_digest(canonical, root, &old,
                                         selected->digest, &staged);
    if (result) goto done;
    if (strcmp(staged.index, candidates.index) || staged.count != 1 ||
        strcmp(staged.digests[0], selected->digest)) { result = 3; goto done; }
    result = holy_state_update_prepare(old_hash, selected->digest,
                                       accepted_arch, accepted_privileged,
                                       root, inner_hash, &inner);
    if (result) goto done;
    if (!output) {
        temporary_dir = strdup("/tmp/holypkg-up-XXXXXX");
        if (!temporary_dir || !mkdtemp(temporary_dir)) { result = 1; goto done; }
        temporary_plan = malloc(strlen(temporary_dir) + 6);
        if (!temporary_plan) { result = 1; goto done; }
        sprintf(temporary_plan, "%s/plan", temporary_dir);
        output = temporary_plan;
    }
    stream = open_memstream(&plan, &size);
    if (!stream) { result = 1; goto done; }
    fprintf(stream, "format holy-up-plan-1\nsource-id %s\nalias ", source_id);
    quoted(stream, alias);
    fputs("\ncatalog ", stream);
    quoted(stream, canonical);
    fprintf(stream, "\nindex %s\nold %s\nnew %s\nstate-plan %s\naccept-arch %s\naccept-privileged %s\n",
            staged.index, old_hash, selected->digest, inner_hash,
            accepted_arch ? accepted_arch : "-",
            accepted_privileged ? accepted_privileged : "-");
    fputs(inner, stream);
    {
        int failed = ferror(stream);
        if (fclose(stream)) failed = 1;
        stream = NULL;
        if (failed || !digest_bytes(plan, size, plan_hash) ||
            !write_plan(output, plan, size)) { result = 1; goto done; }
        plan_written = 1;
    }
    printf("prepared %s %s old %s new %s index %s\n",
           plan_hash, output, old_hash, selected->digest, staged.index);
    if (!prepared) {
        char *apply_argv[] = {"holypkg", "apply", (char *)output, "--sha256",
                              plan_hash, "--root", (char *)root, NULL};
        if (fwrite(plan, 1, size, stdout) != size || fflush(stdout)) {
            result = 1; goto done;
        }
        if (!yes) {
            if (noninteractive || !isatty(STDIN_FILENO)) {
                fprintf(stderr, "holypkg: decision-required plan=%s file=%s; apply the reviewed file with its SHA-256\n",
                        plan_hash, output);
                result = 3; goto done;
            }
            if (fprintf(stderr, "Apply plan %s to %s? [y/N] ", plan_hash, root) < 0 ||
                fflush(stderr) || !fgets(answer, sizeof answer, stdin) ||
                (strcmp(answer, "y\n") && strcmp(answer, "Y\n") &&
                 strcmp(answer, "yes\n") && strcmp(answer, "YES\n"))) {
                result = 3; goto done;
            }
        }
        if (dir >= 0) { close(dir); dir = -1; }
        result = holy_apply_command(7, apply_argv);
        if (!result && temporary_plan) {
            unlink(temporary_plan);
            rmdir(temporary_dir);
        }
        if (result) goto done;
    }
    result = 0;
done:
    if (result == 2)
        fputs("usage: holypkg up SOURCE:PACKAGE [--prepare] [--output NEW_FILE] [--catalog MIRROR] [--choose SHA256] [--arch ARCH] [--libc LIBC] [--accept-arch SHA256] [--accept-privileged SHA256] [--root DIRECTORY] [--yes] [--noninteractive]\n", stderr);
    if (stream) fclose(stream);
    if (dir >= 0) close(dir);
    if (old_snapshot) { unlink(old_snapshot); free(old_snapshot); }
    holy_package_identity_free(&old);
    holy_repo_slot_list_free(&candidates);
    holy_repo_set_free(&staged);
    if (temporary_dir && !plan_written) rmdir(temporary_dir);
    free(alias); free(bound); free(canonical); free(inner); free(plan);
    free(temporary_plan); free(temporary_dir);
    return result;
}

static char *read_plan(int fd, size_t *size)
{
    struct stat st;
    char *data = NULL;
    size_t used = 0;
    if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size < 32 ||
        st.st_size > 64 * 1024 * 1024) return NULL;
    *size = (size_t)st.st_size;
    data = malloc(*size + 1);
    if (!data) return NULL;
    while (used < *size) {
        ssize_t n = read(fd, data + used, *size - used);
        if (n < 0 && errno == EINTR) continue;
        if (n <= 0) { free(data); return NULL; }
        used += (size_t)n;
    }
    if (memchr(data, 0, *size) || data[*size - 1] != '\n') {
        free(data); return NULL;
    }
    data[*size] = 0;
    return data;
}

static int take_field(char **cursor, const char *key, char **value)
{
    char *end = strchr(*cursor, '\n'), **tokens = NULL, *error = NULL;
    size_t count = 0;
    int ok = 0;
    *value = NULL;
    if (!end || end - *cursor > 1024 * 1024 ||
        !holy_lex(*cursor, (size_t)(end - *cursor), &tokens, &count,
                  "up plan", 0, &error)) goto done;
    if (count != 2 || strcmp(tokens[0], key)) goto done;
    *value = strdup(tokens[1]);
    if (!*value) goto done;
    *cursor = end + 1;
    ok = 1;
done:
    free(error);
    holy_tokens_free(tokens, count);
    return ok;
}

int holy_apply_command(int argc, char **argv)
{
    const char *root = "/", *approved = NULL;
    char *snapshot = NULL, *old_snapshot = NULL, *data = NULL, *cursor, *body;
    char *source = NULL, *alias = NULL, *catalog = NULL, *index = NULL;
    char *old = NULL, *next = NULL, *inner = NULL, *arch = NULL, *privileged = NULL;
    struct holy_package_identity old_identity = {0};
    char actual[65], source_id[65], current[65];
    size_t size = 0;
    int input = -1, staged = -1, dir = -1, result = 2;
    if (argc == 5 && !strcmp(argv[3], "--sha256")) approved = argv[4];
    else if (argc == 7 && !strcmp(argv[3], "--sha256") &&
             !strcmp(argv[5], "--root")) { approved = argv[4]; root = argv[6]; }
    if (!approved || !digest_valid(approved) || !*root) goto done;
    input = open(argv[2], O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
    if (input < 0) { result = 6; goto done; }
    {
        struct stat st;
        if (fstat(input, &st) || !S_ISREG(st.st_mode) || st.st_size < 32 ||
            st.st_size > 64 * 1024 * 1024) { result = 2; goto done; }
    }
    snapshot = holy_stage_fd(input, "holy-up-plan");
    staged = snapshot ? open(snapshot, O_RDONLY | O_NOFOLLOW | O_CLOEXEC) : -1;
    if (staged < 0 || !(data = read_plan(staged, &size))) { result = 2; goto done; }
    if (!digest_bytes(data, size, actual)) { result = 1; goto done; }
    if (strcmp(actual, approved)) { result = 3; goto done; }
    if (strncmp(data, "format holy-up-plan-1\n", 22)) { result = 2; goto done; }
    cursor = data + 22;
    if (!take_field(&cursor, "source-id", &source) ||
        !take_field(&cursor, "alias", &alias) ||
        !take_field(&cursor, "catalog", &catalog) ||
        !take_field(&cursor, "index", &index) ||
        !take_field(&cursor, "old", &old) ||
        !take_field(&cursor, "new", &next) ||
        !take_field(&cursor, "state-plan", &inner) ||
        !take_field(&cursor, "accept-arch", &arch) ||
        !take_field(&cursor, "accept-privileged", &privileged) ||
        strncmp(cursor, "[update]\n", 9)) { result = 2; goto done; }
    body = cursor;
    if (!digest_valid(source) || !*alias || catalog[0] != '/' ||
        !digest_valid(index) || !digest_valid(old) || !digest_valid(next) ||
        !digest_valid(inner) ||
        (strcmp(arch, "-") && strcmp(arch, next)) ||
        (strcmp(privileged, "-") && strcmp(privileged, next)) ||
        !digest_bytes(body, strlen(body), actual) || strcmp(actual, inner)) {
        result = 2; goto done;
    }
    dir = open(catalog, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir < 0 || flock(dir, LOCK_SH)) { result = 6; goto done; }
    result = holy_source_active_id(root, alias, source_id);
    if (result) goto done;
    if (strcmp(source, source_id)) { result = 3; goto done; }
    result = holy_source_catalog(root, alias, catalog, source_id);
    if (result) goto done;
    old_snapshot = holy_cache_snapshot(old, root);
    if (!old_snapshot || !holy_package_identity(old_snapshot, &old_identity) ||
        strcmp(old_identity.digest, old)) { result = 6; goto done; }
    result = holy_repo_catalog_slot_digest(catalog, &old_identity, next, current);
    if (result != 0 && result != 3) goto done;
    if (strcmp(current, index)) {
        fprintf(stderr, "holypkg: prepared catalog generation changed\n");
        result = 3; goto done;
    }
    if (result) {
        fprintf(stderr, "holypkg: prepared artifact absent from source slot\n");
        goto done;
    }
    result = holy_state_apply_update(inner, old, next,
                                     strcmp(arch, "-") ? arch : NULL,
                                     strcmp(privileged, "-") ? privileged : NULL,
                                     root);
done:
    if (result == 2)
        fputs("usage: holypkg apply PLAN --sha256 PLAN_SHA256 [--root DIRECTORY]\n", stderr);
    if (dir >= 0) close(dir);
    if (staged >= 0) close(staged);
    if (input >= 0) close(input);
    if (snapshot) { unlink(snapshot); free(snapshot); }
    if (old_snapshot) { unlink(old_snapshot); free(old_snapshot); }
    holy_package_identity_free(&old_identity);
    free(data); free(source); free(alias); free(catalog); free(index);
    free(old); free(next); free(inner); free(arch); free(privileged);
    return result;
}