/*
	Copyright 2013-2019 Jan Filip Chadima <jfch@jagda.eu>
		All Rights Reserved.

----------------------------------------------------------------
 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 of the License, 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, see <http://www.gnu.org/licenses/>.
------------------------------------------------------------------------
*/

/* This program contains memory leaks, if you want to debug it, you may.	*/
/* TODOs are included in this program inviting you ro take part on it.		*/

#define VERSION		"0.7"

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <getopt.h>
#include <magic.h>
#ifdef HAVE_GELF_H
#include <gelf.h>
#else
#ifdef HAVE_LIBELF_H
#include <libelf.h>
#else
#include <libelf/libelf.h>
#endif
#endif
#include <ftw.h>
#include <assert.h>
extern const char *__progname;

static magic_t		magic;					// libmagic handle
static int		option_l = 0;				// proceed only named libraries
static int		option_n = 0;				// proceed only named directories
static int		option_v = 0;				// verbose output
static int		option_X = 0;				// steamless run
static const char	*option_f = NULL;			// configuration file
static const char	*option_r = NULL;			// root

static void
linklib(const char *soname, const char *filename)
{
	char		rl[4096];
	int		rllen;
	struct stat	st;

	if (option_v > 2) {
		fprintf(stderr, "linklib(\"%s\", \"%s\")\n", soname, filename);
	}
	if (!strcmp(soname, filename)) {
		return;
	}
	if ((rllen = readlink(soname, rl, sizeof(rl))) < 0) {
		if (option_v > 1) {
			if (errno != ENOENT) {
				fprintf(stderr, "%s: readlink(%s, *, %d) => %s\n", __progname, soname, sizeof(rl), strerror(errno));
			}
		}
		rl[0] = 0;
	} else {
		rl[rllen] = 0;
	}
	if (option_v) {
		if (strcmp(rl, filename)) {
			printf("	%s => %s (changed)\n", filename, soname);
		} else {
			printf("	%s => %s\n", filename, soname);
		}
	}
	if (!option_X) {
		if (!strcmp(rl, filename)) {
			return;
		}
		if (!lstat(soname, &st)) {
			if (S_ISLNK(st.st_mode)) {
				if (unlink(soname) <0 ) {
					fprintf(stderr, "%s: unlink(%s) => %s\n", __progname, soname, strerror(errno));
				}
			} else {
				return;
			}
		}
		if (symlink(filename, soname) < 0) {
			if (option_v > 1) {
				fprintf(stderr, "%s: symlink(%s, %s) => %s\n", __progname, soname, filename, strerror(errno));
			}
		}
	}
}

static void
soname(const char *path)
{
	int		fd;
	Elf		*e;
	Elf_Scn		*scn;
	Elf64_Shdr	shdr;
	Elf_Data	*data;
	Elf64_Dyn	dyn;
	long long	soname = (long long) -1;
	size_t		shstrndx;
	char		*name;
	long long	count;
	long long	i;

	if (option_v > 2) {
		fprintf(stderr, "soname(\"%s\")\n", path);
	}
	if ((fd = open(path, O_RDONLY)) < 0) {
		if (option_v > 1) {
			fprintf(stderr, "%s: open(%s, O_RDONLY) => %s\n", __progname, path, strerror(errno));
		}
		return;
	}

	if (!(e = elf_begin(fd, ELF_C_READ, NULL))) {
		if (option_v > 1) {
			fprintf(stderr, "%s: elf_begin(%s, ELF_C_READ, NULL) => %s\n", __progname, path, elf_errmsg(-1));
		}
		close(fd);
		return;
	}

	if (elf_kind(e) != ELF_K_ELF) {
		if (option_v > 1) {
			fprintf(stderr, "%s: %s is not an elf object\n", __progname, path);
		}
		elf_end(e);
		close(fd);
		return;
	}

	if (elf_getshdrstrndx(e, &shstrndx)) {
		if (option_v > 1) {
			fprintf(stderr, "%s: elf_getshdrstrndx(%s, *) => %s\n", __progname, path, elf_errmsg(-1));
		}
		elf_end(e);
		close(fd);
		return;
	}

	scn = NULL;

	while ((scn = elf_nextscn(e, scn)) != NULL) {
		if (!gelf_getshdr(scn, &shdr)) {
			if (option_v > 1) {
				fprintf(stderr, "%s: elf_getshdr(%s:*, *) => %s\n", __progname, path, elf_errmsg(-1));
			}
			elf_end(e);
			close(fd);
			return;
		}

		if (!(name = elf_strptr(e, shstrndx , shdr.sh_name))) {
			if (option_v > 1) {
				fprintf(stderr, "%s: elf_stptr(%s, *, sh_name) => %s\n", __progname, path, elf_errmsg(-1));
			}
			elf_end(e);
			close(fd);
			return;
		}

		if (!strcmp(name, ".dynamic")) {
			if (!(data = elf_getdata(scn, NULL))) {
				if (option_v > 1) {
					fprintf(stderr, "%s: elf_getdata(%s:*, *) => %s\n", __progname, path, elf_errmsg(-1));
				}
				elf_end(e);
				close(fd);
				return;
			}

			count = shdr.sh_size / shdr.sh_entsize;

			for (i = 0; i < count; i++) {
				if (!gelf_getdyn(data, (int)i, &dyn)) {
					if (option_v > 1) {
						fprintf(stderr, "%s: elf_getdyn(%s:*, *, *) => %s\n", __progname, path, elf_errmsg(-1));
					}
					elf_end(e);
					close(fd);
					return;
				}

				if (dyn.d_tag == (Elf64_Sxword)DT_SONAME) {
					soname = dyn.d_un.d_ptr;
					break;
				}
			}

		}
	}

	if (soname == (long long) -1) {
		elf_end(e);
		close(fd);

		return;
	}

	scn = NULL;

	while ((scn = elf_nextscn(e, scn)) != NULL) {
		if (!gelf_getshdr(scn, &shdr)) {
			if (option_v > 1) {
				fprintf(stderr, "%s: elf_getshdr(%s:*, *) => %s\n", __progname, path, elf_errmsg(-1));
			}
			elf_end(e);
			close(fd);
			return;
		}

		if (!(name = elf_strptr(e, shstrndx , shdr.sh_name))) {
			if (option_v > 1) {
				fprintf(stderr, "%s: elf_stptr(%s, *, sh_name) => %s\n", __progname, path, elf_errmsg(-1));
			}
			elf_end(e);
			close(fd);
			return;
		}

		if (!strcmp(name, ".dynstr")) {
			if (!(data = elf_getdata(scn, NULL))) {
				if (option_v > 1) {
					fprintf(stderr, "%s: elf_getdata(%s:*, *) => %s\n", __progname, path, elf_errmsg(-1));
				}
				elf_end(e);
				close(fd);
				return;
			}

			linklib((char *)data->d_buf + soname, path + 2);

			elf_end(e);
			close(fd);

			return;
		}
	}

	elf_end(e);
	close(fd);
}

static void
run_body(const char *path)
{
	const char *file_type = magic_file(magic, path);

	if (option_v > 2) {
		fprintf(stderr, "run_body(\"%s\")\n", path);
	}
	if (!strstr(file_type, "ELF ")) return;
	if (strstr(file_type, " shared object")) soname(path);
}

static void
run(const char *path)
{
	if (option_v > 2) {
		fprintf(stderr, "run(\"%s\")\n", path);
	}
	if (option_r) {
		char *fullname;
		asprintf(&fullname, "%s%s", option_r, path);
		run_body(fullname);
	} else {
		run_body(path);
	}
}

static void
runsingle(const char *path)
{
	char *dirname;
	char *basename;

	if (option_v > 2) {
		fprintf(stderr, "runsingle(\"%s\")\n", path);
	}

	dirname = strdup(path);
	basename = strrchr(dirname, '/');
	if (basename) {
		const char *pwd;
		*basename++ = 0;
		pwd = get_current_dir_name();
		if (chdir(dirname) < 0) {
			if (option_v > 1) {
				fprintf(stderr, "%s: chdir(%s) => %s\n", __progname, dirname, strerror(errno));
			}
		} else {
			char *filename;
			asprintf(&filename, "./%s", basename);
			run(filename);
			if (chdir(pwd) < 0) {
				fprintf(stderr, "%s: chdir(%s) => %s\n", __progname, dirname, strerror(errno));
			}
		}
	} else {
		char *filename;
		asprintf(&filename, "./%s", path);
		run(filename);
	}
}

static int
ftw_leaf(const char *path, const struct stat *st, int typeflag, struct FTW *unused)
{
	const char *file_type;

	if (option_v > 2) {
		fprintf(stderr, "ftw_leaf(\"%s\", {%c}, %c, *)\n", path, S_ISREG(st->st_mode)?'R':S_ISDIR(st->st_mode)?'D':'?', (typeflag==FTW_F)?'F':(typeflag==FTW_D)?'D':'?');
	}
	switch (typeflag) {
		case FTW_F:
			if (!S_ISREG(st->st_mode)) return 0;
			run_body(path);

		case FTW_D:
			if (strcmp(path, ".")) {
				return FTW_SKIP_SUBTREE;
			}
	}
	return FTW_CONTINUE;
}

static void
rundir_body(const char *dirname)
{
	if (option_v > 2) {
		fprintf(stderr, "rundir_body(\"%s\")\n", dirname);
	}
	if (chdir(dirname) < 0) {
		if (option_v > 1) {
			fprintf(stderr, "%s: chdir(%s) => %s\n", __progname, dirname, strerror(errno));
		}
	} else {
		if (nftw(".", ftw_leaf, 32, FTW_ACTIONRETVAL|FTW_PHYS) < 0) {
			if (option_v > 1) {
				fprintf(stderr, "%s: ftw(%s, *, *) => %s\n", __progname, dirname, strerror(errno));
			}
		}
	}
}

static void
rundir(const char *dirname)
{
	if (option_v > 2) {
		fprintf(stderr, "rundir(\"%s\")\n", dirname);
	}
	if (option_v) printf("%s\n", dirname);
	if (option_r) {
		char *fullname;
		asprintf(&fullname, "%s%s", option_r, dirname);
		rundir_body(fullname);
	} else {
		rundir_body(dirname);
	}
}

static void
runfile_body(const char *filename)
{
	char	*line;
	size_t	linelen;
	FILE *f = fopen(filename, "r");

	if (option_v > 2) {
		fprintf(stderr, "runfile_body(\"%s\")\n", filename);
	}
	if (!f) {
		if (option_v > 1) {
			fprintf(stderr, "%s: fopen(%s, \"r\") => %s\n", __progname, filename, strerror(errno));
		}
		return;
	}

	for (line = NULL; getline(&line, &linelen, f) >= 0; ) {
		if (line[0] == '#') continue;
		if (!strlen(line)) continue;
		if (!strncmp(line, "include ", strlen("include "))) {
//TODO
			continue;
		}
		if (line[0] != '/') {	
			if (option_v > 1) {
				fprintf(stderr, "%s: illegal line in %s: %s\n", __progname, filename, line);
			}
			continue;
		}
		if (line[strlen(line) - 1] == '\n') line[strlen(line) - 1] = 0;
		rundir(line);
	}

	fclose(f);
}

static void
runfile(const char *filename)
{
	if (option_v > 2) {
		fprintf(stderr, "runfile(\"%s\")\n", filename);
	}
	if (option_r) {
		char *fullname;
		asprintf(&fullname, "%s%s", option_r, filename);
		runfile_body(fullname);
	} else {
		runfile_body(filename);
	}
}

static void
help(void)
{
	printf("Use %s [parameters]\n", __progname);
	printf("	-v --verbose		Generate verbose messages.\n");
	printf("	-h --help		Print this help.\n");
	printf("	-N --no-cache		Don't build cache; dummy parameter.\n");
	printf("	-X --no-link		Don't update symbolic links; steamless run.\n");
	printf("	-r --root ROOT		Change to and use ROOT as root directory.\n");
	printf("	-f --config CONF	Use CONF as configuration file.\n");
	printf("	-n --directories	Only process directories on the command line.\n");
	printf("	-l --libraries		Manually link individual libraries.\n");
}

/* Definitions of arguments for argp functions.  */
static struct option long_options[] = {
	{ "verbose",	0,	0,	'v'},
	{ "version",	0,	0,	'V'},
	{ "help",	0,	0,	'h'},
	{ "no-cache",	0,	0,	'N'},
	{ "no-link",	0,	0,	'X'},
	{ "root",	1,	0,	'r'},
	{ "config",	1,	0,	'f'},
	{ "directory",	0,	0,	'n'},
	{ "file",	0,	0,	'l'},
	{ NULL,		0,	0, 	 0 }
};

int
main(int argc, char **argv)
{
	for(;;) {
		int c;
		int option_index = 0;

		if ((c = getopt_long(argc, argv, "vhNXr:f:nl", long_options, &option_index)) < 0) break;
		switch (c) {
			case 'v':
				++option_v;
				break;
			case 'V':
				printf("xldconfig %s\n", VERSION);
				exit(0);
			case 'h':
				help();
				exit(0);
			case 'N':
				break;
			case 'X':
				option_X = 1;
				break;
			case 'r':
				option_r = optarg;
				break;
			case 'f':
				option_f = optarg;
				break;
			case 'n':
				option_n = 1;
				break;
			case 'l':
				option_l = 1;
				break;
			default:
				fprintf(stderr, "%s: getopt_long(*, *, *, *, *) => %3o\n", __progname, c);
				exit(1);
		}
	}

	if (elf_version(EV_CURRENT) == EV_NONE) {
		fprintf(stderr, "%s: elf_version(EV_CURRENT) => %s\n", __progname, elf_errmsg(-1));
		exit(1);
	}

	if (!(magic = magic_open(0))) {
		fprintf(stderr, "%s: magic_open(*) => %s\n", __progname, magic_error(magic));
		exit(1);
	}

	if (magic_load(magic, NULL) < 0) {
		fprintf(stderr, "%s: magic_load(*, *) => %s\n", __progname, magic_error(magic));
		exit(1);
	}

	setlinebuf(stdout);

	if (option_l) {
		while (optind < argc) runsingle(argv[optind++]);
	} else if (option_n) {
		while (optind < argc) rundir(argv[optind++]);
	} else {
		if (argc != optind) {
			fprintf(stderr, "%s: illegal args count\n", __progname);
			exit(1);
		}

		if (option_f) {
			runfile(option_f);
		} else {
			runfile("/etc/ld.so.conf");
			rundir("/lib");
			rundir("/lib64");
			rundir("/usr/lib");
			rundir("/usr/lib64");
		}
	}

	exit(0);
}
