/*
 * fs.c - posix like file functions
 *
 * Copyright (c) 2007 Sascha Hauer <s.hauer@pengutronix.de>, Pengutronix
 *
 * See file CREDITS for list of people who contributed to this
 * project.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2
 * as published by the Free Software Foundation.
 *
 * 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
 */

#include <common.h>
#include <fs.h>
#include <driver.h>
#include <errno.h>
#include <malloc.h>
#include <linux/stat.h>
#include <fcntl.h>
#include <xfuncs.h>
#include <init.h>
#include <module.h>
#include <libbb.h>

void *read_file(const char *filename, size_t *size)
{
	int fd;
	struct stat s;
	void *buf = NULL;

	if (stat(filename, &s))
		return NULL;

	buf = xzalloc(s.st_size + 1);

	fd = open(filename, O_RDONLY);
	if (fd < 0)
		goto err_out;

	if (read(fd, buf, s.st_size) < s.st_size)
		goto err_out1;

	close(fd);

	if (size)
		*size = s.st_size;

	return buf;

err_out1:
	close(fd);
err_out:
	free(buf);
	return NULL;
}

EXPORT_SYMBOL(read_file);

char *mkmodestr(unsigned long mode, char *str)
{
	static const char *l = "xwr";
	int mask = 1, i;
	char c;

	switch (mode & S_IFMT) {
		case S_IFDIR:    str[0] = 'd'; break;
		case S_IFBLK:    str[0] = 'b'; break;
		case S_IFCHR:    str[0] = 'c'; break;
		case S_IFIFO:    str[0] = 'f'; break;
		case S_IFLNK:    str[0] = 'l'; break;
		case S_IFSOCK:   str[0] = 's'; break;
		case S_IFREG:    str[0] = '-'; break;
		default:         str[0] = '?';
	}

	for(i = 0; i < 9; i++) {
		c = l[i%3];
		str[9-i] = (mode & mask)?c:'-';
		mask = mask<<1;
	}

	if(mode & S_ISUID) str[3] = (mode & S_IXUSR)?'s':'S';
	if(mode & S_ISGID) str[6] = (mode & S_IXGRP)?'s':'S';
	if(mode & S_ISVTX) str[9] = (mode & S_IXOTH)?'t':'T';
	str[10] = '\0';
	return str;
}
EXPORT_SYMBOL(mkmodestr);

static char *cwd;

static int init_cwd(void)
{
	cwd = xzalloc(PATH_MAX);
	*cwd = '/';
	return 0;
}

postcore_initcall(init_cwd);

char *normalise_path(const char *pathname)
{
	char *path = xzalloc(strlen(pathname) + strlen(cwd) + 2);
        char *in, *out, *slashes[32];
	int sl = 0;

	debug("in: %s\n", pathname);

	if (*pathname != '/')
		strcpy(path, cwd);
	strcat(path, "/");
	strcat(path, pathname);

	slashes[0] = in = out = path;

        while (*in) {
                if(*in == '/') {
			slashes[sl++] = out;
                        *out++ = *in++;
                        while(*in == '/')
                                in++;
                } else {
			if (*in == '.' && (*(in + 1) == '/' || !*(in + 1))) {
				sl--;
				if (sl < 0)
					sl = 0;
				out = slashes[sl];
				in++;
				continue;
			}
			if (*in == '.' && *(in + 1) == '.') {
				sl -= 2;
				if (sl < 0)
					sl = 0;
				out = slashes[sl];
				in += 2;
				continue;
			}
                        *out++ = *in++;
                }
        }

	*out-- = 0;

        /*
         * Remove trailing slash
         */
        if (*out == '/')
                *out = 0;

	if (!*path) {
		*path = '/';
		*(path + 1) = 0;
	}

	return path;
}
EXPORT_SYMBOL(normalise_path);

static struct mtab_entry *mtab;

struct mtab_entry *get_mtab_entry_by_path(const char *_path)
{
	struct mtab_entry *match = NULL, *e = mtab;
	char *path, *tok;

	if (*_path != '/')
		return NULL;

	path = strdup(_path);

	tok = strchr(path + 1, '/');
	if (tok)
		*tok = 0;

	while (e) {
		if (!strcmp(path, e->path)) {
			match = e;
			break;
		}
		e = e->next;
	}

	free(path);

	return match ? match : mtab;
}

struct mtab_entry *mtab_next_entry(struct mtab_entry *e)
{
	if (!e)
		return mtab;
	return e->next;
}

const char *fsdev_get_mountpoint(struct fs_device_d *fsdev)
{
	return fsdev->mtab.path;
}

static FILE files[MAX_FILES];

static FILE *get_file(void)
{
	int i;

	for (i = 3; i < MAX_FILES; i++) {
		if (!files[i].in_use) {
			memset(&files[i], 0, sizeof(FILE));
			files[i].in_use = 1;
			files[i].no = i;
			return &files[i];
		}
	}
	return NULL;
}

static void put_file(FILE *f)
{
	files[f->no].in_use = 0;
}

static int check_fd(int fd)
{
	if (fd < 0 || fd >= MAX_FILES || !files[fd].in_use) {
		errno = -EBADF;
		return errno;
	}

	return 0;
}

static struct device_d *get_fs_device_by_path(char **path)
{
	struct device_d *dev;
	struct mtab_entry *e;

	e = get_mtab_entry_by_path(*path);
	if (!e)
		return NULL;
	if (e != mtab)
		*path += strlen(e->path);

	dev = e->dev;

	return dev;
}

static int dir_is_empty(const char *pathname)
{
	DIR *dir;
	struct dirent *d;
	int ret = 1;

	dir = opendir(pathname);
	if (!dir) {
		errno = -ENOENT;
		return -ENOENT;
	}

	while ((d = readdir(dir))) {
		if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
				continue;
		ret = 0;
		break;
	}

	closedir(dir);
	return ret;
}

#define S_UB_IS_EMPTY		(1 << 31)
#define S_UB_EXISTS		(1 << 30)
#define S_UB_DOES_NOT_EXIST	(1 << 29)

/*
 * Helper function to check the prerequisites of a path given
 * to fs functions. Besides the flags above S_IFREG and S_IFDIR
 * can be passed in.
 */
static int path_check_prereq(const char *path, unsigned int flags)
{
	struct stat s;
	unsigned int m;

	errno = 0;

	if (stat(path, &s)) {
		if (flags & S_UB_DOES_NOT_EXIST) {
			errno = 0;
			goto out;
		}
		errno = -ENOENT;
		goto out;
	}

	if (flags & S_UB_DOES_NOT_EXIST) {
		errno = -EEXIST;
		goto out;
	}

	if (flags == S_UB_EXISTS) {
		errno = 0;
		goto out;
	}

	m = s.st_mode;

	if (S_ISDIR(m)) {
		if (flags & S_IFREG) {
			errno = -EISDIR;
			goto out;
		}
		if ((flags & S_UB_IS_EMPTY) && !dir_is_empty(path)) {
			errno = -ENOTEMPTY;
			goto out;
		}
	}
	if ((flags & S_IFDIR) && S_ISREG(m)) {
		errno = -ENOTDIR;
		goto out;
	}

out:
	return errno;
}

const char *getcwd(void)
{
	return cwd;
}
EXPORT_SYMBOL(getcwd);

int chdir(const char *pathname)
{
	char *p = normalise_path(pathname);
	errno = 0;

	if (path_check_prereq(p, S_IFDIR))
		goto out;

	strcpy(cwd, p);

out:
	free(p);

	return errno;
}
EXPORT_SYMBOL(chdir);

int unlink(const char *pathname)
{
	struct device_d *dev;
	struct fs_driver_d *fsdrv;
	char *p = normalise_path(pathname);
	char *freep = p;

	if (path_check_prereq(pathname, S_IFREG))
		goto out;

	dev = get_fs_device_by_path(&p);
	if (!dev)
		goto out;
	fsdrv = (struct fs_driver_d *)dev->driver->type_data;

	if (!fsdrv->unlink) {
		errno = -ENOSYS;
		goto out;
	}

	errno = fsdrv->unlink(dev, p);
out:
	free(freep);
	return errno;
}
EXPORT_SYMBOL(unlink);

int open(const char *pathname, int flags, ...)
{
	struct device_d *dev;
	struct fs_driver_d *fsdrv;
	FILE *f;
	int exist;
	struct stat s;
	char *path = normalise_path(pathname);
	char *freep = path;

	exist = (stat(path, &s) == 0) ? 1 : 0;

	if (exist && S_ISDIR(s.st_mode)) {
		errno = -EISDIR;
		goto out1;
	}

	if (!exist && !(flags & O_CREAT)) {
		errno = -ENOENT;
		goto out1;
	}

	f = get_file();
	if (!f) {
		errno = -EMFILE;
		goto out1;
	}

	dev = get_fs_device_by_path(&path);
	if (!dev)
		goto out;

	fsdrv = (struct fs_driver_d *)dev->driver->type_data;

	f->dev = dev;
	f->flags = flags;

	if ((flags & O_ACCMODE) && !fsdrv->write) {
		errno = -EROFS;
		goto out;
	}

	if (!exist) {
		if (NULL != fsdrv->create)
			errno = fsdrv->create(dev, path,
					S_IFREG | S_IRWXU | S_IRWXG | S_IRWXO);
		else
			errno = -EROFS;
		if (errno)
			goto out;
	}
	errno = fsdrv->open(dev, f, path);
	if (errno)
		goto out;


	if (flags & O_TRUNC) {
		errno = fsdrv->truncate(dev, f, 0);
		f->size = 0;
		if (errno)
			goto out;
	}

	if (flags & O_APPEND)
		f->pos = f->size;

	free(freep);
	return f->no;

out:
	put_file(f);
out1:
	free(freep);
	return errno;
}
EXPORT_SYMBOL(open);

int creat(const char *pathname, mode_t mode)
{
	return open(pathname, O_CREAT | O_WRONLY | O_TRUNC);
}
EXPORT_SYMBOL(creat);

int ioctl(int fd, int request, void *buf)
{
	struct device_d *dev;
	struct fs_driver_d *fsdrv;
	FILE *f = &files[fd];

	if (check_fd(fd))
		return errno;

	dev = f->dev;

	fsdrv = (struct fs_driver_d *)dev->driver->type_data;

	if (fsdrv->ioctl)
		errno = fsdrv->ioctl(dev, f, request, buf);
	else
		errno = -ENOSYS;
	return errno;
}

int read(int fd, void *buf, size_t count)
{
	struct device_d *dev;
	struct fs_driver_d *fsdrv;
	FILE *f = &files[fd];

	if (check_fd(fd))
		return errno;

	dev = f->dev;

	fsdrv = (struct fs_driver_d *)dev->driver->type_data;

	if (f->pos + count > f->size)
		count = f->size - f->pos;

	if (!count)
		return 0;

	errno = fsdrv->read(dev, f, buf, count);

	if (errno > 0)
		f->pos += errno;
	return errno;
}
EXPORT_SYMBOL(read);

ssize_t write(int fd, const void *buf, size_t count)
{
	struct device_d *dev;
	struct fs_driver_d *fsdrv;
	FILE *f = &files[fd];

	if (check_fd(fd))
		return errno;

	dev = f->dev;

	fsdrv = (struct fs_driver_d *)dev->driver->type_data;
	if (f->pos + count > f->size) {
		errno = fsdrv->truncate(dev, f, f->pos + count);
		if (errno) {
			if (errno != -ENOSPC)
				return errno;
			count = f->size - f->pos;
			if (!count)
				return errno;
		} else {
			f->size = f->pos + count;
		}
	}
	errno = fsdrv->write(dev, f, buf, count);

	if (errno > 0)
		f->pos += errno;
	return errno;
}
EXPORT_SYMBOL(write);

int flush(int fd)
{
	struct device_d *dev;
	struct fs_driver_d *fsdrv;
	FILE *f = &files[fd];

	if (check_fd(fd))
		return errno;

	dev = f->dev;

	fsdrv = (struct fs_driver_d *)dev->driver->type_data;
	if (fsdrv->flush)
		errno = fsdrv->flush(dev, f);
	else
		errno = 0;

	return errno;
}

off_t lseek(int fildes, off_t offset, int whence)
{
	struct device_d *dev;
	struct fs_driver_d *fsdrv;
	FILE *f = &files[fildes];
	off_t pos;

	if (check_fd(fildes))
		return -1;

	errno = 0;

	dev = f->dev;
	fsdrv = (struct fs_driver_d *)dev->driver->type_data;
	if (!fsdrv->lseek) {
		errno = -ENOSYS;
		return -1;
	}

	switch(whence) {
	case SEEK_SET:
		if (offset > f->size)
			goto out;
		pos = offset;
		break;
	case SEEK_CUR:
		if (offset + f->pos > f->size)
			goto out;
		pos = f->pos + offset;
		break;
	case SEEK_END:
		if (offset)
			goto out;
		pos = f->size;
		break;
	default:
		goto out;
	}

	return fsdrv->lseek(dev, f, pos);

out:
	errno = -EINVAL;
	return -1;
}
EXPORT_SYMBOL(lseek);

int erase(int fd, size_t count, unsigned long offset)
{
	struct device_d *dev;
	struct fs_driver_d *fsdrv;
	FILE *f = &files[fd];

	if (check_fd(fd))
		return errno;

	dev = f->dev;

	fsdrv = (struct fs_driver_d *)dev->driver->type_data;

	if (f->pos + count > f->size)
		count = f->size - f->pos;

	if (fsdrv->erase)
		errno = fsdrv->erase(dev, f, count, offset);
	else
		errno = -ENOSYS;

	return errno;
}
EXPORT_SYMBOL(erase);

int protect(int fd, size_t count, unsigned long offset, int prot)
{
	struct device_d *dev;
	struct fs_driver_d *fsdrv;
	FILE *f = &files[fd];

	if (check_fd(fd))
		return errno;

	dev = f->dev;

	fsdrv = (struct fs_driver_d *)dev->driver->type_data;

	if (f->pos + count > f->size)
		count = f->size - f->pos;

	if (fsdrv->protect)
		errno = fsdrv->protect(dev, f, count, offset, prot);
	else
		errno = -ENOSYS;

	return errno;
}
EXPORT_SYMBOL(protect);

int protect_file(const char *file, int prot)
{
	int fd, ret;

	fd = open(file, O_WRONLY);
	if (fd < 0)
		return fd;

	ret = protect(fd, ~0, 0, prot);

	close(fd);

	return ret;
}

void *memmap(int fd, int flags)
{
	struct device_d *dev;
	struct fs_driver_d *fsdrv;
	FILE *f = &files[fd];
	void *ret = (void *)-1;

	if (check_fd(fd))
		return ret;

	dev = f->dev;

	fsdrv = (struct fs_driver_d *)dev->driver->type_data;

	if (fsdrv->memmap)
		errno = fsdrv->memmap(dev, f, &ret, flags);
	else
		errno = -EINVAL;

	return ret;
}
EXPORT_SYMBOL(memmap);

int close(int fd)
{
	struct device_d *dev;
	struct fs_driver_d *fsdrv;
	FILE *f = &files[fd];

	if (check_fd(fd))
		return errno;

	dev = f->dev;

	fsdrv = (struct fs_driver_d *)dev->driver->type_data;
	errno = fsdrv->close(dev, f);

	put_file(f);
	return errno;
}
EXPORT_SYMBOL(close);

static LIST_HEAD(fs_driver_list);

int register_fs_driver(struct fs_driver_d *fsdrv)
{
	list_add_tail(&fsdrv->list, &fs_driver_list);
	register_driver(&fsdrv->drv);
	return 0;
}
EXPORT_SYMBOL(register_fs_driver);

/*
 * Mount a device to a directory.
 * We do this by registering a new device on which the filesystem
 * driver will match. The filesystem driver then grabs the infomation
 * it needs from the new devices type_data.
 */
int mount(const char *device, const char *fsname, const char *_path)
{
	struct fs_driver_d *fs_drv = NULL, *f;
	struct mtab_entry *entry;
	struct fs_device_d *fsdev;
	struct device_d *dev, *parent_device = NULL;
	int ret;
	char *path = normalise_path(_path);

	errno = 0;

	debug("mount: %s on %s type %s\n", device, path, fsname);

	if (get_mtab_entry_by_path(path) != mtab) {
		errno = -EBUSY;
		goto out;
	}

	if (strchr(path + 1, '/')) {
		printf("mounting allowed on first directory level only\n");
		errno = -EBUSY;
		goto out;
	}

	list_for_each_entry(f, &fs_driver_list, list) {
		if (!strcmp(f->drv.name, fsname)) {
			fs_drv = f;
			break;
		}
	}

	if (!fs_drv) {
		errno = -EINVAL;
		goto out;
	}

	if (mtab) {
		if (path_check_prereq(path, S_IFDIR))
			goto out;
	} else {
		/* no mtab, so we only allow to mount on '/' */
		if (*path != '/' || *(path + 1)) {
			errno = -ENOTDIR;
			goto out;
		}
	}

	fsdev = xzalloc(sizeof(struct fs_device_d));
	if (!(fs_drv->flags & FS_DRIVER_NO_DEV)) {
		fsdev->backingstore = strdup(device);
		if (!device) {
			printf("need a device for driver %s\n", fsname);
			errno = -ENODEV;
			goto out1;
		}
	}
	safe_strncpy(fsdev->dev.name, fsname, MAX_DRIVER_NAME);
	fsdev->dev.type_data = fsdev;
	fsdev->dev.id = get_free_deviceid(fsdev->dev.name);

	if ((ret = register_device(&fsdev->dev))) {
		errno = ret;
		goto out1;
	}

	if (!fsdev->dev.driver) {
		/* driver didn't accept the device. Bail out */
		errno = -EINVAL;
		goto out2;
	}

	if (parent_device)
		dev_add_child(parent_device, &fsdev->dev);

	dev = &fsdev->dev;

	/* add mtab entry */
	entry = &fsdev->mtab; 
	safe_strncpy(entry->path, path, PATH_MAX);
	entry->dev = dev;
	entry->parent_device = parent_device;
	entry->next = NULL;

	if (!mtab)
		mtab = entry;
	else {
		struct mtab_entry *e = mtab;
		while (e->next)
			e = e->next;
		e->next = entry;
	}
	errno = 0;

	free(path);
	return 0;

out2:
	unregister_device(&fsdev->dev);
out1:
	if (fsdev->backingstore)
		free(fsdev->backingstore);
	free(fsdev);
out:
	free(path);
	return errno;
}
EXPORT_SYMBOL(mount);

int umount(const char *pathname)
{
	struct mtab_entry *entry = mtab;
	struct mtab_entry *last = mtab;
	char *p = normalise_path(pathname);
	struct fs_device_d *fsdev;

	while(entry && strcmp(p, entry->path)) {
		last = entry;
		entry = entry->next;
	}

	free(p);

	if (!entry) {
		errno = -EFAULT;
		return errno;
	}

	if (entry == mtab)
		mtab = mtab->next;
	else
		last->next = entry->next;

	unregister_device(entry->dev);
	fsdev = entry->dev->type_data;
	free(fsdev->backingstore);
	free(fsdev);

	return 0;
}
EXPORT_SYMBOL(umount);

DIR *opendir(const char *pathname)
{
	DIR *dir = NULL;
	struct device_d *dev;
	struct fs_driver_d *fsdrv;
	char *p = normalise_path(pathname);
	char *freep = p;

	if (path_check_prereq(pathname, S_IFDIR))
		goto out;

	dev = get_fs_device_by_path(&p);
	if (!dev)
		goto out;
	fsdrv = (struct fs_driver_d *)dev->driver->type_data;

	debug("opendir: fsdrv: %p\n",fsdrv);

	dir = fsdrv->opendir(dev, p);
	if (dir) {
		dir->dev = dev;
		dir->fsdrv = fsdrv;
	}

out:
	free(freep);
	return dir;
}
EXPORT_SYMBOL(opendir);

struct dirent *readdir(DIR *dir)
{
	if (!dir)
		return NULL;

	return dir->fsdrv->readdir(dir->dev, dir);
}
EXPORT_SYMBOL(readdir);

int closedir(DIR *dir)
{
	if (!dir) {
		errno = -EBADF;
		return -1;
	}

	return dir->fsdrv->closedir(dir->dev, dir);
}
EXPORT_SYMBOL(closedir);

int stat(const char *filename, struct stat *s)
{
	struct device_d *dev;
	struct fs_driver_d *fsdrv;
	struct mtab_entry *e;
	char *f = normalise_path(filename);
	char *freep = f;

	memset(s, 0, sizeof(struct stat));

	e = get_mtab_entry_by_path(f);
	if (!e) {
		errno = -ENOENT;
		goto out;
	}

	if (e != mtab && strcmp(f, e->path)) {
		f += strlen(e->path);
		dev = e->dev;
	} else
		dev = mtab->dev;

	fsdrv = (struct fs_driver_d *)dev->driver->type_data;

	if (*f == 0)
		f = "/";

	errno = fsdrv->stat(dev, f, s);
out:
	free(freep);
	return errno;
}
EXPORT_SYMBOL(stat);

int mkdir (const char *pathname, mode_t mode)
{
	struct fs_driver_d *fsdrv;
	struct device_d *dev;
	char *p = normalise_path(pathname);
	char *freep = p;

	if (path_check_prereq(pathname, S_UB_DOES_NOT_EXIST))
		goto out;

	dev = get_fs_device_by_path(&p);
	if (!dev)
		goto out;
	fsdrv = (struct fs_driver_d *)dev->driver->type_data;

	if (fsdrv->mkdir) {
		errno = fsdrv->mkdir(dev, p);
		goto out;
	}

	errno = -EROFS;
out:
	free(freep);
	return errno;
}
EXPORT_SYMBOL(mkdir);

int rmdir (const char *pathname)
{
	struct fs_driver_d *fsdrv;
	struct device_d *dev;
	char *p = normalise_path(pathname);
	char *freep = p;

	if (path_check_prereq(pathname, S_IFDIR | S_UB_IS_EMPTY))
		goto out;

	dev = get_fs_device_by_path(&p);
	if (!dev)
		goto out;
	fsdrv = (struct fs_driver_d *)dev->driver->type_data;

	if (fsdrv->rmdir) {
		errno = fsdrv->rmdir(dev, p);
		goto out;
	}

	errno = -EROFS;
out:
	free(freep);
	return errno;
}
EXPORT_SYMBOL(rmdir);

static void memcpy_sz(void *_dst, const void *_src, ulong count, ulong rwsize)
{
	ulong dst = (ulong)_dst;
	ulong src = (ulong)_src;

	/* no rwsize specification given. Do whatever memcpy likes best */
	if (!rwsize) {
		memcpy(_dst, _src, count);
		return;
	}

	rwsize = rwsize >> O_RWSIZE_SHIFT;

	count /= rwsize;

	while (count-- > 0) {
		switch (rwsize) {
		case 1:
			*((u_char *)dst) = *((u_char *)src);
			break;
		case 2:
			*((ushort *)dst) = *((ushort *)src);
			break;
		case 4:
			*((ulong  *)dst) = *((ulong  *)src);
			break;
		}
		dst += rwsize;
		src += rwsize;
	}
}

ssize_t mem_read(struct cdev *cdev, void *buf, size_t count, ulong offset, ulong flags)
{
	ulong size;
	struct device_d *dev;

	if (!cdev->dev || cdev->dev->num_resources < 1)
		return -1;
	dev = cdev->dev;

	size = min((ulong)count, dev->resource[0].size - offset);
	memcpy_sz(buf, dev_get_mem_region(dev, 0) + offset, size, flags & O_RWSIZE_MASK);
	return size;
}
EXPORT_SYMBOL(mem_read);

ssize_t mem_write(struct cdev *cdev, const void *buf, size_t count, ulong offset, ulong flags)
{
	ulong size;
	struct device_d *dev;

	if (!cdev->dev || cdev->dev->num_resources < 1)
		return -1;
	dev = cdev->dev;

	size = min((ulong)count, dev->resource[0].size - offset);
	memcpy_sz(dev_get_mem_region(dev, 0) + offset, buf, size, flags & O_RWSIZE_MASK);
	return size;
}
EXPORT_SYMBOL(mem_write);

