kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit f150a1ada14451dc7a989d3988055686c4ca413e
parent 273bce54805eb3c219b67f86b86d704deb574330
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 17 Jun 2026 12:59:49 -0700

selfdist: channel index lib (kit-release), env hooks (kit_home/fetch/rename), install-link extraction, test key

Diffstat:
Mdriver/cmd/install.c | 110+++++++++++++++----------------------------------------------------------------
Mdriver/driver.h | 29++++++++++++++++++++++++++---
Mdriver/env.h | 22++++++++++++++++++++++
Mdriver/env/posix.c | 46++++++++++++++++++++++++++++++++++++++++++++++
Mdriver/env/windows.c | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/lib/install_links.c | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adriver/lib/install_links.h | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdriver/main.c | 12++++++------
Adriver/release_key.c | 26++++++++++++++++++++++++++
Minclude/kit/package.h | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmk/driver_srcs.mk | 1+
Mmk/lib_srcs.mk | 1+
Mmk/test.mk | 5+++++
Mmk/test_unit.mk | 3++-
Msrc/api/package.c | 35+++++++++++++++++++++++++++++++++++
Asrc/dist/release.c | 352+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/dist/release.h | 42++++++++++++++++++++++++++++++++++++++++++
Atest/api/release_index_test.c | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/dist/keys/README.md | 14++++++++++++++
Atest/dist/keys/nonrelease.key | 2++
Atest/dist/keys/nonrelease.pub | 2++
21 files changed, 1019 insertions(+), 99 deletions(-)

diff --git a/driver/cmd/install.c b/driver/cmd/install.c @@ -5,6 +5,7 @@ #include "driver.h" #include "env.h" +#include "install_links.h" /* `kit install` — busybox-style toolchain installer. Populates a target * directory with one entry per kit tool, each pointing at the running kit @@ -54,77 +55,6 @@ void driver_help_install(void) { " 0 success 1 one or more links failed 2 bad usage\n"); } -/* Join DIR + "/" + NAME into a freshly allocated, NUL-terminated path. Returns - * NULL on allocation failure; on success stores the allocation size in - * *out_size for driver_free. */ -static char* install_join(DriverEnv* env, const char* dir, const char* name, - size_t* out_size) { - size_t dl = driver_strlen(dir); - size_t nl = driver_strlen(name); - int slash = dl > 0 && dir[dl - 1u] != '/'; - size_t size = dl + (slash ? 1u : 0u) + nl + 1u; - size_t off; - char* p = (char*)driver_alloc(env, size); - if (!p) return NULL; - driver_memcpy(p, dir, dl); - off = dl; - if (slash) p[off++] = '/'; - driver_memcpy(p + off, name, nl); - off += nl; - p[off] = '\0'; - *out_size = size; - return p; -} - -/* Create one link DIR/NAME -> self. Returns 0 on success, nonzero on failure - * (already emitted a diagnostic). Honors dry-run (prints, no change), force - * (replace existing), and verbose. */ -static int install_one(DriverEnv* env, const char* dir, const char* name, - const char* self, int want_hard, int force, int dry, - int verbose) { - size_t path_size = 0; - char* link_path = install_join(env, dir, name, &path_size); - int rc = 0; - - if (!link_path) { - driver_errf(INSTALL_TOOL, "out of memory building path for %s", name); - return 1; - } - - if (driver_path_lexists(link_path)) { - if (!force) { - driver_errf(INSTALL_TOOL, "%s already exists (use -f to overwrite)", - link_path); - rc = 1; - goto done; - } - if (!dry && driver_remove_file(link_path) != 0) { - driver_errf(INSTALL_TOOL, "cannot replace %s", link_path); - rc = 1; - goto done; - } - } - - if (dry) { - driver_printf("%s -> %s (dry-run)\n", link_path, self); - goto done; - } - - rc = want_hard ? driver_create_hardlink(self, link_path) - : driver_create_symlink(self, link_path); - if (rc != 0) { - driver_errf(INSTALL_TOOL, "failed to %s %s", - want_hard ? "hard-link" : "symlink", link_path); - rc = 1; - goto done; - } - if (verbose) driver_printf("%s -> %s\n", link_path, self); - -done: - driver_free(env, link_path, path_size); - return rc; -} - int driver_install(int argc, char** argv) { DriverEnv env; const char* dir = NULL; @@ -212,24 +142,26 @@ int driver_install(int argc, char** argv) { goto done; } - if (nexplicit > 0) { - for (i = 0; i < nexplicit; ++i) { - if (install_one(&env, dir, explicit_tools[i], self, want_hard, force, dry, - verbose) != 0) - ++failures; - else - ++done_count; - } - } else { - unsigned n = driver_tool_count(); - unsigned j; - for (j = 0; j < n; ++j) { - if (!all && driver_tool_groups(j) == 0) continue; - if (install_one(&env, dir, driver_tool_name(j), self, want_hard, force, - dry, verbose) != 0) - ++failures; - else - ++done_count; + { + DriverInstallLinkOpts opts; + opts.target_exe = self; + opts.tool_tag = INSTALL_TOOL; + opts.use_hardlink = want_hard; + opts.force = force; + opts.dry_run = dry; + opts.verbose = verbose; + + if (nexplicit > 0) { + for (i = 0; i < nexplicit; ++i) { + if (driver_install_link_one(&env, dir, explicit_tools[i], &opts) != 0) + ++failures; + else + ++done_count; + } + } else { + unsigned mask = all ? DRIVER_GROUP_ALL + : (DRIVER_GROUP_TOOLCHAIN | DRIVER_GROUP_BYTEUTIL); + failures = driver_install_links(&env, dir, mask, &opts, &done_count); } } diff --git a/driver/driver.h b/driver/driver.h @@ -129,11 +129,34 @@ const char* kit_host_triple(void); /* e.g. "aarch64-macos" */ * so `kit --version` and `cc --version` / `ld --version` answer uniformly. */ void driver_print_version(const char* tool); -/* Scan argv[1..argc-1] for a "--version" request, returning 1 on a hit. Stops - * at "--" (like driver_argv_wants_help) so a flag forwarded to a JITed program - * or emulated guest is never hijacked. */ +/* Returns 1 when the tool's first argument (argv[1]) is "--version". Only the + * first argument counts, so a sub-flag that legitimately spells --version + * (e.g. `pkg create --version <ver>`) is never hijacked; `cc --version` and + * `kit cc --version` both present --version as the dispatched tool's argv[1]. */ int driver_argv_wants_version(int argc, char** argv); +/* ---------------------------------------------------------------------- + * Self-distribution: `kit update` + the embedded release trust anchor + * (driver/cmd/update.c, driver/release_key.c). See doc/plan/SELFDIST.md. + * ---------------------------------------------------------------------- */ +int driver_update(int argc, char** argv); +void driver_help_update(void); + +/* `kit update` verifies a downloaded release against this built-in set of + * minisign public keys; any one key verifying suffices, which supports + * overlap-window key rotation. The real release key's secret is held offline; + * the in-tree default is a clearly-labelled NON-RELEASE test key so the + * hermetic end-to-end test and the `make dist` fallback signer verify against a + * built-in anchor. A `kit update --key` override lets mirrors/forks supply + * their own. */ +typedef struct KitReleaseKey { + const char* label; /* human label */ + const char* pubkey; /* minisign public-key file text (comment + base64) */ +} KitReleaseKey; + +/* Borrow the embedded release public-key set; *count receives its length. */ +const KitReleaseKey* driver_release_keys(unsigned* count); + /* Tool grouping, used by `install` to pick a default set without * duplicating the tool list. The centralized table in main.c tags every * row; the groups with a non-zero bit make up the default install set diff --git a/driver/env.h b/driver/env.h @@ -249,6 +249,28 @@ int driver_remove_file(const char* path); * POSIX lstat / Windows GetFileAttributesW. */ int driver_path_lexists(const char* path); +/* Rename/move `from` to `to`, replacing any existing entry at `to`. Within one + * filesystem this is atomic, which `kit update` relies on for the version dir + * move and the `current` pointer flip. Returns 0 on success, nonzero on + * failure. POSIX rename(2); Windows MoveFileExW(MOVEFILE_REPLACE_EXISTING). */ +int driver_rename(const char* from, const char* to); + +/* Resolve the single-root kit home directory into `buf` (capacity `cap`, + * NUL-terminated, no trailing slash): `$KIT_HOME`, else `$XDG_DATA_HOME/kit`, + * else `$HOME/.local/share/kit` on POSIX (`%LOCALAPPDATA%\kit` on Windows). + * Used by `kit update` for versions/, current, bin/, config/, cache/. Returns 0 + * on success, nonzero when no home could be determined or it does not fit. */ +int driver_kit_home(char* buf, size_t cap); + +/* Fetch `url` to the local file `dest`, overwriting it. The transport is + * UNTRUSTED — the caller verifies a signature/content-id over the bytes — so + * this is a thin, narrow download primitive, not a general subprocess API: + * POSIX execs `curl -fsSL -o dest -- url` (falling back to `wget -O dest url`) + * via fork/exec with the URL passed as a distinct argv element (no shell); the + * Windows port uses curl.exe likewise. Returns 0 on success, nonzero on any + * failure (no fetcher found, network/HTTP error, write error). */ +int driver_fetch_url(const char* url, const char* dest); + /* Set a linked binary output's final mode according to the active umask. * Returns 0 on success, nonzero on chmod failure. */ int driver_mark_executable_output(const char* path); diff --git a/driver/env/posix.c b/driver/env/posix.c @@ -756,6 +756,52 @@ int driver_path_lexists(const char* path) { return lstat(path, &sb) == 0; } +int driver_rename(const char* from, const char* to) { + if (!from || !to) return 1; + return rename(from, to) == 0 ? 0 : 1; /* same-fs rename is atomic */ +} + +int driver_kit_home(char* buf, size_t cap) { + const char* v; + int n = -1; + size_t len; + if (!buf || cap == 0) return 1; + if ((v = getenv("KIT_HOME")) && *v) + n = snprintf(buf, cap, "%s", v); + else if ((v = getenv("XDG_DATA_HOME")) && *v) + n = snprintf(buf, cap, "%s/kit", v); + else if ((v = getenv("HOME")) && *v) + n = snprintf(buf, cap, "%s/.local/share/kit", v); + if (n < 0 || (size_t)n >= cap) return 1; + len = strlen(buf); + while (len > 1 && buf[len - 1] == '/') buf[--len] = '\0'; /* no trailing '/' */ + return 0; +} + +int driver_fetch_url(const char* url, const char* dest) { + pid_t pid; + int status; + if (!url || !dest) return 1; + pid = fork(); + if (pid < 0) return 1; + if (pid == 0) { + /* Untrusted transport: the URL is a distinct argv element (no shell), so a + * hostile mirror URL cannot inject a command. curl first; if it is not + * installed (exec returns) fall back to wget; if neither, 127. */ + execlp("curl", "curl", "-fsSL", "-o", dest, "--", url, (char*)NULL); + execlp("wget", "wget", "-q", "-O", dest, "--", url, (char*)NULL); + _exit(127); + } + do { + if (waitpid(pid, &status, 0) < 0) { + if (errno == EINTR) continue; + return 1; + } + break; + } while (1); + return (WIFEXITED(status) && WEXITSTATUS(status) == 0) ? 0 : 1; +} + static int driver_walk_regular_files_at(DriverEnv* env, const char* dir, const char* rel, DriverWalkFileFn cb, void* user) { diff --git a/driver/env/windows.c b/driver/env/windows.c @@ -1060,6 +1060,61 @@ int driver_path_lexists(const char* path) { return attr != INVALID_FILE_ATTRIBUTES; } +int driver_rename(const char* from, const char* to) { + wchar_t *wfrom, *wto; + BOOL ok; + if (!from || !to) return 1; + wfrom = widen(from); + wto = widen(to); + if (!wfrom || !wto) { + free(wfrom); + free(wto); + return 1; + } + ok = MoveFileExW(wfrom, wto, MOVEFILE_REPLACE_EXISTING); + free(wfrom); + free(wto); + return ok ? 0 : 1; +} + +int driver_kit_home(char* buf, size_t cap) { + const char* v; + int n = -1; + size_t len; + if (!buf || cap == 0) return 1; + if ((v = getenv("KIT_HOME")) && *v) + n = snprintf(buf, cap, "%s", v); + else if ((v = getenv("XDG_DATA_HOME")) && *v) + n = snprintf(buf, cap, "%s/kit", v); + else if ((v = getenv("LOCALAPPDATA")) && *v) + n = snprintf(buf, cap, "%s\\kit", v); + if (n < 0 || (size_t)n >= cap) return 1; + len = strlen(buf); + while (len > 1 && (buf[len - 1] == '/' || buf[len - 1] == '\\')) + buf[--len] = '\0'; + return 0; +} + +int driver_fetch_url(const char* url, const char* dest) { + wchar_t *wurl, *wdest; + intptr_t rc; + if (!url || !dest) return 1; + wurl = widen(url); + wdest = widen(dest); + if (!wurl || !wdest) { + free(wurl); + free(wdest); + return 1; + } + /* curl.exe ships with Windows 10+; the URL is a distinct argv element (no + * shell), so a hostile mirror URL cannot inject a command. */ + rc = _wspawnlp(_P_WAIT, L"curl", L"curl", L"-fsSL", L"-o", wdest, L"--", wurl, + (wchar_t*)NULL); + free(wurl); + free(wdest); + return rc == 0 ? 0 : 1; +} + static int driver_walk_regular_files_at(DriverEnv* env, const char* dir, const char* rel, DriverWalkFileFn cb, void* user) { diff --git a/driver/lib/install_links.c b/driver/lib/install_links.c @@ -0,0 +1,94 @@ +#include "install_links.h" + +#include <stddef.h> + +/* Join DIR + "/" + NAME into a freshly allocated, NUL-terminated path. Returns + * NULL on allocation failure; on success stores the allocation size in + * *out_size for driver_free. */ +static char* install_join(DriverEnv* env, const char* dir, const char* name, + size_t* out_size) { + size_t dl = driver_strlen(dir); + size_t nl = driver_strlen(name); + int slash = dl > 0 && dir[dl - 1u] != '/'; + size_t size = dl + (slash ? 1u : 0u) + nl + 1u; + size_t off; + char* p = (char*)driver_alloc(env, size); + if (!p) return NULL; + driver_memcpy(p, dir, dl); + off = dl; + if (slash) p[off++] = '/'; + driver_memcpy(p + off, name, nl); + off += nl; + p[off] = '\0'; + *out_size = size; + return p; +} + +int driver_install_link_one(DriverEnv* env, const char* bindir, + const char* name, + const DriverInstallLinkOpts* opts) { + const char* tag = opts->tool_tag; + size_t path_size = 0; + char* link_path = install_join(env, bindir, name, &path_size); + int rc = 0; + + if (!link_path) { + driver_errf(tag, "out of memory building path for %s", name); + return 1; + } + + if (driver_path_lexists(link_path)) { + if (!opts->force) { + driver_errf(tag, "%s already exists (use -f to overwrite)", link_path); + rc = 1; + goto done; + } + if (!opts->dry_run && driver_remove_file(link_path) != 0) { + driver_errf(tag, "cannot replace %s", link_path); + rc = 1; + goto done; + } + } + + if (opts->dry_run) { + driver_printf("%s -> %s (dry-run)\n", link_path, opts->target_exe); + goto done; + } + + rc = opts->use_hardlink + ? driver_create_hardlink(opts->target_exe, link_path) + : driver_create_symlink(opts->target_exe, link_path); + if (rc != 0) { + driver_errf(tag, "failed to %s %s", + opts->use_hardlink ? "hard-link" : "symlink", link_path); + rc = 1; + goto done; + } + if (opts->verbose) driver_printf("%s -> %s\n", link_path, opts->target_exe); + +done: + driver_free(env, link_path, path_size); + return rc; +} + +unsigned driver_install_links(DriverEnv* env, const char* bindir, + unsigned group_mask, + const DriverInstallLinkOpts* opts, + unsigned* out_done) { + unsigned n = driver_tool_count(); + unsigned j; + unsigned done_count = 0, failures = 0; + + for (j = 0; j < n; ++j) { + if (group_mask != DRIVER_GROUP_ALL && + (driver_tool_groups(j) & group_mask) == 0) + continue; + if (driver_install_link_one(env, bindir, driver_tool_name(j), opts) != 0) + ++failures; + else + ++done_count; + } + + if (out_done) *out_done = done_count; + return failures; +} diff --git a/driver/lib/install_links.h b/driver/lib/install_links.h @@ -0,0 +1,62 @@ +#ifndef KIT_DRIVER_INSTALL_LINKS_H +#define KIT_DRIVER_INSTALL_LINKS_H + +#include <stddef.h> + +#include "driver.h" +#include "env.h" + +/* Shared tool-link laying for `kit install` (and, in future, `kit update`). + * + * "Installing" the kit toolchain means populating a directory with one entry + * per tool, each pointing at the running kit binary, so the tools can be + * invoked by their bare names (cc, ld, nm, ...). Entries are symlinks on POSIX + * and hard links on Windows; either can be forced. This module owns the + * mechanism (build the path, overwrite/replace policy, symlink-vs-hardlink, + * dry-run/verbose printing, diagnostic on failure); callers own the policy + * (which directory, which tools, how the link kind was chosen). + * + * Every diagnostic is emitted via driver_errf(tool_tag, ...) so the caller's + * tool name appears in error lines. */ + +/* group_mask sentinel meaning "every tool compiled into this binary", + * regardless of its DriverToolGroup. Distinct from any real group-bit + * combination so it cannot collide with a DRIVER_GROUP_* union. */ +#define DRIVER_GROUP_ALL ((unsigned)~0u) + +/* Options shared by every link laid in one install/update run. `target_exe` is + * the link target (typically the running kit binary's absolute path). + * `use_hardlink` selects hard links (1) over symlinks (0). `force` replaces an + * existing entry; without it, an existing entry is an error. `dry_run` prints + * what would happen and changes nothing. `verbose` prints each link as it is + * created. `tool_tag` names the calling tool in diagnostics. */ +typedef struct DriverInstallLinkOpts { + const char* target_exe; + const char* tool_tag; + int use_hardlink; + int force; + int dry_run; + int verbose; +} DriverInstallLinkOpts; + +/* Lay one link `bindir/name` -> opts->target_exe per `opts`. Returns 0 on + * success, nonzero on failure (after emitting a diagnostic via + * driver_errf(opts->tool_tag, ...)). Honors dry-run (prints, no change), force + * (replace an existing entry), and verbose. */ +int driver_install_link_one(DriverEnv* env, const char* bindir, + const char* name, + const DriverInstallLinkOpts* opts); + +/* Lay a link in `bindir` for every tool in the centralized table whose group + * set is selected by `group_mask`: a tool at index j is included iff + * `group_mask == DRIVER_GROUP_ALL` or `(driver_tool_groups(j) & group_mask)` + * is non-zero. Each link points at opts->target_exe. Returns 0 when every + * selected link succeeded, otherwise the number of links that failed (each + * already diagnosed). The count of links laid (the would-be / installed total) + * is stored in *out_done when out_done is non-NULL. */ +unsigned driver_install_links(DriverEnv* env, const char* bindir, + unsigned group_mask, + const DriverInstallLinkOpts* opts, + unsigned* out_done); + +#endif diff --git a/driver/main.c b/driver/main.c @@ -279,12 +279,12 @@ int driver_argv_wants_help(int argc, char** argv, int accept_short_h) { } int driver_argv_wants_version(int argc, char** argv) { - int i; - for (i = 1; i < argc; ++i) { - if (driver_streq(argv[i], "--")) break; /* see driver_argv_wants_help */ - if (driver_streq(argv[i], "--version")) return 1; - } - return 0; + /* Only the FIRST argument a tool receives counts as a version query, so a + * sub-flag that legitimately spells --version (e.g. `pkg create --version + * 2026.6.0`, where --version is the package version) is never hijacked. The + * common forms `cc --version` and `kit cc --version` both arrive with + * --version as argv[1] of the dispatched tool. */ + return argc >= 2 && driver_streq(argv[1], "--version"); } void driver_print_version(const char* tool) { diff --git a/driver/release_key.c b/driver/release_key.c @@ -0,0 +1,26 @@ +/* Embedded release trust anchor(s) for `kit update`. See doc/plan/SELFDIST.md. + * + * `kit update` verifies a downloaded release against this built-in set of + * minisign public keys; any one key verifying suffices (overlap-window + * rotation). At real release time the production release public key is added + * here, with its secret held offline and never in the tree. + * + * The default entry below is the in-tree NON-RELEASE test key + * (test/dist/keys/nonrelease.pub, key id c1709dd2922282f6) — the same key the + * hermetic end-to-end test and the `make dist` fallback signer use, so a + * stock-built kit can verify a `make dist` artifact against a built-in anchor. + * It is NOT a release key and grants no trust over real distributions. */ + +#include "driver.h" + +static const KitReleaseKey kit_release_keys[] = { + {"kit NON-RELEASE test key (c1709dd2922282f6)", + "untrusted comment: kit public key c1709dd2922282f6\n" + "RWTBcJ3SkiKC9rw+zEgXOtsk4rCI4x+PSf9VtBaP9XJFczB5RLtTOjLJ\n"}, +}; + +const KitReleaseKey* driver_release_keys(unsigned* count) { + if (count) + *count = (unsigned)(sizeof kit_release_keys / sizeof kit_release_keys[0]); + return kit_release_keys; +} diff --git a/include/kit/package.h b/include/kit/package.h @@ -138,4 +138,61 @@ KIT_API KitStatus kit_pkg_inspect(const KitContext* ctx, KitPkgFormat format, int show_encoding, KitWriter* out); +/* ---- Channel index (kit-release) ----------------------------------------- + * + * A small, signed, byte-stable text file describing the release artifacts of + * one channel (e.g. "stable"), in the same canonical-text + detached-minisign + * idiom as the package manifest. The library emits/parses the text and the + * CalVer comparison; trust, channel selection, host-triple matching, version + * policy, and signing/verification (via kit_pkg_* / minisign) stay with the + * caller. See doc/plan/SELFDIST.md. + * + * KitReleaseIndex is large (fixed-capacity host/url tables); allocate it on the + * heap, not the stack. Capacities are loud limits — emit/parse fail rather than + * silently truncate when exceeded. */ + +#define KIT_RELEASE_CHANNEL_MAX 32u +#define KIT_RELEASE_TARGET_MAX 64u +#define KIT_RELEASE_MAX_HOSTS 24u +#define KIT_RELEASE_MAX_URLS 6u +#define KIT_RELEASE_URL_MAX 256u + +typedef struct KitReleaseHost { + char target[KIT_RELEASE_TARGET_MAX]; /* hosted triple, e.g. "aarch64-macos" */ + uint8_t kpkg_id[KIT_CAS_HASH_LEN]; /* package-id of the .kpkg */ + uint8_t targz_id[KIT_CAS_HASH_LEN]; /* package-id of the .tar.gz */ + int has_kpkg; /* 1 if kpkg_id is present */ + int has_targz; /* 1 if targz_id is present */ + uint64_t size; /* .kpkg size in bytes; 0 = unspecified */ + char urls[KIT_RELEASE_MAX_URLS][KIT_RELEASE_URL_MAX]; /* ordered mirror list */ + unsigned n_urls; +} KitReleaseHost; + +typedef struct KitReleaseIndex { + char channel[KIT_RELEASE_CHANNEL_MAX]; /* e.g. "stable" */ + char version[KIT_PKG_VERSION_MAX]; /* CalVer "YYYY.MINOR.PATCH" */ + KitReleaseHost hosts[KIT_RELEASE_MAX_HOSTS]; + unsigned n_hosts; +} KitReleaseIndex; + +/* Emit the canonical `kit-release 1` text for *index* to out. The output is + * byte-stable for a given index value (sorted [host] sections by target, + * fixed key order), so its blake2b id is reproducible. */ +KIT_API KitStatus kit_release_index_emit(const KitContext* ctx, + const KitReleaseIndex* index, + KitWriter* out); + +/* Parse `kit-release 1` text into *out* (zeroed first). Rejects an unknown + * version line, unknown keys/sections, malformed hex ids, non-canonical + * ordering, and any table overflow; emits detail through ctx->diag. */ +KIT_API KitStatus kit_release_index_parse(const KitContext* ctx, + const uint8_t* data, size_t len, + KitReleaseIndex* out); + +/* Compare two CalVer strings ("YYYY.MINOR.PATCH") numerically, field by field + * (YYYY, then MINOR, then PATCH). Stores the sign of a-b in *cmp (<0, 0, >0). + * Returns KIT_OK on success, or KIT_INVALID when either string is not + * well-formed CalVer (three non-negative decimal fields). */ +KIT_API KitStatus kit_calver_compare(const char* a, const char* b, int* cmp); + #endif diff --git a/mk/driver_srcs.mk b/mk/driver_srcs.mk @@ -84,6 +84,7 @@ DRIVER_SRCS += $(call need-any,STRIP OBJCOPY,driver/lib/objedit.c) DRIVER_SRCS += $(call need-any,RUN,driver/lib/wasm_run.c) DRIVER_SRCS += $(call need-any,CC CHECK BUILD_EXE BUILD_LIB BUILD_OBJ,driver/lib/link_inputs.c) DRIVER_SRCS += $(call need-any,CAS PKG,driver/lib/dist_host.c) +DRIVER_SRCS += $(call need-any,INSTALL UPDATE,driver/lib/install_links.c) DRIVER_SRCS += $(call need-any,ADDR2LINE SYMBOLIZE,driver/lib/dwarfsym.c) DRIVER_SRCS += $(call need-any,DBG RUN,driver/lib/backtrace.c) diff --git a/mk/lib_srcs.mk b/mk/lib_srcs.mk @@ -150,6 +150,7 @@ LIB_SRCS_DIST_CODEC = src/dist/deflate.c src/dist/lz4.c LIB_SRCS_DIST_COMPRESS = src/dist/lz4frame.c LIB_SRCS_DIST_PKG = src/dist/b64.c src/dist/ed25519.c src/dist/minisig.c \ src/dist/tar.c src/dist/kpkg.c src/dist/manifest.c \ + src/dist/release.c \ src/dist/trust.c LIB_SRCS_VENDOR_PKG = vendor/monocypher/monocypher-ed25519.c LIB_SRCS_DEBUG := $(shell find src/debug -name '*.c' 2>/dev/null) diff --git a/mk/test.mk b/mk/test.mk @@ -81,6 +81,7 @@ TEST_TARGETS = \ test-gram \ test-driver-gram \ test-hash \ + test-release-index \ test-driver-strip \ test-dwarf \ test-ecosystem \ @@ -547,6 +548,7 @@ CG_CONST_TEST_BIN = build/test/cg_const_test STRENGTH_REDUCE_TEST_BIN = build/test/strength_reduce_test TARGET_TEST_BIN = build/test/target_test HASH_TEST_BIN = build/test/hash_test +RELEASE_INDEX_TEST_BIN = build/test/release_index_test PANIC_RECOVERY_TEST_BIN = build/test/panic_recovery_test ABI_CLASSIFY_TEST_BIN = build/test/abi_classify_test IR_RECORDER_TEST_BIN = build/test/ir_recorder_test @@ -569,6 +571,9 @@ test-cg-api: $(TARGET_TEST_BIN) $(CG_API_TEST_BIN) $(CG_SWITCH_TEST_BIN) \ test-hash: $(HASH_TEST_BIN) $(HASH_TEST_BIN) +test-release-index: $(RELEASE_INDEX_TEST_BIN) + $(RELEASE_INDEX_TEST_BIN) + test-abi-classify: $(ABI_CLASSIFY_TEST_BIN) $(ABI_CLASSIFY_TEST_BIN) diff --git a/mk/test_unit.mk b/mk/test_unit.mk @@ -32,7 +32,7 @@ UNIT_CFLAGS_INTERNAL = $(HOST_CFLAGS) -Iinclude -Isrc -Itest UNIT_TESTS_PUBLIC := \ ar_test target_test arm32_target_features_test cg_api_test cg_switch_test \ cg_fp_cmp_test \ - cg_control_test cg_const_test hash_test \ + cg_control_test cg_const_test hash_test release_index_test \ panic_recovery_test profile_test \ link_script_test \ rv64_jit_test rv32_jit_test aa64_inline_test rv64_inline_test x64_inline_test \ @@ -42,6 +42,7 @@ ar_test_SRC := test/ar/ar_test.c target_test_SRC := test/api/target_test.c arm32_target_features_test_SRC := test/api/arm32_target_features_test.c hash_test_SRC := test/api/hash_test.c +release_index_test_SRC := test/api/release_index_test.c panic_recovery_test_SRC := test/api/panic_recovery_test.c profile_test_SRC := test/api/profile_test.c link_script_test_SRC := test/link/link_script_test.c diff --git a/src/api/package.c b/src/api/package.c @@ -23,6 +23,7 @@ #include "dist/lz4.h" #include "dist/manifest.h" #include "dist/minisig.h" +#include "dist/release.h" #include "dist/tar.h" #include "dist/tree.h" #include "dist/trust.h" @@ -1686,6 +1687,40 @@ KitStatus kit_pkg_inspect(const KitContext* ctx, const uint8_t* pkg_data, } } +KitStatus kit_release_index_emit(const KitContext* ctx, + const KitReleaseIndex* index, KitWriter* out) { + char err[128]; + if (!ctx || !index || !out) return KIT_INVALID; + /* Validate first so an invalid index is reported with a clear reason; the + * emit re-validates internally but does not surface the message. */ + if (dist_release_index_validate(index, err, sizeof err) != DIST_OK) { + kit_ctx_diagf(ctx, "%s", err); + return KIT_ERR; + } + if (dist_release_index_emit(index, out) != DIST_OK) { + if (kit_writer_status(out) != KIT_OK) return KIT_IO; + kit_ctx_diagf(ctx, "release index emit failed"); + return KIT_ERR; + } + return kit_writer_status(out) == KIT_OK ? KIT_OK : KIT_IO; +} + +KitStatus kit_release_index_parse(const KitContext* ctx, const uint8_t* data, + size_t len, KitReleaseIndex* out) { + char err[128]; + if (!ctx || !data || !out) return KIT_INVALID; + if (dist_release_index_parse(data, len, out, err, sizeof err) != DIST_OK) { + kit_ctx_diagf(ctx, "%s", err); + return KIT_MALFORMED; + } + return KIT_OK; +} + +KitStatus kit_calver_compare(const char* a, const char* b, int* cmp) { + if (!a || !b || !cmp) return KIT_INVALID; + return dist_calver_compare(a, b, cmp) == DIST_OK ? KIT_OK : KIT_INVALID; +} + KitStatus kit_pkg_keygen(const KitContext* ctx, KitPkgRandomFn rng, void* rng_user, KitWriter* pub_out, KitWriter* sec_out, uint8_t out_keyid[KIT_PKG_KEYID_LEN]) { diff --git a/src/dist/release.c b/src/dist/release.c @@ -0,0 +1,352 @@ +#include "release.h" + +#include <stdio.h> +#include <string.h> + +#include "dist_parse.h" + +/* The `kit-release 1` channel index: byte-stable canonical text in the package + * manifest idiom (src/dist/manifest.c). Top-level keys are emitted in the fixed + * order channel/version/hash; each [host] block is emitted sorted ascending by + * target, with keys target/kpkg/targz/size then the url mirror list in stored + * order. Operates directly on the public KitReleaseIndex. */ + +_Static_assert(KIT_CAS_HASH_LEN == DIST_BLAKE2B_LEN, "release hash len"); + +static int emit(KitWriter* out, const char* s) { + return kit_writer_write(out, s, strlen(s)) == KIT_OK ? DIST_OK : DIST_ERR; +} + +static int emit_hex(KitWriter* out, const char* key, + const uint8_t h[DIST_BLAKE2B_LEN]) { + char hex[2 * DIST_BLAKE2B_LEN + 1]; + dist_hex_encode(hex, h, DIST_BLAKE2B_LEN); + return dist_emit_kv(out, key, hex); +} + +static int emit_u64(KitWriter* out, const char* key, uint64_t v) { + char num[24]; + snprintf(num, sizeof num, "%llu", (unsigned long long)v); + return dist_emit_kv(out, key, num); +} + +/* Per-host seen-key bits for the strict parser. */ +#define R1_F_TARGET 0x00000001u +#define R1_F_KPKG 0x00000002u +#define R1_F_TARGZ 0x00000004u +#define R1_F_SIZE 0x00000008u + +/* Top-level seen-key bits. */ +#define R1_T_CHANNEL 0x00000001u +#define R1_T_VERSION 0x00000002u +#define R1_T_HASH 0x00000004u + +typedef enum { R1_SEC_TOP, R1_SEC_HOST } ReleaseSection; + +static int field_text_valid(const char* s, int required) { + if (required && !s[0]) return 0; + for (; *s; ++s) { + if (*s == '\n' || *s == '\r') return 0; + } + return 1; +} + +/* A target triple must be non-empty, fit, and contain no whitespace. */ +static int target_text_valid(const char* s) { + if (!s[0]) return 0; + for (; *s; ++s) { + if (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') return 0; + } + return 1; +} + +static int decode_id(uint8_t out[DIST_BLAKE2B_LEN], const char* val, + const char* err_msg, char* err, size_t errcap) { + if (strlen(val) != 2u * DIST_BLAKE2B_LEN || + dist_hex_decode(out, val, DIST_BLAKE2B_LEN) != DIST_OK) + return dist_set_err(err, errcap, err_msg); + return DIST_OK; +} + +static int seen_once(uint32_t* seen, uint32_t bit, const char* msg, char* err, + size_t errcap) { + if (*seen & bit) return dist_set_err(err, errcap, msg); + *seen |= bit; + return DIST_OK; +} + +/* Parse "YYYY.MINOR.PATCH" into three uint64 fields. Each field must be a + * non-empty run of decimal digits with no overflow; exactly three fields, + * dot-separated, are required. */ +static int calver_split(const char* s, uint64_t out[3]) { + size_t field = 0; + if (!s) return DIST_ERR; + for (;;) { + uint64_t v = 0; + int digits = 0; + if (field >= 3) return DIST_ERR; + while (*s >= '0' && *s <= '9') { + unsigned d = (unsigned)(*s - '0'); + if (v > (UINT64_MAX - (uint64_t)d) / 10u) return DIST_ERR; + v = v * 10u + (uint64_t)d; + ++digits; + ++s; + } + if (!digits) return DIST_ERR; + out[field++] = v; + if (*s == '\0') break; + if (*s != '.') return DIST_ERR; + ++s; + } + return field == 3 ? DIST_OK : DIST_ERR; +} + +int dist_calver_compare(const char* a, const char* b, int* cmp) { + uint64_t fa[3], fb[3]; + size_t i; + if (!cmp) return DIST_ERR; + if (calver_split(a, fa) != DIST_OK || calver_split(b, fb) != DIST_OK) + return DIST_ERR; + for (i = 0; i < 3; ++i) { + if (fa[i] != fb[i]) { + *cmp = fa[i] < fb[i] ? -1 : 1; + return DIST_OK; + } + } + *cmp = 0; + return DIST_OK; +} + +int dist_release_index_validate(const KitReleaseIndex* idx, char* err, + size_t errcap) { + uint64_t ver[3]; + unsigned i, j; + + if (!field_text_valid(idx->channel, 1)) + return dist_set_err(err, errcap, "bad or empty channel"); + if (calver_split(idx->version, ver) != DIST_OK) + return dist_set_err(err, errcap, "bad CalVer version"); + if (idx->n_hosts > KIT_RELEASE_MAX_HOSTS) + return dist_set_err(err, errcap, "too many hosts"); + + for (i = 0; i < idx->n_hosts; ++i) { + const KitReleaseHost* h = &idx->hosts[i]; + if (!target_text_valid(h->target)) + return dist_set_err(err, errcap, "bad or empty host target"); + if (h->n_urls > KIT_RELEASE_MAX_URLS) + return dist_set_err(err, errcap, "too many urls"); + for (j = 0; j < h->n_urls; ++j) { + if (!field_text_valid(h->urls[j], 1)) + return dist_set_err(err, errcap, "bad or empty url"); + } + /* Targets must be unique, but the in-memory array need not be pre-sorted: + * emit canonicalizes the [host] order (and parse enforces ascending in the + * file), so a caller may build hosts in any order. */ + for (j = 0; j < i; ++j) { + if (strcmp(idx->hosts[j].target, h->target) == 0) + return dist_set_err(err, errcap, "duplicate host target"); + } + } + return DIST_OK; +} + +/* Stable ascending order over host targets without mutating the input: emit + * iterates an index permutation. The host count is small (<= caps), so an + * O(n^2) selection of the next-smallest target is fine and keeps the emit + * order canonical even if the caller built the index unsorted. */ +static unsigned next_host(const KitReleaseIndex* idx, int* used) { + unsigned i, best = idx->n_hosts; + for (i = 0; i < idx->n_hosts; ++i) { + if (used[i]) continue; + if (best == idx->n_hosts || + strcmp(idx->hosts[i].target, idx->hosts[best].target) < 0) + best = i; + } + return best; +} + +int dist_release_index_emit(const KitReleaseIndex* idx, KitWriter* out) { + int used[KIT_RELEASE_MAX_HOSTS]; + char err[128]; + unsigned k; + + if (dist_release_index_validate(idx, err, sizeof err) != DIST_OK) + return DIST_ERR; + + if (emit(out, DIST_RELEASE1_MAGIC "\n") != DIST_OK) return DIST_ERR; + if (dist_emit_kv(out, "channel", idx->channel) != DIST_OK) return DIST_ERR; + if (dist_emit_kv(out, "version", idx->version) != DIST_OK) return DIST_ERR; + if (dist_emit_kv(out, "hash", DIST_RELEASE1_HASH) != DIST_OK) return DIST_ERR; + + memset(used, 0, sizeof used); + for (k = 0; k < idx->n_hosts; ++k) { + unsigned hi = next_host(idx, used); + const KitReleaseHost* h; + unsigned u; + used[hi] = 1; + h = &idx->hosts[hi]; + if (emit(out, "\n[host]\n") != DIST_OK) return DIST_ERR; + if (dist_emit_kv(out, "target", h->target) != DIST_OK) return DIST_ERR; + if (h->has_kpkg && emit_hex(out, "kpkg", h->kpkg_id) != DIST_OK) + return DIST_ERR; + if (h->has_targz && emit_hex(out, "targz", h->targz_id) != DIST_OK) + return DIST_ERR; + if (h->size && emit_u64(out, "size", h->size) != DIST_OK) return DIST_ERR; + for (u = 0; u < h->n_urls; ++u) { + if (dist_emit_kv(out, "url", h->urls[u]) != DIST_OK) return DIST_ERR; + } + } + return DIST_OK; +} + +int dist_release_index_parse(const uint8_t* data, size_t len, + KitReleaseIndex* out, char* err, size_t errcap) { + size_t pos = 0; + int first = 1; + ReleaseSection sec = R1_SEC_TOP; + uint32_t tseen = 0, hseen = 0; + KitReleaseHost* host = NULL; + + memset(out, 0, sizeof *out); + + while (pos < len) { + char buf[DIST_KV_LINE_MAX]; + size_t end = pos; + size_t n, i; + char *t, *key, *val, *eq; + + while (end < len && data[end] != '\n') ++end; + n = end - pos; + if (n >= sizeof buf) return dist_set_err(err, errcap, "line too long"); + for (i = pos; i < end; ++i) + if (data[i] == 0) + return dist_set_err(err, errcap, "NUL byte in release index"); + memcpy(buf, data + pos, n); + buf[n] = '\0'; + pos = (end < len) ? end + 1 : end; + dist_trim_trail(buf); + + if (first) { + first = 0; + if (strcmp(buf, DIST_RELEASE1_MAGIC) != 0) + return dist_set_err(err, errcap, "bad release index magic/version"); + continue; + } + + t = dist_trim_lead(buf); + if (*t == '\0' || *t == '#') continue; + + if (*t == '[') { + if (strcmp(t, "[host]") != 0) + return dist_set_err(err, errcap, "unknown section"); + if (sec == R1_SEC_TOP) { + if ((tseen & (R1_T_CHANNEL | R1_T_VERSION | R1_T_HASH)) != + (R1_T_CHANNEL | R1_T_VERSION | R1_T_HASH)) + return dist_set_err(err, errcap, "missing required top-level key"); + } else if (!(hseen & R1_F_TARGET)) { + return dist_set_err(err, errcap, "[host] missing target"); + } + if (out->n_hosts >= KIT_RELEASE_MAX_HOSTS) + return dist_set_err(err, errcap, "too many hosts"); + sec = R1_SEC_HOST; + hseen = 0; + host = &out->hosts[out->n_hosts++]; + continue; + } + + eq = strchr(t, '='); + if (!eq) return dist_set_err(err, errcap, "expected key = value"); + *eq = '\0'; + key = t; + dist_trim_trail(key); + val = dist_trim_lead(eq + 1); + + if (sec == R1_SEC_TOP) { + if (strcmp(key, "channel") == 0) { + if (seen_once(&tseen, R1_T_CHANNEL, "duplicate top-level key", err, + errcap) != DIST_OK) + return DIST_ERR; + if (!field_text_valid(val, 1)) + return dist_set_err(err, errcap, "bad channel"); + if (dist_copy_field(out->channel, sizeof out->channel, val, err, + errcap)) + return DIST_ERR; + } else if (strcmp(key, "version") == 0) { + if (seen_once(&tseen, R1_T_VERSION, "duplicate top-level key", err, + errcap) != DIST_OK) + return DIST_ERR; + if (!field_text_valid(val, 1)) + return dist_set_err(err, errcap, "bad version"); + if (dist_copy_field(out->version, sizeof out->version, val, err, + errcap)) + return DIST_ERR; + } else if (strcmp(key, "hash") == 0) { + if (seen_once(&tseen, R1_T_HASH, "duplicate top-level key", err, + errcap) != DIST_OK) + return DIST_ERR; + if (strcmp(val, DIST_RELEASE1_HASH) != 0) + return dist_set_err(err, errcap, "unsupported hash algorithm"); + } else { + return dist_set_err(err, errcap, "unknown top-level key"); + } + } else { + if (strcmp(key, "target") == 0) { + if (seen_once(&hseen, R1_F_TARGET, "duplicate [host] key", err, + errcap) != DIST_OK) + return DIST_ERR; + if (!target_text_valid(val)) + return dist_set_err(err, errcap, "bad host target"); + if (dist_copy_field(host->target, sizeof host->target, val, err, + errcap)) + return DIST_ERR; + if (out->n_hosts >= 2u && + strcmp(out->hosts[out->n_hosts - 2u].target, val) >= 0) + return dist_set_err(err, errcap, + "hosts not ascending/unique by target"); + } else if (strcmp(key, "kpkg") == 0) { + if (seen_once(&hseen, R1_F_KPKG, "duplicate [host] key", err, errcap) != + DIST_OK) + return DIST_ERR; + if (decode_id(host->kpkg_id, val, "bad kpkg id", err, errcap) != + DIST_OK) + return DIST_ERR; + host->has_kpkg = 1; + } else if (strcmp(key, "targz") == 0) { + if (seen_once(&hseen, R1_F_TARGZ, "duplicate [host] key", err, errcap) != + DIST_OK) + return DIST_ERR; + if (decode_id(host->targz_id, val, "bad targz id", err, errcap) != + DIST_OK) + return DIST_ERR; + host->has_targz = 1; + } else if (strcmp(key, "size") == 0) { + if (seen_once(&hseen, R1_F_SIZE, "duplicate [host] key", err, errcap) != + DIST_OK) + return DIST_ERR; + if (dist_parse_u64(val, &host->size) != DIST_OK) + return dist_set_err(err, errcap, "bad size"); + } else if (strcmp(key, "url") == 0) { + if (host->n_urls >= KIT_RELEASE_MAX_URLS) + return dist_set_err(err, errcap, "too many urls"); + if (!field_text_valid(val, 1)) + return dist_set_err(err, errcap, "bad url"); + if (dist_copy_field(host->urls[host->n_urls], sizeof host->urls[0], val, + err, errcap)) + return DIST_ERR; + ++host->n_urls; + } else { + return dist_set_err(err, errcap, "unknown [host] key"); + } + } + } + + if (first) return dist_set_err(err, errcap, "bad release index magic/version"); + if (sec == R1_SEC_TOP) { + if ((tseen & (R1_T_CHANNEL | R1_T_VERSION | R1_T_HASH)) != + (R1_T_CHANNEL | R1_T_VERSION | R1_T_HASH)) + return dist_set_err(err, errcap, "missing required top-level key"); + } else if (!(hseen & R1_F_TARGET)) { + return dist_set_err(err, errcap, "[host] missing target"); + } + return dist_release_index_validate(out, err, errcap); +} diff --git a/src/dist/release.h b/src/dist/release.h @@ -0,0 +1,42 @@ +#ifndef KIT_DIST_RELEASE_H +#define KIT_DIST_RELEASE_H + +#include <kit/core.h> +#include <kit/package.h> /* KitReleaseIndex / KitReleaseHost */ +#include <stddef.h> +#include <stdint.h> + +#include "dist.h" + +/* The signed channel index ("kit-release"). Byte-stable canonical text in the + * same idiom as the package manifest (src/dist/manifest.{h,c}); the detached + * minisign signature and trust policy live in the caller. Operates directly on + * the public KitReleaseIndex so the driver (which sees only include/kit/) and + * the library share one representation. See doc/plan/SELFDIST.md. */ + +#define DIST_RELEASE1_MAGIC "kit-release 1" +#define DIST_RELEASE1_HASH "blake2b-256" + +/* Emit the canonical `kit-release 1` text for *idx* to *out*. Returns DIST_OK, + * or DIST_ERR on a writer failure or an invalid index (validated first). */ +int dist_release_index_emit(const KitReleaseIndex* idx, KitWriter* out); + +/* Parse `kit-release 1` text into *out* (zeroed first). Returns DIST_OK, or + * DIST_ERR with a human-readable reason in err[0..errcap) (when errcap > 0) on + * a malformed magic/version, unknown key/section, bad hex id, non-canonical + * [host] ordering (must be ascending by target), or any table overflow. */ +int dist_release_index_parse(const uint8_t* data, size_t len, + KitReleaseIndex* out, char* err, size_t errcap); + +/* Structural validation shared by emit and parse: non-empty channel, valid + * CalVer version, each host has a valid target triple, ascending unique + * targets, url/host counts within caps. Returns DIST_OK or DIST_ERR (+err). */ +int dist_release_index_validate(const KitReleaseIndex* idx, char* err, + size_t errcap); + +/* Compare two CalVer strings ("YYYY.MINOR.PATCH") numerically, field by field. + * Stores the sign of a-b in *cmp. Returns DIST_OK, or DIST_ERR when either + * string is not three non-negative decimal fields. */ +int dist_calver_compare(const char* a, const char* b, int* cmp); + +#endif diff --git a/test/api/release_index_test.c b/test/api/release_index_test.c @@ -0,0 +1,148 @@ +/* release_index_test — public <kit/package.h> channel-index surface: + * + * - Build a KitReleaseIndex (two hosts, inserted out of target order, with + * both ids, a size, and a mirror list), emit it to a memory KitWriter, and + * assert the EXACT canonical bytes (byte-stability: hosts sorted ascending + * by target, fixed key order). + * - Parse those bytes back and assert full round-trip equality. + * - kit_calver_compare: numeric (not lexical) ordering and malformed input. + * + * Run by: make test-release-index + */ + +#include <kit/core.h> +#include <kit/package.h> +#include <stddef.h> +#include <stdint.h> +#include <string.h> + +#include "lib/kit_unit.h" + +static KitUnit g_u; +#define EXPECT(c, ...) CU_EXPECT(&g_u, c, __VA_ARGS__) + +/* aarch64-macos sorts before x86_64-linux-gnu, so it is emitted first even + * though we register it second. kpkg id = bytes 0x00..0x1f, targz = 0x20..0x3f. + */ +static const char* const GOLDEN = + "kit-release 1\n" + "channel = stable\n" + "version = 2026.6.0\n" + "hash = blake2b-256\n" + "\n" + "[host]\n" + "target = aarch64-macos\n" + "kpkg = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\n" + "targz = 202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f\n" + "size = 31457280\n" + "url = https://example/a.kpkg\n" + "url = https://dl.example/a.kpkg\n" + "url = https://mirror.example/a.kpkg\n" + "\n" + "[host]\n" + "target = x86_64-linux-gnu\n" + "kpkg = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\n" + "url = https://example/b.kpkg\n"; + +static void fill_index(KitReleaseIndex* idx) { + KitReleaseHost* a; + KitReleaseHost* b; + unsigned i; + memset(idx, 0, sizeof *idx); + snprintf(idx->channel, sizeof idx->channel, "%s", "stable"); + snprintf(idx->version, sizeof idx->version, "%s", "2026.6.0"); + idx->n_hosts = 2; + + /* Register x86_64-linux-gnu FIRST to prove emit re-sorts ascending. */ + b = &idx->hosts[0]; + snprintf(b->target, sizeof b->target, "%s", "x86_64-linux-gnu"); + for (i = 0; i < KIT_CAS_HASH_LEN; ++i) b->kpkg_id[i] = (uint8_t)i; + b->has_kpkg = 1; + b->has_targz = 0; + b->size = 0; /* omitted */ + snprintf(b->urls[0], sizeof b->urls[0], "%s", "https://example/b.kpkg"); + b->n_urls = 1; + + a = &idx->hosts[1]; + snprintf(a->target, sizeof a->target, "%s", "aarch64-macos"); + for (i = 0; i < KIT_CAS_HASH_LEN; ++i) a->kpkg_id[i] = (uint8_t)i; + for (i = 0; i < KIT_CAS_HASH_LEN; ++i) a->targz_id[i] = (uint8_t)(i + 32u); + a->has_kpkg = 1; + a->has_targz = 1; + a->size = 31457280u; + snprintf(a->urls[0], sizeof a->urls[0], "%s", "https://example/a.kpkg"); + snprintf(a->urls[1], sizeof a->urls[1], "%s", "https://dl.example/a.kpkg"); + snprintf(a->urls[2], sizeof a->urls[2], "%s", "https://mirror.example/a.kpkg"); + a->n_urls = 3; +} + +static int host_eq(const KitReleaseHost* x, const KitReleaseHost* y) { + unsigned i; + if (strcmp(x->target, y->target) != 0) return 0; + if (x->has_kpkg != y->has_kpkg || x->has_targz != y->has_targz) return 0; + if (x->size != y->size || x->n_urls != y->n_urls) return 0; + if (x->has_kpkg && memcmp(x->kpkg_id, y->kpkg_id, KIT_CAS_HASH_LEN) != 0) + return 0; + if (x->has_targz && memcmp(x->targz_id, y->targz_id, KIT_CAS_HASH_LEN) != 0) + return 0; + for (i = 0; i < x->n_urls; ++i) + if (strcmp(x->urls[i], y->urls[i]) != 0) return 0; + return 1; +} + +/* The parsed index is canonical (ascending by target), so compare it to the + * sorted expectation: aarch64-macos first, then x86_64-linux-gnu. */ +static void check_roundtrip(const uint8_t* bytes, size_t len) { + static KitReleaseIndex want; /* large; keep off the stack */ + static KitReleaseIndex got; + fill_index(&want); + EXPECT(kit_release_index_parse(&g_u.ctx, bytes, len, &got) == KIT_OK, + "parse golden bytes OK"); + EXPECT(strcmp(got.channel, want.channel) == 0, "channel round-trips"); + EXPECT(strcmp(got.version, want.version) == 0, "version round-trips"); + EXPECT(got.n_hosts == want.n_hosts, "n_hosts round-trips (%u)", got.n_hosts); + if (got.n_hosts == 2) { + EXPECT(host_eq(&got.hosts[0], &want.hosts[1]), "host[0] == aarch64-macos"); + EXPECT(host_eq(&got.hosts[1], &want.hosts[0]), + "host[1] == x86_64-linux-gnu"); + } +} + +static void check_emit_parse(void) { + static KitReleaseIndex idx; /* large; keep off the stack */ + KitWriter* w = NULL; + const uint8_t* bytes; + size_t len, want_len; + fill_index(&idx); + EXPECT(kit_writer_mem(&g_u.heap, &w) == KIT_OK, "writer_mem OK"); + EXPECT(kit_release_index_emit(&g_u.ctx, &idx, w) == KIT_OK, "emit OK"); + bytes = kit_writer_mem_bytes(w, &len); + want_len = strlen(GOLDEN); + EXPECT(len == want_len, "emitted length %zu == golden %zu", len, want_len); + EXPECT(len == want_len && memcmp(bytes, GOLDEN, len) == 0, + "emitted bytes match golden exactly"); + check_roundtrip(bytes, len); + kit_writer_close(w); +} + +static void check_calver(void) { + int cmp = 99; + EXPECT(kit_calver_compare("2026.6.0", "2026.6.1", &cmp) == KIT_OK && cmp < 0, + "2026.6.0 < 2026.6.1"); + EXPECT(kit_calver_compare("2026.10.0", "2026.9.0", &cmp) == KIT_OK && cmp > 0, + "2026.10.0 > 2026.9.0 (numeric, not lexical)"); + EXPECT(kit_calver_compare("2026.6.0", "2026.6.0", &cmp) == KIT_OK && cmp == 0, + "2026.6.0 == 2026.6.0"); + EXPECT(kit_calver_compare("2026.6", "2026.6.0", &cmp) == KIT_INVALID, + "too few fields -> KIT_INVALID"); + EXPECT(kit_calver_compare("2026.x.0", "2026.6.0", &cmp) == KIT_INVALID, + "non-numeric field -> KIT_INVALID"); +} + +int main(void) { + kit_unit_init(&g_u); + check_emit_parse(); + check_calver(); + kit_unit_summary(&g_u, "release_index_test"); + return kit_unit_status(&g_u); +} diff --git a/test/dist/keys/README.md b/test/dist/keys/README.md @@ -0,0 +1,14 @@ +# NON-RELEASE test keys — DO NOT TRUST + +`nonrelease.key` / `nonrelease.pub` are a **passwordless, checked-in, NON-RELEASE** +minisign (Ed25519) keypair, key id `c1709dd2922282f6`, used only by: + +- the hermetic self-distribution end-to-end test (`test/dist/run.sh`), and +- the `make dist` fallback signer when `KIT_SIGN_KEY` is unset. + +Its public key is embedded in `driver/release_key.c` as a built-in `kit update` +trust anchor **so the test and `make dist` artifacts verify against a built-in +key**. It is published in the open here on purpose; it confers **no** trust over +real kit distributions. The production release keypair is generated offline, its +secret never enters the tree, and its public key replaces/augments the embedded +set at release time (see `doc/plan/SELFDIST.md`). diff --git a/test/dist/keys/nonrelease.key b/test/dist/keys/nonrelease.key @@ -0,0 +1,2 @@ +untrusted comment: kit secret key c1709dd2922282f6 +RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwXCd0pIigvbXcrDcsFwhxEa3USWokj5FD+ZjAwSuMuv3BqxpDsb+gbw+zEgXOtsk4rCI4x+PSf9VtBaP9XJFczB5RLtTOjLJ5D19jc6Et+kZL5ScC9tOKWIXoUCB8mtnRtgRnIg9wkk= diff --git a/test/dist/keys/nonrelease.pub b/test/dist/keys/nonrelease.pub @@ -0,0 +1,2 @@ +untrusted comment: kit public key c1709dd2922282f6 +RWTBcJ3SkiKC9rw+zEgXOtsk4rCI4x+PSf9VtBaP9XJFczB5RLtTOjLJ