_ _
| |_ ___| |_ _
| | . | | | |
|_|_|___|_|_ |
|___|
git mirror - github.com/owenewans/holy - branch master
file backends/rpm-md.c
#define _XOPEN_SOURCE 700
#include "rpm-md.h"
#ifndef HOLY_HAVE_RPMMD
#include <stdio.h>
int holy_rpm_md_index(const char *a, const char *b, const char *c, const char *d,
const char *e, const char *f, const char *g)
{ (void)a; (void)b; (void)c; (void)d; (void)e; (void)f; (void)g;
fputs("holypkg: rpm-md requires librpm and libxml2 at build time\n", stderr); return 6; }
int holy_rpm_md_sync(const char *a, const char *b, const char *c, const char *d,
const char *e, const char *f)
{ (void)a; (void)b; (void)c; (void)d; (void)e; (void)f; return 6; }
int holy_rpm_md_query(const char *a, const char *b, int c)
{ (void)a; (void)b; (void)c; return 6; }
int holy_rpm_md_providers(const char *a, const char *b)
{ (void)a; (void)b; return 6; }
int holy_rpm_md_fetch(const char *a, const char *b, const char *c, const char *d,
const char *e, const char *f, int g)
{ (void)a; (void)b; (void)c; (void)d; (void)e; (void)f; (void)g; return 6; }
int holy_rpm_md_source_catalog(const char *a, const char *b, const char *c)
{ (void)a; (void)b; (void)c; return 6; }
int holy_rpm_md_bind(const char *a, const char *b, const char *c)
{ (void)a; (void)b; (void)c; return 6; }
int holy_rpm_md_catalog_path(const char *a, const char *b, char **c)
{ (void)a; (void)b; (void)c; return 6; }
#else
#include "rpm-version.h"
#include "../src/fetch.h"
#include "../src/import.h"
#include "../src/source.h"
#include "../src/stage.h"
#include "../src/state.h"
#include <archive.h>
#include <libxml/xmlreader.h>
#include <openssl/evp.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
struct rpmmd_row {
char name[256], evr[256], arch[64], hash[65], href[1024];
unsigned long long size;
char **provides;
size_t provide_count;
};
/* rpm declares capabilities such as libfoo.so.1(N) or plain names. */
static int provide_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;
}
struct rpmmd_catalog {
FILE *file;
char source[256], base[2048], repomd[65], primary[65], catalog[65];
char capabilities[65], source_id[65];
};
static int digest(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 int label(const char *value, size_t limit)
{
const unsigned char *p = (const unsigned char *)value;
size_t n = value ? strlen(value) : 0;
if (!n || n >= limit || !isalnum(*p)) return 0;
for (; *p; ++p)
if (!isalnum(*p) && *p != '.' && *p != '_' && *p != '+' && *p != '-') return 0;
return 1;
}
static int href_valid(const char *href)
{
const char *p = href, *part = href;
size_t n = href ? strlen(href) : 0;
if (!n || n >= 1024 || *p == '/') return 0;
for (; *p; ++p) {
if (*p == '/') {
size_t size = (size_t)(p - part);
if (!size || (size == 1 && *part == '.') ||
(size == 2 && !memcmp(part, "..", 2))) return 0;
part = p + 1;
} else if (!isalnum((unsigned char)*p) && *p != '.' && *p != '_' &&
*p != '-' && *p != '+') return 0;
}
return p != part && strcmp(part, ".") && strcmp(part, "..");
}
static int number(const char *value, unsigned long long *out)
{
char *end;
unsigned long long n;
if (!value || !*value || *value == '-') return 0;
errno = 0;
n = strtoull(value, &end, 10);
if (errno || *end) return 0;
*out = n;
return 1;
}
static int hash_file(const char *path, char hash[65], off_t limit)
{
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
struct stat st;
unsigned char bytes[65536], digest_bytes[32];
unsigned digest_size;
ssize_t got;
size_t i;
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK), ok = 0;
if (!ctx || fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) ||
st.st_size < 0 || st.st_size > limit ||
EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1) goto done;
while ((got = read(fd, bytes, sizeof bytes)) > 0)
if (EVP_DigestUpdate(ctx, bytes, (size_t)got) != 1) goto done;
if (got || EVP_DigestFinal_ex(ctx, digest_bytes, &digest_size) != 1 || digest_size != 32) goto done;
for (i = 0; i < 32; ++i) snprintf(hash + i * 2, 3, "%02x", digest_bytes[i]);
ok = 1;
done:
if (fd >= 0) close(fd);
EVP_MD_CTX_free(ctx);
return ok;
}
static int attr(xmlTextReaderPtr reader, const char *name, char *out, size_t capacity)
{
xmlChar *value = xmlTextReaderGetAttribute(reader, (const xmlChar *)name);
size_t length;
if (!value) return 0;
length = strlen((const char *)value);
if (!length || length >= capacity) { xmlFree(value); return 0; }
memcpy(out, value, length + 1);
xmlFree(value);
return 1;
}
static int element(xmlTextReaderPtr reader, const char *name)
{
const xmlChar *current = xmlTextReaderConstLocalName(reader);
return current && !xmlStrcmp(current, (const xmlChar *)name);
}
/* primary metadata carries capability entries in the rpm namespace. */
static int rpm_element(xmlTextReaderPtr reader)
{
const xmlChar *prefix = xmlTextReaderConstPrefix(reader);
return prefix && !xmlStrcmp(prefix, (const xmlChar *)"rpm");
}
static int contents(xmlTextReaderPtr reader, char *out, size_t capacity)
{
xmlChar *value = xmlTextReaderReadString(reader);
size_t length;
if (!value) return 0;
length = strlen((const char *)value);
if (!length || length >= capacity) { xmlFree(value); return 0; }
memcpy(out, value, length + 1);
xmlFree(value);
return 1;
}
static int repomd_primary(const char *path, char href[1024], char hash[65])
{
xmlTextReaderPtr reader = xmlReaderForFile(path, NULL, XML_PARSE_NONET | XML_PARSE_COMPACT |
XML_PARSE_NOERROR | XML_PARSE_NOWARNING);
int step, in_primary = 0, found = 0, root = 0, ok = 0;
if (!reader) return 0;
while ((step = xmlTextReaderRead(reader)) == 1) {
int type = xmlTextReaderNodeType(reader), depth = xmlTextReaderDepth(reader);
if (type == XML_READER_TYPE_DOCUMENT_TYPE) goto done;
if (type == XML_READER_TYPE_ELEMENT && depth == 0) {
if (root++ || !element(reader, "repomd")) goto done;
} else if (type == XML_READER_TYPE_ELEMENT && depth == 1 && element(reader, "data")) {
char kind[64] = {0};
if (!attr(reader, "type", kind, sizeof kind)) goto done;
in_primary = !strcmp(kind, "primary");
if (in_primary && found++) goto done;
} else if (in_primary && type == XML_READER_TYPE_ELEMENT && depth == 2) {
if (element(reader, "checksum")) {
char kind[32];
if (hash[0] || !attr(reader, "type", kind, sizeof kind) ||
strcmp(kind, "sha256") || !contents(reader, hash, 65)) goto done;
} else if (element(reader, "location")) {
if (href[0] || !attr(reader, "href", href, 1024)) goto done;
}
} else if (type == XML_READER_TYPE_END_ELEMENT && depth == 1 &&
element(reader, "data")) in_primary = 0;
}
ok = step == 0 && root == 1 && found == 1 && digest(hash) && href_valid(href);
done:
xmlFreeTextReader(reader);
return ok;
}
static int unpack_primary(const char *path, FILE *out)
{
struct archive *archive = archive_read_new();
struct archive_entry *entry;
char bytes[65536];
la_ssize_t got;
unsigned long long total = 0;
int ok = 0;
if (!archive) return 0;
if (archive_read_support_filter_all(archive) != ARCHIVE_OK ||
archive_read_support_format_raw(archive) != ARCHIVE_OK ||
archive_read_open_filename(archive, path, 65536) != ARCHIVE_OK ||
archive_read_next_header(archive, &entry) != ARCHIVE_OK) goto done;
while ((got = archive_read_data(archive, bytes, sizeof bytes)) > 0) {
if (total > 1024ULL * 1024 * 1024 - (unsigned long long)got ||
fwrite(bytes, 1, (size_t)got, out) != (size_t)got) goto done;
total += (unsigned long long)got;
}
if (got || !total || archive_read_next_header(archive, &entry) != ARCHIVE_EOF ||
fflush(out) || fseeko(out, 0, SEEK_SET)) goto done;
ok = 1;
done:
archive_read_free(archive);
return ok;
}
static int row_order(const void *a, const void *b)
{
const struct rpmmd_row *left = a, *right = b;
int result = strcmp(left->name, right->name);
if (!result) result = strcmp(left->evr, right->evr);
if (!result) result = strcmp(left->arch, right->arch);
return result;
}
struct rpmmd_capability {
const char *provides;
const struct rpmmd_row *row;
};
static int capability_order(const void *a, const void *b)
{
const struct rpmmd_capability *left = a, *right = b;
int result = strcmp(left->provides, right->provides);
return result ? result : row_order(left->row, right->row);
}
static int write_capabilities(int dir, const struct rpmmd_row *rows, size_t count,
char output[65])
{
struct rpmmd_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].provide_count > (size_t)-1 - total) return 0;
total += rows[i].provide_count;
}
caps = calloc(total ? total : 1, sizeof *caps);
if (!caps) return 0;
for (i = 0; i < count; ++i)
for (j = 0; j < rows[i].provide_count; ++j) {
caps[used].provides = rows[i].provides[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-rpm-md-capabilities-1\n", stream);
for (i = 0; i < used; ++i) {
const struct rpmmd_row *row = caps[i].row;
if (i && !capability_order(&caps[i-1], &caps[i])) continue;
fprintf(stream, "provides %s %s %s %s %s\n", caps[i].provides,
row->name, row->evr, 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;
}
static int parse_primary(FILE *primary, struct rpmmd_row **result, size_t *count)
{
xmlTextReaderPtr reader;
struct rpmmd_row *rows = NULL, row = {0};
size_t used = 0, capacity = 0;
unsigned long long declared = 0;
int fd = dup(fileno(primary)), step, root = 0, in_package = 0, in_provides = 0, ok = 0;
if (fd < 0) return 0;
reader = xmlReaderForFd(fd, NULL, NULL, XML_PARSE_NONET | XML_PARSE_COMPACT |
XML_PARSE_NOERROR | XML_PARSE_NOWARNING);
if (!reader) { close(fd); return 0; }
while ((step = xmlTextReaderRead(reader)) == 1) {
int type = xmlTextReaderNodeType(reader), depth = xmlTextReaderDepth(reader);
if (type == XML_READER_TYPE_DOCUMENT_TYPE) goto done;
if (type == XML_READER_TYPE_ELEMENT && depth == 0) {
char packages[32];
if (root++ || !element(reader, "metadata") ||
!attr(reader, "packages", packages, sizeof packages) ||
!number(packages, &declared) || declared > 100000) goto done;
} else if (type == XML_READER_TYPE_ELEMENT && depth == 1 && element(reader, "package")) {
char kind[16];
if (in_package || !attr(reader, "type", kind, sizeof kind) || strcmp(kind, "rpm")) goto done;
memset(&row, 0, sizeof row);
in_package = 1;
} else if (in_package && type == XML_READER_TYPE_ELEMENT && depth == 2) {
if (element(reader, "name")) {
if (row.name[0] || !contents(reader, row.name, sizeof row.name)) goto done;
} else if (element(reader, "arch")) {
if (row.arch[0] || !contents(reader, row.arch, sizeof row.arch)) goto done;
} else if (element(reader, "version")) {
char epoch[32] = "0", version[128], release[64];
unsigned long long epoch_number;
int length;
if (row.evr[0] || !attr(reader, "ver", version, sizeof version) ||
!attr(reader, "rel", release, sizeof release)) goto done;
if (xmlTextReaderHasAttributes(reader)) {
xmlChar *value = xmlTextReaderGetAttribute(reader, (const xmlChar *)"epoch");
if (value) {
size_t n = strlen((const char *)value);
if (!n || n >= sizeof epoch) { xmlFree(value); goto done; }
memcpy(epoch, value, n + 1); xmlFree(value);
}
}
if (!number(epoch, &epoch_number)) goto done;
length = epoch_number ? snprintf(row.evr, sizeof row.evr, "%llu:%s-%s",
epoch_number, version, release) :
snprintf(row.evr, sizeof row.evr, "%s-%s", version, release);
if (length < 0 || (size_t)length >= sizeof row.evr ||
!holy_rpm_version_valid(row.evr)) goto done;
} else if (element(reader, "checksum")) {
char kind[32];
if (row.hash[0] || !attr(reader, "type", kind, sizeof kind) ||
strcmp(kind, "sha256") || !contents(reader, row.hash, sizeof row.hash)) goto done;
} else if (element(reader, "size")) {
char size[32];
if (row.size || !attr(reader, "package", size, sizeof size) ||
!number(size, &row.size)) goto done;
} else if (element(reader, "location")) {
if (row.href[0] || !attr(reader, "href", row.href, sizeof row.href)) goto done;
} else if (element(reader, "provides") && rpm_element(reader)) {
in_provides = 1;
}
} else if (in_package && in_provides && type == XML_READER_TYPE_ELEMENT &&
element(reader, "entry") && rpm_element(reader)) {
char capability[256];
char **grown;
if (row.provide_count >= 4096 || !attr(reader, "name", capability, sizeof capability) ||
!provide_label(capability)) goto done;
grown = realloc(row.provides, (row.provide_count + 1) * sizeof *grown);
if (!grown) goto done;
row.provides = grown;
row.provides[row.provide_count] = strdup(capability);
if (!row.provides[row.provide_count]) goto done;
++row.provide_count;
} else if (in_package && type == XML_READER_TYPE_END_ELEMENT && depth == 2 &&
element(reader, "provides") && rpm_element(reader)) {
in_provides = 0;
} else if (in_package && type == XML_READER_TYPE_END_ELEMENT && depth == 1 &&
element(reader, "package")) {
struct rpmmd_row *grown;
if (!label(row.name, sizeof row.name) || !label(row.arch, sizeof row.arch) ||
!digest(row.hash) || !href_valid(row.href) ||
!row.size || row.size > 1024ULL * 1024 * 1024 || used >= 100000) goto done;
if (used == capacity) {
size_t next = capacity ? capacity * 2 : 64;
grown = realloc(rows, next * sizeof *rows);
if (!grown) goto done;
rows = grown; capacity = next;
}
rows[used++] = row;
memset(&row, 0, sizeof row);
in_package = 0;
}
}
if (step || root != 1 || in_package || in_provides || used != declared) goto done;
qsort(rows, used, sizeof *rows, row_order);
for (size_t i = 1; i < used; ++i)
if (!row_order(&rows[i-1], &rows[i])) {
for (i = 0; i < used; ++i) {
size_t j;
for (j = 0; j < rows[i].provide_count; ++j) free(rows[i].provides[j]);
free(rows[i].provides);
}
free(rows);
return 0;
}
*result = rows; *count = used; rows = NULL; ok = 1;
done:
xmlFreeTextReader(reader);
if (rows) {
size_t i, j;
for (i = 0; i < used; ++i) {
for (j = 0; j < rows[i].provide_count; ++j) free(rows[i].provides[j]);
free(rows[i].provides);
}
free(rows);
}
for (size_t j = 0; j < row.provide_count; ++j) free(row.provides[j]);
free(row.provides);
return ok;
}
static char *path_join(const char *directory, const char *name)
{
size_t a = strlen(directory), b = strlen(name);
char *out;
if (a > SIZE_MAX - b - 2 || !(out = malloc(a + b + 2))) return NULL;
memcpy(out, directory, a);
out[a] = '/'; memcpy(out + a + 1, name, b + 1);
return out;
}
static char *child_url(const char *base, const char *href)
{
char *copy = NULL, *save = NULL, *part, *current = NULL;
if (!href_valid(href) || !(copy = strdup(href)) || !(current = strdup(base))) goto done;
for (part = strtok_r(copy, "/", &save); part; part = strtok_r(NULL, "/", &save)) {
char *next = holy_fetch_child_url(current, part);
free(current); current = next;
if (!current) goto done;
if (save && *save) {
size_t length = strlen(current);
next = realloc(current, length + 2);
if (!next) { free(current); current = NULL; goto done; }
current = next; current[length] = '/'; current[length + 1] = 0;
}
}
done:
free(copy);
return current;
}
int holy_rpm_md_index(const char *repomd, const char *primary, const char *expected,
const char *source, const char *base, const char *output,
const char *source_id)
{
struct rpmmd_row *rows = NULL;
struct stat st;
char href[1024] = {0}, primary_hash[65] = {0}, actual[65], catalog_hash[65];
char capabilities_hash[65] = {0};
char staged[70], *repomd_path = NULL, *primary_path = NULL, *catalog_path = NULL;
char *test_url = NULL;
FILE *expanded = NULL, *catalog = NULL, *record = NULL;
size_t count = 0, i, provides = 0;
int dir = -1, result = 1;
if (!repomd || !primary || !expected || !digest(expected) ||
!source || !label(source, 256) || !strcmp(source, "local") ||
(source_id && !digest(source_id)) ||
!base || !output || !(test_url = child_url(base, "repodata/repomd.xml"))) {
free(test_url); return 2;
}
free(test_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(repomd, dir, expected, staged) ||
renameat(dir, staged, dir, "repomd")) goto done;
repomd_path = path_join(output, "repomd");
primary_path = path_join(output, "primary");
catalog_path = path_join(output, "catalog");
result = 2;
if (!repomd_path || !primary_path || !catalog_path ||
!hash_file(repomd_path, actual, 16 * 1024 * 1024) || strcmp(actual, expected) ||
!repomd_primary(repomd_path, href, primary_hash) ||
!holy_fetch_at(primary, dir, primary_hash, staged) ||
renameat(dir, staged, dir, "primary") ||
!hash_file(primary_path, actual, 128 * 1024 * 1024) || strcmp(actual, primary_hash)) goto done;
expanded = tmpfile();
if (!expanded || !unpack_primary(primary_path, expanded) ||
!parse_primary(expanded, &rows, &count)) goto done;
result = 1;
{
int fd = openat(dir, "catalog", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
if (fd < 0) goto done;
catalog = fdopen(fd, "w");
if (!catalog) { close(fd); goto done; }
}
fputs("format holy-rpm-md-catalog-1\n", catalog);
for (i = 0; i < count; ++i)
fprintf(catalog, "package %s %s %s %s %llu %s\n", rows[i].name, rows[i].evr,
rows[i].arch, rows[i].hash, rows[i].size, rows[i].href);
if (ferror(catalog) || fflush(catalog) || fsync(fileno(catalog))) goto done;
if (fclose(catalog)) { catalog = NULL; goto done; }
catalog = NULL;
if (!hash_file(catalog_path, catalog_hash, 256 * 1024 * 1024)) goto done;
if (!write_capabilities(dir, rows, count, capabilities_hash)) goto done;
for (i = 0; i < count; ++i) provides += rows[i].provide_count;
{
int fd = openat(dir, "conversion", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
if (fd < 0) goto done;
record = fdopen(fd, "w");
if (!record) { close(fd); goto done; }
}
fprintf(record, "format holy-rpm-md-index-1\nsource %s\nbase %s\nrepomd-sha256 %s\nprimary-sha256 %s\ncatalog-sha256 %s\ncapabilities-sha256 %s\n",
source, base, expected, primary_hash, catalog_hash, capabilities_hash);
if (source_id) fprintf(record, "source-id %s\n", source_id);
fprintf(record, "packages %zu\nprovides %zu\nfile-coverage unavailable\ndependency-coverage partial\nstate complete\n",
count, provides);
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 RPM packages %s\n", count, expected);
result = 0;
done:
if (result) fprintf(stderr, "holypkg: rpm-md index incomplete (status %d)\n", result);
if (catalog) fclose(catalog);
if (record) fclose(record);
if (expanded) fclose(expanded);
if (dir >= 0) close(dir);
free(rows); free(repomd_path); free(primary_path); free(catalog_path);
return result;
}
static int open_catalog(const char *directory, struct rpmmd_catalog *state)
{
char *record_path = path_join(directory, "conversion");
char *catalog_path = path_join(directory, "catalog");
char *capabilities_path = path_join(directory, "capabilities");
char *repomd_path = path_join(directory, "repomd");
char *primary_path = path_join(directory, "primary");
char line[4096], actual[65];
struct stat st;
FILE *record = NULL;
int dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
int fd, complete = 0, ok = 0;
memset(state, 0, sizeof *state);
if (dir < 0 || !record_path || !catalog_path || !capabilities_path ||
!repomd_path || !primary_path ||
fstat(dir, &st) || 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-rpm-md-index-1\n")) goto done;
while (fgets(line, sizeof line, record)) {
char *value, *target = NULL;
size_t capacity = 0, length = strlen(line);
if (!length || line[length-1] != '\n') goto done;
line[length-1] = 0;
value = strchr(line, ' ');
if (value) *value++ = 0;
if (!strcmp(line, "source")) { target = state->source; capacity = sizeof state->source; }
else if (!strcmp(line, "base")) { target = state->base; capacity = sizeof state->base; }
else if (!strcmp(line, "repomd-sha256")) { target = state->repomd; capacity = sizeof state->repomd; }
else if (!strcmp(line, "primary-sha256")) { target = state->primary; capacity = sizeof state->primary; }
else if (!strcmp(line, "catalog-sha256")) { target = state->catalog; capacity = sizeof state->catalog; }
else if (!strcmp(line, "capabilities-sha256")) {
target = state->capabilities; capacity = sizeof state->capabilities;
}
else if (!strcmp(line, "source-id")) { target = state->source_id; capacity = sizeof state->source_id; }
else if (!strcmp(line, "state") && value && !strcmp(value, "complete") && !complete) {
complete = 1; continue;
} else if (!strcmp(line, "packages") || !strcmp(line, "provides") ||
!strcmp(line, "file-coverage") ||
!strcmp(line, "dependency-coverage")) continue;
else goto done;
if (!value || !*value || strlen(value) >= capacity || target[0]) goto done;
strcpy(target, value);
}
if (ferror(record) || !complete || !label(state->source, sizeof state->source) ||
!digest(state->repomd) || !digest(state->primary) || !digest(state->catalog) ||
!digest(state->capabilities) ||
(state->source_id[0] && !digest(state->source_id)) ||
!hash_file(repomd_path, actual, 16 * 1024 * 1024) || strcmp(actual, state->repomd) ||
!hash_file(primary_path, actual, 128 * 1024 * 1024) || strcmp(actual, state->primary) ||
!hash_file(catalog_path, actual, 256 * 1024 * 1024) || strcmp(actual, state->catalog) ||
!hash_file(capabilities_path, actual, 256 * 1024 * 1024) ||
strcmp(actual, state->capabilities)) goto done;
{
char *url = child_url(state->base, "repodata/repomd.xml");
if (!url) goto done;
free(url);
}
fd = openat(dir, "catalog", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0) goto done;
state->file = fdopen(fd, "r");
if (!state->file) { close(fd); goto done; }
if (!fgets(line, sizeof line, state->file) || strcmp(line, "format holy-rpm-md-catalog-1\n")) goto done;
ok = 1;
done:
if (!ok && state->file) { fclose(state->file); state->file = NULL; }
if (record) fclose(record);
if (dir >= 0) close(dir);
free(record_path); free(catalog_path); free(capabilities_path);
free(repomd_path); free(primary_path);
return ok;
}
static int next_row(FILE *file, struct rpmmd_row *row)
{
char line[2048], extra;
int parsed;
if (!fgets(line, sizeof line, file)) return ferror(file) ? -1 : 0;
if (!strchr(line, '\n')) return -1;
memset(row, 0, sizeof *row);
parsed = sscanf(line, "package %255s %255s %63s %64s %llu %1023s %c",
row->name, row->evr, row->arch, row->hash, &row->size, row->href, &extra);
return parsed == 6 && label(row->name, sizeof row->name) &&
holy_rpm_version_valid(row->evr) && label(row->arch, sizeof row->arch) &&
digest(row->hash) && href_valid(row->href) && row->size > 0 &&
row->size <= 1024ULL * 1024 * 1024 ? 1 : -1;
}
/* exact capability lookup; an absent hint is not proof of absence in the source. */
int holy_rpm_md_providers(const char *directory, const char *capability)
{
struct rpmmd_catalog catalog;
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 (!capability || !provide_label(capability) || !open_catalog(directory, &catalog)) return 6;
fclose(catalog.file);
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 > 256LL * 1024 * 1024 ||
!hash_fd(fd, actual) || strcmp(actual, catalog.capabilities) ||
lseek(fd, 0, SEEK_SET) < 0 || !(input = fdopen(fd, "r"))) goto done;
fd = -1;
if (!fgets(line, sizeof line, input) ||
strcmp(line, "format holy-rpm-md-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], evr[256], arch[64], hash[65];
int end = 0;
if (sscanf(line, "provides %255s %255s %255s %63s %64s%n",
found, name, evr, arch, hash, &end) != 5 ||
!end || line[end] != '\n' || line[end + 1] ||
!provide_label(found) || !label(name, 256) ||
!holy_rpm_version_valid(evr) || !label(arch, 64) || !digest(hash)) {
result = 2; goto done;
}
if (strcmp(found, capability)) continue;
if (++count > 10000) { result = 6; goto done; }
fprintf(out, "candidate %s %s %s %s index-hint\n", name, evr, 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_rpm_md_query(const char *directory, const char *query, int info)
{
struct rpmmd_catalog catalog;
struct rpmmd_row row;
size_t matches = 0;
int status;
if (!directory || !query || !*query) return 2;
if (!open_catalog(directory, &catalog)) return 6;
while ((status = next_row(catalog.file, &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\npath %s\n",
row.name, row.evr, row.arch, row.hash, row.size, row.href);
else printf("%s %s %s\n", row.name, row.evr, row.arch);
}
}
fclose(catalog.file);
return status < 0 ? 2 : !matches ? 4 : info && matches > 1 ? 3 : 0;
}
/* the bound catalog must name this exact source-id and mirror base. */
int holy_rpm_md_source_catalog(const char *directory, const char *id, const char *base)
{
struct rpmmd_catalog state;
int ok;
if (!directory || !digest(id) || !base || !open_catalog(directory, &state)) return 6;
ok = !strcmp(state.source_id, id) && !strcmp(state.base, base);
fclose(state.file);
return ok ? 0 : 6;
}
static int binding_name(const char *id, char name[130])
{
unsigned char sum[32];
unsigned length;
size_t i;
if (!digest(id) ||
EVP_Digest(id, strlen(id), sum, &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", sum[i]);
return 1;
}
static int binding_dir(int database, int create)
{
struct stat st;
int dir;
if (create && mkdirat(database, "rpm-md-catalogs", 0700) && errno != EEXIST) return -1;
dir = openat(database, "rpm-md-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_rpm_md_bind(const char *root, const char *source, const char *catalog)
{
char id[65], name[130], digest_value[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_rpm_md(root, source, id, &base, &trust);
if (result) goto done;
if (!binding_name(id, name) || !catalog || !(path = realpath(catalog, NULL))) {
result = 2; goto done;
}
if (strchr(path, '\n') || strlen(path) > 1024 * 1024) { result = 2; goto done; }
result = holy_rpm_md_source_catalog(path, id, base);
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_value)) { result = 6; goto done; }
close(fd); fd = -1;
size = strlen(path) + 256;
record = malloc(size);
if (!record) { result = 1; goto done; }
snprintf(record, size, "format holy-rpm-md-binding-1\nsource-id %s\nconversion-sha256 %s\n%s %s\n",
id, digest_value, path_key, stored_path);
size = strlen(record);
database = holy_state_lock(root, 1, &generation, &result);
if (database < 0) goto done;
dir = 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_rpm_md_catalog_path(const char *root, const char *source, char **catalog)
{
struct stat st;
char id[65], name[130], saved_id[65], saved_digest[65], actual[65];
char *base = NULL, *trust = NULL, *line = NULL, *path = NULL;
char *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_rpm_md(root, source, id, &base, &trust);
if (result) goto done;
if (!binding_name(id, name)) { result = 2; goto done; }
database = holy_state_lock(root, 0, &generation, &result);
if (database < 0) goto done;
dir = 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-rpm-md-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, "conversion-sha256 %64s\n", saved_digest) != 1 ||
!digest(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_rpm_md_source_catalog(path, id, base);
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;
}
int holy_rpm_md_sync(const char *base, const char *expected, const char *source,
const char *output, const char *ca_file, const char *source_id)
{
char temporary[] = "/tmp/holy-rpm-md-XXXXXX", href[1024] = {0};
char hash[65] = {0}, received[65] = {0};
char *repomd_url = NULL, *primary_url = NULL, *repomd = NULL, *primary = NULL;
int result = 2;
if (!base || !digest(expected) || !source || !label(source, 256) ||
(source_id && !digest(source_id)) || !output ||
!(repomd_url = child_url(base, "repodata/repomd.xml"))) goto done;
if (!mkdtemp(temporary)) { result = 1; goto done; }
repomd = path_join(temporary, expected);
if (!repomd) { result = 1; goto cleanup; }
result = holy_fetch_https_data(repomd_url, expected, temporary, ca_file);
if (result) goto cleanup;
result = 2;
if (!repomd_primary(repomd, href, hash) || !(primary_url = child_url(base, href))) goto cleanup;
result = holy_fetch_https_foreign_limited(primary_url, temporary, ca_file, received,
128ULL * 1024 * 1024);
if (result) goto cleanup;
if (strcmp(hash, received)) { result = 4; goto cleanup; }
primary = path_join(temporary, hash);
if (!primary) { result = 1; goto cleanup; }
result = holy_rpm_md_index(repomd, primary, expected, source, base, output, source_id);
cleanup:
if (primary) unlink(primary);
if (repomd) unlink(repomd);
rmdir(temporary);
done:
free(repomd_url); free(primary_url); free(repomd); free(primary);
return result;
}
int holy_rpm_md_fetch(const char *directory, const char *name, const char *evr,
const char *arch, const char *output, const char *ca_file,
int import)
{
struct rpmmd_catalog catalog;
struct rpmmd_row row, selected = {0};
struct stat st;
char digest_bytes[65] = {0};
char *url = NULL, *download = NULL, *converted = NULL;
size_t matches = 0;
int status, result = 1;
if (!directory || !label(name, 256) || !holy_rpm_version_valid(evr) ||
!label(arch, 64) || !output) return 2;
if (!open_catalog(directory, &catalog)) return 6;
while ((status = next_row(catalog.file, &row)) > 0) {
if (!strcmp(name, row.name) && !strcmp(evr, row.evr) && !strcmp(arch, row.arch)) {
if (!matches) selected = row;
++matches;
}
}
fclose(catalog.file);
if (status < 0) return 2;
if (!matches) return 4;
if (matches > 1) return 3;
url = child_url(catalog.base, selected.href);
if (!url) { result = 2; goto done; }
if (mkdir(output, 0700)) goto done;
result = holy_fetch_https_foreign_limited(url, output, ca_file, digest_bytes, selected.size);
if (result) goto done;
if (strcmp(digest_bytes, selected.hash)) { result = 4; goto done; }
download = path_join(output, digest_bytes);
if (!download) { result = 1; goto done; }
if (stat(download, &st) || !S_ISREG(st.st_mode) ||
(unsigned long long)st.st_size != selected.size) { result = 4; goto done; }
if (import) {
converted = path_join(output, "converted");
if (!converted) { result = 1; goto done; }
result = holy_import_rpm(download, catalog.source, converted);
if (result) goto done;
}
printf("fetched %s %s %s %s\n", selected.name, selected.evr, selected.arch, selected.hash);
result = 0;
done:
free(url); free(download); free(converted);
return result;
}
#endif