/*
 * icasefile - LD_PRELOADable wrapper to emulate case-insensitive filesystem
 * Copyright (C) 2009-2011  Tommi Saviranta  <wnd@iki.fi>
 *
 * 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 3 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>.
 *
 *
 * Version history:
 *  xx xxx 2009 - First version: HomeworldSDL is now working
 *  xx xxx 2010 - First improvement: something to do with Cave Story (mods?)
 *  25 Feb 2011 - Cleaned up the code
 *  27 Feb 2011 - Implemented ~20 more functions, including stat(),
 *              - Debug level is now configurable (instead of simple on/off)
 *
 *
 * Note to self:
 *   gcc -Wall -fpic -shared -ldl -o icasefile-64.so icasefile.c
 */


#include <dirent.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>

#define LIBC_SO	"libc.so.6"

#define DEBUG_OFF	0x0000
#define DEBUG_RESULT	0x0001
#define DEBUG_PROCESS	0x0002
#define DEBUG_DLOPEN	0x0004


/*
 * fcntl.h
 *	open(pathname, flags)				test
 *	open(pathname, flags, mode)			test
 *	openat(dirfd, pathname, flags)			test
 *	openat(dirfd, pathname, flags, mode)		test
 *	creat(pathname, mode)				test
 *
 * stdio.h
 *	remove(filename)				test
 *	rename(old, new)				test
 *	renameat(oldfd, old, newfd, new)		test
 *	tempnam(dir, pfx)				test
 *	fopen(path, mode)				test
 *	freopen(path, mode, stream)			test
 *
 * unistd.h
 *	access(pathname, mode)				test
 *	euidaccess(pathname, mode)			test
 *	eaccess(pathname, mode)				test
 *	faccessat(dirfd, pathname, mode, flags)		test
 *	pathconf(path, name)				test
 *	acct(filename)					test
 *	chown(path, owner, group)			test
 *	lchown(path, owner, group)			test
 *	fchownat(dirfd, pathname, owner, group, flags)	test
 *	truncate(path. length)				test
 *
 * sys/stat.h
 *	stat(path, buf)					test
 *	lstat(path, buf)				test
 *	chmod(path, mode)				test
 *	mkdir(pathname, mode)				test
 *	mkdirat(dirfd, pathname, mode)			test
 *	mknod(pathname, mode, dev)			test
 *	mknodat(dirfd, pathname, mode, dev)		test
 */



#define DIR_DELIM	'/'



#define func_wrapper_head(func, lib, ret, fn, ...) \
	ret \
	func(__VA_ARGS__) \
	{ \
		static ret (*func##_real)(__VA_ARGS__) = NULL; \
		char *realfn = get_file(fn); \
		ret r; \
		if (! func##_real) { \
			void *libptr = dlopen(lib, RTLD_NOW); \
			func##_real = dlsym(libptr, #func); \
			if (debug(DEBUG_DLOPEN)) { \
				fprintf(stderr, "icasefile: " #func " = %p\n", \
						func##_real); \
			} \
		}
#define func_wrapper_call(func, ...) \
		\
		r = func##_real(__VA_ARGS__); \
		free(realfn); \
		\
		return r; \
	}


#define strdup_safe(DEST, SRC) \
{ \
	DEST = malloc(strlen(SRC) + 16); \
	strcpy(DEST, SRC); \
	/* DEST = strdup(SRC); */ \
	if (! DEST) { \
		fprintf(stderr, "out of memory!\n"); \
		abort(); \
	} \
}



static int
debug(int flag)
{
	static int debug = -1;
	if (debug == -1) {
		debug = getenv("ICASE_DEBUG")
			? atoi(getenv("ICASE_DEBUG"))
			: DEBUG_OFF;
	}
	return debug & flag;
}



static int
statreal(const char *path, struct stat *buf)
{
#ifdef __GNUC__
	static int (*stat_real)(int ver, const char *path,
			struct stat *buf) = NULL;

	if (! stat_real) {
		void *lib = dlopen(LIBC_SO, RTLD_NOW);
		stat_real = dlsym(lib, "__xstat");
		if (debug(DEBUG_DLOPEN)) {
			fprintf(stderr, "icasefile: __xstat (statreal) = %p\n",
					stat_real);
		}
	}

	return stat_real(_STAT_VER, path, buf);
#else
	return stat(path, buf);
#endif
}



#if 0
static char *
get_dirname(const char *file)
{
	char *dir;
	char *r;
	int len;

	if (file[0] == '\0') {
		return NULL;
	}

	r = strrchr(file, (int) DIR_DELIM);
	if (! r) {
		return NULL;
	}

	len = file - r;
	dir = malloc(len + 1);
	if (! dir) {
		fprintf(stderr, "out of memory!\n");
		abort();
	}

	memcpy(dir, file, len);
	dir[len] = '\0';

	return dir;
}
#endif



static char **
split_string(const char *input, char delimchr, int *len, char **freeptr)
{
	char delim[2] = { delimchr, '\0' };
	const char *ptr;
	char **array;
	char *token, *tokptr;
	int absolute;
	int i;

	if (! input) {
		return NULL;
	}

	*len = 1;
	for (ptr = input; *ptr != '\0'; ptr++) {
		if (*ptr == delimchr) {
			(*len)++;
		}
	}

	array = malloc(sizeof(char *) * *len);
	if (array == NULL) {
		fprintf(stderr, "out of memory!\n");
		abort();
	}

	*freeptr = strdup(input);
	if (*freeptr == NULL) {
		fprintf(stderr, "out of memory!\n");
		free(array);
		abort();
	}

	if (*freeptr[0] == delimchr) {
		absolute = 1;
		i = 1;
	} else {
		absolute = 0;
		i = 0;
	}

	token = strtok_r(*freeptr, delim, &tokptr);
	while (token) {
		array[i] = token;
		i++;
		token = strtok_r(NULL, delim, &tokptr);
	}

	if (absolute) {
		*freeptr[0] = '\0';
		array[0] = *freeptr;
	}

	return array;
}



static int
join_string(char *dest, char **src, char delimchr, int n)
{
	char delim[2] = { delimchr, '\0' };
	int i;

	if (n == 0) {
		dest[0] = '.';
		dest[1] = '\0';
		return 0;
	}

	dest[0] = '\0';
	for (i = 0; i < n; i++) {
		dest = strcat(dest, src[i]);
		dest = strcat(dest, delim);
	}

	i = strlen(dest);
	if (i >= 0) {
		i--;
		dest[i] = '\0';
	}

	return i;
}



char *
find_file(const char *file)
{
	struct stat statbuf;

	char **file_array;
	char *file_free;
	int file_len;

	char *work;
	int i;
	int r = -1;

	strdup_safe(work, file);
	file_array = split_string(file, DIR_DELIM, &file_len, &file_free);

	if (debug(DEBUG_PROCESS)) {
		for (i = 0; i < file_len; i++) {
			fprintf(stderr, "%d: %s\n", i, file_array[i]);
		}
		fprintf(stderr, "-- moving to left...\n");
	}

	for (i = file_len - 1; i >= 0; i--) {
		join_string(work, file_array, DIR_DELIM, i);
		if (debug(DEBUG_PROCESS)) {
			fprintf(stderr, "%d: %s\n", i, work);
		}
		r = statreal(work, &statbuf);
		if (r == 0) {
			break;
		}
	}

	if (r != 0) {
		free(file_free);
		free(file_array);

		if (debug(DEBUG_PROCESS)) {
			fprintf(stderr, "could not find any directory!\n");
		}
		strcpy(work, file);
		return work;
	}

	for (; i < file_len; i++) {
		char delim[2] = { DIR_DELIM, '\0' };
		DIR *dir;
		struct dirent entry;
		struct dirent *result;


		join_string(work, file_array, DIR_DELIM, i);
		strcat(work, delim);
		strcat(work, file_array[i]);

		r = statreal(work, &statbuf);
		if (r == 0) {
			if (debug(DEBUG_PROCESS)) {
				fprintf(stderr, "%s: perfect match\n", work);
			}
			continue;
		}


		join_string(work, file_array, DIR_DELIM, i);
		if (debug(DEBUG_PROCESS)) {
			fprintf(stderr, "now scanning '%s' for '%s'\n",
					work,
					file_array[i]);
		}

		dir = opendir(work);
		if (! dir) {
			fprintf(stderr, "%s: no such directory!?\n", work);
			abort();
		}

		while (1) {
			r = readdir_r(dir, &entry, &result);
			if (r != 0) {
				fprintf(stderr, "readdir: error!\n");
				abort();
			} else if (result == NULL) {
				closedir(dir);
				free(file_free);
				free(file_array);
				if (debug( DEBUG_PROCESS)) {
					fprintf(stderr, "-> no match!\n");
				}
				strcpy(work, file);
				return work;
			}

			if (strcasecmp(entry.d_name, file_array[i]) == 0) {
				strcpy(file_array[i], entry.d_name);
				break;
			}
		}

		closedir(dir);
	}

	join_string(work, file_array, DIR_DELIM, file_len);
	free(file_free);
	free(file_array);

	return work;
}



static char *
get_file(const char *in)
{
	struct stat buf;
	int r;
	char *out;

	r = statreal(in, &buf);
	if (r == 0) {
		strdup_safe(out, in);
		return out;
	} else {
		out = find_file(in);
		if (debug(DEBUG_RESULT)) {
			if (strcmp(in, out)) {
				fprintf(stderr, "icasefile: %s: found %s\n",
						out, in);
			} else if (statreal(out, &buf) != 0) {
				fprintf(stderr, "icasefile: %s: not found\n",
						in);
			}
		}
		return out;
	}
}



/* wrappers */



/* fcntl.h */
/* ------------------------------------------------------------------ */



int
open(const char *pathname, int flags, ...)
{
	static int (*open_real)(const char *pathname, int flags, ...) = NULL;
	char *fn = get_file(pathname);
	int r;

	if (! open_real) {
		void *lib = dlopen(LIBC_SO, RTLD_NOW);
		open_real = dlsym(lib, "open");
		if (debug(DEBUG_DLOPEN)) {
			fprintf(stderr, "icasefile: open = %p\n", open_real);
		}
	}

	if (flags & O_CREAT) {
		va_list args;
		mode_t mode;

		va_start(args, flags);
		mode = va_arg(args, mode_t);
		va_end(args);

		r = open_real(fn, flags, mode);
	} else {
		r = open_real(fn, flags);
	}
	free(fn);

	return r;
}



int
openat(int dirfd, const char *pathname, int flags, ...)
{
	static int (*openat_real)(int dirfd, const char *pathname,
			int flags, ...) = NULL;
	char *fn = get_file(pathname);
	int r;

	if (! openat_real) {
		void *lib = dlopen(LIBC_SO, RTLD_NOW);
		openat_real = dlsym(lib, "openat");
		if (debug(DEBUG_DLOPEN)) {
			fprintf(stderr, "icasefile: openat = %p\n", openat_real);
		}
	}

	if (flags & O_CREAT) {
		va_list args;
		mode_t mode;

		va_start(args, flags);
		mode = va_arg(args, mode_t);
		va_end(args);

		r = openat_real(dirfd, fn, flags, mode);
	} else {
		r = openat_real(dirfd, fn, flags);
	}
	free(fn);

	return r;
}



func_wrapper_head(creat, LIBC_SO, int, pathname,
		const char *pathname, mode_t mode);
func_wrapper_call(creat, realfn, mode);



/* stdio.h */
/* ------------------------------------------------------------------ */



func_wrapper_head(remove, LIBC_SO, int, pathname,
		const char *pathname);
func_wrapper_call(remove, realfn);



int
rename(const char *oldpath, const char *newpath)
{
	static int (*rename_real)(const char *oldpath, const char *newpath) =
		NULL;
	char *oldpath_real = get_file(oldpath);
	char *newpath_real = get_file(newpath);
	int r;

	if (! rename_real) {
		void *lib = dlopen(LIBC_SO, RTLD_NOW);
		rename_real = dlsym(lib, "rename");
		if (debug(DEBUG_DLOPEN)) {
			fprintf(stderr, "icasefile: rename = %p\n", rename_real);
		}
	}

	r = rename_real(oldpath_real, newpath_real);
	free(newpath_real);
	free(oldpath_real);

	return r;
}



int
renameat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath)
{
	static int (*renameat_real)(int olddirfd, const char *oldpath,
			int newdirfd, const char *newpath) =
		NULL;
	char *oldpath_real = get_file(oldpath);
	char *newpath_real = get_file(newpath);
	int r;

	if (! renameat_real) {
		void *lib = dlopen(LIBC_SO, RTLD_NOW);
		renameat_real = dlsym(lib, "renameat");
		if (debug(DEBUG_DLOPEN)) {
			fprintf(stderr, "icasefile: renameat = %p\n",
					renameat_real);
		}
	}

	r = renameat_real(olddirfd, oldpath_real, newdirfd, newpath_real);
	free(newpath_real);
	free(oldpath_real);

	return r;
}



func_wrapper_head(tempnam, "libc.so.6", char *, dir,
		const char *dir, const char *pfx);
func_wrapper_call(tempnam, realfn, pfx);



func_wrapper_head(fopen, "libc.so.6", FILE *, path,
		const char *path, const char *mode);
func_wrapper_call(fopen, realfn, mode);



func_wrapper_head(freopen, "libc.so.6", FILE *, path,
		const char *path, const char *mode, FILE *stream);
func_wrapper_call(freopen, realfn, mode, stream);




/* unistd.h */
/* ------------------------------------------------------------------ */



func_wrapper_head(access, "libc.so.6", int, pathname,
		const char *pathname, int mode);
func_wrapper_call(access, realfn, mode);



func_wrapper_head(euidaccess, "libc.so.6", int, pathname,
		const char *pathname, int mode);
func_wrapper_call(euidaccess, realfn, mode);



func_wrapper_head(eaccess, "libc.so.6", int, pathname,
		const char *pathname, int mode);
func_wrapper_call(eaccess, realfn, mode);



func_wrapper_head(faccessat, "libc.so.6", int, pathname,
		int dirfd, const char *pathname, int mode, int flags);
func_wrapper_call(faccessat, dirfd, realfn, mode, flags);



func_wrapper_head(pathconf, "libc.so.6", long, path,
		const char *path, int name);
		/* manual page says: char *path, int name */
func_wrapper_call(pathconf, realfn, name);



func_wrapper_head(acct, "libc.so.6", int, filename,
		const char *filename);
func_wrapper_call(acct, realfn);



func_wrapper_head(chown, "libc.so.6", int, path,
		const char *path, uid_t owner, gid_t group);
func_wrapper_call(chown, realfn, owner, group);



func_wrapper_head(lchown, "libc.so.6", int, path,
		const char *path, uid_t owner, gid_t group);
func_wrapper_call(lchown, realfn, owner, group);



func_wrapper_head(fchownat, "libc.so.6", int, pathname,
		int dirfd, const char *pathname, uid_t owner, gid_t group,
		int flags);
func_wrapper_call(fchownat, dirfd, realfn, owner, group, flags);



func_wrapper_head(truncate, "libc.so.6", int, path,
		const char *path, off_t length);
func_wrapper_call(truncate, realfn, length);




/* sys/stat.h */
/* ------------------------------------------------------------------ */



#ifdef __GNUC__
int
__xstat(int ver, const char *pathname, struct stat *buf)
{
	static int (*stat_real)(int ver, const char *path,
			struct stat *buf) = NULL;
	char *fn = get_file(pathname);
	int r;

	if (! stat_real) {
		void *lib = dlopen(LIBC_SO, RTLD_NOW);
		stat_real = dlsym(lib, "__xstat");
		if (debug(DEBUG_DLOPEN)) {
			fprintf(stderr, "icasefile: stat (__xstat) = %p\n",
					stat_real);
		}
	}

	r = stat_real(_STAT_VER, fn, buf);
	free(fn);

	return r;
}



int
__lxstat(int ver, const char *path, struct stat *buf)
{
	static int (*lstat_real)(int ver, const char *path,
			struct stat *buf) = NULL;
	char *realfn = get_file(path);
	int r;

	if (! lstat_real) {
		void *lib = dlopen(LIBC_SO, RTLD_NOW);
		lstat_real = dlsym(lib, "__lxstat");
		if (debug(DEBUG_DLOPEN)) {
			fprintf(stderr, "icasefile: lstat (__lxstat) = %p\n",
					lstat_real);
		}
	}

	r = lstat_real(ver, realfn, buf);
	free(realfn);

	return r;
}
#endif



func_wrapper_head(chmod, "libc.so.6", int, path,
		const char *path, mode_t mode);
func_wrapper_call(chmod, realfn, mode);



func_wrapper_head(mkdir, "libc.so.6", int, path,
		const char *path, mode_t mode);
func_wrapper_call(mkdir, realfn, mode);



func_wrapper_head(mkdirat, "libc.so.6", int, path,
		int dirfd, const char *path, mode_t mode);
func_wrapper_call(mkdirat, dirfd, realfn, mode);



func_wrapper_head(mknod, "libc.so.6", int, pathname,
		const char *pathname, mode_t mode, dev_t dev);
func_wrapper_call(mknod, realfn, mode, dev);



func_wrapper_head(mknodat, "libc.so.6", int, pathname,
		int dirfd, const char *pathname, mode_t mode, dev_t dev);
func_wrapper_call(mknodat, dirfd, realfn, mode, dev);



/* ------------------------------------------------------------------ */



#if 0
int
main(int argc, char **argv)
{
	char *ret = find_file(argv[1]);
	printf("%s\n", ret);
	return 0;
}
#endif
