/*===========================================================================
 Copyright (c) 1998-2000, The Santa Cruz Operation
 All rights reserved.

 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are met:

 *Redistributions of source code must retain the above copyright notice,
 this list of conditions and the following disclaimer.

 *Redistributions in binary form must reproduce the above copyright notice,
 this list of conditions and the following disclaimer in the documentation
 and/or other materials provided with the distribution.

 *Neither name of The Santa Cruz Operation nor the names of its contributors
 may be used to endorse or promote products derived from this software
 without specific prior written permission.

 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS
 IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT falseT LIMITED TO,
 THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
 LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 CONSEQUENTIAL DAMAGES (INCLUDING, BUT falseT LIMITED TO, PROCUREMENT OF
 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 INTERRUPTION)
 HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
 DAMAGE.
 =========================================================================*/


/*    cscope - interactive C symbol cross-reference
 *
 *    directory searching functions
 */

#include "global.h"

#include "vpath.h" /* vpdirs and vpndirs */

#include <stdlib.h>
#include <sys/types.h> /* needed by stat.h and dirent.h */
#include <dirent.h>
#include <sys/stat.h>  /* stat */
#include <assert.h>

#define DIRSEPS " ,:"				   /* directory list separators */
#define DIRINC	10					   /* directory list size increment */
#define HASHMOD 2003				   /* must be a prime number */
#define SRCINC	HASHMOD				   /* source file list size increment */
									   /* largest known database had 22049 files */

char		  currentdir[PATHLEN + 1]; /* current directory */
char		**incdirs;				   /* #include directories */
char		**srcdirs;				   /* source directories */
char		**srcfiles;				   /* source files */
unsigned long nincdirs;				   /* number of #include directories */
unsigned long nsrcdirs;				   /* number of source directories */
unsigned long nsrcfiles;			   /* number of source files */
unsigned long msrcfiles = SRCINC;	   /* maximum number of source files */

static bool firstbuild = true;

static char		   **incnames;		   /* #include directory names without view pathing */
static unsigned long mincdirs = DIRINC; /* maximum number of #include directories */
static unsigned long msrcdirs;			/* maximum number of source directories */
static unsigned long nvpsrcdirs;		/* number of view path source directories */

static struct listitem {				/* source file names without view pathing */
		char			*text;
		struct listitem *next;
} *srcnames[HASHMOD];

/* Internal prototypes: */
static bool is_accessible_file(const char *file);
static bool is_source_file(char *file);
static void add_source_directory(char *dir);
static void add_include_directory(char *name, char *path);
static void scan_dir(const char *dirfile, bool recurse);
static void make_vp_source_directories(void);
static void read_listfile(FILE * listfile);

static
void read_listfile(FILE * names) {
	char line[PATHLEN * 10];
	char path[PATHLEN + 1];
	char  dir[PATHLEN + 1];
	char *s;
	/* get the names in the file */
	while(fgets(line, 10 * PATHLEN, names) != NULL) {
		char  *point_in_line	 = line;
		size_t length_of_name	 = 0;
		int	   unfinished_option = 0;
		bool   done				 = false;

		/* Kill away \n left at end of fgets()'d string: */
        {
            size_t last_char_index = strlen(line) - 1;
            if (line[last_char_index] == '\n') { line[last_char_index] = '\0'; }
        }

		/* Parse whitespace-terminated strings in line: */
		while(sscanf(point_in_line, "%" PATHLEN_STR "s", path) == 1) {
			/* Have to store this length --- inviewpath() will
			 * modify path, later! */
			length_of_name = strlen(path);

			if(*path == '-') { /* if an option */
				if(unfinished_option) {
					/* Can't have another option directly after an
					 * -I or -p option with no name after it! */
					fprintf(stderr,
						PROGRAM_NAME
						": Syntax error in namelist file %s: unfinished -I or -p option\n",
						namefile);
					unfinished_option = 0;
				}

				int i = path[1];
				switch(i) {
					case 'c': /* ASCII characters only in crossref */
						compress = false;
						break;
					case 'k': /* ignore DEFAULT_INCLUDE_DIRECTORY */
						kernelmode = true;
						break;
					case 'q': /* quick search */
						invertedindex = true;
						break;
					case 'T': /* truncate symbols to 8 characters */
						trun_syms = true;
						break;
					case 'I': /* #include file directory */
					case 'p': /* file path components to display */
						/* coverity[overwrite_var] */
						s = path + 2;	 /* for "-Ipath" */
						if(*s == '\0') { /* if "-I path" */
							unfinished_option = i;
							break;
						}

						/* this code block used several times in here
						 * --> make it a macro to avoid unnecessary
						 * duplication */
                        /* XXX: i dont think this does anything here?
                         *       if only we had tests
                         */
#define HANDLE_OPTION_ARGUMENT(i, s)                                                     \
	switch(i) {                                                                          \
		case 'I': /* #include file directory */                                          \
			if(firstbuild == true) {                                                     \
				/* expand $ and ~ */                                                     \
				shellpath(dir, sizeof(dir), (s));                                        \
				includedir(dir);                                                         \
			}                                                                            \
			unfinished_option = 0;                                                       \
			done			  = true;                                                    \
			break;                                                                       \
		case 'p': /* file path components to display */                                  \
			if(*(s) < '0' || *(s) > '9') {                                               \
				fprintf(stderr,                                                          \
					"csope: -p option in file %s: missing or invalid numeric value\n",   \
					namefile);                                                           \
			}                                                                            \
			dispcomponents	  = atoi(s);                                                 \
			unfinished_option = 0;                                                       \
			done			  = true;                                                    \
			break;                                                                       \
		default:                                                                         \
			done = false;                                                                \
	} /* switch(i) */

						/* ... and now call it for the first time */
						HANDLE_OPTION_ARGUMENT(i, s)
						break;
					default:
						fprintf(stderr,
							PROGRAM_NAME
							": only -I, -c, -k, -p, and -T options can be in file %s\n",
							namefile);
				} /* switch(i) */
			}	  /* if('-') */
			else if(*path == '"') {
				/* handle quoted filenames... */
				size_t in = 1, out = 0;
				char  *newpath = malloc(PATHLEN + 1);

				while(in < PATHLEN && point_in_line[in] != '\0') {
					if(point_in_line[in] == '"') {
						newpath[out] = '\0';
						/* Tell outer loop to skip over this entire
						 * quoted string */
						length_of_name = in + 1;
						break; /* found end of quoted string */
					} else if(point_in_line[in] == '\\' && in < PATHLEN - 1 &&
							  (point_in_line[in + 1] == '"' ||
								  point_in_line[in + 1] == '\\')) {
						/* un-escape \" or \\ sequence */
						newpath[out++] = point_in_line[in + 1];
						in += 2;
					} else {
						newpath[out++] = point_in_line[in++];
					}
				}					/* while(in) */
				if(in >= PATHLEN) { /* safeguard against almost-overflow */
					newpath[out] = '\0';
				}

				/* If an -I or -p arguments was missing before,
				 * treat this name as the argument: */
				HANDLE_OPTION_ARGUMENT(unfinished_option, newpath);
				if(!done) {
					/* coverity[overwrite_var] */
					if((s = inviewpath(newpath)) != NULL) {
						addsrcfile(s);
					} else {
						fprintf(stderr, PROGRAM_NAME ": cannot find file %s\n", newpath);
						errorsfound = true;
					}
				}
				free(newpath);
			} /* if(quoted name) */
			else {
				/* ... so this is an ordinary file name, unquoted */

				/* If an -I or -p arguments was missing before,
				 * treat this name as the argument: */
				HANDLE_OPTION_ARGUMENT(unfinished_option, path);
				if(!done) {
					if((s = inviewpath(path)) != NULL) {
						addsrcfile(s);
					} else {
						fprintf(stderr, PROGRAM_NAME ": cannot find file %s\n", path);
						errorsfound = true;
					}
				}
			} /* else(ordinary name) */

			point_in_line += length_of_name;
			while(isspace((unsigned char)*point_in_line)) { point_in_line++; }
		} /* while(sscanf(line)) */
	}	  /* while(fgets(line)) */

}

/* make the view source directory list */
static
void make_vp_source_directories(void) {
	/* return if this function has already been called */
	if(nsrcdirs > 0) { return; }
	/* get the current directory name */
	if(getcwd(currentdir, PATHLEN) == NULL) {
		fprintf(stderr, PROGRAM_NAME ": warning: cannot get current directory name\n");
		strcpy(currentdir, "<unknown>");
	}
	/* see if there is a view path and this directory is in it */
	vpinit(currentdir);
	if(vpndirs > 1) {
		nsrcdirs = vpndirs;
	} else {
		nsrcdirs = 1;
	}
	/* create the source directory list */
	msrcdirs = nsrcdirs + DIRINC;
	srcdirs	 = malloc(msrcdirs * sizeof(*srcdirs));
	*srcdirs = "."; /* first source dir is always current dir */
	for(int i = 1; i < vpndirs; ++i) {
		srcdirs[i] = vpdirs[i];
	}
	/* save the number of original source directories in the view path */
	nvpsrcdirs = nsrcdirs;
}

/* add a source directory to the list */
static
void add_source_directory(char *dir) {
	struct stat statstruct;

	char *compressed_path = compress_path(dir);
	/* make sure it is a directory */
	if(lstat(compressed_path, &statstruct) == 0 && S_ISDIR(statstruct.st_mode)) {

		/* note: there already is a source directory list */
		if(nsrcdirs == msrcdirs) {
			msrcdirs += DIRINC;
			srcdirs = realloc(srcdirs, msrcdirs * sizeof(*srcdirs));
		}
		srcdirs[nsrcdirs++] = compressed_path;
	}
}

/* add a #include directory to the list */
static
void add_include_directory(char *name, char *path) {
	struct stat statstruct;

	char *compressed_path = compress_path(path);
	/* make sure it is a directory */
	if(lstat(compressed_path, &statstruct) == 0 && S_ISDIR(statstruct.st_mode)) {
		if(incdirs == NULL) {
			incdirs	 = malloc(mincdirs * sizeof(*incdirs));
			incnames = malloc(mincdirs * sizeof(*incnames));
		} else if(nincdirs == mincdirs) {
			mincdirs += DIRINC;
			incdirs	 = realloc(incdirs, mincdirs * sizeof(*incdirs));
			incnames = realloc(incnames, mincdirs * sizeof(*incnames));
		}
		incdirs[nincdirs]	 = compressed_path;
		incnames[nincdirs++] = strdup(name);
	}
}

/* scan a directory (recursively?) for source files */
static
void scan_dir(const char *adir, bool recurse_dir) {
	DIR *dirfile;
	int	 adir_len = strlen(adir);

	/* FIXME: no guards against adir_len > PATHLEN, yet */

	if((dirfile = opendir(adir)) != NULL) {
		struct dirent *entry;
		char		   path[PATHLEN + 1];

		while((entry = readdir(dirfile)) != NULL) {
			if((strcmp(".", entry->d_name) != 0) && (strcmp("..", entry->d_name) != 0)) {
				struct stat buf;

				snprintf(path,
					sizeof(path),
					"%s/%.*s",
					adir,
					PATHLEN - 2 - adir_len,
					entry->d_name);

				if(lstat(path, &buf)) { continue; }

				if(recurse_dir && S_ISDIR(buf.st_mode)) {
					scan_dir(path, recurse_dir);
				} else
                if(is_source_file(path)
                && !infilelist(path)
                && access(path, R_OK) == 0) {
					addsrcfile(path);
				}
			}
		}
		closedir(dirfile);
	}
	return;
}

/* check if a file is readable enough to be allowed in the
 * database */
static
bool is_accessible_file(const char *file) {

	if(access(file, READ) == 0) {
		struct stat stats;
		if(lstat(file, &stats) == 0 && S_ISREG(stats.st_mode)) {
			return true;
		}
	}
	return false;
}

/* see if this is a source file */
/* NOTE: these macros are somewhat faster than calling strcmp(),
 *        while not significantly uglier
 */
#define IS_SUFFIX_OF_2(s, suffix) (s[0] == suffix[0] && s[1] == suffix[1])
#define IS_SUFFIX_OF_3(s, suffix) (s[0] == suffix[0] && s[1] == suffix[1] && s[2] == suffix[2])
static
bool is_source_file(char *path) {
	struct stat statstruct;
	const char *file              = basename(path);
	char	   *suffix            = strrchr(file, '.');
	bool		looks_like_source = false;

	/* ensure there is some file suffix */
	if(suffix == NULL || *(++suffix) == '\0') { return false; }

	/* if an SCCS or versioned file */
	if(file[1] == '.' && file + 2 != suffix) { /* 1 character prefix */
		switch(*file) {
			case 's':
			case 'S':
				return false;
		}
	}

	if(suffix[1] == '\0') { /* 1 character suffix */
		switch(*suffix) {
			case 'c':
			case 'h':
			case 'l':
			case 'y':
			case 'C':
			case 'G':
			case 'H':
			case 'L':
				looks_like_source = true;
		}
	} else if((suffix[2] == '\0') /* 2 char suffix */
			  && (
                    /* breakpoint listing */
                    IS_SUFFIX_OF_2(suffix, "bp")
                    /* Ingres */
					|| IS_SUFFIX_OF_2(suffix, "qc")
					|| IS_SUFFIX_OF_2(suffix, "qh")
                    /* SDL */
					|| IS_SUFFIX_OF_2(suffix, "sd")
                    /* C++ source */
					|| IS_SUFFIX_OF_2(suffix, "cc")
                    /* C++ header */
					|| IS_SUFFIX_OF_2(suffix, "hh"))) {
		looks_like_source = true;
	} else if(suffix[3] == '\0' && ( /* 3 char suffix */
		      /* C++ template source */
		      IS_SUFFIX_OF_3(suffix, "tcc")
		      /* C++ source: */
		      || IS_SUFFIX_OF_3(suffix, "cpp")
		      || IS_SUFFIX_OF_3(suffix, "cxx")
		      || IS_SUFFIX_OF_3(suffix, "hpp")
		      || IS_SUFFIX_OF_3(suffix, "hxx")
              /**/
		      || IS_SUFFIX_OF_3(suffix, "inc")
    )) {
		looks_like_source = true;
	}

	if (!looks_like_source) { return false; }

	/* make sure it is a file */
	if(lstat(path, &statstruct) == 0 && S_ISREG(statstruct.st_mode)) { return true; }
	return false;
}

/* add a source directory to the list for each view path source directory */
void sourcedir(const char * dirlist) {
	char path[PATHLEN + 1];
	make_vp_source_directories();		   /* make the view source directory list */
	char * mdirlist = strdup(dirlist); /* don't change environment variable text */

	/* parse the directory list */
	char * dir = strtok(mdirlist, DIRSEPS);
	while(dir != NULL) {
		int dir_len = strlen(dir);

		add_source_directory(dir);

		/* if it isn't a full path name and there is a
		   multi-directory view path */
		if(*mdirlist != '/' && vpndirs > 1) {

			/* compute its path from higher view path source dirs */
			for(unsigned i = 1; i < nvpsrcdirs; ++i) {
				snprintf(path,
					sizeof(path),
					"%.*s/%s",
					PATHLEN - 2 - dir_len,
					srcdirs[i],
					dir);
				add_source_directory(path);
			}
		}
		dir = strtok(NULL, DIRSEPS);
	}
	free(mdirlist);
}

// XXX: useless strdup
/* add a #include directory to the list for each view path source directory */
void includedir(const char * dirlist) {
	char		 path[PATHLEN + 1];
	char		*dir;

	make_vp_source_directories();		   /* make the view source directory list */
	char * mdirlist = strdup(dirlist); /* don't change environment variable text */

	/* parse the directory list */
	dir = strtok(mdirlist, DIRSEPS);
	while(dir != NULL) {
		size_t dir_len = strlen(dir);

		add_include_directory(dir, dir);

		/* if it isn't a full path name and there is a
		   multi-directory view path */
		if(*mdirlist != '/' && vpndirs > 1) {

			/* compute its path from higher view path source dirs */
			for(unsigned i = 1; i < nvpsrcdirs; ++i) {
				snprintf(path,
					sizeof(path),
					"%.*s/%s",
					(int)(PATHLEN - 2 - dir_len),
					srcdirs[i],
					dir);
				add_include_directory(dir, path);
			}
		}
		dir = strtok(NULL, DIRSEPS);
	}
	free(mdirlist); /* HBB 20000421: avoid leaks */
}

/* make the source file list */
void makefilelist(const char * const * const argv) {

	make_vp_source_directories(); /* make the view source directory list */

	/* if -i was NOT given and there are source file arguments */
	if(namefile == NULL && *argv) {
		/* put them in a list that can be expanded */
		for(unsigned i = 0; argv[i]; i++) {
			const char * file = argv[i];
			if (!infilelist(file)) {
                char * s = inviewpath(file);
				if (s) {
					addsrcfile(s);
				} else {
					fprintf(stderr, PROGRAM_NAME ": cannot find file %s\n", file);
					errorsfound = true;
				}
			}
		}
		return;
	}

	/* see if a file name file exists */
	if(namefile == NULL && vpaccess(NAMEFILE, READ) == 0) { namefile = NAMEFILE; }

	if(namefile == NULL) {
		/* No namefile --> make a list of all the source files
		 * in the directories */
		for(unsigned i = 0; i < nsrcdirs; i++) {
			scan_dir(srcdirs[i], recurse_dir);
		}
		return;
	}

	/* Came here --> there is a file of source file names */
	FILE *names;				/* name file pointer */

	if(strcmp(namefile, "-") == 0)
		names = stdin;
	else if((names = vpfopen(namefile, "r")) == NULL) {
		cannotopen(namefile);
		myexit(1);
	}

    read_listfile(names);

	if(names == stdin) {
		clearerr(stdin);
    } else {
		fclose(names);
    }

	firstbuild = false;
	return;
}

/* add an include file to the source file list */
void incfile(char *file, char *type) {
	assert(file != NULL); /* should never happen, but let's make sure anyway */

	/* see if the file is already in the source file list */
	if(infilelist(file) == true) { return; }

	/* look in current directory if it was #include "file" */
    {
        char * s;
        if(type[0] == '"' && (s = inviewpath(file)) != NULL) {
            addsrcfile(s);
            return;
        }
    }

	size_t file_len = strlen(file);

	/* search for the file in the #include directory list */
	for(unsigned i = 0; i < nincdirs; i++) {
        char name[PATHLEN + 1];
        char path[PATHLEN + 1];
		/* don't include the file from two directories */
		snprintf(name,
			sizeof(name),
			"%.*s/%s",
			(int)(PATHLEN - 2 - file_len),
			incnames[i],
			file);
		if(infilelist(name) == true) { break; }
		/* make sure it exists and is readable */
		snprintf(path,
			sizeof(path),
			"%.*s/%s",
			(int)(PATHLEN - 2 - file_len),
			incdirs[i],
			file);

		if(access(path, READ) == 0) {
			addsrcfile(path);
			break;
		}
	}
}

/* see if the file is already in the list */
bool infilelist(const char * path) {
	struct listitem *p;

	char *dir_path = compress_path(path);

	for(p = srcnames[hash(dir_path) % HASHMOD]; p != NULL; p = p->next) {
		if(strequal(path, p->text)) {
			free(dir_path);
			return (true);

		}
	}

	free(dir_path);
	return (false);
}

/* search for the file in the view path */
char *inviewpath(const char * file) {
	static char	path[PATHLEN + 1];

	/* look for the file */
	if(is_accessible_file(file)) {
        strcpy(path, file);
        return path;
    }

	/* if it isn't a full path name and there is a multi-directory
	 * view path */
	if(*file != '/'
    && vpndirs > 1) {
		int file_len = strlen(file);

		/* compute its path from higher view path source dirs */
		for(unsigned i = 1; i < nvpsrcdirs; ++i) {
			snprintf(path,
				sizeof(path),
				"%.*s/%s",
				PATHLEN - 2 - file_len,
				srcdirs[i],
				file
            );
			if(is_accessible_file(path)) { return path; }
		}
	}

	return NULL;
}

/* add a source file to the list */
void addsrcfile(char *path) {
	struct listitem *p;
	int				 i;

	/* make sure there is room for the file */
	if(nsrcfiles == msrcfiles) {
		msrcfiles += SRCINC;
		srcfiles = realloc(srcfiles, msrcfiles * sizeof(*srcfiles));
	}
	/* add the file to the list */
	char *dir_path = compress_path(path);
	srcfiles[nsrcfiles++] = strdup(dir_path);
	p					  = malloc(sizeof(*p));
	p->text				  = strdup(dir_path);
	free(dir_path);
	i					  = hash(p->text) % HASHMOD;
	p->next				  = srcnames[i];
	srcnames[i]			  = p;
}

/* free the memory allocated for the source file list */
void freefilelist(void) {
	struct listitem *p, *nextp;
	int				 i;

	/* if '-d' option is used a string space block is allocated */
	if(preserve_database == false) {
		while(nsrcfiles > 0) {
			free(srcfiles[--nsrcfiles]);
		}
	} else {
		/* for '-d' option free the string space block */
		/* protect against empty list */
		if(nsrcfiles > 0) free(srcfiles[0]);
		nsrcfiles = 0;
	}

	free(srcfiles); /* HBB 20000421: avoid leak */
	msrcfiles = 0;
	srcfiles  = 0;

	for(i = 0; i < HASHMOD; ++i) {
		for(p = srcnames[i]; p != NULL; p = nextp) {
			/* HBB 20000421: avoid memory leak */
			free(p->text);
			nextp = p->next;
			free(p);
		}
		srcnames[i] = NULL;
	}
}

void freeinclist() {
	if(!incdirs) return;
	while(nincdirs > 0) {
		free(incdirs[--nincdirs]);
		free(incnames[nincdirs]);
	}
	free(incdirs);
	free(incnames);
}

void freesrclist() {
	if(!srcdirs) { return; }
	while(nsrcdirs > 1) {
        free(srcdirs[--nsrcdirs]);
    }
	free(srcdirs);
}
