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

file src/config.c

#define _POSIX_C_SOURCE 200809L
#include "config.h"

#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

struct frame {
    dev_t dev;
    ino_t ino;
    struct frame *parent;
};

static char *copy(const char *s)
{
    size_t n = strlen(s);
    char *p = malloc(n + 1);
    if (p) memcpy(p, s, n + 1);
    return p;
}

static int fail(char **error, const char *file, size_t line, const char *fmt, ...)
{
    char msg[512];
    int n;
    va_list ap;
    va_start(ap, fmt);
    vsnprintf(msg, sizeof msg, fmt, ap);
    va_end(ap);
    n = snprintf(NULL, 0, "%s:%zu: %s", file, line, msg);
    if (n >= 0) {
        *error = malloc((size_t)n + 1);
        if (*error) snprintf(*error, (size_t)n + 1, "%s:%zu: %s", file, line, msg);
    }
    return 0;
}

void holy_tokens_free(char **v, size_t n)
{
    size_t i;
    for (i = 0; i < n; ++i) free(v[i]);
    free(v);
}

static int append(char **buf, size_t *len, size_t *cap, char c)
{
    char *next;
    size_t size;
    if (*len + 1 >= *cap) {
        if (*cap > (size_t)-1 / 2) return 0;
        size = *cap ? *cap * 2 : 32;
        next = realloc(*buf, size);
        if (!next) return 0;
        *buf = next;
        *cap = size;
    }
    (*buf)[(*len)++] = c;
    return 1;
}

static int hex(char c)
{
    if (c >= '0' && c <= '9') return c - '0';
    if (c >= 'a' && c <= 'f') return c - 'a' + 10;
    if (c >= 'A' && c <= 'F') return c - 'A' + 10;
    return -1;
}

static int utf8(const unsigned char *s, size_t n)
{
    size_t i = 0, j, count;
    unsigned int code, minimum;
    while (i < n) {
        unsigned char c = s[i++];
        if (c < 0x80) continue;
        if (c >= 0xc2 && c <= 0xdf) { count = 1; code = c & 31; minimum = 0x80; }
        else if (c >= 0xe0 && c <= 0xef) { count = 2; code = c & 15; minimum = 0x800; }
        else if (c >= 0xf0 && c <= 0xf4) { count = 3; code = c & 7; minimum = 0x10000; }
        else return 0;
        if (count > n - i) return 0;
        for (j = 0; j < count; ++j) {
            c = s[i++];
            if ((c & 0xc0) != 0x80) return 0;
            code = (code << 6) | (c & 63);
        }
        if (code < minimum || code > 0x10ffff ||
            (code >= 0xd800 && code <= 0xdfff)) return 0;
    }
    return 1;
}

int holy_lex(const char *s, size_t length, char ***out, size_t *count,
             const char *file, size_t line, char **error)
{
    size_t i = 0, used = 0, cap = 0;
    char **v = NULL;
    if (!utf8((const unsigned char *)s, length))
        return fail(error, file, line, "invalid UTF-8");
    while (i < length) {
        char *word = NULL;
        size_t len = 0, size = 0;
        int quoted = 0, active = 0;
        while (i < length && (s[i] == ' ' || s[i] == '\t' || s[i] == '\r')) ++i;
        if (i == length || s[i] == '\n' || s[i] == '#') break;
        while (i < length && s[i] != '\n') {
            char c = s[i++];
            if (c == '"') { quoted = !quoted; active = 1; continue; }
            if (!quoted && (c == ' ' || c == '\t' || c == '\r')) break;
            if (c == '\\') {
                if (i >= length || s[i] == '\n') goto bad_escape;
                c = s[i++];
                if (c == 'n') c = '\n';
                else if (c == 't') c = '\t';
                else if (c == 'r') c = '\r';
                else if (c == 'x') {
                    int hi, lo;
                    if (i + 1 >= length || (hi = hex(s[i])) < 0 ||
                        (lo = hex(s[i + 1])) < 0) goto bad_escape;
                    c = (char)(hi * 16 + lo);
                    i += 2;
                    if (!c) goto bad_escape;
                } else if (c != '"' && c != '\\') goto bad_escape;
            }
            active = 1;
            if (!append(&word, &len, &size, c)) goto oom;
        }
        if (quoted) {
            free(word);
            holy_tokens_free(v, used);
            return fail(error, file, line, "unterminated quote");
        }
        if (!active) { free(word); continue; }
        if (!append(&word, &len, &size, '\0')) goto oom;
        if (used == cap) {
            size_t next = cap ? cap * 2 : 4;
            if (next < cap || next > (size_t)-1 / sizeof(*v)) goto oom;
            {
                char **newv = realloc(v, next * sizeof(*v));
                if (!newv) goto oom;
                v = newv;
            }
            cap = next;
        }
        v[used++] = word;
        continue;
bad_escape:
        free(word);
        holy_tokens_free(v, used);
        return fail(error, file, line, "invalid escape or NUL");
oom:
        free(word);
        holy_tokens_free(v, used);
        return fail(error, file, line, "out of memory");
    }
    *out = v;
    *count = used;
    return 1;
}

static char *relative(const char *base, const char *name)
{
    const char *slash;
    size_t prefix, n;
    char *path;
    if (name[0] == '/') return copy(name);
    slash = strrchr(base, '/');
    prefix = slash ? (size_t)(slash - base + 1) : 0;
    n = strlen(name);
    if (prefix > (size_t)-1 - n - 1) return NULL;
    path = malloc(prefix + n + 1);
    if (!path) return NULL;
    memcpy(path, base, prefix);
    memcpy(path + prefix, name, n + 1);
    return path;
}

static int valid_section(const char *s)
{
    return !strcmp(s, "general") || !strcmp(s, "resolver") ||
           !strcmp(s, "install") || !strcmp(s, "install-plan") ||
           !strcmp(s, "disk") || !strcmp(s, "disk-plan") ||
           !strcmp(s, "disk-finalize-plan") ||
           (!strncmp(s, "source ", 7) && s[7] && strcmp(s + 7, "local")) ||
           (!strncmp(s, "rule ", 5) && s[5]);
}

static int list_key(const char *section, const char *key)
{
    return (!strncmp(section, "source ", 7) && !strcmp(key, "repo")) ||
           (!strcmp(section, "install") && (!strcmp(key, "artifact") ||
                                            !strcmp(key, "source") ||
                                            !strcmp(key, "accept-arch") ||
                                            !strcmp(key, "accept-privileged"))) ||
           (!strcmp(section, "install-plan") && (!strcmp(key, "artifact") ||
                                                 !strcmp(key, "source") ||
                                                 !strcmp(key, "accept-arch") ||
                                                 !strcmp(key, "accept-privileged"))) ||
           (!strcmp(section, "resolver") && !strcmp(key, "prefer"));
}

static int key_arity(const char *section, const char *key)
{
    if (!strcmp(section, "disk-finalize-plan")) {
        static const char *const names[] = {"disk", "esp", "root", "limine"};
        static const char *const suffix[] = {"", "-device", "-inode", "-size", "-sha256"};
        size_t i, j;
        char expected[32];
        if (!strcmp(key, "format") || !strcmp(key, "root-sectors")) return 1;
        for (i = 0; i < sizeof names / sizeof names[0]; ++i)
            for (j = 0; j < sizeof suffix / sizeof suffix[0]; ++j) {
                snprintf(expected, sizeof expected, "%s%s", names[i], suffix[j]);
                if (!strcmp(key, expected)) return 1;
            }
    } else if (!strcmp(section, "disk-plan")) {
        if (!strcmp(key, "format") || !strcmp(key, "image") ||
            !strcmp(key, "device") || !strcmp(key, "inode") ||
            !strcmp(key, "size") || !strcmp(key, "head-sha256") ||
            !strcmp(key, "tail-sha256") || !strcmp(key, "root-sectors") ||
            !strcmp(key, "kind") || !strcmp(key, "serial") ||
            !strcmp(key, "rdev") || !strcmp(key, "sfdisk-sha256") ||
            !strcmp(key, "mkfs-fat-sha256") || !strcmp(key, "mke2fs-sha256") ||
            !strcmp(key, "limine-sha256")) return 1;
    } else if (!strcmp(section, "disk")) {
        if (!strcmp(key, "image") || !strcmp(key, "device") ||
            !strcmp(key, "layout")) return 1;
    } else if (!strcmp(section, "install")) {
        if (!strcmp(key, "source")) return 2;
        if (!strcmp(key, "root") || !strcmp(key, "artifact") ||
            !strcmp(key, "accept-arch") || !strcmp(key, "accept-privileged")) return 1;
    } else if (!strcmp(section, "install-plan")) {
        if (!strcmp(key, "source")) return 2;
        if (!strcmp(key, "format") || !strcmp(key, "root") ||
            !strcmp(key, "device") || !strcmp(key, "inode") ||
            !strcmp(key, "config-sha256") || !strcmp(key, "set-sha256") ||
            !strcmp(key, "artifact") || !strcmp(key, "accept-arch") ||
            !strcmp(key, "accept-privileged")) return 1;
    } else if (!strcmp(section, "general")) {
        if (!strcmp(key, "arch") || !strcmp(key, "compat-arch") ||
            !strcmp(key, "scripts") || !strcmp(key, "trust")) return 1;
    } else if (!strcmp(section, "resolver")) {
        if (!strcmp(key, "ask-sources") || !strcmp(key, "prefer")) return 1;
    } else if (!strncmp(section, "source ", 7)) {
        if (!strcmp(key, "repo")) return 2;
        if (!strcmp(key, "type") || !strcmp(key, "url") ||
            !strcmp(key, "parent") || !strcmp(key, "family") ||
            !strcmp(key, "priority") || !strcmp(key, "trust") ||
            !strcmp(key, "public-key") ||
            !strcmp(key, "public-key-ed25519")) return 1;
    } else if (!strncmp(section, "rule ", 5)) {
        if (!strcmp(key, "consumer") || !strcmp(key, "require") ||
            !strcmp(key, "provider")) return 1;
    }
    return 0;
}

static int known_value(const char *section, const char *key, const char *v)
{
    if (!strncmp(section, "source ", 7) && !strcmp(key, "family"))
        return *v && strcmp(v, "-") && strlen(v) <= 128;
    if (!strncmp(section, "source ", 7) && !strcmp(key, "priority")) {
        char *end;
        long number;
        errno = 0;
        number = strtol(v, &end, 10);
        return *v && !errno && !*end && number >= INT_MIN && number <= INT_MAX;
    }
    if (!strcmp(section, "disk") && !strcmp(key, "layout"))
        return !strcmp(v, "gpt-ext4");
    if (!strcmp(key, "scripts") && !strcmp(section, "general"))
        return !strcmp(v, "ask") || !strcmp(v, "run") || !strcmp(v, "skip");
    if (!strcmp(key, "trust"))
        return !strcmp(v, "warn") || !strcmp(v, "require") || !strcmp(v, "ignore");
    if (!strcmp(key, "ask-sources"))
        return !strcmp(v, "yes") || !strcmp(v, "no");
    if (!strcmp(key, "prefer"))
        return !strcmp(v, "installed") || !strcmp(v, "same-source") ||
               !strcmp(v, "parent") || !strcmp(v, "family");
    return 1;
}

static int url_has_userinfo(const char *url)
{
    const char *scheme = strstr(url, "://"), *authority, *end;
    if (!scheme) return 0;
    authority = scheme + 3;
    end = authority + strcspn(authority, "/?#");
    return memchr(authority, '@', (size_t)(end - authority)) != NULL;
}

static int parse(const char *path, struct holy_config *config,
                 struct frame *parent, char **error, int fixed_plan)
{
    FILE *fp;
    struct stat st;
    struct frame current, *node;
    char *line = NULL, *section = NULL;
    size_t capacity = 0, lineno = 0;
    ssize_t length;
    int ok = 1;
    size_t depth = 0;
    for (node = parent; node; node = node->parent) ++depth;
    if (depth >= 64) return fail(error, path, 0, "include depth exceeds 64");
    if (fixed_plan) {
        int fd = open(path, O_RDONLY | O_NONBLOCK | O_NOFOLLOW | O_CLOEXEC);
        struct stat plan_stat;
        if (fd >= 0 && (fstat(fd, &plan_stat) || !S_ISREG(plan_stat.st_mode) ||
                        plan_stat.st_size > 1048576)) {
            close(fd);
            return fail(error, path, 0, "plan must be a regular file of at most 1 MiB");
        }
        fp = fd < 0 ? NULL : fdopen(fd, "r");
        if (!fp && fd >= 0) close(fd);
    } else fp = fopen(path, "r");
    if (!fp) return fail(error, path, 0, "open: %s", strerror(errno));
    if (fstat(fileno(fp), &st)) {
        ok = fail(error, path, 0, "stat: %s", strerror(errno));
        goto done;
    }
    for (node = parent; node; node = node->parent)
        if (node->dev == st.st_dev && node->ino == st.st_ino) {
            ok = fail(error, path, 0, "include cycle");
            goto done;
        }
    current.dev = st.st_dev;
    current.ino = st.st_ino;
    current.parent = parent;
    while ((length = getline(&line, &capacity, fp)) != -1) {
        char **v = NULL;
        size_t count = 0, i;
        struct holy_entry entry, *entries;
        ++lineno;
        if (memchr(line, '\0', (size_t)length)) {
            ok = fail(error, path, lineno, "NUL byte");
            break;
        }
        if (!holy_lex(line, (size_t)length, &v, &count, path, lineno, error)) { ok = 0; break; }
        if (!count) { holy_tokens_free(v, count); continue; }
        if (!strcmp(v[0], "include") && fixed_plan) {
            ok = fail(error, path, lineno, "include is forbidden in a plan");
        } else if (!strcmp(v[0], "include")) {
            char *child;
            if (count != 2) ok = fail(error, path, lineno, "include expects one path");
            else if (!(child = relative(path, v[1]))) ok = fail(error, path, lineno, "out of memory");
            else { ok = parse(child, config, &current, error, 0); free(child); }
        } else if (v[0][0] == '[') {
            size_t n = strlen(v[0]);
            size_t total = n;
            char *joined = NULL;
            if (count == 2 && total <= (size_t)-1 - strlen(v[1]) - 2) {
                total += strlen(v[1]) + 1;
                joined = malloc(total + 1);
                if (joined) {
                    snprintf(joined, total + 1, "%s %s", v[0], v[1]);
                    n = total;
                }
            } else if (count == 1) joined = copy(v[0]);
            if (!joined) ok = fail(error, path, lineno, "invalid section or out of memory");
            else if (count > 2 || n < 3 || joined[n - 1] != ']' ||
                     (joined[n - 1] = '\0', !valid_section(joined + 1)))
                ok = fail(error, path, lineno, "invalid section");
            else {
                char *next = copy(joined + 1);
                if (!next) ok = fail(error, path, lineno, "out of memory");
                else { free(section); section = next; }
            }
            free(joined);
        } else if (!section || count < 2) {
            ok = fail(error, path, lineno, "key outside section or missing value");
        } else {
            int arity = key_arity(section, v[0]);
            if (!arity) ok = fail(error, path, lineno, "unknown key %s in [%s]", v[0], section);
            else if (count - 1 != (size_t)arity)
                ok = fail(error, path, lineno, "%s expects %d argument(s)", v[0], arity);
            else if (!known_value(section, v[0], v[1]))
                ok = fail(error, path, lineno, "invalid %s value", v[0]);
            for (i = 0; i < config->count; ++i)
                if (ok && !list_key(section, v[0]) &&
                    !strcmp(config->entries[i].section, section) &&
                    !strcmp(config->entries[i].key, v[0])) {
                    ok = fail(error, path, lineno, "duplicate %s (first at %s:%zu)",
                              v[0], config->entries[i].file, config->entries[i].line);
                    break;
                }
            if (ok) {
                if (config->count == (size_t)-1 / sizeof(*entries))
                    ok = fail(error, path, lineno, "too many entries");
                else if (!(entries = realloc(config->entries,
                                             (config->count + 1) * sizeof(*entries))))
                    ok = fail(error, path, lineno, "out of memory");
                else {
                    config->entries = entries;
                    entry.section = copy(section);
                    entry.file = copy(path);
                    entry.key = v[0];
                    entry.values = malloc((count - 1) * sizeof(*entry.values));
                    entry.count = count - 1;
                    if (!entry.section || !entry.file || !entry.values) {
                        free(entry.section); free(entry.file); free(entry.values);
                        ok = fail(error, path, lineno, "out of memory");
                    } else {
                        for (i = 1; i < count; ++i) entry.values[i - 1] = v[i];
                        entry.line = lineno;
                        config->entries[config->count++] = entry;
                        free(v);
                        v = NULL;
                    }
                }
            }
        }
        if (v) holy_tokens_free(v, count);
        if (!ok) break;
    }
    if (ok && ferror(fp)) ok = fail(error, path, lineno, "read: %s", strerror(errno));
done:
    free(line);
    free(section);
    fclose(fp);
    return ok;
}

static int validate(struct holy_config *config, char **error)
{
    size_t i, j;
    for (i = 0; i < config->count; ++i) {
        const struct holy_entry *e = &config->entries[i];
        int type = 0, endpoint = 0, consumer = 0, require = 0, provider = 0;
        if (!e->values[0][0] || (e->count == 2 && !e->values[1][0]))
            return fail(error, e->file, e->line, "empty %s value", e->key);
        if ((!strcmp(e->key, "url") || !strcmp(e->key, "repo")) &&
            url_has_userinfo(e->values[e->count - 1]))
            return fail(error, e->file, e->line, "credentials in %s URL", e->key);
        if (!strcmp(e->key, "repo")) {
            for (j = 0; j < i; ++j) {
                const struct holy_entry *other = &config->entries[j];
                if (!strcmp(e->section, other->section) &&
                    !strcmp(other->key, "repo") &&
                    !strcmp(e->values[0], other->values[0]))
                    return fail(error, e->file, e->line,
                                "duplicate repo name (first at %s:%zu)",
                                other->file, other->line);
            }
        }
        if (strncmp(e->section, "source ", 7) && strncmp(e->section, "rule ", 5))
            continue;
        for (j = 0; j < i; ++j)
            if (!strcmp(e->section, config->entries[j].section)) break;
        if (j != i) continue;
        for (j = i; j < config->count; ++j) {
            const struct holy_entry *field = &config->entries[j];
            if (strcmp(field->section, e->section)) continue;
            if (!strcmp(field->key, "type")) type = 1;
            else if (!strcmp(field->key, "url") || !strcmp(field->key, "repo")) endpoint = 1;
            else if (!strcmp(field->key, "consumer")) consumer = 1;
            else if (!strcmp(field->key, "require")) require = 1;
            else if (!strcmp(field->key, "provider")) provider = 1;
        }
        if (!strncmp(e->section, "source ", 7) && (!type || !endpoint))
            return fail(error, e->file, e->line,
                        "[%s] requires type and url or repo", e->section);
        if (!strncmp(e->section, "rule ", 5) &&
            (!consumer || !require || !provider))
            return fail(error, e->file, e->line,
                        "[%s] requires consumer, require and provider", e->section);
    }
    for (i = 0; i < config->count; ++i) {
        const struct holy_entry *start = &config->entries[i], *parent = start;
        size_t hops = 0;
        if (strcmp(start->key, "parent")) continue;
        while (parent) {
            const struct holy_entry *next = NULL;
            size_t n;
            for (n = 0; n < config->count; ++n)
                if (!strcmp(config->entries[n].key, "parent") &&
                    !strncmp(config->entries[n].section, "source ", 7) &&
                    !strcmp(config->entries[n].section + 7, parent->values[0])) {
                    next = &config->entries[n];
                    break;
                }
            if (!next) {
                int found = 0;
                for (n = 0; n < config->count; ++n)
                    if (!strncmp(config->entries[n].section, "source ", 7) &&
                        !strcmp(config->entries[n].section + 7, parent->values[0])) {
                        found = 1;
                        break;
                    }
                if (!found) return fail(error, parent->file, parent->line,
                                        "parent source is not configured");
            }
            parent = next;
            if (++hops > config->count)
                return fail(error, start->file, start->line, "parent source cycle");
        }
    }
    return 1;
}

int holy_config_load(const char *path, struct holy_config *out, char **error)
{
    *error = NULL;
    return parse(path, out, NULL, error, 0) && validate(out, error);
}

int holy_config_load_plan(const char *path, struct holy_config *out, char **error)
{
    *error = NULL;
    return parse(path, out, NULL, error, 1) && validate(out, error);
}

void holy_config_free(struct holy_config *config)
{
    size_t i, j;
    for (i = 0; i < config->count; ++i) {
        struct holy_entry *e = &config->entries[i];
        free(e->section);
        free(e->key);
        free(e->file);
        for (j = 0; j < e->count; ++j) free(e->values[j]);
        free(e->values);
    }
    free(config->entries);
    config->entries = NULL;
    config->count = 0;
}