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 }