/*******************************************************************/
/*  sltdl: a surrogate ltdl implementation                         */
/*  Copyright (C) 2019 SysDeer Technologies, LLC                   */
/*  Released under the Standard MIT License; see COPYING.SLTDL.    */
/*******************************************************************/

#include <glob.h>
#include <limits.h>
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

#include <sltdl/sltdl.h>
#include "sltdl_core.h"
#include "sltdl_module.h"

#ifdef  O_EXEC
#define SLTDL_MODULE_OPEN_OPTIONS (O_CLOEXEC|O_EXEC)
#else
#define SLTDL_MODULE_OPEN_OPTIONS (O_CLOEXEC)
#endif

static int     lt_status;
static off_t   lt_plen;
static off_t   lt_plocs;
static char *  lt_upath;
static char *  lt_vpath;
static char ** lt_vmark;
static char ** lt_pathv;

static struct lt_modctx * lt_modv_head;
static struct lt_modctx * lt_modv_tail;

static struct lt_modctx * lt_modv_next;
static struct lt_modctx * lt_modv_cap;

static int lt_setstatus(int ret, int status)
{
	lt_status = status;
	return ret;
}

const char * lt_dlgetsearchpath(void)
{
	return lt_upath;
}

static int lt_dlsetsearchpath_locked(const char * path)
{
	const char *    ch;
	char *          uch;
	char *          vch;
	char **         pathv;
	off_t           elements;

	if (path[0] != '/')
		return lt_setstatus(1,SLTDL_ERR_PATH_INVALID_FIRST_CHAR);

	elements = 1;

	for (ch=&path[1]; *ch; ch++) {
		if (*ch == ':') {
			if (ch[1] != '/')
				return lt_setstatus(1,SLTDL_ERR_PATH_INVALID_FIRST_CHAR);

			elements++;
		}
	}

	if (++elements > lt_plocs) {
		if (lt_pathv) {
			free(lt_pathv);
			lt_pathv = 0;
		}

		lt_plocs  = elements;
		lt_plocs += 0x3f;
		lt_plocs |= 0x3f;
		lt_plocs ^= 0x3f;

		if (!(lt_pathv = calloc(lt_plocs,sizeof(char *))))
			return lt_setstatus(1,SLTDL_ERR_SYSTEM_ERROR);
	}

	if ((ch - path) > lt_plen) {
		if (lt_upath) {
			free(lt_upath);
			lt_upath = 0;
		}

		lt_plen = ((ch - path + 1) <= 0x800)
			? 0x800 : ch - path + 1;

		lt_plen += 0x7ff;
		lt_plen |= 0x7ff;
		lt_plen ^= 0x7ff;
		if (!(lt_upath = calloc(2*lt_plen,1)))
			return lt_setstatus(1,SLTDL_ERR_SYSTEM_ERROR);

		lt_vpath = &lt_upath[lt_plen];
	}

	pathv    = lt_pathv;
	*pathv++ = lt_vpath;

	for (ch=path, uch=lt_upath, vch=lt_vpath; *ch; ch++) {
		if (*ch == ':') {
			*uch++ = ch[0];
			*uch++ = ch[1];

			*vch++ = 0;
			*pathv = vch;
			*vch++ = ch[1];

			ch++;
			pathv++;
		} else {
			*uch++ = *ch;
			*vch++ = *ch;
		}
	}

	lt_vmark = pathv;

	return lt_setstatus(0,SLTDL_ERR_OK);
}

int lt_dlsetsearchpath(const char * path)
{
	int ret;
	lt_slock();
	ret = lt_dlsetsearchpath_locked(path);
	return lt_sunlock(ret,lt_status);
}

static int lt_dladdsearchdir_locked(const char * path)
{
	int          ret;
	const char * ch;
	char *       buf;
	off_t        alen;
	off_t        plen;

	if (path[0] != '/')
		return lt_setstatus(1,SLTDL_ERR_PATH_INVALID_FIRST_CHAR);

	for (ch=path; *ch; ch++)
		if (*ch == ':')
			return lt_setstatus(1,SLTDL_ERR_PATH_INVALID_SEPARATTOR_CHAR);

	alen = strlen(path);
	plen = strlen(lt_upath);

	/* no allocation needed? */
	if (!lt_pathv[lt_plocs - 2] && ((plen + 1 + alen + 1) <= lt_plen)) {
		lt_upath[plen] = ':';
		lt_vpath[plen] = 0;

		plen++;

		strcpy(&lt_upath[plen],path);
		strcpy(&lt_vpath[plen],path);

		*lt_vmark++ = &lt_vpath[plen];

		return lt_setstatus(0,SLTDL_ERR_OK);
	}

	/* (allocation needed) */
	if (!(buf = malloc(plen + 1 + alen + 1)))
		return lt_setstatus(1,SLTDL_ERR_SYSTEM_ERROR);

	sprintf(buf,"%s:%s",lt_upath,path);

	ret = lt_dlsetsearchpath_locked(buf);

	free(buf);

	return ret;
}

int lt_dladdsearchdir(const char * path)
{
	int ret;
	lt_slock();
	ret = lt_dladdsearchdir_locked(path);
	return lt_sunlock(ret,lt_status);
}

int lt_dlinsertsearchdir(const char * mark, const char * path)
{
	int          ret;
	const char * ch;
	char *       buf;
	char *       dst;
	off_t        alen;
	off_t        plen;
	off_t        slen;
	off_t        offset;
	char **      pathv;

	if (!mark)
		return lt_dladdsearchdir(path);

	lt_slock();

	if (path[0] != '/')
		return lt_sunlock(1,SLTDL_ERR_PATH_INVALID_FIRST_CHAR);

	for (ch=path; *ch; ch++)
		if (*ch == ':')
			return lt_sunlock(1,SLTDL_ERR_PATH_INVALID_SEPARATTOR_CHAR);

	alen = strlen(path);
	plen = strlen(lt_upath);

	if ((mark < lt_upath) || (mark >= &lt_upath[plen]))
		return lt_sunlock(1,SLTDL_ERR_PATH_INVALID_MARK);

	if ((mark > lt_upath) && (mark[-1] != ':'))
		return lt_sunlock(1,SLTDL_ERR_PATH_INVALID_MARK);

	mark = &lt_vpath[mark - lt_upath];

	/* no allocation needed? */
	if (!lt_pathv[lt_plocs - 2] && ((plen + 1 + alen + 1) <= lt_plen)) {
		for (pathv=lt_vmark; pathv>=lt_pathv; pathv--) {
			slen    = strlen(pathv[-1]);
			offset  = pathv[-1] - lt_vpath;
			offset += alen + 1;

			memcpy(&lt_upath[offset],pathv[-1],slen);
			memcpy(&lt_vpath[offset],&lt_upath[offset],slen);

			if (pathv < lt_vmark)
				lt_upath[offset + slen] = ':';

			pathv[0] = pathv[-1] + alen + 1;

			if (pathv[-1] == mark) {
				offset = mark - lt_vpath;
				strcpy(&lt_vpath[offset],path);
				memcpy(&lt_upath[offset],path,alen);
				lt_upath[offset+alen] = ':';
				lt_vmark++;
				return lt_sunlock(0,SLTDL_ERR_OK);
			}
		}
	}

	/* (allocation needed) */
	if (!(buf = malloc(plen + 1 + alen + 1)))
		return lt_sunlock(1,SLTDL_ERR_SYSTEM_ERROR);

	for (dst=buf, pathv=lt_pathv; *pathv; pathv++) {
		if (*pathv == mark)
			dst += sprintf(dst,"%s:",path);

		if (pathv[1])
			dst += sprintf(dst,"%s:",*pathv);
		else
			dst += sprintf(dst,"%s",*pathv);
	}

	ret = lt_dlsetsearchpath_locked(buf);

	free(buf);

	return lt_sunlock(ret,lt_status);
}

static int lt_dlpathopen_locked(
	const char *  module,
	const char ** extv,
	char **       mpath)
{
	int           fdat;
	int           fdmod;
	char **       ppath;
	const char ** pext;
	size_t        plen;
	size_t        mlen;
	size_t        elen;
	char          path[1024];

	if ((mlen = strlen(module)) >= sizeof(path))
		return lt_setstatus(-1,SLTDL_ERR_PATH_INVALID_LEN);

	memcpy(path,module,mlen);

	if (path[0] == '/') {
		if ((fdmod = open(path,SLTDL_MODULE_OPEN_OPTIONS,0)) >= 0) {
			if (mpath) {
				if (!(*mpath = realpath(path,0))) {
					close(fdmod);
					return lt_setstatus(-1,SLTDL_ERR_SYSTEM_ERROR);
				}
			}

			return lt_setstatus(fdmod,SLTDL_ERR_OK);
		}

		return lt_setstatus(-1,SLTDL_ERR_PATH_NO_ENTRY);
	}

	for (ppath=lt_pathv; ppath && *ppath; ppath++) {
		fdat = open(*ppath,O_RDONLY|O_DIRECTORY|O_CLOEXEC,0);

		if (fdat >= 0) {
			for (pext=extv; *pext; pext++) {
				if (mlen + (elen = strlen(*pext)) >= (sizeof(path))) {
					close(fdat);
					return lt_setstatus(-1,SLTDL_ERR_PATH_INVALID_LEN);
				}

				memcpy(&path[mlen],*pext,elen);
				path[mlen+elen] = 0;

				fdmod = openat(fdat,path,SLTDL_MODULE_OPEN_OPTIONS,0);

				if (fdmod >= 0) {
					if (mpath) {
						plen  = strlen(*ppath);
						plen += mlen + 1 + elen + 1;

						if (!(*mpath = malloc(plen))) {
							close(fdat);
							close(fdmod);
							return lt_setstatus(-1,SLTDL_ERR_SYSTEM_ERROR);
						}

						sprintf(*mpath,"%s/%s",*ppath,path);
					}

					close(fdat);
					return lt_setstatus(fdmod,SLTDL_ERR_OK);
				}
			}

			close(fdat);
		}
	}

	return lt_setstatus(-1,SLTDL_ERR_PATH_NO_ENTRY);
}

int lt_dlpathopen(const char * module, const char ** extv)
{
	int ret;
	lt_slock();
	ret = lt_dlpathopen_locked(module,extv,0);
	return lt_sunlock(ret,lt_status);
}

static struct lt_modctx * lt_dlopen_locked(
	const struct lt_symdef *  symtbl,
	const char *              module,
	const char **             extv,
	int                       mode)
{
	int                       fdmod;
	char *                    mpath;
	void *                    maddr;
	struct lt_modctx *        modctx;
	struct lt_modctx *        modctx_buf;

	/* init */
	mpath = 0;
	maddr = 0;

	/* path open (module) */
	if (module) {
		fdmod = lt_dlpathopen_locked(
			module,extv,&mpath);

		if (fdmod >= 0) {
			close(fdmod);

		} else if (!(mpath = strdup(module))) {
			lt_setstatus(0,SLTDL_ERR_SYSTEM_ERROR);
			return 0;
		}
	}

	/* entry alloc */
	if (lt_modv_next == lt_modv_cap) {
		if (!(modctx_buf = calloc(64,sizeof(*modctx)))) {
			free(mpath);
			lt_setstatus(0,SLTDL_ERR_SYSTEM_ERROR);
			return 0;
		}

		lt_modv_next = modctx_buf;
		lt_modv_cap  = &lt_modv_next[64];
	}

	/* dlopen (module), dlopen (self) */
	if (!symtbl && !(maddr = dlopen(mpath,mode))) {
		free(mpath);
		lt_setstatus(0,SLTDL_ERR_DLFCN_ERROR);
		return 0;
	}

	/* module already dlopen'ed? */
	for (modctx=lt_modv_head; maddr && modctx; modctx=modctx->mnext) {
		if (modctx->maddr == maddr) {
			free(mpath);
			modctx->mrefs++;
			return modctx;
		}
	}

	/* symbol table already preloaded? */
	for (modctx=lt_modv_head; symtbl && modctx; modctx=modctx->mnext)
		if (modctx->symtbl == symtbl)
			return modctx;

	/* module or symtbl entry */
	modctx          = lt_modv_next;
	modctx->symtbl  = symtbl;
	modctx->maddr   = maddr;
	modctx->mpath   = mpath;
	modctx->mrefs   = 1;
	lt_modv_next++;

	/* add to list */
	if (lt_modv_tail) {
		lt_modv_tail->mnext = modctx;
		lt_modv_tail        = modctx;
	} else {
		lt_modv_head        = modctx;
		lt_modv_tail        = modctx;
	}

	/* all done */
	return modctx;
}

struct lt_modctx * lt_dlopen(const char * module)
{
	char *             dot;
	size_t             slen;
	struct lt_modctx * modctx;
	const char *       extv[2] = {"",0};
	char               dsobuf[PATH_MAX];

	lt_slock();

	if (module && (slen = strlen(module)) >= PATH_MAX) {
		lt_setstatus(0,SLTDL_ERR_SYSTEM_ERROR);
		lt_sunlock(0,lt_status);
		return 0;
	}

	if (module) {
		strcpy(dsobuf,module);
		module = dsobuf;
	}

	if ((dot = strrchr(dsobuf,'.')))
		if (!strcmp(dot,".la"))
			if ((slen = (dot - dsobuf)))
				if (PATH_MAX - slen > strlen(OS_LIB_SUFFIX))
					strcpy(dot,OS_LIB_SUFFIX);

	modctx = lt_dlopen_locked(0,module,extv,RTLD_NOW);
	lt_sunlock(0,lt_status);
	return modctx;
}

struct lt_modctx * lt_dlopenext(const char * module)
{
	struct lt_modctx * modctx;
	const char *       extv[3] = {"",OS_LIB_SUFFIX,0};

	lt_slock();
	modctx = lt_dlopen_locked(0,module,extv,RTLD_NOW);
	lt_sunlock(0,lt_status);
	return modctx;
}

struct lt_modctx * lt_dlopenadvise(const char * module, struct lt_modctl * modctl)
{
	(void)modctl;
	return lt_dlopenext(module);
}

void * lt_dlsym(struct lt_modctx * modctx, const char * symname)
{
	const struct lt_symdef * sym;

	if (modctx->maddr)
		return dlsym(modctx->maddr,symname);

	for (sym=modctx->symtbl; sym->name; sym++)
		if (!strcmp(sym->name,symname))
			return sym->address;

	return 0;
}

int lt_dlclose(struct lt_modctx * modctx)
{
	struct lt_modctx * pmod;

	lt_slock();

	for (pmod=lt_modv_head; pmod ; pmod=pmod->mnext) {
		if (pmod == modctx) {
			if (pmod->mrefs) {
				pmod->mrefs--;
				return lt_sunlock(0,SLTDL_ERR_OK);
			}

			return lt_sunlock(-1,SLTDL_ERR_MODULE_REF_COUNT);
		}
	}

	return lt_sunlock(-1,SLTDL_ERR_MODULE_PTR_INVALID);
}

static int lt_dlforeachfile_one(
	const char *  path,
	int         (*fusr)(const char * direntry, void * data),
	void *        data)
{
	int           ret;
	char **       argv;
	glob_t        globbuf;
	char          pathbuf[PATH_MAX];

	globbuf.gl_pathc = 0;
	globbuf.gl_pathv = 0;
	globbuf.gl_offs  = 0;

	ret = snprintf(
		pathbuf,
		sizeof(pathbuf),
		"%s/" "*" OS_LIB_SUFFIX,
		path);

	if (ret >= PATH_MAX) {
		errno = EINVAL;
		return -1;
	}

	if ((ret = glob(pathbuf,0,0,&globbuf)) < 0)
		return ret;

	for (argv=globbuf.gl_pathv; argv && *argv; argv++) {
		if ((ret = fusr(*argv,data))) {
			globfree(&globbuf);
			return ret;
		}
	}

	globfree(&globbuf);

	return 0;
}

int lt_dlforeachfile(
	const char *  path,
	int         (*fusr)(const char * direntry, void * data),
	void *        data)
{
	int           ret;
	char **       pathv;
	char *        spath;
	char *        mark;
	char *        ch;

	if (path) {
		if (!(spath = strdup(path)))
			return -1;

		mark = spath;
		ch   = spath;

		for (; *ch; ) {
			for (ch=mark; ch[0] && ch[0] != ':'; ch++)
				(void)0;

			if (ch[0])
				*ch++ = '\0';

			if ((ret = lt_dlforeachfile_one(mark,fusr,data))) {
				free(spath);
				return ret;
			}

			mark = ch;
		}

		free(spath);
	} else {
		for (pathv=lt_pathv; pathv && *pathv; pathv++)
			if ((ret = lt_dlforeachfile_one(*pathv,fusr,data)))
				return ret;
	}

	return 0;
}

int lt_dlloader_add(const struct lt_dlentry * ldr)
{
	(void)ldr;
	return -1;
}

const struct lt_dlentry * lt_dlloader_find(const char * ldrname)
{
	(void)ldrname;
	return 0;
}

static int lt_dlpreload_remove_locked(const struct lt_symdef * symtbl)
{
	struct lt_modctx * pmod;

	for (pmod=lt_modv_head; pmod ; pmod=pmod->mnext) {
		if (pmod->symtbl == symtbl) {
			if (pmod->mrefs > 0) {
				pmod->mrefs--;
				return 0;
			}

			return SLTDL_ERR_MODULE_REF_COUNT;
		}
	}

	return SLTDL_ERR_MODULE_PTR_INVALID;
}

static void lt_dlpreload_reset_locked(void)
{
	struct lt_modctx * pmod;

	for (pmod=lt_modv_head; pmod ; pmod=pmod->mnext)
		if (pmod->symtbl && (pmod->mrefs > 0))
			pmod->mrefs = 0;
}

static int lt_dlpreload_modctl_locked(
	const struct lt_symdef *  symtbl,
	enum sltdl_modctl         op)
{
	struct lt_modctx * modctx;
	switch (op) {
		case SLTDL_MODCTL_PRELOAD_ADD:
			return lt_dlopen_locked(symtbl,0,0,1)
				? 0 : -1;

		case SLTDL_MODCTL_PRELOAD_REMOVE:
			return lt_dlpreload_remove_locked(symtbl)
				? 0 : -1;

		case SLTDL_MODCTL_PRELOAD_DEFAULT:
			if (!(modctx = lt_dlopen_locked(symtbl,0,0,0)))
				return -1;

			modctx->mrefs = -1;
			return 0;

		case SLTDL_MODCTL_PRELOAD_RESET:
			lt_dlpreload_reset_locked();
			break;

		default:
			break;
	}

	return 0;
}

static int lt_dlpreload_modctl_impl(
	const struct lt_symdef *  symtbl,
	enum sltdl_modctl         op)
{
	int ret;

	lt_slock();

	ret = lt_dlpreload_modctl_locked(
		symtbl,op);

	return lt_sunlock((-1)*!!ret,ret);
}

int lt_dlpreload_modctl(
	const struct lt_symdef *  symtbl,
	enum sltdl_modctl         op)
{
	return lt_dlpreload_modctl_impl(symtbl,op);
}

int lt_dlpreload_default(const struct lt_symdef * symtbl)
{
	return lt_dlpreload_modctl_impl(symtbl,SLTDL_MODCTL_PRELOAD_DEFAULT);
}
