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

file src/change.c

#define _POSIX_C_SOURCE 200809L
#include "change.h"
#include "install.h"
#include "package.h"
#include "config.h"

#include <archive.h>
#include <archive_entry.h>
#include <errno.h>
#include <openssl/evp.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

struct entries {
    struct holy_manifest_entry *items;
    size_t count, capacity;
};

static void free_entry(struct holy_manifest_entry *e)
{
    free((char *)e->path); free((char *)e->link);
    free((char *)e->hardlink); free((char *)e->group); free((unsigned char *)e->hash);
}

void holy_file_plan_free(struct holy_file_plan *plan)
{
    size_t i;
    for (i = 0; i < plan->old_count; ++i) free_entry(&plan->old_entries[i]);
    for (i = 0; i < plan->new_count; ++i) free_entry(&plan->new_entries[i]);
    for (i = 0; i < plan->count; ++i) free(plan->changes[i].relocated_path);
    free(plan->old_entries); free(plan->new_entries); free(plan->changes);
    memset(plan, 0, sizeof *plan);
}

static int collect(void *context, const struct holy_manifest_entry *entry)
{
    struct entries *entries = context;
    struct holy_manifest_entry *copy;
    if (entries->count == entries->capacity) {
        size_t capacity = entries->capacity ? entries->capacity * 2 : 32;
        void *next;
        if (capacity < entries->capacity || capacity > SIZE_MAX / sizeof *copy) return 0;
        next = realloc(entries->items, capacity * sizeof *copy);
        if (!next) return 0;
        entries->items = next; entries->capacity = capacity;
    }
    copy = &entries->items[entries->count++];
    memset(copy, 0, sizeof *copy);
    copy->path = strdup(entry->path);
    copy->link = entry->link ? strdup(entry->link) : NULL;
    copy->hardlink = entry->hardlink ? strdup(entry->hardlink) : NULL;
    copy->group = entry->group ? strdup(entry->group) : NULL;
    copy->hash = entry->hash ? malloc(32) : NULL;
    if (copy->hash) memcpy((unsigned char *)copy->hash, entry->hash, 32);
    copy->size = entry->size; copy->mode = entry->mode;
    copy->uid = entry->uid; copy->gid = entry->gid; copy->directory = entry->directory;
    copy->config = entry->config; copy->mutable = entry->mutable;
    return copy->path && (!entry->link || copy->link) && (!entry->hardlink || copy->hardlink) &&
           (!entry->group || copy->group) && (!entry->hash || copy->hash);
}

static int entry_order(const void *left, const void *right)
{
    return strcmp(((const struct holy_manifest_entry *)left)->path,
                  ((const struct holy_manifest_entry *)right)->path);
}

static int same_string(const char *a, const char *b)
{
    return a && b ? !strcmp(a, b) : a == b;
}

static int same_entry(const struct holy_manifest_entry *a, const struct holy_manifest_entry *b)
{
    return a->directory == b->directory && a->config == b->config &&
           a->mutable == b->mutable && a->mode == b->mode && a->uid == b->uid &&
           a->gid == b->gid && a->size == b->size && same_string(a->link, b->link) &&
           same_string(a->hardlink, b->hardlink) && same_string(a->group, b->group) &&
           (a->directory || a->link || (a->hash && b->hash && !memcmp(a->hash, b->hash, 32)));
}

static int change_id(const struct holy_file_plan *plan, const char *path, char output[65])
{
    unsigned char bytes[32];
    unsigned length;
    size_t i;
    int ok;
    EVP_MD_CTX *ctx = EVP_MD_CTX_new();
    if (!ctx) return 0;
    ok = EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) == 1 &&
         EVP_DigestUpdate(ctx, "holy-file-change-1", sizeof "holy-file-change-1") == 1 &&
         EVP_DigestUpdate(ctx, plan->old_artifact, 65) == 1 &&
         EVP_DigestUpdate(ctx, plan->new_artifact, 65) == 1 &&
         EVP_DigestUpdate(ctx, path, strlen(path) + 1) == 1 &&
         EVP_DigestFinal_ex(ctx, bytes, &length) == 1 && length == 32;
    if (ok) for (i = 0; i < 32; ++i) snprintf(output + i * 2, 3, "%02x", bytes[i]);
    EVP_MD_CTX_free(ctx);
    return ok;
}

int holy_file_plan_collect(const char *old_snapshot, const char *new_snapshot,
                           struct holy_file_plan *plan)
{
    struct holy_package_identity old = {0}, next = {0};
    struct entries before = {0}, after = {0};
    size_t i = 0, j = 0, maximum;
    int ok = 0;
    memset(plan, 0, sizeof *plan);
    if (!holy_package_identity(old_snapshot, &old) || !holy_package_identity(new_snapshot, &next) ||
        !holy_verify_visit(old_snapshot, collect, &before) ||
        !holy_verify_visit(new_snapshot, collect, &after)) goto done;
    memcpy(plan->old_artifact, old.digest, 65); memcpy(plan->new_artifact, next.digest, 65);
    if (before.count) qsort(before.items, before.count, sizeof *before.items, entry_order);
    if (after.count) qsort(after.items, after.count, sizeof *after.items, entry_order);
    if (before.count > SIZE_MAX - after.count) goto done;
    maximum = before.count + after.count;
    if (maximum > SIZE_MAX / sizeof *plan->changes) goto done;
    plan->changes = calloc(maximum ? maximum : 1, sizeof *plan->changes);
    if (!plan->changes) goto done;
    while (i < before.count || j < after.count) {
        struct holy_file_change *change = &plan->changes[plan->count++];
        int order = i == before.count ? 1 : j == after.count ? -1 :
                    strcmp(before.items[i].path, after.items[j].path);
        if (order <= 0) change->before = &before.items[i++];
        if (order >= 0) change->after = &after.items[j++];
        change->kind = !change->before ? HOLY_ADD : !change->after ? HOLY_REMOVE :
                       same_entry(change->before, change->after) ? HOLY_RETAIN : HOLY_REPLACE;
        change->source_path = change->after ? change->after->path : change->before->path;
        if (!change_id(plan, change->after ? change->after->path : change->before->path, change->id)) goto done;
    }
    ok = 1;
done:
    plan->old_entries = before.items; plan->old_count = before.count;
    plan->new_entries = after.items; plan->new_count = after.count;
    holy_package_identity_free(&old); holy_package_identity_free(&next);
    return ok;
}

int holy_file_plan_preserve_configs(struct holy_file_plan *plan, int root,
                                    const char *saved_record)
{
    size_t i;
    for (i = 0; i < plan->count; ++i) {
        struct holy_file_change *c = &plan->changes[i];
        struct holy_manifest_entry observed;
        size_t length, j;
        int prior;
        if (!c->before || !c->after || !c->before->config || !c->after->config ||
            c->before->directory || c->before->link || c->before->hardlink ||
            c->before->group || c->after->directory || c->after->link ||
            c->after->hardlink || c->after->group) continue;
        length = strlen(c->source_path);
        if (length > SIZE_MAX - sizeof ".holy-new") return 6;
        c->relocated_path = malloc(length + sizeof ".holy-new");
        if (!c->relocated_path) return 1;
        memcpy(c->relocated_path, c->source_path, length);
        memcpy(c->relocated_path + length, ".holy-new", sizeof ".holy-new");
        c->relocated_before = *c->before;
        c->relocated_before.path = c->relocated_path;
        c->relocated_before.config = c->relocated_before.mutable = 0;
        prior = holy_install_check_entry(root, c->before);
        if (prior == 1) {
            if (holy_install_check_entry(root, &c->relocated_before) != 2) return 4;
            continue;
        }
        if (prior != 0 || !holy_install_observe_regular(root, c->before, &observed,
                                                         c->preserved_hash)) return 4;
        for (j = 0; j < plan->new_count; ++j)
            if (!strcmp(plan->new_entries[j].path, c->relocated_path)) return 4;
        c->preserved = observed;
        c->preserved.hash = c->preserved_hash;
        c->relocated_after = *c->after;
        c->relocated_after.path = c->relocated_path;
        c->relocated_after.config = c->relocated_after.mutable = 0;
        prior = holy_install_check_entry(root, &c->relocated_before);
        if (saved_record) {
            char marker[96];
            const char *section = strstr(saved_record, "\n[files]\n"), *position;
            int n = snprintf(marker, sizeof marker, "\nchange %s ", c->id);
            if (!section || n < 0 || (size_t)n >= sizeof marker ||
                !(position = strstr(section, marker))) return 4;
            position += strlen(marker);
            if (!strncmp(position, "add\nbefore absent\n", sizeof "add\nbefore absent\n" - 1)) prior = 2;
            else if (!strncmp(position, "replace\nbefore file ", sizeof "replace\nbefore file " - 1)) prior = 1;
            else return 4;
        } else if (prior != 1 && prior != 2) return 4;
        c->before = prior == 1 ? &c->relocated_before : NULL;
        c->after = &c->relocated_after;
        c->kind = prior == 1 ? HOLY_REPLACE : HOLY_ADD;
        c->keep_config = 1;
    }
    return 0;
}

static size_t find_change(const struct holy_file_plan *plan, const char *path)
{
    size_t low = 0, high = plan->count;
    while (low < high) {
        size_t middle = low + (high - low) / 2;
        const struct holy_file_change *c = &plan->changes[middle];
        const char *name = c->source_path;
        int order = strcmp(name, path);
        if (!order) return middle;
        if (order < 0) low = middle + 1;
        else high = middle;
    }
    return plan->count;
}

static void group_name(const struct holy_file_change *anchor, char name[84])
{
    snprintf(name, 84, ".holy-update-%s-group", anchor->id);
}

static const struct holy_file_change *group_anchor(const struct holy_file_plan *plan,
                                                   const struct holy_file_change *change)
{
    size_t index;
    if (!change->after || !change->after->group) return NULL;
    if (!change->after->hardlink) return change;
    index = find_change(plan, change->after->hardlink);
    return index < plan->count ? &plan->changes[index] : NULL;
}

static int group_state(const struct holy_file_plan *plan, int root,
                         const struct holy_file_change *change, struct stat *observed)
{
    const struct holy_file_change *anchor = group_anchor(plan, change);
    char name[84];
    if (!anchor) return 0;
    group_name(anchor, name);
    return holy_install_entry_state(root, anchor->after, name, observed);
}

struct inode_fact { const char *group, *path; struct stat state; };

static int inode_order(const void *left, const void *right)
{
    const struct inode_fact *a = left, *b = right;
    if (a->state.st_dev != b->state.st_dev) return a->state.st_dev < b->state.st_dev ? -1 : 1;
    if (a->state.st_ino != b->state.st_ino) return a->state.st_ino < b->state.st_ino ? -1 : 1;
    return strcmp(a->path, b->path);
}

static int old_group_order(const void *left, const void *right)
{
    return strcmp(((const struct inode_fact *)left)->group,
                  ((const struct inode_fact *)right)->group);
}

static int same_inode(const struct stat *a, const struct stat *b)
{
    return a->st_dev == b->st_dev && a->st_ino == b->st_ino;
}

static int partial_groups(const struct holy_file_plan *plan, int root)
{
    struct inode_fact *old;
    size_t i, used = 0;
    int ok = 0;
    if (plan->count > SIZE_MAX / sizeof *old) return 0;
    old = calloc(plan->count ? plan->count : 1, sizeof *old);
    if (!old) return 0;
    for (i = 0; i < plan->count; ++i) {
        const struct holy_file_change *c = &plan->changes[i];
        struct stat current, next;
        if (!c->before || !c->before->group ||
            holy_install_entry_state(root, c->before, NULL, &current) != 1) continue;
        if (c->after && holy_install_entry_state(root, c->after, NULL, &next) == 1 &&
            (!c->after->group || (group_state(plan, root, c, &next) == 1 && same_inode(&current, &next)))) continue;
        old[used].group = c->before->group; old[used].path = c->before->path;
        old[used++].state = current;
    }
    qsort(old, used, sizeof *old, old_group_order);
    for (i = 1; i < used; ++i)
        if (!strcmp(old[i-1].group, old[i].group) && !same_inode(&old[i-1].state, &old[i].state)) goto done;
    ok = 1;
done:
    free(old);
    return ok;
}

static int topology(const struct holy_file_plan *plan, int after, int root)
{
    const struct holy_manifest_entry *entries = after ? plan->new_entries : plan->old_entries;
    size_t count = after ? plan->new_count : plan->old_count;
    struct inode_fact *facts;
    size_t i, used = 0;
    int ok = 0;
    if (count > SIZE_MAX - plan->count || count + plan->count > SIZE_MAX / sizeof *facts) return 0;
    facts = calloc(count + plan->count ? count + plan->count : 1, sizeof *facts);
    if (!facts) return 0;
    for (i = 0; i < count; ++i) {
        const struct holy_manifest_entry *e = &entries[i];
        size_t index = find_change(plan, e->path);
        if (index < plan->count && plan->changes[index].keep_config)
            e = &plan->changes[index].preserved;
        if (e->directory || e->link) continue;
        if (holy_install_entry_state(root, e, NULL, &facts[used].state) != 1) goto done;
        facts[used].group = e->group; facts[used++].path = e->path;
    }
    for (i = 0; i < plan->count; ++i) if (plan->changes[i].keep_config) {
        const struct holy_manifest_entry *e = after ? plan->changes[i].after : plan->changes[i].before;
        if (!e) continue;
        if (holy_install_entry_state(root, e, NULL, &facts[used].state) != 1) goto done;
        facts[used].group = e->group; facts[used++].path = e->path;
    }
    qsort(facts, used, sizeof *facts, inode_order);
    for (i = 1; i < used; ++i) if (same_inode(&facts[i-1].state, &facts[i].state) &&
        (!facts[i-1].group || !facts[i].group || strcmp(facts[i-1].group, facts[i].group))) goto done;
    ok = 1;
done:
    free(facts);
    return ok;
}

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 entry_record(FILE *out, const char *side, const struct holy_manifest_entry *e)
{
    size_t i;
    fprintf(out, "%s ", side);
    if (!e) { fputs("absent\n", out); return; }
    fprintf(out, "%s ", e->directory ? "dir" : e->link ? "symlink" : e->hardlink ? "hardlink" : "file");
    quote(out, e->path);
    fprintf(out, " %o %lld %lld %lld ", e->mode, e->uid, e->gid, e->size);
    if (e->directory || e->link) fputc('-', out);
    else for (i = 0; i < 32; ++i) fprintf(out, "%02x", e->hash[i]);
    fputc(' ', out); quote(out, e->link ? e->link : e->hardlink ? e->hardlink : "-");
    fputc(' ', out); quote(out, e->group ? e->group : "-");
    fprintf(out, " %s\n", e->config ? e->mutable ? "config,mutable" : "config" :
            e->mutable ? "mutable" : "none");
}

static int manifest_entry(FILE *out, const struct holy_manifest_entry *e,
                          const char *owner, const char *group)
{
    size_t i;
    fputs("file ", out); quote(out, e->path);
    fprintf(out, " %o ", e->mode); quote(out, owner);
    fputc(' ', out); quote(out, group);
    fprintf(out, " %lld %lld %lld ", e->uid, e->gid, e->size);
    for (i = 0; i < 32; ++i) fprintf(out, "%02x", e->hash[i]);
    fprintf(out, " %s - -\n", e->config ? e->mutable ? "config,mutable" : "config" :
            e->mutable ? "mutable" : "none");
    return !ferror(out);
}

int holy_file_plan_installed_manifest(const struct holy_file_plan *plan,
                                     const char *source, size_t length,
                                     char **record, size_t *size)
{
    FILE *out;
    size_t start = 0, line = 0, mapped = 0;
    int ok = 0;
    *record = NULL; *size = 0;
    out = open_memstream(record, size);
    if (!out) return 0;
    while (start < length) {
        const char *end = memchr(source + start, '\n', length - start);
        size_t bytes = end ? (size_t)(end - source - start) : length - start;
        char **v = NULL, *error = NULL;
        size_t count = 0, i;
        ++line;
        if (!holy_lex(source + start, bytes, &v, &count, "HOLY/files", line, &error)) {
            free(error); goto done;
        }
        free(error);
        for (i = 0; count >= 2 && i < plan->count; ++i)
            if (plan->changes[i].keep_config &&
                !strcmp(v[1], plan->changes[i].source_path)) break;
        if (count >= 2 && i < plan->count && plan->changes[i].keep_config) {
            const struct holy_file_change *c = &plan->changes[i];
            if (count != 12 || strcmp(v[0], "file") ||
                !manifest_entry(out, &c->preserved, v[3], v[4]) ||
                !manifest_entry(out, c->after, v[3], v[4])) {
                holy_tokens_free(v, count); goto done;
            }
            ++mapped;
        } else if (fwrite(source + start, 1, bytes + !!end, out) != bytes + !!end) {
            holy_tokens_free(v, count); goto done;
        }
        holy_tokens_free(v, count);
        start += bytes + !!end;
    }
    ok = mapped != 0 && !ferror(out);
    for (start = 0; ok && start < plan->count; ++start)
        if (plan->changes[start].keep_config) --mapped;
    ok = ok && mapped == 0;
done:
    if (fclose(out)) ok = 0;
    if (!ok) { free(*record); *record = NULL; *size = 0; }
    return ok;
}

int holy_file_plan_record(const struct holy_file_plan *plan, char **record, size_t *size)
{
    static const char *const names[] = {"retain", "add", "replace", "remove"};
    FILE *out;
    size_t i;
    int ok;
    *record = NULL; *size = 0;
    out = open_memstream(record, size);
    if (!out) return 0;
    fprintf(out, "format holy-file-plan-1\nold %s\nnew %s\n", plan->old_artifact, plan->new_artifact);
    for (i = 0; i < plan->count; ++i) {
        const struct holy_file_change *change = &plan->changes[i];
        fprintf(out, "change %s %s\n", change->id, names[change->kind]);
        entry_record(out, "before", change->before); entry_record(out, "after", change->after);
        if (change->keep_config) entry_record(out, "preserved", &change->preserved);
    }
    for (i = 0; i < plan->count; ++i) {
        const struct holy_file_change *c = &plan->changes[i];
        char name[84];
        if (!c->after || !c->after->group || c->after->hardlink) continue;
        group_name(c, name);
        fputs("group-stage ", out); quote(out, c->after->path);
        fputc(' ', out); quote(out, name); fputc('\n', out);
    }
    ok = !ferror(out);
    if (fclose(out)) ok = 0;
    if (!ok) { free(*record); *record = NULL; *size = 0; }
    return ok;
}

static int supported(const struct holy_manifest_entry *entry, int privileged)
{
    return !entry || ((!(entry->mode & 07000) ||
                      (privileged && (entry->mode & 07000) == 04000 &&
                       (entry->mode & 0111) && !entry->directory &&
                       !entry->link && !entry->hardlink && !entry->group)) &&
                     entry->uid == (long long)geteuid() && entry->gid == (long long)getegid() &&
                     (!entry->link || entry->link[0] != '/'));
}

int holy_file_plan_check(const struct holy_file_plan *plan, int root, int recovering, size_t *failed)
{
    size_t i;
    for (i = 0; i < plan->count; ++i) {
        const struct holy_file_change *c = &plan->changes[i];
        if (failed) *failed = i;
        if (!supported(c->before, plan->before_privileged) ||
            !supported(c->after, plan->after_privileged) ||
            (c->keep_config && !supported(&c->preserved, plan->before_privileged)) ||
            (c->kind == HOLY_REPLACE && ((c->before && c->before->directory) ||
                                        (c->after && c->after->directory)))) return 6;
    }
    if (failed) *failed = 0;
    if (!holy_install_directory_plan(root, plan->new_entries, plan->new_count, 0, recovering)) return 4;
    for (i = 0; i < plan->count; ++i) {
        const struct holy_file_change *c = &plan->changes[i];
        int result = 0;
        if (failed) *failed = i;
        if (c->keep_config && holy_install_check_entry(root, &c->preserved) != 1) return 4;
        if ((c->before && c->before->directory) || (c->after && c->after->directory)) {
            const struct holy_manifest_entry *e = c->before ? c->before : c->after;
            result = holy_install_check_entry(root, e);
            if (result != 1 && !(c->kind == HOLY_ADD && result == 2)) return 4;
        } else if (!holy_install_transition_check(root, c->before, c->after, recovering) ||
                   (!recovering && c->before && holy_install_check_entry(root, c->before) != 1)) return 4;
    }
    if ((!recovering && !topology(plan, 0, root)) ||
        (recovering && holy_file_plan_finished(plan, root) && !partial_groups(plan, root))) return 4;
    if (failed) *failed = plan->count;
    return 0;
}

static void temporary_name(const struct holy_file_change *change, char name[78])
{
    snprintf(name, 78, ".holy-update-%s", change->id);
}

int holy_file_plan_reservations(const struct holy_file_plan *plan, int root)
{
    size_t i;
    for (i = 0; i < plan->count; ++i) {
        const struct holy_file_change *c = &plan->changes[i];
        char name[78];
        if (c->after && c->after->group && !c->after->hardlink) {
            char group[84];
            group_name(c, group);
            if (holy_install_temporary_state(root, c->after, group) != 2) return 4;
        }
        if (!c->after || c->after->directory || c->kind == HOLY_RETAIN) continue;
        temporary_name(c, name);
        if (holy_install_temporary_state(root, c->after, name) != 2) return 4;
    }
    return 0;
}


static int group_witnesses(const struct holy_file_plan *plan, int root, int completed)
{
    size_t i;
    for (i = 0; i < plan->count; ++i) {
        const struct holy_file_change *c = &plan->changes[i];
        struct stat witness, published, old;
        int state;
        if (!c->after || !c->after->group) continue;
        state = group_state(plan, root, c, &witness);
        if (state == 2 && completed) continue;
        if (state != 1) return 4;
        state = holy_install_entry_state(root, c->after, NULL, &published);
        if (state == 1 && (completed || c->kind == HOLY_RETAIN || !c->before ||
            holy_install_entry_state(root, c->before, NULL, &old) != 1) &&
            !same_inode(&witness, &published)) return 4;
        if (completed && state != 1) return 4;
    }
    return 0;
}

static int discard_staging(const struct holy_file_plan *plan, int root)
{
    size_t i;
    for (i = 0; i < plan->count; ++i) {
        const struct holy_file_change *c = &plan->changes[i];
        char name[78];
        struct stat staged, published;
        int state;
        if (!c->after || c->after->directory || c->kind == HOLY_RETAIN) continue;
        temporary_name(c, name);
        state = holy_install_entry_state(root, c->after, name, &staged);
        if (state == 2) continue;
        if (state != 1 || (c->after->group &&
            (holy_install_entry_state(root, c->after, NULL, &published) != 1 ||
             !same_inode(&staged, &published))) ||
            !holy_install_remove_temporary(root, c->after, name)) return 4;
    }
    return 0;
}

int holy_file_plan_stage(const struct holy_file_plan *plan, const char *snapshot,
                         int root, int recovering, size_t *failed)
{
    struct archive *archive = NULL;
    struct archive_entry *entry;
    struct holy_package_identity identity = {0};
    FILE *content = NULL;
    unsigned char *seen = NULL;
    int result = 1, status;
    size_t i;
    result = holy_file_plan_check(plan, root, recovering, failed);
    if (result) return result;
    if (recovering && !holy_file_plan_finished(plan, root)) return group_witnesses(plan, root, 1);
    if (!recovering && (result = holy_file_plan_reservations(plan, root))) return result;
    result = 6;
    if (!holy_package_identity(snapshot, &identity) || strcmp(identity.digest, plan->new_artifact)) goto done;
    result = 1;
    if (!holy_install_directory_plan(root, plan->new_entries, plan->new_count, 1, recovering)) goto done;
    for (i = 0; i < plan->count; ++i) {
        const struct holy_file_change *c = &plan->changes[i];
        char name[84];
        if (!c->after || !c->after->group || c->after->hardlink || c->kind != HOLY_RETAIN) continue;
        group_name(c, name);
        if (!holy_install_prepare_link(root, c->after, name, c->after, NULL, recovering)) goto done;
    }
    seen = calloc(plan->count ? plan->count : 1, 1);
    archive = archive_read_new();
    if (!seen || !archive || archive_read_support_filter_lz4(archive) != ARCHIVE_OK ||
        archive_read_support_format_tar(archive) != ARCHIVE_OK ||
        archive_read_open_filename(archive, snapshot, 65536) != ARCHIVE_OK) goto done;
    while ((status = archive_read_next_header(archive, &entry)) == ARCHIVE_OK) {
        const char *path = archive_entry_pathname(entry);
        const struct holy_file_change *c;
        char name[84];
        int prepared;
        if (!path || strncmp(path, "DATA/", 5)) {
            if (archive_read_data_skip(archive) != ARCHIVE_OK) goto done;
            continue;
        }
        i = find_change(plan, path + 5);
        if (i == plan->count || !(c = &plan->changes[i])->after ||
            c->after->directory || c->kind == HOLY_RETAIN) {
            if (archive_read_data_skip(archive) != ARCHIVE_OK) goto done;
            continue;
        }
        if (failed) *failed = i;
        if (seen[i]++) goto done;
        if (c->after->hardlink) {
            if (archive_read_data_skip(archive) != ARCHIVE_OK) goto done;
            continue;
        }
        if (c->after->group) group_name(c, name);
        else temporary_name(c, name);
        prepared = holy_install_temporary_state(root, c->after, name);
        if (prepared == 0 || (prepared == 1 && !recovering)) {
            fputs("holypkg: reserved update object requires inspection: ", stderr);
            quote(stderr, c->after->path);
            fprintf(stderr, " (%s)\n", name);
            result = 4; goto done;
        }
        if (prepared == 1 || (recovering && !c->after->group &&
            (!c->before || (!c->before->group && !c->before->hardlink)) &&
            holy_install_check_entry(root, c->after) == 1)) {
            if (archive_read_data_skip(archive) != ARCHIVE_OK) goto done;
            continue;
        }
        if (!c->after->link) {
            char buffer[65536];
            la_ssize_t got;
            long long total = 0;
            content = tmpfile();
            if (!content) goto done;
            while ((got = archive_read_data(archive, buffer, sizeof buffer)) > 0) {
                if (got > c->after->size - total || fwrite(buffer, 1, (size_t)got, content) != (size_t)got) goto done;
                total += got;
            }
            if (got < 0 || total != c->after->size || fflush(content) || fsync(fileno(content))) goto done;
        }
        if (!holy_install_prepare_file(root, c->after, content ? fileno(content) : -1, name)) goto done;
        if (content) { fclose(content); content = NULL; }
    }
    if (status != ARCHIVE_EOF) goto done;
    result = group_witnesses(plan, root, 0);
    if (result) goto done;
    result = 1;
    for (i = 0; i < plan->count; ++i) {
        const struct holy_file_change *c = &plan->changes[i], *anchor;
        char name[78], group[84];
        if (!c->after || !c->after->group || c->kind == HOLY_RETAIN) continue;
        if (failed) *failed = i;
        anchor = group_anchor(plan, c);
        if (!anchor) goto done;
        temporary_name(c, name); group_name(anchor, group);
        if (!holy_install_prepare_link(root, c->after, name, anchor->after, group, recovering)) goto done;
    }
    for (i = 0; i < plan->count; ++i) {
        const struct holy_file_change *c = &plan->changes[i];
        if (c->after && !c->after->directory && c->kind != HOLY_RETAIN && !seen[i]) goto done;
    }
    if (failed) *failed = plan->count;
    result = 0;
done:
    if (content) fclose(content);
    if (archive) archive_read_free(archive);
    holy_package_identity_free(&identity);
    free(seen);
    return result;
}

int holy_file_plan_finished(const struct holy_file_plan *plan, int root)
{
    size_t i;
    for (i = 0; i < plan->count; ++i) {
        const struct holy_file_change *c = &plan->changes[i];
        const struct holy_manifest_entry *e = c->after ? c->after : c->before;
        int expected = c->after || e->directory ? 1 : 2;
        if (c->keep_config && holy_install_check_entry(root, &c->preserved) != 1) return 4;
        if (holy_install_check_entry(root, e) != expected) return 4;
    }
    return topology(plan, 1, root) ? 0 : 4;
}

int holy_file_plan_apply(const struct holy_file_plan *plan, int root,
                         holy_change_progress progress, void *context, size_t *failed)
{
    size_t i;
    int result = holy_file_plan_check(plan, root, 1, failed);
    if (result) return result;
    if (!holy_file_plan_finished(plan, root)) {
        result = group_witnesses(plan, root, 1);
        return result ? result : discard_staging(plan, root);
    }
    if ((result = group_witnesses(plan, root, 0))) return result;
    for (i = 0; i < plan->count; ++i) {
        const struct holy_file_change *c = &plan->changes[i];
        char name[78];
        int staged;
        if (!c->after || c->after->directory || c->kind == HOLY_RETAIN) continue;
        temporary_name(c, name);
        staged = holy_install_temporary_state(root, c->after, name);
        if (c->after->group && staged == 1) {
            struct stat witness, current;
            if (group_state(plan, root, c, &witness) != 1 ||
                holy_install_entry_state(root, c->after, name, &current) != 1 ||
                !same_inode(&witness, &current)) staged = 0;
        }
        if (staged == 0 || (staged == 2 &&
            holy_install_entry_state(root, c->after, NULL, NULL) != 1)) {
            if (failed) *failed = i;
            return 4;
        }
    }
    for (i = 0; i < plan->count; ++i) {
        const struct holy_file_change *c = &plan->changes[i];
        char name[78];
        if (failed) *failed = i;
        if (c->kind == HOLY_RETAIN || (c->after && c->after->directory) ||
            (c->before && c->before->directory)) continue;
        temporary_name(c, name);
        if (progress && !progress(context, i, 0)) return 1;
        if (!holy_install_transition(root, c->before, c->after, name)) return 4;
        if (progress && !progress(context, i, 1)) return 1;
    }
    if (failed) *failed = plan->count;
    return holy_file_plan_finished(plan, root);
}

int holy_file_plan_cleanup(const struct holy_file_plan *plan, int root)
{
    size_t i;
    int result = holy_file_plan_finished(plan, root);
    if (result || (result = group_witnesses(plan, root, 1)) ||
        (result = discard_staging(plan, root))) return result;
    for (i = 0; i < plan->count; ++i) {
        const struct holy_file_change *c = &plan->changes[i];
        char name[84];
        if (!c->after || !c->after->group || c->after->hardlink) continue;
        group_name(c, name);
        if (!holy_install_remove_temporary(root, c->after, name)) return 4;
    }
    return 0;
}