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:
| M | driver/cmd/cmp.c | | | 111 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------ |
| M | driver/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;
}