1/*	$OpenBSD: trigger.c,v 1.23 2015/11/05 09:48:21 nicm Exp $	*/
2/*
3 * Copyright (c) 2008 Tobias Stoeckmann <tobias@openbsd.org>
4 * Copyright (c) 2008 Jonathan Armani <dbd@asystant.net>
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/queue.h>
20#include <sys/types.h>
21
22#include <ctype.h>
23#include <errno.h>
24#include <libgen.h>
25#include <pwd.h>
26#include <regex.h>
27#include <stdio.h>
28#include <stdlib.h>
29#include <string.h>
30#include <unistd.h>
31
32#include "config.h"
33#include "cvs.h"
34
35static int	 expand_args(BUF *, struct file_info_list *, const char *,
36    const char *, char *);
37static int	 expand_var(BUF *, const char *);
38static char	*parse_cmd(int, char *, const char *, struct file_info_list *);
39
40static int
41expand_args(BUF *buf, struct file_info_list *file_info, const char *repo,
42    const char *allowed_args, char *format)
43{
44	int oldstyle, quote;
45	struct file_info *fi = NULL;
46	char *p, valbuf[2] = { '\0', '\0' };
47	const char *val;
48
49	if (file_info != NULL && !TAILQ_EMPTY(file_info))
50		fi = TAILQ_FIRST(file_info);
51
52	quote = oldstyle = 0;
53
54	/* Why does GNU cvs print something if it encounters %{}? */
55	if (*format == '\0')
56		oldstyle = 1;
57
58	for (p = format; *p != '\0'; p++) {
59		if (*p != '%' && strchr(allowed_args, *p) == NULL)
60			return 1;
61
62		switch (*p) {
63		case 's':
64		case 'V':
65		case 'v':
66			quote = 1;
67			oldstyle = 1;
68			break;
69		default:
70			break;
71		}
72	}
73	if (quote)
74		buf_putc(buf, '"');
75	if (oldstyle) {
76		buf_puts(buf, repo);
77		buf_putc(buf, ' ');
78	}
79
80	if (*format == '\0')
81		return 0;
82
83	/*
84	 * check like this, add only uses loginfo for directories anyway
85	 */
86	if (cvs_cmdop == CVS_OP_ADD) {
87		buf_puts(buf, "- New directory");
88		if (quote)
89			buf_putc(buf, '"');
90		return (0);
91	}
92
93	if (cvs_cmdop == CVS_OP_IMPORT) {
94		buf_puts(buf, "- Imported sources");
95		if (quote)
96			buf_putc(buf, '"');
97		return (0);
98	}
99
100	for (;;) {
101		for (p = format; *p != '\0';) {
102			val = NULL;
103
104			switch (*p) {
105			case '%':
106				val = "%";
107				break;
108			case 'b':
109				if (fi != NULL) {
110					valbuf[0] = fi->tag_type;
111					val = valbuf;
112				}
113				break;
114			case 'o':
115				if (fi != NULL)
116					val = fi->tag_op;
117				break;
118			case 'p':
119				val = current_cvsroot->cr_dir;
120				break;
121			case 'r':
122				val = repo;
123				break;
124			case 'l':
125			case 'S':
126			case 's':
127				if (fi != NULL)
128					val = fi->file_path;
129				break;
130			case 't':
131				if (fi != NULL)
132					val = fi->tag_new;
133				break;
134			case 'V':
135				if (fi != NULL) {
136					if (fi->crevstr != NULL &&
137					    !strcmp(fi->crevstr,
138					    "Non-existent"))
139						val = "NONE";
140					else
141						val = fi->crevstr;
142				}
143				break;
144			case 'v':
145				if (fi != NULL) {
146					if (fi->nrevstr != NULL &&
147					    !strcmp(fi->nrevstr, "Removed"))
148						val = "NONE";
149					else
150						val = fi->nrevstr;
151				}
152				break;
153			default:
154				return 1;
155			}
156
157			if (val != NULL)
158				buf_puts(buf, val);
159
160			if (*(++p) != '\0')
161				buf_putc(buf, ',');
162		}
163
164		if (fi != NULL)
165			fi = TAILQ_NEXT(fi, flist);
166		if (fi == NULL)
167			break;
168
169		if (strlen(format) == 1 && (*format == '%' || *format == 'o' ||
170		    *format == 'p' || *format == 'r' || *format == 't'))
171			break;
172
173		buf_putc(buf, ' ');
174	}
175
176	if (quote)
177		buf_putc(buf, '"');
178
179	return 0;
180}
181
182static int
183expand_var(BUF *buf, const char *var)
184{
185	struct passwd *pw;
186	const char *val;
187
188	if (*var == '=') {
189		if ((val = cvs_var_get(++var)) == NULL) {
190			cvs_log(LP_ERR, "no such user variable ${=%s}", var);
191			return (1);
192		}
193		buf_puts(buf, val);
194	} else {
195		if (strcmp(var, "CVSEDITOR") == 0 ||
196		    strcmp(var, "EDITOR") == 0 ||
197		    strcmp(var, "VISUAL") == 0)
198			buf_puts(buf, cvs_editor);
199		else if (strcmp(var, "CVSROOT") == 0)
200			buf_puts(buf, current_cvsroot->cr_dir);
201		else if (strcmp(var, "USER") == 0) {
202			pw = getpwuid(geteuid());
203			if (pw == NULL) {
204				cvs_log(LP_ERR, "unable to retrieve "
205				    "caller ID");
206				return (1);
207			}
208			buf_puts(buf, pw->pw_name);
209		} else if (strcmp(var, "RCSBIN") == 0) {
210			cvs_log(LP_ERR, "RCSBIN internal variable is no "
211			    "longer supported");
212			return (1);
213		} else {
214			cvs_log(LP_ERR, "no such internal variable $%s", var);
215			return (1);
216		}
217	}
218
219	return (0);
220}
221
222static char *
223parse_cmd(int type, char *cmd, const char *repo,
224    struct file_info_list *file_info)
225{
226	int expanded = 0;
227	char argbuf[2] = { '\0', '\0' };
228	char *allowed_args, *default_args, *args, *file, *p, *q = NULL;
229	size_t pos;
230	BUF *buf;
231
232	switch (type) {
233	case CVS_TRIGGER_COMMITINFO:
234		allowed_args = "prsS{}";
235		default_args = " %p/%r %S";
236		file = CVS_PATH_COMMITINFO;
237		break;
238	case CVS_TRIGGER_LOGINFO:
239		allowed_args = "prsSvVt{}";
240		default_args = NULL;
241		file = CVS_PATH_LOGINFO;
242		break;
243	case CVS_TRIGGER_VERIFYMSG:
244		allowed_args = "l";
245		default_args = " %l";
246		file = CVS_PATH_VERIFYMSG;
247		break;
248	case CVS_TRIGGER_TAGINFO:
249		allowed_args = "btoprsSvV{}";
250		default_args = " %t %o %p/%r %{sv}";
251		file = CVS_PATH_TAGINFO;
252		break;
253	default:
254		return (NULL);
255	}
256
257	/* before doing any stuff, check if the command starts with % */
258	for (p = cmd;
259	     *p != '%' && !isspace((unsigned char)*p) && *p != '\0'; p++)
260		;
261	if (*p == '%')
262		return (NULL);
263
264	buf = buf_alloc(1024);
265
266	p = cmd;
267again:
268	for (; *p != '\0'; p++) {
269		if ((pos = strcspn(p, "$%")) != 0) {
270			buf_append(buf, p, pos);
271			p += pos;
272		}
273
274		q = NULL;
275		if (*p == '\0')
276			break;
277		if (*p++ == '$') {
278			if (*p == '{') {
279				pos = strcspn(++p, "}");
280				if (p[pos] == '\0')
281					goto bad;
282			} else {
283				for (pos = 0; isalpha(p[pos]); pos++)
284					;
285				if (pos == 0) {
286					cvs_log(LP_ERR,
287					    "unrecognized variable syntax");
288					goto bad;
289				}
290			}
291			q = xmalloc(pos + 1);
292			memcpy(q, p, pos);
293			q[pos] = '\0';
294			if (expand_var(buf, q))
295				goto bad;
296			p += pos - (*(p - 1) == '{' ? 0 : 1);
297		} else {
298			switch (*p) {
299			case '\0':
300				goto bad;
301			case '{':
302				if (strchr(allowed_args, '{') == NULL)
303					goto bad;
304				pos = strcspn(++p, "}");
305				if (p[pos] == '\0')
306					goto bad;
307				q = xmalloc(pos + 1);
308				memcpy(q, p, pos);
309				q[pos] = '\0';
310				args = q;
311				p += pos;
312				break;
313			default:
314				argbuf[0] = *p;
315				args = argbuf;
316				break;
317			}
318
319			if (expand_args(buf, file_info, repo, allowed_args,
320			    args))
321				goto bad;
322			expanded = 1;
323		}
324
325		free(q);
326	}
327
328	if (!expanded && default_args != NULL) {
329		p = default_args;
330		expanded = 1;
331		goto again;
332	}
333
334	buf_putc(buf, '\0');
335	return (buf_release(buf));
336
337bad:
338	free(q);
339	cvs_log(LP_NOTICE, "%s contains malformed command '%s'", file, cmd);
340	buf_free(buf);
341	return (NULL);
342}
343
344int
345cvs_trigger_handle(int type, char *repo, char *in, struct trigger_list *list,
346    struct file_info_list *files)
347{
348	int r;
349	char *cmd;
350	struct trigger_line *line;
351
352	TAILQ_FOREACH(line, list, flist) {
353		if ((cmd = parse_cmd(type, line->line, repo, files)) == NULL)
354			return (1);
355		switch(type) {
356		case CVS_TRIGGER_COMMITINFO:
357		case CVS_TRIGGER_TAGINFO:
358		case CVS_TRIGGER_VERIFYMSG:
359			if ((r = cvs_exec(cmd, NULL, 1)) != 0) {
360				free(cmd);
361				return (r);
362			}
363			break;
364		default:
365			(void)cvs_exec(cmd, in, 1);
366			break;
367		}
368		free(cmd);
369	}
370
371	return (0);
372}
373
374struct trigger_list *
375cvs_trigger_getlines(char * file, char * repo)
376{
377	FILE *fp;
378	int allow_all, lineno, match = 0;
379	size_t len;
380	regex_t preg;
381	struct trigger_list *list;
382	struct trigger_line *tline;
383	char fpath[PATH_MAX];
384	char *currentline, *defaultline = NULL, *nline, *p, *q, *regex;
385
386	if (strcmp(file, CVS_PATH_EDITINFO) == 0 ||
387	    strcmp(file, CVS_PATH_VERIFYMSG) == 0)
388		allow_all = 0;
389	else
390		allow_all = 1;
391
392	(void)xsnprintf(fpath, PATH_MAX, "%s/%s", current_cvsroot->cr_dir,
393	    file);
394
395	if ((fp = fopen(fpath, "r")) == NULL) {
396		if (errno != ENOENT)
397			cvs_log(LP_ERRNO, "cvs_trigger_getlines: %s", file);
398		return (NULL);
399	}
400
401	list = xmalloc(sizeof(*list));
402	TAILQ_INIT(list);
403
404	lineno = 0;
405	nline = NULL;
406	while ((currentline = fgetln(fp, &len)) != NULL) {
407		if (currentline[len - 1] == '\n') {
408			currentline[len - 1] = '\0';
409		} else {
410			nline = xmalloc(len + 1);
411			memcpy(nline, currentline, len);
412			nline[len] = '\0';
413			currentline = nline;
414		}
415
416		lineno++;
417
418		for (p = currentline; isspace((unsigned char)*p); p++)
419			;
420
421		if (*p == '\0' || *p == '#')
422			continue;
423
424		for (q = p; !isspace((unsigned char)*q) && *q != '\0'; q++)
425			;
426
427		if (*q == '\0')
428			goto bad;
429
430		*q++ = '\0';
431		regex = p;
432
433		for (; isspace((unsigned char)*q); q++)
434			;
435
436		if (*q == '\0')
437			goto bad;
438
439		if (strcmp(regex, "ALL") == 0 && allow_all) {
440			tline = xmalloc(sizeof(*tline));
441			tline->line = xstrdup(q);
442			TAILQ_INSERT_TAIL(list, tline, flist);
443		} else if (defaultline == NULL && !match &&
444		    strcmp(regex, "DEFAULT") == 0) {
445			defaultline = xstrdup(q);
446		} else if (!match) {
447			if (regcomp(&preg, regex, REG_NOSUB|REG_EXTENDED))
448				goto bad;
449
450			if (regexec(&preg, repo, 0, NULL, 0) != REG_NOMATCH) {
451				match = 1;
452
453				tline = xmalloc(sizeof(*tline));
454				tline->line = xstrdup(q);
455				TAILQ_INSERT_HEAD(list, tline, flist);
456			}
457			regfree(&preg);
458		}
459	}
460
461	free(nline);
462
463	if (defaultline != NULL) {
464		if (!match) {
465			tline = xmalloc(sizeof(*tline));
466			tline->line = defaultline;
467			TAILQ_INSERT_HEAD(list, tline, flist);
468		} else
469			free(defaultline);
470	}
471
472	(void)fclose(fp);
473
474	if (TAILQ_EMPTY(list)) {
475		free(list);
476		list = NULL;
477	}
478
479	return (list);
480
481bad:
482	cvs_log(LP_NOTICE, "%s: malformed line %d", file, lineno);
483	free(defaultline);
484	cvs_trigger_freelist(list);
485
486	(void)fclose(fp);
487
488	return (NULL);
489}
490
491void
492cvs_trigger_freelist(struct trigger_list * list)
493{
494	struct trigger_line *line;
495
496	while ((line = TAILQ_FIRST(list)) != NULL) {
497		TAILQ_REMOVE(list, line, flist);
498		free(line->line);
499		free(line);
500	}
501
502	free(list);
503}
504
505void
506cvs_trigger_freeinfo(struct file_info_list * list)
507{
508	struct file_info * fi;
509
510	while ((fi = TAILQ_FIRST(list)) != NULL) {
511		TAILQ_REMOVE(list, fi, flist);
512
513		free(fi->file_path);
514		free(fi->file_wd);
515		free(fi->crevstr);
516		free(fi->nrevstr);
517		free(fi->tag_new);
518		free(fi->tag_old);
519		free(fi);
520	}
521}
522
523void
524cvs_trigger_loginfo_header(BUF *buf, char *repo)
525{
526	char *dir, pwd[PATH_MAX];
527	char hostname[HOST_NAME_MAX+1];
528
529	if (gethostname(hostname, sizeof(hostname)) == -1) {
530		fatal("cvs_trigger_loginfo_header: gethostname failed %s",
531		    strerror(errno));
532	}
533
534	if (getcwd(pwd, sizeof(pwd)) == NULL)
535		fatal("cvs_trigger_loginfo_header: Cannot get working "
536		    "directory");
537
538	if ((dir = dirname(pwd)) == NULL) {
539		fatal("cvs_trigger_loginfo_header: dirname failed %s",
540		    strerror(errno));
541	}
542
543	buf_puts(buf, "Update of ");
544	buf_puts(buf, current_cvsroot->cr_dir);
545	buf_putc(buf, '/');
546	buf_puts(buf, repo);
547	buf_putc(buf, '\n');
548
549	buf_puts(buf, "In directory ");
550	buf_puts(buf, hostname);
551	buf_puts(buf, ":");
552	buf_puts(buf, dir);
553	buf_putc(buf, '/');
554	buf_puts(buf, repo);
555	buf_putc(buf, '\n');
556	buf_putc(buf, '\n');
557}
558
559