rcsutil.c revision 1.47
1104834Sobrien/*	$OpenBSD: rcsutil.c,v 1.47 2020/10/14 20:07:19 naddy Exp $	*/
289857Sobrien/*
389857Sobrien * Copyright (c) 2005, 2006 Joris Vink <joris@openbsd.org>
489857Sobrien * Copyright (c) 2006 Xavier Santolaria <xsa@openbsd.org>
589857Sobrien * Copyright (c) 2006 Niall O'Higgins <niallo@openbsd.org>
689857Sobrien * Copyright (c) 2006 Ray Lai <ray@openbsd.org>
789857Sobrien * All rights reserved.
889857Sobrien *
989857Sobrien * Redistribution and use in source and binary forms, with or without
1089857Sobrien * modification, are permitted provided that the following conditions
1189857Sobrien * are met:
1289857Sobrien *
1389857Sobrien * 1. Redistributions of source code must retain the above copyright
14 *    notice, this list of conditions and the following disclaimer.
15 * 2. The name of the author may not be used to endorse or promote products
16 *    derived from this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
19 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
20 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
21 * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22 * EXEMPLARY, OR CONSEQUENTIAL  DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
24 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
25 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
26 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
27 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29
30#include <sys/stat.h>
31#include <sys/time.h>
32
33#include <ctype.h>
34#include <err.h>
35#include <fcntl.h>
36#include <stdio.h>
37#include <stdlib.h>
38#include <string.h>
39#include <unistd.h>
40
41#include "rcsprog.h"
42
43/*
44 * rcs_get_mtime()
45 *
46 * Get <filename> last modified time.
47 * Returns last modified time on success, or -1 on failure.
48 */
49time_t
50rcs_get_mtime(RCSFILE *file)
51{
52	struct stat st;
53	time_t mtime;
54
55	if (file->rf_file == NULL)
56		return (-1);
57
58	if (fstat(fileno(file->rf_file), &st) == -1) {
59		warn("%s", file->rf_path);
60		return (-1);
61	}
62
63	mtime = st.st_mtimespec.tv_sec;
64
65	return (mtime);
66}
67
68/*
69 * rcs_set_mtime()
70 *
71 * Set <filename> last modified time to <mtime> if it's not set to -1.
72 */
73void
74rcs_set_mtime(RCSFILE *file, time_t mtime)
75{
76	static struct timeval tv[2];
77
78	if (file->rf_file == NULL || mtime == -1)
79		return;
80
81	tv[0].tv_sec = mtime;
82	tv[1].tv_sec = tv[0].tv_sec;
83
84	if (futimes(fileno(file->rf_file), tv) == -1)
85		err(1, "utimes");
86}
87
88int
89rcs_getopt(int argc, char **argv, const char *optstr)
90{
91	char *a;
92	const char *c;
93	static int i = 1;
94	int opt, hasargument, ret;
95
96	hasargument = 0;
97	rcs_optarg = NULL;
98
99	if (i >= argc)
100		return (-1);
101
102	a = argv[i++];
103	if (*a++ != '-')
104		return (-1);
105
106	ret = 0;
107	opt = *a;
108	for (c = optstr; *c != '\0'; c++) {
109		if (*c == opt) {
110			a++;
111			ret = opt;
112
113			if (*(c + 1) == ':') {
114				if (*(c + 2) == ':') {
115					if (*a != '\0')
116						hasargument = 1;
117				} else {
118					if (*a != '\0') {
119						hasargument = 1;
120					} else {
121						ret = 1;
122						break;
123					}
124				}
125			}
126
127			if (hasargument == 1)
128				rcs_optarg = a;
129
130			if (ret == opt)
131				rcs_optind++;
132			break;
133		}
134	}
135
136	if (ret == 0)
137		warnx("unknown option -%c", opt);
138	else if (ret == 1)
139		warnx("missing argument for option -%c", opt);
140
141	return (ret);
142}
143
144/*
145 * rcs_choosefile()
146 *
147 * Given a relative filename, decide where the corresponding RCS file
148 * should be.  Tries each extension until a file is found.  If no file
149 * was found, returns a path with the first extension.
150 *
151 * Opens and returns file descriptor to RCS file.
152 */
153int
154rcs_choosefile(const char *filename, char *out, size_t len)
155{
156	int fd;
157	struct stat sb;
158	char *p, *ext, name[PATH_MAX], *next, *ptr, rcsdir[PATH_MAX],
159	    *suffixes, rcspath[PATH_MAX];
160
161	/*
162	 * If `filename' contains a directory, `rcspath' contains that
163	 * directory, including a trailing slash.  Otherwise `rcspath'
164	 * contains an empty string.
165	 */
166	if (strlcpy(rcspath, filename, sizeof(rcspath)) >= sizeof(rcspath))
167		errx(1, "rcs_choosefile: truncation");
168
169	/* If `/' is found, end string after `/'. */
170	if ((ptr = strrchr(rcspath, '/')) != NULL)
171		*(++ptr) = '\0';
172	else
173		rcspath[0] = '\0';
174
175	/* Append RCS/ to `rcspath' if it exists. */
176	if (strlcpy(rcsdir, rcspath, sizeof(rcsdir)) >= sizeof(rcsdir) ||
177	    strlcat(rcsdir, RCSDIR, sizeof(rcsdir)) >= sizeof(rcsdir))
178		errx(1, "rcs_choosefile: truncation");
179
180	if (stat(rcsdir, &sb) == 0 && S_ISDIR(sb.st_mode))
181		if (strlcpy(rcspath, rcsdir, sizeof(rcspath))
182		    >= sizeof(rcspath) ||
183		    strlcat(rcspath, "/", sizeof(rcspath)) >= sizeof(rcspath))
184			errx(1, "rcs_choosefile: truncation");
185
186	/* Name of file without path. */
187	if ((ptr = strrchr(filename, '/')) == NULL) {
188		if (strlcpy(name, filename, sizeof(name)) >= sizeof(name))
189			errx(1, "rcs_choosefile: truncation");
190	} else {
191		/* Skip `/'. */
192		if (strlcpy(name, ptr + 1, sizeof(name)) >= sizeof(name))
193			errx(1, "rcs_choosefile: truncation");
194	}
195
196	/* Name of RCS file without an extension. */
197	if (strlcat(rcspath, name, sizeof(rcspath)) >= sizeof(rcspath))
198		errx(1, "rcs_choosefile: truncation");
199
200	/*
201	 * If only the empty suffix was given, use existing rcspath.
202	 * This ensures that there is at least one suffix for strsep().
203	 */
204	if (strcmp(rcs_suffixes, "") == 0) {
205		if (strlcpy(out, rcspath, len) >= len)
206			errx(1, "rcs_choosefile: truncation");
207		fd = open(rcspath, O_RDONLY);
208		return (fd);
209	}
210
211	/*
212	 * Cycle through slash-separated `rcs_suffixes', appending each
213	 * extension to `rcspath' and testing if the file exists.  If it
214	 * does, return that string.  Otherwise return path with first
215	 * extension.
216	 */
217	suffixes = xstrdup(rcs_suffixes);
218	for (next = suffixes; (ext = strsep(&next, "/")) != NULL;) {
219		char fpath[PATH_MAX];
220
221		if ((p = strrchr(rcspath, ',')) != NULL) {
222			if (!strcmp(p, ext)) {
223				if ((fd = open(rcspath, O_RDONLY)) == -1)
224					continue;
225
226				if (fstat(fd, &sb) == -1)
227					err(1, "%s", rcspath);
228
229				if (strlcpy(out, rcspath, len) >= len)
230					errx(1, "rcs_choosefile: truncation");
231
232				free(suffixes);
233				return (fd);
234			}
235
236			continue;
237		}
238
239		/* Construct RCS file path. */
240		if (strlcpy(fpath, rcspath, sizeof(fpath)) >= sizeof(fpath) ||
241		    strlcat(fpath, ext, sizeof(fpath)) >= sizeof(fpath))
242			errx(1, "rcs_choosefile: truncation");
243
244		/* Don't use `filename' as RCS file. */
245		if (strcmp(fpath, filename) == 0)
246			continue;
247
248		if ((fd = open(fpath, O_RDONLY)) == -1)
249			continue;
250
251		if (fstat(fd, &sb) == -1)
252			err(1, "%s", fpath);
253
254		if (strlcpy(out, fpath, len) >= len)
255			errx(1, "rcs_choosefile: truncation");
256
257		free(suffixes);
258		return (fd);
259	}
260
261	/*
262	 * `suffixes' should now be NUL separated, so the first
263	 * extension can be read just by reading `suffixes'.
264	 */
265	if (strlcat(rcspath, suffixes, sizeof(rcspath)) >= sizeof(rcspath))
266		errx(1, "rcs_choosefile: truncation");
267
268	free(suffixes);
269
270	if (strlcpy(out, rcspath, len) >= len)
271		errx(1, "rcs_choosefile: truncation");
272
273	fd = open(rcspath, O_RDONLY);
274
275	return (fd);
276}
277
278/*
279 * Set <str> to <new_str>.  Print warning if <str> is redefined.
280 */
281void
282rcs_setrevstr(char **str, char *new_str)
283{
284	if (new_str == NULL)
285		return;
286	if (*str != NULL)
287		warnx("redefinition of revision number");
288	*str = new_str;
289}
290
291/*
292 * Set <str1> or <str2> to <new_str>, depending on which is not set.
293 * If both are set, error out.
294 */
295void
296rcs_setrevstr2(char **str1, char **str2, char *new_str)
297{
298	if (new_str == NULL)
299		return;
300	if (*str1 == NULL)
301		*str1 = new_str;
302	else if (*str2 == NULL)
303		*str2 = new_str;
304	else
305		errx(1, "too many revision numbers");
306}
307
308/*
309 * Get revision from file.  The revision can be specified as a symbol or
310 * a revision number.
311 */
312RCSNUM *
313rcs_getrevnum(const char *rev_str, RCSFILE *file)
314{
315	RCSNUM *rev;
316
317	/* Search for symbol. */
318	rev = rcs_sym_getrev(file, rev_str);
319
320	/* Search for revision number. */
321	if (rev == NULL)
322		rev = rcsnum_parse(rev_str);
323
324	return (rev);
325}
326
327/*
328 * Prompt for and store user's input in an allocated string.
329 *
330 * Returns the string's pointer.
331 */
332char *
333rcs_prompt(const char *prompt, int flags)
334{
335	BUF *bp;
336	size_t len;
337	char *buf;
338
339	if (!(flags & INTERACTIVE) && isatty(STDIN_FILENO))
340		flags |= INTERACTIVE;
341
342	bp = buf_alloc(0);
343	if (flags & INTERACTIVE)
344		(void)fprintf(stderr, "%s", prompt);
345	if (flags & INTERACTIVE)
346		(void)fprintf(stderr, ">> ");
347	clearerr(stdin);
348	while ((buf = fgetln(stdin, &len)) != NULL) {
349		/* The last line may not be EOL terminated. */
350		if (buf[0] == '.' && (len == 1 || buf[1] == '\n'))
351			break;
352		else
353			buf_append(bp, buf, len);
354
355		if (flags & INTERACTIVE)
356			(void)fprintf(stderr, ">> ");
357	}
358	buf_putc(bp, '\0');
359
360	return (buf_release(bp));
361}
362
363u_int
364rcs_rev_select(RCSFILE *file, const char *range)
365{
366	int i;
367	u_int nrev;
368	const char *ep;
369	char *lstr, *rstr;
370	struct rcs_delta *rdp;
371	struct rcs_argvector *revargv, *revrange;
372	RCSNUM lnum, rnum;
373
374	nrev = 0;
375	(void)memset(&lnum, 0, sizeof(lnum));
376	(void)memset(&rnum, 0, sizeof(rnum));
377
378	if (range == NULL) {
379		TAILQ_FOREACH(rdp, &file->rf_delta, rd_list)
380			if (rcsnum_cmp(rdp->rd_num, file->rf_head, 0) == 0) {
381				rdp->rd_flags |= RCS_RD_SELECT;
382				return (1);
383			}
384		return (0);
385	}
386
387	revargv = rcs_strsplit(range, ",");
388	for (i = 0; revargv->argv[i] != NULL; i++) {
389		revrange = rcs_strsplit(revargv->argv[i], ":");
390		if (revrange->argv[0] == NULL)
391			/* should not happen */
392			errx(1, "invalid revision range: %s", revargv->argv[i]);
393		else if (revrange->argv[1] == NULL)
394			lstr = rstr = revrange->argv[0];
395		else {
396			if (revrange->argv[2] != NULL)
397				errx(1, "invalid revision range: %s",
398				    revargv->argv[i]);
399			lstr = revrange->argv[0];
400			rstr = revrange->argv[1];
401			if (strcmp(lstr, "") == 0)
402				lstr = NULL;
403			if (strcmp(rstr, "") == 0)
404				rstr = NULL;
405		}
406
407		if (lstr == NULL)
408			lstr = RCS_HEAD_INIT;
409		if (rcsnum_aton(lstr, &ep, &lnum) == 0 || (*ep != '\0'))
410			errx(1, "invalid revision: %s", lstr);
411
412		if (rstr != NULL) {
413			if (rcsnum_aton(rstr, &ep, &rnum) == 0 || (*ep != '\0'))
414				errx(1, "invalid revision: %s", rstr);
415		} else
416			rcsnum_cpy(file->rf_head, &rnum, 0);
417
418		rcs_argv_destroy(revrange);
419
420		TAILQ_FOREACH(rdp, &file->rf_delta, rd_list)
421			if (rcsnum_cmp(rdp->rd_num, &lnum, 0) <= 0 &&
422			    rcsnum_cmp(rdp->rd_num, &rnum, 0) >= 0 &&
423			    !(rdp->rd_flags & RCS_RD_SELECT)) {
424				rdp->rd_flags |= RCS_RD_SELECT;
425				nrev++;
426			}
427	}
428	rcs_argv_destroy(revargv);
429
430	free(lnum.rn_id);
431	free(rnum.rn_id);
432
433	return (nrev);
434}
435
436/*
437 * Load description from <in> to <file>.
438 * If <in> starts with a `-', <in> is taken as the description.
439 * Otherwise <in> is the name of the file containing the description.
440 * If <in> is NULL, the description is read from stdin.
441 * Returns 0 on success, -1 on failure, setting errno.
442 */
443int
444rcs_set_description(RCSFILE *file, const char *in, int flags)
445{
446	BUF *bp;
447	char *content;
448	const char *prompt =
449	    "enter description, terminated with single '.' or end of file:\n"
450	    "NOTE: This is NOT the log message!\n";
451
452	/* Description is in file <in>. */
453	if (in != NULL && *in != '-') {
454		if ((bp = buf_load(in)) == NULL)
455			return (-1);
456		buf_putc(bp, '\0');
457		content = buf_release(bp);
458	/* Description is in <in>. */
459	} else if (in != NULL)
460		/* Skip leading `-'. */
461		content = xstrdup(in + 1);
462	/* Get description from stdin. */
463	else
464		content = rcs_prompt(prompt, flags);
465
466	rcs_desc_set(file, content);
467	free(content);
468	return (0);
469}
470
471/*
472 * Split the contents of a file into a list of lines.
473 */
474struct rcs_lines *
475rcs_splitlines(u_char *data, size_t len)
476{
477	u_char *c, *p;
478	struct rcs_lines *lines;
479	struct rcs_line *lp;
480	size_t i, tlen;
481
482	lines = xcalloc(1, sizeof(*lines));
483	TAILQ_INIT(&(lines->l_lines));
484
485	lp = xcalloc(1, sizeof(*lp));
486	TAILQ_INSERT_TAIL(&(lines->l_lines), lp, l_list);
487
488
489	p = c = data;
490	for (i = 0; i < len; i++) {
491		if (*p == '\n' || (i == len - 1)) {
492			tlen = p - c + 1;
493			lp = xmalloc(sizeof(*lp));
494			lp->l_line = c;
495			lp->l_len = tlen;
496			lp->l_lineno = ++(lines->l_nblines);
497			TAILQ_INSERT_TAIL(&(lines->l_lines), lp, l_list);
498			c = p + 1;
499		}
500		p++;
501	}
502
503	return (lines);
504}
505
506void
507rcs_freelines(struct rcs_lines *lines)
508{
509	struct rcs_line *lp;
510
511	while ((lp = TAILQ_FIRST(&(lines->l_lines))) != NULL) {
512		TAILQ_REMOVE(&(lines->l_lines), lp, l_list);
513		free(lp);
514	}
515
516	free(lines);
517}
518
519BUF *
520rcs_patchfile(u_char *data, size_t dlen, u_char *patch, size_t plen,
521    int (*p)(struct rcs_lines *, struct rcs_lines *))
522{
523	struct rcs_lines *dlines, *plines;
524	struct rcs_line *lp;
525	BUF *res;
526
527	dlines = rcs_splitlines(data, dlen);
528	plines = rcs_splitlines(patch, plen);
529
530	if (p(dlines, plines) < 0) {
531		rcs_freelines(dlines);
532		rcs_freelines(plines);
533		return (NULL);
534	}
535
536	res = buf_alloc(1024);
537	TAILQ_FOREACH(lp, &dlines->l_lines, l_list) {
538		if (lp->l_line == NULL)
539			continue;
540		buf_append(res, lp->l_line, lp->l_len);
541	}
542
543	rcs_freelines(dlines);
544	rcs_freelines(plines);
545	return (res);
546}
547
548/*
549 * rcs_yesno()
550 *
551 * Read a char from standard input, returns defc if the
552 * user enters an equivalent to defc, else whatever char
553 * was entered.  Converts input to lower case.
554 */
555int
556rcs_yesno(int defc)
557{
558	int c, ret;
559
560	fflush(stderr);
561	fflush(stdout);
562
563	clearerr(stdin);
564	if (isalpha(c = getchar()))
565		c = tolower(c);
566	if (c == defc || c == '\n' || (c == EOF && feof(stdin)))
567		ret = defc;
568	else
569		ret = c;
570
571	while (c != EOF && c != '\n')
572		c = getchar();
573
574	return (ret);
575}
576
577/*
578 * rcs_strsplit()
579 *
580 * Split a string <str> of <sep>-separated values and allocate
581 * an argument vector for the values found.
582 */
583struct rcs_argvector *
584rcs_strsplit(const char *str, const char *sep)
585{
586	struct rcs_argvector *av;
587	size_t i = 0;
588	char *cp, *p;
589
590	cp = xstrdup(str);
591	av = xmalloc(sizeof(*av));
592	av->str = cp;
593	av->argv = xmalloc(sizeof(*(av->argv)));
594
595	while ((p = strsep(&cp, sep)) != NULL) {
596		av->argv[i++] = p;
597		av->argv = xreallocarray(av->argv,
598		    i + 1, sizeof(*(av->argv)));
599	}
600	av->argv[i] = NULL;
601
602	return (av);
603}
604
605/*
606 * rcs_argv_destroy()
607 *
608 * Free an argument vector previously allocated by rcs_strsplit().
609 */
610void
611rcs_argv_destroy(struct rcs_argvector *av)
612{
613	free(av->str);
614	free(av->argv);
615	free(av);
616}
617
618/*
619 * Strip suffix from filename.
620 */
621void
622rcs_strip_suffix(char *filename)
623{
624	char *p, *suffixes, *next, *ext;
625
626	if ((p = strrchr(filename, ',')) != NULL) {
627		suffixes = xstrdup(rcs_suffixes);
628		for (next = suffixes; (ext = strsep(&next, "/")) != NULL;) {
629			if (!strcmp(p, ext)) {
630				*p = '\0';
631				break;
632			}
633		}
634		free(suffixes);
635	}
636}
637