#define _POSIX_C_SOURCE 200809L #include "docs.h" #include "config.h" #include "state.h" #include "install.h" #include #include #include #include #include #include #include #include #include #include #include #define SOURCE_LIMIT (16 * 1024 * 1024) #define TEXT_LIMIT (64 * 1024 * 1024) struct bundle { FILE *out; const char *artifact; char *name, *version, *source; int root, manifest; size_t packages, pages, aliases, missing, omitted, package_pages; }; static void quoted(FILE *out, const char *text) { const unsigned char *p = (const unsigned char *)text; fputc('"', out); for (; *p; ++p) { if (*p == '"' || *p == '\\') fprintf(out, "\\%c", *p); else if (*p < 32 || *p >= 127) fprintf(out, "\\x%02x", (unsigned)*p); else fputc(*p, out); } fputc('"', out); } static unsigned char *read_file(int fd, size_t *size) { struct stat st; unsigned char *data; size_t used = 0; if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size < 0 || st.st_size > SOURCE_LIMIT) return NULL; *size = (size_t)st.st_size; data = malloc(*size + 1); if (!data) return NULL; while (used < *size) { ssize_t got = read(fd, data + used, *size - used); if (got < 0 && errno == EINTR) continue; if (got <= 0) { free(data); return NULL; } used += (size_t)got; } data[*size] = 0; return data; } static int records(int dir, const char *name, int (*visit)(struct bundle *, char **, size_t), struct bundle *b) { size_t size, start = 0, i, line = 0; unsigned char *data; int fd = openat(dir, name, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK), ok = 1; if (fd < 0) return 0; data = read_file(fd, &size); close(fd); if (!data) return 0; if (memchr(data, 0, size)) { free(data); return 0; } for (i = 0; i <= size; ++i) { char **v = NULL, *error = NULL; size_t n = 0; if (i < size && data[i] != '\n') continue; ++line; ok = holy_lex((char *)data + start, i - start, &v, &n, name, line, &error); if (ok && n) ok = visit(b, v, n); free(error); holy_tokens_free(v, n); if (!ok) break; start = i + 1; } free(data); return ok; } static int identity(struct bundle *b, char **v, size_t n) { char **field = NULL; if (!strcmp(v[0], "name")) field = &b->name; else if (!strcmp(v[0], "version")) field = &b->version; if (!field) return 1; if (n != 2 || !v[1][0] || *field) return 0; *field = strdup(v[1]); return *field != NULL; } static int source(struct bundle *b, char **v, size_t n) { if (strcmp(v[0], "source-id")) return 1; if (n != 2 || !v[1][0] || b->source) return 0; b->source = strdup(v[1]); return b->source != NULL; } static void attribution(struct bundle *b) { fprintf(b->out, " artifact %s package ", b->artifact); quoted(b->out, b->name); fputs(" version ", b->out); quoted(b->out, b->version); fputs(" source-id ", b->out); quoted(b->out, b->source); } static int open_page(int root, const char *path) { char *copy = strdup(path), *part, *slash; int fd = dup(root); if (!copy || fd < 0) { free(copy); if (fd >= 0) close(fd); return -1; } part = copy; for (;;) { int next; slash = strchr(part, '/'); if (slash) *slash = 0; if (!*part || !strcmp(part, ".") || !strcmp(part, "..")) { close(fd); fd = -1; break; } next = openat(fd, part, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | (slash ? O_DIRECTORY : O_NONBLOCK)); close(fd); fd = next; if (fd < 0 || !slash) break; part = slash + 1; } free(copy); return fd; } static int text_body(FILE *out, const unsigned char *data, size_t size, const char *codec) { struct archive *a; struct archive_entry *entry; int support = ARCHIVE_FATAL, ok = 0; char buffer[8192]; size_t total = 0; la_ssize_t got; if (!codec) return size && !memchr(data, 0, size) && fwrite(data, 1, size, out) == size; a = archive_read_new(); if (!a) return 0; if (!strcmp(codec, "gz")) support = archive_read_support_filter_gzip(a); else if (!strcmp(codec, "bz2")) support = archive_read_support_filter_bzip2(a); else if (!strcmp(codec, "xz")) support = archive_read_support_filter_xz(a); else if (!strcmp(codec, "zst")) support = archive_read_support_filter_zstd(a); if (support != ARCHIVE_OK || archive_read_support_format_raw(a) != ARCHIVE_OK || archive_read_open_memory(a, data, size) != ARCHIVE_OK || archive_read_next_header(a, &entry) != ARCHIVE_OK || archive_filter_code(a, 0) == ARCHIVE_FILTER_NONE) goto done; while ((got = archive_read_data(a, buffer, sizeof buffer)) > 0) { if ((size_t)got > TEXT_LIMIT - total || memchr(buffer, 0, (size_t)got) || fwrite(buffer, 1, (size_t)got, out) != (size_t)got) goto done; total += (size_t)got; } ok = got == 0 && total && archive_read_next_header(a, &entry) == ARCHIVE_EOF; done: archive_read_free(a); return ok; } static int page(struct bundle *b, char **v, size_t n) { const char *base, *section, *codec = NULL; char *name = NULL, *dot; unsigned char *data = NULL, digest[32]; unsigned int length; char hash[65]; size_t size, i; int fd = -1, ok = 0; if (n < 2) return 0; if (strncmp(v[1], "usr/share/man/", 14) || !strcmp(v[0], "dir")) return 1; base = strrchr(v[1], '/'); if (!base || !base[1] || !(name = strdup(base + 1))) return 0; dot = strrchr(name, '.'); if (dot && (!strcmp(dot, ".gz") || !strcmp(dot, ".bz2") || !strcmp(dot, ".xz") || !strcmp(dot, ".zst"))) { codec = strrchr(base, '.') + 1; *dot = 0; dot = strrchr(name, '.'); } if (!dot || dot == name || dot[1] < '1' || dot[1] > '9') { fputs("omitted-man path ", b->out); quoted(b->out, v[1]); attribution(b); fputs(" reason unsupported-name-or-codec\n", b->out); ++b->omitted; ok = 1; goto done; } *dot = 0; section = dot + 1; if (!strcmp(v[0], "symlink") && n == 13) { if (holy_install_check_path(b->manifest, b->root, v[1]) != 1) goto done; fputs("alias path ", b->out); quoted(b->out, v[1]); fputs(" target ", b->out); quoted(b->out, v[12]); attribution(b); fputc('\n', b->out); ++b->aliases; ok = 1; goto done; } if ((strcmp(v[0], "file") || n != 12) && (strcmp(v[0], "hardlink") || n != 13)) goto done; if (strlen(v[8]) != 64 || (!strcmp(v[0], "hardlink") && holy_install_check_path(b->manifest, b->root, v[1]) != 1)) goto done; fd = open_page(b->root, v[1]); if (fd < 0 || !(data = read_file(fd, &size)) || EVP_Digest(data, size, digest, &length, EVP_sha256(), NULL) != 1 || length != 32) goto done; for (i = 0; i < 32; ++i) snprintf(hash + i * 2, 3, "%02x", digest[i]); if (strcmp(hash, v[8])) goto done; fputs("page name ", b->out); quoted(b->out, name); fputs(" section ", b->out); quoted(b->out, section); fputs(" path ", b->out); quoted(b->out, v[1]); attribution(b); fprintf(b->out, " sha256 %s encoding roff-source\n", hash); if (!text_body(b->out, data, size, codec)) goto done; fputs("\nend-page\n", b->out); ++b->pages; ++b->package_pages; ok = !ferror(b->out); done: if (fd >= 0) close(fd); free(data); free(name); return ok; } static int instance(void *context, int root, int item, const char *artifact) { struct bundle *b = context; int ok; b->root = root; b->artifact = artifact; b->package_pages = 0; b->manifest = openat(item, "files", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK); ok = b->manifest >= 0 && records(item, "meta", identity, b) && b->name && b->version && records(item, "state", source, b) && b->source && records(item, "files", page, b); if (b->manifest >= 0) close(b->manifest); if (ok && !b->package_pages) { fputs("missing-man", b->out); attribution(b); fputc('\n', b->out); ++b->missing; } free(b->name); free(b->version); free(b->source); b->name = b->version = b->source = NULL; ++b->packages; return ok && !ferror(b->out) ? 0 : 4; } int holy_docs(const char *root, const char *output) { struct bundle b = {0}; unsigned long long generation = 0; size_t length = strlen(output); char *temp; int fd = -1, result = 1; if (length > SIZE_MAX - 16 || !(temp = malloc(length + 16))) return 1; snprintf(temp, length + 16, "%s.tmp.XXXXXX", output); fd = mkstemp(temp); if (fd < 0) goto done; b.out = fdopen(fd, "w"); if (!b.out) goto done; fputs("format holy-docs-1\n", b.out); result = holy_state_visit(root, instance, &b, &generation); if (result) goto done; fprintf(b.out, "summary generation %llu packages %zu pages %zu aliases %zu missing-man %zu omitted %zu\n", generation, b.packages, b.pages, b.aliases, b.missing, b.omitted); if (fflush(b.out) || fsync(fd) || link(temp, output)) result = 1; done: if (b.out) fclose(b.out); else if (fd >= 0) close(fd); if (fd >= 0) unlink(temp); free(temp); if (result) fprintf(stderr, "holypkg: documentation unavailable, changed, or output exists (status %d)\n", result); return result; }