/*
   euses - A small utility in C that quickly displays use flag descriptions

   Copyright 2005-2020 Jeroen Roovers

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2, or (at your option)
   any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software Foundation,
   Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

   HISTORY
   euses is the first working incarnation of something I called `guses'
   (for Gentoo uses, see <http://www.xs4all.nl/~rooversj/gentoo/guses>)
   which is a bash script I wrote that searches crudely for use flags in
   use.desc (USE_DESC) and use.local.desc (USE_LOCAL_DESC) and returns the
   matching lines with some fancy colouring.  I originally intended that to
   be the ultimate, but I opted to learn a bit of C instead and have a much
   faster working program. Not that speed matters much in this case...

   This program was first released on May 9, 2005.

   REQUIREMENTS
   euses should compile on any basic Linux/UNIX system. A compiler and GCC are
   all you need. However, at runtime you should probably have a portage(5) tree
   around (see also http://www.gentoo.org/). Using portage implies using
   bash(1), so you need that as well.

   ACKNOWLEDGMENTS
   Thanks to KillerFox and #gentoo-hppa on freenode for their general C
   programming clues. :)

   BUGS
   Mail your suggestions, improvements, complaints and questions to
   jer@gentoo.org

*/

#include <stdio.h>		/* fgets(), popen(), pclose(), rewind() */
#include <limits.h>		/* realpath() */
#include <stdlib.h>		/* getenv() */
#include <sys/types.h>		/* stat() */
#include <sys/stat.h>		/* stat() */
#include <unistd.h>
#include <string.h>		/* strlen(), strchr(), strcmp(), strstr() */
#include <getopt.h>
#include <ctype.h>		/* tolower(), isgraph() */
#include <glob.h>		/* Find *.desc files */
#include <libgen.h>		/* Find *.desc files */
#include "euses.h"

static void usage()
{
	printf("Usage:  euses [OPTION]... [STRING]...\n\n"
	       " -a, --allfields   " "Search entire lines\n"
	       " -A, --allfiles    " "Search all description files\n"
	       " -c, --colour      " "Coloured output\n"
	       " -d, --description "
	       "Search only use flag descriptions\n"
	       " -f, --flag        " "Search only use flags\n"
	       " -h, --help        " "Show (this) usage information\n"
	       " -i, --ignorecase  " "Case-insensitive search\n");
	printf(" -p, --package     " "Search only package names\n"
	       " -P, --pedantic    " "Be even more verbose than --verbose\n"
	       " -s, --strict      "
	       "Match full STRING instead of substring\n"
	       " -S, --strip       "
	       "Strip emerge's *, +, %% and - from STRINGs\n"
	       " -v, --verbose     "
	       "Prefix output lines with the description file's name\n"
	       " -V, --version     " "Show euses version and exit\n");
}

static void version()
{
	printf("%s %s\n"
	       "Copyright Jeroen Roovers 2005-2020 - Please report bugs to %s\n"
	       "This is Free Software released under the GPL version 2.\n",
	       PACKAGE_NAME, PACKAGE_VERSION, PACKAGE_BUGREPORT);
}

static void doerror(int err_no, char *first, char *second)
{
	switch (err_no) {
	case FILE_ERR:
		if (first == NULL)
			puts("Do you Gentoo?");
		else
			printf("%s not found\n", first);
		exit(EXIT_FAILURE);
		break;

	case FORMAT_ERR:
		fprintf(stderr, " * There is a format error ");
		if (first != NULL) {
			fprintf(stderr, "in file: %s\n", first);
			if (second != NULL)
				fprintf(stderr, " * on this line:\n%s", second);
		} else
			fprintf(stderr, "%s, %s.\n", first, second);
		break;

	case NOT_IMPLEMENTED:
		fprintf(stderr, "* %s not implemented yet, sorry.\n"
			"Bug me about it at %s", first, PACKAGE_BUGREPORT);
		exit(EXIT_FAILURE);
		break;

	case OPTION_CONFLICT:
		fprintf(stderr,
			"The %s and %s options conflict.\n"
			"  Please review euses(1).\n", first, second);
		exit(EXIT_FAILURE);
		break;
	default:
		fprintf(stderr, "Please send a bug report to %s",
			PACKAGE_BUGREPORT);
		exit(EXIT_FAILURE);
	}
}

static char *getportdir(char *portdir)
{
	FILE *fd;
	struct stat *finfo;
	char *path;

	finfo = malloc(sizeof(struct stat));
	path = malloc(FILENAME_MAX);
	/* Check if PORTDIR is set in the environment */
	if (getenv("PORTDIR") != NULL) {
		strncpy(portdir, getenv("PORTDIR"), FILENAME_MAX);
	} else {

		/* or load it from etc{,portage}/make.{globals,conf} */
		/* Open a pipe. */
		fd = popen
			(
				"for file in "
				DATADIR "/portage/config/make.globals "
				SYSCONFDIR "/make.globals "
				SYSCONFDIR "/make.conf "
				SYSCONFDIR "/portage/make.conf "
				SYSCONFDIR "/eusesrc "
				"${HOME}/.eusesrc "
				"; do [ -f $file ] && . $file; done; "
				"printf \"%s\" \"${PORTDIR}\"", "r"
			);

		/* Get the first line from the pipe */
		if (fgets(portdir, (int)FILENAME_MAX, fd) == NULL)
			doerror(FILE_ERR, "PORTDIR", NULL);

		/* Close the pipe. */
		pclose(fd);
	}
	/* Turn the portdir string into a path */
	stat(realpath(portdir, path), finfo);
	/* Check if the portdir path points to a proper directory */
	if (finfo->st_mode == S_IFDIR) {
		/* or else set it to the default */
		strncpy(portdir, "/usr/portage", 13);
	}
	free(finfo);
	free(path);
	return portdir;
}

static int line_is_no_comment(char *line, char *desc_file_name)
{
	int i = 0;		/* Counter for characters in line */
	int ret = -1;		/* Return value */
	while (line[i] != '\0') {
		switch (line[i]) {
		case ' ':
		case '\t':
			i++;
			break;
		case '\n':
		case '#':
			ret = 0;
			break;
		default:
			ret = 1;
		}
		if (ret >= 0)
			break;
	}
	if (i > 0)
		doerror(FORMAT_ERR, desc_file_name, line);
	return ret;
}

static void match_err(int output_in_colour, int output_type,
		      char *input_pattern, char *desc_file_name)
{
	if (output_in_colour) {
		printf("%s%s%s: ", BRIGHT_RED, desc_file_name, NORMAL_WHITE);
		printf("No matches for: %s%s%s\n", BRIGHT_WHITE, input_pattern,
		       NORMAL_WHITE);
	} else {
		printf("%s: ", desc_file_name);
		printf("No matches for %s\n", input_pattern);
	}
}

static int pkg_flag_desc(char *line, char *pkg, char *flagdesc)
{
	char *p;
	char *q;
	char buf[LINE_LENGTH];

	strncpy(buf, line, LINE_LENGTH - 1);
	p = strchr(buf, ':');
	q = strchr(buf, ' ');
	if (p != NULL && p < q) {
		/* This specimen has a colon: */
		*p = '\0';
		p = buf;
		q = p + strlen(p) + 1;
		strncpy(pkg, p, LINE_LENGTH);
		strncpy(flagdesc, q, LINE_LENGTH);
		return 1;
	} else
		/* This specimen has no colon: */
		return 0;
}

static int flag_desc(char *line, char *flag, char *desc)
{
	char *p;
	char *q;
	char buf[LINE_LENGTH];
	/* int ped = 0; <-- Pedantic mode */

	strncpy(buf, line, LINE_LENGTH);
	if ((p = strchr(buf + 1, ' ')) != NULL) {
		/* if (ped && p[1] != '-' || p[2] != ' ' || p[3] == ' ')
		   doerror(FORMAT_ERR, NULL, line); <-- Pedantic mode */
		*p = '\0';
		p = buf;
		q = p + strlen(p) + 3;
		strncpy(flag, p, LINE_LENGTH);
		strncpy(desc, q, LINE_LENGTH);
		return 1;
	} else
		return 0;
}

static int printm(int output_in_colour, int fileformat, char *desc_file_name,
		  int output_type, char *pkg, char *flag, char *desc, int counter)
{
	if (output_in_colour) {
		if (output_type >= OUTPUT_VERBOSE) {
			printf("%s%s%s::%d::", BRIGHT_WHITE, desc_file_name,
			       NORMAL_WHITE, counter);
		}
		if (fileformat == FF_USE_LOCAL_DESC)
			printf("%s%s%s:%s%s%s - %s", NORMAL_CYAN, pkg,
			       NORMAL_WHITE, BRIGHT_GREEN, flag, NORMAL_WHITE,
			       desc);
		else
			printf("%s%s%s - %s", BRIGHT_GREEN, flag, NORMAL_WHITE,
			       desc);
	} else {
		if (output_type >= OUTPUT_VERBOSE) {
			printf("%s::%d::", desc_file_name, counter);
		}
		if (fileformat == FF_USE_LOCAL_DESC)
			printf("%s:%s - %s", pkg, flag, desc);
		else
			printf("%s - %s", flag, desc);
	}
	return 0;
}

static void low(char *word)
{
	for (; *word; word++)
		*word = tolower(*word);
}

/* The flag stripper strips the characters from USE flags that
 * emerge --pretend/--ask tends to add. */
static char *flagstripper(char *flag)
{
	int last, i;

	last = strlen(flag);

	for (i = 0; i < last; i++) {

		/* If it's a disabled flag, remove the prefixed dash. */
		switch (flag[0]) {
		case '(':
		case '-':
		case '+':
			flag++;
			break;
		}

		/* Find the position of the last character in the argv. */
		/* If it's a suffixed asterisk or percent,
		 * remove it from anywhere. */
		if (flag[last] == '*' || flag[last] == '%' || flag[last] == ')')
			flag[last] = '\0';
	}

	return flag;
}

static int
match(char *input_pattern, FILE * fp, int output_in_colour, int output_type,
      int search_target, int search_ignore_case, int search_strict,
      char *desc_file_name)
{
	int m = 0;		/* Local match counter */
	int fileformat = FF_USE_DESC;
	char l[LINE_LENGTH];
	char p[LINE_LENGTH];
	char f[LINE_LENGTH];
	char d[LINE_LENGTH];
	char fd[LINE_LENGTH];
	char t[LINE_LENGTH];
	char *line = l;
	int c = 1;		/* Line counter. */

	/* Retrieve a line from fp and search it. */
	while (fgets(line, LINE_LENGTH, fp) != NULL) {
		char *pkg = p;
		char *flag = f;
		char *desc = d;
		char *flagdesc = fd;
		char *target = t;

		if (line_is_no_comment(line, desc_file_name)) {
			/* Separate pkg from flagdesc */
			if (pkg_flag_desc(line, pkg, flagdesc))
				fileformat = FF_USE_LOCAL_DESC;
			else
				strncpy(flagdesc, line, LINE_LENGTH);

			/* Separate flag from desc */
			if (!flag_desc(flagdesc, flag, desc)) {
				doerror(FORMAT_ERR, desc_file_name, line);
			}

			switch (search_target) {
			case FLAG:
				strncpy(target, flag, LINE_LENGTH);
				break;
			case DESC:
				strncpy(target, desc, LINE_LENGTH);
				break;
			case PKG:
				strncpy(target, pkg, LINE_LENGTH);
				break;
			case LINE:
				strncpy(target, line, LINE_LENGTH);
				break;
			}

			if (search_ignore_case) {
				low(target);
				low(input_pattern);
			}
			if (search_strict && strcmp(target, input_pattern) == 0) {
				printm(output_in_colour, fileformat,
				       desc_file_name, output_type, pkg,
				       flag, desc, c);
				m++;

			} else if (!search_strict
				   && strstr(target, input_pattern) != NULL) {
				printm(output_in_colour, fileformat,
				       desc_file_name, output_type, pkg,
				       flag, desc, c);
				m++;
			}
		}
		c++;
	}
	rewind(fp);
	if (m > 0)
		return 1;
	else
		return 0;
}

extern int main(int argc, char **argv)
{
	int c;			/* getopt */
	int i;			/* glob match */
	glob_t desc_files;	/* struct for *.desc matches */
	int output_in_colour = 0;	/* Colourize output */
	int output_type = OUTPUT_NORMAL;
	/* OUTPUT_NORMAL: Be quiet
	 * OUTPUT_VERBOSE: Display filename
	 * OUTPUT_PEDANTIC: Display non-matching filename */
	int fm = 0;		/* Number of files matching STRINGs */
	int m = 0;		/* Number of matches per STRING */
	int patterns = 0;	/* Number of patterns to match */
	/* Only search use.{,local.}desc */
	int desc_files_to_search = SEARCH_ALL_USE;
	int do_strip = 0;	/* Whether to strip patterns. */
	int search_strict = 0;	/* Only match full patterns (a == b) */

	FILE *desc_fp = NULL;
	char path[FILENAME_MAX];
	char desc_path[FILENAME_MAX];
	char desc_path_desc[FILENAME_MAX];
	char desc_file_name[FILENAME_MAX];
	char *portdir = path;
	char *portdir_profiles_glob = desc_path;	/* $PORTDIR/profiles/[foo].desc */
	char *portdir_profiles_desc_glob = desc_path_desc;	/* $PORTDIR/profiles/desc/[foo].desc */
	int search_target = FLAG;	/* Do not match entire lines by default */
	int search_ignore_case = 0;	/* Do not ignore case by default */
	char *input_pattern;	/* What's left of an argv[optind] */
	int opt_c;		/* Count remaining args */

	/* With that out of the way, do getopt stuff: */
	while (1) {
		int option_index = 0;
		static struct option long_options[] = {
			{"allfields", 0, 0, 'a'},
			{"allfiles", 0, 0, 'A'},
			{"colour", 0, 0, 'c'},
			{"description", 0, 0, 'd'},
			{"flag", 0, 0, 'f'},
			{"help", 0, 0, 'h'},
			{"ignorecase", 0, 0, 'i'},
			{"package", 0, 0, 'p'},
			{"pedantic", 0, 0, 'P'},
			{"strict", 0, 0, 's'},
			{"strip", 0, 0, 'S'},
			{"verbose", 0, 0, 'v'},
			{"version", 0, 0, 'V'},
			{0, 0, 0, 0}
		};

		c = getopt_long(argc, argv, "+aAcdfhipPsSvV",
				long_options, &option_index);
		if (c == -1)
			break;

		switch (c) {
		case 'c':
			output_in_colour = 1;
			break;
		case 'h':
			usage();
			exit(EXIT_SUCCESS);
		case 'V':
			version();
			exit(EXIT_SUCCESS);
		case 's':
			search_strict = 1;
			break;
		case 'S':
			do_strip = 1;
			break;
		case 'v':
			if (output_type == 0)
				output_type = OUTPUT_VERBOSE;
			break;
		case 'f':
			search_target = FLAG;
			break;
		case 'a':
			search_target = LINE;
			break;
		case 'A':
			desc_files_to_search = SEARCH_ALL;
			break;
		case 'd':
			search_target = DESC;
			break;
		case 'p':
			search_target = PKG;
			break;
		case 'P':
			output_type = OUTPUT_PEDANTIC;
			break;
		case 'i':
			search_ignore_case = 1;
			break;
		case '?':
			break;
		}
	}
	/* No arguments were given */
	if (optind >= argc)
		usage();

	/* Store optind for later use */
	opt_c = optind;

	/* If --strict and --allfields are both set, bail out. */
	if (search_strict && search_target == LINE)
		doerror(OPTION_CONFLICT, "--allfields", "--strict");
	/* If --strict and --ignorecase are set, bail out. */
	if (search_strict && search_ignore_case)
		doerror(OPTION_CONFLICT, "--ignorecase", "--strict");

	if (search_target == PKG && desc_files_to_search == SEARCH_ALL)
		doerror(OPTION_CONFLICT, "--package", "--allfiles");
	/* Find flag description files in $PORTDIR/profiles/{,desc/}*.desc */
	memset(&desc_files, '\0', sizeof(desc_files));

	/* Get PORTDIR */
	portdir = getportdir(portdir);
	if (output_type == OUTPUT_PEDANTIC)
		printf("PORTDIR:%s\n", portdir);

	/* Concatenate globs */
	memset(portdir_profiles_glob, '\0', FILENAME_MAX);
	memset(portdir_profiles_desc_glob, '\0', FILENAME_MAX);

	/* Dirty hack to fill the glob strings.
	 * TODO: Replace with code to fill the glob struct itself some time. */
	if (desc_files_to_search == SEARCH_USE) {
		snprintf(portdir_profiles_glob, FILENAME_MAX,
			 "%s/profiles/use*.desc", portdir);
	}
	if (desc_files_to_search == SEARCH_LOCAL) {
		snprintf(portdir_profiles_glob, FILENAME_MAX,
			 "%s/profiles/use.local.desc", portdir);
	} else {
		snprintf(portdir_profiles_glob, FILENAME_MAX,
			 "%s/profiles/use*.desc", portdir);
		snprintf(portdir_profiles_desc_glob, FILENAME_MAX,
			 "%s/profiles/desc/*.desc", portdir);
	}

	/* Find all .desc files in profiles/{,desc/} */
	glob(portdir_profiles_glob, GLOB_DOOFFS, NULL, &desc_files);

	if (desc_files_to_search > SEARCH_LOCAL)
		glob(portdir_profiles_desc_glob, GLOB_DOOFFS | GLOB_APPEND,
		     NULL, &desc_files);

	/* Test if any .desc files were found */
	if (desc_files.gl_pathc < 1) {
		doerror(FILE_ERR, NULL, NULL);
	} else {
		for (i = 0; i < desc_files.gl_pathc; ++i) {
			sprintf(desc_file_name, "%s",
				basename(desc_files.gl_pathv[i]));

			/* arches.desc is special */
			if(strncmp(desc_file_name, "arches.desc", 12) == 0)
				continue;

			/* Let's open the file */
			if ((desc_fp = fopen(desc_files.gl_pathv[i],
					     "r")) == NULL)
				doerror(FILE_ERR, desc_file_name, NULL);

			/* Calculate how many patterns to process. */
			patterns = argc - optind;

			/* loop through arguments. */
			while (optind < argc) {
				/* Deploy the flag stripper. */
				if (do_strip)
					input_pattern =
					    flagstripper(argv[optind]);
				else
					input_pattern = argv[optind];

				/* Search in this desc file. */
				m += match(input_pattern, desc_fp,
					   output_in_colour, output_type,
					   search_target, search_ignore_case,
					   search_strict,
					   basename(desc_files.gl_pathv[i])
				    );

				/* All matching done: */
				if (m > 0) {
					fm++;
				/* Display non-matching files */
				} else if (output_type == OUTPUT_PEDANTIC) {
						match_err(output_in_colour,
							  output_type,
							  input_pattern,
							  basename
							  (desc_files.gl_pathv[i]));
				}

				/* Reset match counters for the next string
				 * search: */
				m = 0;

				/* Next argument: */
				optind++;
			}	/* loop through arguments */

			/* Close those files that were opened. */
			fclose(desc_fp);
			optind = opt_c;
		}		/* loop through desc_files */

		/* Report statistics */
		if (output_type == OUTPUT_PEDANTIC) {
			if (output_in_colour) {
				printf
				    (" %s*%s %s%d%s out of %s%zu%s files matched"
				     " %s%d%s patterns\n", BRIGHT_GREEN,
				     NORMAL_WHITE, BRIGHT_YELLOW, fm,
				     NORMAL_WHITE, BRIGHT_YELLOW,
				     desc_files.gl_pathc, NORMAL_WHITE,
				     BRIGHT_YELLOW, patterns, NORMAL_WHITE);
			} else {
				printf
				    (" * %d out of %zu files matched %d patterns\n",
				     fm, desc_files.gl_pathc, patterns);
			}
		}
		/* Report statistics */
		globfree(&desc_files);
	}

	if (fm < patterns) {
		exit(EXIT_FAILURE);
	} else
		exit(EXIT_SUCCESS);
}
