/*
  fuse subdir module: offset paths with a base directory
  Copyright (C) 2007  Miklos Szeredi <miklos@szeredi.hu>

  This program can be distributed under the terms of the GNU LGPLv2.
  See the file COPYING.LIB
*/

#include <config.h>

#include <fuse.h>
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>
#include <errno.h>

struct subdir {
	char *base;
	size_t baselen;
	int rellinks;
	struct fuse_fs *next;
};

static struct subdir *subdir_get(void)
{
	return fuse_get_context()->private_data;
}

static int subdir_addpath(struct subdir *d, const char *path, char **newpathp)
{
	char *newpath = NULL;

	if (path != NULL) {
		unsigned newlen = d->baselen + strlen(path);

		newpath = malloc(newlen + 2);
		if (!newpath)
			return -ENOMEM;

		if (path[0] == '/')
			path++;
		strcpy(newpath, d->base);
		strcpy(newpath + d->baselen, path);
		if (!newpath[0])
			strcpy(newpath, ".");
	}
	*newpathp = newpath;

	return 0;
}

static int subdir_getattr(const char *path, struct stat *stbuf,
			  struct fuse_file_info *fi)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_getattr(d->next, newpath, stbuf, fi);
		free(newpath);
	}
	return err;
}

static int subdir_access(const char *path, int mask)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_access(d->next, newpath, mask);
		free(newpath);
	}
	return err;
}


static int count_components(const char *p)
{
	int ctr;

	for (; *p == '/'; p++);
	for (ctr = 0; *p; ctr++) {
		for (; *p && *p != '/'; p++);
		for (; *p == '/'; p++);
	}
	return ctr;
}

static void strip_common(const char **sp, const char **tp)
{
	const char *s = *sp;
	const char *t = *tp;
	do {
		for (; *s == '/'; s++);
		for (; *t == '/'; t++);
		*tp = t;
		*sp = s;
		for (; *s == *t && *s && *s != '/'; s++, t++);
	} while ((*s == *t && *s) || (!*s && *t == '/') || (*s == '/' && !*t));
}

static void transform_symlink(struct subdir *d, const char *path,
			      char *buf, size_t size)
{
	const char *l = buf;
	size_t llen;
	char *s;
	int dotdots;
	int i;

	if (l[0] != '/' || d->base[0] != '/')
		return;

	strip_common(&l, &path);
	if (l - buf < (long) d->baselen)
		return;

	dotdots = count_components(path);
	if (!dotdots)
		return;
	dotdots--;

	llen = strlen(l);
	if (dotdots * 3 + llen + 2 > size)
		return;

	s = buf + dotdots * 3;
	if (llen)
		memmove(s, l, llen + 1);
	else if (!dotdots)
		strcpy(s, ".");
	else
		*s = '\0';

	for (s = buf, i = 0; i < dotdots; i++, s += 3)
		memcpy(s, "../", 3);
}


static int subdir_readlink(const char *path, char *buf, size_t size)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_readlink(d->next, newpath, buf, size);
		if (!err && d->rellinks)
			transform_symlink(d, newpath, buf, size);
		free(newpath);
	}
	return err;
}

static int subdir_opendir(const char *path, struct fuse_file_info *fi)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_opendir(d->next, newpath, fi);
		free(newpath);
	}
	return err;
}

static int subdir_readdir(const char *path, void *buf,
			  fuse_fill_dir_t filler, off_t offset,
			  struct fuse_file_info *fi,
			  enum fuse_readdir_flags flags)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_readdir(d->next, newpath, buf, filler, offset,
				      fi, flags);
		free(newpath);
	}
	return err;
}

static int subdir_releasedir(const char *path, struct fuse_file_info *fi)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_releasedir(d->next, newpath, fi);
		free(newpath);
	}
	return err;
}

static int subdir_mknod(const char *path, mode_t mode, dev_t rdev)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_mknod(d->next, newpath, mode, rdev);
		free(newpath);
	}
	return err;
}

static int subdir_mkdir(const char *path, mode_t mode)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_mkdir(d->next, newpath, mode);
		free(newpath);
	}
	return err;
}

static int subdir_unlink(const char *path)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_unlink(d->next, newpath);
		free(newpath);
	}
	return err;
}

static int subdir_rmdir(const char *path)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_rmdir(d->next, newpath);
		free(newpath);
	}
	return err;
}

static int subdir_symlink(const char *from, const char *path)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_symlink(d->next, from, newpath);
		free(newpath);
	}
	return err;
}

static int subdir_rename(const char *from, const char *to, unsigned int flags)
{
	struct subdir *d = subdir_get();
	char *newfrom;
	char *newto;
	int err = subdir_addpath(d, from, &newfrom);
	if (!err) {
		err = subdir_addpath(d, to, &newto);
		if (!err) {
			err = fuse_fs_rename(d->next, newfrom, newto, flags);
			free(newto);
		}
		free(newfrom);
	}
	return err;
}

static int subdir_link(const char *from, const char *to)
{
	struct subdir *d = subdir_get();
	char *newfrom;
	char *newto;
	int err = subdir_addpath(d, from, &newfrom);
	if (!err) {
		err = subdir_addpath(d, to, &newto);
		if (!err) {
			err = fuse_fs_link(d->next, newfrom, newto);
			free(newto);
		}
		free(newfrom);
	}
	return err;
}

static int subdir_chmod(const char *path, mode_t mode,
			struct fuse_file_info *fi)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_chmod(d->next, newpath, mode, fi);
		free(newpath);
	}
	return err;
}

static int subdir_chown(const char *path, uid_t uid, gid_t gid,
			struct fuse_file_info *fi)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_chown(d->next, newpath, uid, gid, fi);
		free(newpath);
	}
	return err;
}

static int subdir_truncate(const char *path, off_t size,
			   struct fuse_file_info *fi)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_truncate(d->next, newpath, size, fi);
		free(newpath);
	}
	return err;
}

static int subdir_utimens(const char *path, const struct timespec ts[2],
			  struct fuse_file_info *fi)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_utimens(d->next, newpath, ts, fi);
		free(newpath);
	}
	return err;
}

static int subdir_create(const char *path, mode_t mode,
			 struct fuse_file_info *fi)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_create(d->next, newpath, mode, fi);
		free(newpath);
	}
	return err;
}

static int subdir_open(const char *path, struct fuse_file_info *fi)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_open(d->next, newpath, fi);
		free(newpath);
	}
	return err;
}

static int subdir_read_buf(const char *path, struct fuse_bufvec **bufp,
			   size_t size, off_t offset, struct fuse_file_info *fi)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_read_buf(d->next, newpath, bufp, size, offset, fi);
		free(newpath);
	}
	return err;
}

static int subdir_write_buf(const char *path, struct fuse_bufvec *buf,
			off_t offset, struct fuse_file_info *fi)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_write_buf(d->next, newpath, buf, offset, fi);
		free(newpath);
	}
	return err;
}

static int subdir_statfs(const char *path, struct statvfs *stbuf)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_statfs(d->next, newpath, stbuf);
		free(newpath);
	}
	return err;
}

static int subdir_flush(const char *path, struct fuse_file_info *fi)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_flush(d->next, newpath, fi);
		free(newpath);
	}
	return err;
}

static int subdir_release(const char *path, struct fuse_file_info *fi)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_release(d->next, newpath, fi);
		free(newpath);
	}
	return err;
}

static int subdir_fsync(const char *path, int isdatasync,
			struct fuse_file_info *fi)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_fsync(d->next, newpath, isdatasync, fi);
		free(newpath);
	}
	return err;
}

static int subdir_fsyncdir(const char *path, int isdatasync,
			   struct fuse_file_info *fi)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_fsyncdir(d->next, newpath, isdatasync, fi);
		free(newpath);
	}
	return err;
}

static int subdir_setxattr(const char *path, const char *name,
			   const char *value, size_t size, int flags)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_setxattr(d->next, newpath, name, value, size,
				       flags);
		free(newpath);
	}
	return err;
}

static int subdir_getxattr(const char *path, const char *name, char *value,
			   size_t size)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_getxattr(d->next, newpath, name, value, size);
		free(newpath);
	}
	return err;
}

static int subdir_listxattr(const char *path, char *list, size_t size)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_listxattr(d->next, newpath, list, size);
		free(newpath);
	}
	return err;
}

static int subdir_removexattr(const char *path, const char *name)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_removexattr(d->next, newpath, name);
		free(newpath);
	}
	return err;
}

static int subdir_lock(const char *path, struct fuse_file_info *fi, int cmd,
		       struct flock *lock)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_lock(d->next, newpath, fi, cmd, lock);
		free(newpath);
	}
	return err;
}

static int subdir_flock(const char *path, struct fuse_file_info *fi, int op)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_flock(d->next, newpath, fi, op);
		free(newpath);
	}
	return err;
}

static int subdir_bmap(const char *path, size_t blocksize, uint64_t *idx)
{
	struct subdir *d = subdir_get();
	char *newpath;
	int err = subdir_addpath(d, path, &newpath);
	if (!err) {
		err = fuse_fs_bmap(d->next, newpath, blocksize, idx);
		free(newpath);
	}
	return err;
}

static off_t subdir_lseek(const char *path, off_t off, int whence,
			  struct fuse_file_info *fi)
{
	struct subdir *ic = subdir_get();
	char *newpath;
	int res = subdir_addpath(ic, path, &newpath);
	if (!res) {
		res = fuse_fs_lseek(ic->next, newpath, off, whence, fi);
		free(newpath);
	}
	return res;
}

static void *subdir_init(struct fuse_conn_info *conn,
			 struct fuse_config *cfg)
{
	struct subdir *d = subdir_get();
	fuse_fs_init(d->next, conn, cfg);
	/* Don't touch cfg->nullpath_ok, we can work with
	   either */
	return d;
}

static void subdir_destroy(void *data)
{
	struct subdir *d = data;
	fuse_fs_destroy(d->next);
	free(d->base);
	free(d);
}

static const struct fuse_operations subdir_oper = {
	.destroy	= subdir_destroy,
	.init		= subdir_init,
	.getattr	= subdir_getattr,
	.access		= subdir_access,
	.readlink	= subdir_readlink,
	.opendir	= subdir_opendir,
	.readdir	= subdir_readdir,
	.releasedir	= subdir_releasedir,
	.mknod		= subdir_mknod,
	.mkdir		= subdir_mkdir,
	.symlink	= subdir_symlink,
	.unlink		= subdir_unlink,
	.rmdir		= subdir_rmdir,
	.rename		= subdir_rename,
	.link		= subdir_link,
	.chmod		= subdir_chmod,
	.chown		= subdir_chown,
	.truncate	= subdir_truncate,
	.utimens	= subdir_utimens,
	.create		= subdir_create,
	.open		= subdir_open,
	.read_buf	= subdir_read_buf,
	.write_buf	= subdir_write_buf,
	.statfs		= subdir_statfs,
	.flush		= subdir_flush,
	.release	= subdir_release,
	.fsync		= subdir_fsync,
	.fsyncdir	= subdir_fsyncdir,
	.setxattr	= subdir_setxattr,
	.getxattr	= subdir_getxattr,
	.listxattr	= subdir_listxattr,
	.removexattr	= subdir_removexattr,
	.lock		= subdir_lock,
	.flock		= subdir_flock,
	.bmap		= subdir_bmap,
	.lseek		= subdir_lseek,
};

static const struct fuse_opt subdir_opts[] = {
	FUSE_OPT_KEY("-h", 0),
	FUSE_OPT_KEY("--help", 0),
	{ "subdir=%s", offsetof(struct subdir, base), 0 },
	{ "rellinks", offsetof(struct subdir, rellinks), 1 },
	{ "norellinks", offsetof(struct subdir, rellinks), 0 },
	FUSE_OPT_END
};

static void subdir_help(void)
{
	printf(
"    -o subdir=DIR	    prepend this directory to all paths (mandatory)\n"
"    -o [no]rellinks	    transform absolute symlinks to relative\n");
}

static int subdir_opt_proc(void *data, const char *arg, int key,
			   struct fuse_args *outargs)
{
	(void) data; (void) arg; (void) outargs;

	if (!key) {
		subdir_help();
		return -1;
	}

	return 1;
}

static struct fuse_fs *subdir_new(struct fuse_args *args,
				  struct fuse_fs *next[])
{
	struct fuse_fs *fs;
	struct subdir *d;

	d = calloc(1, sizeof(struct subdir));
	if (d == NULL) {
		fuse_log(FUSE_LOG_ERR, "fuse-subdir: memory allocation failed\n");
		return NULL;
	}

	if (fuse_opt_parse(args, d, subdir_opts, subdir_opt_proc) == -1)
		goto out_free;

	if (!next[0] || next[1]) {
		fuse_log(FUSE_LOG_ERR, "fuse-subdir: exactly one next filesystem required\n");
		goto out_free;
	}

	if (!d->base) {
		fuse_log(FUSE_LOG_ERR, "fuse-subdir: missing 'subdir' option\n");
		goto out_free;
	}

	if (d->base[0] && d->base[strlen(d->base)-1] != '/') {
		char *tmp = realloc(d->base, strlen(d->base) + 2);
		if (!tmp) {
			fuse_log(FUSE_LOG_ERR, "fuse-subdir: memory allocation failed\n");
			goto out_free;
		}
		d->base = tmp;
		strcat(d->base, "/");
	}
	d->baselen = strlen(d->base);
	d->next = next[0];
	fs = fuse_fs_new(&subdir_oper, sizeof(subdir_oper), d);
	if (!fs)
		goto out_free;
	return fs;

out_free:
	free(d->base);
	free(d);
	return NULL;
}

FUSE_REGISTER_MODULE(subdir, subdir_new);
