kit

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

cmp.c (10756B)


      1 #include <kit/core.h>
      2 #include <stddef.h>
      3 #include <stdint.h>
      4 #include <string.h>
      5 
      6 #include "driver.h"
      7 #include "env.h"
      8 
      9 /* `kit cmp` — compare two files byte by byte, GNU cmp style. Default prints
     10  * the first differing byte (1-based offset + line); -l lists every difference;
     11  * -s is silent. Exit codes follow GNU cmp and kit's convention exactly:
     12  * 0 identical, 1 differ or operational failure, 2 bad usage. With FILE2
     13  * omitted or `-`, the second
     14  * operand is stdin. Optional SKIP1/SKIP2 skip leading bytes of each input. */
     15 
     16 #define CMP_TOOL "cmp"
     17 
     18 typedef struct CmpOpts {
     19   int silent;     /* -s / --quiet / --silent */
     20   int list;       /* -l : list all differing bytes */
     21   int show_bytes; /* -b : show the differing byte values */
     22   uint64_t max;   /* -n N : compare at most N bytes (0 = unlimited) */
     23   int have_max;   /* whether -n was given */
     24 } CmpOpts;
     25 
     26 void driver_help_cmp(void) {
     27   driver_printf(
     28       "%.*s",
     29       KIT_SLICE_ARG(KIT_SLICE_LIT(
     30           "kit cmp — compare two files byte by byte\n"
     31           "\n"
     32           "USAGE\n"
     33           "  kit cmp [OPTIONS] FILE1 [FILE2 [SKIP1 [SKIP2]]]\n"
     34           "\n"
     35           "DESCRIPTION\n"
     36           "  Compares FILE1 and FILE2. With FILE2 omitted or `-`, reads "
     37           "stdin.\n"
     38           "  SKIP1/SKIP2 skip that many leading bytes of each file before\n"
     39           "  comparing (decimal, or 0x-prefixed hex).\n"
     40           "\n"
     41           "OPTIONS\n"
     42           "  -s, --quiet, --silent   print nothing; status only\n"
     43           "  -l, --verbose           list each differing byte (octal)\n"
     44           "  -b, --print-bytes       show the differing byte values\n"
     45           "  -n N, --bytes=N         compare at most N bytes\n"
     46           "  -i N, --ignore-initial=N\n"
     47           "                           skip N bytes in both inputs; N1:N2 skips\n"
     48           "                           different counts in FILE1 and FILE2\n"
     49           "  -h, --help              show this help\n"
     50           "\n"
     51           "COMPATIBILITY AND PATHS\n"
     52           "  Positional SKIP1/SKIP2 remain accepted. Use -- before a\n"
     53           "  leading-dash file name.\n"
     54           "\n"
     55           "EXAMPLES\n"
     56           "  kit cmp original.bin copy.bin\n"
     57           "  kit cmp -s original.bin copy.bin && echo identical\n"
     58           "  kit cmp -n 64 first.bin second.bin\n"
     59           "  kit cmp first.bin second.bin 0x100 0x200\n"
     60           "  cat copy.bin | kit cmp original.bin -\n"
     61           "\n"
     62           "EXIT CODES\n"
     63           "  0   identical         1   differ / I/O error  2   bad usage\n")));
     64 }
     65 
     66 static int cmp_parse_count_span(const char* s, size_t n, uint64_t* out) {
     67   uint64_t v = 0;
     68   size_t i = 0;
     69   unsigned base = 10;
     70   if (!s || !n) return 1;
     71   if (n > 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
     72     base = 16;
     73     i = 2;
     74   }
     75   if (i == n) return 1;
     76   for (; i < n; ++i) {
     77     int d = driver_hex_nibble(s[i]);
     78     if (d < 0 || (unsigned)d >= base ||
     79         v > (UINT64_MAX - (uint64_t)d) / (uint64_t)base)
     80       return 1;
     81     v = v * (uint64_t)base + (uint64_t)d;
     82   }
     83   *out = v;
     84   return 0;
     85 }
     86 
     87 /* Parse GNU cmp's SKIP or SKIP1:SKIP2 spelling. A single count applies to
     88  * both inputs. */
     89 static int cmp_parse_initial(const char* s, uint64_t skip[2]) {
     90   const char* colon = driver_strchr(s, ':');
     91   size_t n = driver_strlen(s);
     92   if (!colon) {
     93     if (cmp_parse_count_span(s, n, &skip[0]) != 0) return 1;
     94     skip[1] = skip[0];
     95     return 0;
     96   }
     97   if (driver_strchr(colon + 1, ':') ||
     98       cmp_parse_count_span(s, (size_t)(colon - s), &skip[0]) != 0 ||
     99       cmp_parse_count_span(colon + 1, n - (size_t)(colon + 1 - s), &skip[1]) !=
    100           0)
    101     return 1;
    102   return 0;
    103 }
    104 
    105 /* Load a named operand, or stdin when the name is "-". Returns 0 on success
    106  * with data and len set. A stdin read fills stdin_buf/stdin_len (freed with
    107  * driver_free); a file read fills ld (freed with driver_release_bytes). */
    108 static int cmp_load(DriverEnv* env, const char* name, const uint8_t** data,
    109                     size_t* len, DriverLoad* ld, uint8_t** stdin_buf,
    110                     size_t* stdin_len) {
    111   if (driver_streq(name, "-")) {
    112     if (!driver_read_stdin(env, stdin_buf, stdin_len)) {
    113       driver_errf(CMP_TOOL, "failed to read stdin");
    114       return 1;
    115     }
    116     *data = *stdin_buf;
    117     *len = *stdin_len;
    118     return 0;
    119   }
    120   {
    121     KitSlice in;
    122     if (driver_load_bytes(&env->file_io, CMP_TOOL, name, ld, &in) != 0)
    123       return 1;
    124     *data = in.data;
    125     *len = in.len;
    126     return 0;
    127   }
    128 }
    129 
    130 /* Render a byte the way `cat -v` / cmp -b does: M- for the high bit, ^X for
    131  * control codes, ^? for DEL, otherwise the literal character. */
    132 static void cmp_print_catv(uint8_t c) {
    133   if (c >= 128) {
    134     driver_printf("M-");
    135     c = (uint8_t)(c - 128);
    136   }
    137   if (c < 32)
    138     driver_printf("^%c", (char)(c + 64));
    139   else if (c == 127)
    140     driver_printf("^?");
    141   else
    142     driver_printf("%c", (char)c);
    143 }
    144 
    145 int driver_cmp(int argc, char** argv) {
    146   DriverEnv env;
    147   CmpOpts opts;
    148   const char* names[2] = {NULL, NULL};
    149   uint64_t skip[2] = {0, 0};
    150   int npos = 0; /* count of positional operands seen */
    151   int options = 1;
    152   int i, rc = 2;
    153 
    154   const uint8_t* d1 = NULL;
    155   const uint8_t* d2 = NULL;
    156   size_t l1 = 0, l2 = 0;
    157   DriverLoad ld1 = {0}, ld2 = {0};
    158   uint8_t* sb1 = NULL;
    159   uint8_t* sb2 = NULL;
    160   size_t sl1 = 0, sl2 = 0;
    161   int loaded1 = 0, loaded2 = 0;
    162 
    163   if (argc < 2 || driver_argv_wants_help(argc, argv, 1)) {
    164     driver_help_cmp();
    165     return 0;
    166   }
    167 
    168   memset(&opts, 0, sizeof opts);
    169   driver_env_init(&env);
    170 
    171   for (i = 1; i < argc; ++i) {
    172     const char* a = argv[i];
    173     const char* val = NULL;
    174     if (options && driver_streq(a, "--")) {
    175       options = 0;
    176       continue;
    177     }
    178     if (options && (driver_streq(a, "-s") || driver_streq(a, "--quiet") ||
    179                     driver_streq(a, "--silent"))) {
    180       opts.silent = 1;
    181       continue;
    182     }
    183     if (options &&
    184         (driver_streq(a, "-l") || driver_streq(a, "--verbose"))) {
    185       opts.list = 1;
    186       continue;
    187     }
    188     if (options &&
    189         (driver_streq(a, "-b") || driver_streq(a, "--print-bytes"))) {
    190       opts.show_bytes = 1;
    191       continue;
    192     }
    193     if (options &&
    194         (driver_streq(a, "-n") || driver_streq(a, "--bytes"))) {
    195       if (i + 1 >= argc || driver_parse_u64(argv[++i], &opts.max) != 0) {
    196         driver_errf(CMP_TOOL, "%s requires a non-negative count", a);
    197         goto done;
    198       }
    199       opts.have_max = 1;
    200       continue;
    201     }
    202     if (options && driver_strneq(a, "--bytes=", 8)) val = a + 8;
    203     if (options && !val && a[0] == '-' && a[1] == 'n' && a[2]) val = a + 2;
    204     if (val) {
    205       if (driver_parse_u64(val, &opts.max) != 0) {
    206         driver_errf(CMP_TOOL, "invalid byte count: %s", val);
    207         goto done;
    208       }
    209       opts.have_max = 1;
    210       continue;
    211     }
    212     if (options &&
    213         (driver_streq(a, "-i") || driver_streq(a, "--ignore-initial"))) {
    214       if (i + 1 >= argc || cmp_parse_initial(argv[++i], skip) != 0) {
    215         driver_errf(CMP_TOOL, "%s requires SKIP or SKIP1:SKIP2", a);
    216         goto done;
    217       }
    218       continue;
    219     }
    220     val = NULL;
    221     if (options && driver_strneq(a, "--ignore-initial=", 17)) val = a + 17;
    222     if (options && !val && a[0] == '-' && a[1] == 'i' && a[2]) val = a + 2;
    223     if (val) {
    224       if (cmp_parse_initial(val, skip) != 0) {
    225         driver_errf(CMP_TOOL, "invalid initial skip: %s", val);
    226         goto done;
    227       }
    228       continue;
    229     }
    230     if (driver_streq(a, "-")) {
    231       /* stdin operand */
    232     } else if (options && a[0] == '-' && a[1] != '\0') {
    233       driver_errf(CMP_TOOL, "unknown option: %s", a);
    234       goto done;
    235     }
    236     /* positional: FILE1, FILE2, SKIP1, SKIP2 */
    237     if (npos < 2) {
    238       names[npos] = a;
    239     } else if (npos < 4) {
    240       if (driver_parse_u64(a, &skip[npos - 2]) != 0) {
    241         driver_errf(CMP_TOOL, "invalid skip value: %s", a);
    242         goto done;
    243       }
    244     } else {
    245       driver_errf(CMP_TOOL, "too many operands: %s", a);
    246       goto done;
    247     }
    248     ++npos;
    249   }
    250 
    251   if (npos < 1) {
    252     driver_errf(CMP_TOOL, "missing operand (need FILE1)");
    253     goto done;
    254   }
    255   if (!names[1]) names[1] = "-"; /* FILE2 defaults to stdin */
    256   if (driver_streq(names[0], "-") && driver_streq(names[1], "-")) {
    257     driver_errf(CMP_TOOL, "only one operand may be stdin (`-`)");
    258     goto done;
    259   }
    260 
    261   if (cmp_load(&env, names[0], &d1, &l1, &ld1, &sb1, &sl1) != 0) {
    262     rc = 1;
    263     goto done;
    264   }
    265   loaded1 = 1;
    266   if (cmp_load(&env, names[1], &d2, &l2, &ld2, &sb2, &sl2) != 0) {
    267     rc = 1;
    268     goto done;
    269   }
    270   loaded2 = 1;
    271 
    272   /* Apply leading-byte skips. A skip past EOF yields an empty view. */
    273   d1 = d1 + (skip[0] < l1 ? skip[0] : l1);
    274   l1 = (skip[0] < l1) ? (l1 - skip[0]) : 0;
    275   d2 = d2 + (skip[1] < l2 ? skip[1] : l2);
    276   l2 = (skip[1] < l2) ? (l2 - skip[1]) : 0;
    277 
    278   {
    279     size_t cmp_len = l1 < l2 ? l1 : l2;
    280     size_t k;
    281     uint64_t line = 1; /* 1-based line of the current position */
    282     int differ = 0;
    283 
    284     if (opts.have_max && opts.max < (uint64_t)cmp_len)
    285       cmp_len = (size_t)opts.max;
    286 
    287     for (k = 0; k < cmp_len; ++k) {
    288       if (d1[k] != d2[k]) {
    289         differ = 1;
    290         if (opts.silent) break;
    291         if (opts.list) {
    292           /* -l: "<byte> <oct1> <oct2>", 1-based, octal byte values. */
    293           driver_printf("%6llu %3llo %3llo\n", (unsigned long long)(k + 1),
    294                         (unsigned long long)d1[k], (unsigned long long)d2[k]);
    295           continue;
    296         }
    297         /* Default: report the first difference and stop. POSIX/GNU/BSD all
    298          * phrase this as "char N" (the 1-based byte offset). */
    299         driver_printf("%s %s differ: char %llu, line %llu", names[0], names[1],
    300                       (unsigned long long)(k + 1), (unsigned long long)line);
    301         if (opts.show_bytes) {
    302           driver_printf(" is %3llo ", (unsigned long long)d1[k]);
    303           cmp_print_catv(d1[k]);
    304           driver_printf(" %3llo ", (unsigned long long)d2[k]);
    305           cmp_print_catv(d2[k]);
    306         }
    307         driver_printf("\n");
    308         break;
    309       }
    310       if (d1[k] == '\n') ++line;
    311     }
    312 
    313     if (opts.list && differ) {
    314       /* listed all diffs above */
    315     }
    316 
    317     if (!differ && l1 != l2 &&
    318         (!opts.have_max || opts.max > (uint64_t)cmp_len)) {
    319       /* Equal up to the shorter length: EOF on the shorter file. */
    320       const char* shorter = l1 < l2 ? names[0] : names[1];
    321       if (!opts.silent) {
    322         driver_errf(CMP_TOOL, "EOF on %s after byte %llu", shorter,
    323                     (unsigned long long)cmp_len);
    324       }
    325       differ = 1;
    326     }
    327 
    328     rc = differ ? 1 : 0;
    329   }
    330 
    331 done:
    332   if (loaded1) {
    333     if (sb1) driver_free(&env, sb1, sl1);
    334     driver_release_bytes(&env.file_io, &ld1);
    335   }
    336   if (loaded2) {
    337     if (sb2) driver_free(&env, sb2, sl2);
    338     driver_release_bytes(&env.file_io, &ld2);
    339   }
    340   driver_env_fini(&env);
    341   return rc;
    342 }