kit

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

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 }