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

file src/installer.c

#define _XOPEN_SOURCE 700
#include "config.h"
#include "disk.h"

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

struct install_input {
    char *root;
    char **artifacts;
    size_t count;
    char **accepted_arch;
    size_t accept_count;
    char **accepted_privileged;
    size_t privileged_count;
    char **sources;
    size_t source_count;
    struct stat root_stat;
    char hash[65];
};

static int digest_valid(const char *s)
{
    size_t i;
    if (strlen(s) != 64) return 0;
    for (i = 0; i < 64; ++i)
        if (!((s[i] >= '0' && s[i] <= '9') ||
              (s[i] >= 'a' && s[i] <= 'f'))) return 0;
    return 1;
}

static char *source_binding(const char *artifact, const char *source)
{
    char *binding;
    if (!digest_valid(artifact) || !digest_valid(source)) return NULL;
    binding = malloc(130);
    if (binding) snprintf(binding, 130, "%s=%s", artifact, source);
    return binding;
}

static int hex_digest(const unsigned char *bytes, char output[65])
{
    static const char digits[] = "0123456789abcdef";
    size_t i;
    for (i = 0; i < 32; ++i) {
        output[i * 2] = digits[bytes[i] >> 4];
        output[i * 2 + 1] = digits[bytes[i] & 15];
    }
    output[64] = 0;
    return 1;
}

static int hash_string(EVP_MD_CTX *ctx, const char *s)
{
    size_t size = strlen(s), i;
    unsigned char length[8];
    if ((unsigned long long)size != size) return 0;
    for (i = 0; i < sizeof length; ++i)
        length[i] = (unsigned char)((unsigned long long)size >> (i * 8));
    return EVP_DigestUpdate(ctx, length, sizeof length) == 1 &&
           EVP_DigestUpdate(ctx, s, size) == 1;
}

static int config_hash(const struct holy_config *config, char output[65])
{
    EVP_MD_CTX *ctx = EVP_MD_CTX_new();
    unsigned char bytes[32];
    unsigned length;
    size_t i, j;
    int ok = ctx && EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) == 1;
    for (i = 0; i < config->count && ok; ++i) {
        const struct holy_entry *e = &config->entries[i];
        ok = hash_string(ctx, e->section) && hash_string(ctx, e->key);
        for (j = 0; j < e->count && ok; ++j) ok = hash_string(ctx, e->values[j]);
    }
    if (ok) ok = EVP_DigestFinal_ex(ctx, bytes, &length) == 1 && length == 32;
    EVP_MD_CTX_free(ctx);
    return ok && hex_digest(bytes, output);
}

static const char *field(const struct holy_config *config, const char *section,
                         const char *key)
{
    size_t i;
    for (i = 0; i < config->count; ++i)
        if (!strcmp(config->entries[i].section, section) &&
            !strcmp(config->entries[i].key, key))
            return config->entries[i].values[0];
    return NULL;
}

static int load_input(const char *path, struct holy_config *config,
                      struct install_input *input)
{
    const char *root;
    char *error = NULL;
    size_t i, j;
    if (!holy_config_load(path, config, &error)) {
        fprintf(stderr, "holyinstall: %s\n", error ? error : "invalid config");
        free(error);
        return 2;
    }
    root = field(config, "install", "root");
    if (!root) { fputs("holyinstall: [install] root is required\n", stderr); return 2; }
    input->root = realpath(root, NULL);
    if (!input->root || stat(input->root, &input->root_stat) ||
        !S_ISDIR(input->root_stat.st_mode)) {
        fputs("holyinstall: target root must be an existing directory\n", stderr);
        return 6;
    }
    if (!strcmp(input->root, "/")) {
        fputs("holyinstall: host / is not an installation target\n", stderr);
        return 2;
    }
    input->artifacts = calloc(config->count ? config->count : 1, sizeof *input->artifacts);
    if (!input->artifacts) return 1;
    for (i = 0; i < config->count; ++i) {
        const struct holy_entry *e = &config->entries[i];
        if (strcmp(e->section, "install") || strcmp(e->key, "artifact")) continue;
        if (input->count >= 1024) {
            fputs("holyinstall: at most 1024 artifacts are supported\n", stderr);
            return 2;
        }
        if (!digest_valid(e->values[0])) {
            fprintf(stderr, "holyinstall: invalid artifact at %s:%zu\n", e->file, e->line);
            return 2;
        }
        for (j = 0; j < input->count; ++j)
            if (!strcmp(input->artifacts[j], e->values[0])) {
                fprintf(stderr, "holyinstall: duplicate artifact at %s:%zu\n", e->file, e->line);
                return 2;
            }
        input->artifacts[input->count] = strdup(e->values[0]);
        if (!input->artifacts[input->count]) return 1;
        ++input->count;
    }
    if (!input->count) { fputs("holyinstall: [install] needs artifact entries\n", stderr); return 2; }
    input->accepted_arch = calloc(config->count ? config->count : 1,
                                  sizeof *input->accepted_arch);
    input->accepted_privileged = calloc(config->count ? config->count : 1,
                                        sizeof *input->accepted_privileged);
    if (!input->accepted_arch || !input->accepted_privileged) return 1;
    for (i = 0; i < config->count; ++i) {
        const struct holy_entry *e = &config->entries[i];
        char ***accepted;
        size_t *accepted_count;
        if (strcmp(e->section, "install")) continue;
        if (!strcmp(e->key, "accept-arch")) {
            accepted = &input->accepted_arch;
            accepted_count = &input->accept_count;
        } else if (!strcmp(e->key, "accept-privileged")) {
            accepted = &input->accepted_privileged;
            accepted_count = &input->privileged_count;
        } else continue;
        if (!digest_valid(e->values[0])) {
            fprintf(stderr, "holyinstall: invalid %s at %s:%zu\n", e->key, e->file, e->line);
            return 2;
        }
        for (j = 0; j < input->count; ++j)
            if (!strcmp(input->artifacts[j], e->values[0])) break;
        if (j == input->count) {
            fprintf(stderr, "holyinstall: %s needs a selected artifact at %s:%zu\n",
                    e->key, e->file, e->line);
            return 2;
        }
        for (j = 0; j < *accepted_count; ++j)
            if (!strcmp((*accepted)[j], e->values[0])) break;
        if (j != *accepted_count) {
            fprintf(stderr, "holyinstall: duplicate %s at %s:%zu\n", e->key, e->file, e->line);
            return 2;
        }
        (*accepted)[*accepted_count] = strdup(e->values[0]);
        if (!(*accepted)[*accepted_count]) return 1;
        ++*accepted_count;
    }
    input->sources = calloc(config->count ? config->count : 1, sizeof *input->sources);
    if (!input->sources) return 1;
    for (i = 0; i < config->count; ++i) {
        const struct holy_entry *e = &config->entries[i];
        char *binding;
        if (strcmp(e->section, "install") || strcmp(e->key, "source")) continue;
        for (j = 0; j < input->count; ++j)
            if (!strcmp(input->artifacts[j], e->values[0])) break;
        if (j == input->count || !(binding = source_binding(e->values[0], e->values[1]))) {
            fprintf(stderr, "holyinstall: invalid source binding at %s:%zu\n", e->file, e->line);
            return 2;
        }
        for (j = 0; j < input->source_count; ++j)
            if (!strncmp(input->sources[j], binding, 65)) break;
        if (j != input->source_count) {
            free(binding);
            fprintf(stderr, "holyinstall: duplicate source binding at %s:%zu\n", e->file, e->line);
            return 2;
        }
        input->sources[input->source_count++] = binding;
    }
    if (!config_hash(config, input->hash)) return 1;
    return 0;
}

static int run_package_manager(const char *binary, const struct install_input *input,
                               const char *approved, char **output)
{
    char **args;
    size_t i, count = input->count + (input->accept_count + input->privileged_count + input->source_count) * 2 +
                      (approved ? 7 : 6), used = 0, capacity = 0;
    char *buffer = NULL;
    int pipefd[2] = {-1, -1}, status, rc = 1;
    pid_t child;
    args = calloc(count, sizeof *args);
    if (!args) return 1;
    args[0] = (char *)binary;
    args[1] = "db";
    args[2] = approved ? "apply-set" : "plan-set";
    i = 3;
    if (approved) args[i++] = (char *)approved;
    for (size_t j = 0; j < input->count; ++j) args[i++] = (char *)input->artifacts[j];
    for (size_t j = 0; j < input->accept_count; ++j) {
        args[i++] = "--accept-arch";
        args[i++] = input->accepted_arch[j];
    }
    for (size_t j = 0; j < input->privileged_count; ++j) {
        args[i++] = "--accept-privileged";
        args[i++] = input->accepted_privileged[j];
    }
    for (size_t j = 0; j < input->source_count; ++j) {
        args[i++] = "--source";
        args[i++] = input->sources[j];
    }
    args[i++] = "--root";
    args[i++] = input->root;
    if (pipe(pipefd)) goto done;
    child = fork();
    if (child < 0) goto done;
    if (!child) {
        close(pipefd[0]);
        if (dup2(pipefd[1], STDOUT_FILENO) < 0) _exit(1);
        close(pipefd[1]);
        execv(binary, args);
        _exit(127);
    }
    close(pipefd[1]); pipefd[1] = -1;
    for (;;) {
        ssize_t got;
        char *next;
        if (used == capacity) {
            size_t new_capacity = capacity ? capacity * 2 : 4096;
            if (new_capacity > 16 * 1024 * 1024) { rc = 1; break; }
            next = realloc(buffer, new_capacity + 1);
            if (!next) { rc = 1; break; }
            buffer = next; capacity = new_capacity;
        }
        got = read(pipefd[0], buffer + used, capacity - used);
        if (got < 0 && errno == EINTR) continue;
        if (got < 0) { rc = 1; break; }
        if (!got) { rc = 0; break; }
        used += (size_t)got;
    }
    close(pipefd[0]); pipefd[0] = -1;
    while (waitpid(child, &status, 0) < 0) if (errno != EINTR) goto done;
    if (rc || !WIFEXITED(status)) { rc = 1; goto done; }
    rc = WEXITSTATUS(status);
    if (rc) goto done;
    if (!buffer) { rc = 1; goto done; }
    buffer[used] = 0;
    if (memchr(buffer, 0, used)) { rc = 1; goto done; }
    *output = buffer; buffer = NULL;
done:
    if (pipefd[0] >= 0) close(pipefd[0]);
    if (pipefd[1] >= 0) close(pipefd[1]);
    free(buffer); free(args);
    return rc;
}

static int set_hash(const char *output, char hash[65])
{
    const char *marker = " sha256 ", *line = output, *end;
    int found = 0;
    while (*line) {
        end = strchr(line, '\n');
        if (!end) return 0;
        if (!strncmp(line, "plan-set ", 9)) {
            const char *at = strstr(line, marker);
            if (!at || at >= end || end - at != 8 + 64 + 10 ||
                memcmp(at + 8 + 64, " read-only", 10)) return 0;
            memcpy(hash, at + 8, 64); hash[64] = 0;
            if (!digest_valid(hash) || ++found != 1) return 0;
        }
        line = end + 1;
    }
    return found == 1;
}

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

static int sync_parent(const char *path)
{
    const char *slash = strrchr(path, '/');
    char *directory = slash ? strndup(path, (size_t)(slash - path)) : strdup(".");
    int fd, ok;
    if (!directory) return 0;
    if (!*directory) { free(directory); directory = strdup("/"); }
    if (!directory) return 0;
    fd = open(directory, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
    free(directory);
    if (fd < 0) return 0;
    ok = !fsync(fd);
    if (close(fd)) ok = 0;
    return ok;
}

static int write_plan(const char *path, const struct install_input *input,
                      const char *hash)
{
    int fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
    FILE *stream;
    size_t i;
    int ok = 1;
    if (fd < 0) { perror("holyinstall: plan"); return 1; }
    stream = fdopen(fd, "w");
    if (!stream) { close(fd); unlink(path); return 1; }
    if (fprintf(stream, "[install-plan]\nformat holy-install-plan-%d\nroot ",
                input->source_count ? 4 : input->privileged_count ? 3 : input->accept_count ? 2 : 1) < 0 ||
        !quote(stream, input->root) ||
        fprintf(stream, "\ndevice %ju\ninode %ju\nconfig-sha256 %s\nset-sha256 %s\n",
                (uintmax_t)input->root_stat.st_dev, (uintmax_t)input->root_stat.st_ino,
                input->hash, hash) < 0) ok = 0;
    for (i = 0; i < input->count && ok; ++i)
        if (fprintf(stream, "artifact %s\n", input->artifacts[i]) < 0) ok = 0;
    for (i = 0; i < input->accept_count && ok; ++i)
        if (fprintf(stream, "accept-arch %s\n", input->accepted_arch[i]) < 0) ok = 0;
    for (i = 0; i < input->privileged_count && ok; ++i)
        if (fprintf(stream, "accept-privileged %s\n", input->accepted_privileged[i]) < 0) ok = 0;
    for (i = 0; i < input->source_count && ok; ++i)
        if (fprintf(stream, "source %.64s %s\n", input->sources[i], input->sources[i] + 65) < 0) ok = 0;
    if (fflush(stream) || fsync(fd)) ok = 0;
    if (fclose(stream)) ok = 0;
    if (ok) ok = sync_parent(path);
    if (!ok) unlink(path);
    return ok ? 0 : 1;
}

static int check_plan(const char *path, struct install_input *input,
                      char hash[65])
{
    int fd = open(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
    FILE *stream;
    struct stat plan_stat;
    char *line = NULL, *resolved = NULL;
    size_t capacity = 0, row = 0;
    ssize_t length;
    int rc = 2, version = 0, phase = 0;
    if (fd < 0) { perror("holyinstall: plan"); return 2; }
    if (fstat(fd, &plan_stat) || !S_ISREG(plan_stat.st_mode) ||
        plan_stat.st_size < 0 || plan_stat.st_size > 2 * 1024 * 1024) {
        close(fd); fputs("holyinstall: invalid plan file\n", stderr); return 2;
    }
    stream = fdopen(fd, "r");
    if (!stream) { close(fd); return 1; }
    while ((length = getline(&line, &capacity, stream)) >= 0) {
        char **v = NULL, *error = NULL;
        char expected[64];
        size_t count = 0;
        ++row;
        if (length > 1024 * 1024 || memchr(line, 0, (size_t)length) ||
            !holy_lex(line, (size_t)length, &v, &count, path, row, &error)) {
            free(error); holy_tokens_free(v, count); goto done;
        }
        if (row == 1) {
            if (count != 1 || strcmp(v[0], "[install-plan]")) {
                holy_tokens_free(v, count); goto done;
            }
        } else if (count != 2 && !(count == 3 && !strcmp(v[0], "source"))) {
            holy_tokens_free(v, count); goto done;
        } else if (row == 2) {
            if (strcmp(v[0], "format") ||
                (strcmp(v[1], "holy-install-plan-1") &&
                 strcmp(v[1], "holy-install-plan-2") &&
                 strcmp(v[1], "holy-install-plan-3") &&
                 strcmp(v[1], "holy-install-plan-4"))) {
                holy_tokens_free(v, count); goto done;
            }
            version = !strcmp(v[1], "holy-install-plan-4") ? 4 :
                      !strcmp(v[1], "holy-install-plan-3") ? 3 :
                      !strcmp(v[1], "holy-install-plan-2") ? 2 : 1;
        } else if (row == 3) {
            if (strcmp(v[0], "root") || !(input->root = strdup(v[1])) ||
                !(resolved = realpath(input->root, NULL)) ||
                strcmp(resolved, input->root) || !strcmp(input->root, "/") ||
                stat(input->root, &input->root_stat) ||
                !S_ISDIR(input->root_stat.st_mode)) {
                holy_tokens_free(v, count); goto done;
            }
        } else if (row == 4 || row == 5) {
            snprintf(expected, sizeof expected, "%ju", row == 4 ?
                (uintmax_t)input->root_stat.st_dev : (uintmax_t)input->root_stat.st_ino);
            if (strcmp(v[0], row == 4 ? "device" : "inode") || strcmp(v[1], expected)) {
                holy_tokens_free(v, count); goto done;
            }
        } else if (row == 6) {
            if (strcmp(v[0], "config-sha256") || !digest_valid(v[1])) {
                holy_tokens_free(v, count); goto done;
            }
            memcpy(input->hash, v[1], 65);
        } else if (row == 7) {
            if (strcmp(v[0], "set-sha256") || !digest_valid(v[1])) {
                holy_tokens_free(v, count); goto done;
            }
            memcpy(hash, v[1], 65);
        } else if (!strcmp(v[0], "accept-arch") ||
                   !strcmp(v[0], "accept-privileged")) {
            char **next;
            char ***accepted;
            size_t *accepted_count;
            size_t i;
            int privileged = !strcmp(v[0], "accept-privileged");
            if (privileged) {
                if (version < 3 || phase >= 3) { holy_tokens_free(v, count); goto done; }
                phase = 2;
                accepted = &input->accepted_privileged;
                accepted_count = &input->privileged_count;
            } else {
                if (phase >= 2 || version < 2) { holy_tokens_free(v, count); goto done; }
                phase = 1;
                accepted = &input->accepted_arch;
                accepted_count = &input->accept_count;
            }
            if (!digest_valid(v[1]) || *accepted_count >= input->count) {
                holy_tokens_free(v, count); goto done;
            }
            for (i = 0; i < input->count; ++i)
                if (!strcmp(v[1], input->artifacts[i])) break;
            if (i == input->count) { holy_tokens_free(v, count); goto done; }
            for (i = 0; i < *accepted_count; ++i)
                if (!strcmp(v[1], (*accepted)[i])) break;
            if (i != *accepted_count ||
                !(next = realloc(*accepted,
                                 (*accepted_count + 1) * sizeof *next))) {
                holy_tokens_free(v, count); goto done;
            }
            *accepted = next;
            (*accepted)[*accepted_count] = strdup(v[1]);
            if (!(*accepted)[*accepted_count]) {
                holy_tokens_free(v, count); goto done;
            }
            ++*accepted_count;
        } else if (!strcmp(v[0], "source")) {
            char *binding, **next;
            size_t i;
            if (version != 4 || count != 3 ||
                !(binding = source_binding(v[1], v[2]))) {
                holy_tokens_free(v, count); goto done;
            }
            phase = 3;
            for (i = 0; i < input->count; ++i)
                if (!strcmp(input->artifacts[i], v[1])) break;
            if (i == input->count || input->source_count >= input->count) {
                free(binding); holy_tokens_free(v, count); goto done;
            }
            for (i = 0; i < input->source_count; ++i)
                if (!strncmp(input->sources[i], binding, 65)) break;
            if (i != input->source_count ||
                !(next = realloc(input->sources,
                                 (input->source_count + 1) * sizeof *next))) {
                free(binding); holy_tokens_free(v, count); goto done;
            }
            input->sources = next;
            input->sources[input->source_count++] = binding;
        } else {
            char **next;
            size_t i;
            if (phase || strcmp(v[0], "artifact") || !digest_valid(v[1]) ||
                input->count >= 1024) {
                holy_tokens_free(v, count); goto done;
            }
            for (i = 0; i < input->count; ++i)
                if (!strcmp(v[1], input->artifacts[i])) break;
            if (i != input->count || input->count == (size_t)-1 / sizeof *next ||
                !(next = realloc(input->artifacts, (input->count + 1) * sizeof *next))) {
                holy_tokens_free(v, count); goto done;
            }
            input->artifacts = next;
            input->artifacts[input->count] = strdup(v[1]);
            if (!input->artifacts[input->count]) { holy_tokens_free(v, count); goto done; }
            ++input->count;
        }
        holy_tokens_free(v, count);
    }
    if (ferror(stream) || row != 7 + input->count + input->accept_count +
                                input->privileged_count + input->source_count ||
        !input->count || (version == 1 && input->accept_count) ||
        (version == 2 && (!input->accept_count || input->privileged_count)) ||
        (version == 3 && !input->privileged_count) ||
        (version == 4 && !input->source_count)) goto done;
    rc = 0;
done:
    free(line);
    free(resolved);
    fclose(stream);
    if (rc) fputs("holyinstall: plan inputs changed or invalid\n", stderr);
    return rc;
}

struct menu_state {
    char *root;
    char *disk_image;
    char **artifacts;
    size_t count;
    char **accepted_arch;
    size_t accept_count;
    char **accepted_privileged;
    size_t privileged_count;
    char **sources;
    size_t source_count;
};

static void free_input(struct install_input *input)
{
    size_t i;
    for (i = 0; i < input->count; ++i) free(input->artifacts[i]);
    free(input->artifacts);
    for (i = 0; i < input->accept_count; ++i) free(input->accepted_arch[i]);
    free(input->accepted_arch);
    for (i = 0; i < input->privileged_count; ++i) free(input->accepted_privileged[i]);
    free(input->accepted_privileged);
    for (i = 0; i < input->source_count; ++i) free(input->sources[i]);
    free(input->sources);
    free(input->root);
    memset(input, 0, sizeof *input);
}

static void free_menu(struct menu_state *menu)
{
    size_t i;
    for (i = 0; i < menu->count; ++i) free(menu->artifacts[i]);
    free(menu->artifacts);
    for (i = 0; i < menu->accept_count; ++i) free(menu->accepted_arch[i]);
    free(menu->accepted_arch);
    for (i = 0; i < menu->privileged_count; ++i) free(menu->accepted_privileged[i]);
    free(menu->accepted_privileged);
    for (i = 0; i < menu->source_count; ++i) free(menu->sources[i]);
    free(menu->sources);
    free(menu->root);
    free(menu->disk_image);
}

static int menu_load(const char *path, struct menu_state *menu)
{
    struct holy_config config = {0};
    char *error = NULL;
    size_t i;
    if (access(path, F_OK) && errno == ENOENT) return 0;
    if (!holy_config_load(path, &config, &error)) {
        fprintf(stderr, "holyinstall: %s\n", error ? error : "invalid config");
        free(error); return 2;
    }
    for (i = 0; i < config.count; ++i) {
        const struct holy_entry *e = &config.entries[i];
        char **next;
        if (strcmp(e->section, "install") && strcmp(e->section, "disk")) {
            fputs("holyinstall: text menu accepts [install] and [disk] only\n", stderr);
            holy_config_free(&config); return 2;
        }
        if (!strcmp(e->section, "disk")) {
            if (!strcmp(e->key, "image")) {
                menu->disk_image = strdup(e->values[0]);
                if (!menu->disk_image) { holy_config_free(&config); return 1; }
            } else if (strcmp(e->key, "layout") || strcmp(e->values[0], "gpt-ext4")) {
                holy_config_free(&config); return 2;
            }
            continue;
        }
        if (!strcmp(e->key, "root")) {
            menu->root = strdup(e->values[0]);
            if (!menu->root) { holy_config_free(&config); return 1; }
        } else if (!strcmp(e->key, "artifact")) {
            next = realloc(menu->artifacts, (menu->count + 1) * sizeof *next);
            if (!next) { holy_config_free(&config); return 1; }
            menu->artifacts = next;
            menu->artifacts[menu->count] = strdup(e->values[0]);
            if (!menu->artifacts[menu->count]) { holy_config_free(&config); return 1; }
            ++menu->count;
        }
    }
    if (menu->disk_image && !field(&config, "disk", "layout")) {
        fputs("holyinstall: [disk] layout is required\n", stderr);
        holy_config_free(&config); return 2;
    }
    if (!menu->disk_image && field(&config, "disk", "layout")) {
        fputs("holyinstall: [disk] image is required\n", stderr);
        holy_config_free(&config); return 2;
    }
    for (i = 0; i < config.count; ++i) {
        const struct holy_entry *e = &config.entries[i];
        size_t j;
        char **next;
        char ***accepted;
        size_t *accepted_count;
        if (strcmp(e->section, "install")) continue;
        if (!strcmp(e->key, "source")) {
            char *binding = source_binding(e->values[0], e->values[1]);
            for (j = 0; j < menu->count; ++j)
                if (!strcmp(e->values[0], menu->artifacts[j])) break;
            if (!binding || j == menu->count) {
                free(binding); holy_config_free(&config); return 2;
            }
            for (j = 0; j < menu->source_count; ++j)
                if (!strncmp(menu->sources[j], binding, 65)) break;
            if (j != menu->source_count ||
                !(next = realloc(menu->sources,
                                 (menu->source_count + 1) * sizeof *next))) {
                free(binding); holy_config_free(&config); return 2;
            }
            menu->sources = next;
            menu->sources[menu->source_count++] = binding;
            continue;
        }
        if (!strcmp(e->key, "accept-arch")) {
            accepted = &menu->accepted_arch;
            accepted_count = &menu->accept_count;
        } else if (!strcmp(e->key, "accept-privileged")) {
            accepted = &menu->accepted_privileged;
            accepted_count = &menu->privileged_count;
        } else continue;
        for (j = 0; j < menu->count; ++j)
            if (!strcmp(e->values[0], menu->artifacts[j])) break;
        if (!digest_valid(e->values[0]) || j == menu->count) {
            fprintf(stderr, "holyinstall: %s needs a selected artifact\n", e->key);
            holy_config_free(&config); return 2;
        }
        for (j = 0; j < *accepted_count; ++j)
            if (!strcmp(e->values[0], (*accepted)[j])) break;
        if (j != *accepted_count) {
            fprintf(stderr, "holyinstall: duplicate %s\n", e->key);
            holy_config_free(&config); return 2;
        }
        next = realloc(*accepted, (*accepted_count + 1) * sizeof *next);
        if (!next) { holy_config_free(&config); return 1; }
        *accepted = next;
        (*accepted)[*accepted_count] = strdup(e->values[0]);
        if (!(*accepted)[*accepted_count]) {
            holy_config_free(&config); return 1;
        }
        ++*accepted_count;
    }
    holy_config_free(&config);
    return 0;
}

static int menu_write(FILE *stream, const struct menu_state *menu)
{
    size_t i;
    if (fputs("[install]\n", stream) == EOF) return 0;
    if (menu->root && (fputs("root ", stream) == EOF ||
                       !quote(stream, menu->root) || fputc('\n', stream) == EOF)) return 0;
    for (i = 0; i < menu->count; ++i)
        if (fprintf(stream, "artifact %s\n", menu->artifacts[i]) < 0) return 0;
    for (i = 0; i < menu->accept_count; ++i)
        if (fprintf(stream, "accept-arch %s\n", menu->accepted_arch[i]) < 0) return 0;
    for (i = 0; i < menu->privileged_count; ++i)
        if (fprintf(stream, "accept-privileged %s\n", menu->accepted_privileged[i]) < 0) return 0;
    for (i = 0; i < menu->source_count; ++i)
        if (fprintf(stream, "source %.64s %s\n", menu->sources[i], menu->sources[i] + 65) < 0) return 0;
    if (menu->disk_image && (fputs("[disk]\nimage ", stream) == EOF ||
                             !quote(stream, menu->disk_image) ||
                             fputs("\nlayout gpt-ext4\n", stream) == EOF)) return 0;
    return 1;
}

static int menu_config_file(const char *path, const struct menu_state *menu)
{
    size_t size = strlen(path);
    char *temporary;
    FILE *stream;
    int fd, ok;
    if (size > (size_t)-1 - 12) return 0;
    temporary = malloc(size + 12);
    if (!temporary) return 0;
    snprintf(temporary, size + 12, "%s.XXXXXX", path);
    fd = mkstemp(temporary);
    if (fd < 0) { free(temporary); return 0; }
    stream = fdopen(fd, "w");
    if (!stream) { close(fd); unlink(temporary); free(temporary); return 0; }
    ok = menu_write(stream, menu);
    if (fflush(stream) || fsync(fd)) ok = 0;
    if (fclose(stream)) ok = 0;
    if (ok) ok = !rename(temporary, path) && sync_parent(path);
    if (!ok) unlink(temporary);
    free(temporary);
    return ok;
}

static int menu_line(const char *prompt, char **answer)
{
    size_t capacity = 0;
    ssize_t n;
    fputs(prompt, stdout);
    if (fflush(stdout)) return 0;
    n = getline(answer, &capacity, stdin);
    if (n < 0) return 0;
    if (n && (*answer)[n - 1] == '\n') (*answer)[n - 1] = 0;
    return 1;
}

static void menu_path(const char *path)
{
    const unsigned char *p = (const unsigned char *)path;
    for (; *p; ++p)
        if (*p < 32 || *p == 127) printf("\\x%02x", *p);
        else putchar(*p);
}

static int menu_packages(struct menu_state *menu, int *dirty)
{
    for (;;) {
        char *answer = NULL;
        size_t i;
        int found = 0;
        puts("Packages: first entry is the explicit root package");
        for (i = 0; i < menu->count; ++i) {
            size_t j, k, s;
            for (j = 0; j < menu->accept_count; ++j)
                if (!strcmp(menu->artifacts[i], menu->accepted_arch[j])) break;
            for (k = 0; k < menu->privileged_count; ++k)
                if (!strcmp(menu->artifacts[i], menu->accepted_privileged[k])) break;
            for (s = 0; s < menu->source_count; ++s)
                if (!strncmp(menu->artifacts[i], menu->sources[s], 64)) break;
            printf("%zu %s%s%s%s\n", i + 1, menu->artifacts[i],
                   j < menu->accept_count ? " [arch override]" : "",
                   k < menu->privileged_count ? " [setuid approved]" : "",
                   s < menu->source_count ? " [source assigned]" : "");
        }
        if (!menu_line("a add, d delete, x arch override, p setuid approval, s source, b back > ", &answer)) {
            free(answer); return 0;
        }
        if (!strcmp(answer, "b")) { free(answer); return 1; }
        if (!strcmp(answer, "a")) {
            char *digest = NULL, **next;
            free(answer);
            if (!menu_line("SHA-256 > ", &digest)) { free(digest); return 0; }
            if (!digest_valid(digest) || menu->count >= 1024) {
                puts("Invalid SHA-256 or package limit reached"); free(digest); continue;
            }
            for (i = 0; i < menu->count; ++i)
                if (!strcmp(menu->artifacts[i], digest)) { found = 1; break; }
            if (found) { puts("Already selected"); free(digest); continue; }
            next = realloc(menu->artifacts, (menu->count + 1) * sizeof *next);
            if (!next) { free(digest); return 0; }
            menu->artifacts = next;
            menu->artifacts[menu->count++] = digest;
            *dirty = 1;
            continue;
        }
        if (!strcmp(answer, "d") || !strcmp(answer, "x") ||
            !strcmp(answer, "p") || !strcmp(answer, "s")) {
            char *number = NULL, *end;
            unsigned long long index;
            int remove = !strcmp(answer, "d");
            int source_choice = !strcmp(answer, "s");
            char ***accepted = !strcmp(answer, "p") ? &menu->accepted_privileged :
                                &menu->accepted_arch;
            size_t *accepted_count = !strcmp(answer, "p") ? &menu->privileged_count :
                                     &menu->accept_count;
            free(answer);
            if (!menu_line("Number > ", &number)) { free(number); return 0; }
            errno = 0;
            index = strtoull(number, &end, 10);
            if (errno || !number[0] || *end || !index || index > menu->count) {
                puts("Invalid number"); free(number); continue;
            }
            if (source_choice) {
                char *id = NULL, *binding = NULL;
                size_t j;
                free(number);
                if (!menu_line("Source ID SHA-256 (empty clears) > ", &id)) {
                    free(id); return 0;
                }
                if (*id && !(binding = source_binding(menu->artifacts[index - 1], id))) {
                    puts("Invalid source ID"); free(id); continue;
                }
                free(id);
                for (j = 0; j < menu->source_count; ++j)
                    if (!strncmp(menu->sources[j], menu->artifacts[index - 1], 64)) break;
                if (j < menu->source_count) {
                    free(menu->sources[j]);
                    for (++j; j < menu->source_count; ++j)
                        menu->sources[j - 1] = menu->sources[j];
                    --menu->source_count;
                }
                if (binding) {
                    char **next = realloc(menu->sources,
                                          (menu->source_count + 1) * sizeof *next);
                    if (!next) { free(binding); return 0; }
                    menu->sources = next;
                    menu->sources[menu->source_count++] = binding;
                }
                *dirty = 1;
                continue;
            }
            for (i = 0; i < *accepted_count; ++i)
                if (!strcmp((*accepted)[i], menu->artifacts[index - 1])) break;
            if (i < *accepted_count) {
                free((*accepted)[i]);
                for (++i; i < *accepted_count; ++i)
                    (*accepted)[i - 1] = (*accepted)[i];
                --*accepted_count;
            } else if (!remove) {
                char **next = realloc(*accepted, (*accepted_count + 1) * sizeof *next);
                if (!next) { free(number); return 0; }
                *accepted = next;
                (*accepted)[*accepted_count] = strdup(menu->artifacts[index - 1]);
                if (!(*accepted)[*accepted_count]) { free(number); return 0; }
                ++*accepted_count;
            }
            if (remove) {
                for (i = 0; i < menu->source_count; ++i)
                    if (!strncmp(menu->sources[i], menu->artifacts[index - 1], 64)) break;
                if (i < menu->source_count) {
                    free(menu->sources[i]);
                    for (++i; i < menu->source_count; ++i)
                        menu->sources[i - 1] = menu->sources[i];
                    --menu->source_count;
                }
                char **other = menu->accepted_privileged;
                size_t *other_count = &menu->privileged_count;
                if (accepted == &menu->accepted_privileged) {
                    other = menu->accepted_arch;
                    other_count = &menu->accept_count;
                }
                for (i = 0; i < *other_count; ++i)
                    if (!strcmp(other[i], menu->artifacts[index - 1])) break;
                if (i < *other_count) {
                    free(other[i]);
                    for (++i; i < *other_count; ++i) other[i - 1] = other[i];
                    --*other_count;
                }
                free(menu->artifacts[index - 1]);
                for (i = (size_t)index; i < menu->count; ++i)
                    menu->artifacts[i - 1] = menu->artifacts[i];
                --menu->count;
            }
            *dirty = 1;
            free(number);
            continue;
        }
        puts("Choose a, d, x, p, s or b");
        free(answer);
    }
}

static int menu_prepare(const char *plan_path,
                        const char *binary, const struct menu_state *menu,
                        int save_plan)
{
    struct holy_config config = {0};
    struct install_input input = {0};
    char temporary[] = "/tmp/holyinstall-menu-XXXXXX";
    char *output = NULL, hash[65];
    FILE *stream;
    int fd, rc, ok;
    fd = mkstemp(temporary);
    if (fd < 0) return 1;
    stream = fdopen(fd, "w");
    if (!stream) { close(fd); unlink(temporary); return 1; }
    ok = menu_write(stream, menu);
    if (fflush(stream) || fsync(fd)) ok = 0;
    if (fclose(stream)) ok = 0;
    if (!ok) { unlink(temporary); return 1; }
    rc = load_input(temporary, &config, &input);
    unlink(temporary);
    if (rc) goto done;
    rc = run_package_manager(binary, &input, NULL, &output);
    if (rc) goto done;
    if (!set_hash(output, hash)) { rc = 1; goto done; }
    if (fputs(output, stdout) == EOF) { rc = 1; goto done; }
    if (save_plan) {
        rc = write_plan(plan_path, &input, hash);
        if (!rc) printf("holyinstall plan %s set %s\n", plan_path, hash);
    }
done:
    free(output);
    free_input(&input);
    holy_config_free(&config);
    return rc;
}

static int menu_disk_plan(const char *plan_path, const struct menu_state *menu)
{
    char temporary[] = "/tmp/holyinstall-disk-menu-XXXXXX";
    char *args[] = {"plan", "--config", temporary, "--output", (char *)plan_path};
    FILE *stream;
    int fd, ok, rc;
    if (!menu->disk_image) { puts("Select a disk image first"); return 3; }
    fd = mkstemp(temporary);
    if (fd < 0) return 1;
    stream = fdopen(fd, "w");
    if (!stream) { close(fd); unlink(temporary); return 1; }
    ok = menu_write(stream, menu);
    if (fflush(stream) || fsync(fd)) ok = 0;
    if (fclose(stream)) ok = 0;
    if (!ok) { unlink(temporary); return 1; }
    rc = holy_disk_main(5, args);
    unlink(temporary);
    return rc;
}

static int menu_disk_apply(const char *plan_path, const struct menu_state *menu)
{
    char *show[] = {"show", "--plan", (char *)plan_path};
    char *apply[] = {"apply", "--plan", (char *)plan_path, "--confirm", NULL};
    char *answer = NULL, *canonical = NULL;
    int rc;
    if (!menu->disk_image) { puts("Select a disk image first"); return 3; }
    rc = holy_disk_main(3, show);
    if (rc) return rc;
    canonical = realpath(menu->disk_image, NULL);
    if (!canonical) return 6;
    if (!menu_line("Type the exact disk image path to erase > ", &answer)) {
        free(canonical); free(answer); return 3;
    }
    if (strcmp(answer, canonical)) {
        puts("Confirmation does not match the selected image");
        free(canonical); free(answer); return 3;
    }
    apply[4] = answer;
    rc = holy_disk_main(5, apply);
    free(canonical); free(answer);
    return rc;
}

static int menu_run(const char *config_path, const char *plan_path,
                    const char *binary)
{
    struct menu_state menu = {0};
    char *disk_plan_path;
    int rc, prepared = 0, dirty = access(config_path, F_OK) != 0;
    if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) {
        fputs("holyinstall: menu requires a terminal\n", stderr);
        return 3;
    }
    if (strlen(plan_path) > (size_t)-1 - 6) return 2;
    disk_plan_path = malloc(strlen(plan_path) + 6);
    if (!disk_plan_path) return 1;
    sprintf(disk_plan_path, "%s.disk", plan_path);
    rc = menu_load(config_path, &menu);
    if (rc) goto done;
    for (;;) {
        char *answer = NULL;
        fputs("\nHoly installer: prepared root package stage\n1 Target root: ", stdout);
        menu_path(menu.root ? menu.root : "unset");
        printf("\n2 Packages: %zu\nDisk image: ", menu.count);
        menu_path(menu.disk_image ? menu.disk_image : "unset");
        printf("\nConfig: %s\n"
               "3 Preview packages\n4 Save config\n5 Prepare package plan\n"
               "6 Install prepared packages\n7 Abort\n"
               "8 Select disk image\n9 Prepare disk plan\n10 Apply disk plan\n",
               dirty ? "modified" : "saved");
        if (!menu_line("Choice > ", &answer)) { free(answer); rc = 0; break; }
        if (!strcmp(answer, "1")) {
            char *root = NULL;
            free(answer);
            if (!menu_line("Target root > ", &root)) { free(root); rc = 0; break; }
            if (root[0]) { free(menu.root); menu.root = root; dirty = 1; prepared = 0; }
            else free(root);
            continue;
        }
        if (!strcmp(answer, "2")) {
            int changed = 0;
            free(answer);
            if (!menu_packages(&menu, &changed)) { rc = 0; break; }
            if (changed) { dirty = 1; prepared = 0; }
            continue;
        }
        if (!strcmp(answer, "3")) {
            free(answer);
            rc = menu_prepare(plan_path, binary, &menu, 0);
            if (rc) printf("Preview failed (status %d)\n", rc);
            continue;
        }
        if (!strcmp(answer, "4")) {
            free(answer);
            if (!menu_config_file(config_path, &menu)) puts("Save failed");
            else { dirty = 0; puts("Config saved"); }
            continue;
        }
        if (!strcmp(answer, "5")) {
            free(answer);
            rc = menu_prepare(plan_path, binary, &menu, 1);
            prepared = rc == 0;
            if (rc) printf("Plan failed (status %d)\n", rc);
            continue;
        }
        if (!strcmp(answer, "6")) {
            struct install_input input = {0};
            char hash[65], *confirm = NULL, *output = NULL;
            free(answer);
            if (!prepared) { puts("Prepare and review a plan first"); continue; }
            rc = check_plan(plan_path, &input, hash);
            if (rc) { free_input(&input); printf("Plan failed (status %d)\n", rc); prepared = 0; continue; }
            fputs("Root ", stdout);
            menu_path(input.root);
            printf("\nSet %s\nArtifacts %zu\nArchitecture overrides %zu\nSetuid approvals %zu\n",
                   hash, input.count, input.accept_count, input.privileged_count);
            for (size_t j = 0; j < input.accept_count; ++j)
                printf("accept-arch %s\n", input.accepted_arch[j]);
            for (size_t j = 0; j < input.privileged_count; ++j)
                printf("accept-privileged %s\n", input.accepted_privileged[j]);
            if (!menu_line("Type yes to install > ", &confirm)) {
                free(confirm); free_input(&input); rc = 0; break;
            }
            if (!strcmp(confirm, "yes")) {
                rc = run_package_manager(binary, &input, hash, &output);
                if (!rc) { fputs(output, stdout); puts("Package transaction complete"); }
                else printf("Install failed (status %d)\n", rc);
                prepared = 0;
            }
            free(confirm); free(output); free_input(&input);
            continue;
        }
        if (!strcmp(answer, "7")) { free(answer); rc = 0; break; }
        if (!strcmp(answer, "8")) {
            char *image = NULL;
            free(answer);
            if (!menu_line("Disk image (blank clears selection) > ", &image)) {
                free(image); rc = 0; break;
            }
            free(menu.disk_image);
            menu.disk_image = *image ? image : NULL;
            if (!*image) free(image);
            dirty = 1;
            prepared = 0;
            continue;
        }
        if (!strcmp(answer, "9")) {
            free(answer);
            rc = menu_disk_plan(disk_plan_path, &menu);
            if (rc) printf("Disk plan failed (status %d)\n", rc);
            else printf("Disk plan %s ready for review\n", disk_plan_path);
            continue;
        }
        if (!strcmp(answer, "10")) {
            free(answer);
            rc = menu_disk_apply(disk_plan_path, &menu);
            if (rc) printf("Disk apply failed (status %d)\n", rc);
            else puts("Disk image prepared");
            continue;
        }
        puts("Choose 1 through 10, or 7 to abort");
        free(answer);
    }
done:
    free_menu(&menu);
    free(disk_plan_path);
    return rc;
}

int main(int argc, char **argv)
{
    const char *config_path = NULL, *plan_path = NULL;
    const char *binary = "/usr/bin/holypkg";
    struct holy_config config = {0};
    struct install_input input = {0};
    char hash[65], *output = NULL;
    int i, apply = 0, menu = 0, rc;
    if (argc > 1 && !strcmp(argv[1], "disk"))
        return holy_disk_main(argc - 2, argv + 2);
    for (i = 1; i < argc; ++i) {
        if (!strcmp(argv[i], "--config") && ++i < argc && !config_path)
            config_path = argv[i];
        else if (!strcmp(argv[i], "--plan") && ++i < argc && !plan_path && !apply)
            plan_path = argv[i];
        else if (!strcmp(argv[i], "--apply") && ++i < argc && !plan_path) {
            plan_path = argv[i]; apply = 1;
        } else if (!strcmp(argv[i], "--menu") && !menu) {
            menu = 1;
        } else if (!strcmp(argv[i], "--holypkg") && ++i < argc)
            binary = argv[i];
        else goto usage;
    }
    if (!plan_path || (apply ? config_path != NULL || menu : config_path == NULL)) goto usage;
    if (menu) return menu_run(config_path, plan_path, binary);
    if (apply) {
        rc = check_plan(plan_path, &input, hash);
        if (rc) goto done;
        rc = run_package_manager(binary, &input, hash, &output);
        if (!rc && fputs(output, stdout) == EOF) rc = 1;
    } else {
        rc = load_input(config_path, &config, &input);
        if (rc) goto done;
        rc = run_package_manager(binary, &input, NULL, &output);
        if (rc) goto done;
        if (!set_hash(output, hash)) { rc = 1; goto done; }
        if (fputs(output, stdout) == EOF) { rc = 1; goto done; }
        rc = write_plan(plan_path, &input, hash);
        if (!rc) printf("holyinstall plan %s set %s\n", plan_path, hash);
    }
done:
    free(output); free_input(&input); holy_config_free(&config);
    return rc;
usage:
    fputs("usage: holyinstall [--menu] --config FILE --plan NEW_FILE [--holypkg FILE] | --apply PLAN [--holypkg FILE]\n", stderr);
    return 2;
}