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