/****************************************************************************
 * bfs                                                                      *
 * Copyright (C) 2021-2022 Tavian Barnes <tavianator@tavianator.com>        *
 *                                                                          *
 * Permission to use, copy, modify, and/or distribute this software for any *
 * purpose with or without fee is hereby granted.                           *
 *                                                                          *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES *
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF         *
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR  *
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES   *
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN    *
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF  *
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.           *
 ****************************************************************************/

#include "dir.h"
#include "util.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#if __linux__
#	include <sys/syscall.h>
#endif // __linux__

enum bfs_type bfs_mode_to_type(mode_t mode) {
	switch (mode & S_IFMT) {
#ifdef S_IFBLK
	case S_IFBLK:
		return BFS_BLK;
#endif
#ifdef S_IFCHR
	case S_IFCHR:
		return BFS_CHR;
#endif
#ifdef S_IFDIR
	case S_IFDIR:
		return BFS_DIR;
#endif
#ifdef S_IFDOOR
	case S_IFDOOR:
		return BFS_DOOR;
#endif
#ifdef S_IFIFO
	case S_IFIFO:
		return BFS_FIFO;
#endif
#ifdef S_IFLNK
	case S_IFLNK:
		return BFS_LNK;
#endif
#ifdef S_IFPORT
	case S_IFPORT:
		return BFS_PORT;
#endif
#ifdef S_IFREG
	case S_IFREG:
		return BFS_REG;
#endif
#ifdef S_IFSOCK
	case S_IFSOCK:
		return BFS_SOCK;
#endif
#ifdef S_IFWHT
	case S_IFWHT:
		return BFS_WHT;
#endif

	default:
		return BFS_UNKNOWN;
	}
}

#if __linux__
/**
 * This is not defined in the kernel headers for some reason, callers have to
 * define it themselves.
 */
struct linux_dirent64 {
	ino64_t d_ino;
	off64_t d_off;
	unsigned short d_reclen;
	unsigned char d_type;
	char d_name[];
};

// Make the whole allocation 64k
#define BUF_SIZE ((64 << 10) - 8)
#endif

struct bfs_dir {
#if __linux__
	int fd;
	unsigned short pos;
	unsigned short size;
#else
	DIR *dir;
	struct dirent *de;
#endif
};

struct bfs_dir *bfs_opendir(int at_fd, const char *at_path) {
#if __linux__
	struct bfs_dir *dir = malloc(sizeof(*dir) + BUF_SIZE);
#else
	struct bfs_dir *dir = malloc(sizeof(*dir));
#endif
	if (!dir) {
		return NULL;
	}

	int fd;
	if (at_path) {
		fd = openat(at_fd, at_path, O_RDONLY | O_CLOEXEC | O_DIRECTORY);
	} else if (at_fd >= 0) {
		fd = at_fd;
	} else {
		free(dir);
		errno = EBADF;
		return NULL;
	}

	if (fd < 0) {
		free(dir);
		return NULL;
	}

#if __linux__
	dir->fd = fd;
	dir->pos = 0;
	dir->size = 0;
#else
	dir->dir = fdopendir(fd);
	if (!dir->dir) {
		if (at_path) {
			close_quietly(fd);
		}
		free(dir);
		return NULL;
	}

	dir->de = NULL;
#endif // __linux__

	return dir;
}

int bfs_dirfd(const struct bfs_dir *dir) {
#if __linux__
	return dir->fd;
#else
	return dirfd(dir->dir);
#endif
}

/** Convert a dirent type to a bfs_type. */
static enum bfs_type translate_type(int d_type) {
	switch (d_type) {
#ifdef DT_BLK
	case DT_BLK:
		return BFS_BLK;
#endif
#ifdef DT_CHR
	case DT_CHR:
		return BFS_CHR;
#endif
#ifdef DT_DIR
	case DT_DIR:
		return BFS_DIR;
#endif
#ifdef DT_DOOR
	case DT_DOOR:
		return BFS_DOOR;
#endif
#ifdef DT_FIFO
	case DT_FIFO:
		return BFS_FIFO;
#endif
#ifdef DT_LNK
	case DT_LNK:
		return BFS_LNK;
#endif
#ifdef DT_PORT
	case DT_PORT:
		return BFS_PORT;
#endif
#ifdef DT_REG
	case DT_REG:
		return BFS_REG;
#endif
#ifdef DT_SOCK
	case DT_SOCK:
		return BFS_SOCK;
#endif
#ifdef DT_WHT
	case DT_WHT:
		return BFS_WHT;
#endif
	}

	return BFS_UNKNOWN;
}

#if !__linux__
/** Get the type from a struct dirent if it exists, and convert it. */
static enum bfs_type dirent_type(const struct dirent *de) {
#if defined(_DIRENT_HAVE_D_TYPE) || defined(DT_UNKNOWN)
	return translate_type(de->d_type);
#else
	return BFS_UNKNOWN;
#endif
}
#endif

/** Check if a name is . or .. */
static bool is_dot(const char *name) {
	return name[0] == '.' && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0'));
}

int bfs_readdir(struct bfs_dir *dir, struct bfs_dirent *de) {
	while (true) {
#if __linux__
		char *buf = (char *)(dir + 1);

		if (dir->pos >= dir->size) {
#if BFS_HAS_FEATURE(memory_sanitizer, false)
			// Make sure msan knows the buffer is initialized
			memset(buf, 0, BUF_SIZE);
#endif

			ssize_t size = syscall(__NR_getdents64, dir->fd, buf, BUF_SIZE);
			if (size <= 0) {
				return size;
			}
			dir->pos = 0;
			dir->size = size;
		}

		const struct linux_dirent64 *lde = (void *)(buf + dir->pos);
		dir->pos += lde->d_reclen;

		if (is_dot(lde->d_name)) {
			continue;
		}

		if (de) {
			de->type = translate_type(lde->d_type);
			de->name = lde->d_name;
		}

		return 1;
#else // !__linux__
		errno = 0;
		dir->de = readdir(dir->dir);
		if (dir->de) {
			if (is_dot(dir->de->d_name)) {
				continue;
			}
			if (de) {
				de->type = dirent_type(dir->de);
				de->name = dir->de->d_name;
			}
			return 1;
		} else if (errno != 0) {
			return -1;
		} else {
			return 0;
		}
#endif // !__linux__
	}
}

int bfs_closedir(struct bfs_dir *dir) {
#if __linux__
	int ret = xclose(dir->fd);
#else
	int ret = closedir(dir->dir);
#endif
	free(dir);
	return ret;
}

int bfs_freedir(struct bfs_dir *dir) {
#if __linux__
	int ret = dir->fd;
	free(dir);
	return ret;
#elif __FreeBSD__
	int ret = fdclosedir(dir->dir);
	free(dir);
	return ret;
#else
	int ret = dup_cloexec(dirfd(dir->dir));
	bfs_closedir(dir);
	return ret;
#endif
}