hash.c (18560B)
1 #include <kit/core.h> 2 #include <kit/hash.h> 3 #include <stddef.h> 4 #include <stdint.h> 5 #include <string.h> 6 7 #include "driver.h" 8 #include "env.h" 9 10 /* `kit hash` — print the SHA-256, BLAKE2b-256, or CRC-32 digest of each 11 * input. Output is coreutils-style ("<hex> <name>"), so it diffs cleanly 12 * against sha256sum / b2sum / cksum -a output. With no FILE, or with `-`, 13 * reads stdin. Drives the streaming kit_hasher_* API (the one-shot 14 * kit_hash stays for library callers). 15 * 16 * The same core also backs the standard-named aliases, each of which pins the 17 * algorithm so the tool is a drop-in for the matching command: 18 * sha256sum -> SHA-256 b2sum -> BLAKE2b-256 crc32 -> CRC-32 19 * (Note: GNU b2sum defaults to BLAKE2b-512; kit's BLAKE2b is 256-bit, so the 20 * digests differ in width.) The aliases reject -a, since their algorithm is 21 * fixed; the generic `hash` tool keeps -a for selecting any of the three. */ 22 23 static const char HASH_HEX[] = "0123456789abcdef"; 24 25 /* Invocation personality: which algorithm, and whether the name pins it. */ 26 typedef struct HashPersona { 27 const char* name; /* tool name for diagnostics: hash/sha256sum/... */ 28 KitHashAlgo algo; /* default (and, when locked, the only) algorithm */ 29 int locked; /* alias pins the algorithm: -a is rejected */ 30 } HashPersona; 31 32 static const HashPersona HASH_GENERIC = {"hash", KIT_HASH_SHA256, 0}; 33 static const HashPersona HASH_SHA256SUM = {"sha256sum", KIT_HASH_SHA256, 1}; 34 static const HashPersona HASH_B2SUM = {"b2sum", KIT_HASH_BLAKE2B, 1}; 35 static const HashPersona HASH_CRC32 = {"crc32", KIT_HASH_CRC32, 1}; 36 37 typedef struct HashOpts { 38 KitHashAlgo algo; 39 int check; 40 int quiet; 41 int status_only; 42 } HashOpts; 43 44 static const char* hash_algo_name(KitHashAlgo algo) { 45 switch (algo) { 46 case KIT_HASH_SHA256: 47 return "sha256"; 48 case KIT_HASH_BLAKE2B: 49 return "blake2b"; 50 case KIT_HASH_CRC32: 51 return "crc32"; 52 } 53 return "?"; 54 } 55 56 static int hash_parse_algo(const char* s, KitHashAlgo* out) { 57 if (driver_streq(s, "sha256")) { 58 *out = KIT_HASH_SHA256; 59 return 0; 60 } 61 if (driver_streq(s, "blake2b")) { 62 *out = KIT_HASH_BLAKE2B; 63 return 0; 64 } 65 if (driver_streq(s, "crc32")) { 66 *out = KIT_HASH_CRC32; 67 return 0; 68 } 69 return 1; 70 } 71 72 static void hash_help(const HashPersona* persona) { 73 if (!persona->locked) { 74 driver_printf( 75 "%.*s", 76 KIT_SLICE_ARG(KIT_SLICE_LIT( 77 "kit hash — hash files with SHA-256, BLAKE2b-256, or CRC-32\n" 78 "\n" 79 "USAGE\n" 80 " kit hash [-a ALGO] [FILE...]\n" 81 "\n" 82 "DESCRIPTION\n" 83 " Prints one line per input: the lowercase-hex digest, two spaces,\n" 84 " then the file name (`-` for stdin). With no FILE, reads stdin.\n" 85 " Multiple files are processed in command-line order.\n" 86 "\n" 87 "OPTIONS\n" 88 " -a ALGO sha256 (default) | blake2b | crc32\n" 89 " -h, --help show this help and exit\n" 90 "\n" 91 "ALIASES\n" 92 " sha256sum, b2sum, and crc32 fix the algorithm and reject -a.\n" 93 " sha256sum and b2sum also verify manifests with -c/--check.\n" 94 "\n" 95 "PATHS\n" 96 " Use -- before a leading-dash file name.\n" 97 "\n" 98 "EXAMPLES\n" 99 " kit hash README.txt\n" 100 " kit hash -a blake2b archive.a\n" 101 " kit hash -a crc32 one.bin two.bin\n" 102 " printf 'hello\\n' | kit hash -a sha256\n" 103 "\n" 104 "EXIT CODES\n" 105 " 0 success 1 I/O error 2 bad usage\n"))); 106 return; 107 } 108 109 if (persona->algo == KIT_HASH_SHA256) { 110 driver_printf( 111 "sha256sum — SHA-256 of files or standard input\n" 112 "\n" 113 "USAGE\n" 114 " sha256sum [FILE...]\n" 115 " sha256sum -c [--quiet|--status] [MANIFEST...]\n" 116 "\n" 117 "DESCRIPTION\n" 118 " Prints lowercase SHA-256, two spaces, then the file name. With no\n" 119 " FILE, or with -, reads stdin. The algorithm is fixed; -a is rejected.\n" 120 " -c verifies standard SHA-256 manifest lines.\n" 121 "\n" 122 "OPTIONS\n" 123 " -c, --check verify manifests\n" 124 " --quiet omit OK rows while checking\n" 125 " --status print no check rows; status only\n" 126 " -h, --help show this help and exit\n" 127 "\n" 128 "PATHS\n" 129 " Use -- before a leading-dash file name.\n" 130 "\n" 131 "EXAMPLES\n" 132 " sha256sum release.kpkg\n" 133 " printf 'hello\\n' | sha256sum\n" 134 "\n" 135 "EXIT CODES\n" 136 " 0 success 1 I/O error 2 bad usage\n"); 137 } else if (persona->algo == KIT_HASH_BLAKE2B) { 138 driver_printf( 139 "b2sum — BLAKE2b-256 of files or standard input\n" 140 "\n" 141 "USAGE\n" 142 " b2sum [FILE...]\n" 143 " b2sum -c [--quiet|--status] [MANIFEST...]\n" 144 "\n" 145 "DESCRIPTION\n" 146 " Prints lowercase BLAKE2b-256, two spaces, then the file name. GNU\n" 147 " b2sum defaults to BLAKE2b-512, so its default digest is twice as\n" 148 " wide. The algorithm is fixed; -a is rejected. Kit verifies only\n" 149 " 256-bit BLAKE2b manifests. With no FILE, reads stdin.\n" 150 "\n" 151 "OPTIONS\n" 152 " -c, --check verify BLAKE2b-256 manifests\n" 153 " --quiet omit OK rows while checking\n" 154 " --status print no check rows; status only\n" 155 " -h, --help show this help and exit\n" 156 "\n" 157 "PATHS\n" 158 " Use -- before a leading-dash file name.\n" 159 "\n" 160 "EXAMPLES\n" 161 " b2sum tree.manifest\n" 162 " printf 'hello\\n' | b2sum\n" 163 "\n" 164 "EXIT CODES\n" 165 " 0 success 1 I/O error 2 bad usage\n"); 166 } else { 167 driver_printf( 168 "crc32 — CRC-32 of files or standard input\n" 169 "\n" 170 "USAGE\n" 171 " crc32 [FILE...]\n" 172 "\n" 173 "DESCRIPTION\n" 174 " Prints an eight-digit lowercase CRC-32, two spaces, then the file\n" 175 " name. With no FILE, or with -, reads stdin. The algorithm is fixed;\n" 176 " -a is rejected.\n" 177 "\n" 178 "OPTIONS\n" 179 " -h, --help show this help and exit\n" 180 "\n" 181 "PATHS\n" 182 " Use -- before a leading-dash file name.\n" 183 "\n" 184 "EXAMPLES\n" 185 " crc32 image.bin\n" 186 " printf 'hello\\n' | crc32\n" 187 "\n" 188 "EXIT CODES\n" 189 " 0 success 1 I/O error 2 bad usage\n"); 190 } 191 } 192 193 void driver_help_hash(void) { hash_help(&HASH_GENERIC); } 194 void driver_help_sha256sum(void) { hash_help(&HASH_SHA256SUM); } 195 void driver_help_b2sum(void) { hash_help(&HASH_B2SUM); } 196 void driver_help_crc32(void) { hash_help(&HASH_CRC32); } 197 198 static int hash_bytes(const KitContext* ctx, KitHashAlgo algo, 199 const uint8_t* data, size_t len, uint8_t* digest, 200 size_t* dlen) { 201 KitHasher* h = NULL; 202 if (kit_hasher_new(ctx, algo, &h) != KIT_OK) return 1; 203 kit_hasher_update(h, data, len); 204 kit_hasher_final(h, digest, dlen); 205 kit_hasher_free(h); 206 return 0; 207 } 208 209 /* Hash data[0..len) with opts->algo and print "<hex> <name>". Returns 0 on 210 * success, 1 on failure (error already reported under `tool`). */ 211 static int hash_one(const KitContext* ctx, const HashOpts* opts, 212 const char* tool, const uint8_t* data, size_t len, 213 const char* name) { 214 uint8_t digest[KIT_HASH_MAX_LEN]; 215 char hex[KIT_HASH_MAX_LEN * 2 + 1]; 216 size_t dlen = 0, i; 217 218 if (hash_bytes(ctx, opts->algo, data, len, digest, &dlen) != 0) { 219 driver_errf(tool, "failed to start hasher"); 220 return 1; 221 } 222 223 for (i = 0; i < dlen; ++i) { 224 hex[i * 2] = HASH_HEX[digest[i] >> 4]; 225 hex[i * 2 + 1] = HASH_HEX[digest[i] & 0x0f]; 226 } 227 hex[dlen * 2] = '\0'; 228 driver_printf("%s %s\n", hex, name); 229 return 0; 230 } 231 232 typedef struct HashCheckState { 233 DriverEnv* env; 234 KitContext* ctx; 235 const HashOpts* opts; 236 const HashPersona* persona; 237 uint8_t* stdin_data; 238 size_t stdin_len; 239 int stdin_loaded; 240 } HashCheckState; 241 242 /* Decode one coreutils manifest line. Returns 0 on success, 1 for malformed 243 * syntax, 2 for a GNU BLAKE2b-512 row, and 3 for allocation failure. */ 244 static int hash_manifest_line(DriverEnv* env, const HashPersona* persona, 245 const uint8_t* line, size_t len, 246 uint8_t expected[32], char** out_name, 247 size_t* out_name_size) { 248 size_t off = 0, hex_len = 0, i, w = 0; 249 int escaped = 0; 250 char* name; 251 252 if (len && line[len - 1] == '\r') --len; 253 if (len && line[0] == '\\') { 254 escaped = 1; 255 off = 1; 256 } 257 while (off + hex_len < len && driver_hex_nibble((char)line[off + hex_len]) >= 0) 258 ++hex_len; 259 if (persona->algo == KIT_HASH_BLAKE2B && hex_len == 128) 260 return 2; 261 if (hex_len != 64 || off + 66 > len || line[off + 64] != ' ' || 262 (line[off + 65] != ' ' && line[off + 65] != '*') || off + 66 == len) 263 return 1; 264 for (i = 0; i < 32; ++i) { 265 int hi = driver_hex_nibble((char)line[off + i * 2]); 266 int lo = driver_hex_nibble((char)line[off + i * 2 + 1]); 267 if (hi < 0 || lo < 0) return 1; 268 expected[i] = (uint8_t)((hi << 4) | lo); 269 } 270 271 name = driver_alloc(env, len - (off + 66) + 1); 272 if (!name) return 3; 273 for (i = off + 66; i < len; ++i) { 274 uint8_t c = line[i]; 275 if (c == '\0') { 276 driver_free(env, name, len - (off + 66) + 1); 277 return 1; 278 } 279 if (escaped && c == '\\') { 280 if (++i >= len) { 281 driver_free(env, name, len - (off + 66) + 1); 282 return 1; 283 } 284 if (line[i] == '\\') 285 c = '\\'; 286 else if (line[i] == 'n') 287 c = '\n'; 288 else if (line[i] == 'r') 289 c = '\r'; 290 else { 291 driver_free(env, name, len - (off + 66) + 1); 292 return 1; 293 } 294 } 295 name[w++] = (char)c; 296 } 297 name[w] = '\0'; 298 *out_name = name; 299 *out_name_size = len - (off + 66) + 1; 300 return 0; 301 } 302 303 static int hash_check_payload(HashCheckState* st, const char* name, 304 const uint8_t expected[32]) { 305 DriverLoad ld = {0}; 306 KitSlice input = KIT_SLICE_NULL; 307 uint8_t digest[KIT_HASH_MAX_LEN]; 308 size_t dlen = 0; 309 int loaded = 0, ok = 0; 310 311 if (driver_streq(name, "-")) { 312 if (!st->stdin_loaded) { 313 if (!driver_read_stdin(st->env, &st->stdin_data, &st->stdin_len)) { 314 driver_errf(st->persona->name, "failed to read stdin payload"); 315 goto done; 316 } 317 st->stdin_loaded = 1; 318 } 319 input.data = st->stdin_data; 320 input.len = st->stdin_len; 321 } else { 322 if (driver_load_bytes(&st->env->file_io, st->persona->name, name, &ld, 323 &input) != 0) 324 goto done; 325 loaded = 1; 326 } 327 if (hash_bytes(st->ctx, st->opts->algo, input.data, input.len, digest, 328 &dlen) != 0) { 329 driver_errf(st->persona->name, "%s: failed to compute checksum", name); 330 goto done; 331 } 332 ok = dlen == 32 && memcmp(digest, expected, 32) == 0; 333 334 done: 335 if (!st->opts->status_only && (!ok || !st->opts->quiet)) 336 driver_printf("%s: %s\n", name, ok ? "OK" : "FAILED"); 337 if (loaded) driver_release_bytes(&st->env->file_io, &ld); 338 return ok ? 0 : 1; 339 } 340 341 static int hash_check_manifest(HashCheckState* st, const char* manifest) { 342 DriverLoad ld = {0}; 343 KitSlice text = KIT_SLICE_NULL; 344 uint8_t* stdin_manifest = NULL; 345 size_t stdin_manifest_len = 0; 346 size_t off = 0, line_no = 0, valid = 0; 347 int loaded = 0, rc = 0; 348 int manifest_is_stdin = driver_streq(manifest, "-"); 349 350 if (manifest_is_stdin) { 351 if (!driver_read_stdin(st->env, &stdin_manifest, &stdin_manifest_len)) { 352 driver_errf(st->persona->name, "failed to read checksum manifest stdin"); 353 return 1; 354 } 355 text.data = stdin_manifest; 356 text.len = stdin_manifest_len; 357 } else { 358 if (driver_load_bytes(&st->env->file_io, st->persona->name, manifest, &ld, 359 &text) != 0) 360 return 1; 361 loaded = 1; 362 } 363 364 while (off < text.len) { 365 size_t end = off; 366 uint8_t expected[32]; 367 char* name = NULL; 368 size_t name_size = 0; 369 int prc; 370 while (end < text.len && text.data[end] != '\n') ++end; 371 ++line_no; 372 if (end == off || (end == off + 1 && text.data[off] == '\r')) { 373 off = end < text.len ? end + 1 : end; 374 continue; 375 } 376 prc = hash_manifest_line(st->env, st->persona, text.data + off, end - off, 377 expected, &name, &name_size); 378 if (prc != 0) { 379 if (prc == 2) 380 driver_errf(st->persona->name, 381 "%s:%llu: BLAKE2b-512 manifest is incompatible with " 382 "Kit's BLAKE2b-256 contract", 383 manifest, (unsigned long long)line_no); 384 else if (prc == 3) 385 driver_errf(st->persona->name, "out of memory"); 386 else 387 driver_errf(st->persona->name, "%s:%llu: malformed checksum line", 388 manifest, (unsigned long long)line_no); 389 rc = 1; 390 off = end < text.len ? end + 1 : end; 391 continue; 392 } 393 ++valid; 394 if (manifest_is_stdin && driver_streq(name, "-")) { 395 driver_errf(st->persona->name, 396 "manifest and payload cannot both read standard input"); 397 if (!st->opts->status_only) 398 driver_printf("%s: FAILED\n", name); 399 rc = 1; 400 } else if (hash_check_payload(st, name, expected) != 0) { 401 rc = 1; 402 } 403 driver_free(st->env, name, name_size); 404 off = end < text.len ? end + 1 : end; 405 } 406 if (!valid) { 407 driver_errf(st->persona->name, "%s: no properly formatted checksum lines", 408 manifest); 409 rc = 1; 410 } 411 if (loaded) driver_release_bytes(&st->env->file_io, &ld); 412 if (stdin_manifest) 413 driver_free(st->env, stdin_manifest, stdin_manifest_len); 414 return rc; 415 } 416 417 static int hash_main(int argc, char** argv, const HashPersona* persona) { 418 DriverEnv env; 419 KitContext ctx; 420 HashOpts opts; 421 HashCheckState check; 422 int i, rc = 1, any_input = 0, options = 1; 423 424 if (driver_argv_wants_help(argc, argv, 1)) { 425 hash_help(persona); 426 return 0; 427 } 428 429 memset(&opts, 0, sizeof opts); 430 opts.algo = persona->algo; 431 driver_env_init(&env); 432 ctx = driver_env_to_context(&env); 433 434 /* First pass: options. */ 435 for (i = 1; i < argc; ++i) { 436 const char* a = argv[i]; 437 if (options && driver_streq(a, "--")) { 438 options = 0; 439 continue; 440 } 441 if (options && driver_streq(a, "-a")) { 442 if (persona->locked) { 443 driver_errf(persona->name, "-a is not accepted; %s always uses %s", 444 persona->name, hash_algo_name(persona->algo)); 445 rc = 2; 446 goto done; 447 } 448 if (i + 1 >= argc || hash_parse_algo(argv[++i], &opts.algo) != 0) { 449 driver_errf(persona->name, "-a requires sha256, blake2b, or crc32"); 450 rc = 2; 451 goto done; 452 } 453 continue; 454 } 455 if (options && (driver_streq(a, "-c") || driver_streq(a, "--check"))) { 456 if (!persona->locked || persona->algo == KIT_HASH_CRC32) { 457 driver_errf(persona->name, 458 "checksum manifests are supported by sha256sum and b2sum"); 459 rc = 2; 460 goto done; 461 } 462 opts.check = 1; 463 continue; 464 } 465 if (options && driver_streq(a, "--quiet")) { 466 opts.quiet = 1; 467 continue; 468 } 469 if (options && driver_streq(a, "--status")) { 470 opts.status_only = 1; 471 continue; 472 } 473 if (driver_streq(a, "-")) { 474 any_input = 1; 475 continue; 476 } 477 if (options && a[0] == '-' && a[1] != '\0') { 478 driver_errf(persona->name, "unknown option: %s", a); 479 rc = 2; 480 goto done; 481 } 482 any_input = 1; 483 } 484 485 if ((opts.quiet || opts.status_only) && !opts.check) { 486 driver_errf(persona->name, "--quiet and --status require --check"); 487 rc = 2; 488 goto done; 489 } 490 491 if (opts.check) { 492 memset(&check, 0, sizeof check); 493 check.env = &env; 494 check.ctx = &ctx; 495 check.opts = &opts; 496 check.persona = persona; 497 rc = 0; 498 if (!any_input) { 499 rc = hash_check_manifest(&check, "-"); 500 } else { 501 options = 1; 502 for (i = 1; i < argc; ++i) { 503 const char* a = argv[i]; 504 if (options && driver_streq(a, "--")) { 505 options = 0; 506 continue; 507 } 508 if (options && (driver_streq(a, "-c") || 509 driver_streq(a, "--check") || 510 driver_streq(a, "--quiet") || 511 driver_streq(a, "--status"))) 512 continue; 513 if (hash_check_manifest(&check, a) != 0) rc = 1; 514 } 515 } 516 if (check.stdin_data) 517 driver_free(&env, check.stdin_data, check.stdin_len); 518 goto done; 519 } 520 521 /* No file operands: hash stdin. */ 522 if (!any_input) { 523 uint8_t* buf = NULL; 524 size_t n = 0; 525 if (!driver_read_stdin(&env, &buf, &n)) { 526 driver_errf(persona->name, "failed to read stdin"); 527 rc = 1; 528 goto done; 529 } 530 rc = hash_one(&ctx, &opts, persona->name, buf, n, "-"); 531 driver_free(&env, buf, n); 532 goto done; 533 } 534 535 /* Second pass: inputs, in argv order. */ 536 rc = 0; 537 options = 1; 538 for (i = 1; i < argc; ++i) { 539 const char* a = argv[i]; 540 if (options && driver_streq(a, "--")) { 541 options = 0; 542 continue; 543 } 544 if (options && driver_streq(a, "-a")) { 545 ++i; /* skip its value (only reachable for the generic, unlocked tool) */ 546 continue; 547 } 548 if (driver_streq(a, "-")) { 549 uint8_t* buf = NULL; 550 size_t n = 0; 551 if (!driver_read_stdin(&env, &buf, &n)) { 552 driver_errf(persona->name, "failed to read stdin"); 553 rc = 1; 554 continue; 555 } 556 if (hash_one(&ctx, &opts, persona->name, buf, n, "-") != 0) rc = 1; 557 driver_free(&env, buf, n); 558 continue; 559 } 560 { 561 DriverLoad ld = {0}; 562 KitSlice input; 563 if (driver_load_bytes(&env.file_io, persona->name, a, &ld, &input) != 0) { 564 rc = 1; 565 continue; 566 } 567 if (hash_one(&ctx, &opts, persona->name, input.data, input.len, a) != 0) 568 rc = 1; 569 driver_release_bytes(&env.file_io, &ld); 570 } 571 } 572 573 done: 574 driver_env_fini(&env); 575 return rc; 576 } 577 578 int driver_hash(int argc, char** argv) { 579 return hash_main(argc, argv, &HASH_GENERIC); 580 } 581 582 int driver_sha256sum(int argc, char** argv) { 583 return hash_main(argc, argv, &HASH_SHA256SUM); 584 } 585 586 int driver_b2sum(int argc, char** argv) { 587 return hash_main(argc, argv, &HASH_B2SUM); 588 } 589 590 int driver_crc32(int argc, char** argv) { 591 return hash_main(argc, argv, &HASH_CRC32); 592 }