1/*	$OpenBSD: diff.c,v 1.164 2021/10/24 21:24:16 deraadt Exp $	*/
2/*
3 * Copyright (c) 2008 Tobias Stoeckmann <tobias@openbsd.org>
4 * Copyright (c) 2006 Joris Vink <joris@openbsd.org>
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
19#include <sys/stat.h>
20#include <sys/time.h>
21
22#include <errno.h>
23#include <fcntl.h>
24#include <stdlib.h>
25#include <string.h>
26#include <time.h>
27#include <unistd.h>
28
29#include "cvs.h"
30#include "diff.h"
31#include "remote.h"
32
33void	cvs_diff_local(struct cvs_file *);
34
35static int	 dflags = 0;
36static int	 Nflag = 0;
37static int	 force_head = 0;
38static char	*koptstr;
39static char	*rev1 = NULL;
40static char	*rev2 = NULL;
41static time_t	 date1 = -1;
42static time_t	 date2 = -1;
43static char	*dateflag1 = NULL;
44static char	*dateflag2 = NULL;
45
46struct cvs_cmd cvs_cmd_diff = {
47	CVS_OP_DIFF, CVS_USE_WDIR, "diff",
48	{ "di", "dif" },
49	"Show differences between revisions",
50	"[-abcdilNnpRuw] [[-D date] [-r rev] [-D date2 | -r rev2]] "
51	"[-k mode] [file ...]",
52	"abcfC:dD:ik:lNnpr:RuU:w",
53	NULL,
54	cvs_diff
55};
56
57struct cvs_cmd cvs_cmd_rdiff = {
58	CVS_OP_RDIFF, 0, "rdiff",
59	{ "patch", "pa" },
60	"Show differences between revisions",
61	"[-flR] [-c | -u] [-s | -t] [-V ver] -D date | -r rev\n"
62	"[-D date2 | -r rev2] [-k mode] module ...",
63	"cfD:k:lr:RuV:",
64	NULL,
65	cvs_diff
66};
67
68int
69cvs_diff(int argc, char **argv)
70{
71	int ch, flags;
72	char *arg = ".";
73	const char *errstr;
74	struct cvs_recursion cr;
75
76	flags = CR_RECURSE_DIRS;
77	strlcpy(diffargs, cvs_cmdop == CVS_OP_DIFF ? "diff" : "rdiff",
78	    sizeof(diffargs));
79
80	while ((ch = getopt(argc, argv, cvs_cmdop == CVS_OP_DIFF ?
81	    cvs_cmd_diff.cmd_opts : cvs_cmd_rdiff.cmd_opts)) != -1) {
82		switch (ch) {
83		case 'a':
84			strlcat(diffargs, " -a", sizeof(diffargs));
85			dflags |= D_FORCEASCII;
86			break;
87		case 'b':
88			strlcat(diffargs, " -b", sizeof(diffargs));
89			dflags |= D_FOLDBLANKS;
90			break;
91		case 'c':
92			strlcat(diffargs, " -c", sizeof(diffargs));
93			diff_format = D_CONTEXT;
94			break;
95		case 'C':
96			diff_context = strtonum(optarg, 0, INT_MAX, &errstr);
97			if (errstr != NULL)
98				fatal("context lines %s: %s", errstr, optarg);
99			strlcat(diffargs, " -C ", sizeof(diffargs));
100			strlcat(diffargs, optarg, sizeof(diffargs));
101			diff_format = D_CONTEXT;
102			break;
103		case 'd':
104			strlcat(diffargs, " -d", sizeof(diffargs));
105			dflags |= D_MINIMAL;
106			break;
107		case 'D':
108			if (date1 == -1 && rev1 == NULL) {
109				if ((date1 = date_parse(optarg)) == -1)
110					fatal("invalid date: %s", optarg);
111				dateflag1 = optarg;
112			} else if (date2 == -1 && rev2 == NULL) {
113				if ((date2 = date_parse(optarg)) == -1)
114					fatal("invalid date: %s", optarg);
115				dateflag2 = optarg;
116			} else {
117				fatal("no more than 2 revisions/dates can"
118				    " be specified");
119			}
120			break;
121		case 'f':
122			force_head = 1;
123			break;
124		case 'i':
125			strlcat(diffargs, " -i", sizeof(diffargs));
126			dflags |= D_IGNORECASE;
127			break;
128		case 'k':
129			koptstr = optarg;
130			kflag = rcs_kflag_get(koptstr);
131			if (RCS_KWEXP_INVAL(kflag)) {
132				cvs_log(LP_ERR,
133				    "invalid RCS keyword expansion mode");
134				fatal("%s", cvs_cmdop == CVS_OP_DIFF ?
135				    cvs_cmd_diff.cmd_synopsis :
136				    cvs_cmd_rdiff.cmd_synopsis);
137			}
138			break;
139		case 'l':
140			flags &= ~CR_RECURSE_DIRS;
141			break;
142		case 'n':
143			strlcat(diffargs, " -n", sizeof(diffargs));
144			diff_format = D_RCSDIFF;
145			break;
146		case 'N':
147			strlcat(diffargs, " -N", sizeof(diffargs));
148			Nflag = 1;
149			break;
150		case 'p':
151			strlcat(diffargs, " -p", sizeof(diffargs));
152			dflags |= D_PROTOTYPE;
153			break;
154		case 'R':
155			flags |= CR_RECURSE_DIRS;
156			break;
157		case 'r':
158			if (date1 == -1 && rev1 == NULL) {
159				rev1 = optarg;
160			} else if (date2 == -1 && rev2 == NULL) {
161				rev2 = optarg;
162			} else {
163				fatal("no more than 2 revisions/dates can"
164				    " be specified");
165			}
166			break;
167		case 't':
168			strlcat(diffargs, " -t", sizeof(diffargs));
169			dflags |= D_EXPANDTABS;
170			break;
171		case 'u':
172			strlcat(diffargs, " -u", sizeof(diffargs));
173			diff_format = D_UNIFIED;
174			break;
175		case 'U':
176			diff_context = strtonum(optarg, 0, INT_MAX, &errstr);
177			if (errstr != NULL)
178				fatal("context lines %s: %s", errstr, optarg);
179			strlcat(diffargs, " -U ", sizeof(diffargs));
180			strlcat(diffargs, optarg, sizeof(diffargs));
181			diff_format = D_UNIFIED;
182			break;
183		case 'V':
184			fatal("the -V option is obsolete "
185			    "and should not be used");
186		case 'w':
187			strlcat(diffargs, " -w", sizeof(diffargs));
188			dflags |= D_IGNOREBLANKS;
189			break;
190		default:
191			fatal("%s", cvs_cmdop == CVS_OP_DIFF ?
192			    cvs_cmd_diff.cmd_synopsis :
193			    cvs_cmd_rdiff.cmd_synopsis);
194		}
195	}
196
197	argc -= optind;
198	argv += optind;
199
200	cr.enterdir = NULL;
201	cr.leavedir = NULL;
202
203	if (cvs_cmdop == CVS_OP_RDIFF) {
204		if (rev1 == NULL && rev2 == NULL && dateflag1 == NULL &&
205		    dateflag2 == NULL)
206			fatal("must specify at least one revision/date!");
207
208		if (!argc)
209			fatal("%s", cvs_cmd_rdiff.cmd_synopsis);
210
211		if (!diff_format) {
212			strlcat(diffargs, " -c", sizeof(diffargs));
213			diff_format = D_CONTEXT;
214		}
215
216		flags |= CR_REPO;
217	}
218
219	if (cvsroot_is_remote()) {
220		cvs_client_connect_to_server();
221		cr.fileproc = cvs_client_sendfile;
222
223		if (!(flags & CR_RECURSE_DIRS))
224			cvs_client_send_request("Argument -l");
225
226		if (kflag)
227			cvs_client_send_request("Argument -k%s", koptstr);
228
229		switch (diff_format) {
230		case D_CONTEXT:
231			if (cvs_cmdop == CVS_OP_RDIFF)
232				cvs_client_send_request("Argument -c");
233			else {
234				cvs_client_send_request("Argument -C %d",
235				    diff_context);
236			}
237			break;
238		case D_RCSDIFF:
239			cvs_client_send_request("Argument -n");
240			break;
241		case D_UNIFIED:
242			if (cvs_cmdop == CVS_OP_RDIFF || diff_context == 3)
243				cvs_client_send_request("Argument -u");
244			else {
245				cvs_client_send_request("Argument -U %d",
246				    diff_context);
247			}
248			break;
249		default:
250			break;
251		}
252
253		if (Nflag == 1)
254			cvs_client_send_request("Argument -N");
255
256		if (dflags & D_PROTOTYPE)
257			cvs_client_send_request("Argument -p");
258
259		if (rev1 != NULL)
260			cvs_client_send_request("Argument -r%s", rev1);
261		if (rev2 != NULL)
262			cvs_client_send_request("Argument -r%s", rev2);
263
264		if (dateflag1 != NULL)
265			cvs_client_send_request("Argument -D%s", dateflag1);
266		if (dateflag2 != NULL)
267			cvs_client_send_request("Argument -D%s", dateflag2);
268	} else {
269		if (cvs_cmdop == CVS_OP_RDIFF &&
270		    chdir(current_cvsroot->cr_dir) == -1)
271			fatal("cvs_diff: %s", strerror(errno));
272
273		cr.fileproc = cvs_diff_local;
274	}
275
276	cr.flags = flags;
277
278	diff_rev1 = diff_rev2 = NULL;
279
280	if (cvs_cmdop == CVS_OP_DIFF || cvsroot_is_local()) {
281		if (argc > 0)
282			cvs_file_run(argc, argv, &cr);
283		else
284			cvs_file_run(1, &arg, &cr);
285	}
286
287	if (cvsroot_is_remote()) {
288		cvs_client_send_files(argv, argc);
289		cvs_client_senddir(".");
290
291		cvs_client_send_request((cvs_cmdop == CVS_OP_RDIFF) ?
292		    "rdiff" : "diff");
293
294		cvs_client_get_responses();
295	}
296
297	return (0);
298}
299
300void
301cvs_diff_local(struct cvs_file *cf)
302{
303	BUF *b1;
304	int fd1, fd2;
305	struct stat st;
306	struct timeval tv[2], tv2[2];
307	struct tm datetm;
308	char rbuf[CVS_REV_BUFSZ], tbuf[CVS_TIME_BUFSZ], *p1, *p2;
309
310	b1 = NULL;
311	fd1 = fd2 = -1;
312	p1 = p2 = NULL;
313
314	cvs_log(LP_TRACE, "cvs_diff_local(%s)", cf->file_path);
315
316	if (cf->file_type == CVS_DIR) {
317		if (verbosity > 1)
318			cvs_log(LP_ERR, "Diffing inside %s", cf->file_path);
319		return;
320	}
321
322	cvs_file_classify(cf, cvs_directory_tag);
323
324	if (cvs_cmdop == CVS_OP_DIFF) {
325		if (cf->file_ent == NULL) {
326			cvs_log(LP_ERR, "I know nothing about %s",
327			    cf->file_path);
328			return;
329		}
330
331		switch (cf->file_ent->ce_status) {
332		case CVS_ENT_ADDED:
333			if (Nflag == 0) {
334				cvs_log(LP_ERR, "%s is a new entry, no "
335				    "comparison available", cf->file_path);
336				return;
337			}
338			if (!(cf->file_flags & FILE_ON_DISK)) {
339				cvs_log(LP_ERR, "cannot find %s",
340				    cf->file_path);
341				return;
342			}
343			break;
344		case CVS_ENT_REMOVED:
345			if (Nflag == 0) {
346				cvs_log(LP_ERR, "%s was removed, no "
347				    "comparison available", cf->file_path);
348				return;
349			}
350			if (cf->file_rcs == NULL) {
351				cvs_log(LP_ERR, "cannot find RCS file for %s",
352				    cf->file_path);
353				return;
354			}
355			break;
356		default:
357			if (!(cf->file_flags & FILE_ON_DISK)) {
358				cvs_printf("? %s\n", cf->file_path);
359				return;
360			}
361
362			if (cf->file_rcs == NULL) {
363				cvs_log(LP_ERR, "cannot find RCS file for %s",
364				    cf->file_path);
365				return;
366			}
367			break;
368		}
369	}
370
371	if (cf->file_status == FILE_UPTODATE && rev1 == NULL && rev2 == NULL &&
372	    date1 == -1 && date2 == -1)
373		return;
374
375	if (cf->file_rcs != NULL && cf->file_rcs->rf_head == NULL) {
376		cvs_log(LP_ERR, "no head revision in RCS file for %s\n",
377		    cf->file_path);
378		return;
379	}
380
381	if (kflag && cf->file_rcs != NULL)
382		rcs_kwexp_set(cf->file_rcs, kflag);
383
384	if (cf->file_rcs == NULL)
385		diff_rev1 = NULL;
386	else if (rev1 != NULL || date1 != -1) {
387		cvs_specified_date = date1;
388		diff_rev1 = rcs_translate_tag(rev1, cf->file_rcs);
389		if (diff_rev1 == NULL && cvs_cmdop == CVS_OP_DIFF) {
390			if (rev1 != NULL) {
391				cvs_log(LP_ERR, "tag %s not in file %s", rev1,
392				    cf->file_path);
393				goto cleanup;
394			} else if (Nflag) {
395				diff_rev1 = NULL;
396			} else {
397				gmtime_r(&cvs_specified_date, &datetm);
398				strftime(tbuf, sizeof(tbuf),
399				    "%Y.%m.%d.%H.%M.%S", &datetm);
400				cvs_log(LP_ERR, "no revision for date %s in "
401				    "file %s", tbuf, cf->file_path);
402				goto cleanup;
403			}
404		} else if (diff_rev1 == NULL && cvs_cmdop == CVS_OP_RDIFF &&
405		    force_head) {
406			/* -f is not allowed for unknown symbols */
407			if ((diff_rev1 = rcsnum_parse(rev1)) == NULL)
408				fatal("no such tag %s", rev1);
409			free(diff_rev1);
410
411			diff_rev1 = cf->file_rcs->rf_head;
412		}
413		cvs_specified_date = -1;
414	} else if (cvs_cmdop == CVS_OP_DIFF) {
415		if (cf->file_ent->ce_status == CVS_ENT_ADDED)
416			diff_rev1 = NULL;
417		else
418			diff_rev1 = cf->file_ent->ce_rev;
419	}
420
421	if (cf->file_rcs == NULL)
422		diff_rev2 = NULL;
423	else if (rev2 != NULL || date2 != -1) {
424		cvs_specified_date = date2;
425		diff_rev2 = rcs_translate_tag(rev2, cf->file_rcs);
426		if (diff_rev2 == NULL && cvs_cmdop == CVS_OP_DIFF) {
427			if (rev2 != NULL) {
428				cvs_log(LP_ERR, "tag %s not in file %s", rev2,
429				    cf->file_path);
430				goto cleanup;
431			} else if (Nflag) {
432				diff_rev2 = NULL;
433			} else {
434				gmtime_r(&cvs_specified_date, &datetm);
435				strftime(tbuf, sizeof(tbuf),
436				    "%Y.%m.%d.%H.%M.%S", &datetm);
437				cvs_log(LP_ERR, "no revision for date %s in "
438				    "file %s", tbuf, cf->file_path);
439				goto cleanup;
440			}
441		} else if (diff_rev2 == NULL && cvs_cmdop == CVS_OP_RDIFF &&
442		    force_head) {
443			/* -f is not allowed for unknown symbols */
444			if ((diff_rev2 = rcsnum_parse(rev2)) == NULL)
445				fatal("no such tag %s", rev2);
446			free(diff_rev2);
447
448			diff_rev2 = cf->file_rcs->rf_head;
449		}
450		cvs_specified_date = -1;
451	} else if (cvs_cmdop == CVS_OP_RDIFF)
452		diff_rev2 = cf->file_rcs->rf_head;
453	else if (cf->file_ent->ce_status == CVS_ENT_REMOVED)
454		diff_rev2 = NULL;
455
456	if (diff_rev1 != NULL && diff_rev2 != NULL &&
457	    rcsnum_cmp(diff_rev1, diff_rev2, 0) == 0)
458		goto cleanup;
459
460	switch (cvs_cmdop) {
461	case CVS_OP_DIFF:
462		if (cf->file_status == FILE_UPTODATE) {
463			if (diff_rev2 == NULL &&
464			    !rcsnum_cmp(diff_rev1, cf->file_rcsrev, 0))
465				goto cleanup;
466		}
467		break;
468	case CVS_OP_RDIFF:
469		if (diff_rev1 == NULL && diff_rev2 == NULL)
470			goto cleanup;
471		break;
472	}
473
474	cvs_printf("Index: %s\n", cf->file_path);
475	if (cvs_cmdop == CVS_OP_DIFF)
476		cvs_printf("%s\nRCS file: %s\n", RCS_DIFF_DIV,
477		    cf->file_rcs != NULL ? cf->file_rpath : cf->file_path);
478
479	if (diff_rev1 != NULL) {
480		if (cvs_cmdop == CVS_OP_DIFF && diff_rev1 != NULL) {
481			(void)rcsnum_tostr(diff_rev1, rbuf, sizeof(rbuf));
482			cvs_printf("retrieving revision %s\n", rbuf);
483		}
484
485		tv[0].tv_sec = rcs_rev_getdate(cf->file_rcs, diff_rev1);
486		tv[0].tv_usec = 0;
487		tv[1] = tv[0];
488
489		(void)xasprintf(&p1, "%s/diff1.XXXXXXXXXX", cvs_tmpdir);
490		fd1 = rcs_rev_write_stmp(cf->file_rcs, diff_rev1, p1, 0);
491		if (futimes(fd1, tv) == -1)
492			fatal("cvs_diff_local: utimes failed");
493	}
494
495	if (diff_rev2 != NULL) {
496		if (cvs_cmdop == CVS_OP_DIFF && rev2 != NULL) {
497			(void)rcsnum_tostr(diff_rev2, rbuf, sizeof(rbuf));
498			cvs_printf("retrieving revision %s\n", rbuf);
499		}
500
501		tv2[0].tv_sec = rcs_rev_getdate(cf->file_rcs, diff_rev2);
502		tv2[0].tv_usec = 0;
503		tv2[1] = tv2[0];
504
505		(void)xasprintf(&p2, "%s/diff2.XXXXXXXXXX", cvs_tmpdir);
506		fd2 = rcs_rev_write_stmp(cf->file_rcs, diff_rev2, p2, 0);
507		if (futimes(fd2, tv2) == -1)
508			fatal("cvs_diff_local: utimes failed");
509	} else if (cvs_cmdop == CVS_OP_DIFF &&
510	    (cf->file_flags & FILE_ON_DISK) &&
511	    cf->file_ent->ce_status != CVS_ENT_REMOVED) {
512		(void)xasprintf(&p2, "%s/diff2.XXXXXXXXXX", cvs_tmpdir);
513		if (cvs_server_active == 1 && cf->fd == -1) {
514			tv2[0].tv_sec = rcs_rev_getdate(cf->file_rcs,
515			    cf->file_ent->ce_rev);
516			tv2[0].tv_usec = 0;
517			tv2[1] = tv2[0];
518
519			fd2 = rcs_rev_write_stmp(cf->file_rcs,
520			    cf->file_ent->ce_rev, p2, 0);
521			if (futimes(fd2, tv2) == -1)
522				fatal("cvs_diff_local: futimes failed");
523		} else {
524			if (fstat(cf->fd, &st) == -1)
525				fatal("fstat failed %s", strerror(errno));
526			b1 = buf_load_fd(cf->fd);
527
528			tv2[0].tv_sec = st.st_mtime;
529			tv2[0].tv_usec = 0;
530			tv2[1] = tv2[0];
531
532			fd2 = buf_write_stmp(b1, p2, tv2);
533			buf_free(b1);
534		}
535	}
536
537	switch (cvs_cmdop) {
538	case CVS_OP_DIFF:
539		cvs_printf("%s", diffargs);
540
541		if (rev1 != NULL && diff_rev1 != NULL) {
542			(void)rcsnum_tostr(diff_rev1, rbuf, sizeof(rbuf));
543			cvs_printf(" -r%s", rbuf);
544
545			if (rev2 != NULL && diff_rev2 != NULL) {
546				(void)rcsnum_tostr(diff_rev2, rbuf,
547				    sizeof(rbuf));
548				cvs_printf(" -r%s", rbuf);
549			}
550		}
551
552		if (diff_rev2 == NULL)
553			cvs_printf(" %s", cf->file_path);
554		cvs_printf("\n");
555		break;
556	case CVS_OP_RDIFF:
557		cvs_printf("diff ");
558		switch (diff_format) {
559		case D_CONTEXT:
560			cvs_printf("-c ");
561			break;
562		case D_RCSDIFF:
563			cvs_printf("-n ");
564			break;
565		case D_UNIFIED:
566			cvs_printf("-u ");
567			break;
568		default:
569			break;
570		}
571		if (diff_rev1 == NULL) {
572			cvs_printf("%s ", CVS_PATH_DEVNULL);
573		} else {
574			(void)rcsnum_tostr(diff_rev1, rbuf, sizeof(rbuf));
575			cvs_printf("%s:%s ", cf->file_path, rbuf);
576		}
577
578		if (diff_rev2 == NULL) {
579			cvs_printf("%s:removed\n", cf->file_path);
580		} else {
581			(void)rcsnum_tostr(diff_rev2 != NULL ? diff_rev2 :
582			    cf->file_rcs->rf_head, rbuf, sizeof(rbuf));
583			cvs_printf("%s:%s\n", cf->file_path, rbuf);
584		}
585		break;
586	}
587
588	if (fd1 == -1) {
589		if ((fd1 = open(CVS_PATH_DEVNULL, O_RDONLY)) == -1)
590			fatal("cannot open %s", CVS_PATH_DEVNULL);
591	}
592	if (fd2 == -1) {
593		if ((fd2 = open(CVS_PATH_DEVNULL, O_RDONLY)) == -1)
594			fatal("cannot open %s", CVS_PATH_DEVNULL);
595	}
596
597	if (diffreg(p1 != NULL ? cf->file_path : CVS_PATH_DEVNULL,
598	    p2 != NULL ? cf->file_path : CVS_PATH_DEVNULL, fd1, fd2, NULL,
599	    dflags) == D_ERROR)
600		fatal("cvs_diff_local: failed to get RCS patch");
601
602	close(fd1);
603	close(fd2);
604
605	worklist_run(&temp_files, worklist_unlink);
606
607	free(p1);
608	free(p2);
609
610cleanup:
611	if (diff_rev1 != NULL &&
612	    (cf->file_rcs == NULL || diff_rev1 != cf->file_rcs->rf_head) &&
613	    (cf->file_ent == NULL || diff_rev1 != cf->file_ent->ce_rev))
614		free(diff_rev1);
615	diff_rev1 = NULL;
616
617	if (diff_rev2 != NULL &&
618	    (cf->file_rcs == NULL || diff_rev2 != cf->file_rcs->rf_head))
619		free(diff_rev2);
620	diff_rev2 = NULL;
621}
622