#define _POSIX_C_SOURCE 200809L #include "repo.h" #include "config.h" #include "fetch.h" #include "cache.h" #include "deps.h" #include "extract.h" #include "provides.h" #include "resolve.h" #include "package.h" #include "scan.h" #include "stage.h" #include "verify.h" #include "sign.h" #include "version.h" #include "../backends/pacman.h" #include "../backends/deb-version.h" #include "../backends/apk-version.h" #include "../backends/xbps-version.h" #include #include #include #include #include #include #include #include #include #include #include #include #include static int same_identity(const struct holy_package_identity *a, const struct holy_package_identity *b) { return !strcmp(a->name, b->name) && !strcmp(a->version, b->version) && !strcmp(a->release, b->release) && !strcmp(a->os, b->os) && !strcmp(a->arch, b->arch) && !strcmp(a->libc, b->libc); } static int same_slot(const struct holy_package_identity *a, const struct holy_package_identity *b) { return !strcmp(a->name, b->name) && !strcmp(a->os, b->os) && !strcmp(a->arch, b->arch) && !strcmp(a->libc, b->libc); } struct claim { char *kind, *name, *arch, *libc, *version, *evidence; }; struct indexed_requirement { char *fields[10]; }; struct soname_fact { char *name, *arch, *libc, *path; }; struct version_fact { char *path, *name; }; struct export_fact { char *path, *name, *version; unsigned binding, type, visibility, hidden; }; struct object { char *filename; struct holy_package_identity identity; int provider_match; struct claim *claims; size_t claim_count; struct indexed_requirement *requirements; size_t requirement_count; struct soname_fact *sonames; size_t soname_count; struct version_fact *versions; size_t version_count; struct export_fact *exports; size_t export_count; char **files; size_t file_count; }; static void free_files(struct object *object) { size_t i; for (i = 0; i < object->file_count; ++i) free(object->files[i]); free(object->files); } static int add_file(struct object *object, const char *path) { char **next; char *copy; if (object->file_count == (size_t)-1 / sizeof *object->files) return 0; copy = strdup(path); if (!copy) return 0; next = realloc(object->files, (object->file_count + 1) * sizeof *object->files); if (!next) { free(copy); return 0; } object->files = next; object->files[object->file_count++] = copy; return 1; } static int collect_file(void *opaque, const struct holy_manifest_entry *entry) { return entry->directory || add_file(opaque, entry->path); } struct file_cursor { struct object *object; size_t index; }; static int compare_file(void *opaque, const struct holy_manifest_entry *entry) { struct file_cursor *cursor = opaque; if (entry->directory) return 1; return cursor->index < cursor->object->file_count && !strcmp(cursor->object->files[cursor->index++], entry->path); } static void free_claims(struct object *object) { size_t i; for (i = 0; i < object->claim_count; ++i) { struct claim *c = &object->claims[i]; free(c->kind); free(c->name); free(c->arch); free(c->libc); free(c->version); free(c->evidence); } free(object->claims); } static int add_claim(struct object *object, const char *kind, const char *name, const char *arch, const char *libc, const char *version, const char *evidence) { struct claim c = {0}, *next; if (object->claim_count == (size_t)-1 / sizeof *object->claims) return 0; c.kind = strdup(kind); c.name = strdup(name); c.arch = strdup(arch); c.libc = strdup(libc); c.version = strdup(version); c.evidence = strdup(evidence); if (!c.kind || !c.name || !c.arch || !c.libc || !c.version || !c.evidence) { free(c.kind); free(c.name); free(c.arch); free(c.libc); free(c.version); free(c.evidence); return 0; } next = realloc(object->claims, (object->claim_count + 1) * sizeof *object->claims); if (!next) { free(c.kind); free(c.name); free(c.arch); free(c.libc); free(c.version); free(c.evidence); return 0; } object->claims = next; object->claims[object->claim_count++] = c; return 1; } static int collect_claim(void *opaque, const char *kind, const char *name, const char *arch, const char *libc, const char *version, const char *evidence) { return add_claim(opaque, kind, name, arch, libc, version, evidence); } struct claim_cursor { struct object *object; size_t index; }; static int compare_claim(void *opaque, const char *kind, const char *name, const char *arch, const char *libc, const char *version, const char *evidence) { struct claim_cursor *cursor = opaque; struct claim *c; if (cursor->index == cursor->object->claim_count) return 0; c = &cursor->object->claims[cursor->index++]; return !strcmp(c->kind, kind) && !strcmp(c->name, name) && !strcmp(c->arch, arch) && !strcmp(c->libc, libc) && !strcmp(c->version, version) && !strcmp(c->evidence, evidence); } static void free_requirements(struct object *object) { size_t i, j; for (i = 0; i < object->requirement_count; ++i) for (j = 0; j < 10; ++j) free(object->requirements[i].fields[j]); free(object->requirements); } static int add_requirement(struct object *object, const char *fields[10]) { struct indexed_requirement item = {{0}}, *next; size_t i; if (object->requirement_count >= 1024 * 1024 / 11) return 0; for (i = 0; i < 10; ++i) { item.fields[i] = strdup(fields[i]); if (!item.fields[i]) goto fail; } next = realloc(object->requirements, (object->requirement_count + 1) * sizeof *next); if (!next) goto fail; object->requirements = next; object->requirements[object->requirement_count++] = item; return 1; fail: for (i = 0; i < 10; ++i) free(item.fields[i]); return 0; } static int collect_requirement(void *opaque, const char *id, const char *consumer, const char *kind, const char *name, const char *arch, const char *libc, const char *relation, const char *version, const char *original, const char *evidence) { const char *fields[] = {id, consumer, kind, name, arch, libc, relation, version, original, evidence}; return add_requirement(opaque, fields); } struct requirement_cursor { struct object *object; size_t index; }; static int compare_requirement(void *opaque, const char *id, const char *consumer, const char *kind, const char *name, const char *arch, const char *libc, const char *relation, const char *version, const char *original, const char *evidence) { struct requirement_cursor *cursor = opaque; const char *fields[] = {id, consumer, kind, name, arch, libc, relation, version, original, evidence}; size_t i; if (cursor->index == cursor->object->requirement_count) return 0; for (i = 0; i < 10; ++i) if (strcmp(cursor->object->requirements[cursor->index].fields[i], fields[i])) return 0; ++cursor->index; return 1; } static void free_sonames(struct object *object) { size_t i; for (i = 0; i < object->soname_count; ++i) { free(object->sonames[i].name); free(object->sonames[i].arch); free(object->sonames[i].libc); free(object->sonames[i].path); } free(object->sonames); } static int add_soname(struct object *object, const char *name, const char *arch, const char *libc, const char *path) { struct soname_fact fact = {0}, *next; if (object->soname_count >= 100000) return 0; fact.name = strdup(name); fact.arch = strdup(arch); fact.libc = strdup(libc); fact.path = strdup(path); if (!fact.name || !fact.arch || !fact.libc || !fact.path) goto fail; next = realloc(object->sonames, (object->soname_count + 1) * sizeof *next); if (!next) goto fail; object->sonames = next; object->sonames[object->soname_count++] = fact; return 1; fail: free(fact.name); free(fact.arch); free(fact.libc); free(fact.path); return 0; } static int soname_order(const void *left, const void *right) { const struct soname_fact *a = left, *b = right; int cmp = strcmp(a->path, b->path); return cmp ? cmp : strcmp(a->name, b->name); } static void free_versions(struct object *object) { size_t i; for (i = 0; i < object->version_count; ++i) { free(object->versions[i].path); free(object->versions[i].name); } free(object->versions); } static int version_order(const void *left, const void *right) { const struct version_fact *a = left, *b = right; int order = strcmp(a->path, b->path); return order ? order : strcmp(a->name, b->name); } static int add_version(struct object *object, const char *path, const char *name) { struct version_fact fact = {0}, *next; if (object->version_count >= 100000) return 0; fact.path = strdup(path); fact.name = strdup(name); if (!fact.path || !fact.name) goto fail; next = realloc(object->versions, (object->version_count + 1) * sizeof *next); if (!next) goto fail; object->versions = next; object->versions[object->version_count++] = fact; return 1; fail: free(fact.path); free(fact.name); return 0; } static void free_exports(struct object *object) { size_t i; for (i = 0; i < object->export_count; ++i) { free(object->exports[i].path); free(object->exports[i].name); free(object->exports[i].version); } free(object->exports); } static int export_order(const void *left, const void *right) { const struct export_fact *a = left, *b = right; int order = strcmp(a->path, b->path); if (!order) order = strcmp(a->name, b->name); if (!order) order = strcmp(a->version, b->version); if (!order && a->binding != b->binding) order = a->binding < b->binding ? -1 : 1; if (!order && a->type != b->type) order = a->type < b->type ? -1 : 1; if (!order && a->visibility != b->visibility) order = a->visibility < b->visibility ? -1 : 1; if (!order && a->hidden != b->hidden) order = a->hidden < b->hidden ? -1 : 1; return order; } static int add_export(struct object *object, const char *path, const struct holy_elf_symbol *symbol) { struct export_fact fact = {0}, *next; if (object->export_count >= 500000) return 0; fact.path = strdup(path); fact.name = strdup(symbol->name); fact.version = strdup(symbol->version ? symbol->version : ""); fact.binding = symbol->binding; fact.type = symbol->type; fact.visibility = symbol->visibility; fact.hidden = !!symbol->version_hidden; if (!fact.path || !fact.name || !fact.version) goto fail; next = realloc(object->exports, (object->export_count + 1) * sizeof *next); if (!next) goto fail; object->exports = next; object->exports[object->export_count++] = fact; return 1; fail: free(fact.path); free(fact.name); free(fact.version); return 0; } static int collect_sonames(const char *snapshot, struct object *object) { struct holy_scan_result scan = {0}; size_t i; int ok = holy_scan_collect(snapshot, &scan); for (i = 0; ok && i < scan.count; ++i) { const struct holy_scanned_file *file = &scan.files[i]; if (file->elf.type == ET_DYN && !(file->elf.flags1 & DF_1_PIE) && file->elf.soname) { size_t j; ok = add_soname(object, file->elf.soname, holy_elf_machine(&file->elf), file->runtime, file->path); for (j = 0; ok && j < file->elf.defined_version_count; ++j) ok = add_version(object, file->path, file->elf.defined_versions[j].name); for (j = 0; ok && j < file->elf.symbol_count; ++j) { const struct holy_elf_symbol *s = &file->elf.symbols[j]; if (s->section && s->name[0] && (s->binding == STB_GLOBAL || s->binding == STB_WEAK || s->binding == STB_GNU_UNIQUE) && (s->visibility == STV_DEFAULT || s->visibility == STV_PROTECTED)) ok = add_export(object, file->path, s); } } } holy_scan_free(&scan); if (ok && object->soname_count) qsort(object->sonames, object->soname_count, sizeof *object->sonames, soname_order); if (ok && object->version_count) { qsort(object->versions, object->version_count, sizeof *object->versions, version_order); for (i = 1; i < object->version_count; ++i) if (!version_order(&object->versions[i - 1], &object->versions[i])) return 0; } if (ok && object->export_count) qsort(object->exports, object->export_count, sizeof *object->exports, export_order); return ok; } static int compare_sonames(const char *snapshot, struct object *object, int versions, int exports) { struct object actual = {0}; size_t i; int ok = collect_sonames(snapshot, &actual) && actual.soname_count == object->soname_count; for (i = 0; ok && i < actual.soname_count; ++i) { const struct soname_fact *a = &actual.sonames[i], *b = &object->sonames[i]; ok = !strcmp(a->name, b->name) && !strcmp(a->arch, b->arch) && !strcmp(a->libc, b->libc) && !strcmp(a->path, b->path); } if (versions && actual.version_count != object->version_count) ok = 0; for (i = 0; ok && versions && i < actual.version_count; ++i) ok = !strcmp(actual.versions[i].path, object->versions[i].path) && !strcmp(actual.versions[i].name, object->versions[i].name); if (exports && actual.export_count != object->export_count) ok = 0; for (i = 0; ok && exports && i < actual.export_count; ++i) ok = !export_order(&actual.exports[i], &object->exports[i]); free_sonames(&actual); free_versions(&actual); free_exports(&actual); return ok; } static int compare_names(const void *left, const void *right) { const struct object *a = left, *b = right; return strcmp(a->filename, b->filename); } static int quote(FILE *fp, const char *value) { const unsigned char *p = (const unsigned char *)value; if (fputc('"', fp) == EOF) return 0; for (; *p; ++p) { if (*p == '"' || *p == '\\') { if (fputc('\\', fp) == EOF || fputc(*p, fp) == EOF) return 0; } else if (*p <= 32 || *p >= 127) { if (fprintf(fp, "\\x%02x", (unsigned int)*p) < 0) return 0; } else if (fputc(*p, fp) == EOF) return 0; } return fputc('"', fp) != EOF; } static void json_string(const char *value) { const unsigned char *p = (const unsigned char *)value; putchar('"'); for (; *p; ++p) { if (*p == '"' || *p == '\\') { putchar('\\'); putchar(*p); } else if (*p >= 32 && *p < 127) putchar(*p); else printf("\\u%04x", (unsigned int)*p); } putchar('"'); } static void candidate_json(const struct object *object) { const struct holy_package_identity *id = &object->identity; fputs("{\"schema\":\"holy-repo-candidates-1\",\"type\":\"candidate\",\"name\":", stdout); json_string(id->name); fputs(",\"version\":", stdout); json_string(id->version); fputs(",\"release\":", stdout); json_string(id->release); fputs(",\"os\":", stdout); json_string(id->os); fputs(",\"arch\":", stdout); json_string(id->arch); fputs(",\"libc\":", stdout); json_string(id->libc); fputs(",\"filename\":", stdout); json_string(object->filename); printf(",\"sha256\":\"%s\",\"size\":%" PRIu64 "}\n", id->digest, id->size); } static int record(FILE *fp, const struct object *object) { const struct holy_package_identity *id = &object->identity; const char *values[] = {id->name, id->version, id->release, id->os, id->arch, id->libc, object->filename}; size_t i; if (fputs("package", fp) == EOF) return 0; for (i = 0; i < sizeof values / sizeof *values; ++i) { if (fputc(' ', fp) == EOF || !quote(fp, values[i])) return 0; } if (fprintf(fp, " %s %" PRIu64 " ", id->digest, id->size) < 0) return 0; return quote(fp, id->version_family ? id->version_family : "-") && fputc('\n', fp) != EOF; } static int claim_record(FILE *fp, const struct object *object, const struct claim *claim) { const char *values[] = {claim->kind, claim->name, claim->arch, claim->libc, claim->version, claim->evidence}; size_t i; if (fprintf(fp, "claim %s", object->identity.digest) < 0) return 0; for (i = 0; i < sizeof values / sizeof *values; ++i) if (fputc(' ', fp) == EOF || !quote(fp, values[i])) return 0; return fputc('\n', fp) != EOF; } static int requirement_record(FILE *fp, const struct object *object, const struct indexed_requirement *requirement) { size_t i; if (fprintf(fp, "require %s", object->identity.digest) < 0) return 0; for (i = 0; i < 10; ++i) if (fputc(' ', fp) == EOF || !quote(fp, requirement->fields[i])) return 0; return fputc('\n', fp) != EOF; } static int soname_record(FILE *fp, const struct object *object, const struct soname_fact *fact) { const char *values[] = {fact->name, fact->arch, fact->libc, fact->path}; size_t i; if (fprintf(fp, "soname %s", object->identity.digest) < 0) return 0; for (i = 0; i < 4; ++i) if (fputc(' ', fp) == EOF || !quote(fp, values[i])) return 0; return fputc('\n', fp) != EOF; } static int version_record(FILE *fp, const struct object *object, const struct version_fact *fact) { return fprintf(fp, "elf-version %s ", object->identity.digest) >= 0 && quote(fp, fact->path) && fputc(' ', fp) != EOF && quote(fp, fact->name) && fputc('\n', fp) != EOF; } static int export_record(FILE *fp, const struct object *object, const struct export_fact *fact) { return fprintf(fp, "elf-export %s ", object->identity.digest) >= 0 && quote(fp, fact->path) && fputc(' ', fp) != EOF && quote(fp, fact->name) && fputc(' ', fp) != EOF && quote(fp, fact->version) && fprintf(fp, " %u %u %u %u\n", fact->binding, fact->type, fact->visibility, fact->hidden) >= 0; } static int file_record(FILE *fp, const struct object *object, const char *path) { return fprintf(fp, "file %s ", object->identity.digest) >= 0 && quote(fp, path) && fputc('\n', fp) != EOF; } static int digest_file(const char *path, char hex[65]) { EVP_MD_CTX *ctx = EVP_MD_CTX_new(); unsigned char hash[32], buffer[65536]; unsigned int length; size_t n, i; FILE *fp = fopen(path, "rb"); int ok = fp && ctx && EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) == 1; while (ok && (n = fread(buffer, 1, sizeof buffer, fp)) > 0) ok = EVP_DigestUpdate(ctx, buffer, n) == 1; if (ok) ok = !ferror(fp) && EVP_DigestFinal_ex(ctx, hash, &length) == 1 && length == sizeof hash; if (ok) { for (i = 0; i < sizeof hash; ++i) snprintf(hex + i * 2, 3, "%02x", hash[i]); hex[64] = '\0'; } if (fp) fclose(fp); EVP_MD_CTX_free(ctx); return ok; } static int read_current(int dir, char digest[65]) { char line[72]; struct stat st; size_t i; int fd = openat(dir, "current", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK); if (fd < 0) return errno == ENOENT ? 0 : -1; if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size != sizeof line || pread(fd, line, sizeof line, 0) != sizeof line || memcmp(line, "sha256 ", 7) || line[71] != '\n') { close(fd); return -1; } close(fd); for (i = 0; i < 64; ++i) if (!((line[i + 7] >= '0' && line[i + 7] <= '9') || (line[i + 7] >= 'a' && line[i + 7] <= 'f'))) return -1; memcpy(digest, line + 7, 64); digest[64] = '\0'; return 1; } int holy_repo_index(const char *directory) { struct object *objects = NULL; DIR *listing = NULL; struct dirent *entry; struct stat st; size_t count = 0, i, j; int dir = -1, temp = -1, ok = 0; FILE *stream = NULL; char temporary[43] = {0}; dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); if (dir < 0 || flock(dir, LOCK_EX) < 0) goto done; listing = fdopendir(dup(dir)); if (!listing) goto done; errno = 0; while ((entry = readdir(listing)) != NULL) { size_t length = strlen(entry->d_name); struct object *next; if (length < 6 || strcmp(entry->d_name + length - 5, ".holy")) continue; if (count == (size_t)-1 / sizeof *objects) goto done; next = realloc(objects, (count + 1) * sizeof *objects); if (!next) goto done; objects = next; memset(&objects[count], 0, sizeof *objects); objects[count].filename = strdup(entry->d_name); if (!objects[count].filename) goto done; ++count; errno = 0; } if (errno) goto done; closedir(listing); listing = NULL; if (count) qsort(objects, count, sizeof *objects, compare_names); for (i = 0; i < count; ++i) { int input = openat(dir, objects[i].filename, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK); char *snapshot; if (input < 0) goto done; snapshot = holy_stage_fd(input, "holy-index"); close(input); if (!snapshot) goto done; if (!holy_verify_with_output(snapshot, 0) || !holy_scan_local_with_output(snapshot, 0) || !holy_deps_local_with_output(snapshot, 0) || !holy_provides_local(snapshot, 0) || !holy_package_identity(snapshot, &objects[i].identity) || !holy_provides_visit(snapshot, collect_claim, &objects[i]) || !holy_deps_visit(snapshot, collect_requirement, &objects[i]) || !collect_sonames(snapshot, &objects[i]) || !holy_verify_visit(snapshot, collect_file, &objects[i])) { unlink(snapshot); free(snapshot); goto done; } unlink(snapshot); free(snapshot); for (j = 0; j < i; ++j) { struct holy_package_identity *a = &objects[i].identity; struct holy_package_identity *b = &objects[j].identity; if (same_identity(a, b)) { fprintf(stderr, "holypkg: duplicate package identity\n"); goto done; } } } if (fstatat(dir, "index", &st, AT_SYMLINK_NOFOLLOW) == 0) { if (!S_ISREG(st.st_mode)) { fprintf(stderr, "holypkg: index is not a regular file\n"); goto done; } } else if (errno != ENOENT) { goto done; } temp = holy_temporary_at(dir, temporary); if (temp < 0) goto done; stream = fdopen(temp, "w"); if (!stream) goto done; temp = -1; if (fputs("format holy-index-prototype-8\ncoverage files complete\ncoverage dependencies complete\ncoverage elf-sonames complete\ncoverage elf-versions complete\ncoverage elf-exports complete\n", stream) == EOF) goto done; for (i = 0; i < count; ++i) { if (!record(stream, &objects[i])) goto done; for (j = 0; j < objects[i].claim_count; ++j) if (!claim_record(stream, &objects[i], &objects[i].claims[j])) goto done; for (j = 0; j < objects[i].requirement_count; ++j) if (!requirement_record(stream, &objects[i], &objects[i].requirements[j])) goto done; for (j = 0; j < objects[i].soname_count; ++j) if (!soname_record(stream, &objects[i], &objects[i].sonames[j])) goto done; for (j = 0; j < objects[i].version_count; ++j) if (!version_record(stream, &objects[i], &objects[i].versions[j])) goto done; for (j = 0; j < objects[i].export_count; ++j) if (!export_record(stream, &objects[i], &objects[i].exports[j])) goto done; for (j = 0; j < objects[i].file_count; ++j) if (!file_record(stream, &objects[i], objects[i].files[j])) goto done; } if (fflush(stream) || ftello(stream) < 0 || ftello(stream) > 128LL * 1024 * 1024 || fchmod(fileno(stream), 0644) || fsync(fileno(stream))) goto done; if (fclose(stream)) { stream = NULL; goto done; } stream = NULL; if (renameat(dir, temporary, dir, "index") || fsync(dir)) goto done; temporary[0] = '\0'; printf("indexed %zu packages\n", count); ok = 1; done: if (!ok) fprintf(stderr, "holypkg: repository index incomplete\n"); if (stream) fclose(stream); if (temp >= 0) close(temp); if (temporary[0] && dir >= 0) unlinkat(dir, temporary, 0); if (listing) closedir(listing); for (i = 0; i < count; ++i) { free(objects[i].filename); holy_package_identity_free(&objects[i].identity); free_claims(&objects[i]); free_requirements(&objects[i]); free_sonames(&objects[i]); free_versions(&objects[i]); free_exports(&objects[i]); free_files(&objects[i]); } free(objects); if (dir >= 0) close(dir); return ok; } static int parse_record(char **v, size_t n, struct object *object) { unsigned long long size; char *end; size_t i; const char *filename; if ((n != 10 && n != 11) || strcmp(v[0], "package") || (n == 11 && (!v[10][0] || strlen(v[10]) > 128))) return 0; filename = v[7]; i = strlen(filename); if (i < 6 || strcmp(filename + i - 5, ".holy") || strchr(filename, '/') || strlen(v[8]) != 64 || !v[1][0] || !v[2][0] || !v[3][0]) return 0; for (i = 0; i < 64; ++i) if (!((v[8][i] >= '0' && v[8][i] <= '9') || (v[8][i] >= 'a' && v[8][i] <= 'f'))) return 0; errno = 0; size = strtoull(v[9], &end, 10); if (errno || !v[9][0] || *end || v[9][0] == '-' || !size) return 0; object->filename = v[7]; v[7] = NULL; object->identity.name = v[1]; v[1] = NULL; object->identity.version = v[2]; v[2] = NULL; object->identity.release = v[3]; v[3] = NULL; object->identity.os = v[4]; v[4] = NULL; object->identity.arch = v[5]; v[5] = NULL; object->identity.libc = v[6]; v[6] = NULL; if (n == 11 && strcmp(v[10], "-")) { object->identity.version_family = v[10]; v[10] = NULL; } memcpy(object->identity.digest, v[8], 65); object->identity.size = size; return 1; } static int parse_claim(char **v, size_t n, struct object *object) { size_t i; if (n != 8 || strcmp(v[0], "claim") || strcmp(v[1], object->identity.digest) || !holy_provides_claim_valid(v[2], v[3], v[4], v[5], v[6], v[7])) return 0; for (i = 0; i < object->claim_count; ++i) { const struct claim *c = &object->claims[i]; if (!strcmp(c->kind, v[2]) && !strcmp(c->name, v[3]) && !strcmp(c->arch, v[4]) && !strcmp(c->libc, v[5]) && !strcmp(c->version, v[6])) return 0; } return add_claim(object, v[2], v[3], v[4], v[5], v[6], v[7]); } static int parse_requirement(char **v, size_t n, struct object *object) { size_t i; const char *fields[10]; if (n != 12 || strcmp(v[0], "require") || strcmp(v[1], object->identity.digest) || !holy_deps_record_valid(v[2], v[3], v[4], v[5], v[6], v[7], v[8], v[9], v[10], v[11])) return 0; for (i = 0; i < object->requirement_count; ++i) if (!strcmp(object->requirements[i].fields[0], v[2])) return 0; for (i = 0; i < 10; ++i) fields[i] = v[i + 2]; return add_requirement(object, fields); } static int safe_target_path(const char *path) { size_t length = strlen(path); char *archive_path; int valid; if (!length || length > (size_t)-1 - 6 || path[length - 1] == '/') return 0; archive_path = malloc(length + 6); if (!archive_path) return 0; memcpy(archive_path, "DATA/", 5); memcpy(archive_path + 5, path, length + 1); valid = holy_safe_archive_path(archive_path); free(archive_path); return valid; } static int parse_file(char **v, size_t n, struct object *object) { if (n != 3 || strcmp(v[0], "file") || strcmp(v[1], object->identity.digest) || !safe_target_path(v[2]) || (object->file_count && strcmp(object->files[object->file_count - 1], v[2]) >= 0)) return 0; return add_file(object, v[2]); } static int parse_soname(char **v, size_t n, struct object *object) { struct soname_fact current; if (n != 6 || strcmp(v[0], "soname") || strcmp(v[1], object->identity.digest) || !v[2][0] || (strcmp(v[3], "x86") && strcmp(v[3], "x86_64")) || (strcmp(v[4], "glibc") && strcmp(v[4], "musl")) || !safe_target_path(v[5])) return 0; current.name = v[2]; current.arch = v[3]; current.libc = v[4]; current.path = v[5]; if (object->soname_count && soname_order(&object->sonames[object->soname_count - 1], ¤t) >= 0) return 0; return add_soname(object, v[2], v[3], v[4], v[5]); } static int parse_version(char **v, size_t n, struct object *object) { struct version_fact current; size_t i; if (n != 4 || strcmp(v[0], "elf-version") || strcmp(v[1], object->identity.digest) || !safe_target_path(v[2]) || !v[3][0]) return 0; for (i = 0; i < object->soname_count; ++i) if (!strcmp(object->sonames[i].path, v[2])) break; if (i == object->soname_count) return 0; current.path = v[2]; current.name = v[3]; if (object->version_count && version_order(&object->versions[object->version_count - 1], ¤t) >= 0) return 0; return add_version(object, v[2], v[3]); } static int parse_export(char **v, size_t n, struct object *object) { struct holy_elf_symbol symbol = {0}; struct export_fact current = {0}; unsigned *values[] = {&symbol.binding, &symbol.type, &symbol.visibility, ¤t.hidden}; size_t i; if (n != 9 || strcmp(v[0], "elf-export") || strcmp(v[1], object->identity.digest) || !safe_target_path(v[2]) || !v[3][0]) return 0; for (i = 0; i < object->soname_count; ++i) if (!strcmp(object->sonames[i].path, v[2])) break; if (i == object->soname_count) return 0; for (i = 0; i < 4; ++i) { char *end; unsigned long value; errno = 0; value = strtoul(v[5 + i], &end, 10); if (errno || !v[5 + i][0] || *end || value > 255) return 0; *values[i] = (unsigned)value; } if ((symbol.binding != STB_GLOBAL && symbol.binding != STB_WEAK && symbol.binding != STB_GNU_UNIQUE) || (symbol.visibility != STV_DEFAULT && symbol.visibility != STV_PROTECTED) || current.hidden > 1) return 0; symbol.name = v[3]; symbol.version = v[4]; symbol.version_hidden = (int)current.hidden; current.path = v[2]; current.name = v[3]; current.version = v[4]; current.binding = symbol.binding; current.type = symbol.type; current.visibility = symbol.visibility; if (object->export_count && export_order(&object->exports[object->export_count - 1], ¤t) > 0) return 0; return add_export(object, v[2], &symbol); } static int indexed_file(const struct object *object, const char *path) { size_t low = 0, high = object->file_count; while (low < high) { size_t middle = low + (high - low) / 2; int cmp = strcmp(object->files[middle], path); if (!cmp) return 1; if (cmp < 0) low = middle + 1; else high = middle; } return 0; } struct or_match { const struct object *object; int found; }; static int indexed_version_matches(const struct object *object, const char *candidate, const char *relation, const char *version, int identity) { const char *family = object->identity.version_family; const char *actual = candidate; char *joined = NULL; int order, compared; if (!strcmp(relation, "any")) return 1; if (!family || !candidate || !strcmp(candidate, "-")) return 0; if (identity && !strcmp(family, "xbps") && object->identity.release) { size_t a = strlen(candidate), b = strlen(object->identity.release); if (a > SIZE_MAX - b - 2 || !(joined = malloc(a + b + 2))) return 0; snprintf(joined, a + b + 2, "%s_%s", candidate, object->identity.release); actual = joined; } compared = !strcmp(family, "pacman") ? holy_pacman_version_compare(actual, version, &order) : !strcmp(family, "deb") ? holy_deb_version_compare(actual, version, &order) : !strcmp(family, "holy") ? holy_version_compare(actual, version, &order) : !strcmp(family, "apk") ? holy_apk_version_compare(actual, version, &order) : !strcmp(family, "xbps") ? holy_xbps_version_compare(actual, version, &order) : 0; free(joined); if (!compared) return 0; return !strcmp(relation, "eq") ? order == 0 : !strcmp(relation, "ge") ? order >= 0 : !strcmp(relation, "gt") ? order > 0 : !strcmp(relation, "le") ? order <= 0 : !strcmp(relation, "lt") && order < 0; } static int indexed_or_branch(void *opaque, const char *name, const char *relation, const char *version) { struct or_match *match = opaque; size_t i; if (!strcmp(match->object->identity.name, name) && indexed_version_matches(match->object, match->object->identity.version, relation, version, 1)) match->found = 1; for (i = 0; i < match->object->claim_count; ++i) if (!strcmp(match->object->claims[i].kind, "package") && !strcmp(match->object->claims[i].name, name) && indexed_version_matches(match->object, match->object->claims[i].version, relation, version, 0)) match->found = 1; return 1; } static int indexed_provider(const struct object *object, const char *kind, const char *name, int file_index, int soname_index) { static const char *const dirs[] = {"usr/bin/", "bin/", "usr/sbin/", "sbin/"}; size_t i; if (!strcmp(kind, "package-or")) { struct or_match match = {object, 0}; return holy_package_or_each(name, indexed_or_branch, &match) && match.found; } if (!strcmp(kind, "package") && !strcmp(object->identity.name, name)) return 1; if (!strcmp(kind, "soname")) { if (!soname_index) return 0; for (i = 0; i < object->soname_count; ++i) if (!strcmp(object->sonames[i].name, name)) return 1; return 0; } if (!strcmp(kind, "file")) return file_index && name[0] == '/' && indexed_file(object, name + 1); if (!strcmp(kind, "command")) { if (!file_index || !*name || strchr(name, '/')) return 0; for (i = 0; i < sizeof dirs / sizeof *dirs; ++i) { size_t a = strlen(dirs[i]), b = strlen(name); char *path; int found; if (a > SIZE_MAX - b - 1) return 0; path = malloc(a + b + 1); if (!path) return 0; memcpy(path, dirs[i], a); memcpy(path + a, name, b + 1); found = indexed_file(object, path); free(path); if (found) return 1; } return 0; } for (i = 0; i < object->claim_count; ++i) if (!strcmp(object->claims[i].kind, kind) && !strcmp(object->claims[i].name, name)) return 1; return 0; } static unsigned char ascii_lower(unsigned char c) { return c >= 'A' && c <= 'Z' ? (unsigned char)(c + 'a' - 'A') : c; } static int ascii_prefix(const char *text, const char *prefix) { while (*prefix) { if (!*text || ascii_lower((unsigned char)*text++) != ascii_lower((unsigned char)*prefix++)) return 0; } return 1; } static int fuzzy_rank(const char *query, const char *target) { size_t q = strlen(query), t = strlen(target), i, j; unsigned int prev[129], next[129], row_min; if (!q || !t || q > 256 || t > 256) return -1; if (ascii_prefix(target, query)) return q == t ? 0 : 1; for (i = 1; i < t; ++i) if (ascii_prefix(target + i, query)) return 2; if (q > 128 || t > 128 || q > t + 2 || t > q + 2) return -1; for (j = 0; j <= t; ++j) prev[j] = (unsigned int)j; for (i = 1; i <= q; ++i) { next[0] = (unsigned int)i; row_min = next[0]; for (j = 1; j <= t; ++j) { unsigned int cost = ascii_lower((unsigned char)query[i - 1]) == ascii_lower((unsigned char)target[j - 1]) ? 0 : 1; unsigned int deletion = prev[j] + 1; unsigned int insertion = next[j - 1] + 1; unsigned int replacement = prev[j - 1] + cost; next[j] = deletion < insertion ? deletion : insertion; if (replacement < next[j]) next[j] = replacement; if (next[j] < row_min) row_min = next[j]; } if (row_min > 2) return -1; memcpy(prev, next, (t + 1) * sizeof *prev); } return prev[t] == 1 ? 3 : prev[t] == 2 ? 4 : -1; } static int file_hint(const struct object *object, const char *query, const char **matched) { const char *basename = strrchr(query, '/'); size_t i; int best = -1; basename = basename ? basename + 1 : query; *matched = NULL; if (!*basename) return -1; for (i = 0; i < object->file_count; ++i) { const char *path = object->files[i], *name = strrchr(path, '/'); int score = fuzzy_rank(basename, name ? name + 1 : path); if (query[0] == '/' && score >= 0) { if (!strcmp(query + 1, path)) score = 0; else ++score; } if (score >= 0 && (best < 0 || score < best || (score == best && strcmp(path, *matched) < 0))) { best = score; *matched = path; } } return best; } struct mirror { const char *base, *ca_file, *downloads; int status; }; struct stage_request { const char *root; struct holy_repo_set *set; const struct holy_package_identity *slot; const char *digest; int index_only; int provider; struct holy_repo_slot_list *candidates; }; static int copy_identity(struct holy_package_identity *to, const struct holy_package_identity *from) { memset(to, 0, sizeof *to); to->name = strdup(from->name); to->version = strdup(from->version); to->release = strdup(from->release); to->version_family = from->version_family ? strdup(from->version_family) : NULL; to->os = strdup(from->os); to->arch = strdup(from->arch); to->libc = strdup(from->libc); memcpy(to->digest, from->digest, sizeof to->digest); to->size = from->size; return to->name && to->version && to->release && to->os && to->arch && to->libc && (!from->version_family || to->version_family); } struct closure { unsigned char *selected; size_t *queue; size_t count; struct provider_key *providers; size_t provider_count; }; struct provider_key { const char *kind, *name; size_t index; }; static int provider_key_order(const void *left, const void *right) { const struct provider_key *a = left, *b = right; int order = strcmp(a->kind, b->kind); if (!order) order = strcmp(a->name, b->name); return order ? order : a->index < b->index ? -1 : a->index > b->index; } static int closure_index(const struct object *objects, size_t count, struct closure *closure) { static const char *const dirs[] = {"usr/bin/", "bin/", "usr/sbin/", "sbin/"}; size_t capacity = count, i, j, k, used = 0; struct provider_key *keys; for (i = 0; i < count; ++i) { const struct object *o = &objects[i]; size_t extra; if (o->claim_count > SIZE_MAX - o->soname_count) return 0; extra = o->claim_count + o->soname_count; if (o->file_count > (SIZE_MAX - extra) / 2) return 0; extra += o->file_count * 2; if (capacity > SIZE_MAX - extra) return 0; capacity += extra; } if (capacity > SIZE_MAX / sizeof *keys) return 0; keys = calloc(capacity ? capacity : 1, sizeof *keys); if (!keys) return 0; for (i = 0; i < count; ++i) { const struct object *o = &objects[i]; keys[used++] = (struct provider_key){"package", o->identity.name, i}; for (j = 0; j < o->claim_count; ++j) keys[used++] = (struct provider_key){o->claims[j].kind, o->claims[j].name, i}; for (j = 0; j < o->soname_count; ++j) keys[used++] = (struct provider_key){"soname", o->sonames[j].name, i}; for (j = 0; j < o->file_count; ++j) { const char *path = o->files[j]; keys[used++] = (struct provider_key){"file", path, i}; for (k = 0; k < sizeof dirs / sizeof *dirs; ++k) if (!strncmp(path, dirs[k], strlen(dirs[k])) && path[strlen(dirs[k])]) { keys[used++] = (struct provider_key){"command", path + strlen(dirs[k]), i}; break; } } } qsort(keys, used, sizeof *keys, provider_key_order); closure->providers = keys; closure->provider_count = used; return 1; } static int closure_add(struct closure *closure, size_t index) { if (!closure->selected[index]) { closure->selected[index] = 1; closure->queue[closure->count++] = index; } return 1; } static int closure_provider(struct closure *closure, const char *kind, const char *name) { size_t low = 0, high = closure->provider_count, i; const char *file = !strcmp(kind, "file") && name[0] == '/' ? name + 1 : name; while (low < high) { size_t middle = low + (high - low) / 2; const struct provider_key *key = &closure->providers[middle]; int order = strcmp(key->kind, kind); if (!order) order = strcmp(key->name, file); if (order < 0) low = middle + 1; else high = middle; } for (i = low; i < closure->provider_count; ++i) { const struct provider_key *key = &closure->providers[i]; if (strcmp(key->kind, kind) || strcmp(key->name, file)) break; if (!closure_add(closure, key->index)) return 0; } return 1; } static int candidate_symbols(int dir, const struct object *object, const struct soname_fact *fact, const struct holy_scanned_file *consumer, const char *needed) { struct holy_scan_result scan = {0}; char *snapshot = NULL, actual[65]; size_t i, j; int fd, result = 0, required = 0; for (i = 0; i < consumer->elf.symbol_count; ++i) { const struct holy_elf_symbol *symbol = &consumer->elf.symbols[i]; if (!symbol->section && symbol->binding != STB_WEAK && symbol->provider && !strcmp(symbol->provider, needed)) { required = 1; break; } } if (!required) return 1; fd = openat(dir, object->filename, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK); if (fd < 0) return -1; snapshot = holy_stage_fd(fd, "holy-soname-probe"); close(fd); if (!snapshot || !digest_file(snapshot, actual) || strcmp(actual, object->identity.digest) || !holy_scan_collect(snapshot, &scan)) { result = -1; goto done; } for (i = 0; i < scan.count; ++i) { const struct holy_scanned_file *file = &scan.files[i]; if (strcmp(file->path, fact->path) || file->elf.type != ET_DYN || (file->elf.flags1 & DF_1_PIE) || !file->elf.soname || strcmp(file->elf.soname, needed) || strcmp(holy_elf_machine(&file->elf), fact->arch) || strcmp(file->runtime, fact->libc)) continue; result = 1; for (j = 0; j < consumer->elf.symbol_count; ++j) { const struct holy_elf_symbol *symbol = &consumer->elf.symbols[j]; if (symbol->section || symbol->binding == STB_WEAK || !symbol->provider || strcmp(symbol->provider, needed)) continue; if (!holy_elf_exports_symbol(&file->elf, symbol)) { result = 0; break; } } break; } done: holy_scan_free(&scan); if (snapshot) { unlink(snapshot); free(snapshot); } return result; } static int indexed_exports_symbol(const struct object *object, const char *path, const struct holy_elf_symbol *wanted) { size_t i; for (i = 0; i < object->export_count; ++i) { const struct export_fact *s = &object->exports[i]; if (strcmp(s->path, path) || strcmp(s->name, wanted->name) || (wanted->type == STT_TLS) != (s->type == STT_TLS) || (wanted->type == STT_FUNC && s->type != STT_FUNC && s->type != 10) || (wanted->type == STT_OBJECT && s->type != STT_OBJECT) || (wanted->version && strcmp(s->version, wanted->version)) || (!wanted->version && s->hidden)) continue; return 1; } return 0; } static int candidate_exports(const struct object *object, const struct soname_fact *fact, const struct holy_scanned_file *consumer, const char *needed) { size_t i; for (i = 0; i < consumer->elf.symbol_count; ++i) { const struct holy_elf_symbol *want = &consumer->elf.symbols[i]; if (want->section || want->binding == STB_WEAK || !want->provider || strcmp(want->provider, needed)) continue; if (!indexed_exports_symbol(object, fact->path, want)) return 0; } return 1; } static int object_soname_matches(int dir, const struct object *object, const struct holy_scanned_file *consumer, const char *needed, int export_index) { size_t j, k; for (j = 0; j < object->soname_count; ++j) { const struct soname_fact *fact = &object->sonames[j]; int compatible = 1, symbols; if (strcmp(fact->name, needed) || strcmp(fact->arch, holy_elf_machine(&consumer->elf)) || strcmp(fact->libc, consumer->runtime)) continue; for (k = 0; k < consumer->elf.version_count; ++k) { const struct holy_elf_version *want = &consumer->elf.versions[k]; size_t n; if (want->weak || strcmp(want->provider, needed)) continue; for (n = 0; n < object->version_count; ++n) if (!strcmp(object->versions[n].path, fact->path) && !strcmp(object->versions[n].name, want->name)) break; if (n == object->version_count) { compatible = 0; break; } } if (!compatible) continue; symbols = export_index ? candidate_exports(object, fact, consumer, needed) : candidate_symbols(dir, object, fact, consumer, needed); if (symbols) return symbols; } return 0; } static int closure_soname(int dir, const struct object *objects, struct closure *closure, const struct holy_scanned_file *consumer, const char *needed, int export_index) { size_t low = 0, high = closure->provider_count, i; while (low < high) { size_t middle = low + (high - low) / 2; const struct provider_key *key = &closure->providers[middle]; int order = strcmp(key->kind, "soname"); if (!order) order = strcmp(key->name, needed); if (order < 0) low = middle + 1; else high = middle; } for (i = low; i < closure->provider_count; ++i) { const struct provider_key *key = &closure->providers[i]; const struct object *object = &objects[key->index]; if (strcmp(key->kind, "soname") || strcmp(key->name, needed)) break; int matches = object_soname_matches(dir, object, consumer, needed, export_index); if (matches < 0) return 0; if (matches && !closure_add(closure, key->index)) return 0; } return 1; } static int closure_or_provider(void *opaque, const char *name, const char *relation, const char *version) { (void)relation; (void)version; return closure_provider(opaque, "package", name); } static int closure_expand(int dir, const struct object *objects, struct closure *closure, size_t index, const char *snapshot, int version_index, int export_index) { const struct object *o = &objects[index]; struct holy_scan_result scan = {0}; size_t i, j; int ok = 1; for (i = 0; i < o->requirement_count && ok; ++i) { const char *kind = o->requirements[i].fields[2]; const char *name = o->requirements[i].fields[3]; ok = !strcmp(kind, "package-or") ? holy_package_or_each(name, closure_or_provider, closure) : closure_provider(closure, kind, name); } if (ok) ok = holy_scan_collect(snapshot, &scan); for (i = 0; i < scan.count && ok; ++i) { const struct holy_elf_info *elf = &scan.files[i].elf; if (elf->interpreter && elf->interpreter[0] == '/') ok = closure_provider(closure, "file", elf->interpreter); for (j = 0; j < elf->needed_count && ok; ++j) ok = version_index && elf->needed[j][0] != '/' ? closure_soname(dir, objects, closure, &scan.files[i], elf->needed[j], export_index) : closure_provider(closure, elf->needed[j][0] == '/' ? "file" : "soname", elf->needed[j]); } for (i = 0; i < scan.script_count && ok; ++i) if (scan.scripts[i].kind == 1 && scan.scripts[i].interpreter[0] == '/') ok = closure_provider(closure, "file", scan.scripts[i].interpreter); for (i = 0; i < scan.symlink_count && ok; ++i) { char *path = scan.symlinks[i].target[0] == '/' ? strdup(scan.symlinks[i].target) : holy_relative_link_path(scan.symlinks[i].path, strlen(scan.symlinks[i].path), scan.symlinks[i].target, ""); if (!path) { ok = 0; break; } ok = closure_provider(closure, "file", path); free(path); } holy_scan_free(&scan); return ok; } static int mirror_object(struct mirror *mirror, int dir, const struct object *object) { char *url = holy_fetch_child_url(mirror->base, object->filename); char name[70]; int downloads, ok; if (!url) { mirror->status = 2; return 0; } mirror->status = holy_fetch_https(url, object->identity.digest, mirror->downloads, mirror->ca_file, 0); free(url); if (mirror->status) return 0; downloads = open(mirror->downloads, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); if (downloads < 0) { mirror->status = 1; return 0; } snprintf(name, sizeof name, "%s.holy", object->identity.digest); ok = !linkat(downloads, name, dir, object->filename, 0) && !fsync(dir); if (ok) ok = !unlinkat(downloads, name, 0) && !fsync(downloads); close(downloads); if (!ok) mirror->status = 1; return ok; } static int list_probe(const char *directory, const char *query, const char *forced_index, int lock, int emit, const char *fetch_digest, const char *output, const char *provider_kind, const char *provider_name, const char *solve_name, const char *solve_choice, int solve_json, int *solve_rc, struct mirror *mirror, int extract_name, const struct stage_request *stage, const char *file_query, const struct holy_scanned_file *probe) { struct object *objects = NULL; char **candidate_snapshots = NULL; struct closure closure = {0}; struct stat st; FILE *index = NULL; char *line = NULL, *error = NULL, *index_snapshot = NULL, *chosen = NULL; char expected[65], actual_digest[65], index_name[71]; size_t capacity = 0, count = 0, i, j, number = 0, cursor; ssize_t length; int dir = -1, fd = -1, ok = 0, indexed = 0, file_index = 0; int dependency_index = 0, soname_index = 0, version_index = 0; int export_index = 0, comparator_index = 0; dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); if (dir < 0 || (lock && flock(dir, LOCK_SH) < 0)) goto done; if (forced_index) { if (!*forced_index || strlen(forced_index) >= sizeof index_name || strchr(forced_index, '/')) goto done; strcpy(index_name, forced_index); } else { if (read_current(dir, expected) != 1) goto done; snprintf(index_name, sizeof index_name, "index.%s", expected); } fd = openat(dir, index_name, 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 > 128LL * 1024 * 1024) goto done; index_snapshot = holy_stage_fd(fd, "holy-catalog"); close(fd); fd = -1; if (!index_snapshot || (!forced_index && (!digest_file(index_snapshot, actual_digest) || strcmp(actual_digest, expected)))) goto done; index = fopen(index_snapshot, "r"); if (!index) goto done; while ((length = getline(&line, &capacity, index)) >= 0) { char **v = NULL; size_t n = 0; struct object *next; ++number; if (length > 1024 * 1024 || memchr(line, '\0', (size_t)length) || !holy_lex(line, (size_t)length, &v, &n, "index", number, &error)) { holy_tokens_free(v, n); goto done; } if (number == 1) { int valid = n == 2 && !strcmp(v[0], "format") && (!strcmp(v[1], "holy-index-prototype-1") || !strcmp(v[1], "holy-index-prototype-2") || !strcmp(v[1], "holy-index-prototype-3") || !strcmp(v[1], "holy-index-prototype-4") || !strcmp(v[1], "holy-index-prototype-5") || !strcmp(v[1], "holy-index-prototype-6") || !strcmp(v[1], "holy-index-prototype-7") || !strcmp(v[1], "holy-index-prototype-8")); if (valid) { indexed = strcmp(v[1], "holy-index-prototype-1") != 0; file_index = !strcmp(v[1], "holy-index-prototype-3") || !strcmp(v[1], "holy-index-prototype-4") || !strcmp(v[1], "holy-index-prototype-5") || !strcmp(v[1], "holy-index-prototype-6") || !strcmp(v[1], "holy-index-prototype-7") || !strcmp(v[1], "holy-index-prototype-8"); dependency_index = !strcmp(v[1], "holy-index-prototype-4") || !strcmp(v[1], "holy-index-prototype-5") || !strcmp(v[1], "holy-index-prototype-6") || !strcmp(v[1], "holy-index-prototype-7") || !strcmp(v[1], "holy-index-prototype-8"); soname_index = !strcmp(v[1], "holy-index-prototype-5") || !strcmp(v[1], "holy-index-prototype-6") || !strcmp(v[1], "holy-index-prototype-7") || !strcmp(v[1], "holy-index-prototype-8"); version_index = !strcmp(v[1], "holy-index-prototype-6") || !strcmp(v[1], "holy-index-prototype-7") || !strcmp(v[1], "holy-index-prototype-8"); export_index = !strcmp(v[1], "holy-index-prototype-7") || !strcmp(v[1], "holy-index-prototype-8"); comparator_index = !strcmp(v[1], "holy-index-prototype-8"); } holy_tokens_free(v, n); if (!valid) goto done; continue; } if (file_index && number == 2) { int valid = n == 3 && !strcmp(v[0], "coverage") && !strcmp(v[1], "files") && !strcmp(v[2], "complete"); holy_tokens_free(v, n); if (!valid) goto done; continue; } if (dependency_index && number == 3) { int valid = n == 3 && !strcmp(v[0], "coverage") && !strcmp(v[1], "dependencies") && !strcmp(v[2], "complete"); holy_tokens_free(v, n); if (!valid) goto done; continue; } if (soname_index && number == 4) { int valid = n == 3 && !strcmp(v[0], "coverage") && !strcmp(v[1], "elf-sonames") && !strcmp(v[2], "complete"); holy_tokens_free(v, n); if (!valid) goto done; continue; } if (version_index && number == 5) { int valid = n == 3 && !strcmp(v[0], "coverage") && !strcmp(v[1], "elf-versions") && !strcmp(v[2], "complete"); holy_tokens_free(v, n); if (!valid) goto done; continue; } if (export_index && number == 6) { int valid = n == 3 && !strcmp(v[0], "coverage") && !strcmp(v[1], "elf-exports") && !strcmp(v[2], "complete"); holy_tokens_free(v, n); if (!valid) goto done; continue; } if (indexed && n && !strcmp(v[0], "claim")) { int valid = count && !objects[count - 1].file_count && !objects[count - 1].requirement_count && !objects[count - 1].soname_count && !objects[count - 1].version_count && !objects[count - 1].export_count && parse_claim(v, n, &objects[count - 1]); holy_tokens_free(v, n); if (!valid) goto done; continue; } if (dependency_index && n && !strcmp(v[0], "require")) { int valid = count && !objects[count - 1].file_count && !objects[count - 1].soname_count && !objects[count - 1].version_count && !objects[count - 1].export_count && parse_requirement(v, n, &objects[count - 1]); holy_tokens_free(v, n); if (!valid) goto done; continue; } if (soname_index && n && !strcmp(v[0], "soname")) { int valid = count && !objects[count - 1].file_count && !objects[count - 1].version_count && !objects[count - 1].export_count && parse_soname(v, n, &objects[count - 1]); holy_tokens_free(v, n); if (!valid) goto done; continue; } if (version_index && n && !strcmp(v[0], "elf-version")) { int valid = count && !objects[count - 1].file_count && !objects[count - 1].export_count && parse_version(v, n, &objects[count - 1]); holy_tokens_free(v, n); if (!valid) goto done; continue; } if (export_index && n && !strcmp(v[0], "elf-export")) { int valid = count && !objects[count - 1].file_count && parse_export(v, n, &objects[count - 1]); holy_tokens_free(v, n); if (!valid) goto done; continue; } if (file_index && n && !strcmp(v[0], "file")) { int valid = count && parse_file(v, n, &objects[count - 1]); holy_tokens_free(v, n); if (!valid) goto done; continue; } if (count == (size_t)-1 / sizeof *objects) { holy_tokens_free(v, n); goto done; } next = realloc(objects, (count + 1) * sizeof *objects); if (!next) { holy_tokens_free(v, n); goto done; } objects = next; memset(&objects[count], 0, sizeof *objects); if ((comparator_index && n != 11) || !parse_record(v, n, &objects[count])) { holy_tokens_free(v, n); goto done; } holy_tokens_free(v, n); ++count; for (i = 0; i + 1 < count; ++i) if (!strcmp(objects[i].filename, objects[count - 1].filename) || same_identity(&objects[i].identity, &objects[count - 1].identity)) goto done; } if (ferror(index) || !number || (file_index && number < 2) || (dependency_index && number < 3) || (soname_index && number < 4) || (version_index && number < 5) || (export_index && number < 6)) goto done; if (emit == 8 && (((!strcmp(provider_kind, "file") || !strcmp(provider_kind, "command")) && !file_index) || (!strcmp(provider_kind, "soname") && !soname_index) || (probe && !version_index) || !strcmp(provider_kind, "symbol-version"))) { *solve_rc = 6; ok = 1; goto done; } if (mirror) { for (i = 0; i < count; ++i) if (!mirror_object(mirror, dir, &objects[i])) goto done; } if (solve_name || (stage && stage->provider)) { candidate_snapshots = calloc(count ? count : 1, sizeof *candidate_snapshots); if (!candidate_snapshots) goto done; } if (stage && !stage->slot && !stage->index_only && dependency_index && file_index && soname_index) { size_t root = count, roots = 0; if (!stage->provider) { for (i = 0; i < count; ++i) if (!strcmp(objects[i].identity.name, solve_name)) { root = i; ++roots; } if (roots != 1) { *solve_rc = roots ? 3 : 6; fprintf(stderr, "holypkg: repository root %s\n", roots ? "requires package choice" : "not found"); ok = 1; goto done; } } closure.selected = calloc(count, 1); closure.queue = calloc(count, sizeof *closure.queue); if (!closure.selected || !closure.queue || !closure_index(objects, count, &closure)) goto done; if (stage->provider) { for (i = 0; i < count; ++i) if (indexed_provider(&objects[i], provider_kind, provider_name, file_index, soname_index)) closure_add(&closure, i); if (!closure.count) { *solve_rc = 4; ok = 1; goto done; } } else closure_add(&closure, root); } if (stage && stage->provider && !closure.selected) { *solve_rc = 6; ok = 1; goto done; } for (cursor = 0; cursor < (closure.selected ? closure.count : count); ++cursor) { struct holy_package_identity actual; char *snapshot; int input, matches; i = closure.selected ? closure.queue[cursor] : cursor; if (stage && (stage->index_only || (stage->slot && !same_slot(&objects[i].identity, stage->slot)) || (stage->digest && strcmp(objects[i].identity.digest, stage->digest)))) continue; if (query && (emit == 1 || emit == 3) && strcmp(query, objects[i].identity.name)) continue; if (fetch_digest && strcmp(fetch_digest, objects[i].identity.digest)) continue; if (solve_name && output && !stage && strcmp(solve_name, objects[i].identity.name)) continue; if (emit == 5 && query && fuzzy_rank(query, objects[i].identity.name) < 0) continue; if (file_query) { const char *matched; if (!file_index || (emit == 6 ? file_hint(&objects[i], file_query, &matched) < 0 : !indexed_file(&objects[i], file_query))) continue; } if (indexed && provider_kind && !(stage && stage->provider)) { size_t k; objects[i].provider_match = emit == 8 ? indexed_provider(&objects[i], provider_kind, provider_name, file_index, soname_index) : !strcmp(provider_kind, "package") && !strcmp(provider_name, objects[i].identity.name); if (emit == 8 && probe && objects[i].provider_match) { int match = object_soname_matches(dir, &objects[i], probe, provider_name, export_index); if (match < 0) goto done; objects[i].provider_match = match; } if (emit == 8) { if (!objects[i].provider_match) continue; } else if (soname_index && !strcmp(provider_kind, "soname")) { for (k = 0; k < objects[i].soname_count; ++k) if (!strcmp(objects[i].sonames[k].name, provider_name)) objects[i].provider_match = 1; } else for (k = 0; k < objects[i].claim_count; ++k) if (!strcmp(objects[i].claims[k].kind, provider_kind) && !strcmp(objects[i].claims[k].name, provider_name)) objects[i].provider_match = 1; if (!objects[i].provider_match) continue; } if (emit == 7 || (emit == 8 && indexed)) continue; input = openat(dir, objects[i].filename, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK); if (input < 0) goto done; snapshot = holy_stage_fd(input, "holy-list"); close(input); if (!snapshot) goto done; matches = holy_verify_with_output(snapshot, 0) && holy_scan_local_with_output(snapshot, 0) && holy_deps_local_with_output(snapshot, 0) && holy_provides_local(snapshot, 0) && holy_package_identity(snapshot, &actual); if (matches) { matches = same_identity(&objects[i].identity, &actual) && (!comparator_index || (!objects[i].identity.version_family == !actual.version_family && (!actual.version_family || !strcmp(objects[i].identity.version_family, actual.version_family)))) && !strcmp(objects[i].identity.digest, actual.digest) && objects[i].identity.size == actual.size; holy_package_identity_free(&actual); } if (!matches) { unlink(snapshot); free(snapshot); goto done; } if (indexed) { struct claim_cursor cursor = {&objects[i], 0}; if (!holy_provides_visit(snapshot, compare_claim, &cursor) || cursor.index != objects[i].claim_count) { unlink(snapshot); free(snapshot); goto done; } } if (dependency_index) { struct requirement_cursor cursor = {&objects[i], 0}; if (!holy_deps_visit(snapshot, compare_requirement, &cursor) || cursor.index != objects[i].requirement_count) { unlink(snapshot); free(snapshot); goto done; } } if (soname_index && !compare_sonames(snapshot, &objects[i], version_index, export_index)) { unlink(snapshot); free(snapshot); goto done; } if (file_index) { struct file_cursor cursor = {&objects[i], 0}; if (!holy_verify_visit(snapshot, compare_file, &cursor) || cursor.index != objects[i].file_count) { unlink(snapshot); free(snapshot); goto done; } } if (closure.selected && !closure_expand(dir, objects, &closure, i, snapshot, version_index, export_index)) { unlink(snapshot); free(snapshot); goto done; } if (provider_kind && !indexed && !(stage && stage->provider)) { int claim = 0; if (!holy_provides_match(snapshot, provider_kind, provider_name, &claim)) { unlink(snapshot); free(snapshot); goto done; } objects[i].provider_match = claim || (!strcmp(provider_kind, "package") && !strcmp(provider_name, objects[i].identity.name)); } if (solve_name || (stage && stage->provider)) candidate_snapshots[i] = snapshot; else if (fetch_digest && !strcmp(fetch_digest, objects[i].identity.digest)) chosen = snapshot; else { unlink(snapshot); free(snapshot); } } if (fetch_digest && (!chosen || !holy_fetch_local(chosen, output))) goto done; if (solve_name) { const char **paths = NULL; size_t root = count, roots = 0, next = 1; for (i = 0; i < count; ++i) if (!strcmp(objects[i].identity.name, solve_name)) { root = i; ++roots; } if (roots != 1 && !(stage && stage->slot)) { *solve_rc = roots ? 3 : 6; fprintf(stderr, "holypkg: repository root %s\n", roots ? "requires package choice" : "not found"); if (solve_json) printf("{\"schema\":\"holy-local-solve-1\",\"type\":\"error\",\"code\":\"%s\"}\n", roots ? "decision-required" : "unavailable-artifact"); } else if (stage) { size_t position = 0; *solve_rc = 6; if (count > 100000) { ok = 1; goto done; } stage->set->digests = calloc(closure.selected ? closure.count : count, sizeof *stage->set->digests); if (!stage->set->digests) { *solve_rc = 1; ok = 1; goto done; } for (cursor = 0; cursor < (closure.selected ? closure.count : count); ++cursor) { i = closure.selected ? closure.queue[cursor] : cursor; if (stage->slot && !same_slot(&objects[i].identity, stage->slot)) continue; if (stage->digest && strcmp(objects[i].identity.digest, stage->digest)) continue; size_t selected = closure.selected ? i : stage->slot ? i : i ? (i <= root ? i - 1 : i) : root; char actual[65]; if (position >= 10000) { *solve_rc = 6; ok = 1; goto done; } if (!holy_cache_stage_local_digest(candidate_snapshots[selected], stage->root, actual)) { *solve_rc = 1; ok = 1; goto done; } if (strcmp(actual, objects[selected].identity.digest)) { *solve_rc = 4; ok = 1; goto done; } stage->set->digests[position] = strdup(actual); if (!stage->set->digests[position]) { *solve_rc = 1; ok = 1; goto done; } ++position; stage->set->count = position; } memcpy(stage->set->index, expected, 65); *solve_rc = position ? 0 : 6; } else if (output) { *solve_rc = (extract_name ? holy_extract_local(candidate_snapshots[root], output) : holy_fetch_local(candidate_snapshots[root], output)) ? 0 : 1; } else { paths = calloc(count, sizeof *paths); if (!paths) goto done; paths[0] = candidate_snapshots[root]; for (i = 0; i < count; ++i) if (i != root) paths[next++] = candidate_snapshots[i]; *solve_rc = holy_resolve_local(paths, count, solve_json, expected, solve_choice); free(paths); } } if (stage && stage->provider) { size_t position = 0; stage->set->digests = calloc(closure.count, sizeof *stage->set->digests); if (!stage->set->digests) { *solve_rc = 1; ok = 1; goto done; } for (cursor = 0; cursor < closure.count; ++cursor) { char actual[65]; i = closure.queue[cursor]; if (position >= 10000 || !candidate_snapshots[i] || !holy_cache_stage_local_digest(candidate_snapshots[i], stage->root, actual)) { *solve_rc = 6; ok = 1; goto done; } if (strcmp(actual, objects[i].identity.digest)) { *solve_rc = 4; ok = 1; goto done; } stage->set->digests[position] = strdup(actual); if (!stage->set->digests[position]) { *solve_rc = 1; ok = 1; goto done; } stage->set->count = ++position; } memcpy(stage->set->index, expected, 65); *solve_rc = position ? 0 : 6; } if (emit == 7) { size_t found = count, matches = 0; if (!dependency_index) { fputs("status unknown: source has no complete dependency index\n", stdout); if (solve_rc) *solve_rc = 6; } else { for (j = 0; j < count; ++j) if (!strcmp(objects[j].identity.name, query)) { found = j; ++matches; } if (matches == 1) { if (!record(stdout, &objects[found])) goto done; for (j = 0; j < objects[found].requirement_count; ++j) if (!requirement_record(stdout, &objects[found], &objects[found].requirements[j])) goto done; printf("requirements %zu\n", objects[found].requirement_count); if (solve_rc) *solve_rc = 0; } else { fprintf(stderr, "holypkg: repository package %s\n", matches ? "requires an architecture/ABI choice" : "not found"); if (solve_rc) *solve_rc = matches ? 3 : 6; } } } else if (emit == 5 || emit == 6) { size_t matches = 0, shown = 0; int rank; for (rank = 0; rank <= 5; ++rank) for (j = 0; j < count; ++j) { const char *matched = NULL; int score = emit == 5 ? fuzzy_rank(query, objects[j].identity.name) : file_index ? file_hint(&objects[j], file_query, &matched) : -1; if (score != rank) continue; ++matches; if (shown == 20) continue; printf("suggestion score %d %s ", score, emit == 5 ? "name" : "path"); if (!quote(stdout, emit == 5 ? objects[j].identity.name : matched) || fputc('\n', stdout) == EOF || !record(stdout, &objects[j])) goto done; ++shown; } if (emit == 6) { printf("coverage files %s index %s time %lld\n", file_index ? "complete" : "unavailable", expected, (long long)st.st_mtime); if (!file_index) { fputs("status unknown: source has no complete file index\n", stdout); if (solve_rc) *solve_rc = 6; } else if (solve_rc) *solve_rc = 0; } printf("suggested %zu of %zu %s\n", shown, matches, emit == 5 ? "packages" : "file candidates"); } else { size_t matches = 0, only = count; for (j = 0; j < count; ++j) { if (query && strcmp(objects[j].identity.name, query)) continue; if (provider_kind && !objects[j].provider_match) continue; if (file_query && (!file_index || !indexed_file(&objects[j], file_query))) continue; if (emit == 1 && !record(stdout, &objects[j])) goto done; if (emit == 2) candidate_json(&objects[j]); if (emit == 3) only = j; if (emit == 4 && !record(stdout, &objects[j])) goto done; ++matches; } if (emit == 1) printf("listed %zu %s\n", matches, provider_kind ? "candidates" : "packages"); if (emit == 2) printf("{\"schema\":\"holy-repo-candidates-1\",\"type\":\"summary\",\"count\":%zu}\n", matches); if (emit == 3) { *solve_rc = matches > 1 ? 3 : matches ? 0 : 6; if (matches == 1 && !record(stdout, &objects[only])) goto done; if (matches != 1) fprintf(stderr, "holypkg: repository package %s\n", matches ? "requires an architecture/ABI choice" : "not found"); } if (emit == 8 && solve_rc) *solve_rc = matches ? 0 : 4; if (emit == 4) { printf("coverage files %s index %s time %lld\n", file_index ? "complete" : "unavailable", expected, (long long)st.st_mtime); printf("listed %zu file candidates\n", matches); if (!file_index) { fputs("status unknown: source has no complete file index\n", stdout); if (solve_rc) *solve_rc = 6; } else if (solve_rc) *solve_rc = 0; } } if (stage && stage->index_only) { memcpy(stage->set->index, expected, 65); if (stage->candidates) { struct holy_repo_slot_list *list = stage->candidates; size_t matches = 0; for (i = 0; i < count; ++i) if (same_slot(&objects[i].identity, stage->slot)) ++matches; list->items = calloc(matches ? matches : 1, sizeof *list->items); if (!list->items) goto done; for (i = 0; i < count; ++i) if (same_slot(&objects[i].identity, stage->slot)) { if (!copy_identity(&list->items[list->count], &objects[i].identity)) { ++list->count; goto done; } ++list->count; } memcpy(list->index, expected, 65); *solve_rc = matches ? 0 : 6; } if (stage->digest) { *solve_rc = 3; for (i = 0; i < count; ++i) if (!strcmp(objects[i].identity.digest, stage->digest) && same_slot(&objects[i].identity, stage->slot)) { *solve_rc = 0; break; } } } ok = 1; done: if (!ok) { fprintf(stderr, "holypkg: invalid or stale repository index%s%s\n", error ? ": " : "", error ? error : ""); if (emit == 2) puts("{\"schema\":\"holy-repo-candidates-1\",\"type\":\"error\",\"code\":\"invalid-catalog\"}"); } free(error); free(line); if (index) fclose(index); if (index_snapshot) { unlink(index_snapshot); free(index_snapshot); } if (chosen) { unlink(chosen); free(chosen); } if (candidate_snapshots) { for (i = 0; i < count; ++i) if (candidate_snapshots[i]) { unlink(candidate_snapshots[i]); free(candidate_snapshots[i]); } free(candidate_snapshots); } if (fd >= 0) close(fd); for (i = 0; i < count; ++i) { free(objects[i].filename); holy_package_identity_free(&objects[i].identity); free_claims(&objects[i]); free_requirements(&objects[i]); free_sonames(&objects[i]); free_versions(&objects[i]); free_exports(&objects[i]); free_files(&objects[i]); } free(objects); free(closure.selected); free(closure.queue); free(closure.providers); if (dir >= 0) close(dir); return ok; } static int list(const char *directory, const char *query, const char *forced_index, int lock, int emit, const char *fetch_digest, const char *output, const char *provider_kind, const char *provider_name, const char *solve_name, const char *solve_choice, int solve_json, int *solve_rc, struct mirror *mirror, int extract_name, const struct stage_request *stage, const char *file_query) { return list_probe(directory, query, forced_index, lock, emit, fetch_digest, output, provider_kind, provider_name, solve_name, solve_choice, solve_json, solve_rc, mirror, extract_name, stage, file_query, NULL); } int holy_repo_list(const char *directory) { return list(directory, NULL, NULL, 1, 1, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, 0, NULL, NULL); } int holy_repo_search(const char *directory, const char *query) { if (!query || !*query) { fprintf(stderr, "holypkg: package name required\n"); return 0; } return list(directory, query, NULL, 1, 1, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, 0, NULL, NULL); } int holy_repo_search_fuzzy(const char *directory, const char *query) { if (!query || !*query) return 2; return list(directory, query, NULL, 1, 5, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, 0, NULL, NULL) ? 0 : 6; } int holy_repo_search_file(const char *directory, const char *query) { int result = 6; if (!query || query[0] != '/' || !query[1] || !safe_target_path(query + 1)) { fprintf(stderr, "holypkg: absolute file path required\n"); return 2; } if (!list(directory, NULL, NULL, 1, 4, NULL, NULL, NULL, NULL, NULL, NULL, 0, &result, NULL, 0, NULL, query + 1)) return 6; return result; } int holy_repo_search_file_fuzzy(const char *directory, const char *query) { int result = 6; if (!query || !*query || query[strlen(query) - 1] == '/' || (query[0] != '/' && strchr(query, '/'))) return 2; if (query[0] == '/' && !safe_target_path(query + 1)) return 2; if (!list(directory, NULL, NULL, 1, 6, NULL, NULL, NULL, NULL, NULL, NULL, 0, &result, NULL, 0, NULL, query)) return 6; return result; } int holy_repo_info_name(const char *directory, const char *name) { int result = 6; if (!name || !*name) return 2; if (!list(directory, name, NULL, 1, 3, NULL, NULL, NULL, NULL, NULL, NULL, 0, &result, NULL, 0, NULL, NULL)) return 6; return result; } int holy_repo_requirements(const char *directory, const char *name) { int result = 6; if (!name || !*name) return 2; if (!list(directory, name, NULL, 1, 7, NULL, NULL, NULL, NULL, NULL, NULL, 0, &result, NULL, 0, NULL, NULL)) return 6; return result; } int holy_repo_providers(const char *directory, const char *kind, const char *name, int json) { if (!holy_provides_kind(kind) || !name || !*name) { fprintf(stderr, "holypkg: provider kind and exact name required\n"); if (json) puts("{\"schema\":\"holy-repo-candidates-1\",\"type\":\"error\",\"code\":\"invalid-query\"}"); return 0; } return list(directory, NULL, NULL, 1, json ? 2 : 1, NULL, NULL, kind, name, NULL, NULL, 0, NULL, NULL, 0, NULL, NULL); } int holy_repo_has_provider(const char *directory, const char *kind, const char *name) { int result = 6; if ((!holy_provides_kind(kind) && strcmp(kind, "package-or")) || !name || !*name || (!strcmp(kind, "package-or") && !holy_package_or_each(name, NULL, NULL))) return 2; if (!list(directory, NULL, NULL, 1, 8, NULL, NULL, kind, name, NULL, NULL, 0, &result, NULL, 0, NULL, NULL)) return 6; return result; } int holy_repo_has_compatible_soname(const char *directory, const char *name, const char *root, const char *consumer_digest, const char *consumer_path) { struct holy_scan_result scan = {0}; const struct holy_scanned_file *consumer = NULL; char *snapshot; size_t i, j; int result = 6; if (!directory || !name || !*name || strchr(name, '/') || !root || !consumer_digest || strlen(consumer_digest) != 64 || !consumer_path) return 2; snapshot = holy_cache_snapshot(consumer_digest, root); if (!snapshot) return 6; if (!holy_scan_collect(snapshot, &scan)) goto done; for (i = 0; i < scan.count; ++i) { const struct holy_scanned_file *file = &scan.files[i]; if (strcmp(file->path, consumer_path)) continue; for (j = 0; j < file->elf.needed_count; ++j) if (!strcmp(file->elf.needed[j], name)) break; if (j == file->elf.needed_count || consumer) goto done; consumer = file; } if (!consumer) goto done; if (!list_probe(directory, NULL, NULL, 1, 8, NULL, NULL, "soname", name, NULL, NULL, 0, &result, NULL, 0, NULL, NULL, consumer)) result = 6; done: holy_scan_free(&scan); unlink(snapshot); free(snapshot); return result; } int holy_repo_solve(const char *directory, const char *name, const char *choice, int json) { int result = 6; if (!name || !*name) { fprintf(stderr, "holypkg: repository package name required\n"); if (json) puts("{\"schema\":\"holy-local-solve-1\",\"type\":\"error\",\"code\":\"invalid-query\"}"); return 2; } if (!list(directory, NULL, NULL, 1, 0, NULL, NULL, NULL, NULL, name, choice, json, &result, NULL, 0, NULL, NULL)) { if (json) puts("{\"schema\":\"holy-local-solve-1\",\"type\":\"error\",\"code\":\"invalid-catalog\"}"); return 6; } return result; } int holy_repo_fetch(const char *directory, const char *digest, const char *output) { size_t i; if (!digest || strlen(digest) != 64) goto invalid; for (i = 0; i < 64; ++i) if (!((digest[i] >= '0' && digest[i] <= '9') || (digest[i] >= 'a' && digest[i] <= 'f'))) goto invalid; return list(directory, NULL, NULL, 1, 0, digest, output, NULL, NULL, NULL, NULL, 0, NULL, NULL, 0, NULL, NULL); invalid: fprintf(stderr, "holypkg: expected a lowercase SHA-256 digest\n"); return 0; } int holy_repo_fetch_name(const char *directory, const char *name, const char *output, int extract) { int result = 6; if (!name || !*name || !output || !*output) return 2; if (!list(directory, NULL, NULL, 1, 0, NULL, output, NULL, NULL, name, NULL, 0, &result, NULL, extract, NULL, NULL)) return 6; return result; } void holy_repo_set_free(struct holy_repo_set *set) { size_t i; for (i = 0; i < set->count; ++i) free(set->digests[i]); free(set->digests); memset(set, 0, sizeof *set); } int holy_repo_stage_set(const char *directory, const char *name, const char *root, struct holy_repo_set *set) { struct stage_request stage = {root, set, NULL, NULL, 0, 0, NULL}; int result = 6; memset(set, 0, sizeof *set); if (!name || !*name || !root || !*root) return 2; if (!list(directory, NULL, NULL, 1, 0, NULL, NULL, NULL, NULL, name, NULL, 0, &result, NULL, 0, &stage, NULL)) { holy_repo_set_free(set); return 6; } if (result) holy_repo_set_free(set); return result; } int holy_repo_stage_provider(const char *directory, const char *kind, const char *name, const char *root, struct holy_repo_set *set) { struct stage_request stage = {root, set, NULL, NULL, 0, 1, NULL}; int result = 6; memset(set, 0, sizeof *set); if (!kind || (strcmp(kind, "package") && strcmp(kind, "package-or") && strcmp(kind, "file") && strcmp(kind, "command") && strcmp(kind, "soname")) || !name || !*name || !root || !*root || (!strcmp(kind, "package-or") && !holy_package_or_each(name, NULL, NULL))) return 2; if (!list(directory, NULL, NULL, 1, 0, NULL, NULL, kind, name, NULL, NULL, 0, &result, NULL, 0, &stage, NULL)) { holy_repo_set_free(set); return 6; } if (result) holy_repo_set_free(set); return result; } int holy_repo_stage_slot(const char *directory, const char *root, const struct holy_package_identity *slot, struct holy_repo_set *set) { struct stage_request stage = {root, set, slot, NULL, 0, 0, NULL}; int result = 6; memset(set, 0, sizeof *set); if (!root || !*root || !slot || !slot->name || !slot->os || !slot->arch || !slot->libc) return 2; if (!list(directory, NULL, NULL, 1, 0, NULL, NULL, NULL, NULL, slot->name, NULL, 0, &result, NULL, 0, &stage, NULL)) { holy_repo_set_free(set); return 6; } if (result) holy_repo_set_free(set); return result; } void holy_repo_slot_list_free(struct holy_repo_slot_list *list) { size_t i; if (!list) return; for (i = 0; i < list->count; ++i) holy_package_identity_free(&list->items[i]); free(list->items); memset(list, 0, sizeof *list); } int holy_repo_slot_candidates(const char *directory, const struct holy_package_identity *slot, struct holy_repo_slot_list *out) { struct holy_repo_set index = {0}; struct stage_request stage = {NULL, &index, slot, NULL, 1, 0, out}; int result = 6; if (!directory || !slot || !slot->name || !slot->os || !slot->arch || !slot->libc || !out) return 2; memset(out, 0, sizeof *out); if (!list(directory, NULL, NULL, 1, 0, NULL, NULL, NULL, NULL, NULL, NULL, 0, &result, NULL, 0, &stage, NULL)) result = 6; if (result) holy_repo_slot_list_free(out); return result; } int holy_repo_stage_slot_digest(const char *directory, const char *root, const struct holy_package_identity *slot, const char *digest, struct holy_repo_set *set) { struct stage_request stage = {root, set, slot, digest, 0, 0, NULL}; int result = 6; if (!directory || !root || !slot || !slot->name || !slot->os || !slot->arch || !slot->libc || !digest || strlen(digest) != 64 || strspn(digest, "0123456789abcdef") != 64 || !set) return 2; memset(set, 0, sizeof *set); if (!list(directory, NULL, NULL, 1, 0, NULL, NULL, NULL, NULL, slot->name, NULL, 0, &result, NULL, 0, &stage, NULL)) result = 6; if (result) holy_repo_set_free(set); return result; } int holy_repo_source_catalog(const char *directory, const char *source_id, const char *url, const char *public_key) { struct stat st; char digest[65], key_hash[65], *record = NULL, *expected = NULL; size_t used = 0, expected_size = 0; int dir = -1, fd = -1, ok = 0; FILE *stream = NULL; if (!source_id || strlen(source_id) != 64 || strspn(source_id, "0123456789abcdef") != 64 || !url) return 0; dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); if (dir < 0 || flock(dir, LOCK_SH) || read_current(dir, digest) != 1) goto done; if (public_key && !holy_verify_index_keyhash(dir, digest, public_key, key_hash)) goto done; fd = openat(dir, "mirror-origin", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK); if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || (st.st_mode & 0022) || (st.st_uid != geteuid() && st.st_uid != 0) || st.st_size <= 0 || st.st_size > 65536) goto done; record = malloc((size_t)st.st_size + 1); if (!record) goto done; while (used < (size_t)st.st_size) { ssize_t n = pread(fd, record + used, (size_t)st.st_size - used, (off_t)used); if (n < 0 && errno == EINTR) continue; if (n <= 0) goto done; used += (size_t)n; } record[used] = 0; stream = open_memstream(&expected, &expected_size); if (!stream) goto done; { int wrote = fputs("format holy-mirror-1\nurl ", stream) != EOF && quote(stream, url) && fprintf(stream, "\nindex-sha256 %s\nverification %s\n", digest, public_key ? "ed25519-pinned-key" : "digest-pinned-unsigned") >= 0 && (!public_key || fprintf(stream, "public-key-sha256 %s\n", key_hash) >= 0) && fprintf(stream, "source-id %s\n", source_id) >= 0 && !ferror(stream); if (fclose(stream)) wrote = 0; stream = NULL; if (!wrote) goto done; } ok = (used == expected_size && !memcmp(record, expected, used)) || (!public_key && used == expected_size + sizeof "selection current-accepted-unsigned\n" - 1 && !memcmp(record, expected, expected_size) && !memcmp(record + expected_size, "selection current-accepted-unsigned\n", sizeof "selection current-accepted-unsigned\n" - 1)); done: if (stream) fclose(stream); free(record); free(expected); if (fd >= 0) close(fd); if (dir >= 0) close(dir); return ok; } int holy_repo_catalog_index(const char *directory, char digest[65]) { int dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); int ok = 0; digest[0] = 0; if (dir >= 0 && !flock(dir, LOCK_SH) && read_current(dir, digest) == 1 && list(directory, NULL, NULL, 1, 0, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, 0, NULL, NULL)) ok = 1; if (dir >= 0) close(dir); if (!ok) digest[0] = 0; return ok; } int holy_repo_catalog_index_fast(const char *directory, char digest[65]) { struct holy_repo_set index = {0}; struct stage_request stage = {NULL, &index, NULL, NULL, 1, 0, NULL}; int ok = list(directory, NULL, NULL, 1, 0, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, 0, &stage, NULL); if (ok) memcpy(digest, index.index, 65); else digest[0] = 0; return ok; } int holy_repo_catalog_slot_digest(const char *directory, const struct holy_package_identity *slot, const char *artifact, char index_digest[65]) { struct holy_repo_set index = {0}; struct stage_request stage = {NULL, &index, slot, artifact, 1, 0, NULL}; int result = 6; if (!slot || !artifact || strlen(artifact) != 64 || strspn(artifact, "0123456789abcdef") != 64) return 2; if (!list(directory, NULL, NULL, 1, 0, NULL, NULL, NULL, NULL, NULL, NULL, 0, &result, NULL, 0, &stage, NULL)) return 6; memcpy(index_digest, index.index, 65); return result; } static int seal(const char *directory, const char *expected, const char *private_key) { char temporary[43] = {0}, pointer_temp[43] = {0}; char index_name[71], digest[65], previous[65], line[73]; char buffer[65536]; char *snapshot = NULL; struct stat st; int dir = -1, input = -1, output = -1, pointer = -1, ok = 0; ssize_t got; size_t written; dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); if (dir < 0 || flock(dir, LOCK_EX) < 0) goto done; if (read_current(dir, previous) < 0) goto done; input = openat(dir, "index", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK); if (input < 0 || fstat(input, &st) || !S_ISREG(st.st_mode) || st.st_size < 0 || st.st_size > 128LL * 1024 * 1024) goto done; output = holy_temporary_at(dir, temporary); if (output < 0) goto done; while (lseek(output, 0, SEEK_CUR) < st.st_size) { off_t position = lseek(output, 0, SEEK_CUR); size_t amount; if (position < 0) goto done; amount = st.st_size - position < (off_t)sizeof buffer ? (size_t)(st.st_size - position) : sizeof buffer; got = pread(input, buffer, amount, position); size_t offset = 0; if (got < 0) { if (errno == EINTR) continue; goto done; } if (!got) goto done; while (offset < (size_t)got) { ssize_t sent = write(output, buffer + offset, (size_t)got - offset); if (sent < 0 && errno == EINTR) continue; if (sent <= 0) goto done; offset += (size_t)sent; } } if (fstat(input, &st) || st.st_size < 0 || lseek(output, 0, SEEK_CUR) != st.st_size) goto done; if (fchmod(output, 0644) || fsync(output)) goto done; close(output); output = -1; close(input); input = -1; if (!list(directory, NULL, temporary, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, NULL, 0, NULL, NULL)) goto done; input = openat(dir, temporary, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); if (input < 0) goto done; snapshot = holy_stage_fd(input, "holy-seal"); if (!snapshot || !digest_file(snapshot, digest) || (expected && strcmp(digest, expected))) goto done; snprintf(index_name, sizeof index_name, "index.%s", digest); if (linkat(dir, temporary, dir, index_name, 0)) { if (errno != EEXIST) goto done; close(input); input = openat(dir, index_name, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); if (input < 0) goto done; unlink(snapshot); free(snapshot); snapshot = holy_stage_fd(input, "holy-seal"); if (!snapshot || !digest_file(snapshot, previous) || strcmp(previous, digest)) goto done; } if (fsync(dir)) goto done; if (private_key && !holy_sign_index(dir, digest, private_key)) goto done; if (fstatat(dir, "current", &st, AT_SYMLINK_NOFOLLOW) == 0) { if (!S_ISREG(st.st_mode)) goto done; } else if (errno != ENOENT) goto done; pointer = holy_temporary_at(dir, pointer_temp); if (pointer < 0) goto done; snprintf(line, sizeof line, "sha256 %s\n", digest); written = 0; while (written < 72) { ssize_t sent = write(pointer, line + written, 72 - written); if (sent < 0 && errno == EINTR) continue; if (sent <= 0) goto done; written += (size_t)sent; } if (fchmod(pointer, 0644) || fsync(pointer)) goto done; close(pointer); pointer = -1; if (renameat(dir, pointer_temp, dir, "current") || fsync(dir)) goto done; pointer_temp[0] = '\0'; printf("sealed %s\n", digest); ok = 1; done: if (!ok) fprintf(stderr, "holypkg: repository seal incomplete\n"); if (snapshot) { unlink(snapshot); free(snapshot); } if (pointer >= 0) close(pointer); if (output >= 0) close(output); if (input >= 0) close(input); if (dir >= 0) { if (temporary[0]) unlinkat(dir, temporary, 0); if (pointer_temp[0]) unlinkat(dir, pointer_temp, 0); close(dir); } return ok; } int holy_repo_seal(const char *directory) { return seal(directory, NULL, NULL); } int holy_repo_seal_signed(const char *directory, const char *private_key) { return private_key && *private_key && seal(directory, NULL, private_key); } int holy_repo_verify_signature(const char *directory, const char *public_key) { char digest[65]; int dir, ok; if (!public_key || !*public_key) return 2; dir = open(directory, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); if (dir < 0) return 1; ok = !flock(dir, LOCK_SH) && holy_repo_catalog_index(directory, digest) && holy_verify_index(dir, digest, public_key); close(dir); if (ok) printf("verified %s\n", digest); else fputs("holypkg: repository signature invalid or unavailable\n", stderr); return ok ? 0 : 4; } static int mirror_source(const char *base, const char *digest, const char *output, const char *ca_file, const char *source_id, int current_accepted, const char *public_key) { struct mirror mirror = { base, ca_file, NULL, 0 }; char index_name[71], signature_name[75], key_hash[65] = {0}; char *url = NULL, *downloads = NULL; unsigned char signature[64]; size_t i, length = strlen(output); int dir = -1, provenance = -1, sidecar = -1, result = 1; FILE *record = NULL; if (strlen(digest) != 64) return 2; for (i = 0; i < 64; ++i) if (!((digest[i] >= '0' && digest[i] <= '9') || (digest[i] >= 'a' && digest[i] <= 'f'))) return 2; if (current_accepted && !source_id) return 2; if (source_id) { if (strlen(source_id) != 64) return 2; for (i = 0; i < 64; ++i) if (!((source_id[i] >= '0' && source_id[i] <= '9') || (source_id[i] >= 'a' && source_id[i] <= 'f'))) return 2; } snprintf(index_name, sizeof index_name, "index.%s", digest); url = holy_fetch_child_url(base, index_name); if (!url) return 2; if (length > (size_t)-1 - 12 || !(downloads = malloc(length + 12))) goto done; snprintf(downloads, length + 12, "%s/.downloads", output); mirror.downloads = downloads; if (mkdir(output, 0700)) goto done; dir = open(output, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); if (dir < 0 || flock(dir, LOCK_EX) || mkdirat(dir, ".downloads", 0700)) goto done; result = holy_fetch_https_data(url, digest, output, ca_file); if (result) goto done; if (public_key) { result = holy_fetch_https_signature(base, digest, ca_file, signature); if (result) goto done; result = 4; if (linkat(dir, digest, dir, index_name, 0) || !holy_verify_index_bytes(dir, digest, public_key, signature, key_hash)) goto done; snprintf(signature_name, sizeof signature_name, "signature.%s", digest); sidecar = openat(dir, signature_name, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0644); if (sidecar < 0) { result = 1; goto done; } for (i = 0; i < sizeof signature;) { ssize_t sent = write(sidecar, signature + i, sizeof signature - i); if (sent < 0 && errno == EINTR) continue; if (sent <= 0) { result = 1; goto done; } i += (size_t)sent; } if (fsync(sidecar)) { result = 1; goto done; } if (close(sidecar)) { sidecar = -1; result = 1; goto done; } sidecar = -1; if (fsync(dir)) { result = 1; goto done; } } if (!list(output, NULL, digest, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, 0, NULL, &mirror, 0, NULL, NULL)) { result = mirror.status ? mirror.status : 4; goto done; } result = 1; if (linkat(dir, digest, dir, "index", 0)) goto done; provenance = openat(dir, "mirror-origin", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600); if (provenance < 0 || !(record = fdopen(provenance, "w"))) goto done; fputs("format holy-mirror-1\nurl ", record); if (!quote(record, base) || fprintf(record, "\nindex-sha256 %s\nverification %s\n", digest, public_key ? "ed25519-pinned-key" : "digest-pinned-unsigned") < 0 || (public_key && fprintf(record, "public-key-sha256 %s\n", key_hash) < 0) || (source_id && fprintf(record, "source-id %s\n", source_id) < 0) || (current_accepted && fputs("selection current-accepted-unsigned\n", record) == EOF) || fflush(record) || fsync(provenance)) goto done; if (fclose(record)) { record = NULL; provenance = -1; goto done; } record = NULL; provenance = -1; if (unlinkat(dir, digest, 0) || unlinkat(dir, ".downloads", AT_REMOVEDIR) || fsync(dir)) goto done; /* seal takes its own exclusive lock after the private download phase. */ close(dir); dir = -1; result = seal(output, digest, NULL) ? 0 : 4; done: if (result) fprintf(stderr, "holypkg: HTTPS catalog mirror incomplete (status %d)\n", result); if (record) fclose(record); else if (provenance >= 0) close(provenance); if (sidecar >= 0) close(sidecar); if (dir >= 0) close(dir); free(downloads); free(url); return result; } int holy_repo_mirror_source(const char *base, const char *digest, const char *output, const char *ca_file, const char *source_id, int current_accepted) { return mirror_source(base, digest, output, ca_file, source_id, current_accepted, NULL); } int holy_repo_mirror_source_signed(const char *base, const char *digest, const char *output, const char *ca_file, const char *source_id, const char *public_key) { if (!public_key || !*public_key) return 2; return mirror_source(base, digest, output, ca_file, source_id, 0, public_key); } int holy_repo_mirror(const char *base, const char *digest, const char *output, const char *ca_file) { return mirror_source(base, digest, output, ca_file, NULL, 0, NULL); } int holy_repo_mirror_signed(const char *base, const char *digest, const char *output, const char *ca_file, const char *public_key) { if (!public_key || !*public_key) return 2; return mirror_source(base, digest, output, ca_file, NULL, 0, public_key); }