_ _
| |_ ___| |_ _
| | . | | | |
|_|_|___|_|_ |
|___|
git mirror - github.com/owenewans/holy - branch master
file src/source.c
#define _XOPEN_SOURCE 700
#include "source.h"
#include "config.h"
#include "state.h"
#include "stage.h"
#include "repo.h"
#include "fetch.h"
#include "sign.h"
#include "git.h"
#include "../backends/apk.h"
#include "../backends/apt-release.h"
#include "../backends/xbps.h"
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <limits.h>
#include <openssl/evp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <unistd.h>
struct source {
char id[65];
char *alias, *definition, *trust, *family;
char key[65], parent[65];
int active, priority;
};
struct registry {
struct source *items;
size_t count;
unsigned long long revision;
};
static const char *source_public_key(const struct source *source, char spec[69])
{
if (!source->key[0]) return NULL;
memcpy(spec, "raw:", 4);
memcpy(spec + 4, source->key, 65);
return spec;
}
static int hash(const char *data, char output[65])
{
unsigned char bytes[32];
unsigned int size;
size_t i;
if (EVP_Digest(data, strlen(data), bytes, &size, EVP_sha256(), NULL) != 1 || size != 32) return 0;
for (i = 0; i < 32; ++i) snprintf(output + i * 2, 3, "%02x", bytes[i]);
return 1;
}
static int valid_hash(const char *text)
{
return strlen(text) == 64 && strspn(text, "0123456789abcdef") == 64;
}
static int key_fingerprint(const char key[65], char output[65])
{
unsigned char raw[32], digest[32];
unsigned size;
size_t i;
if (!key[0]) { strcpy(output, "-"); return 1; }
if (!valid_hash(key)) return 0;
for (i = 0; i < 32; ++i) {
unsigned hi = (unsigned)(key[2 * i] <= '9' ? key[2 * i] - '0' : key[2 * i] - 'a' + 10);
unsigned lo = (unsigned)(key[2 * i + 1] <= '9' ? key[2 * i + 1] - '0' : key[2 * i + 1] - 'a' + 10);
raw[i] = (unsigned char)(hi * 16 + lo);
}
if (EVP_Digest(raw, sizeof raw, digest, &size, EVP_sha256(), NULL) != 1 || size != 32)
return 0;
for (i = 0; i < 32; ++i) snprintf(output + 2 * i, 3, "%02x", digest[i]);
return 1;
}
static void quote(FILE *out, const char *text)
{
const unsigned char *p = (const unsigned char *)text;
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 clear_registry(struct registry *r)
{
size_t i;
for (i = 0; i < r->count; ++i) {
free(r->items[i].alias); free(r->items[i].definition);
free(r->items[i].trust); free(r->items[i].family);
}
free(r->items);
memset(r, 0, sizeof *r);
}
static int source_order(const void *a, const void *b)
{
return strcmp(((const struct source *)a)->id, ((const struct source *)b)->id);
}
static int row_order(const void *a, const void *b)
{
return strcmp(*(const char *const *)a, *(const char *const *)b);
}
static char *read_fd(int fd)
{
struct stat st;
char *data;
size_t used = 0;
if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size < 1 || st.st_size > 32 * 1024 * 1024) return NULL;
data = malloc((size_t)st.st_size + 1);
if (!data) return NULL;
while (used < (size_t)st.st_size) {
ssize_t n = read(fd, data + used, (size_t)st.st_size - used);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) { free(data); return NULL; }
used += (size_t)n;
}
if (memchr(data, 0, used) || data[used - 1] != '\n') { free(data); return NULL; }
data[used] = 0;
return data;
}
static int tokens(const char *line, size_t size, char ***v, size_t *n)
{
char *error = NULL;
int ok = holy_lex(line, size, v, n, "source registry", 0, &error);
free(error);
return ok;
}
static int endpoint_valid(const char *value)
{
const char *scheme = strstr(value, "://"), *end;
const unsigned char *p = (const unsigned char *)value;
if (!scheme || scheme == value || strchr(value, '?') || strchr(value, '#')) return 0;
for (; *p; ++p) if (*p <= 32 || *p == 127) return 0;
scheme += 3;
end = scheme + strcspn(scheme, "/");
return memchr(scheme, '@', (size_t)(end - scheme)) == NULL;
}
static int definition_valid(const char *text)
{
const char *line = text;
unsigned types = 0, urls = 0, repos = 0;
int ok = 1;
char *previous = NULL, *repo_name = NULL;
while (*line && ok) {
const char *end = strchr(line, '\n');
char **v = NULL, *canonical = NULL;
size_t n = 0, size = 0, i;
ok = end && tokens(line, (size_t)(end - line), &v, &n);
if (ok && n == 2 && !strcmp(v[0], "type")) ok = v[1][0] && ++types == 1;
else if (ok && n == 2 && !strcmp(v[0], "url")) ok = ++urls == 1 && endpoint_valid(v[1]);
else if (ok && n == 3 && !strcmp(v[0], "repo")) {
++repos;
ok = v[1][0] && endpoint_valid(v[2]) && (!repo_name || strcmp(repo_name, v[1]));
free(repo_name); repo_name = strdup(v[1]);
if (!repo_name) ok = 0;
}
else ok = 0;
if (ok) {
FILE *out = open_memstream(&canonical, &size);
if (!out) ok = 0;
else {
fputs(v[0], out);
for (i = 1; i < n; ++i) { fputc(' ', out); quote(out, v[i]); }
fputc('\n', out);
ok = !ferror(out);
if (fclose(out)) ok = 0;
if (ok) ok = size == (size_t)(end - line + 1) && !memcmp(line, canonical, size) &&
(!previous || strcmp(previous, canonical) < 0);
}
}
free(previous); previous = canonical;
holy_tokens_free(v, n);
if (end) line = end + 1;
}
free(previous); free(repo_name);
return ok && types == 1 && (urls || repos);
}
static char *serialize(struct registry *r)
{
char *data = NULL;
size_t size = 0, i;
FILE *out = open_memstream(&data, &size);
int ok;
if (!out) return NULL;
if (r->count) qsort(r->items, r->count, sizeof *r->items, source_order);
fprintf(out, "format holy-sources-3\nrevision %llu\n", r->revision);
for (i = 0; i < r->count; ++i) {
struct source *s = &r->items[i];
fprintf(out, "source %s ", s->id); quote(out, s->alias);
fprintf(out, " %s ", s->active ? "active" : "inactive"); quote(out, s->definition);
fprintf(out, " %s %s %s ", s->trust, s->key[0] ? s->key : "-",
s->parent[0] ? s->parent : "-");
quote(out, s->family ? s->family : "-");
fprintf(out, " %d\n", s->priority);
}
ok = !ferror(out);
if (fclose(out)) ok = 0;
if (!ok || size > 16 * 1024 * 1024) { free(data); return NULL; }
return data;
}
static int parse_registry(const char *data, struct registry *r)
{
const char *line = data;
size_t number = 0, i;
int version = 0;
if (!*data) return 1;
while (*line) {
const char *end = strchr(line, '\n');
char **v = NULL;
size_t n = 0;
int ok = end && tokens(line, (size_t)(end - line), &v, &n);
if (ok && number == 0) {
version = n == 2 && !strcmp(v[0], "format") && !strcmp(v[1], "holy-sources-1") ? 1 :
n == 2 && !strcmp(v[0], "format") && !strcmp(v[1], "holy-sources-2") ? 2 :
n == 2 && !strcmp(v[0], "format") && !strcmp(v[1], "holy-sources-3") ? 3 : 0;
ok = version != 0;
}
else if (ok && number == 1) {
char *last = NULL;
errno = 0;
if (n != 2 || strcmp(v[0], "revision") || !v[1][0] || strspn(v[1], "0123456789") != strlen(v[1])) ok = 0;
else { r->revision = strtoull(v[1], &last, 10); ok = !errno && !*last; }
} else if (ok && ((version == 1 && n >= 5 && n <= 7) ||
(version == 2 && n == 8) ||
(version == 3 && n == 10)) &&
!strcmp(v[0], "source") && valid_hash(v[1]) &&
v[2][0] && strcmp(v[2], "local") &&
(!strcmp(v[3], "active") || !strcmp(v[3], "inactive")) &&
definition_valid(v[4]) &&
(n == 5 || !strcmp(v[5], "warn") || !strcmp(v[5], "require") ||
!strcmp(v[5], "ignore")) &&
(version == 1 ? n < 7 || valid_hash(v[6]) :
(!strcmp(v[6], "-") || valid_hash(v[6])) &&
(!strcmp(v[7], "-") || valid_hash(v[7]))) &&
(version != 3 || (!strcmp(v[8], "-") ||
(v[8][0] && strlen(v[8]) <= 128)))) {
struct source *grown;
char id[65];
int active = !strcmp(v[3], "active");
long priority = 0;
char *end_priority = NULL;
if (version == 3) {
errno = 0;
priority = strtol(v[9], &end_priority, 10);
if (errno || !v[9][0] || *end_priority || priority < INT_MIN ||
priority > INT_MAX) ok = 0;
}
ok = ok && hash(v[4], id) && !strcmp(id, v[1]) && r->count < 10000 &&
!(((version == 1 && n == 6) ||
(version >= 2 && !strcmp(v[6], "-"))) &&
!strcmp(v[5], "require") &&
(strstr(v[4], "type \"holy-http\"\n") ||
strstr(v[4], "type \"holy-git\"\n") ||
strstr(v[4], "type \"apk\"\n") ||
strstr(v[4], "type \"apt\"\n") ||
strstr(v[4], "type \"rpm-md\"\n") ||
strstr(v[4], "type \"xbps\"\n")));
for (i = 0; ok && i < r->count; ++i)
if (!strcmp(r->items[i].id, id) || (active && r->items[i].active && !strcmp(r->items[i].alias, v[2]))) ok = 0;
grown = ok ? realloc(r->items, (r->count + 1) * sizeof *grown) : NULL;
if (!grown) ok = 0;
else {
struct source *s;
r->items = grown; s = &grown[r->count++]; memset(s, 0, sizeof *s);
memcpy(s->id, id, 65); s->alias = strdup(v[2]);
s->definition = strdup(v[4]); s->trust = strdup(n >= 6 ? v[5] : "warn");
if ((version == 1 && n == 7) || (version >= 2 && strcmp(v[6], "-")))
memcpy(s->key, v[6], 65);
if (version >= 2 && strcmp(v[7], "-")) memcpy(s->parent, v[7], 65);
if (version == 3 && strcmp(v[8], "-")) s->family = strdup(v[8]);
s->priority = (int)priority;
s->active = active;
ok = s->alias && s->definition && s->trust &&
(version != 3 || !strcmp(v[8], "-") || s->family);
}
} else ok = 0;
holy_tokens_free(v, n);
if (!ok) return 0;
++number;
line = end + 1;
}
if (number < 2) return 0;
for (number = 0; number < r->count; ++number) if (r->items[number].parent[0]) {
size_t depth = 0, current = number;
while (r->items[current].parent[0]) {
for (i = 0; i < r->count; ++i)
if (!strcmp(r->items[i].id, r->items[current].parent)) break;
if (i == r->count || i == number || ++depth > r->count) return 0;
current = i;
}
}
return 1;
}
static char *load_registry(int dir, struct registry *r)
{
struct stat st;
int fd = openat(dir, "sources", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
char *data;
if (fd < 0) return errno == ENOENT ? strdup("") : NULL;
if (fstat(fd, &st) || (st.st_mode & 0022) || (st.st_uid != 0 && st.st_uid != geteuid())) { close(fd); return NULL; }
data = read_fd(fd);
close(fd);
if (data && !parse_registry(data, r)) { free(data); data = NULL; }
return data;
}
static char *definition(const struct holy_config *config, const char *section)
{
char **rows = NULL, *data = NULL;
size_t count = 0, i, j, size = 0;
FILE *out = NULL;
int ok = 0;
rows = calloc(config->count ? config->count : 1, sizeof *rows);
if (!rows) goto done;
for (i = 0; i < config->count; ++i) {
const struct holy_entry *e = &config->entries[i];
if (strcmp(e->section, section) || (strcmp(e->key, "type") && strcmp(e->key, "url") && strcmp(e->key, "repo"))) continue;
out = open_memstream(&rows[count], &size);
if (!out) goto done;
++count;
fputs(e->key, out);
for (j = 0; j < e->count; ++j) { fputc(' ', out); quote(out, e->values[j]); }
fputc('\n', out);
ok = !ferror(out);
if (fclose(out)) ok = 0;
out = NULL;
if (!ok) goto done;
}
qsort(rows, count, sizeof *rows, row_order);
out = open_memstream(&data, &size);
if (!out) { ok = 0; goto done; }
for (i = 0; i < count; ++i) fputs(rows[i], out);
ok = !ferror(out);
if (fclose(out)) ok = 0;
out = NULL;
if (ok) ok = size <= 1024 * 1024 && definition_valid(data);
done:
if (out) fclose(out);
for (i = 0; i < count; ++i) free(rows[i]);
free(rows);
if (!ok) { free(data); data = NULL; }
return data;
}
static const struct holy_entry *config_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];
return NULL;
}
static char *source_key_path(const struct holy_entry *entry)
{
const char *name = entry->values[0], *slash;
char *path, *resolved;
size_t a, b;
if (name[0] == '/') return realpath(name, NULL);
slash = strrchr(entry->file, '/');
a = slash ? (size_t)(slash - entry->file + 1) : 0;
b = strlen(name);
if (a > (size_t)-1 - b - 1) return NULL;
path = malloc(a + b + 1);
if (!path) return NULL;
memcpy(path, entry->file, a);
memcpy(path + a, name, b + 1);
resolved = realpath(path, NULL);
free(path);
return resolved;
}
static void describe_changes(const struct registry *before, const struct registry *after)
{
size_t i, j, k;
for (i = 0; i < after->count; ++i) {
const struct source *next = &after->items[i];
if (!next->active) continue;
for (j = 0; j < before->count; ++j) if (!strcmp(next->id, before->items[j].id)) break;
if (j < before->count && before->items[j].active) {
const struct source *old = &before->items[j];
if (strcmp(old->trust, next->trust) || strcmp(old->key, next->key) ||
strcmp(old->parent, next->parent) ||
strcmp(old->family ? old->family : "", next->family ? next->family : "") ||
old->priority != next->priority) {
fprintf(stderr, "policy-change %s ", next->id);
quote(stderr, next->alias);
fprintf(stderr, " trust=%s parent=%s family=", next->trust,
next->parent[0] ? next->parent : "-");
quote(stderr, next->family ? next->family : "-");
fprintf(stderr, " priority=%d key=%s\n", next->priority,
next->key[0] ? next->key : "-");
}
if (!strcmp(next->alias, old->alias)) continue;
}
for (k = 0; k < before->count; ++k)
if (before->items[k].active && !strcmp(next->alias, before->items[k].alias) && strcmp(next->id, before->items[k].id)) break;
if (k < before->count) {
fputs("origin-change ", stderr); quote(stderr, next->alias);
fprintf(stderr, " %s -> %s\n", before->items[k].id, next->id);
} else if (j < before->count) {
fprintf(stderr, "%s %s ", before->items[j].active ? "alias-change" : "reactivate", next->id);
quote(stderr, before->items[j].alias); fputs(" -> ", stderr); quote(stderr, next->alias); fputc('\n', stderr);
} else {
fprintf(stderr, "add-source %s ", next->id); quote(stderr, next->alias); fputc('\n', stderr);
}
}
for (i = 0; i < before->count; ++i) if (before->items[i].active) {
for (j = 0; j < after->count; ++j)
if (!strcmp(before->items[i].id, after->items[j].id) && after->items[j].active) break;
if (j == after->count) {
fprintf(stderr, "deactivate %s ", before->items[i].id);
quote(stderr, before->items[i].alias); fputc('\n', stderr);
}
}
}
int holy_source_plan(const char *path, const char *root)
{
struct holy_config config = {0};
struct registry r = {0}, before = {0};
struct stat st;
char *error = NULL, *old = NULL, *next = NULL, base[65];
int dir = -1, result = 1;
unsigned long long generation;
size_t i, j;
if (!holy_config_load(path, &config, &error)) { fprintf(stderr, "holypkg: %s\n", error ? error : "invalid config"); result = 2; goto done; }
dir = holy_state_lock(root, 0, &generation, &result);
if (dir < 0) goto done;
result = 1;
old = load_registry(dir, &r);
if (!old || !parse_registry(old, &before) || fstat(dir, &st) || !hash(old, base)) goto done;
for (i = 0; i < r.count; ++i) r.items[i].active = 0;
for (i = 0; i < config.count; ++i) {
const struct holy_entry *e = &config.entries[i];
const struct holy_entry *trust_entry, *key_entry, *raw_entry;
char *text, id[65], *alias, *trust, *key_path = NULL;
char key[65] = {0};
if (strncmp(e->section, "source ", 7) || strcmp(e->key, "type")) continue;
trust_entry = config_field(&config, e->section, "trust");
if (!trust_entry) trust_entry = config_field(&config, "general", "trust");
key_entry = config_field(&config, e->section, "public-key");
raw_entry = config_field(&config, e->section, "public-key-ed25519");
trust = strdup(trust_entry ? trust_entry->values[0] : "warn");
if (!trust) goto done;
if (key_entry && raw_entry) {
fputs("holypkg: source accepts one public key form\n", stderr);
free(trust); result = 2; goto done;
}
if (key_entry) {
key_path = source_key_path(key_entry);
if (!key_path ||
(!strcmp(e->values[0], "apk") ?
!holy_apk_key_fingerprint(key_path, key) :
!strcmp(e->values[0], "apt") ?
!holy_apt_key_fingerprint(key_path, key) :
!strcmp(e->values[0], "xbps") ?
!holy_xbps_key_fingerprint(key_path, key) :
!holy_public_key_hex(key_path, key))) {
fprintf(stderr, "holypkg: invalid %s public key at %s:%zu\n",
!strcmp(e->values[0], "apk") ? "APK RSA" :
!strcmp(e->values[0], "apt") ? "APT keyring" :
!strcmp(e->values[0], "xbps") ? "XBPS RSA" : "Ed25519",
key_entry->file, key_entry->line);
free(key_path); free(trust); result = 2; goto done;
}
free(key_path);
} else if (raw_entry) {
if (!strcmp(e->values[0], "apk") || !strcmp(e->values[0], "apt") ||
!strcmp(e->values[0], "xbps") ||
!valid_hash(raw_entry->values[0])) {
fprintf(stderr, "holypkg: invalid Ed25519 raw key at %s:%zu\n",
raw_entry->file, raw_entry->line);
free(trust); result = 2; goto done;
}
memcpy(key, raw_entry->values[0], 65);
}
if ((!strcmp(e->values[0], "holy-http") ||
!strcmp(e->values[0], "holy-git") ||
!strcmp(e->values[0], "apk") ||
!strcmp(e->values[0], "apt") ||
!strcmp(e->values[0], "xbps")) &&
!strcmp(trust, "require") && !key[0]) {
fprintf(stderr, "holypkg: [%s] trust require needs public-key\n", e->section);
free(trust); result = 2; goto done;
}
if (!strcmp(e->values[0], "rpm-md") && !strcmp(trust, "require")) {
fprintf(stderr, "holypkg: [%s] rpm-md checks no repository signature; "
"trust require is unavailable\n", e->section);
free(trust); result = 2; goto done;
}
if (!strcmp(e->values[0], "apt") || !strcmp(e->values[0], "xbps") ||
!strcmp(e->values[0], "rpm-md")) {
const struct holy_entry *endpoint = config_field(&config, e->section, "url");
char *probe = endpoint ? holy_fetch_child_url(endpoint->values[0], "probe") : NULL;
int repos = 0;
for (j = 0; j < config.count; ++j)
if (!strcmp(config.entries[j].section, e->section) &&
!strcmp(config.entries[j].key, "repo")) repos = 1;
if (!probe || repos) {
fprintf(stderr, "holypkg: [%s] %s requires one HTTPS url and no repo entries\n",
e->section, e->values[0]);
free(probe); free(trust); result = 2; goto done;
}
free(probe);
}
text = definition(&config, e->section);
if (!text) {
fputs("holypkg: invalid source endpoints for ", stderr); quote(stderr, e->section + 7);
fputs("; omit credentials, query and fragment\n", stderr);
free(trust);
result = 2; goto done;
}
if (!hash(text, id)) { free(text); free(trust); goto done; }
for (j = 0; j < r.count; ++j) if (!strcmp(r.items[j].id, id)) break;
if (j < r.count && r.items[j].active) {
fprintf(stderr, "holypkg: decision-required source-definition-%s: duplicate aliases\n", id);
free(text); free(trust); result = 3; goto done;
}
alias = strdup(e->section + 7);
if (!alias) { free(text); free(trust); goto done; }
if (j == r.count) {
struct source *grown = r.count < 10000 ? realloc(r.items, (r.count + 1) * sizeof *grown) : NULL;
if (!grown) { free(text); free(alias); free(trust); goto done; }
r.items = grown; memset(&grown[r.count++], 0, sizeof *grown);
memcpy(r.items[j].id, id, 65);
r.items[j].definition = text;
} else free(text);
free(r.items[j].trust); r.items[j].trust = trust;
memcpy(r.items[j].key, key, 65);
free(r.items[j].alias); r.items[j].alias = alias; r.items[j].active = 1;
}
for (i = 0; i < r.count; ++i) if (r.items[i].active) {
const struct holy_entry *parent, *family, *priority;
char *section;
size_t length = strlen(r.items[i].alias);
if (length > SIZE_MAX - sizeof "source ") { result = 2; goto done; }
section = malloc(length + sizeof "source ");
if (!section) goto done;
snprintf(section, length + sizeof "source ", "source %s", r.items[i].alias);
parent = config_field(&config, section, "parent");
family = config_field(&config, section, "family");
priority = config_field(&config, section, "priority");
free(section);
free(r.items[i].family); r.items[i].family = NULL;
r.items[i].priority = 0;
if (family) {
if (!family->values[0][0] || !strcmp(family->values[0], "-") ||
strlen(family->values[0]) > 128 ||
!(r.items[i].family = strdup(family->values[0]))) {
result = 2; goto done;
}
}
if (priority) {
char *end = NULL;
long value;
errno = 0;
value = strtol(priority->values[0], &end, 10);
if (errno || !priority->values[0][0] || *end ||
value < INT_MIN || value > INT_MAX) { result = 2; goto done; }
r.items[i].priority = (int)value;
}
r.items[i].parent[0] = 0;
if (!parent) continue;
for (j = 0; j < r.count; ++j)
if (r.items[j].active && !strcmp(r.items[j].alias, parent->values[0])) break;
if (j == r.count) { result = 2; goto done; }
memcpy(r.items[i].parent, r.items[j].id, 65);
}
next = serialize(&r);
if (!next) goto done;
if (strcmp(next, old)) {
if (r.revision == ULLONG_MAX) { result = 6; goto done; }
++r.revision; free(next); next = serialize(&r);
if (!next) goto done;
}
describe_changes(&before, &r);
printf("format holy-source-plan-1\nbase %s\ngeneration %llu\ndatabase %ju:%ju\n%s", base,
generation, (uintmax_t)st.st_dev, (uintmax_t)st.st_ino, next);
result = ferror(stdout) ? 1 : 0;
done:
if (result) fprintf(stderr, "holypkg: source plan failed (status %d)\n", result);
if (dir >= 0) close(dir);
free(error); free(old); free(next);
clear_registry(&r); clear_registry(&before); holy_config_free(&config);
return result;
}
int holy_source_apply(const char *path, const char *approved, const char *root)
{
struct registry old_registry = {0}, next_registry = {0};
struct stat st;
char *snapshot = NULL, *data = NULL, *old = NULL, *header_end, *next, *canonical = NULL;
char actual[65], base[65], prefix[256], temp_name[43] = {0};
size_t i, j, used = 0, size;
int dir = -1, input = -1, temp = -1, result = 1;
unsigned long long generation;
if (!valid_hash(approved)) return 2;
snapshot = holy_stage_local(path, "holy-source-plan");
input = snapshot ? open(snapshot, O_RDONLY | O_NOFOLLOW | O_CLOEXEC) : -1;
if (input < 0 || !(data = read_fd(input)) || !hash(data, actual)) goto done;
if (strcmp(actual, approved)) { result = 3; goto done; }
dir = holy_state_lock(root, 1, &generation, &result);
if (dir < 0) goto done;
result = 1;
old = load_registry(dir, &old_registry);
if (!old || !hash(old, base) || fstat(dir, &st)) goto done;
snprintf(prefix, sizeof prefix, "format holy-source-plan-1\nbase %s\ngeneration %llu\ndatabase %ju:%ju\n", base,
generation, (uintmax_t)st.st_dev, (uintmax_t)st.st_ino);
if (strncmp(data, prefix, strlen(prefix))) { result = 3; goto done; }
header_end = data + strlen(prefix); next = header_end;
if (!parse_registry(next, &next_registry) || !(canonical = serialize(&next_registry)) || strcmp(next, canonical)) { result = 2; goto done; }
if (!strcmp(old, next)) { result = 0; goto done; }
if (old_registry.revision == ULLONG_MAX || next_registry.revision != old_registry.revision + 1) { result = 3; goto done; }
for (i = 0; i < old_registry.count; ++i) {
for (j = 0; j < next_registry.count; ++j) if (!strcmp(old_registry.items[i].id, next_registry.items[j].id)) break;
if (j == next_registry.count || strcmp(old_registry.items[i].definition, next_registry.items[j].definition)) { result = 3; goto done; }
}
temp = holy_temporary_at(dir, temp_name);
if (temp < 0) goto done;
size = strlen(next);
while (used < size) {
ssize_t n = write(temp, next + used, size - used);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) goto done;
used += (size_t)n;
}
if (fsync(temp) || renameat(dir, temp_name, dir, "sources") || fsync(dir)) goto done;
result = 0;
done:
if (temp >= 0) close(temp);
if (dir >= 0) { if (temp_name[0]) unlinkat(dir, temp_name, 0); close(dir); }
if (input >= 0) close(input);
if (snapshot) { unlink(snapshot); free(snapshot); }
if (result) fprintf(stderr, "holypkg: source apply failed (status %d)\n", result);
else printf("source-registry revision %llu\n", next_registry.revision);
free(data); free(old); free(canonical);
clear_registry(&old_registry); clear_registry(&next_registry);
return result;
}
int holy_source_list(const char *root)
{
struct registry r = {0};
char *data = NULL;
size_t i;
unsigned long long generation;
int result, dir = holy_state_lock(root, 0, &generation, &result);
if (dir < 0) { fprintf(stderr, "holypkg: source registry unavailable (status %d)\n", result); return result; }
data = load_registry(dir, &r);
result = data ? 0 : 1;
if (data) {
if (r.count) qsort(r.items, r.count, sizeof *r.items, source_order);
for (i = 0; i < r.count; ++i) {
printf("source %s ", r.items[i].id); quote(stdout, r.items[i].alias);
printf(" %s\n", r.items[i].active ? "active" : "inactive");
}
printf("revision %llu sources %zu\n", r.revision, r.count);
if (ferror(stdout)) result = 1;
}
if (result) fprintf(stderr, "holypkg: source registry unavailable (status %d)\n", result);
close(dir); free(data); clear_registry(&r);
return result;
}
int holy_source_show(const char *root, const char *alias)
{
struct registry r = {0};
unsigned long long generation;
char *data = NULL;
size_t i;
int dir, result = 1;
if (!root || !alias || !*alias) return 2;
dir = holy_state_lock(root, 0, &generation, &result);
if (dir < 0) return result;
data = load_registry(dir, &r);
if (!data) goto done;
result = 6;
for (i = 0; i < r.count; ++i) if (r.items[i].active && !strcmp(r.items[i].alias, alias)) {
const struct source *s = &r.items[i];
char fingerprint[65];
if (!key_fingerprint(s->key, fingerprint)) { result = 1; break; }
printf("source-id %s\nalias ", s->id); quote(stdout, s->alias);
fputs("\ndefinition\n", stdout);
fputs(s->definition, stdout);
printf("trust %s\npublic-key-sha256 %s\nparent-id %s\nfamily ",
s->trust, fingerprint, s->parent[0] ? s->parent : "-");
quote(stdout, s->family ? s->family : "-");
printf("\npriority %d\nrevision %llu\n", s->priority, r.revision);
result = ferror(stdout) ? 1 : 0;
break;
}
done:
if (result) fprintf(stderr, "holypkg: source %s unavailable (status %d)\n", alias, result);
free(data); clear_registry(&r); close(dir);
return result;
}
static int alias_order(const void *a, const void *b)
{
return strcmp(*(char *const *)a, *(char *const *)b);
}
int holy_source_active_aliases(const char *root, char ***aliases, size_t *count)
{
struct registry r = {0};
unsigned long long generation;
char *data = NULL;
char **items = NULL;
size_t i, used = 0;
int dir, result = 1;
if (!aliases || !count) return 2;
*aliases = NULL;
*count = 0;
dir = holy_state_lock(root, 0, &generation, &result);
if (dir < 0) return result;
data = load_registry(dir, &r);
if (!data) goto done;
items = calloc(r.count ? r.count : 1, sizeof *items);
if (!items) goto done;
for (i = 0; i < r.count; ++i) if (r.items[i].active) {
items[used] = strdup(r.items[i].alias);
if (!items[used]) goto done;
++used;
}
qsort(items, used, sizeof *items, alias_order);
*aliases = items;
*count = used;
items = NULL;
result = 0;
done:
if (items) {
for (i = 0; i < used; ++i) free(items[i]);
free(items);
}
free(data); clear_registry(&r); close(dir);
return result;
}
int holy_source_active_id(const char *root, const char *alias, char output[65])
{
struct registry registry = {0};
unsigned long long generation;
char *data = NULL;
size_t i;
int dir, result = 1;
output[0] = 0;
if (!alias || !*alias || !strcmp(alias, "local")) return 2;
dir = holy_state_lock(root, 0, &generation, &result);
if (dir < 0) return result;
result = 1;
data = load_registry(dir, ®istry);
if (!data) goto done;
result = 6;
for (i = 0; i < registry.count; ++i)
if (registry.items[i].active && !strcmp(registry.items[i].alias, alias)) {
memcpy(output, registry.items[i].id, 65);
result = 0;
break;
}
done:
if (result) {
fputs("holypkg: active source unavailable: ", stderr);
quote(stderr, alias); fputc('\n', stderr);
}
free(data); clear_registry(®istry); close(dir);
return result;
}
int holy_source_type(const char *root, const char *alias, char **type)
{
struct registry registry = {0};
unsigned long long generation;
char *data = NULL;
size_t i;
int dir, result = 1;
if (!root || !alias || !*alias || !type) return 2;
*type = NULL;
dir = holy_state_lock(root, 0, &generation, &result);
if (dir < 0) return result;
data = load_registry(dir, ®istry);
if (!data) goto done;
result = 6;
for (i = 0; i < registry.count; ++i)
if (registry.items[i].active && !strcmp(registry.items[i].alias, alias)) {
const char *line = registry.items[i].definition;
while (*line) {
const char *end = strchr(line, '\n');
char **v = NULL;
size_t n = 0;
if (!end || !tokens(line, (size_t)(end - line), &v, &n)) {
holy_tokens_free(v, n); result = 2; goto done;
}
if (n == 2 && !strcmp(v[0], "type")) {
*type = strdup(v[1]);
holy_tokens_free(v, n);
result = *type ? 0 : 1;
goto done;
}
holy_tokens_free(v, n);
line = end + 1;
}
result = 2;
break;
}
done:
free(data); clear_registry(®istry); close(dir);
return result;
}
int holy_source_parent_id(const char *root, const char *id, char output[65])
{
struct registry registry = {0};
unsigned long long generation;
char *data = NULL;
size_t i;
int dir, result = 1;
if (!root || !id || !valid_hash(id) || !output) return 2;
output[0] = 0;
dir = holy_state_lock(root, 0, &generation, &result);
if (dir < 0) return result;
data = load_registry(dir, ®istry);
if (!data) goto done;
result = 6;
for (i = 0; i < registry.count; ++i)
if (!strcmp(registry.items[i].id, id)) {
memcpy(output, registry.items[i].parent, 65);
result = 0; break;
}
done:
free(data); clear_registry(®istry); close(dir);
return result;
}
int holy_source_rank_info(const char *root, const char *id,
char **family, int *priority)
{
struct registry registry = {0};
unsigned long long generation;
char *data = NULL;
size_t i;
int dir, result = 1;
if (!root || !id || !valid_hash(id) || !family || !priority) return 2;
*family = NULL; *priority = 0;
dir = holy_state_lock(root, 0, &generation, &result);
if (dir < 0) return result;
data = load_registry(dir, ®istry);
if (!data) goto done;
result = 6;
for (i = 0; i < registry.count; ++i)
if (!strcmp(registry.items[i].id, id)) {
*family = registry.items[i].family ? strdup(registry.items[i].family) : NULL;
*priority = registry.items[i].priority;
result = registry.items[i].family && !*family ? 1 : 0;
break;
}
done:
free(data); clear_registry(®istry); close(dir);
return result;
}
int holy_source_apk_repos(const char *root, const char *alias,
char ***repos, size_t *count)
{
struct registry registry = {0};
unsigned long long generation;
char *data = NULL, **items = NULL;
size_t i, used = 0;
int dir, result = 1, apk = 0;
if (!root || !alias || !*alias || !repos || !count) return 2;
*repos = NULL; *count = 0;
dir = holy_state_lock(root, 0, &generation, &result);
if (dir < 0) return result;
data = load_registry(dir, ®istry);
if (!data) goto done;
result = 6;
for (i = 0; i < registry.count; ++i)
if (registry.items[i].active && !strcmp(registry.items[i].alias, alias)) break;
if (i == registry.count) goto done;
{
const char *line = registry.items[i].definition;
while (*line) {
const char *end = strchr(line, '\n');
char **v = NULL;
size_t n = 0;
char **grown;
if (!end || !tokens(line, (size_t)(end - line), &v, &n)) {
holy_tokens_free(v, n); result = 2; goto done;
}
if (n == 2 && !strcmp(v[0], "type")) apk = !strcmp(v[1], "apk");
if (n == 3 && !strcmp(v[0], "repo")) {
grown = realloc(items, (used + 1) * sizeof *items);
if (!grown) { holy_tokens_free(v, n); result = 1; goto done; }
items = grown;
items[used] = strdup(v[1]);
if (!items[used]) { holy_tokens_free(v, n); result = 1; goto done; }
++used;
}
holy_tokens_free(v, n);
line = end + 1;
}
}
if (!apk || !used) goto done;
qsort(items, used, sizeof *items, alias_order);
*repos = items; *count = used;
items = NULL; result = 0;
done:
if (items) {
for (i = 0; i < used; ++i) free(items[i]);
free(items);
}
free(data); clear_registry(®istry); close(dir);
return result;
}
int holy_source_known_id(const char *root, const char *alias, char output[65])
{
struct registry registry = {0};
unsigned long long generation;
char *data = NULL;
size_t i, matches = 0;
int dir, result = 1, active = 0;
output[0] = 0;
if (!alias || !*alias || !strcmp(alias, "local")) return 2;
dir = holy_state_lock(root, 0, &generation, &result);
if (dir < 0) return result;
data = load_registry(dir, ®istry);
if (!data) goto done;
result = 6;
for (i = 0; i < registry.count; ++i) {
if (strcmp(registry.items[i].alias, alias)) continue;
if (registry.items[i].active) {
memcpy(output, registry.items[i].id, 65);
active = 1;
matches = 1;
break;
}
if (++matches == 1) memcpy(output, registry.items[i].id, 65);
}
if (active || matches == 1) result = 0;
else if (matches > 1) result = 3;
done:
if (result) fprintf(stderr, "holypkg: source alias %s for installed package: %s\n",
result == 3 ? "ambiguous" : "unavailable", alias);
free(data); clear_registry(®istry); close(dir);
return result;
}
static char *native_endpoint(const char *definition, int *git)
{
const char *line = definition;
char *url = NULL;
int native = 0, unsupported = 0;
if (git) *git = 0;
while (*line) {
const char *end = strchr(line, '\n');
char **v = NULL;
size_t n = 0;
if (!end || !tokens(line, (size_t)(end - line), &v, &n)) {
holy_tokens_free(v, n);
free(url);
return NULL;
}
if (n == 2 && !strcmp(v[0], "type")) {
native = !strcmp(v[1], "holy-http") || !strcmp(v[1], "holy-git");
if (git) *git = !strcmp(v[1], "holy-git");
}
else if (n == 2 && !strcmp(v[0], "url")) {
free(url);
url = strdup(v[1]);
if (!url) { holy_tokens_free(v, n); return NULL; }
} else if (n == 3 && !strcmp(v[0], "repo")) unsupported = 1;
holy_tokens_free(v, n);
line = end + 1;
}
if (!native || unsupported) { free(url); return NULL; }
return url;
}
int holy_source_apk_repo(const char *root, const char *alias, const char *repo,
char id[65], char **url, char **trust, char key[65])
{
struct registry registry = {0};
char *data = NULL, *selected = NULL;
const char *line;
unsigned long long generation;
size_t i;
int dir, result = 1, apk = 0;
id[0] = 0;
if (key) key[0] = 0;
*url = NULL;
*trust = NULL;
if (!alias || !*alias || !strcmp(alias, "local") || !repo || !*repo) return 2;
dir = holy_state_lock(root, 0, &generation, &result);
if (dir < 0) return result;
data = load_registry(dir, ®istry);
if (!data) goto done;
result = 6;
for (i = 0; i < registry.count; ++i)
if (registry.items[i].active && !strcmp(registry.items[i].alias, alias)) break;
if (i == registry.count) goto done;
line = registry.items[i].definition;
while (*line) {
const char *end = strchr(line, '\n');
char **v = NULL;
size_t n = 0;
if (!end || !tokens(line, (size_t)(end - line), &v, &n)) {
holy_tokens_free(v, n); result = 2; goto done;
}
if (n == 2 && !strcmp(v[0], "type")) apk = !strcmp(v[1], "apk");
if (n == 3 && !strcmp(v[0], "repo") && !strcmp(v[1], repo)) {
if (selected) { holy_tokens_free(v, n); result = 2; goto done; }
selected = strdup(v[2]);
if (!selected) { holy_tokens_free(v, n); result = 1; goto done; }
}
holy_tokens_free(v, n);
line = end + 1;
}
if (!apk || !selected || strncmp(selected, "https://", 8) ||
selected[strlen(selected) - 1] != '/') goto done;
*trust = strdup(registry.items[i].trust);
if (!*trust) { result = 1; goto done; }
*url = selected; selected = NULL;
memcpy(id, registry.items[i].id, 65);
if (key) memcpy(key, registry.items[i].key, 65);
result = 0;
done:
if (result) { free(*url); free(*trust); *url = NULL; *trust = NULL; id[0] = 0; if (key) key[0] = 0; }
free(selected); free(data); clear_registry(®istry); close(dir);
return result;
}
int holy_source_apt(const char *root, const char *alias,
char id[65], char **url, char **trust, char key[65])
{
struct registry registry = {0};
char *data = NULL, *selected = NULL;
const char *line;
unsigned long long generation;
size_t i;
int dir, result = 1, apt = 0, repos = 0;
id[0] = key[0] = 0;
*url = *trust = NULL;
if (!root || !alias || !*alias || !strcmp(alias, "local")) return 2;
dir = holy_state_lock(root, 0, &generation, &result);
if (dir < 0) return result;
data = load_registry(dir, ®istry);
if (!data) goto done;
result = 6;
for (i = 0; i < registry.count; ++i)
if (registry.items[i].active && !strcmp(registry.items[i].alias, alias)) break;
if (i == registry.count) goto done;
line = registry.items[i].definition;
while (*line) {
const char *end = strchr(line, '\n');
char **v = NULL;
size_t n = 0;
if (!end || !tokens(line, (size_t)(end - line), &v, &n)) {
holy_tokens_free(v, n); result = 2; goto done;
}
if (n == 2 && !strcmp(v[0], "type")) apt = !strcmp(v[1], "apt");
else if (n == 2 && !strcmp(v[0], "url")) {
free(selected); selected = strdup(v[1]);
if (!selected) { holy_tokens_free(v, n); result = 1; goto done; }
} else if (n == 3 && !strcmp(v[0], "repo")) repos = 1;
holy_tokens_free(v, n);
line = end + 1;
}
if (!apt || repos || !selected || strncmp(selected, "https://", 8) ||
selected[strlen(selected) - 1] != '/') goto done;
*trust = strdup(registry.items[i].trust);
if (!*trust) { result = 1; goto done; }
*url = selected; selected = NULL;
memcpy(id, registry.items[i].id, 65);
memcpy(key, registry.items[i].key, 65);
result = 0;
done:
if (result) {
free(*url); free(*trust); *url = *trust = NULL;
id[0] = key[0] = 0;
}
free(selected); free(data); clear_registry(®istry); close(dir);
return result;
}
int holy_source_xbps(const char *root, const char *alias,
char id[65], char **url, char **trust, char key[65])
{
struct registry registry = {0};
char *data = NULL, *selected = NULL;
const char *line;
unsigned long long generation;
size_t i;
int dir, result = 1, xbps = 0, repos = 0;
id[0] = key[0] = 0;
*url = *trust = NULL;
if (!root || !alias || !*alias || !strcmp(alias, "local")) return 2;
dir = holy_state_lock(root, 0, &generation, &result);
if (dir < 0) return result;
data = load_registry(dir, ®istry);
if (!data) goto done;
result = 6;
for (i = 0; i < registry.count; ++i)
if (registry.items[i].active && !strcmp(registry.items[i].alias, alias)) break;
if (i == registry.count) goto done;
line = registry.items[i].definition;
while (*line) {
const char *end = strchr(line, '\n');
char **v = NULL;
size_t n = 0;
if (!end || !tokens(line, (size_t)(end - line), &v, &n)) {
holy_tokens_free(v, n); result = 2; goto done;
}
if (n == 2 && !strcmp(v[0], "type")) xbps = !strcmp(v[1], "xbps");
else if (n == 2 && !strcmp(v[0], "url")) {
if (selected) { holy_tokens_free(v, n); result = 2; goto done; }
selected = strdup(v[1]);
if (!selected) { holy_tokens_free(v, n); result = 1; goto done; }
} else if (n == 3 && !strcmp(v[0], "repo")) repos = 1;
holy_tokens_free(v, n);
line = end + 1;
}
if (!xbps || repos || !selected || strncmp(selected, "https://", 8) ||
selected[strlen(selected) - 1] != '/') goto done;
*trust = strdup(registry.items[i].trust);
if (!*trust) { result = 1; goto done; }
*url = selected; selected = NULL;
memcpy(id, registry.items[i].id, 65);
memcpy(key, registry.items[i].key, 65);
result = 0;
done:
if (result) {
free(*url); free(*trust); *url = *trust = NULL;
id[0] = key[0] = 0;
}
free(selected); free(data); clear_registry(®istry); close(dir);
return result;
}
/* rpm-md mirrors one https base and carries no repository list or key. */
int holy_source_rpm_md(const char *root, const char *alias,
char id[65], char **url, char **trust)
{
struct registry registry = {0};
char *data = NULL, *selected = NULL;
const char *line;
unsigned long long generation;
size_t i;
int dir, result = 1, rpm = 0, repos = 0;
id[0] = 0;
*url = *trust = NULL;
if (!root || !alias || !*alias || !strcmp(alias, "local")) return 2;
dir = holy_state_lock(root, 0, &generation, &result);
if (dir < 0) return result;
data = load_registry(dir, ®istry);
if (!data) goto done;
result = 6;
for (i = 0; i < registry.count; ++i)
if (registry.items[i].active && !strcmp(registry.items[i].alias, alias)) break;
if (i == registry.count) goto done;
line = registry.items[i].definition;
while (*line) {
const char *end = strchr(line, '\n');
char **v = NULL;
size_t n = 0;
if (!end || !tokens(line, (size_t)(end - line), &v, &n)) {
holy_tokens_free(v, n); result = 2; goto done;
}
if (n == 2 && !strcmp(v[0], "type")) rpm = !strcmp(v[1], "rpm-md");
else if (n == 2 && !strcmp(v[0], "url")) {
if (selected) { holy_tokens_free(v, n); result = 2; goto done; }
selected = strdup(v[1]);
if (!selected) { holy_tokens_free(v, n); result = 1; goto done; }
} else if (n == 3 && !strcmp(v[0], "repo")) repos = 1;
holy_tokens_free(v, n);
line = end + 1;
}
if (!rpm || repos || !selected || strncmp(selected, "https://", 8) ||
selected[strlen(selected) - 1] != '/') goto done;
*trust = strdup(registry.items[i].trust);
if (!*trust) { result = 1; goto done; }
*url = selected; selected = NULL;
memcpy(id, registry.items[i].id, 65);
result = 0;
done:
if (result) {
free(*url); free(*trust); *url = *trust = NULL;
id[0] = 0;
}
free(selected); free(data); clear_registry(®istry); close(dir);
return result;
}
int holy_source_catalog(const char *root, const char *alias,
const char *catalog, char source_id[65])
{
struct registry registry = {0};
unsigned long long generation;
char *data = NULL, *url = NULL, key_spec[69];
size_t i;
int dir, result = 1, git = 0;
source_id[0] = 0;
if (!alias || !*alias || !strcmp(alias, "local") || !catalog || !*catalog) return 2;
dir = holy_state_lock(root, 0, &generation, &result);
if (dir < 0) return result;
result = 1;
data = load_registry(dir, ®istry);
if (!data) goto done;
result = 6;
for (i = 0; i < registry.count; ++i)
if (registry.items[i].active && !strcmp(registry.items[i].alias, alias)) break;
if (i == registry.count) goto done;
url = native_endpoint(registry.items[i].definition, &git);
if (!url) goto done;
if (!holy_repo_source_catalog(catalog, registry.items[i].id, url,
source_public_key(®istry.items[i], key_spec)) ||
(git && !holy_git_catalog_commit(catalog, NULL))) goto done;
memcpy(source_id, registry.items[i].id, 65);
result = 0;
done:
if (result) {
fputs("holypkg: source catalog unavailable for ", stderr);
quote(stderr, alias); fputc('\n', stderr);
}
free(url); free(data); clear_registry(®istry); close(dir);
return result;
}
static int catalogs_dir(int database, int create)
{
struct stat st;
int fd;
if (create && mkdirat(database, "catalogs", 0700) && errno != EEXIST)
return -1;
if (create && fsync(database)) return -1;
fd = openat(database, "catalogs", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0) return -1;
if (fstat(fd, &st) || !S_ISDIR(st.st_mode) ||
(st.st_uid != 0 && st.st_uid != geteuid()) || (st.st_mode & 0022)) {
close(fd); return -1;
}
return fd;
}
static int read_catalog_binding(int database, const char *id,
char index[65], char **path, int *root_relative)
{
struct stat st;
char *data = NULL, *line, **v = NULL;
size_t n = 0;
int catalogs = -1, fd = -1, ok = 0;
*path = NULL;
*root_relative = 0;
index[0] = 0;
catalogs = catalogs_dir(database, 0);
if (catalogs < 0) goto done;
fd = openat(catalogs, id, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) ||
(st.st_uid != 0 && st.st_uid != geteuid()) || (st.st_mode & 0022) ||
st.st_size > 1024 * 1024) goto done;
data = read_fd(fd);
if (!data || strncmp(data, "format holy-source-catalog-1\n", 29)) goto done;
line = data + 29;
{
char *end = strchr(line, '\n');
if (!end || !tokens(line, (size_t)(end - line), &v, &n) ||
n != 2 || strcmp(v[0], "index") || !valid_hash(v[1])) goto done;
memcpy(index, v[1], 65);
holy_tokens_free(v, n); v = NULL; n = 0;
line = end + 1;
}
{
char *end = strchr(line, '\n');
if (!end || end[1] || !tokens(line, (size_t)(end - line), &v, &n) ||
n != 2 || (strcmp(v[0], "path") && strcmp(v[0], "root-path")) ||
v[1][0] != '/') goto done;
*root_relative = !strcmp(v[0], "root-path");
*path = strdup(v[1]);
ok = *path != NULL;
}
done:
holy_tokens_free(v, n);
free(data);
if (fd >= 0) close(fd);
if (catalogs >= 0) close(catalogs);
if (!ok) { free(*path); *path = NULL; index[0] = 0; }
return ok;
}
int holy_source_bind_catalog(const char *root, const char *alias,
const char *catalog)
{
struct registry registry = {0};
char *data = NULL, *url = NULL, *path = NULL, *root_path = NULL, *record = NULL;
char index[65], key_spec[69], temp_name[43] = {0};
size_t i, size = 0, used = 0;
unsigned long long generation;
FILE *out = NULL;
int database = -1, catalogs = -1, temp = -1, result = 1, git = 0;
if (!alias || !*alias || !strcmp(alias, "local") || !catalog || !*catalog)
return 2;
database = holy_state_lock(root, 1, &generation, &result);
if (database < 0) return result;
data = load_registry(database, ®istry);
if (!data) goto done;
result = 6;
for (i = 0; i < registry.count; ++i)
if (registry.items[i].active && !strcmp(registry.items[i].alias, alias)) break;
if (i == registry.count) goto done;
url = native_endpoint(registry.items[i].definition, &git);
path = realpath(catalog, NULL);
if (!url || !path || !holy_repo_source_catalog(path, registry.items[i].id, url,
source_public_key(®istry.items[i], key_spec)) ||
(git && !holy_git_catalog_commit(path, NULL)) ||
!holy_repo_catalog_index(path, index)) goto done;
result = 1;
out = open_memstream(&record, &size);
if (!out) goto done;
root_path = realpath(root, NULL);
if (!root_path) goto done;
fprintf(out, "format holy-source-catalog-1\nindex %s\n%s ", index,
strcmp(root_path, "/") && !strncmp(path, root_path, strlen(root_path)) &&
path[strlen(root_path)] == '/' ? "root-path" : "path");
quote(out, strcmp(root_path, "/") && !strncmp(path, root_path, strlen(root_path)) &&
path[strlen(root_path)] == '/' ? path + strlen(root_path) : path);
fputc('\n', out);
{
int failed = ferror(out);
if (fclose(out)) failed = 1;
out = NULL;
if (failed) goto done;
}
catalogs = catalogs_dir(database, 1);
if (catalogs < 0) goto done;
temp = holy_temporary_at(catalogs, temp_name);
if (temp < 0) goto done;
while (used < size) {
ssize_t n = write(temp, record + used, size - used);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) goto done;
used += (size_t)n;
}
if (fsync(temp) || renameat(catalogs, temp_name, catalogs,
registry.items[i].id) || fsync(catalogs)) goto done;
result = 0;
printf("catalog-bound %s index %s\n", registry.items[i].id, index);
done:
if (out) fclose(out);
if (temp >= 0) close(temp);
if (catalogs >= 0) {
if (temp_name[0]) unlinkat(catalogs, temp_name, 0);
close(catalogs);
}
if (database >= 0) close(database);
free(data); free(url); free(path); free(root_path); free(record);
clear_registry(®istry);
if (result) fprintf(stderr, "holypkg: source catalog binding failed (status %d)\n", result);
return result;
}
static int source_catalog_path(const char *root, const char *alias,
char **path, int fast)
{
struct registry registry = {0};
char *data = NULL, *url = NULL, *saved = NULL, *joined = NULL, *canonical = NULL;
char expected[65], actual[65], key_spec[69];
size_t i;
unsigned long long generation;
int database = -1, result = 1, root_relative = 0, git = 0;
*path = NULL;
if (!alias || !*alias || !strcmp(alias, "local")) return 2;
database = holy_state_lock(root, 0, &generation, &result);
if (database < 0) return result;
data = load_registry(database, ®istry);
if (!data) goto done;
result = 6;
for (i = 0; i < registry.count; ++i)
if (registry.items[i].active && !strcmp(registry.items[i].alias, alias)) break;
if (i == registry.count) goto done;
url = native_endpoint(registry.items[i].definition, &git);
if (!url || !read_catalog_binding(database, registry.items[i].id,
expected, &saved, &root_relative)) goto done;
if (root_relative) {
size_t a, b;
canonical = realpath(root, NULL);
if (!canonical) goto done;
a = strlen(canonical); b = strlen(saved);
if (a > (size_t)-1 - b - 1) goto done;
joined = malloc(a + b + 1);
if (!joined) goto done;
if (!strcmp(canonical, "/")) memcpy(joined, saved, b + 1);
else {
memcpy(joined, canonical, a);
memcpy(joined + a, saved, b + 1);
}
free(saved);
saved = realpath(joined, NULL);
if (!saved || (strcmp(canonical, "/") &&
(strncmp(saved, canonical, a) || saved[a] != '/'))) goto done;
}
if (
!holy_repo_source_catalog(saved, registry.items[i].id, url,
source_public_key(®istry.items[i], key_spec)) ||
(git && !holy_git_catalog_commit(saved, NULL)) ||
!(fast ? holy_repo_catalog_index_fast(saved, actual) :
holy_repo_catalog_index(saved, actual)) ||
strcmp(expected, actual)) goto done;
*path = saved;
saved = NULL;
result = 0;
done:
if (result) fprintf(stderr, "holypkg: bound catalog unavailable for source ");
if (result) { quote(stderr, alias); fputc('\n', stderr); }
free(saved); free(joined); free(canonical); free(url); free(data);
clear_registry(®istry);
if (database >= 0) close(database);
return result;
}
int holy_source_catalog_path(const char *root, const char *alias, char **path)
{
return source_catalog_path(root, alias, path, 0);
}
int holy_source_catalog_path_fast(const char *root, const char *alias, char **path)
{
return source_catalog_path(root, alias, path, 1);
}
static int sync_catalog_parent(const char *root, const char *id, char **path)
{
static const char *const parts[] = {"var", "cache", "holypkg", "catalogs"};
char *canonical = NULL, *name = NULL;
struct stat st;
size_t i, length;
int fd = -1;
*path = NULL;
canonical = realpath(root, NULL);
if (!canonical) return -1;
length = strlen(canonical);
if (length > (size_t)-1 - strlen(id) - 31) goto done;
name = malloc(length + strlen(id) + 31);
if (!name) goto done;
snprintf(name, length + strlen(id) + 31,
"%s/var/cache/holypkg/catalogs/%s", canonical, id);
fd = open(canonical, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0) goto done;
for (i = 0; i <= sizeof parts / sizeof *parts; ++i) {
int next;
const char *part = i == sizeof parts / sizeof *parts ? id : parts[i];
if (fstat(fd, &st) || !S_ISDIR(st.st_mode) ||
(st.st_uid != geteuid() && st.st_uid != 0) || (st.st_mode & 0022))
goto done;
if (mkdirat(fd, part, 0700)) {
if (errno != EEXIST) goto done;
} else if (fsync(fd)) goto done;
next = openat(fd, part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (next < 0) goto done;
close(fd);
fd = next;
}
if (fstat(fd, &st) || !S_ISDIR(st.st_mode) ||
(st.st_uid != geteuid() && st.st_uid != 0) || (st.st_mode & 0022))
goto done;
*path = name;
name = NULL;
done:
free(name);
free(canonical);
if (!*path && fd >= 0) { close(fd); fd = -1; }
return fd;
}
static int sync_bound_catalog(const char *alias, const char *root, const char *url,
const char *id, const char *digest, const char *ca_file,
int current_accepted, const char *public_key,
const char *commit)
{
char *parent_path = NULL, *catalog = NULL, *temporary = NULL;
char actual[65];
struct stat st;
size_t length;
int parent = -1, result = 1;
parent = sync_catalog_parent(root, id, &parent_path);
if (parent < 0 || flock(parent, LOCK_EX)) goto done;
length = strlen(parent_path);
if (length > (size_t)-1 - 80) goto done;
catalog = malloc(length + 66);
temporary = malloc(length + 20);
if (!catalog || !temporary) goto done;
snprintf(catalog, length + 66, "%s/%s", parent_path, digest);
if (fstatat(parent, digest, &st, AT_SYMLINK_NOFOLLOW) == 0) {
if (!S_ISDIR(st.st_mode) ||
!holy_repo_source_catalog(catalog, id, url, public_key) ||
(commit && !holy_git_catalog_commit(catalog, commit)) ||
!holy_repo_catalog_index(catalog, actual) || strcmp(actual, digest)) {
result = 6; goto done;
}
} else {
if (errno != ENOENT) goto done;
snprintf(temporary, length + 20, "%s/.sync-XXXXXX", parent_path);
if (!mkdtemp(temporary) || rmdir(temporary)) goto done;
result = commit ? holy_git_mirror_source(url, commit, digest, temporary,
id, public_key, ca_file) :
public_key ? holy_repo_mirror_source_signed(url, digest, temporary,
ca_file, id, public_key) :
holy_repo_mirror_source(url, digest, temporary, ca_file, id,
current_accepted);
if (result) {
fprintf(stderr, "holypkg: incomplete catalog retained at %s\n", temporary);
goto done;
}
result = 1;
if (renameat(parent, strrchr(temporary, '/') + 1, parent, digest) ||
fsync(parent)) goto done;
}
result = holy_source_bind_catalog(root, alias, catalog);
done:
if (result) fprintf(stderr, "holypkg: source catalog sync failed (status %d)\n", result);
if (parent >= 0) close(parent);
free(parent_path); free(catalog); free(temporary);
return result;
}
int holy_source_sync(const char *alias, const char *root, const char *digest,
const char *accepted_unsigned, const char *output,
const char *ca_file, const char *commit)
{
struct registry registry = {0};
char *data = NULL, *url = NULL;
char source_id[65] = {0}, key_spec[69];
char current[65];
const char *public_key = NULL;
unsigned long long generation;
int dir, result = 1, git = 0;
size_t i;
if (!alias || !*alias || !strcmp(alias, "local") ||
(digest && accepted_unsigned) ||
(digest && !valid_hash(digest)) ||
(accepted_unsigned && !valid_hash(accepted_unsigned)) ||
(commit && ((strlen(commit) != 40 && strlen(commit) != 64) ||
strspn(commit, "0123456789abcdef") != strlen(commit))) ||
(output && !*output)) return 2;
dir = holy_state_lock(root, 0, &generation, &result);
if (dir < 0) return result;
result = 1;
data = load_registry(dir, ®istry);
if (!data) goto done;
result = 6;
for (i = 0; i < registry.count; ++i)
if (registry.items[i].active && !strcmp(registry.items[i].alias, alias)) break;
if (i == registry.count) {
fputs("holypkg: active source unavailable: ", stderr);
quote(stderr, alias); fputc('\n', stderr);
goto done;
}
url = native_endpoint(registry.items[i].definition, &git);
if (!url) {
fputs("holypkg: source ", stderr);
quote(stderr, alias); fputs(" requires a single native URL\n", stderr);
goto done;
}
if ((git && (!commit || !digest || accepted_unsigned)) ||
(!git && commit)) { result = 2; goto done; }
public_key = source_public_key(®istry.items[i], key_spec);
if (public_key && accepted_unsigned) {
fputs("holypkg: signed source cannot accept unsigned current\n", stderr);
result = 2; goto done;
}
if (!public_key && !strcmp(registry.items[i].trust, "require")) {
fputs("holypkg: source trust require needs a registered public key\n", stderr);
result = 6; goto done;
}
memcpy(source_id, registry.items[i].id, sizeof source_id);
close(dir); dir = -1;
if (!digest) {
result = holy_fetch_https_current(url, ca_file, current);
if (result) goto done;
if (!public_key && (!accepted_unsigned || strcmp(accepted_unsigned, current))) {
fprintf(stderr, "holypkg: decision-required unsigned current source=%s index=%s; --accept-unsigned %s confirms this generation\n",
source_id, current, current);
result = 3; goto done;
}
digest = current;
}
result = output ? (git ? holy_git_mirror_source(url, commit, digest, output,
source_id, public_key, ca_file) :
public_key ? holy_repo_mirror_source_signed(url, digest,
output, ca_file, source_id, public_key) :
holy_repo_mirror_source(url, digest, output, ca_file,
source_id, accepted_unsigned != NULL)) :
sync_bound_catalog(alias, root, url, source_id, digest, ca_file,
accepted_unsigned != NULL, public_key, commit);
if (!result) printf("synced source %s index %s\n", source_id, digest);
done:
if (dir >= 0) close(dir);
free(data); free(url); clear_registry(®istry);
return result;
}
int holy_source_record(int database, const char *id, char **record, char registry[65])
{
struct registry r = {0};
char *data = load_registry(database, &r), *result = NULL;
size_t i, size = 0;
int status = 1;
FILE *out = NULL;
*record = NULL;
if (!data || !hash(data, registry)) goto done;
status = 6;
for (i = 0; i < r.count; ++i) if (!strcmp(r.items[i].id, id)) break;
if (i == r.count || !r.items[i].active) goto done;
status = 1;
out = open_memstream(&result, &size);
if (!out) goto done;
fprintf(out, "source %s ", id); quote(out, r.items[i].alias); fputc('\n', out);
status = ferror(out) ? 1 : 0;
if (fclose(out)) status = 1;
if (size > 16 * 1024 * 1024) status = 1;
done:
if (!status) *record = result; else free(result);
free(data); clear_registry(&r);
return status;
}
int holy_source_instance(int instance, char id[65], char digest[65])
{
struct stat st;
char *data = NULL, **v = NULL;
size_t count = 0;
int fd = openat(instance, "source", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC), ok = 0;
if (fd < 0 || fstat(fd, &st) || (st.st_mode & 0022) ||
(st.st_uid != 0 && st.st_uid != geteuid())) goto done;
data = read_fd(fd);
if (!data || !tokens(data, strlen(data), &v, &count) || count != 3 ||
strcmp(v[0], "source") || !valid_hash(v[1]) || !v[2][0] ||
!strcmp(v[2], "local") || !hash(data, digest)) goto done;
memcpy(id, v[1], 65);
ok = 1;
done:
if (fd >= 0) close(fd);
free(data); holy_tokens_free(v, count);
return ok;
}