1 /* $OpenBSD: diff.c,v 1.67 2019/06/28 13:35:00 deraadt Exp $ */ 2 3 /* 4 * Copyright (c) 2003 Todd C. Miller <Todd.Miller@courtesan.com> 5 * 6 * Permission to use, copy, modify, and distribute this software for any 7 * purpose with or without fee is hereby granted, provided that the above 8 * copyright notice and this permission notice appear in all copies. 9 * 10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 * 18 * Sponsored in part by the Defense Advanced Research Projects 19 * Agency (DARPA) and Air Force Research Laboratory, Air Force 20 * Materiel Command, USAF, under agreement number F39502-99-1-0512. 21 */ 22 23 #include <sys/cdefs.h> 24 __FBSDID("$FreeBSD$"); 25 26 #include <sys/stat.h> 27 28 #include <ctype.h> 29 #include <err.h> 30 #include <errno.h> 31 #include <getopt.h> 32 #include <stdlib.h> 33 #include <stdio.h> 34 #include <string.h> 35 #include <unistd.h> 36 #include <limits.h> 37 38 #include "diff.h" 39 #include "xmalloc.h" 40 41 bool lflag, Nflag, Pflag, rflag, sflag, Tflag, cflag; 42 bool ignore_file_case, suppress_common, color; 43 int diff_format, diff_context, status; 44 int tabsize = 8, width = 130; 45 static int colorflag = COLORFLAG_NEVER; 46 char *start, *ifdefname, *diffargs, *label[2], *ignore_pats; 47 char *group_format = NULL; 48 const char *add_code, *del_code; 49 struct stat stb1, stb2; 50 struct excludes *excludes_list; 51 regex_t ignore_re; 52 53 #define OPTIONS "0123456789aBbC:cdD:efHhI:iL:lnNPpqrS:sTtU:uwW:X:x:y" 54 enum { 55 OPT_TSIZE = CHAR_MAX + 1, 56 OPT_STRIPCR, 57 OPT_IGN_FN_CASE, 58 OPT_NO_IGN_FN_CASE, 59 OPT_NORMAL, 60 OPT_HORIZON_LINES, 61 OPT_CHANGED_GROUP_FORMAT, 62 OPT_SUPPRESS_COMMON, 63 OPT_COLOR, 64 }; 65 66 static struct option longopts[] = { 67 { "text", no_argument, 0, 'a' }, 68 { "ignore-space-change", no_argument, 0, 'b' }, 69 { "context", optional_argument, 0, 'C' }, 70 { "ifdef", required_argument, 0, 'D' }, 71 { "minimal", no_argument, 0, 'd' }, 72 { "ed", no_argument, 0, 'e' }, 73 { "forward-ed", no_argument, 0, 'f' }, 74 { "speed-large-files", no_argument, NULL, 'H' }, 75 { "ignore-blank-lines", no_argument, 0, 'B' }, 76 { "ignore-matching-lines", required_argument, 0, 'I' }, 77 { "ignore-case", no_argument, 0, 'i' }, 78 { "paginate", no_argument, NULL, 'l' }, 79 { "label", required_argument, 0, 'L' }, 80 { "new-file", no_argument, 0, 'N' }, 81 { "rcs", no_argument, 0, 'n' }, 82 { "unidirectional-new-file", no_argument, 0, 'P' }, 83 { "show-c-function", no_argument, 0, 'p' }, 84 { "brief", no_argument, 0, 'q' }, 85 { "recursive", no_argument, 0, 'r' }, 86 { "report-identical-files", no_argument, 0, 's' }, 87 { "starting-file", required_argument, 0, 'S' }, 88 { "expand-tabs", no_argument, 0, 't' }, 89 { "initial-tab", no_argument, 0, 'T' }, 90 { "unified", optional_argument, 0, 'U' }, 91 { "ignore-all-space", no_argument, 0, 'w' }, 92 { "width", required_argument, 0, 'W' }, 93 { "exclude", required_argument, 0, 'x' }, 94 { "exclude-from", required_argument, 0, 'X' }, 95 { "side-by-side", no_argument, NULL, 'y' }, 96 { "ignore-file-name-case", no_argument, NULL, OPT_IGN_FN_CASE }, 97 { "horizon-lines", required_argument, NULL, OPT_HORIZON_LINES }, 98 { "no-ignore-file-name-case", no_argument, NULL, OPT_NO_IGN_FN_CASE }, 99 { "normal", no_argument, NULL, OPT_NORMAL }, 100 { "strip-trailing-cr", no_argument, NULL, OPT_STRIPCR }, 101 { "tabsize", required_argument, NULL, OPT_TSIZE }, 102 { "changed-group-format", required_argument, NULL, OPT_CHANGED_GROUP_FORMAT}, 103 { "suppress-common-lines", no_argument, NULL, OPT_SUPPRESS_COMMON }, 104 { "color", optional_argument, NULL, OPT_COLOR }, 105 { NULL, 0, 0, '\0'} 106 }; 107 108 static void usage(void) __dead2; 109 static void conflicting_format(void) __dead2; 110 static void push_excludes(char *); 111 static void push_ignore_pats(char *); 112 static void read_excludes_file(char *file); 113 static void set_argstr(char **, char **); 114 static char *splice(char *, char *); 115 static bool do_color(void); 116 117 int 118 main(int argc, char **argv) 119 { 120 const char *errstr = NULL; 121 char *ep, **oargv; 122 long l; 123 int ch, dflags, lastch, gotstdin, prevoptind, newarg; 124 125 oargv = argv; 126 gotstdin = 0; 127 dflags = 0; 128 lastch = '\0'; 129 prevoptind = 1; 130 newarg = 1; 131 diff_context = 3; 132 diff_format = D_UNSET; 133 #define FORMAT_MISMATCHED(type) \ 134 (diff_format != D_UNSET && diff_format != (type)) 135 while ((ch = getopt_long(argc, argv, OPTIONS, longopts, NULL)) != -1) { 136 switch (ch) { 137 case '0': case '1': case '2': case '3': case '4': 138 case '5': case '6': case '7': case '8': case '9': 139 if (newarg) 140 usage(); /* disallow -[0-9]+ */ 141 else if (lastch == 'c' || lastch == 'u') 142 diff_context = 0; 143 else if (!isdigit(lastch) || diff_context > INT_MAX / 10) 144 usage(); 145 diff_context = (diff_context * 10) + (ch - '0'); 146 break; 147 case 'a': 148 dflags |= D_FORCEASCII; 149 break; 150 case 'b': 151 dflags |= D_FOLDBLANKS; 152 break; 153 case 'C': 154 case 'c': 155 if (FORMAT_MISMATCHED(D_CONTEXT)) 156 conflicting_format(); 157 diff_format = D_CONTEXT; 158 if (optarg != NULL) { 159 l = strtol(optarg, &ep, 10); 160 if (*ep != '\0' || l < 0 || l >= INT_MAX) 161 usage(); 162 diff_context = (int)l; 163 } 164 break; 165 case 'd': 166 dflags |= D_MINIMAL; 167 break; 168 case 'D': 169 if (FORMAT_MISMATCHED(D_IFDEF)) 170 conflicting_format(); 171 diff_format = D_IFDEF; 172 ifdefname = optarg; 173 break; 174 case 'e': 175 if (FORMAT_MISMATCHED(D_EDIT)) 176 conflicting_format(); 177 diff_format = D_EDIT; 178 break; 179 case 'f': 180 if (FORMAT_MISMATCHED(D_REVERSE)) 181 conflicting_format(); 182 diff_format = D_REVERSE; 183 break; 184 case 'H': 185 /* ignore but needed for compatibility with GNU diff */ 186 break; 187 case 'h': 188 /* silently ignore for backwards compatibility */ 189 break; 190 case 'B': 191 dflags |= D_SKIPBLANKLINES; 192 break; 193 case 'I': 194 push_ignore_pats(optarg); 195 break; 196 case 'i': 197 dflags |= D_IGNORECASE; 198 break; 199 case 'L': 200 if (label[0] == NULL) 201 label[0] = optarg; 202 else if (label[1] == NULL) 203 label[1] = optarg; 204 else 205 usage(); 206 break; 207 case 'l': 208 lflag = true; 209 break; 210 case 'N': 211 Nflag = true; 212 break; 213 case 'n': 214 if (FORMAT_MISMATCHED(D_NREVERSE)) 215 conflicting_format(); 216 diff_format = D_NREVERSE; 217 break; 218 case 'p': 219 dflags |= D_PROTOTYPE; 220 break; 221 case 'P': 222 Pflag = true; 223 break; 224 case 'r': 225 rflag = true; 226 break; 227 case 'q': 228 if (FORMAT_MISMATCHED(D_BRIEF)) 229 conflicting_format(); 230 diff_format = D_BRIEF; 231 break; 232 case 'S': 233 start = optarg; 234 break; 235 case 's': 236 sflag = true; 237 break; 238 case 'T': 239 Tflag = true; 240 break; 241 case 't': 242 dflags |= D_EXPANDTABS; 243 break; 244 case 'U': 245 case 'u': 246 if (FORMAT_MISMATCHED(D_UNIFIED)) 247 conflicting_format(); 248 diff_format = D_UNIFIED; 249 if (optarg != NULL) { 250 l = strtol(optarg, &ep, 10); 251 if (*ep != '\0' || l < 0 || l >= INT_MAX) 252 usage(); 253 diff_context = (int)l; 254 } 255 break; 256 case 'w': 257 dflags |= D_IGNOREBLANKS; 258 break; 259 case 'W': 260 width = (int) strtonum(optarg, 1, INT_MAX, &errstr); 261 if (errstr) { 262 warnx("Invalid argument for width"); 263 usage(); 264 } 265 break; 266 case 'X': 267 read_excludes_file(optarg); 268 break; 269 case 'x': 270 push_excludes(optarg); 271 break; 272 case 'y': 273 if (FORMAT_MISMATCHED(D_SIDEBYSIDE)) 274 conflicting_format(); 275 diff_format = D_SIDEBYSIDE; 276 break; 277 case OPT_CHANGED_GROUP_FORMAT: 278 if (FORMAT_MISMATCHED(D_GFORMAT)) 279 conflicting_format(); 280 diff_format = D_GFORMAT; 281 group_format = optarg; 282 break; 283 case OPT_HORIZON_LINES: 284 break; /* XXX TODO for compatibility with GNU diff3 */ 285 case OPT_IGN_FN_CASE: 286 ignore_file_case = true; 287 break; 288 case OPT_NO_IGN_FN_CASE: 289 ignore_file_case = false; 290 break; 291 case OPT_NORMAL: 292 if (FORMAT_MISMATCHED(D_NORMAL)) 293 conflicting_format(); 294 diff_format = D_NORMAL; 295 break; 296 case OPT_TSIZE: 297 tabsize = (int) strtonum(optarg, 1, INT_MAX, &errstr); 298 if (errstr) { 299 warnx("Invalid argument for tabsize"); 300 usage(); 301 } 302 break; 303 case OPT_STRIPCR: 304 dflags |= D_STRIPCR; 305 break; 306 case OPT_SUPPRESS_COMMON: 307 suppress_common = 1; 308 break; 309 case OPT_COLOR: 310 if (optarg == NULL || strncmp(optarg, "auto", 4) == 0) 311 colorflag = COLORFLAG_AUTO; 312 else if (strncmp(optarg, "always", 6) == 0) 313 colorflag = COLORFLAG_ALWAYS; 314 else if (strncmp(optarg, "never", 5) == 0) 315 colorflag = COLORFLAG_NEVER; 316 else 317 errx(2, "unsupported --color value '%s' (must be always, auto, or never)", 318 optarg); 319 break; 320 default: 321 usage(); 322 break; 323 } 324 lastch = ch; 325 newarg = optind != prevoptind; 326 prevoptind = optind; 327 } 328 if (diff_format == D_UNSET && (dflags & D_PROTOTYPE) != 0) 329 diff_format = D_CONTEXT; 330 if (diff_format == D_UNSET) 331 diff_format = D_NORMAL; 332 argc -= optind; 333 argv += optind; 334 335 if (do_color()) { 336 char *p; 337 const char *env; 338 339 color = true; 340 add_code = "32"; 341 del_code = "31"; 342 env = getenv("DIFFCOLORS"); 343 if (env != NULL && *env != '\0' && (p = strdup(env))) { 344 add_code = p; 345 strsep(&p, ":"); 346 if (p != NULL) 347 del_code = p; 348 } 349 } 350 351 #ifdef __OpenBSD__ 352 if (pledge("stdio rpath tmppath", NULL) == -1) 353 err(2, "pledge"); 354 #endif 355 356 /* 357 * Do sanity checks, fill in stb1 and stb2 and call the appropriate 358 * driver routine. Both drivers use the contents of stb1 and stb2. 359 */ 360 if (argc != 2) 361 usage(); 362 if (ignore_pats != NULL) { 363 char buf[BUFSIZ]; 364 int error; 365 366 if ((error = regcomp(&ignore_re, ignore_pats, 367 REG_NEWLINE | REG_EXTENDED)) != 0) { 368 regerror(error, &ignore_re, buf, sizeof(buf)); 369 if (*ignore_pats != '\0') 370 errx(2, "%s: %s", ignore_pats, buf); 371 else 372 errx(2, "%s", buf); 373 } 374 } 375 if (strcmp(argv[0], "-") == 0) { 376 fstat(STDIN_FILENO, &stb1); 377 gotstdin = 1; 378 } else if (stat(argv[0], &stb1) != 0) { 379 if (!Nflag || errno != ENOENT) 380 err(2, "%s", argv[0]); 381 dflags |= D_EMPTY1; 382 memset(&stb1, 0, sizeof(struct stat)); 383 } 384 385 if (strcmp(argv[1], "-") == 0) { 386 fstat(STDIN_FILENO, &stb2); 387 gotstdin = 1; 388 } else if (stat(argv[1], &stb2) != 0) { 389 if (!Nflag || errno != ENOENT) 390 err(2, "%s", argv[1]); 391 dflags |= D_EMPTY2; 392 memset(&stb2, 0, sizeof(stb2)); 393 stb2.st_mode = stb1.st_mode; 394 } 395 396 if (dflags & D_EMPTY1 && dflags & D_EMPTY2){ 397 warn("%s", argv[0]); 398 warn("%s", argv[1]); 399 exit(2); 400 } 401 402 if (stb1.st_mode == 0) 403 stb1.st_mode = stb2.st_mode; 404 405 if (gotstdin && (S_ISDIR(stb1.st_mode) || S_ISDIR(stb2.st_mode))) 406 errx(2, "can't compare - to a directory"); 407 set_argstr(oargv, argv); 408 if (S_ISDIR(stb1.st_mode) && S_ISDIR(stb2.st_mode)) { 409 if (diff_format == D_IFDEF) 410 errx(2, "-D option not supported with directories"); 411 diffdir(argv[0], argv[1], dflags); 412 } else { 413 if (S_ISDIR(stb1.st_mode)) { 414 argv[0] = splice(argv[0], argv[1]); 415 if (stat(argv[0], &stb1) == -1) 416 err(2, "%s", argv[0]); 417 } 418 if (S_ISDIR(stb2.st_mode)) { 419 argv[1] = splice(argv[1], argv[0]); 420 if (stat(argv[1], &stb2) == -1) 421 err(2, "%s", argv[1]); 422 } 423 print_status(diffreg(argv[0], argv[1], dflags, 1), argv[0], 424 argv[1], ""); 425 } 426 exit(status); 427 } 428 429 static void 430 set_argstr(char **av, char **ave) 431 { 432 size_t argsize; 433 char **ap; 434 435 argsize = 4 + *ave - *av + 1; 436 diffargs = xmalloc(argsize); 437 strlcpy(diffargs, "diff", argsize); 438 for (ap = av + 1; ap < ave; ap++) { 439 if (strcmp(*ap, "--") != 0) { 440 strlcat(diffargs, " ", argsize); 441 strlcat(diffargs, *ap, argsize); 442 } 443 } 444 } 445 446 /* 447 * Read in an excludes file and push each line. 448 */ 449 static void 450 read_excludes_file(char *file) 451 { 452 FILE *fp; 453 char *buf, *pattern; 454 size_t len; 455 456 if (strcmp(file, "-") == 0) 457 fp = stdin; 458 else if ((fp = fopen(file, "r")) == NULL) 459 err(2, "%s", file); 460 while ((buf = fgetln(fp, &len)) != NULL) { 461 if (buf[len - 1] == '\n') 462 len--; 463 if ((pattern = strndup(buf, len)) == NULL) 464 err(2, "xstrndup"); 465 push_excludes(pattern); 466 } 467 if (strcmp(file, "-") != 0) 468 fclose(fp); 469 } 470 471 /* 472 * Push a pattern onto the excludes list. 473 */ 474 static void 475 push_excludes(char *pattern) 476 { 477 struct excludes *entry; 478 479 entry = xmalloc(sizeof(*entry)); 480 entry->pattern = pattern; 481 entry->next = excludes_list; 482 excludes_list = entry; 483 } 484 485 static void 486 push_ignore_pats(char *pattern) 487 { 488 size_t len; 489 490 if (ignore_pats == NULL) 491 ignore_pats = xstrdup(pattern); 492 else { 493 /* old + "|" + new + NUL */ 494 len = strlen(ignore_pats) + strlen(pattern) + 2; 495 ignore_pats = xreallocarray(ignore_pats, 1, len); 496 strlcat(ignore_pats, "|", len); 497 strlcat(ignore_pats, pattern, len); 498 } 499 } 500 501 void 502 print_status(int val, char *path1, char *path2, const char *entry) 503 { 504 if (label[0] != NULL) 505 path1 = label[0]; 506 if (label[1] != NULL) 507 path2 = label[1]; 508 509 switch (val) { 510 case D_BINARY: 511 printf("Binary files %s%s and %s%s differ\n", 512 path1, entry, path2, entry); 513 break; 514 case D_DIFFER: 515 if (diff_format == D_BRIEF) 516 printf("Files %s%s and %s%s differ\n", 517 path1, entry, path2, entry); 518 break; 519 case D_SAME: 520 if (sflag) 521 printf("Files %s%s and %s%s are identical\n", 522 path1, entry, path2, entry); 523 break; 524 case D_MISMATCH1: 525 printf("File %s%s is a directory while file %s%s is a regular file\n", 526 path1, entry, path2, entry); 527 break; 528 case D_MISMATCH2: 529 printf("File %s%s is a regular file while file %s%s is a directory\n", 530 path1, entry, path2, entry); 531 break; 532 case D_SKIPPED1: 533 printf("File %s%s is not a regular file or directory and was skipped\n", 534 path1, entry); 535 break; 536 case D_SKIPPED2: 537 printf("File %s%s is not a regular file or directory and was skipped\n", 538 path2, entry); 539 break; 540 case D_ERROR: 541 break; 542 } 543 } 544 545 static void 546 usage(void) 547 { 548 (void)fprintf(stderr, 549 "usage: diff [-aBbdilpTtw] [-c | -e | -f | -n | -q | -u] [--ignore-case]\n" 550 " [--no-ignore-case] [--normal] [--strip-trailing-cr] [--tabsize]\n" 551 " [-I pattern] [-L label] file1 file2\n" 552 " diff [-aBbdilpTtw] [-I pattern] [-L label] [--ignore-case]\n" 553 " [--no-ignore-case] [--normal] [--strip-trailing-cr] [--tabsize]\n" 554 " -C number file1 file2\n" 555 " diff [-aBbdiltw] [-I pattern] [--ignore-case] [--no-ignore-case]\n" 556 " [--normal] [--strip-trailing-cr] [--tabsize] -D string file1 file2\n" 557 " diff [-aBbdilpTtw] [-I pattern] [-L label] [--ignore-case]\n" 558 " [--no-ignore-case] [--normal] [--tabsize] [--strip-trailing-cr]\n" 559 " -U number file1 file2\n" 560 " diff [-aBbdilNPprsTtw] [-c | -e | -f | -n | -q | -u] [--ignore-case]\n" 561 " [--no-ignore-case] [--normal] [--tabsize] [-I pattern] [-L label]\n" 562 " [-S name] [-X file] [-x pattern] dir1 dir2\n" 563 " diff [-aBbditwW] [--expand-tabs] [--ignore-all-blanks]\n" 564 " [--ignore-blank-lines] [--ignore-case] [--minimal]\n" 565 " [--no-ignore-file-name-case] [--strip-trailing-cr]\n" 566 " [--suppress-common-lines] [--tabsize] [--text] [--width]\n" 567 " -y | --side-by-side file1 file2\n"); 568 569 exit(2); 570 } 571 572 static void 573 conflicting_format(void) 574 { 575 576 fprintf(stderr, "error: conflicting output format options.\n"); 577 usage(); 578 } 579 580 static bool 581 do_color(void) 582 { 583 const char *p, *p2; 584 585 switch (colorflag) { 586 case COLORFLAG_AUTO: 587 p = getenv("CLICOLOR"); 588 p2 = getenv("COLORTERM"); 589 if ((p != NULL && *p != '\0') || (p2 != NULL && *p2 != '\0')) 590 return isatty(STDOUT_FILENO); 591 break; 592 case COLORFLAG_ALWAYS: 593 return (true); 594 case COLORFLAG_NEVER: 595 return (false); 596 } 597 598 return (false); 599 } 600 601 static char * 602 splice(char *dir, char *path) 603 { 604 char *tail, *buf; 605 size_t dirlen; 606 607 dirlen = strlen(dir); 608 while (dirlen != 0 && dir[dirlen - 1] == '/') 609 dirlen--; 610 if ((tail = strrchr(path, '/')) == NULL) 611 tail = path; 612 else 613 tail++; 614 xasprintf(&buf, "%.*s/%s", (int)dirlen, dir, tail); 615 return (buf); 616 } 617