#include <unistd.h>
#include <limits.h>

#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <sys/inotify.h>
#include <unistd.h>
#include <stdlib.h>
#include <linux/fanotify.h>

#include "zdtmtst.h"

#ifdef __x86_64__
#define __NR_fanotify_init 300
#define __NR_fanotify_mark 301
#elif defined(__PPC64__)
#define __NR_fanotify_init 323
#define __NR_fanotify_mark 324
#elif (__aarch64__ || __riscv)
#define __NR_fanotify_init 262
#define __NR_fanotify_mark 263
#elif __s390x__
#define __NR_fanotify_init 332
#define __NR_fanotify_mark 333
#else
#define __NR_fanotify_init 338
#define __NR_fanotify_mark 339
#endif

const char *test_doc = "Check for fanotify delivery";
const char *test_author = "Cyrill Gorcunov <gorcunov@openvz.org>";

const char fanotify_path[] = "fanotify-del-after-cr";

#define BUFF_SIZE (8192)

struct fanotify_mark_inode {
	unsigned long i_ino;
	unsigned int s_dev;
	unsigned int mflags;
	unsigned int mask;
	unsigned int ignored_mask;
	unsigned int fhandle_bytes;
	unsigned int fhandle_type;
	unsigned char fhandle[512];
};

struct fanotify_mark_mount {
	unsigned int mnt_id;
	unsigned int mflags;
	unsigned int mask;
	unsigned int ignored_mask;
};

struct fanotify_glob {
	unsigned int faflags;
	unsigned int evflags;
};

struct fanotify_obj {
	struct fanotify_glob glob;
	struct fanotify_mark_inode inode;
	struct fanotify_mark_mount mount;
};

static int fanotify_init(unsigned int flags, unsigned int event_f_flags)
{
	return syscall(__NR_fanotify_init, flags, event_f_flags);
}

static int fanotify_mark(int fanotify_fd, unsigned int flags, unsigned long mask, int dfd, const char *pathname)
{
#ifdef __i386__
	return syscall(__NR_fanotify_mark, fanotify_fd, flags, mask, 0, dfd, pathname);
#else
	return syscall(__NR_fanotify_mark, fanotify_fd, flags, mask, dfd, pathname);
#endif
}

#define fdinfo_field(str, field) !strncmp(str, field ":", sizeof(field))

static void show_fanotify_obj(struct fanotify_obj *obj)
{
	test_msg("fanotify obj at %p\n", obj);

	test_msg(" glob\n");
	test_msg("  faflags: %x evflags: %x\n", obj->glob.faflags, obj->glob.evflags);

	test_msg(" inode\n");
	test_msg("  i_ino: %lx s_dev: %x mflags: %x "
		 "mask: %x ignored_mask: %x "
		 "fhandle_bytes: %x fhandle_type: %x "
		 "fhandle: %s",
		 obj->inode.i_ino, obj->inode.s_dev, obj->inode.mflags, obj->inode.mask, obj->inode.ignored_mask,
		 obj->inode.fhandle_bytes, obj->inode.fhandle_type, obj->inode.fhandle);

	test_msg(" mount\n");
	test_msg("  mnt_id: %x mflags: %x mask: %x ignored_mask: %x\n", obj->mount.mnt_id, obj->mount.mflags,
		 obj->mount.mask, obj->mount.ignored_mask);
}

static void copy_fhandle(char *tok, struct fanotify_mark_inode *inode)
{
	int off = 0;

	while (*tok && (*tok > '0' || *tok < 'f')) {
		inode->fhandle[off++] = *tok++;
		if (off >= sizeof(inode->fhandle) - 1)
			break;
	}
	inode->fhandle[off] = '\0';
}

static int cmp_fanotify_obj(struct fanotify_obj *old, struct fanotify_obj *new)
{
	/*
	 * mnt_id and s_dev may change during container migration,
	 * moreover the backend (say PLOOP) may be re-mounted during
	 * c/r, so exclude them.
	 */
	if ((old->glob.faflags != new->glob.faflags) || (old->glob.evflags != new->glob.evflags) ||
	    (old->inode.i_ino != new->inode.i_ino) || (old->inode.mflags != new->inode.mflags) ||
	    (old->inode.mask != new->inode.mask) || (old->inode.ignored_mask != new->inode.ignored_mask))
		return -1;

	if (memcmp(old->inode.fhandle, new->inode.fhandle, sizeof(new->inode.fhandle)))
		return -2;

	if ((old->mount.mflags != new->mount.mflags) || (old->mount.mask != new->mount.mask) ||
	    (old->mount.ignored_mask != new->mount.ignored_mask))
		return -3;

	return 0;
}

int parse_fanotify_fdinfo(int fd, struct fanotify_obj *obj, unsigned int expected_to_meet)
{
	unsigned int met = 0;
	char str[512];
	FILE *f;
	int ret;

	sprintf(str, "/proc/self/fdinfo/%d", fd);
	f = fopen(str, "r");
	if (!f) {
		pr_perror("Can't open fdinfo to parse");
		return -1;
	}

	while (fgets(str, sizeof(str), f)) {
		if (fdinfo_field(str, "fanotify flags")) {
			ret = sscanf(str, "fanotify flags:%x event-flags:%x", &obj->glob.faflags, &obj->glob.evflags);
			if (ret != 2)
				goto parse_err;
			met++;
			continue;
		}
		if (fdinfo_field(str, "fanotify mnt_id")) {
			ret = sscanf(str, "fanotify mnt_id:%x mflags:%x mask:%x ignored_mask:%x", &obj->mount.mnt_id,
				     &obj->mount.mflags, &obj->mount.mask, &obj->mount.ignored_mask);
			if (ret != 4)
				goto parse_err;
			met++;
			continue;
		}
		if (fdinfo_field(str, "fanotify ino")) {
			int hoff;
			ret = sscanf(str,
				     "fanotify ino:%lx sdev:%x mflags:%x mask:%x ignored_mask:%x "
				     "fhandle-bytes:%x fhandle-type:%x f_handle: %n",
				     &obj->inode.i_ino, &obj->inode.s_dev, &obj->inode.mflags, &obj->inode.mask,
				     &obj->inode.ignored_mask, &obj->inode.fhandle_bytes, &obj->inode.fhandle_type,
				     &hoff);
			if (ret != 7)
				goto parse_err;
			copy_fhandle(&str[hoff], &obj->inode);
			met++;
			continue;
		}
	}

	if (expected_to_meet != met) {
		pr_perror("Expected to meet %d entries but got %d", expected_to_meet, met);
		return -1;
	}

	return 0;

parse_err:
	pr_perror("Can't parse '%s'", str);
	return -1;
}

int main(int argc, char *argv[])
{
	struct fanotify_obj old = {}, new = {};
	int fa_fd, fd, del_after;
	char buf[BUFF_SIZE];
	ssize_t length;
	int ns = getenv("ZDTM_NEWNS") != NULL;

	test_init(argc, argv);

	if (ns) {
		if (mkdir("/tmp", 666) && errno != EEXIST) {
			pr_perror("Unable to create the /tmp directory");
			return -1;
		}
		if (mount("zdtm", "/tmp", "tmpfs", 0, NULL)) {
			pr_perror("Unable to mount tmpfs into %s", "/tmp");
		}
	}

	fa_fd = fanotify_init(FAN_NONBLOCK | FAN_CLASS_NOTIF | FAN_UNLIMITED_QUEUE, O_RDONLY | O_LARGEFILE);
	if (fa_fd < 0) {
		pr_perror("fanotify_init failed");
		exit(1);
	}

	del_after = open(fanotify_path, O_CREAT | O_TRUNC);
	if (del_after < 0) {
		pr_perror("open failed");
		exit(1);
	}

	if (fanotify_mark(fa_fd, FAN_MARK_ADD, FAN_MODIFY | FAN_ACCESS | FAN_OPEN | FAN_CLOSE, AT_FDCWD,
			  fanotify_path)) {
		pr_perror("fanotify_mark failed");
		exit(1);
	}

	if (fanotify_mark(fa_fd, FAN_MARK_ADD | FAN_MARK_MOUNT, FAN_ONDIR | FAN_OPEN | FAN_CLOSE, AT_FDCWD, "/tmp")) {
		pr_perror("fanotify_mark failed");
		exit(1);
	}

	if (fanotify_mark(fa_fd, FAN_MARK_ADD | FAN_MARK_MOUNT | FAN_MARK_IGNORED_MASK | FAN_MARK_IGNORED_SURV_MODIFY,
			  FAN_MODIFY | FAN_ACCESS, AT_FDCWD, "/tmp")) {
		pr_perror("fanotify_mark failed");
		exit(1);
	}

	if (parse_fanotify_fdinfo(fa_fd, &old, 3)) {
		pr_perror("parsing fanotify fdinfo failed");
		exit(1);
	}

	show_fanotify_obj(&old);

	test_daemon();
	test_waitsig();

	fd = open("/", O_RDONLY);
	close(fd);

	fd = open(fanotify_path, O_RDWR);
	close(fd);

	if (unlink(fanotify_path)) {
		fail("can't unlink %s", fanotify_path);
		exit(1);
	}

	if (parse_fanotify_fdinfo(fa_fd, &new, 3)) {
		fail("parsing fanotify fdinfo failed");
		exit(1);
	}

	show_fanotify_obj(&new);

	if (cmp_fanotify_obj(&old, &new)) {
		fail("fanotify mismatch on fdinfo level");
		exit(1);
	}

	length = read(fa_fd, buf, sizeof(buf));
	if (length <= 0) {
		fail("No events in fanotify queue");
		exit(1);
	}

	if (fanotify_mark(fa_fd, FAN_MARK_REMOVE | FAN_MARK_MOUNT, FAN_ONDIR | FAN_OPEN | FAN_CLOSE, AT_FDCWD,
			  "/tmp")) {
		pr_perror("fanotify_mark failed");
		exit(1);
	}

	pass();

	return 0;
}
