1/*-
2 * Copyright (c) 2003-2006, Maxime Henrion <mux@FreeBSD.org>
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 *    notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 *    notice, this list of conditions and the following disclaimer in the
12 *    documentation and/or other materials provided with the distribution.
13 *
14 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
15 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
18 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24 * SUCH DAMAGE.
25 *
26 * $FreeBSD$
27 */
28
29#include <assert.h>
30#include <err.h>
31#include <errno.h>
32#include <stdio.h>
33#include <stdlib.h>
34#include <string.h>
35#include <time.h>
36
37#include "diff.h"
38#include "keyword.h"
39#include "misc.h"
40#include "queue.h"
41#include "stream.h"
42
43/*
44 * The keyword API is used to expand the CVS/RCS keywords in files,
45 * such as $Id$, $Revision$, etc.  The server does it for us when it
46 * sends us entire files, but we need to handle the expansion when
47 * applying a diff update.
48 */
49
50enum rcskey {
51	RCSKEY_AUTHOR,
52	RCSKEY_CVSHEADER,
53	RCSKEY_DATE,
54	RCSKEY_HEADER,
55	RCSKEY_ID,
56	RCSKEY_LOCKER,
57	RCSKEY_LOG,
58	RCSKEY_NAME,
59	RCSKEY_RCSFILE,
60	RCSKEY_REVISION,
61	RCSKEY_SOURCE,
62	RCSKEY_STATE
63};
64
65typedef enum rcskey rcskey_t;
66
67struct tag {
68	char *ident;
69	rcskey_t key;
70	int enabled;
71	STAILQ_ENTRY(tag) next;
72};
73
74static struct tag	*tag_new(const char *, rcskey_t);
75static char		*tag_expand(struct tag *, struct diffinfo *);
76static void		 tag_free(struct tag *);
77
78struct keyword {
79	STAILQ_HEAD(, tag) keywords;		/* Enabled keywords. */
80	size_t minkeylen;
81	size_t maxkeylen;
82};
83
84/* Default CVS keywords. */
85static struct {
86	const char *ident;
87	rcskey_t key;
88} tag_defaults[] = {
89	{ "Author",	RCSKEY_AUTHOR },
90	{ "CVSHeader",	RCSKEY_CVSHEADER },
91	{ "Date",	RCSKEY_DATE },
92	{ "Header",	RCSKEY_HEADER },
93	{ "Id",		RCSKEY_ID },
94	{ "Locker",	RCSKEY_LOCKER },
95	{ "Log",	RCSKEY_LOG },
96	{ "Name",	RCSKEY_NAME },
97	{ "RCSfile",	RCSKEY_RCSFILE },
98	{ "Revision",	RCSKEY_REVISION },
99	{ "Source",	RCSKEY_SOURCE },
100	{ "State",	RCSKEY_STATE },
101	{ NULL,		0, }
102};
103
104struct keyword *
105keyword_new(void)
106{
107	struct keyword *new;
108	struct tag *tag;
109	size_t len;
110	int i;
111
112	new = xmalloc(sizeof(struct keyword));
113	STAILQ_INIT(&new->keywords);
114	new->minkeylen = ~0;
115	new->maxkeylen = 0;
116	for (i = 0; tag_defaults[i].ident != NULL; i++) {
117		tag = tag_new(tag_defaults[i].ident, tag_defaults[i].key);
118		STAILQ_INSERT_TAIL(&new->keywords, tag, next);
119		len = strlen(tag->ident);
120		/*
121		 * These values are only computed here and not updated when
122		 * adding an alias.  This is a bug, but CVSup has it and we
123		 * need to be bug-to-bug compatible since the server will
124		 * expect us to do the same, and we will fail with an MD5
125		 * checksum mismatch if we don't.
126		 */
127		new->minkeylen = min(new->minkeylen, len);
128		new->maxkeylen = max(new->maxkeylen, len);
129	}
130	return (new);
131}
132
133int
134keyword_decode_expand(const char *expand)
135{
136
137	if (strcmp(expand, ".") == 0)
138		return (EXPAND_DEFAULT);
139	else if (strcmp(expand, "kv") == 0)
140		return (EXPAND_KEYVALUE);
141	else if (strcmp(expand, "kvl") == 0)
142		return (EXPAND_KEYVALUELOCKER);
143	else if (strcmp(expand, "k") == 0)
144		return (EXPAND_KEY);
145	else if (strcmp(expand, "o") == 0)
146		return (EXPAND_OLD);
147	else if (strcmp(expand, "b") == 0)
148		return (EXPAND_BINARY);
149	else if (strcmp(expand, "v") == 0)
150		return (EXPAND_VALUE);
151	else
152		return (-1);
153}
154
155const char *
156keyword_encode_expand(int expand)
157{
158
159	switch (expand) {
160		case EXPAND_DEFAULT:
161			return (".");
162		case EXPAND_KEYVALUE:
163			return ("kv");
164		case EXPAND_KEYVALUELOCKER:
165			return ("kvl");
166		case EXPAND_KEY:
167			return ("k");
168		case EXPAND_OLD:
169			return ("o");
170		case EXPAND_BINARY:
171			return ("b");
172		case EXPAND_VALUE:
173			return ("v");
174	}
175	return (NULL);
176}
177
178void
179keyword_free(struct keyword *keyword)
180{
181	struct tag *tag;
182
183	if (keyword == NULL)
184		return;
185	while (!STAILQ_EMPTY(&keyword->keywords)) {
186		tag = STAILQ_FIRST(&keyword->keywords);
187		STAILQ_REMOVE_HEAD(&keyword->keywords, next);
188		tag_free(tag);
189	}
190	free(keyword);
191}
192
193int
194keyword_alias(struct keyword *keyword, const char *ident, const char *rcskey)
195{
196	struct tag *new, *tag;
197
198	STAILQ_FOREACH(tag, &keyword->keywords, next) {
199		if (strcmp(tag->ident, rcskey) == 0) {
200			new = tag_new(ident, tag->key);
201			STAILQ_INSERT_HEAD(&keyword->keywords, new, next);
202			return (0);
203		}
204	}
205	errno = ENOENT;
206	return (-1);
207}
208
209int
210keyword_enable(struct keyword *keyword, const char *ident)
211{
212	struct tag *tag;
213	int all;
214
215	all = 0;
216	if (strcmp(ident, ".") == 0)
217		all = 1;
218
219	STAILQ_FOREACH(tag, &keyword->keywords, next) {
220		if (!all && strcmp(tag->ident, ident) != 0)
221			continue;
222		tag->enabled = 1;
223		if (!all)
224			return (0);
225	}
226	if (!all) {
227		errno = ENOENT;
228		return (-1);
229	}
230	return (0);
231}
232
233int
234keyword_disable(struct keyword *keyword, const char *ident)
235{
236	struct tag *tag;
237	int all;
238
239	all = 0;
240	if (strcmp(ident, ".") == 0)
241		all = 1;
242
243	STAILQ_FOREACH(tag, &keyword->keywords, next) {
244		if (!all && strcmp(tag->ident, ident) != 0)
245			continue;
246		tag->enabled = 0;
247		if (!all)
248			return (0);
249	}
250
251	if (!all) {
252		errno = ENOENT;
253		return (-1);
254	}
255	return (0);
256}
257
258void
259keyword_prepare(struct keyword *keyword)
260{
261	struct tag *tag, *temp;
262
263	STAILQ_FOREACH_SAFE(tag, &keyword->keywords, next, temp) {
264		if (!tag->enabled) {
265			STAILQ_REMOVE(&keyword->keywords, tag, tag, next);
266			tag_free(tag);
267			continue;
268		}
269	}
270}
271
272/*
273 * Expand appropriate RCS keywords.  If there's no tag to expand,
274 * keyword_expand() returns 0, otherwise it returns 1 and writes a
275 * pointer to the new line in *buf and the new len in *len.  The
276 * new line is allocated with malloc() and needs to be freed by the
277 * caller after use.
278 */
279int
280keyword_expand(struct keyword *keyword, struct diffinfo *di, char *line,
281    size_t size, char **buf, size_t *len)
282{
283	struct tag *tag;
284	char *dollar, *keystart, *valstart, *vallim, *next;
285	char *linestart, *newline, *newval, *cp, *tmp;
286	size_t left, newsize, vallen;
287
288	if (di->di_expand == EXPAND_OLD || di->di_expand == EXPAND_BINARY)
289		return (0);
290	newline = NULL;
291	newsize = 0;
292	left = size;
293	linestart = cp = line;
294again:
295	dollar = memchr(cp, '$', left);
296	if (dollar == NULL) {
297		if (newline != NULL) {
298			*buf = newline;
299			*len = newsize;
300			return (1);
301		}
302		return (0);
303	}
304	keystart = dollar + 1;
305	left -= keystart - cp;
306	vallim = memchr(keystart, '$', left);
307	if (vallim == NULL) {
308		if (newline != NULL) {
309			*buf = newline;
310			*len = newsize;
311			return (1);
312		}
313		return (0);
314	}
315	if (vallim == keystart) {
316		cp = keystart;
317		goto again;
318	}
319	valstart = memchr(keystart, ':', left);
320	if (valstart == keystart) {
321		cp = vallim;
322		left -= vallim - keystart;
323		goto again;
324	}
325	if (valstart == NULL || valstart > vallim)
326		valstart = vallim;
327
328	if (valstart < keystart + keyword->minkeylen ||
329	    valstart > keystart + keyword->maxkeylen) {
330		cp = vallim;
331		left -= vallim -keystart;
332		goto again;
333	}
334	STAILQ_FOREACH(tag, &keyword->keywords, next) {
335		if (strncmp(tag->ident, keystart, valstart - keystart) == 0 &&
336		    tag->ident[valstart - keystart] == '\0') {
337			if (newline != NULL)
338				tmp = newline;
339			else
340				tmp = NULL;
341			newval = NULL;
342			if (di->di_expand == EXPAND_KEY) {
343				newsize = dollar - linestart + 1 +
344				    valstart - keystart + 1 +
345				    size - (vallim + 1 - linestart);
346				newline = xmalloc(newsize);
347				cp = newline;
348				memcpy(cp, linestart, dollar - linestart);
349				cp += dollar - linestart;
350				*cp++ = '$';
351				memcpy(cp, keystart, valstart - keystart);
352				cp += valstart - keystart;
353				*cp++ = '$';
354				next = cp;
355				memcpy(cp, vallim + 1,
356				    size - (vallim + 1 - linestart));
357			} else if (di->di_expand == EXPAND_VALUE) {
358				newval = tag_expand(tag, di);
359				if (newval == NULL)
360					vallen = 0;
361				else
362					vallen = strlen(newval);
363				newsize = dollar - linestart +
364				    vallen +
365				    size - (vallim + 1 - linestart);
366				newline = xmalloc(newsize);
367				cp = newline;
368				memcpy(cp, linestart, dollar - linestart);
369				cp += dollar - linestart;
370				if (newval != NULL) {
371					memcpy(cp, newval, vallen);
372					cp += vallen;
373				}
374				next = cp;
375				memcpy(cp, vallim + 1,
376				    size - (vallim + 1 - linestart));
377			} else {
378				assert(di->di_expand == EXPAND_DEFAULT ||
379				    di->di_expand == EXPAND_KEYVALUE ||
380				    di->di_expand == EXPAND_KEYVALUELOCKER);
381				newval = tag_expand(tag, di);
382				if (newval == NULL)
383					vallen = 0;
384				else
385					vallen = strlen(newval);
386				newsize = dollar - linestart + 1 +
387				    valstart - keystart + 2 +
388				    vallen + 2 +
389				    size - (vallim + 1 - linestart);
390				newline = xmalloc(newsize);
391				cp = newline;
392				memcpy(cp, linestart, dollar - linestart);
393				cp += dollar - linestart;
394				*cp++ = '$';
395				memcpy(cp, keystart, valstart - keystart);
396				cp += valstart - keystart;
397				*cp++ = ':';
398				*cp++ = ' ';
399				if (newval != NULL) {
400					memcpy(cp, newval, vallen);
401					cp += vallen;
402				}
403				*cp++ = ' ';
404				*cp++ = '$';
405				next = cp;
406				memcpy(cp, vallim + 1,
407				    size - (vallim + 1 - linestart));
408			}
409			if (newval != NULL)
410				free(newval);
411			if (tmp != NULL)
412				free(tmp);
413			/*
414			 * Continue looking for tags in the rest of the line.
415			 */
416			cp = next;
417			size = newsize;
418			left = size - (cp - newline);
419			linestart = newline;
420			goto again;
421		}
422	}
423	cp = vallim;
424	left = size - (cp - linestart);
425	goto again;
426}
427
428static struct tag *
429tag_new(const char *ident, rcskey_t key)
430{
431	struct tag *new;
432
433	new = xmalloc(sizeof(struct tag));
434	new->ident = xstrdup(ident);
435	new->key = key;
436	new->enabled = 1;
437	return (new);
438}
439
440static void
441tag_free(struct tag *tag)
442{
443
444	free(tag->ident);
445	free(tag);
446}
447
448/*
449 * Expand a specific tag and return the new value.  If NULL
450 * is returned, the tag is empty.
451 */
452static char *
453tag_expand(struct tag *tag, struct diffinfo *di)
454{
455	/*
456	 * CVS formats dates as "XXXX/XX/XX XX:XX:XX".  32 bytes
457	 * is big enough until year 10,000,000,000,000,000 :-).
458	 */
459	char cvsdate[32];
460	struct tm tm;
461	char *filename, *val;
462	int error;
463
464	error = rcsdatetotm(di->di_revdate, &tm);
465	if (error)
466		err(1, "strptime");
467	if (strftime(cvsdate, sizeof(cvsdate), "%Y/%m/%d %H:%M:%S", &tm) == 0)
468		err(1, "strftime");
469	filename = strrchr(di->di_rcsfile, '/');
470	if (filename == NULL)
471		filename = di->di_rcsfile;
472	else
473		filename++;
474
475	switch (tag->key) {
476	case RCSKEY_AUTHOR:
477		xasprintf(&val, "%s", di->di_author);
478		break;
479	case RCSKEY_CVSHEADER:
480		xasprintf(&val, "%s %s %s %s %s", di->di_rcsfile,
481		    di->di_revnum, cvsdate, di->di_author, di->di_state);
482		break;
483	case RCSKEY_DATE:
484		xasprintf(&val, "%s", cvsdate);
485		break;
486	case RCSKEY_HEADER:
487		xasprintf(&val, "%s/%s %s %s %s %s", di->di_cvsroot,
488		    di->di_rcsfile, di->di_revnum, cvsdate, di->di_author,
489		    di->di_state);
490		break;
491	case RCSKEY_ID:
492		xasprintf(&val, "%s %s %s %s %s", filename, di->di_revnum,
493		    cvsdate, di->di_author, di->di_state);
494		break;
495	case RCSKEY_LOCKER:
496		/*
497		 * Unimplemented even in CVSup sources.  It seems we don't
498		 * even have this information sent by the server.
499		 */
500		return (NULL);
501	case RCSKEY_LOG:
502		/* XXX */
503		printf("%s: Implement Log keyword expansion\n", __func__);
504		return (NULL);
505	case RCSKEY_NAME:
506		if (di->di_tag != NULL)
507			xasprintf(&val, "%s", di->di_tag);
508		else
509			return (NULL);
510		break;
511	case RCSKEY_RCSFILE:
512		xasprintf(&val, "%s", filename);
513		break;
514	case RCSKEY_REVISION:
515		xasprintf(&val, "%s", di->di_revnum);
516		break;
517	case RCSKEY_SOURCE:
518		xasprintf(&val, "%s/%s", di->di_cvsroot, di->di_rcsfile);
519		break;
520	case RCSKEY_STATE:
521		xasprintf(&val, "%s", di->di_state);
522		break;
523	}
524	return (val);
525}
526