1/*
2 * Copyright (C) 1984-2017  Mark Nudelman
3 *
4 * You may distribute under the terms of either the GNU General Public
5 * License or the Less License, as specified in the README file.
6 *
7 * For more information, see the README file.
8 */
9
10
11#include "less.h"
12
13#define	WHITESP(c)	((c)==' ' || (c)=='\t')
14
15#if TAGS
16
17public char ztags[] = "tags";
18public char *tags = ztags;
19
20static int total;
21static int curseq;
22
23extern int linenums;
24extern int sigs;
25extern int ctldisp;
26
27enum tag_result {
28	TAG_FOUND,
29	TAG_NOFILE,
30	TAG_NOTAG,
31	TAG_NOTYPE,
32	TAG_INTR
33};
34
35/*
36 * Tag type
37 */
38enum {
39	T_CTAGS,	/* 'tags': standard and extended format (ctags) */
40	T_CTAGS_X,	/* stdin: cross reference format (ctags) */
41	T_GTAGS,	/* 'GTAGS': function defenition (global) */
42	T_GRTAGS,	/* 'GRTAGS': function reference (global) */
43	T_GSYMS,	/* 'GSYMS': other symbols (global) */
44	T_GPATH		/* 'GPATH': path name (global) */
45};
46
47static enum tag_result findctag();
48static enum tag_result findgtag();
49static char *nextgtag();
50static char *prevgtag();
51static POSITION ctagsearch();
52static POSITION gtagsearch();
53static int getentry();
54
55/*
56 * The list of tags generated by the last findgtag() call.
57 *
58 * Use either pattern or line number.
59 * findgtag() always uses line number, so pattern is always NULL.
60 * findctag() uses either pattern (in which case line number is 0),
61 * or line number (in which case pattern is NULL).
62 */
63struct taglist {
64	struct tag *tl_first;
65	struct tag *tl_last;
66};
67struct tag {
68	struct tag *next, *prev; /* List links */
69	char *tag_file;		/* Source file containing the tag */
70	LINENUM tag_linenum;	/* Appropriate line number in source file */
71	char *tag_pattern;	/* Pattern used to find the tag */
72	char tag_endline;	/* True if the pattern includes '$' */
73};
74#define TAG_END  ((struct tag *) &taglist)
75static struct taglist taglist = { TAG_END, TAG_END };
76static struct tag *curtag;
77
78#define TAG_INS(tp) \
79	(tp)->next = TAG_END; \
80	(tp)->prev = taglist.tl_last; \
81	taglist.tl_last->next = (tp); \
82	taglist.tl_last = (tp);
83
84#define TAG_RM(tp) \
85	(tp)->next->prev = (tp)->prev; \
86	(tp)->prev->next = (tp)->next;
87
88/*
89 * Delete tag structures.
90 */
91	public void
92cleantags()
93{
94	struct tag *tp;
95
96	/*
97	 * Delete any existing tag list.
98	 * {{ Ideally, we wouldn't do this until after we know that we
99	 *    can load some other tag information. }}
100	 */
101	while ((tp = taglist.tl_first) != TAG_END)
102	{
103		TAG_RM(tp);
104		free(tp->tag_file);
105		free(tp->tag_pattern);
106		free(tp);
107	}
108	curtag = NULL;
109	total = curseq = 0;
110}
111
112/*
113 * Create a new tag entry.
114 */
115	static struct tag *
116maketagent(name, file, linenum, pattern, endline)
117	char *name;
118	char *file;
119	LINENUM linenum;
120	char *pattern;
121	int endline;
122{
123	struct tag *tp;
124
125	tp = (struct tag *) ecalloc(sizeof(struct tag), 1);
126	tp->tag_file = (char *) ecalloc(strlen(file) + 1, sizeof(char));
127	strcpy(tp->tag_file, file);
128	tp->tag_linenum = linenum;
129	tp->tag_endline = endline;
130	if (pattern == NULL)
131		tp->tag_pattern = NULL;
132	else
133	{
134		tp->tag_pattern = (char *) ecalloc(strlen(pattern) + 1, sizeof(char));
135		strcpy(tp->tag_pattern, pattern);
136	}
137	return (tp);
138}
139
140/*
141 * Get tag mode.
142 */
143	public int
144gettagtype()
145{
146	int f;
147
148	if (strcmp(tags, "GTAGS") == 0)
149		return T_GTAGS;
150	if (strcmp(tags, "GRTAGS") == 0)
151		return T_GRTAGS;
152	if (strcmp(tags, "GSYMS") == 0)
153		return T_GSYMS;
154	if (strcmp(tags, "GPATH") == 0)
155		return T_GPATH;
156	if (strcmp(tags, "-") == 0)
157		return T_CTAGS_X;
158	f = open(tags, OPEN_READ);
159	if (f >= 0)
160	{
161		close(f);
162		return T_CTAGS;
163	}
164	return T_GTAGS;
165}
166
167/*
168 * Find tags in tag file.
169 * Find a tag in the "tags" file.
170 * Sets "tag_file" to the name of the file containing the tag,
171 * and "tagpattern" to the search pattern which should be used
172 * to find the tag.
173 */
174	public void
175findtag(tag)
176	char *tag;
177{
178	int type = gettagtype();
179	enum tag_result result;
180
181	if (type == T_CTAGS)
182		result = findctag(tag);
183	else
184		result = findgtag(tag, type);
185	switch (result)
186	{
187	case TAG_FOUND:
188	case TAG_INTR:
189		break;
190	case TAG_NOFILE:
191		error("No tags file", NULL_PARG);
192		break;
193	case TAG_NOTAG:
194		error("No such tag in tags file", NULL_PARG);
195		break;
196	case TAG_NOTYPE:
197		error("unknown tag type", NULL_PARG);
198		break;
199	}
200}
201
202/*
203 * Search for a tag.
204 */
205	public POSITION
206tagsearch()
207{
208	if (curtag == NULL)
209		return (NULL_POSITION);  /* No gtags loaded! */
210	if (curtag->tag_linenum != 0)
211		return gtagsearch();
212	else
213		return ctagsearch();
214}
215
216/*
217 * Go to the next tag.
218 */
219	public char *
220nexttag(n)
221	int n;
222{
223	char *tagfile = (char *) NULL;
224
225	while (n-- > 0)
226		tagfile = nextgtag();
227	return tagfile;
228}
229
230/*
231 * Go to the previous tag.
232 */
233	public char *
234prevtag(n)
235	int n;
236{
237	char *tagfile = (char *) NULL;
238
239	while (n-- > 0)
240		tagfile = prevgtag();
241	return tagfile;
242}
243
244/*
245 * Return the total number of tags.
246 */
247	public int
248ntags()
249{
250	return total;
251}
252
253/*
254 * Return the sequence number of current tag.
255 */
256	public int
257curr_tag()
258{
259	return curseq;
260}
261
262/*****************************************************************************
263 * ctags
264 */
265
266/*
267 * Find tags in the "tags" file.
268 * Sets curtag to the first tag entry.
269 */
270	static enum tag_result
271findctag(tag)
272	char *tag;
273{
274	char *p;
275	FILE *f;
276	int taglen;
277	LINENUM taglinenum;
278	char *tagfile;
279	char *tagpattern;
280	int tagendline;
281	int search_char;
282	int err;
283	char tline[TAGLINE_SIZE];
284	struct tag *tp;
285
286	p = shell_unquote(tags);
287	f = fopen(p, "r");
288	free(p);
289	if (f == NULL)
290		return TAG_NOFILE;
291
292	cleantags();
293	total = 0;
294	taglen = (int) strlen(tag);
295
296	/*
297	 * Search the tags file for the desired tag.
298	 */
299	while (fgets(tline, sizeof(tline), f) != NULL)
300	{
301		if (tline[0] == '!')
302			/* Skip header of extended format. */
303			continue;
304		if (strncmp(tag, tline, taglen) != 0 || !WHITESP(tline[taglen]))
305			continue;
306
307		/*
308		 * Found it.
309		 * The line contains the tag, the filename and the
310		 * location in the file, separated by white space.
311		 * The location is either a decimal line number,
312		 * or a search pattern surrounded by a pair of delimiters.
313		 * Parse the line and extract these parts.
314		 */
315		tagpattern = NULL;
316
317		/*
318		 * Skip over the whitespace after the tag name.
319		 */
320		p = skipsp(tline+taglen);
321		if (*p == '\0')
322			/* File name is missing! */
323			continue;
324
325		/*
326		 * Save the file name.
327		 * Skip over the whitespace after the file name.
328		 */
329		tagfile = p;
330		while (!WHITESP(*p) && *p != '\0')
331			p++;
332		*p++ = '\0';
333		p = skipsp(p);
334		if (*p == '\0')
335			/* Pattern is missing! */
336			continue;
337
338		/*
339		 * First see if it is a line number.
340		 */
341		tagendline = 0;
342		taglinenum = getnum(&p, 0, &err);
343		if (err)
344		{
345			/*
346			 * No, it must be a pattern.
347			 * Delete the initial "^" (if present) and
348			 * the final "$" from the pattern.
349			 * Delete any backslash in the pattern.
350			 */
351			taglinenum = 0;
352			search_char = *p++;
353			if (*p == '^')
354				p++;
355			tagpattern = p;
356			while (*p != search_char && *p != '\0')
357			{
358				if (*p == '\\')
359					p++;
360				p++;
361			}
362			tagendline = (p[-1] == '$');
363			if (tagendline)
364				p--;
365			*p = '\0';
366		}
367		tp = maketagent(tag, tagfile, taglinenum, tagpattern, tagendline);
368		TAG_INS(tp);
369		total++;
370	}
371	fclose(f);
372	if (total == 0)
373		return TAG_NOTAG;
374	curtag = taglist.tl_first;
375	curseq = 1;
376	return TAG_FOUND;
377}
378
379/*
380 * Edit current tagged file.
381 */
382	public int
383edit_tagfile()
384{
385	if (curtag == NULL)
386		return (1);
387	return (edit(curtag->tag_file));
388}
389
390	static int
391curtag_match(char const *line, POSITION linepos)
392{
393	/*
394	 * Test the line to see if we have a match.
395	 * Use strncmp because the pattern may be
396	 * truncated (in the tags file) if it is too long.
397	 * If tagendline is set, make sure we match all
398	 * the way to end of line (no extra chars after the match).
399	 */
400	int len = (int) strlen(curtag->tag_pattern);
401	if (strncmp(curtag->tag_pattern, line, len) == 0 &&
402	    (!curtag->tag_endline || line[len] == '\0' || line[len] == '\r'))
403	{
404		curtag->tag_linenum = find_linenum(linepos);
405		return 1;
406	}
407	return 0;
408}
409
410/*
411 * Search for a tag.
412 * This is a stripped-down version of search().
413 * We don't use search() for several reasons:
414 *   -	We don't want to blow away any search string we may have saved.
415 *   -	The various regular-expression functions (from different systems:
416 *	regcmp vs. re_comp) behave differently in the presence of
417 *	parentheses (which are almost always found in a tag).
418 */
419	static POSITION
420ctagsearch()
421{
422	POSITION pos, linepos;
423	LINENUM linenum;
424	int line_len;
425	char *line;
426	int found;
427
428	pos = ch_zero();
429	linenum = find_linenum(pos);
430
431	for (found = 0; !found;)
432	{
433		/*
434		 * Get lines until we find a matching one or
435		 * until we hit end-of-file.
436		 */
437		if (ABORT_SIGS())
438			return (NULL_POSITION);
439
440		/*
441		 * Read the next line, and save the
442		 * starting position of that line in linepos.
443		 */
444		linepos = pos;
445		pos = forw_raw_line(pos, &line, &line_len);
446		if (linenum != 0)
447			linenum++;
448
449		if (pos == NULL_POSITION)
450		{
451			/*
452			 * We hit EOF without a match.
453			 */
454			error("Tag not found", NULL_PARG);
455			return (NULL_POSITION);
456		}
457
458		/*
459		 * If we're using line numbers, we might as well
460		 * remember the information we have now (the position
461		 * and line number of the current line).
462		 */
463		if (linenums)
464			add_lnum(linenum, pos);
465
466		if (ctldisp != OPT_ONPLUS)
467		{
468			if (curtag_match(line, linepos))
469				found = 1;
470		} else
471		{
472			int cvt_ops = CVT_ANSI;
473			int cvt_len = cvt_length(line_len, cvt_ops);
474			int *chpos = cvt_alloc_chpos(cvt_len);
475			char *cline = (char *) ecalloc(1, cvt_len);
476			cvt_text(cline, line, chpos, &line_len, cvt_ops);
477			if (curtag_match(cline, linepos))
478				found = 1;
479			free(chpos);
480			free(cline);
481		}
482	}
483
484	return (linepos);
485}
486
487/*******************************************************************************
488 * gtags
489 */
490
491/*
492 * Find tags in the GLOBAL's tag file.
493 * The findgtag() will try and load information about the requested tag.
494 * It does this by calling "global -x tag" and storing the parsed output
495 * for future use by gtagsearch().
496 * Sets curtag to the first tag entry.
497 */
498	static enum tag_result
499findgtag(tag, type)
500	char *tag;		/* tag to load */
501	int type;		/* tags type */
502{
503	char buf[256];
504	FILE *fp;
505	struct tag *tp;
506
507	if (type != T_CTAGS_X && tag == NULL)
508		return TAG_NOFILE;
509
510	cleantags();
511	total = 0;
512
513	/*
514	 * If type == T_CTAGS_X then read ctags's -x format from stdin
515	 * else execute global(1) and read from it.
516	 */
517	if (type == T_CTAGS_X)
518	{
519		fp = stdin;
520		/* Set tag default because we cannot read stdin again. */
521		tags = ztags;
522	} else
523	{
524#if !HAVE_POPEN
525		return TAG_NOFILE;
526#else
527		char *command;
528		char *flag;
529		char *qtag;
530		char *cmd = lgetenv("LESSGLOBALTAGS");
531
532		if (cmd == NULL || *cmd == '\0')
533			return TAG_NOFILE;
534		/* Get suitable flag value for global(1). */
535		switch (type)
536		{
537		case T_GTAGS:
538			flag = "" ;
539			break;
540		case T_GRTAGS:
541			flag = "r";
542			break;
543		case T_GSYMS:
544			flag = "s";
545			break;
546		case T_GPATH:
547			flag = "P";
548			break;
549		default:
550			return TAG_NOTYPE;
551		}
552
553		/* Get our data from global(1). */
554		qtag = shell_quote(tag);
555		if (qtag == NULL)
556			qtag = tag;
557		command = (char *) ecalloc(strlen(cmd) + strlen(flag) +
558				strlen(qtag) + 5, sizeof(char));
559		sprintf(command, "%s -x%s %s", cmd, flag, qtag);
560		if (qtag != tag)
561			free(qtag);
562		fp = popen(command, "r");
563		free(command);
564#endif
565	}
566	if (fp != NULL)
567	{
568		while (fgets(buf, sizeof(buf), fp))
569		{
570			char *name, *file, *line;
571			int len;
572
573			if (sigs)
574			{
575#if HAVE_POPEN
576				if (fp != stdin)
577					pclose(fp);
578#endif
579				return TAG_INTR;
580			}
581			len = (int) strlen(buf);
582			if (len > 0 && buf[len-1] == '\n')
583				buf[len-1] = '\0';
584			else
585			{
586				int c;
587				do {
588					c = fgetc(fp);
589				} while (c != '\n' && c != EOF);
590			}
591
592 			if (getentry(buf, &name, &file, &line))
593			{
594				/*
595				 * Couldn't parse this line for some reason.
596				 * We'll just pretend it never happened.
597				 */
598				break;
599			}
600
601			/* Make new entry and add to list. */
602			tp = maketagent(name, file, (LINENUM) atoi(line), NULL, 0);
603			TAG_INS(tp);
604			total++;
605		}
606		if (fp != stdin)
607		{
608			if (pclose(fp))
609			{
610				curtag = NULL;
611				total = curseq = 0;
612				return TAG_NOFILE;
613			}
614		}
615	}
616
617	/* Check to see if we found anything. */
618	tp = taglist.tl_first;
619	if (tp == TAG_END)
620		return TAG_NOTAG;
621	curtag = tp;
622	curseq = 1;
623	return TAG_FOUND;
624}
625
626static int circular = 0;	/* 1: circular tag structure */
627
628/*
629 * Return the filename required for the next gtag in the queue that was setup
630 * by findgtag().  The next call to gtagsearch() will try to position at the
631 * appropriate tag.
632 */
633	static char *
634nextgtag()
635{
636	struct tag *tp;
637
638	if (curtag == NULL)
639		/* No tag loaded */
640		return NULL;
641
642	tp = curtag->next;
643	if (tp == TAG_END)
644	{
645		if (!circular)
646			return NULL;
647		/* Wrapped around to the head of the queue */
648		curtag = taglist.tl_first;
649		curseq = 1;
650	} else
651	{
652		curtag = tp;
653		curseq++;
654	}
655	return (curtag->tag_file);
656}
657
658/*
659 * Return the filename required for the previous gtag in the queue that was
660 * setup by findgtat().  The next call to gtagsearch() will try to position
661 * at the appropriate tag.
662 */
663	static char *
664prevgtag()
665{
666	struct tag *tp;
667
668	if (curtag == NULL)
669		/* No tag loaded */
670		return NULL;
671
672	tp = curtag->prev;
673	if (tp == TAG_END)
674	{
675		if (!circular)
676			return NULL;
677		/* Wrapped around to the tail of the queue */
678		curtag = taglist.tl_last;
679		curseq = total;
680	} else
681	{
682		curtag = tp;
683		curseq--;
684	}
685	return (curtag->tag_file);
686}
687
688/*
689 * Position the current file at at what is hopefully the tag that was chosen
690 * using either findtag() or one of nextgtag() and prevgtag().  Returns -1
691 * if it was unable to position at the tag, 0 if successful.
692 */
693	static POSITION
694gtagsearch()
695{
696	if (curtag == NULL)
697		return (NULL_POSITION);  /* No gtags loaded! */
698	return (find_pos(curtag->tag_linenum));
699}
700
701/*
702 * The getentry() parses both standard and extended ctags -x format.
703 *
704 * [standard format]
705 * <tag>   <lineno>  <file>         <image>
706 * +------------------------------------------------
707 * |main     30      main.c         main(argc, argv)
708 * |func     21      subr.c         func(arg)
709 *
710 * The following commands write this format.
711 *	o Traditinal Ctags with -x option
712 *	o Global with -x option
713 *		See <http://www.gnu.org/software/global/global.html>
714 *
715 * [extended format]
716 * <tag>   <type>  <lineno>   <file>        <image>
717 * +----------------------------------------------------------
718 * |main     function 30      main.c         main(argc, argv)
719 * |func     function 21      subr.c         func(arg)
720 *
721 * The following commands write this format.
722 *	o Exuberant Ctags with -x option
723 *		See <http://ctags.sourceforge.net>
724 *
725 * Returns 0 on success, -1 on error.
726 * The tag, file, and line will each be NUL-terminated pointers
727 * into buf.
728 */
729	static int
730getentry(buf, tag, file, line)
731	char *buf;	/* standard or extended ctags -x format data */
732	char **tag;	/* name of the tag we actually found */
733	char **file;	/* file in which to find this tag */
734	char **line;	/* line number of file where this tag is found */
735{
736	char *p = buf;
737
738	for (*tag = p;  *p && !IS_SPACE(*p);  p++)	/* tag name */
739		;
740	if (*p == 0)
741		return (-1);
742	*p++ = 0;
743	for ( ;  *p && IS_SPACE(*p);  p++)		/* (skip blanks) */
744		;
745	if (*p == 0)
746		return (-1);
747	/*
748	 * If the second part begin with other than digit,
749	 * it is assumed tag type. Skip it.
750	 */
751	if (!IS_DIGIT(*p))
752	{
753		for ( ;  *p && !IS_SPACE(*p);  p++)	/* (skip tag type) */
754			;
755		for (;  *p && IS_SPACE(*p);  p++)	/* (skip blanks) */
756			;
757	}
758	if (!IS_DIGIT(*p))
759		return (-1);
760	*line = p;					/* line number */
761	for (*line = p;  *p && !IS_SPACE(*p);  p++)
762		;
763	if (*p == 0)
764		return (-1);
765	*p++ = 0;
766	for ( ; *p && IS_SPACE(*p);  p++)		/* (skip blanks) */
767		;
768	if (*p == 0)
769		return (-1);
770	*file = p;					/* file name */
771	for (*file = p;  *p && !IS_SPACE(*p);  p++)
772		;
773	if (*p == 0)
774		return (-1);
775	*p = 0;
776
777	/* value check */
778	if (strlen(*tag) && strlen(*line) && strlen(*file) && atoi(*line) > 0)
779		return (0);
780	return (-1);
781}
782
783#endif
784