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/stat.h>
24
25 #include <ctype.h>
26 #include <err.h>
27 #include <errno.h>
28 #include <getopt.h>
29 #include <limits.h>
30 #include <stdio.h>
31 #include <stdlib.h>
32 #include <string.h>
33 #include <unistd.h>
34
35 #include "diff.h"
36 #include "xmalloc.h"
37
38 static const char diff_version[] = "FreeBSD diff 20260206";
39 bool lflag, Nflag, Pflag, rflag, sflag, Tflag, cflag;
40 bool ignore_file_case, suppress_common, color, noderef;
41 static bool help = false;
42 int diff_format, diff_context, diff_algorithm, status;
43 bool diff_algorithm_set;
44 int tabsize = 8, width = 130;
45 static int colorflag = COLORFLAG_NEVER;
46 char *start, *ifdefname, *diffargs, *label[2];
47 char *ignore_pats, *most_recent_pat;
48 char *group_format = NULL;
49 const char *add_code, *del_code;
50 struct stat stb1, stb2;
51 struct excludes *excludes_list;
52 regex_t ignore_re, most_recent_re;
53
54 static struct algorithm {
55 const char *name;
56 int id;
57 } algorithms[] = {
58 {"stone", D_DIFFSTONE},
59 {"myers", D_DIFFMYERS},
60 {"patience", D_DIFFPATIENCE},
61 {NULL, D_DIFFNONE}
62 };
63
64 #define OPTIONS "0123456789A:aBbC:cdD:efF:HhI:iL:lnNPpqrS:sTtU:uwW:X:x:y"
65 enum {
66 OPT_TSIZE = CHAR_MAX + 1,
67 OPT_STRIPCR,
68 OPT_IGN_FN_CASE,
69 OPT_NO_IGN_FN_CASE,
70 OPT_NORMAL,
71 OPT_HELP,
72 OPT_HORIZON_LINES,
73 OPT_CHANGED_GROUP_FORMAT,
74 OPT_SUPPRESS_COMMON,
75 OPT_COLOR,
76 OPT_NO_DEREFERENCE,
77 OPT_VERSION,
78 };
79
80 static struct option longopts[] = {
81 { "algorithm", required_argument, 0, 'A' },
82 { "text", no_argument, 0, 'a' },
83 { "ignore-space-change", no_argument, 0, 'b' },
84 { "context", optional_argument, 0, 'C' },
85 { "ifdef", required_argument, 0, 'D' },
86 { "minimal", no_argument, 0, 'd' },
87 { "ed", no_argument, 0, 'e' },
88 { "forward-ed", no_argument, 0, 'f' },
89 { "show-function-line", required_argument, 0, 'F' },
90 { "speed-large-files", no_argument, NULL, 'H' },
91 { "ignore-blank-lines", no_argument, 0, 'B' },
92 { "ignore-matching-lines", required_argument, 0, 'I' },
93 { "ignore-case", no_argument, 0, 'i' },
94 { "paginate", no_argument, NULL, 'l' },
95 { "label", required_argument, 0, 'L' },
96 { "new-file", no_argument, 0, 'N' },
97 { "rcs", no_argument, 0, 'n' },
98 { "unidirectional-new-file", no_argument, 0, 'P' },
99 { "show-c-function", no_argument, 0, 'p' },
100 { "brief", no_argument, 0, 'q' },
101 { "recursive", no_argument, 0, 'r' },
102 { "report-identical-files", no_argument, 0, 's' },
103 { "starting-file", required_argument, 0, 'S' },
104 { "expand-tabs", no_argument, 0, 't' },
105 { "initial-tab", no_argument, 0, 'T' },
106 { "unified", optional_argument, 0, 'U' },
107 { "ignore-all-space", no_argument, 0, 'w' },
108 { "width", required_argument, 0, 'W' },
109 { "exclude", required_argument, 0, 'x' },
110 { "exclude-from", required_argument, 0, 'X' },
111 { "side-by-side", no_argument, NULL, 'y' },
112 { "ignore-file-name-case", no_argument, NULL, OPT_IGN_FN_CASE },
113 { "help", no_argument, NULL, OPT_HELP},
114 { "horizon-lines", required_argument, NULL, OPT_HORIZON_LINES },
115 { "no-dereference", no_argument, NULL, OPT_NO_DEREFERENCE},
116 { "no-ignore-file-name-case", no_argument, NULL, OPT_NO_IGN_FN_CASE },
117 { "normal", no_argument, NULL, OPT_NORMAL },
118 { "strip-trailing-cr", no_argument, NULL, OPT_STRIPCR },
119 { "tabsize", required_argument, NULL, OPT_TSIZE },
120 { "changed-group-format", required_argument, NULL, OPT_CHANGED_GROUP_FORMAT},
121 { "suppress-common-lines", no_argument, NULL, OPT_SUPPRESS_COMMON },
122 { "color", optional_argument, NULL, OPT_COLOR },
123 { "version", no_argument, NULL, OPT_VERSION},
124 { NULL, 0, 0, '\0'}
125 };
126
127 static void checked_regcomp(char const *, regex_t *);
128 static void usage(void) __dead2;
129 static void conflicting_format(void) __dead2;
130 static void push_excludes(char *);
131 static void push_ignore_pats(char *);
132 static void read_excludes_file(char *file);
133 static void set_argstr(char **, char **);
134 static char *splice(char *, char *);
135 static bool do_color(void);
136
137 int
main(int argc,char ** argv)138 main(int argc, char **argv)
139 {
140 const char *errstr;
141 char **oargv;
142 int ch, dflags, lastch, gotstdin, prevoptind, newarg;
143
144 oargv = argv;
145 gotstdin = 0;
146 dflags = 0;
147 lastch = '\0';
148 prevoptind = 1;
149 newarg = 1;
150 diff_context = 3;
151 diff_format = D_UNSET;
152 diff_algorithm = D_DIFFMYERS;
153 diff_algorithm_set = false;
154 #define FORMAT_MISMATCHED(type) \
155 (diff_format != D_UNSET && diff_format != (type))
156 while ((ch = getopt_long(argc, argv, OPTIONS, longopts, NULL)) != -1) {
157 switch (ch) {
158 case '0': case '1': case '2': case '3': case '4':
159 case '5': case '6': case '7': case '8': case '9':
160 if (newarg)
161 usage(); /* disallow -[0-9]+ */
162 else if (lastch == 'c' || lastch == 'u')
163 diff_context = 0;
164 else if (!isdigit(lastch) || diff_context > INT_MAX / 10)
165 usage();
166 diff_context = (diff_context * 10) + (ch - '0');
167 break;
168 case 'A':
169 diff_algorithm = D_DIFFNONE;
170 for (struct algorithm *a = algorithms; a->name;a++) {
171 if(strcasecmp(optarg, a->name) == 0) {
172 diff_algorithm = a->id;
173 diff_algorithm_set = true;
174 break;
175 }
176 }
177
178 if (diff_algorithm == D_DIFFNONE) {
179 warnx("unknown algorithm: %s", optarg);
180 usage();
181 }
182 break;
183 case 'a':
184 dflags |= D_FORCEASCII;
185 break;
186 case 'b':
187 dflags |= D_FOLDBLANKS;
188 break;
189 case 'C':
190 case 'c':
191 if (FORMAT_MISMATCHED(D_CONTEXT))
192 conflicting_format();
193 cflag = true;
194 diff_format = D_CONTEXT;
195 if (optarg != NULL) {
196 diff_context = (int) strtonum(optarg,
197 1, INT_MAX, &errstr);
198 if (errstr != NULL) {
199 warnx("context size is %s: %s",
200 errstr, optarg);
201 usage();
202 }
203 }
204 break;
205 case 'd':
206 dflags |= D_MINIMAL;
207 break;
208 case 'D':
209 if (FORMAT_MISMATCHED(D_IFDEF))
210 conflicting_format();
211 diff_format = D_IFDEF;
212 ifdefname = optarg;
213 break;
214 case 'e':
215 if (FORMAT_MISMATCHED(D_EDIT))
216 conflicting_format();
217 diff_format = D_EDIT;
218 break;
219 case 'f':
220 if (FORMAT_MISMATCHED(D_REVERSE))
221 conflicting_format();
222 diff_format = D_REVERSE;
223 break;
224 case 'H':
225 /* ignore but needed for compatibility with GNU diff */
226 break;
227 case 'h':
228 /* silently ignore for backwards compatibility */
229 break;
230 case 'B':
231 dflags |= D_SKIPBLANKLINES;
232 break;
233 case 'F':
234 if (dflags & D_PROTOTYPE)
235 conflicting_format();
236 dflags |= D_MATCHLAST;
237 most_recent_pat = xstrdup(optarg);
238 break;
239 case 'I':
240 push_ignore_pats(optarg);
241 break;
242 case 'i':
243 dflags |= D_IGNORECASE;
244 break;
245 case 'L':
246 if (label[0] == NULL)
247 label[0] = optarg;
248 else if (label[1] == NULL)
249 label[1] = optarg;
250 else
251 usage();
252 break;
253 case 'l':
254 dflags |= D_PAGINATION;
255 lflag = true;
256 break;
257 case 'N':
258 Nflag = true;
259 break;
260 case 'n':
261 if (FORMAT_MISMATCHED(D_NREVERSE))
262 conflicting_format();
263 diff_format = D_NREVERSE;
264 break;
265 case 'p':
266 if (dflags & D_MATCHLAST)
267 conflicting_format();
268 dflags |= D_PROTOTYPE;
269 break;
270 case 'P':
271 Pflag = true;
272 break;
273 case 'r':
274 rflag = true;
275 break;
276 case 'q':
277 if (FORMAT_MISMATCHED(D_BRIEF))
278 conflicting_format();
279 diff_format = D_BRIEF;
280 break;
281 case 'S':
282 start = optarg;
283 break;
284 case 's':
285 sflag = true;
286 break;
287 case 'T':
288 Tflag = true;
289 break;
290 case 't':
291 dflags |= D_EXPANDTABS;
292 break;
293 case 'U':
294 case 'u':
295 if (FORMAT_MISMATCHED(D_UNIFIED))
296 conflicting_format();
297 diff_format = D_UNIFIED;
298 if (optarg != NULL) {
299 diff_context = (int) strtonum(optarg,
300 0, INT_MAX, &errstr);
301 if (errstr != NULL) {
302 warnx("context size is %s: %s",
303 errstr, optarg);
304 usage();
305 }
306 }
307 break;
308 case 'w':
309 dflags |= D_IGNOREBLANKS;
310 break;
311 case 'W':
312 width = (int) strtonum(optarg, 1, INT_MAX, &errstr);
313 if (errstr != NULL) {
314 warnx("width is %s: %s", errstr, optarg);
315 usage();
316 }
317 break;
318 case 'X':
319 read_excludes_file(optarg);
320 break;
321 case 'x':
322 push_excludes(optarg);
323 break;
324 case 'y':
325 if (FORMAT_MISMATCHED(D_SIDEBYSIDE))
326 conflicting_format();
327 diff_format = D_SIDEBYSIDE;
328 break;
329 case OPT_CHANGED_GROUP_FORMAT:
330 if (FORMAT_MISMATCHED(D_GFORMAT))
331 conflicting_format();
332 diff_format = D_GFORMAT;
333 group_format = optarg;
334 break;
335 case OPT_HELP:
336 help = true;
337 usage();
338 break;
339 case OPT_HORIZON_LINES:
340 break; /* XXX TODO for compatibility with GNU diff3 */
341 case OPT_IGN_FN_CASE:
342 ignore_file_case = true;
343 break;
344 case OPT_NO_IGN_FN_CASE:
345 ignore_file_case = false;
346 break;
347 case OPT_NORMAL:
348 if (FORMAT_MISMATCHED(D_NORMAL))
349 conflicting_format();
350 diff_format = D_NORMAL;
351 break;
352 case OPT_TSIZE:
353 tabsize = (int) strtonum(optarg, 1, INT_MAX, &errstr);
354 if (errstr != NULL) {
355 warnx("tabsize is %s: %s", errstr, optarg);
356 usage();
357 }
358 break;
359 case OPT_STRIPCR:
360 dflags |= D_STRIPCR;
361 break;
362 case OPT_SUPPRESS_COMMON:
363 suppress_common = 1;
364 break;
365 case OPT_COLOR:
366 if (optarg == NULL || strncmp(optarg, "auto", 4) == 0)
367 colorflag = COLORFLAG_AUTO;
368 else if (strncmp(optarg, "always", 6) == 0)
369 colorflag = COLORFLAG_ALWAYS;
370 else if (strncmp(optarg, "never", 5) == 0)
371 colorflag = COLORFLAG_NEVER;
372 else {
373 warnx("unsupported --color value "
374 "(must be always, auto, or never): "
375 "%s", optarg);
376 usage();
377 }
378 break;
379 case OPT_NO_DEREFERENCE:
380 noderef = true;
381 break;
382 case OPT_VERSION:
383 printf("%s\n", diff_version);
384 exit(0);
385 default:
386 usage();
387 break;
388 }
389 lastch = ch;
390 newarg = optind != prevoptind;
391 prevoptind = optind;
392 }
393 if (diff_format == D_UNSET && (dflags & D_PROTOTYPE) != 0)
394 diff_format = D_CONTEXT;
395 if (diff_format == D_UNSET)
396 diff_format = D_NORMAL;
397 argc -= optind;
398 argv += optind;
399
400 if (do_color()) {
401 char *p;
402 const char *env;
403
404 color = true;
405 add_code = "32";
406 del_code = "31";
407 env = getenv("DIFFCOLORS");
408 if (env != NULL && *env != '\0' && (p = strdup(env))) {
409 add_code = p;
410 strsep(&p, ":");
411 if (p != NULL)
412 del_code = p;
413 }
414 }
415
416 #ifdef __OpenBSD__
417 if (pledge("stdio rpath tmppath", NULL) == -1)
418 err(2, "pledge");
419 #endif
420
421 /*
422 * Do sanity checks, fill in stb1 and stb2 and call the appropriate
423 * driver routine. Both drivers use the contents of stb1 and stb2.
424 */
425 if (argc != 2)
426 usage();
427 checked_regcomp(ignore_pats, &ignore_re);
428 checked_regcomp(most_recent_pat, &most_recent_re);
429 if (strcmp(argv[0], "-") == 0) {
430 fstat(STDIN_FILENO, &stb1);
431 gotstdin = 1;
432 } else if (stat(argv[0], &stb1) != 0) {
433 if (!Nflag || errno != ENOENT)
434 err(2, "%s", argv[0]);
435 dflags |= D_EMPTY1;
436 memset(&stb1, 0, sizeof(struct stat));
437 }
438
439 if (strcmp(argv[1], "-") == 0) {
440 fstat(STDIN_FILENO, &stb2);
441 gotstdin = 1;
442 } else if (stat(argv[1], &stb2) != 0) {
443 if (!Nflag || errno != ENOENT)
444 err(2, "%s", argv[1]);
445 dflags |= D_EMPTY2;
446 memset(&stb2, 0, sizeof(stb2));
447 stb2.st_mode = stb1.st_mode;
448 }
449
450 if (dflags & D_EMPTY1 && dflags & D_EMPTY2){
451 warn("%s", argv[0]);
452 warn("%s", argv[1]);
453 exit(2);
454 }
455
456 if (stb1.st_mode == 0)
457 stb1.st_mode = stb2.st_mode;
458
459 if (gotstdin && (S_ISDIR(stb1.st_mode) || S_ISDIR(stb2.st_mode)))
460 errx(2, "can't compare - to a directory");
461 set_argstr(oargv, argv);
462 if (S_ISDIR(stb1.st_mode) && S_ISDIR(stb2.st_mode)) {
463 if (diff_format == D_IFDEF)
464 errx(2, "-D option not supported with directories");
465 diffdir(argv[0], argv[1], dflags);
466 } else {
467 if (S_ISDIR(stb1.st_mode)) {
468 argv[0] = splice(argv[0], argv[1]);
469 if (stat(argv[0], &stb1) == -1)
470 err(2, "%s", argv[0]);
471 }
472 if (S_ISDIR(stb2.st_mode)) {
473 argv[1] = splice(argv[1], argv[0]);
474 if (stat(argv[1], &stb2) == -1)
475 err(2, "%s", argv[1]);
476 }
477 print_status(diffreg(argv[0], argv[1], dflags, 1), argv[0],
478 argv[1], "");
479 }
480 if (fflush(stdout) != 0)
481 err(2, "stdout");
482 exit(status);
483 }
484
485 static void
checked_regcomp(char const * pattern,regex_t * comp)486 checked_regcomp(char const *pattern, regex_t *comp)
487 {
488 char buf[BUFSIZ];
489 int error;
490
491 if (pattern == NULL)
492 return;
493
494 error = regcomp(comp, pattern, REG_NEWLINE | REG_EXTENDED);
495 if (error != 0) {
496 regerror(error, comp, buf, sizeof(buf));
497 if (*pattern != '\0')
498 errx(2, "%s: %s", pattern, buf);
499 else
500 errx(2, "%s", buf);
501 }
502 }
503
504 static void
set_argstr(char ** av,char ** ave)505 set_argstr(char **av, char **ave)
506 {
507 size_t argsize;
508 char **ap;
509
510 argsize = 4 + *ave - *av + 1;
511 diffargs = xmalloc(argsize);
512 strlcpy(diffargs, "diff", argsize);
513 for (ap = av + 1; ap < ave; ap++) {
514 if (strcmp(*ap, "--") != 0) {
515 strlcat(diffargs, " ", argsize);
516 strlcat(diffargs, *ap, argsize);
517 }
518 }
519 }
520
521 /*
522 * Read in an excludes file and push each line.
523 */
524 static void
read_excludes_file(char * file)525 read_excludes_file(char *file)
526 {
527 FILE *fp;
528 char *pattern = NULL;
529 size_t blen = 0;
530 ssize_t len;
531
532 if (strcmp(file, "-") == 0)
533 fp = stdin;
534 else if ((fp = fopen(file, "r")) == NULL)
535 err(2, "%s", file);
536 while ((len = getline(&pattern, &blen, fp)) >= 0) {
537 if ((len > 0) && (pattern[len - 1] == '\n'))
538 pattern[len - 1] = '\0';
539 push_excludes(pattern);
540 /* we allocate a new string per line */
541 pattern = NULL;
542 blen = 0;
543 }
544 free(pattern);
545 if (strcmp(file, "-") != 0)
546 fclose(fp);
547 }
548
549 /*
550 * Push a pattern onto the excludes list.
551 */
552 static void
push_excludes(char * pattern)553 push_excludes(char *pattern)
554 {
555 struct excludes *entry;
556
557 entry = xmalloc(sizeof(*entry));
558 entry->pattern = pattern;
559 entry->next = excludes_list;
560 excludes_list = entry;
561 }
562
563 static void
push_ignore_pats(char * pattern)564 push_ignore_pats(char *pattern)
565 {
566 size_t len;
567
568 if (ignore_pats == NULL)
569 ignore_pats = xstrdup(pattern);
570 else {
571 /* old + "|" + new + NUL */
572 len = strlen(ignore_pats) + strlen(pattern) + 2;
573 ignore_pats = xreallocarray(ignore_pats, 1, len);
574 strlcat(ignore_pats, "|", len);
575 strlcat(ignore_pats, pattern, len);
576 }
577 }
578
579 void
print_status(int val,char * path1,char * path2,const char * entry)580 print_status(int val, char *path1, char *path2, const char *entry)
581 {
582 if (label[0] != NULL)
583 path1 = label[0];
584 if (label[1] != NULL)
585 path2 = label[1];
586
587 switch (val) {
588 case D_BINARY:
589 printf("Binary files %s%s and %s%s differ\n",
590 path1, entry, path2, entry);
591 break;
592 case D_DIFFER:
593 if (diff_format == D_BRIEF)
594 printf("Files %s%s and %s%s differ\n",
595 path1, entry, path2, entry);
596 break;
597 case D_SAME:
598 if (sflag)
599 printf("Files %s%s and %s%s are identical\n",
600 path1, entry, path2, entry);
601 break;
602 case D_MISMATCH1:
603 printf("File %s%s is a directory while file %s%s is a regular file\n",
604 path1, entry, path2, entry);
605 break;
606 case D_MISMATCH2:
607 printf("File %s%s is a regular file while file %s%s is a directory\n",
608 path1, entry, path2, entry);
609 break;
610 case D_SKIPPED1:
611 printf("File %s%s is not a regular file or directory and was skipped\n",
612 path1, entry);
613 break;
614 case D_SKIPPED2:
615 printf("File %s%s is not a regular file or directory and was skipped\n",
616 path2, entry);
617 break;
618 case D_ERROR:
619 break;
620 }
621 }
622
623 static void
usage(void)624 usage(void)
625 {
626 (void)fprintf(help ? stdout : stderr,
627 "usage: diff [-aBbdilpTtw] [-c | -e | -f | -n | -q | -u] [--ignore-case]\n"
628 " [--no-ignore-case] [--normal] [--strip-trailing-cr] [--tabsize]\n"
629 " [-I pattern] [-F pattern] [-L label] file1 file2\n"
630 " diff [-aBbdilpTtw] [-I pattern] [-L label] [--ignore-case]\n"
631 " [--no-ignore-case] [--normal] [--strip-trailing-cr] [--tabsize]\n"
632 " [-F pattern] -C number file1 file2\n"
633 " diff [-aBbdiltw] [-I pattern] [--ignore-case] [--no-ignore-case]\n"
634 " [--normal] [--strip-trailing-cr] [--tabsize] -D string file1 file2\n"
635 " diff [-aBbdilpTtw] [-I pattern] [-L label] [--ignore-case]\n"
636 " [--no-ignore-case] [--normal] [--tabsize] [--strip-trailing-cr]\n"
637 " [-F pattern] -U number file1 file2\n"
638 " diff [-aBbdilNPprsTtw] [-c | -e | -f | -n | -q | -u] [--ignore-case]\n"
639 " [--no-ignore-case] [--normal] [--tabsize] [-I pattern] [-L label]\n"
640 " [-F pattern] [-S name] [-X file] [-x pattern] dir1 dir2\n"
641 " diff [-aBbditwW] [--expand-tabs] [--ignore-all-space]\n"
642 " [--ignore-blank-lines] [--ignore-case] [--minimal]\n"
643 " [--no-ignore-file-name-case] [--strip-trailing-cr]\n"
644 " [--suppress-common-lines] [--tabsize] [--text] [--width]\n"
645 " -y | --side-by-side file1 file2\n"
646 " diff [--help] [--version]\n");
647
648 if (help)
649 exit(0);
650 else
651 exit(2);
652 }
653
654 static void
conflicting_format(void)655 conflicting_format(void)
656 {
657
658 fprintf(stderr, "error: conflicting output format options.\n");
659 usage();
660 }
661
662 static bool
do_color(void)663 do_color(void)
664 {
665 const char *p, *p2;
666
667 switch (colorflag) {
668 case COLORFLAG_AUTO:
669 p = getenv("CLICOLOR");
670 p2 = getenv("COLORTERM");
671 if ((p != NULL && *p != '\0') || (p2 != NULL && *p2 != '\0'))
672 return isatty(STDOUT_FILENO);
673 break;
674 case COLORFLAG_ALWAYS:
675 return (true);
676 case COLORFLAG_NEVER:
677 return (false);
678 }
679
680 return (false);
681 }
682
683 static char *
splice(char * dir,char * path)684 splice(char *dir, char *path)
685 {
686 char *tail, *buf;
687 size_t dirlen;
688
689 dirlen = strlen(dir);
690 while (dirlen != 0 && dir[dirlen - 1] == '/')
691 dirlen--;
692 if ((tail = strrchr(path, '/')) == NULL)
693 tail = path;
694 else
695 tail++;
696 xasprintf(&buf, "%.*s/%s", (int)dirlen, dir, tail);
697 return (buf);
698 }
699