_ _
| |_ ___| |_ _
| | . | | | |
|_|_|___|_|_ |
|___|
git mirror - github.com/owenewans/holy - branch master
file src/preview.c
#define _POSIX_C_SOURCE 200809L
#include "preview.h"
#include "extract.h"
#include "package.h"
#include "scan.h"
#include "stage.h"
#include "verify.h"
#include "deps.h"
#include "provides.h"
#include <archive.h>
#include <archive_entry.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
struct action {
char *path;
char *interpreter;
char *helper;
int state;
};
static void print_path(const char *path)
{
const unsigned char *p = (const unsigned char *)path;
for (; *p; ++p)
if (*p == '\\' || *p <= 32 || *p >= 127)
printf("\\x%02x", (unsigned int)*p);
else putchar(*p);
}
static void json_string(const char *text)
{
const unsigned char *p = (const unsigned char *)text;
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 char *env_helper(const char *text, size_t used, size_t position)
{
size_t start, end;
while (position < used && (text[position] == ' ' || text[position] == '\t'))
++position;
if (position + 2 < used && text[position] == '-' &&
text[position + 1] == 'S' &&
(text[position + 2] == ' ' || text[position + 2] == '\t')) {
position += 2;
while (position < used && (text[position] == ' ' || text[position] == '\t'))
++position;
}
start = position;
while (position < used && text[position] != ' ' && text[position] != '\t' &&
text[position] != '\r' && text[position] != '\n' && text[position])
++position;
end = position;
if (end > start && (end < used || used < 256) && text[start] != '-') {
for (position = start; position < end; ++position)
if (text[position] == '"' || text[position] == '\'' ||
text[position] == '\\') break;
if (position == end) return strndup(text + start, end - start);
}
return strdup("unknown");
}
/* 0: new, 1: existing directory, 2: conflict, -1: I/O failure. */
static int inspect_path(int root, const char *name, int wants_directory)
{
char *copy = strdup(name), *cursor, *slash;
struct stat st;
size_t length;
int current = root, next, state = -1;
if (!copy) return -1;
length = strlen(copy);
if (length && copy[length - 1] == '/') copy[length - 1] = '\0';
cursor = copy;
while ((slash = strchr(cursor, '/')) != NULL) {
*slash = '\0';
next = openat(current, cursor, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (next < 0) {
state = errno == ENOENT ? 0 :
(errno == ENOTDIR || errno == ELOOP) ? 2 : -1;
goto done;
}
if (current != root) close(current);
current = next;
cursor = slash + 1;
}
if (!*cursor) { state = -1; goto done; }
if (fstatat(current, cursor, &st, AT_SYMLINK_NOFOLLOW) < 0)
state = errno == ENOENT ? 0 : -1;
else state = wants_directory && S_ISDIR(st.st_mode) ? 1 : 2;
done:
if (current != root) close(current);
free(copy);
return state;
}
static int preview(const char *package, const char *root_path,
int json, int resolved, int completed, int accepted_privileged,
int skipped_hooks)
{
struct archive *archive = NULL;
struct archive_entry *entry;
struct holy_package_identity identity = {0};
struct action *actions = NULL;
char *snapshot = holy_stage_local(package, "holy-preview");
size_t count = 0, i, conflicts = 0, requirements = 0, elf_needed = 0;
size_t script_interpreters = 0, helper_commands = 0;
int root = -1, status, rc = 2, hook_decision = 0;
if (!snapshot || !holy_verify_with_output(snapshot, 0) ||
!holy_extract_preflight(snapshot) ||
!holy_scan_local_facts(snapshot, 0, &elf_needed) ||
!holy_deps_count(snapshot, &requirements) ||
!holy_provides_local(snapshot, 0) ||
!holy_package_identity(snapshot, &identity)) goto done;
root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (root < 0) { perror("holypkg: target root"); rc = 1; goto done; }
archive = archive_read_new();
if (!archive || archive_read_support_filter_lz4(archive) != ARCHIVE_OK ||
archive_read_support_format_tar(archive) != ARCHIVE_OK ||
archive_read_open_filename(archive, snapshot, 8192) != ARCHIVE_OK) goto done;
while ((status = archive_read_next_header(archive, &entry)) == ARCHIVE_OK) {
const char *path = archive_entry_pathname(entry);
struct action *next;
int state;
if (!strcmp(path, "HOLY/hooks")) {
if (archive_entry_size(entry) != 0 && !skipped_hooks) {
fprintf(stderr, "holypkg: decision-required hooks; review and skip explicitly\n");
rc = 3;
hook_decision = 1;
goto done;
}
}
if (strncmp(path, "DATA/", 5) || !path[5]) {
if (archive_read_data_skip(archive) != ARCHIVE_OK) goto done;
continue;
}
if ((archive_entry_perm(entry) & 07000) &&
(!accepted_privileged || archive_entry_filetype(entry) != AE_IFREG ||
archive_entry_hardlink(entry) || (archive_entry_perm(entry) & 03000))) {
fprintf(stderr, "holypkg: privileged payload mode requires review\n");
rc = 6;
goto done;
}
if (count == (size_t)-1 / sizeof *actions) goto done;
state = inspect_path(root, path + 5,
archive_entry_filetype(entry) == AE_IFDIR);
if (state < 0) { rc = 1; goto done; }
next = realloc(actions, (count + 1) * sizeof *actions);
if (!next) { rc = 1; goto done; }
actions = next;
actions[count].path = strdup(path + 5);
if (!actions[count].path) { rc = 1; goto done; }
actions[count].interpreter = NULL;
actions[count].helper = NULL;
actions[count].state = state;
++count;
if (state == 2) ++conflicts;
if (archive_entry_filetype(entry) == AE_IFREG &&
!archive_entry_hardlink(entry) &&
(archive_entry_perm(entry) & 0111)) {
char prefix[257];
size_t used = 0, limit = archive_entry_size(entry) < 256 ?
(size_t)archive_entry_size(entry) : 256;
while (used < limit) {
la_ssize_t got = archive_read_data(archive, prefix + used,
limit - used);
if (got <= 0) goto done;
used += (size_t)got;
}
if (used >= 2 && !memcmp(prefix, "#!", 2)) {
size_t start = 2, end;
if (script_interpreters == (size_t)-1) goto done;
++script_interpreters;
while (start < used && (prefix[start] == ' ' || prefix[start] == '\t'))
++start;
end = start;
while (end < used && prefix[end] != ' ' && prefix[end] != '\t' &&
prefix[end] != '\r' && prefix[end] != '\n' && prefix[end])
++end;
if (end > start && prefix[start] == '/' &&
(end < used || used < 256)) {
char separator = end < used ? prefix[end] : '\0';
prefix[end] = '\0';
actions[count - 1].interpreter = strdup(prefix + start);
prefix[end] = separator;
} else actions[count - 1].interpreter = strdup("unknown");
if (!actions[count - 1].interpreter) { rc = 1; goto done; }
if (!strcmp(actions[count - 1].interpreter, "/usr/bin/env") ||
!strcmp(actions[count - 1].interpreter, "/bin/env")) {
if (helper_commands == (size_t)-1) goto done;
actions[count - 1].helper = env_helper(prefix, used, end);
if (!actions[count - 1].helper) { rc = 1; goto done; }
++helper_commands;
}
} else if (used < 4 || memcmp(prefix, "\177ELF", 4)) {
fprintf(stderr, "holypkg: executable format unknown; interpreter decision required\n");
rc = 6;
goto done;
}
}
if (archive_read_data_skip(archive) != ARCHIVE_OK) goto done;
}
if (status != ARCHIVE_EOF) goto done;
if (json > 0) {
for (i = 0; i < count; ++i) {
fputs("{\"schema\":\"holy-preview-1\",\"type\":\"path\",\"path\":", stdout);
json_string(actions[i].path);
printf(",\"state\":\"%s\",\"interpreter\":",
actions[i].state == 0 ? "new" :
actions[i].state == 1 ? "existing-dir" : "conflict");
if (actions[i].interpreter) json_string(actions[i].interpreter);
else fputs("null", stdout);
fputs(",\"helper\":", stdout);
if (actions[i].helper) json_string(actions[i].helper);
else fputs("null", stdout);
puts("}");
}
printf("{\"schema\":\"holy-preview-1\",\"type\":\"summary\",\"artifact\":\"%s\",\"paths\":%zu,\"conflicts\":%zu,\"requirements\":%zu,\"elf_needed\":%zu,\"script_interpreters\":%zu,\"helper_commands\":%zu}\n",
identity.digest, count, conflicts, requirements, elf_needed,
script_interpreters, helper_commands);
} else if (json == 0) {
printf("preview artifact=%s paths=%zu conflicts=%zu requirements=%zu elf-needed=%zu script-interpreters=%zu helper-commands=%zu\n",
identity.digest, count, conflicts, requirements, elf_needed,
script_interpreters, helper_commands);
for (i = 0; i < count; ++i) {
printf("%s ", actions[i].state == 0 ? "new" :
actions[i].state == 1 ? "existing-dir" : "conflict");
print_path(actions[i].path);
putchar('\n');
if (actions[i].interpreter) {
fputs("interpreter ", stdout);
print_path(actions[i].path);
putchar(' ');
print_path(actions[i].interpreter);
putchar('\n');
}
if (actions[i].helper) {
fputs("helper-command ", stdout);
print_path(actions[i].path);
putchar(' ');
print_path(actions[i].helper);
putchar('\n');
}
}
}
rc = (conflicts && !completed) ? 4 :
(!resolved && (requirements || elf_needed || script_interpreters)) ? 3 : 0;
done:
if (rc == 2) fprintf(stderr, "holypkg: cannot preview package\n");
if (json > 0 && hook_decision)
puts("{\"schema\":\"holy-preview-1\",\"type\":\"error\",\"code\":\"decision-required\"}");
if (json > 0 && rc != 0 && rc != 3 && rc != 4)
printf("{\"schema\":\"holy-preview-1\",\"type\":\"error\",\"code\":\"%s\"}\n",
rc == 6 ? "unsupported-input" :
rc == 1 ? "operational-error" : "invalid-preview");
if (archive) archive_read_free(archive);
if (root >= 0) close(root);
if (snapshot) { unlink(snapshot); free(snapshot); }
for (i = 0; i < count; ++i) {
free(actions[i].path);
free(actions[i].interpreter);
free(actions[i].helper);
}
free(actions);
holy_package_identity_free(&identity);
return rc;
}
int holy_preview_local(const char *package, const char *root_path)
{
return holy_preview_local_format(package, root_path, 0);
}
int holy_preview_local_format(const char *package, const char *root_path, int json)
{
return preview(package, root_path, json, 0, 0, 0, 0);
}
int holy_preview_resolved(const char *package, const char *root_path, int completed,
int accepted_privileged, int skipped_hooks)
{
return preview(package, root_path, -1, 1, completed, accepted_privileged, skipped_hooks);
}