_ _
| |_ ___| |_ _
| | . | | | |
|_|_|___|_|_ |
|___|
git mirror - github.com/owenewans/holy - branch master
file backends/xbps.c
#define _XOPEN_SOURCE 700
#include "xbps.h"
#include "../src/fetch.h"
#include "../src/stage.h"
#include "../src/source.h"
#include "../src/state.h"
#include "../src/import.h"
#include "../src/package.h"
#include "../src/provides.h"
#include <archive.h>
#include <archive_entry.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
#ifdef __TINYC__
#define __llvm__ 1
#endif
#include <plist/plist.h>
#ifdef __TINYC__
#undef __llvm__
#endif
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
struct xbps_row {
char *name, *version, *arch, *hash;
char **sonames;
size_t soname_count;
unsigned long long size;
};
struct xbps_capability {
const char *soname;
const struct xbps_row *row;
};
static int row_order(const void *left, const void *right);
static int label(const char *value)
{
const unsigned char *p = (const unsigned char *)value;
if (!p || !*p || strlen(value) > 255 || !isalnum(*p)) return 0;
for (; *p; ++p)
if (!isalnum(*p) && *p != '.' && *p != '_' && *p != '+' && *p != '-' && *p != '~') return 0;
return 1;
}
static int digest_label(const char *value)
{
return value && strlen(value) == 64 && strspn(value, "0123456789abcdef") == 64;
}
static int hash_fd(int fd, char result[65])
{
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
unsigned char digest[32], buffer[65536];
unsigned length;
ssize_t got;
size_t i;
int ok = 0;
if (!ctx || lseek(fd, 0, SEEK_SET) < 0 || EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1) goto done;
while ((got = read(fd, buffer, sizeof buffer)) > 0)
if (EVP_DigestUpdate(ctx, buffer, (size_t)got) != 1) goto done;
if (got || EVP_DigestFinal_ex(ctx, digest, &length) != 1 || length != 32) goto done;
for (i = 0; i < 32; ++i) snprintf(result + i * 2, 3, "%02x", digest[i]);
ok = 1;
done:
EVP_MD_CTX_free(ctx);
return ok;
}
static char *string(plist_t dict, const char *key)
{
plist_t node = plist_dict_get_item(dict, key);
char *value = NULL;
if (node && plist_get_node_type(node) == PLIST_STRING) plist_get_string_val(node, &value);
return value;
}
static EVP_PKEY *public_key_bytes(const char *data, size_t size)
{
BIO *bio;
EVP_PKEY *key;
if (!data || !size || size > 16384) return NULL;
bio = BIO_new_mem_buf(data, (int)size);
if (!bio) return NULL;
key = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL);
BIO_free(bio);
if (key && EVP_PKEY_base_id(key) != EVP_PKEY_RSA) {
EVP_PKEY_free(key); return NULL;
}
return key;
}
static EVP_PKEY *public_key_file(const char *path)
{
struct stat st;
char *data = NULL;
EVP_PKEY *key = NULL;
size_t used = 0;
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size <= 0 ||
st.st_size > 16384) goto done;
data = malloc((size_t)st.st_size);
if (!data) goto done;
while (used < (size_t)st.st_size) {
ssize_t got = read(fd, data + used, (size_t)st.st_size - used);
if (got < 0 && errno == EINTR) continue;
if (got <= 0) goto done;
used += (size_t)got;
}
key = public_key_bytes(data, used);
done:
if (fd >= 0) close(fd);
free(data);
return key;
}
static int key_digest(EVP_PKEY *key, char output[65])
{
unsigned char *der = NULL, *cursor, hash[32];
unsigned length;
int size = i2d_PUBKEY(key, NULL), ok = 0;
size_t i;
if (size <= 0 || size > 16384 || !(der = malloc((size_t)size))) return 0;
cursor = der;
if (i2d_PUBKEY(key, &cursor) != size ||
EVP_Digest(der, (size_t)size, hash, &length, EVP_sha256(), NULL) != 1 ||
length != 32) goto done;
for (i = 0; i < 32; ++i) snprintf(output + i * 2, 3, "%02x", hash[i]);
ok = 1;
done:
free(der);
return ok;
}
int holy_xbps_key_fingerprint(const char *path, char output[65])
{
EVP_PKEY *key = public_key_file(path);
int ok = key && key_digest(key, output);
EVP_PKEY_free(key);
return ok;
}
static int index_key(const char *xml, size_t size, const char *trusted_path, char hash[65])
{
plist_t root = NULL, node, bits_node;
EVP_PKEY *advertised = NULL, *trusted = NULL;
char *algorithm = NULL;
const char *data;
uint64_t length = 0, bits = 0;
int ok = 0;
if (size > UINT32_MAX || plist_from_xml(xml, (uint32_t)size, &root) != PLIST_ERR_SUCCESS ||
!root || plist_get_node_type(root) != PLIST_DICT) goto done;
node = plist_dict_get_item(root, "public-key");
if (!node) { ok = !trusted_path; goto done; }
if (plist_get_node_type(node) != PLIST_DATA) goto done;
algorithm = string(root, "signature-type");
bits_node = plist_dict_get_item(root, "public-key-size");
if (bits_node && plist_get_node_type(bits_node) == PLIST_INT)
plist_get_uint_val(bits_node, &bits);
data = plist_get_data_ptr(node, &length);
if (!algorithm || strcmp(algorithm, "rsa") || !data || length > 16384 ||
!(advertised = public_key_bytes(data, (size_t)length)) ||
!bits_node || bits != (uint64_t)EVP_PKEY_bits(advertised) ||
!key_digest(advertised, hash)) goto done;
if (!trusted_path) { ok = 1; goto done; }
trusted = public_key_file(trusted_path);
ok = trusted && EVP_PKEY_eq(advertised, trusted) == 1;
done:
if (!ok) hash[0] = 0;
free(algorithm); EVP_PKEY_free(advertised); EVP_PKEY_free(trusted); plist_free(root);
return ok;
}
static int verify_signature(const char *package, const char *signature, EVP_PKEY *key)
{
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
struct stat st;
unsigned char *sig = NULL, buffer[65536];
size_t used = 0;
ssize_t got;
int package_fd = -1, signature_fd = -1, ok = 0;
signature_fd = open(signature, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
package_fd = open(package, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (!ctx || signature_fd < 0 || package_fd < 0 ||
fstat(signature_fd, &st) || !S_ISREG(st.st_mode) ||
st.st_size <= 0 || st.st_size > 16384 ||
!(sig = malloc((size_t)st.st_size)) ||
EVP_DigestVerifyInit(ctx, NULL, EVP_sha256(), NULL, key) != 1) goto done;
while (used < (size_t)st.st_size) {
got = read(signature_fd, sig + used, (size_t)st.st_size - used);
if (got < 0 && errno == EINTR) continue;
if (got <= 0) goto done;
used += (size_t)got;
}
while ((got = read(package_fd, buffer, sizeof buffer)) > 0)
if (EVP_DigestVerifyUpdate(ctx, buffer, (size_t)got) != 1) goto done;
if (got || EVP_DigestVerifyFinal(ctx, sig, used) != 1) goto done;
ok = 1;
done:
if (signature_fd >= 0) close(signature_fd);
if (package_fd >= 0) close(package_fd);
EVP_MD_CTX_free(ctx); free(sig);
return ok;
}
static void free_rows(struct xbps_row *rows, size_t count)
{
size_t i, j;
for (i = 0; i < count; ++i) {
free(rows[i].name); free(rows[i].version);
free(rows[i].arch); free(rows[i].hash);
for (j = 0; j < rows[i].soname_count; ++j) free(rows[i].sonames[j]);
free(rows[i].sonames);
}
free(rows);
}
static int soname_label(const char *value)
{
const unsigned char *p = (const unsigned char *)value;
size_t length;
if (!p || !(length = strlen(value)) || length > 255) return 0;
for (; *p; ++p) if (*p < 33 || *p > 126) return 0;
return 1;
}
static int capability_order(const void *left, const void *right)
{
const struct xbps_capability *a = left, *b = right;
int result = strcmp(a->soname, b->soname);
return result ? result : row_order(a->row, b->row);
}
static int row_order(const void *left, const void *right)
{
const struct xbps_row *a = left, *b = right;
int result = strcmp(a->name, b->name);
if (!result) result = strcmp(a->version, b->version);
if (!result) result = strcmp(a->arch, b->arch);
return result;
}
static int parse_rows(const char *xml, size_t size, struct xbps_row **out, size_t *count)
{
plist_t root = NULL, entry = NULL;
plist_dict_iter iter = NULL;
struct xbps_row *rows = NULL;
char *key = NULL;
size_t used = 0, capacity = 0, i, total_sonames = 0;
int ok = 0;
if (size > UINT32_MAX || plist_from_xml(xml, (uint32_t)size, &root) != PLIST_ERR_SUCCESS ||
!root || plist_get_node_type(root) != PLIST_DICT) goto done;
plist_dict_new_iter(root, &iter);
if (!iter) goto done;
for (;;) {
struct xbps_row *row;
char *pkgver = NULL;
plist_t bytes;
uint64_t length = 0;
plist_dict_next_item(root, iter, &key, &entry);
if (!key) break;
if (!label(key) || !entry || plist_get_node_type(entry) != PLIST_DICT || used == 100000) goto done;
if (used == capacity) {
size_t next = capacity ? capacity * 2 : 256;
struct xbps_row *grown = realloc(rows, next * sizeof *rows);
if (!grown) goto done;
rows = grown; capacity = next;
}
row = &rows[used++]; memset(row, 0, sizeof *row);
row->name = key; key = NULL;
pkgver = string(entry, "pkgver");
row->arch = string(entry, "architecture");
row->hash = string(entry, "filename-sha256");
bytes = plist_dict_get_item(entry, "filename-size");
if (bytes && plist_get_node_type(bytes) == PLIST_INT) plist_get_uint_val(bytes, &length);
if (!pkgver || strlen(pkgver) <= strlen(row->name) + 1 ||
strncmp(pkgver, row->name, strlen(row->name)) ||
pkgver[strlen(row->name)] != '-' ||
!(row->version = strdup(pkgver + strlen(row->name) + 1)) ||
!label(row->version) || !label(row->arch) || !digest_label(row->hash) ||
!bytes || plist_get_node_type(bytes) != PLIST_INT || !length ||
length > 1024ULL * 1024 * 1024 * 1024) { free(pkgver); goto done; }
row->size = length;
{
plist_t claims = plist_dict_get_item(entry, "shlib-provides");
if (claims) {
uint32_t j, amount;
if (plist_get_node_type(claims) != PLIST_ARRAY ||
(amount = plist_array_get_size(claims)) > 1024 ||
total_sonames > 1000000 - amount) { free(pkgver); goto done; }
row->sonames = calloc(amount ? amount : 1, sizeof *row->sonames);
if (!row->sonames) { free(pkgver); goto done; }
for (j = 0; j < amount; ++j) {
plist_t claim = plist_array_get_item(claims, j);
if (!claim || plist_get_node_type(claim) != PLIST_STRING) {
free(pkgver); goto done;
}
plist_get_string_val(claim, &row->sonames[j]);
if (!soname_label(row->sonames[j])) {
free(row->sonames[j]); row->sonames[j] = NULL;
free(pkgver); goto done;
}
++row->soname_count;
}
total_sonames += amount;
}
}
free(pkgver);
}
if (!used) goto done;
qsort(rows, used, sizeof *rows, row_order);
for (i = 1; i < used; ++i)
if (!row_order(&rows[i-1], &rows[i])) goto done;
*out = rows; *count = used; rows = NULL; used = 0; ok = 1;
done:
free(key); free(iter); plist_free(root); free_rows(rows, used);
return ok;
}
static int read_repodata(const char *path, char **xml, size_t *size,
char **meta, size_t *meta_size)
{
struct archive *archive = archive_read_new();
struct archive_entry *entry;
int status, found = 0, metadata = 0, ok = 0;
if (!archive) return 0;
if (archive_read_support_filter_zstd(archive) != ARCHIVE_OK ||
archive_read_support_filter_gzip(archive) != ARCHIVE_OK ||
archive_read_support_filter_xz(archive) != ARCHIVE_OK ||
archive_read_support_filter_bzip2(archive) != ARCHIVE_OK ||
archive_read_support_filter_lz4(archive) != ARCHIVE_OK ||
archive_read_support_format_tar(archive) != ARCHIVE_OK ||
archive_read_open_filename(archive, path, 65536) != ARCHIVE_OK) goto done;
while ((status = archive_read_next_header(archive, &entry)) == ARCHIVE_OK) {
const char *name = archive_entry_pathname(entry);
la_int64_t length = archive_entry_size(entry);
if (!name || archive_entry_filetype(entry) != AE_IFREG ||
archive_entry_hardlink(entry) || archive_entry_xattr_count(entry) ||
archive_entry_acl_types(entry)) goto done;
if (!strcmp(name, "index.plist")) {
size_t used = 0;
if (found || length <= 0 || length > 64LL * 1024 * 1024) goto done;
*xml = malloc((size_t)length + 1);
if (!*xml) goto done;
while (used < (size_t)length) {
la_ssize_t got = archive_read_data(archive, *xml + used, (size_t)length - used);
if (got <= 0) goto done;
used += (size_t)got;
}
if (memchr(*xml, 0, used)) goto done;
(*xml)[used] = 0; *size = used; found = 1;
} else if (!strcmp(name, "index-meta.plist")) {
size_t used = 0;
if (metadata++ || length <= 0 || length > 1024 * 1024) goto done;
*meta = malloc((size_t)length + 1);
if (!*meta) goto done;
while (used < (size_t)length) {
la_ssize_t got = archive_read_data(archive, *meta + used, (size_t)length - used);
if (got <= 0) goto done;
used += (size_t)got;
}
if (memchr(*meta, 0, used)) goto done;
(*meta)[used] = 0; *meta_size = used;
} else if (!strcmp(name, "stage.plist")) {
if (length != 0 || archive_read_data_skip(archive) != ARCHIVE_OK) goto done;
} else goto done;
}
ok = status == ARCHIVE_EOF && found && metadata == 1;
done:
archive_read_free(archive);
return ok;
}
static int package_identity(const char *path, const char *name,
const char *version, const char *arch)
{
struct archive *archive = archive_read_new();
struct archive_entry *entry;
plist_t props = NULL;
char *xml = NULL, *pkgname = NULL, *pkgver = NULL, *source_arch = NULL;
int found = 0, status, ok = 0;
if (!archive) return 0;
if (archive_read_support_filter_gzip(archive) != ARCHIVE_OK ||
archive_read_support_filter_zstd(archive) != ARCHIVE_OK ||
archive_read_support_filter_xz(archive) != ARCHIVE_OK ||
archive_read_support_filter_bzip2(archive) != ARCHIVE_OK ||
archive_read_support_filter_lz4(archive) != ARCHIVE_OK ||
archive_read_support_format_tar(archive) != ARCHIVE_OK ||
archive_read_open_filename(archive, path, 65536) != ARCHIVE_OK) goto done;
while ((status = archive_read_next_header(archive, &entry)) == ARCHIVE_OK) {
const char *entry_name = archive_entry_pathname(entry);
la_int64_t length = archive_entry_size(entry);
if (entry_name && (!strcmp(entry_name, "props.plist") ||
!strcmp(entry_name, "./props.plist"))) {
size_t used = 0;
if (found || archive_entry_filetype(entry) != AE_IFREG ||
length <= 0 || length > 1024 * 1024) goto done;
xml = malloc((size_t)length + 1);
if (!xml) goto done;
while (used < (size_t)length) {
la_ssize_t got = archive_read_data(archive, xml + used, (size_t)length - used);
if (got <= 0) goto done;
used += (size_t)got;
}
if (memchr(xml, 0, used) ||
plist_from_xml(xml, (uint32_t)used, &props) != PLIST_ERR_SUCCESS ||
!props || plist_get_node_type(props) != PLIST_DICT) goto done;
found = 1;
} else if (archive_read_data_skip(archive) != ARCHIVE_OK) goto done;
}
if (status != ARCHIVE_EOF || !found) goto done;
pkgname = string(props, "pkgname");
pkgver = string(props, "pkgver");
source_arch = string(props, "architecture");
if (pkgname && pkgver && source_arch && !strcmp(pkgname, name) &&
!strcmp(source_arch, arch) && strlen(pkgver) == strlen(name) + strlen(version) + 1 &&
!strncmp(pkgver, name, strlen(name)) && pkgver[strlen(name)] == '-' &&
!strcmp(pkgver + strlen(name) + 1, version)) ok = 1;
done:
free(pkgname); free(pkgver); free(source_arch); free(xml);
plist_free(props); archive_read_free(archive);
return ok;
}
static int write_capabilities(int dir, const struct xbps_row *rows, size_t count,
char output[65])
{
struct xbps_capability *caps = NULL;
FILE *stream = NULL;
size_t total = 0, used = 0, i, j;
int fd = -1, ok = 0;
for (i = 0; i < count; ++i) {
if (rows[i].soname_count > (size_t)-1 - total) return 0;
total += rows[i].soname_count;
}
caps = calloc(total ? total : 1, sizeof *caps);
if (!caps) return 0;
for (i = 0; i < count; ++i)
for (j = 0; j < rows[i].soname_count; ++j) {
caps[used].soname = rows[i].sonames[j];
caps[used++].row = &rows[i];
}
qsort(caps, used, sizeof *caps, capability_order);
fd = openat(dir, "capabilities", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
if (fd < 0 || !(stream = fdopen(fd, "w"))) goto done;
fd = -1;
fputs("format holy-xbps-capabilities-1\n", stream);
for (i = 0; i < used; ++i) {
const struct xbps_row *row = caps[i].row;
if (i && !capability_order(&caps[i-1], &caps[i])) continue;
fprintf(stream, "soname %s %s %s %s %s\n", caps[i].soname,
row->name, row->version, row->arch, row->hash);
}
{
int failed = ferror(stream) || fflush(stream) || fsync(fileno(stream));
if (fclose(stream)) failed = 1;
stream = NULL;
if (failed) goto done;
}
fd = openat(dir, "capabilities", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd >= 0) ok = hash_fd(fd, output);
done:
if (stream) fclose(stream);
if (fd >= 0) close(fd);
free(caps);
return ok;
}
int holy_xbps_index(const char *input, const char *source, const char *base,
const char *output, const char *expected, const char *public_key,
const char *source_id)
{
struct xbps_row *rows = NULL;
struct stat st;
char *xml = NULL, *meta = NULL, *url = NULL;
char filename[70], original[65], catalog_hash[65], capabilities_hash[65];
char key_hash[65] = {0};
size_t size = 0, meta_size = 0, count = 0, i;
FILE *catalog = NULL, *record = NULL;
int dir = -1, fd = -1, result = 1;
if (!input || !source || !label(source) || !strcmp(source, "local") || !base || !output ||
!digest_label(expected) || (source_id && !digest_label(source_id)) ||
!(url = holy_fetch_child_url(base, "repodata"))) {
free(url); return 2;
}
free(url);
if (mkdir(output, 0700)) return 1;
dir = open(output, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (dir < 0 || fstat(dir, &st) || st.st_uid != geteuid() ||
(st.st_mode & 0777) != 0700 || !holy_fetch_at(input, dir, expected, filename) ||
renameat(dir, filename, dir, "repodata")) goto done;
fd = openat(dir, "repodata", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size > 64LL * 1024 * 1024 ||
!hash_fd(fd, original) || strcmp(original, expected)) goto done;
{
char descriptor[64];
snprintf(descriptor, sizeof descriptor, "/proc/self/fd/%d", fd);
result = 2;
if (!read_repodata(descriptor, &xml, &size, &meta, &meta_size) ||
!parse_rows(xml, size, &rows, &count)) goto done;
}
free(xml); xml = NULL;
if (!index_key(meta, meta_size, public_key, key_hash)) { result = 4; goto done; }
result = 1;
{
int catalog_fd = openat(dir, "catalog", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
if (catalog_fd < 0) goto done;
catalog = fdopen(catalog_fd, "w");
if (!catalog) { close(catalog_fd); goto done; }
}
fputs("format holy-xbps-catalog-1\n", catalog);
for (i = 0; i < count; ++i)
fprintf(catalog, "package %s %s %s %s %llu\n", rows[i].name, rows[i].version,
rows[i].arch, rows[i].hash, rows[i].size);
if (ferror(catalog) || fflush(catalog) || fsync(fileno(catalog))) goto done;
if (fclose(catalog)) { catalog = NULL; goto done; }
catalog = NULL;
{
int catalog_fd = openat(dir, "catalog", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (catalog_fd < 0) goto done;
result = hash_fd(catalog_fd, catalog_hash) ? 1 : 2;
close(catalog_fd);
if (!result) goto done;
}
if (!write_capabilities(dir, rows, count, capabilities_hash)) goto done;
{
int record_fd = openat(dir, "conversion", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
if (record_fd < 0) goto done;
record = fdopen(record_fd, "w");
if (!record) { close(record_fd); goto done; }
}
fprintf(record, "format holy-xbps-index-record-1\nsource %s\nbase %s\noriginal-sha256 %s\ncatalog-sha256 %s\ncapabilities-sha256 %s\nverification %s\n",
source, base, original, catalog_hash, capabilities_hash,
public_key ? "key-matched" : "hash-pinned");
if (source_id) fprintf(record, "source-id %s\n", source_id);
if (key_hash[0]) fprintf(record, "public-key-sha256 %s\n", key_hash);
fprintf(record, "package-coverage complete\nfile-coverage unavailable\npackages %zu\nstate complete\n", count);
if (ferror(record) || fflush(record) || fsync(fileno(record))) goto done;
if (fclose(record)) { record = NULL; goto done; }
record = NULL;
if (fsync(dir)) goto done;
printf("indexed %zu XBPS packages %s\n", count, original);
result = 0;
done:
if (result) fprintf(stderr, "holypkg: XBPS index incomplete (status %d)\n", result);
if (record) fclose(record);
if (catalog) fclose(catalog);
if (fd >= 0) close(fd);
if (dir >= 0) close(dir);
free(xml); free(meta); free_rows(rows, count);
return result;
}
struct catalog_state {
FILE *catalog;
char source[256], base[2048], original[65], catalog_hash[65];
char capabilities_hash[65], key_hash[65], source_id[65];
};
static int open_catalog(const char *directory, struct catalog_state *state)
{
struct stat st;
FILE *record = NULL;
char line[4096], actual[65];
int dir = -1, fd, complete = 0, ok = 0;
memset(state, 0, sizeof *state);
dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (dir < 0 || fstat(dir, &st) || !S_ISDIR(st.st_mode) ||
st.st_uid != geteuid() ||
(st.st_mode & 0022)) goto done;
fd = openat(dir, "conversion", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0) goto done;
record = fdopen(fd, "r");
if (!record) { close(fd); goto done; }
if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size > 4096 ||
!fgets(line, sizeof line, record) ||
strcmp(line, "format holy-xbps-index-record-1\n")) goto done;
while (fgets(line, sizeof line, record)) {
size_t length = strlen(line);
if (!length || line[length - 1] != '\n') goto done;
line[length - 1] = 0;
if (!strncmp(line, "source ", 7)) {
if (state->source[0] || strlen(line + 7) >= sizeof state->source) goto done;
strcpy(state->source, line + 7);
} else if (!strncmp(line, "source-id ", 10)) {
if (state->source_id[0] || strlen(line + 10) >= sizeof state->source_id) goto done;
strcpy(state->source_id, line + 10);
} else if (!strncmp(line, "base ", 5)) {
if (state->base[0] || strlen(line + 5) >= sizeof state->base) goto done;
strcpy(state->base, line + 5);
} else if (!strncmp(line, "original-sha256 ", 16)) {
if (state->original[0] || strlen(line + 16) >= sizeof state->original) goto done;
strcpy(state->original, line + 16);
} else if (!strncmp(line, "catalog-sha256 ", 15)) {
if (state->catalog_hash[0] || strlen(line + 15) >= sizeof state->catalog_hash) goto done;
strcpy(state->catalog_hash, line + 15);
} else if (!strncmp(line, "capabilities-sha256 ", 20)) {
if (state->capabilities_hash[0] ||
strlen(line + 20) >= sizeof state->capabilities_hash) goto done;
strcpy(state->capabilities_hash, line + 20);
} else if (!strncmp(line, "public-key-sha256 ", 18)) {
if (state->key_hash[0] || strlen(line + 18) >= sizeof state->key_hash) goto done;
strcpy(state->key_hash, line + 18);
} else if (!strcmp(line, "state complete")) complete = 1;
}
if (ferror(record) || !complete || !label(state->source) ||
!digest_label(state->original) || !digest_label(state->catalog_hash) ||
(state->capabilities_hash[0] && !digest_label(state->capabilities_hash)) ||
(state->key_hash[0] && !digest_label(state->key_hash)) ||
(state->source_id[0] && !digest_label(state->source_id))) goto done;
{
char *url = holy_fetch_child_url(state->base, "fixture.xbps");
if (!url) goto done;
free(url);
}
fd = openat(dir, "repodata", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0 || !hash_fd(fd, actual) || strcmp(actual, state->original)) {
if (fd >= 0) close(fd);
goto done;
}
close(fd);
fd = openat(dir, "catalog", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0) goto done;
if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size > 32LL * 1024 * 1024 ||
!hash_fd(fd, actual) || strcmp(actual, state->catalog_hash) ||
lseek(fd, 0, SEEK_SET) < 0) { close(fd); goto done; }
state->catalog = fdopen(fd, "r");
if (!state->catalog) { close(fd); goto done; }
if (!fgets(line, sizeof line, state->catalog) ||
strcmp(line, "format holy-xbps-catalog-1\n")) goto done;
if (state->capabilities_hash[0]) {
fd = openat(dir, "capabilities", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
if (fd < 0) goto done;
if (fstat(fd, &st) || !S_ISREG(st.st_mode) ||
st.st_uid != geteuid() || (st.st_mode & 0022) ||
st.st_size <= 0 || st.st_size > 128LL * 1024 * 1024 ||
!hash_fd(fd, actual) || strcmp(actual, state->capabilities_hash)) {
close(fd); goto done;
}
close(fd);
}
ok = 1;
done:
if (!ok && state->catalog) { fclose(state->catalog); state->catalog = NULL; }
if (record) fclose(record);
if (dir >= 0) close(dir);
return ok;
}
int holy_xbps_source_catalog(const char *directory, const char *id,
const char *base, const char *key)
{
struct catalog_state state;
int ok;
if (!directory || !id || !base || !key ||
!open_catalog(directory, &state)) return 6;
ok = !strcmp(state.source_id, id) &&
!strcmp(state.base, base) &&
(!key[0] || !strcmp(state.key_hash, key));
fclose(state.catalog);
return ok ? 0 : 6;
}
static int xbps_binding_name(const char *id, const char *arch, char name[130])
{
unsigned char digest[32];
unsigned length;
size_t i;
if (!digest_label(id) || !label(arch) ||
EVP_Digest(arch, strlen(arch), digest, &length, EVP_sha256(), NULL) != 1 ||
length != 32) return 0;
memcpy(name, id, 64);
name[64] = '.';
for (i = 0; i < 32; ++i) snprintf(name + 65 + 2 * i, 3, "%02x", digest[i]);
return 1;
}
static int xbps_binding_dir(int database, int create)
{
struct stat st;
int dir;
if (create && mkdirat(database, "xbps-catalogs", 0700) && errno != EEXIST) return -1;
dir = openat(database, "xbps-catalogs", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (dir < 0) return -1;
if (fstat(dir, &st) || !S_ISDIR(st.st_mode) || st.st_uid != geteuid() ||
(st.st_mode & 0022)) { close(dir); return -1; }
return dir;
}
int holy_xbps_bind(const char *root, const char *source, const char *arch,
const char *catalog)
{
char id[65], key[65], name[130], digest[65], temporary[43] = {0};
char *base = NULL, *trust = NULL, *path = NULL, *root_path = NULL, *record = NULL;
const char *stored_path, *path_key;
unsigned long long generation;
size_t size, used = 0;
int database = -1, dir = -1, fd = -1, catalog_dir = -1, result;
result = holy_source_xbps(root, source, id, &base, &trust, key);
if (result) goto done;
if (!xbps_binding_name(id, arch, name) || !catalog ||
!(path = realpath(catalog, NULL))) { result = 2; goto done; }
if (strchr(path, '\n') || strlen(path) > 1024 * 1024) { result = 2; goto done; }
result = holy_xbps_source_catalog(path, id, base, key);
if (result) goto done;
root_path = realpath(root, NULL);
if (!root_path) { result = 6; goto done; }
path_key = "path";
stored_path = path;
if (strcmp(root_path, "/") && !strncmp(path, root_path, strlen(root_path)) &&
path[strlen(root_path)] == '/') {
path_key = "root-path";
stored_path = path + strlen(root_path);
}
catalog_dir = open(path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
fd = catalog_dir < 0 ? -1 : openat(catalog_dir, "conversion", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0 || !hash_fd(fd, digest)) { result = 6; goto done; }
close(fd); fd = -1;
size = strlen(path) + strlen(arch) + 256;
record = malloc(size);
if (!record) { result = 1; goto done; }
snprintf(record, size, "format holy-xbps-binding-1\nsource-id %s\narch %s\nconversion-sha256 %s\n%s %s\n",
id, arch, digest, path_key, stored_path);
size = strlen(record);
database = holy_state_lock(root, 1, &generation, &result);
if (database < 0) goto done;
dir = xbps_binding_dir(database, 1);
if (dir < 0 || (fd = holy_temporary_at(dir, temporary)) < 0) { result = 1; goto done; }
while (used < size) {
ssize_t written = write(fd, record + used, size - used);
if (written < 0 && errno == EINTR) continue;
if (written <= 0) { result = 1; goto done; }
used += (size_t)written;
}
if (fsync(fd) || renameat(dir, temporary, dir, name) || fsync(dir)) {
result = 1; goto done;
}
result = 0;
done:
if (fd >= 0) close(fd);
if (dir >= 0) {
if (temporary[0]) unlinkat(dir, temporary, 0);
close(dir);
}
if (catalog_dir >= 0) close(catalog_dir);
if (database >= 0) close(database);
free(base); free(trust); free(path); free(root_path); free(record);
return result;
}
int holy_xbps_catalog_path(const char *root, const char *source,
const char *arch, char **catalog)
{
struct stat st;
char id[65], key[65], name[130], saved_id[65], saved_arch[256];
char saved_digest[65], actual[65], *base = NULL, *trust = NULL;
char *line = NULL, *path = NULL, *root_path = NULL, *candidate = NULL;
size_t capacity = 0;
unsigned long long generation;
FILE *record = NULL;
int database = -1, dir = -1, fd = -1, catalog_dir = -1, result;
*catalog = NULL;
result = holy_source_xbps(root, source, id, &base, &trust, key);
if (result) goto done;
if (!xbps_binding_name(id, arch, name)) { result = 2; goto done; }
database = holy_state_lock(root, 0, &generation, &result);
if (database < 0) goto done;
dir = xbps_binding_dir(database, 0);
fd = dir < 0 ? -1 : openat(dir, name, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) ||
st.st_size <= 0 || st.st_size > 1024 * 1024 + 512 ||
st.st_uid != geteuid() || (st.st_mode & 0022)) { result = 6; goto done; }
record = fdopen(fd, "r");
if (!record) { result = 1; goto done; }
fd = -1;
if (getline(&line, &capacity, record) < 0 ||
strcmp(line, "format holy-xbps-binding-1\n") ||
getline(&line, &capacity, record) < 0 ||
sscanf(line, "source-id %64s\n", saved_id) != 1 || strcmp(saved_id, id) ||
getline(&line, &capacity, record) < 0 ||
sscanf(line, "arch %255s\n", saved_arch) != 1 || strcmp(saved_arch, arch) ||
getline(&line, &capacity, record) < 0 ||
sscanf(line, "conversion-sha256 %64s\n", saved_digest) != 1 ||
!digest_label(saved_digest) || getline(&line, &capacity, record) < 0 ||
(strncmp(line, "path ", 5) && strncmp(line, "root-path ", 10)) ||
strlen(line) > 1024 * 1024 + 12 ||
line[strlen(line) - 1] != '\n' ||
fgetc(record) != EOF || ferror(record)) { result = 6; goto done; }
line[strlen(line) - 1] = 0;
if (!strncmp(line, "root-path ", 10)) {
root_path = realpath(root, NULL);
if (!root_path || line[10] != '/' ||
strlen(root_path) > (size_t)-1 - strlen(line + 10) - 1) {
result = 6; goto done;
}
candidate = malloc(strlen(root_path) + strlen(line + 10) + 1);
if (!candidate) { result = 1; goto done; }
if (!strcmp(root_path, "/")) strcpy(candidate, line + 10);
else sprintf(candidate, "%s%s", root_path, line + 10);
} else candidate = strdup(line + 5);
if (!candidate) { result = 1; goto done; }
path = realpath(candidate, NULL);
if (!path || strcmp(path, candidate)) { result = 6; goto done; }
catalog_dir = open(path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
fd = catalog_dir < 0 ? -1 : openat(catalog_dir, "conversion", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0 || !hash_fd(fd, actual) || strcmp(actual, saved_digest)) {
result = 6; goto done;
}
result = holy_xbps_source_catalog(path, id, base, key);
if (!result) { *catalog = path; path = NULL; }
done:
if (fd >= 0) close(fd);
if (record) fclose(record);
if (catalog_dir >= 0) close(catalog_dir);
if (dir >= 0) close(dir);
if (database >= 0) close(database);
free(base); free(trust); free(line); free(path); free(root_path); free(candidate);
return result;
}
static int next_row(FILE *catalog, struct xbps_row *row)
{
char line[1024], name[256], version[256], arch[256], hash[65];
unsigned long long size;
int end = 0;
if (!fgets(line, sizeof line, catalog)) return ferror(catalog) ? -1 : 0;
if (sscanf(line, "package %255s %255s %255s %64s %llu%n",
name, version, arch, hash, &size, &end) != 5 ||
!end || line[end] != '\n' || line[end + 1] ||
!label(name) || !label(version) || !label(arch) ||
!digest_label(hash) || !size || size > 1024ULL * 1024 * 1024 * 1024) return -1;
row->name = strdup(name); row->version = strdup(version);
row->arch = strdup(arch); row->hash = strdup(hash); row->size = size;
return row->name && row->version && row->arch && row->hash ? 1 : -1;
}
int holy_xbps_providers(const char *directory, const char *soname)
{
struct catalog_state state;
struct stat st;
char line[1200], actual[65], *matches = NULL;
size_t matches_size = 0, count = 0;
FILE *input = NULL, *out = NULL;
int dir = -1, fd = -1, result = 6;
if (!soname_label(soname) || !open_catalog(directory, &state)) return 6;
fclose(state.catalog);
if (!state.capabilities_hash[0]) goto done;
dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
fd = dir < 0 ? -1 : openat(dir, "capabilities", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) ||
st.st_uid != geteuid() || (st.st_mode & 0022) ||
st.st_size <= 0 || st.st_size > 128LL * 1024 * 1024 ||
!hash_fd(fd, actual) || strcmp(actual, state.capabilities_hash) ||
lseek(fd, 0, SEEK_SET) < 0 || !(input = fdopen(fd, "r"))) goto done;
fd = -1;
if (!fgets(line, sizeof line, input) ||
strcmp(line, "format holy-xbps-capabilities-1\n")) goto done;
out = open_memstream(&matches, &matches_size);
if (!out) { result = 1; goto done; }
while (fgets(line, sizeof line, input)) {
char found[256], name[256], version[256], arch[256], hash[65];
int end = 0;
if (sscanf(line, "soname %255s %255s %255s %255s %64s%n",
found, name, version, arch, hash, &end) != 5 ||
!end || line[end] != '\n' || line[end + 1] ||
!soname_label(found) || !label(name) || !label(version) ||
!label(arch) || !digest_label(hash)) { result = 2; goto done; }
if (strcmp(found, soname)) continue;
if (++count > 10000) { result = 6; goto done; }
fprintf(out, "candidate %s %s %s %s index-hint\n",
name, version, arch, hash);
}
{
int failed = ferror(input) || ferror(out);
if (fclose(out)) failed = 1;
out = NULL;
if (failed) { result = 1; goto done; }
}
if (matches_size && fwrite(matches, 1, matches_size, stdout) != matches_size) {
result = 1; goto done;
}
printf("listed %zu candidates coverage partial\n", count);
result = 0;
done:
if (out) fclose(out);
if (input) fclose(input);
if (fd >= 0) close(fd);
if (dir >= 0) close(dir);
free(matches);
return result;
}
int holy_xbps_query(const char *directory, const char *query, int info)
{
struct catalog_state state;
struct xbps_row row = {0};
size_t matches = 0;
int status, result = 2;
if (!directory || !query || !*query || !open_catalog(directory, &state)) return 6;
while ((status = next_row(state.catalog, &row)) > 0) {
if ((info && !strcmp(query, row.name)) || (!info && strstr(row.name, query))) {
++matches;
if (info) printf("package %s\nversion %s\narch %s\nsha256 %s\nsize %llu\n",
row.name, row.version, row.arch, row.hash, row.size);
else printf("%s %s %s\n", row.name, row.version, row.arch);
}
free(row.name); free(row.version); free(row.arch); free(row.hash);
memset(&row, 0, sizeof row);
}
result = status < 0 ? 2 : !matches ? 4 : info && matches > 1 ? 3 : 0;
free(row.name); free(row.version); free(row.arch); free(row.hash);
fclose(state.catalog);
return result;
}
static int imported_arch_matches(const char *source, const char *output)
{
if (!strcmp(output, "noarch")) return 1;
if (!strncmp(source, "x86_64", 6))
return !strcmp(output, "x86_64") || !strcmp(output, "x86");
return !strncmp(source, "i686", 4) && !strcmp(output, "x86");
}
static int imported_claim(const char *directory, const char *name, const char *version,
const char *arch, const char *soname)
{
DIR *dir = opendir(directory);
struct dirent *entry;
const char *release = strrchr(version, '_');
size_t count = 0;
int found = 0, ok = 0;
if (!dir || !release || release == version || !release[1]) goto done;
errno = 0;
while ((entry = readdir(dir))) {
struct holy_package_identity identity = {0};
size_t length = strlen(entry->d_name);
char *path;
int matched = 0;
if (length < 6 || strcmp(entry->d_name + length - 5, ".holy")) continue;
path = malloc(strlen(directory) + length + 2);
if (!path) goto done;
sprintf(path, "%s/%s", directory, entry->d_name);
if (!holy_package_identity(path, &identity) ||
strcmp(identity.name, name) ||
strlen(identity.version) != (size_t)(release - version) ||
strncmp(identity.version, version, (size_t)(release - version)) ||
strcmp(identity.release, release + 1) ||
!imported_arch_matches(arch, identity.arch) ||
(soname && !holy_provides_match(path, "soname", soname, &matched))) {
holy_package_identity_free(&identity); free(path); goto done;
}
if (matched) found = 1;
holy_package_identity_free(&identity); free(path);
++count;
errno = 0;
}
ok = count && !errno && (!soname || found);
done:
if (dir) closedir(dir);
return ok;
}
int holy_xbps_fetch(const char *directory, const char *name, const char *version,
const char *arch, const char *output, const char *ca_file,
const char *public_key, int source_checked, int import,
const char *required_soname)
{
struct catalog_state state;
struct xbps_row row = {0}, selected = {0};
EVP_PKEY *key = NULL;
char digest[65] = {0}, signature_digest[65] = {0}, key_hash[65] = {0};
char *filename = NULL, *url = NULL, *download = NULL, *converted = NULL;
char *signature_name = NULL, *signature_url = NULL, *signature_path = NULL;
FILE *record = NULL;
struct stat st;
size_t matches = 0, length;
int status, result = 1, dir = -1;
if (!directory || !label(name) || !label(version) || !label(arch) || !output ||
(required_soname && (!import || !soname_label(required_soname)))) return 2;
if (!open_catalog(directory, &state)) return 6;
while ((status = next_row(state.catalog, &row)) > 0) {
if (!strcmp(row.name, name) && !strcmp(row.version, version) && !strcmp(row.arch, arch)) {
if (!matches) { selected = row; memset(&row, 0, sizeof row); }
++matches;
}
free(row.name); free(row.version); free(row.arch); free(row.hash);
memset(&row, 0, sizeof row);
if (matches > 1) break;
}
fclose(state.catalog);
if (status < 0) { result = 2; goto done; }
if (!matches) { result = 4; goto done; }
if (matches > 1) { result = 3; goto done; }
if (public_key) {
key = public_key_file(public_key);
if (!key || !key_digest(key, key_hash) ||
!state.key_hash[0] || strcmp(key_hash, state.key_hash)) {
result = 4; goto done;
}
}
if (selected.size > 1024ULL * 1024 * 1024) { result = 6; goto done; }
length = strlen(name) + strlen(version) + strlen(arch) + 8;
filename = malloc(length);
if (!filename) goto done;
snprintf(filename, length, "%s-%s.%s.xbps", name, version, arch);
url = holy_fetch_child_url(state.base, filename);
if (!url) { result = 2; goto done; }
if (mkdir(output, 0700)) goto done;
dir = open(output, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (dir < 0 || fstat(dir, &st) || st.st_uid != geteuid() ||
(st.st_mode & 0777) != 0700) goto done;
result = holy_fetch_https_foreign_limited(url, output, ca_file, digest, selected.size);
if (result) goto done;
if (strcmp(digest, selected.hash)) { result = 4; goto done; }
download = malloc(strlen(output) + 66);
if (!download) { result = 1; goto done; }
sprintf(download, "%s/%s", output, digest);
if (stat(download, &st) || !S_ISREG(st.st_mode) ||
(unsigned long long)st.st_size != selected.size ||
!package_identity(download, name, version, arch)) {
result = 4; goto done;
}
if (key) {
signature_name = malloc(strlen(filename) + 6);
signature_path = malloc(strlen(output) + 66);
if (!signature_name || !signature_path) { result = 1; goto done; }
sprintf(signature_name, "%s.sig2", filename);
signature_url = holy_fetch_child_url(state.base, signature_name);
if (!signature_url) { result = 2; goto done; }
result = holy_fetch_https_foreign_limited(signature_url, output, ca_file,
signature_digest, 16384);
if (result) goto done;
sprintf(signature_path, "%s/%s", output, signature_digest);
if (!verify_signature(download, signature_path, key)) {
result = 4; goto done;
}
}
if (import) {
converted = malloc(strlen(output) + sizeof "/converted");
if (!converted) { result = 1; goto done; }
sprintf(converted, "%s/converted", output);
result = holy_import_xbps_verified(download, state.source, converted, digest,
key ? "rsa-sha256" : "hash-pinned",
key ? key_hash : NULL,
key ? signature_digest : NULL,
state.original, state.base);
if (result) goto done;
if (!imported_claim(converted, name, version, arch, required_soname)) {
if (required_soname)
fprintf(stderr, "holypkg: XBPS payload does not provide SONAME %s\n", required_soname);
result = 4; goto done;
}
}
{
int fd = openat(dir, "conversion", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
if (fd < 0) { result = 1; goto done; }
record = fdopen(fd, "w");
if (!record) { close(fd); result = 1; goto done; }
}
fprintf(record, "format holy-xbps-fetch-record-1\nsource %s\nindex-sha256 %s\npackage %s\nversion %s\narch %s\nsha256 %s\nsize %llu\nverification %s\n",
state.source, state.original, name, version, arch, digest, selected.size,
key ? "rsa-sha256" : "hash-pinned");
if (key) fprintf(record, "public-key-sha256 %s\nsignature-sha256 %s\n",
key_hash, signature_digest);
if (source_checked && state.source_id[0])
fprintf(record, "source-id %s\nsource-binding checked\n", state.source_id);
if (required_soname) fprintf(record, "required-soname %s\nsoname-provider verified-payload\n", required_soname);
fprintf(record, "imported %s\n", import ? "yes" : "no");
fputs("state complete\n", record);
if (ferror(record) || fflush(record) || fsync(fileno(record))) { result = 1; goto done; }
if (fclose(record)) { record = NULL; result = 1; goto done; }
record = NULL;
if (fsync(dir)) { result = 1; goto done; }
printf("fetched %s sha256 %s%s\n", filename, digest, import ? " imported" : "");
result = 0;
done:
if (result) fprintf(stderr, "holypkg: XBPS fetch incomplete (status %d)\n", result);
if (record) fclose(record);
if (dir >= 0) close(dir);
free(selected.name); free(selected.version); free(selected.arch); free(selected.hash);
EVP_PKEY_free(key);
free(filename); free(url); free(download); free(converted);
free(signature_name); free(signature_url); free(signature_path);
return result;
}
int holy_xbps_sync(const char *base, const char *arch, const char *source,
const char *output, const char *expected, const char *ca_file,
const char *public_key, const char *source_id)
{
char template[] = "/tmp/holy-xbps-sync-XXXXXX";
char *url = NULL, *path = NULL, *filename = NULL;
int result;
if (!base || !arch || !label(arch) || !source || !label(source) ||
!strcmp(source, "local") || !output || !digest_label(expected)) return 2;
filename = malloc(strlen(arch) + 10);
if (!filename) return 1;
sprintf(filename, "%s-repodata", arch);
url = holy_fetch_child_url(base, filename);
free(filename);
if (!url) return 2;
if (!mkdtemp(template)) { free(url); return 1; }
result = holy_fetch_https_data(url, expected, template, ca_file);
free(url);
if (!result) {
path = malloc(strlen(template) + 66);
if (!path) result = 1;
else {
sprintf(path, "%s/%s", template, expected);
result = holy_xbps_index(path, source, base, output, expected, public_key, source_id);
}
}
if (path) { unlink(path); free(path); }
rmdir(template);
return result;
}