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

file src/package.c

#define _POSIX_C_SOURCE 200809L
#include "package.h"
#include "config.h"
#include "version.h"
#include "../backends/rpm-version.h"

#include <archive.h>
#include <archive_entry.h>
#include <openssl/evp.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>

#define META_LIMIT (1024u * 1024u)

static const char *const fields[] = {
    "format", "name", "version", "release", "os", "arch", "libc", "x-version-family"
};

static const char *const members[] = {
    "HOLY/meta", "HOLY/files", "HOLY/deps", "HOLY/provides",
    "HOLY/hooks", "HOLY/origin", "HOLY/transform"
};

static int safe_value(const char *s)
{
    const unsigned char *p = (const unsigned char *)s;
    if (!*p) return 0;
    while (*p) {
        if (*p < 32 || *p == 127) return 0;
        ++p;
    }
    return 1;
}

static int one_of(const char *value, const char *a, const char *b,
                  const char *c)
{
    return !strcmp(value, a) || !strcmp(value, b) ||
           (c && !strcmp(value, c));
}

int holy_safe_archive_path(const char *name)
{
    const char *part, *end;
    size_t n;
    if (!name || !*name || name[0] == '/' ||
        (strcmp(name, "HOLY") && strcmp(name, "DATA") &&
         strncmp(name, "HOLY/", 5) && strncmp(name, "DATA/", 5))) return 0;
    part = name;
    while (*part) {
        end = strchr(part, '/');
        n = end ? (size_t)(end - part) : strlen(part);
        if (!n || (n == 1 && part[0] == '.') ||
            (n == 2 && part[0] == '.' && part[1] == '.')) return 0;
        if (!end) break;
        part = end + 1;
        if (!*part) return 1;
    }
    return 1;
}

static int meta_line(char *line, size_t len, const char *path, size_t number,
                     char **values, char **error)
{
    char **v = NULL;
    size_t count = 0, i;
    if (memchr(line, '\0', len)) {
        fprintf(stderr, "%s: HOLY/meta:%zu: NUL byte\n", path, number);
        return 0;
    }
    if (!holy_lex(line, len, &v, &count, path, number, error)) {
        fprintf(stderr, "%s\n", *error ? *error : "out of memory");
        holy_tokens_free(v, count);
        return 0;
    }
    if (!count) { holy_tokens_free(v, count); return 1; }
    if (count != 2 || !safe_value(v[0]) || !safe_value(v[1])) {
        fprintf(stderr, "%s: HOLY/meta:%zu: invalid field\n", path, number);
        holy_tokens_free(v, count);
        return 0;
    }
    if (!strcmp(v[0], "requires-feature")) {
        fprintf(stderr, "%s: HOLY/meta:%zu: unsupported feature\n", path, number);
        holy_tokens_free(v, count);
        return 0;
    }
    for (i = 0; i < sizeof fields / sizeof *fields; ++i) {
        if (strcmp(fields[i], v[0])) continue;
        if (values[i]) {
            fprintf(stderr, "%s: HOLY/meta:%zu: duplicate %s\n", path, number, v[0]);
            holy_tokens_free(v, count);
            return 0;
        }
        values[i] = v[1];
        v[1] = NULL;
        break;
    }
    holy_tokens_free(v, count);
    return 1;
}

static int read_meta(const char *path, const char *data, size_t length, int emit,
                     char **arch, char **libc, struct holy_package_identity *identity)
{
    size_t i, start = 0, line = 1;
    char *values[sizeof fields / sizeof *fields] = {0};
    char *error = NULL;
    int ok = 1;
    for (i = 0; i <= length; ++i) {
        if (i != length && data[i] != '\n') continue;
        if (!meta_line((char *)data + start, i - start, path, line++, values, &error)) {
            ok = 0;
            break;
        }
        start = i + 1;
    }
    if (ok) for (i = 0; i < 7; ++i) {
        if (!values[i]) {
            fprintf(stderr, "%s: HOLY/meta: missing %s\n", path, fields[i]);
            ok = 0;
        }
    }
    if (ok && strcmp(values[0], "holy-package-1")) {
        fprintf(stderr, "%s: unsupported format %s\n", path, values[0]);
        ok = 0;
    }
    if (ok && !one_of(values[4], "linux", "windows", NULL)) {
        fprintf(stderr, "%s: unsupported os in HOLY/meta\n", path);
        ok = 0;
    }
    if (ok && !one_of(values[5], "x86", "x86_64", "noarch")) {
        fprintf(stderr, "%s: unsupported arch in HOLY/meta\n", path);
        ok = 0;
    }
    if (ok && !one_of(values[6], "glibc", "musl", "nolibc")) {
        fprintf(stderr, "%s: unsupported libc in HOLY/meta\n", path);
        ok = 0;
    }
    if (ok && !strcmp(values[5], "noarch") && strcmp(values[6], "nolibc")) {
        fprintf(stderr, "%s: noarch requires nolibc in HOLY/meta\n", path);
        ok = 0;
    }
    if (ok && values[7] && !strcmp(values[7], "holy")) {
        int order;
        if (!holy_version_compare(values[2], values[2], &order) ||
            !holy_version_compare(values[3], values[3], &order)) {
            fprintf(stderr, "%s: invalid Holy native version or release in HOLY/meta\n", path);
            ok = 0;
        }
    }
    if (ok && values[7] && !strcmp(values[7], "rpm") &&
        (!holy_rpm_version_valid(values[2]) || !holy_rpm_version_valid(values[3]) ||
         strchr(values[2], '-') || strchr(values[3], ':') || strchr(values[3], '-'))) {
        fprintf(stderr, "%s: invalid RPM version or release in HOLY/meta\n", path);
        ok = 0;
    }
    if (ok && emit) for (i = 0; i < sizeof fields / sizeof *fields; ++i)
        if (values[i]) printf("%s %s\n", fields[i], values[i]);
    if (ok && arch && libc) {
        *arch = strdup(values[5]);
        *libc = strdup(values[6]);
        if (!*arch || !*libc) {
            free(*arch);
            free(*libc);
            *arch = *libc = NULL;
            ok = 0;
        }
    }
    if (ok && identity) {
        identity->version_family = values[7]; values[7] = NULL;
        identity->name = values[1]; values[1] = NULL;
        identity->version = values[2]; values[2] = NULL;
        identity->release = values[3]; values[3] = NULL;
        identity->os = values[4]; values[4] = NULL;
        identity->arch = values[5]; values[5] = NULL;
        identity->libc = values[6]; values[6] = NULL;
    }
    for (i = 0; i < sizeof fields / sizeof *fields; ++i) free(values[i]);
    free(error);
    return ok;
}

static int inspect(const char *path, int emit, char **arch, char **libc,
                   struct holy_package_identity *identity)
{
    static const unsigned char magic[] = {0x04, 0x22, 0x4d, 0x18};
    unsigned char head[4];
    char buffer[8192], *meta = NULL;
    struct archive *a = NULL;
    struct archive_entry *entry;
    EVP_MD_CTX *digest_ctx = NULL;
    unsigned char digest[EVP_MAX_MD_SIZE];
    unsigned int digest_size = 0;
    FILE *fp;
    size_t meta_size = 0, n, i;
    int status, ok = 0;
    unsigned char seen[sizeof members / sizeof *members] = {0};
    struct stat input_stat;
    fp = fopen(path, "rb");
    if (!fp) { perror(path); return 0; }
    if (identity && (fstat(fileno(fp), &input_stat) || !S_ISREG(input_stat.st_mode) ||
                     input_stat.st_size < 0)) goto done;
    if (fread(head, 1, sizeof head, fp) != sizeof head ||
        memcmp(head, magic, sizeof head)) {
        fprintf(stderr, "%s: expected LZ4 frame\n", path);
        goto done;
    }
    digest_ctx = EVP_MD_CTX_new();
    if (!digest_ctx || EVP_DigestInit_ex(digest_ctx, EVP_sha256(), NULL) != 1) {
        fprintf(stderr, "%s: SHA-256 initialization failed\n", path);
        goto done;
    }
    if (fseek(fp, 0, SEEK_SET)) goto done;
    while ((n = fread(buffer, 1, sizeof buffer, fp)) > 0)
        if (EVP_DigestUpdate(digest_ctx, buffer, n) != 1) goto done;
    if (ferror(fp) || EVP_DigestFinal_ex(digest_ctx, digest, &digest_size) != 1 ||
        digest_size != 32) {
        fprintf(stderr, "%s: SHA-256 read failed\n", path);
        goto done;
    }
    if (fseek(fp, 0, SEEK_SET)) {
        fprintf(stderr, "%s: rewind failed\n", path);
        goto done;
    }
    a = archive_read_new();
    if (!a || archive_read_support_filter_lz4(a) != ARCHIVE_OK ||
        archive_read_support_format_tar(a) != ARCHIVE_OK ||
        archive_read_open_FILE(a, fp) != ARCHIVE_OK) {
        fprintf(stderr, "%s: archive open: %s\n", path,
                a ? archive_error_string(a) : "out of memory");
        goto done;
    }
    while ((status = archive_read_next_header(a, &entry)) == ARCHIVE_OK) {
        const char *name = archive_entry_pathname(entry);
        int is_meta = name && !strcmp(name, "HOLY/meta");
        la_ssize_t got;
        if (!holy_safe_archive_path(name)) {
            fprintf(stderr, "%s: unsafe or unexpected archive path\n", path);
            goto done;
        }
        if ((!strcmp(name, "HOLY") || !strcmp(name, "HOLY/") ||
             !strcmp(name, "DATA") || !strcmp(name, "DATA/")) &&
            archive_entry_filetype(entry) != AE_IFDIR) {
            fprintf(stderr, "%s: archive root marker is not a directory\n", path);
            goto done;
        }
        for (i = 0; i < sizeof members / sizeof *members; ++i) {
            if (strcmp(name, members[i])) continue;
            if (seen[i]++ || archive_entry_filetype(entry) != AE_IFREG ||
                archive_entry_hardlink(entry) || archive_entry_size(entry) < 0 ||
                (is_meta && archive_entry_size(entry) > META_LIMIT)) {
                fprintf(stderr, "%s: invalid or repeated %s\n", path, members[i]);
                goto done;
            }
            break;
        }
        while ((got = archive_read_data(a, buffer, sizeof buffer)) > 0) {
            if (is_meta) {
                if ((size_t)got > META_LIMIT - meta_size) {
                    fprintf(stderr, "%s: oversized HOLY/meta\n", path);
                    goto done;
                }
                {
                    char *next = realloc(meta, meta_size + (size_t)got + 1);
                    if (!next) { fprintf(stderr, "%s: out of memory\n", path); goto done; }
                    meta = next;
                }
                memcpy(meta + meta_size, buffer, (size_t)got);
                meta_size += (size_t)got;
                meta[meta_size] = '\0';
            }
        }
        if (got < 0) { fprintf(stderr, "%s: archive data: %s\n", path, archive_error_string(a)); goto done; }
    }
    if (status != ARCHIVE_EOF) {
        fprintf(stderr, "%s: archive header: %s\n", path, archive_error_string(a));
        goto done;
    }
    for (i = 0; i < sizeof members / sizeof *members; ++i)
        if (!seen[i]) {
            fprintf(stderr, "%s: missing %s\n", path, members[i]);
            goto done;
        }
    ok = read_meta(path, meta ? meta : "", meta_size, emit, arch, libc, identity);
    if (ok && identity) {
        for (i = 0; i < digest_size; ++i)
            snprintf(identity->digest + i * 2, 3, "%02x", digest[i]);
        identity->digest[64] = '\0';
        identity->size = (uint64_t)input_stat.st_size;
    }
    if (ok && emit) {
        fputs("sha256 ", stdout);
        for (i = 0; i < digest_size; ++i) printf("%02x", digest[i]);
        putchar('\n');
    }
done:
    if (a) archive_read_free(a);
    fclose(fp);
    free(meta);
    EVP_MD_CTX_free(digest_ctx);
    return ok;
}

int holy_package_inspect(const char *path, int emit)
{
    return inspect(path, emit, NULL, NULL, NULL);
}

int holy_package_tags(const char *path, char **arch, char **libc)
{
    *arch = *libc = NULL;
    return inspect(path, 0, arch, libc, NULL);
}

int holy_package_identity(const char *path, struct holy_package_identity *out)
{
    memset(out, 0, sizeof *out);
    return inspect(path, 0, NULL, NULL, out);
}

void holy_package_identity_free(struct holy_package_identity *info)
{
    free(info->name);
    free(info->version);
    free(info->release);
    free(info->version_family);
    free(info->os);
    free(info->arch);
    free(info->libc);
    memset(info, 0, sizeof *info);
}

int holy_package_info(const char *path)
{
    return holy_package_inspect(path, 1);
}