kit

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

commit e8bc95ca28685f8642767f92489c7b8b8da8781f
parent 72567eb207a5b848e9066eba1837f61d96b96004
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu, 16 Jul 2026 10:12:59 -0700

tools: verify checksum manifests and classify cmp status

Diffstat:
Mdriver/cmd/cmp.c | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mdriver/cmd/hash.c | 302++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
2 files changed, 378 insertions(+), 35 deletions(-)

diff --git a/driver/cmd/cmp.c b/driver/cmd/cmp.c @@ -9,7 +9,8 @@ /* `kit cmp` — compare two files byte by byte, GNU cmp style. Default prints * the first differing byte (1-based offset + line); -l lists every difference; * -s is silent. Exit codes follow GNU cmp and kit's convention exactly: - * 0 identical, 1 differ, 2 trouble/usage. With FILE2 omitted or `-`, the second + * 0 identical, 1 differ or operational failure, 2 bad usage. With FILE2 + * omitted or `-`, the second * operand is stdin. Optional SKIP1/SKIP2 skip leading bytes of each input. */ #define CMP_TOOL "cmp" @@ -41,13 +42,15 @@ void driver_help_cmp(void) { " -s, --quiet, --silent print nothing; status only\n" " -l, --verbose list each differing byte (octal)\n" " -b, --print-bytes show the differing byte values\n" - " -n N compare at most N bytes\n" + " -n N, --bytes=N compare at most N bytes\n" + " -i N, --ignore-initial=N\n" + " skip N bytes in both inputs; N1:N2 skips\n" + " different counts in FILE1 and FILE2\n" " -h, --help show this help\n" "\n" "COMPATIBILITY AND PATHS\n" - " --ignore-initial and --bytes are not accepted; use positional\n" - " SKIP1/SKIP2 and -n. Bare -- is not accepted, so spell a\n" - " leading-dash file as ./-name.\n" + " Positional SKIP1/SKIP2 remain accepted. Use -- before a\n" + " leading-dash file name.\n" "\n" "EXAMPLES\n" " kit cmp original.bin copy.bin\n" @@ -57,9 +60,46 @@ void driver_help_cmp(void) { " cat copy.bin | kit cmp original.bin -\n" "\n" "EXIT CODES\n" - " 0 identical 1 differ 2 trouble/usage\n" - " A missing or unreadable file currently returns 2, as does malformed\n" - " command-line usage; do not treat 2 as a usage-only status.\n"))); + " 0 identical 1 differ / I/O error 2 bad usage\n"))); +} + +static int cmp_parse_count_span(const char* s, size_t n, uint64_t* out) { + uint64_t v = 0; + size_t i = 0; + unsigned base = 10; + if (!s || !n) return 1; + if (n > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) { + base = 16; + i = 2; + } + if (i == n) return 1; + for (; i < n; ++i) { + int d = driver_hex_nibble(s[i]); + if (d < 0 || (unsigned)d >= base || + v > (UINT64_MAX - (uint64_t)d) / (uint64_t)base) + return 1; + v = v * (uint64_t)base + (uint64_t)d; + } + *out = v; + return 0; +} + +/* Parse GNU cmp's SKIP or SKIP1:SKIP2 spelling. A single count applies to + * both inputs. */ +static int cmp_parse_initial(const char* s, uint64_t skip[2]) { + const char* colon = driver_strchr(s, ':'); + size_t n = driver_strlen(s); + if (!colon) { + if (cmp_parse_count_span(s, n, &skip[0]) != 0) return 1; + skip[1] = skip[0]; + return 0; + } + if (driver_strchr(colon + 1, ':') || + cmp_parse_count_span(s, (size_t)(colon - s), &skip[0]) != 0 || + cmp_parse_count_span(colon + 1, n - (size_t)(colon + 1 - s), &skip[1]) != + 0) + return 1; + return 0; } /* Load a named operand, or stdin when the name is "-". Returns 0 on success @@ -108,6 +148,7 @@ int driver_cmp(int argc, char** argv) { const char* names[2] = {NULL, NULL}; uint64_t skip[2] = {0, 0}; int npos = 0; /* count of positional operands seen */ + int options = 1; int i, rc = 2; const uint8_t* d1 = NULL; @@ -129,30 +170,66 @@ int driver_cmp(int argc, char** argv) { for (i = 1; i < argc; ++i) { const char* a = argv[i]; - if (driver_streq(a, "-s") || driver_streq(a, "--quiet") || - driver_streq(a, "--silent")) { + const char* val = NULL; + if (options && driver_streq(a, "--")) { + options = 0; + continue; + } + if (options && (driver_streq(a, "-s") || driver_streq(a, "--quiet") || + driver_streq(a, "--silent"))) { opts.silent = 1; continue; } - if (driver_streq(a, "-l") || driver_streq(a, "--verbose")) { + if (options && + (driver_streq(a, "-l") || driver_streq(a, "--verbose"))) { opts.list = 1; continue; } - if (driver_streq(a, "-b") || driver_streq(a, "--print-bytes")) { + if (options && + (driver_streq(a, "-b") || driver_streq(a, "--print-bytes"))) { opts.show_bytes = 1; continue; } - if (driver_streq(a, "-n")) { + if (options && + (driver_streq(a, "-n") || driver_streq(a, "--bytes"))) { if (i + 1 >= argc || driver_parse_u64(argv[++i], &opts.max) != 0) { - driver_errf(CMP_TOOL, "-n requires a non-negative count"); + driver_errf(CMP_TOOL, "%s requires a non-negative count", a); + goto done; + } + opts.have_max = 1; + continue; + } + if (options && driver_strneq(a, "--bytes=", 8)) val = a + 8; + if (options && !val && a[0] == '-' && a[1] == 'n' && a[2]) val = a + 2; + if (val) { + if (driver_parse_u64(val, &opts.max) != 0) { + driver_errf(CMP_TOOL, "invalid byte count: %s", val); goto done; } opts.have_max = 1; continue; } + if (options && + (driver_streq(a, "-i") || driver_streq(a, "--ignore-initial"))) { + if (i + 1 >= argc || cmp_parse_initial(argv[++i], skip) != 0) { + driver_errf(CMP_TOOL, "%s requires SKIP or SKIP1:SKIP2", a); + goto done; + } + continue; + } + val = NULL; + if (options && driver_strneq(a, "--ignore-initial=", 17)) val = a + 17; + if (options && !val && a[0] == '-' && a[1] == 'i' && a[2]) val = a + 2; + if (val) { + if (cmp_parse_initial(val, skip) != 0) { + driver_errf(CMP_TOOL, "invalid initial skip: %s", val); + goto done; + } + continue; + } if (driver_streq(a, "-")) { /* stdin operand */ - } else if (a[0] == '-' && a[1] != '\0') { + } else if (options && a[0] == '-' && a[1] != '\0') { driver_errf(CMP_TOOL, "unknown option: %s", a); goto done; } @@ -182,12 +259,12 @@ int driver_cmp(int argc, char** argv) { } if (cmp_load(&env, names[0], &d1, &l1, &ld1, &sb1, &sl1) != 0) { - rc = 2; + rc = 1; goto done; } loaded1 = 1; if (cmp_load(&env, names[1], &d2, &l2, &ld2, &sb2, &sl2) != 0) { - rc = 2; + rc = 1; goto done; } loaded2 = 1; diff --git a/driver/cmd/hash.c b/driver/cmd/hash.c @@ -36,6 +36,9 @@ static const HashPersona HASH_CRC32 = {"crc32", KIT_HASH_CRC32, 1}; typedef struct HashOpts { KitHashAlgo algo; + int check; + int quiet; + int status_only; } HashOpts; static const char* hash_algo_name(KitHashAlgo algo) { @@ -87,11 +90,10 @@ static void hash_help(const HashPersona* persona) { "\n" "ALIASES\n" " sha256sum, b2sum, and crc32 fix the algorithm and reject -a.\n" - " These tools do not implement checksum-list verification (-c).\n" + " sha256sum and b2sum also verify manifests with -c/--check.\n" "\n" "PATHS\n" - " A bare -- terminator is not accepted in this release. Prefix a\n" - " leading-dash file with ./, for example ./-payload.bin.\n" + " Use -- before a leading-dash file name.\n" "\n" "EXAMPLES\n" " kit hash README.txt\n" @@ -110,17 +112,21 @@ static void hash_help(const HashPersona* persona) { "\n" "USAGE\n" " sha256sum [FILE...]\n" + " sha256sum -c [--quiet|--status] [MANIFEST...]\n" "\n" "DESCRIPTION\n" " Prints lowercase SHA-256, two spaces, then the file name. With no\n" " FILE, or with -, reads stdin. The algorithm is fixed; -a is rejected.\n" - " Checksum-list verification (-c) is not implemented.\n" + " -c verifies standard SHA-256 manifest lines.\n" "\n" "OPTIONS\n" + " -c, --check verify manifests\n" + " --quiet omit OK rows while checking\n" + " --status print no check rows; status only\n" " -h, --help show this help and exit\n" "\n" "PATHS\n" - " Bare -- is not accepted; spell a leading-dash file as ./-name.\n" + " Use -- before a leading-dash file name.\n" "\n" "EXAMPLES\n" " sha256sum release.kpkg\n" @@ -134,18 +140,22 @@ static void hash_help(const HashPersona* persona) { "\n" "USAGE\n" " b2sum [FILE...]\n" + " b2sum -c [--quiet|--status] [MANIFEST...]\n" "\n" "DESCRIPTION\n" " Prints lowercase BLAKE2b-256, two spaces, then the file name. GNU\n" " b2sum defaults to BLAKE2b-512, so its default digest is twice as\n" - " wide. The algorithm is fixed; -a is rejected, and checksum-list\n" - " verification (-c) is not implemented. With no FILE, reads stdin.\n" + " wide. The algorithm is fixed; -a is rejected. Kit verifies only\n" + " 256-bit BLAKE2b manifests. With no FILE, reads stdin.\n" "\n" "OPTIONS\n" + " -c, --check verify BLAKE2b-256 manifests\n" + " --quiet omit OK rows while checking\n" + " --status print no check rows; status only\n" " -h, --help show this help and exit\n" "\n" "PATHS\n" - " Bare -- is not accepted; spell a leading-dash file as ./-name.\n" + " Use -- before a leading-dash file name.\n" "\n" "EXAMPLES\n" " b2sum tree.manifest\n" @@ -169,7 +179,7 @@ static void hash_help(const HashPersona* persona) { " -h, --help show this help and exit\n" "\n" "PATHS\n" - " Bare -- is not accepted; spell a leading-dash file as ./-name.\n" + " Use -- before a leading-dash file name.\n" "\n" "EXAMPLES\n" " crc32 image.bin\n" @@ -185,23 +195,30 @@ void driver_help_sha256sum(void) { hash_help(&HASH_SHA256SUM); } void driver_help_b2sum(void) { hash_help(&HASH_B2SUM); } void driver_help_crc32(void) { hash_help(&HASH_CRC32); } +static int hash_bytes(const KitContext* ctx, KitHashAlgo algo, + const uint8_t* data, size_t len, uint8_t* digest, + size_t* dlen) { + KitHasher* h = NULL; + if (kit_hasher_new(ctx, algo, &h) != KIT_OK) return 1; + kit_hasher_update(h, data, len); + kit_hasher_final(h, digest, dlen); + kit_hasher_free(h); + return 0; +} + /* Hash data[0..len) with opts->algo and print "<hex> <name>". Returns 0 on * success, 1 on failure (error already reported under `tool`). */ static int hash_one(const KitContext* ctx, const HashOpts* opts, const char* tool, const uint8_t* data, size_t len, const char* name) { - KitHasher* h = NULL; uint8_t digest[KIT_HASH_MAX_LEN]; char hex[KIT_HASH_MAX_LEN * 2 + 1]; size_t dlen = 0, i; - if (kit_hasher_new(ctx, opts->algo, &h) != KIT_OK) { + if (hash_bytes(ctx, opts->algo, data, len, digest, &dlen) != 0) { driver_errf(tool, "failed to start hasher"); return 1; } - kit_hasher_update(h, data, len); - kit_hasher_final(h, digest, &dlen); - kit_hasher_free(h); for (i = 0; i < dlen; ++i) { hex[i * 2] = HASH_HEX[digest[i] >> 4]; @@ -212,11 +229,197 @@ static int hash_one(const KitContext* ctx, const HashOpts* opts, return 0; } +typedef struct HashCheckState { + DriverEnv* env; + KitContext* ctx; + const HashOpts* opts; + const HashPersona* persona; + uint8_t* stdin_data; + size_t stdin_len; + int stdin_loaded; +} HashCheckState; + +/* Decode one coreutils manifest line. Returns 0 on success, 1 for malformed + * syntax, 2 for a GNU BLAKE2b-512 row, and 3 for allocation failure. */ +static int hash_manifest_line(DriverEnv* env, const HashPersona* persona, + const uint8_t* line, size_t len, + uint8_t expected[32], char** out_name, + size_t* out_name_size) { + size_t off = 0, hex_len = 0, i, w = 0; + int escaped = 0; + char* name; + + if (len && line[len - 1] == '\r') --len; + if (len && line[0] == '\\') { + escaped = 1; + off = 1; + } + while (off + hex_len < len && driver_hex_nibble((char)line[off + hex_len]) >= 0) + ++hex_len; + if (persona->algo == KIT_HASH_BLAKE2B && hex_len == 128) + return 2; + if (hex_len != 64 || off + 66 > len || line[off + 64] != ' ' || + (line[off + 65] != ' ' && line[off + 65] != '*') || off + 66 == len) + return 1; + for (i = 0; i < 32; ++i) { + int hi = driver_hex_nibble((char)line[off + i * 2]); + int lo = driver_hex_nibble((char)line[off + i * 2 + 1]); + if (hi < 0 || lo < 0) return 1; + expected[i] = (uint8_t)((hi << 4) | lo); + } + + name = driver_alloc(env, len - (off + 66) + 1); + if (!name) return 3; + for (i = off + 66; i < len; ++i) { + uint8_t c = line[i]; + if (c == '\0') { + driver_free(env, name, len - (off + 66) + 1); + return 1; + } + if (escaped && c == '\\') { + if (++i >= len) { + driver_free(env, name, len - (off + 66) + 1); + return 1; + } + if (line[i] == '\\') + c = '\\'; + else if (line[i] == 'n') + c = '\n'; + else if (line[i] == 'r') + c = '\r'; + else { + driver_free(env, name, len - (off + 66) + 1); + return 1; + } + } + name[w++] = (char)c; + } + name[w] = '\0'; + *out_name = name; + *out_name_size = len - (off + 66) + 1; + return 0; +} + +static int hash_check_payload(HashCheckState* st, const char* name, + const uint8_t expected[32]) { + DriverLoad ld = {0}; + KitSlice input = KIT_SLICE_NULL; + uint8_t digest[KIT_HASH_MAX_LEN]; + size_t dlen = 0; + int loaded = 0, ok = 0; + + if (driver_streq(name, "-")) { + if (!st->stdin_loaded) { + if (!driver_read_stdin(st->env, &st->stdin_data, &st->stdin_len)) { + driver_errf(st->persona->name, "failed to read stdin payload"); + goto done; + } + st->stdin_loaded = 1; + } + input.data = st->stdin_data; + input.len = st->stdin_len; + } else { + if (driver_load_bytes(&st->env->file_io, st->persona->name, name, &ld, + &input) != 0) + goto done; + loaded = 1; + } + if (hash_bytes(st->ctx, st->opts->algo, input.data, input.len, digest, + &dlen) != 0) { + driver_errf(st->persona->name, "%s: failed to compute checksum", name); + goto done; + } + ok = dlen == 32 && memcmp(digest, expected, 32) == 0; + +done: + if (!st->opts->status_only && (!ok || !st->opts->quiet)) + driver_printf("%s: %s\n", name, ok ? "OK" : "FAILED"); + if (loaded) driver_release_bytes(&st->env->file_io, &ld); + return ok ? 0 : 1; +} + +static int hash_check_manifest(HashCheckState* st, const char* manifest) { + DriverLoad ld = {0}; + KitSlice text = KIT_SLICE_NULL; + uint8_t* stdin_manifest = NULL; + size_t stdin_manifest_len = 0; + size_t off = 0, line_no = 0, valid = 0; + int loaded = 0, rc = 0; + int manifest_is_stdin = driver_streq(manifest, "-"); + + if (manifest_is_stdin) { + if (!driver_read_stdin(st->env, &stdin_manifest, &stdin_manifest_len)) { + driver_errf(st->persona->name, "failed to read checksum manifest stdin"); + return 1; + } + text.data = stdin_manifest; + text.len = stdin_manifest_len; + } else { + if (driver_load_bytes(&st->env->file_io, st->persona->name, manifest, &ld, + &text) != 0) + return 1; + loaded = 1; + } + + while (off < text.len) { + size_t end = off; + uint8_t expected[32]; + char* name = NULL; + size_t name_size = 0; + int prc; + while (end < text.len && text.data[end] != '\n') ++end; + ++line_no; + if (end == off || (end == off + 1 && text.data[off] == '\r')) { + off = end < text.len ? end + 1 : end; + continue; + } + prc = hash_manifest_line(st->env, st->persona, text.data + off, end - off, + expected, &name, &name_size); + if (prc != 0) { + if (prc == 2) + driver_errf(st->persona->name, + "%s:%llu: BLAKE2b-512 manifest is incompatible with " + "Kit's BLAKE2b-256 contract", + manifest, (unsigned long long)line_no); + else if (prc == 3) + driver_errf(st->persona->name, "out of memory"); + else + driver_errf(st->persona->name, "%s:%llu: malformed checksum line", + manifest, (unsigned long long)line_no); + rc = 1; + off = end < text.len ? end + 1 : end; + continue; + } + ++valid; + if (manifest_is_stdin && driver_streq(name, "-")) { + driver_errf(st->persona->name, + "manifest and payload cannot both read standard input"); + if (!st->opts->status_only) + driver_printf("%s: FAILED\n", name); + rc = 1; + } else if (hash_check_payload(st, name, expected) != 0) { + rc = 1; + } + driver_free(st->env, name, name_size); + off = end < text.len ? end + 1 : end; + } + if (!valid) { + driver_errf(st->persona->name, "%s: no properly formatted checksum lines", + manifest); + rc = 1; + } + if (loaded) driver_release_bytes(&st->env->file_io, &ld); + if (stdin_manifest) + driver_free(st->env, stdin_manifest, stdin_manifest_len); + return rc; +} + static int hash_main(int argc, char** argv, const HashPersona* persona) { DriverEnv env; KitContext ctx; HashOpts opts; - int i, rc = 1, any_input = 0; + HashCheckState check; + int i, rc = 1, any_input = 0, options = 1; if (driver_argv_wants_help(argc, argv, 1)) { hash_help(persona); @@ -231,7 +434,11 @@ static int hash_main(int argc, char** argv, const HashPersona* persona) { /* First pass: options. */ for (i = 1; i < argc; ++i) { const char* a = argv[i]; - if (driver_streq(a, "-a")) { + if (options && driver_streq(a, "--")) { + options = 0; + continue; + } + if (options && driver_streq(a, "-a")) { if (persona->locked) { driver_errf(persona->name, "-a is not accepted; %s always uses %s", persona->name, hash_algo_name(persona->algo)); @@ -245,11 +452,29 @@ static int hash_main(int argc, char** argv, const HashPersona* persona) { } continue; } + if (options && (driver_streq(a, "-c") || driver_streq(a, "--check"))) { + if (!persona->locked || persona->algo == KIT_HASH_CRC32) { + driver_errf(persona->name, + "checksum manifests are supported by sha256sum and b2sum"); + rc = 2; + goto done; + } + opts.check = 1; + continue; + } + if (options && driver_streq(a, "--quiet")) { + opts.quiet = 1; + continue; + } + if (options && driver_streq(a, "--status")) { + opts.status_only = 1; + continue; + } if (driver_streq(a, "-")) { any_input = 1; continue; } - if (a[0] == '-' && a[1] != '\0') { + if (options && a[0] == '-' && a[1] != '\0') { driver_errf(persona->name, "unknown option: %s", a); rc = 2; goto done; @@ -257,6 +482,42 @@ static int hash_main(int argc, char** argv, const HashPersona* persona) { any_input = 1; } + if ((opts.quiet || opts.status_only) && !opts.check) { + driver_errf(persona->name, "--quiet and --status require --check"); + rc = 2; + goto done; + } + + if (opts.check) { + memset(&check, 0, sizeof check); + check.env = &env; + check.ctx = &ctx; + check.opts = &opts; + check.persona = persona; + rc = 0; + if (!any_input) { + rc = hash_check_manifest(&check, "-"); + } else { + options = 1; + for (i = 1; i < argc; ++i) { + const char* a = argv[i]; + if (options && driver_streq(a, "--")) { + options = 0; + continue; + } + if (options && (driver_streq(a, "-c") || + driver_streq(a, "--check") || + driver_streq(a, "--quiet") || + driver_streq(a, "--status"))) + continue; + if (hash_check_manifest(&check, a) != 0) rc = 1; + } + } + if (check.stdin_data) + driver_free(&env, check.stdin_data, check.stdin_len); + goto done; + } + /* No file operands: hash stdin. */ if (!any_input) { uint8_t* buf = NULL; @@ -273,9 +534,14 @@ static int hash_main(int argc, char** argv, const HashPersona* persona) { /* Second pass: inputs, in argv order. */ rc = 0; + options = 1; for (i = 1; i < argc; ++i) { const char* a = argv[i]; - if (driver_streq(a, "-a")) { + if (options && driver_streq(a, "--")) { + options = 0; + continue; + } + if (options && driver_streq(a, "-a")) { ++i; /* skip its value (only reachable for the generic, unlocked tool) */ continue; }