// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE

#include <stdio.h>
#include <stdbool.h>
#include <linux/kernel.h>
#include <linux/magic.h>
#include <linux/mman.h>
#include <sys/mman.h>
#include <sys/shm.h>
#include <sys/syscall.h>
#include <sys/vfs.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>

#include "../kselftest.h"

#define NR_TESTS	9

static const char * const dev_files[] = {
	"/dev/zero", "/dev/null", "/dev/urandom",
	"/proc/version", "/proc"
};

void print_cachestat(struct cachestat *cs)
{
	ksft_print_msg(
	"Using cachestat: Cached: %lu, Dirty: %lu, Writeback: %lu, Evicted: %lu, Recently Evicted: %lu\n",
	cs->nr_cache, cs->nr_dirty, cs->nr_writeback,
	cs->nr_evicted, cs->nr_recently_evicted);
}

bool write_exactly(int fd, size_t filesize)
{
	int random_fd = open("/dev/urandom", O_RDONLY);
	char *cursor, *data;
	int remained;
	bool ret;

	if (random_fd < 0) {
		ksft_print_msg("Unable to access urandom.\n");
		ret = false;
		goto out;
	}

	data = malloc(filesize);
	if (!data) {
		ksft_print_msg("Unable to allocate data.\n");
		ret = false;
		goto close_random_fd;
	}

	remained = filesize;
	cursor = data;

	while (remained) {
		ssize_t read_len = read(random_fd, cursor, remained);

		if (read_len <= 0) {
			ksft_print_msg("Unable to read from urandom.\n");
			ret = false;
			goto out_free_data;
		}

		remained -= read_len;
		cursor += read_len;
	}

	/* write random data to fd */
	remained = filesize;
	cursor = data;
	while (remained) {
		ssize_t write_len = write(fd, cursor, remained);

		if (write_len <= 0) {
			ksft_print_msg("Unable write random data to file.\n");
			ret = false;
			goto out_free_data;
		}

		remained -= write_len;
		cursor += write_len;
	}

	ret = true;
out_free_data:
	free(data);
close_random_fd:
	close(random_fd);
out:
	return ret;
}

/*
 * fsync() is implemented via noop_fsync() on tmpfs. This makes the fsync()
 * test fail below, so we need to check for test file living on a tmpfs.
 */
static bool is_on_tmpfs(int fd)
{
	struct statfs statfs_buf;

	if (fstatfs(fd, &statfs_buf))
		return false;

	return statfs_buf.f_type == TMPFS_MAGIC;
}

/*
 * Open/create the file at filename, (optionally) write random data to it
 * (exactly num_pages), then test the cachestat syscall on this file.
 *
 * If test_fsync == true, fsync the file, then check the number of dirty
 * pages.
 */
static int test_cachestat(const char *filename, bool write_random, bool create,
			  bool test_fsync, unsigned long num_pages,
			  int open_flags, mode_t open_mode)
{
	size_t PS = sysconf(_SC_PAGESIZE);
	int filesize = num_pages * PS;
	int ret = KSFT_PASS;
	long syscall_ret;
	struct cachestat cs;
	struct cachestat_range cs_range = { 0, filesize };

	int fd = open(filename, open_flags, open_mode);

	if (fd == -1) {
		ksft_print_msg("Unable to create/open file.\n");
		ret = KSFT_FAIL;
		goto out;
	} else {
		ksft_print_msg("Create/open %s\n", filename);
	}

	if (write_random) {
		if (!write_exactly(fd, filesize)) {
			ksft_print_msg("Unable to access urandom.\n");
			ret = KSFT_FAIL;
			goto out1;
		}
	}

	syscall_ret = syscall(__NR_cachestat, fd, &cs_range, &cs, 0);

	ksft_print_msg("Cachestat call returned %ld\n", syscall_ret);

	if (syscall_ret) {
		ksft_print_msg("Cachestat returned non-zero.\n");
		ret = KSFT_FAIL;
		goto out1;

	} else {
		print_cachestat(&cs);

		if (write_random) {
			if (cs.nr_cache + cs.nr_evicted != num_pages) {
				ksft_print_msg(
					"Total number of cached and evicted pages is off.\n");
				ret = KSFT_FAIL;
			}
		}
	}

	if (test_fsync) {
		if (is_on_tmpfs(fd)) {
			ret = KSFT_SKIP;
		} else if (fsync(fd)) {
			ksft_print_msg("fsync fails.\n");
			ret = KSFT_FAIL;
		} else {
			syscall_ret = syscall(__NR_cachestat, fd, &cs_range, &cs, 0);

			ksft_print_msg("Cachestat call (after fsync) returned %ld\n",
				syscall_ret);

			if (!syscall_ret) {
				print_cachestat(&cs);

				if (cs.nr_dirty) {
					ret = KSFT_FAIL;
					ksft_print_msg(
						"Number of dirty should be zero after fsync.\n");
				}
			} else {
				ksft_print_msg("Cachestat (after fsync) returned non-zero.\n");
				ret = KSFT_FAIL;
				goto out1;
			}
		}
	}

out1:
	close(fd);

	if (create)
		remove(filename);
out:
	return ret;
}

bool test_cachestat_shmem(void)
{
	size_t PS = sysconf(_SC_PAGESIZE);
	size_t filesize = PS * 512 * 2; /* 2 2MB huge pages */
	int syscall_ret;
	size_t compute_len = PS * 512;
	struct cachestat_range cs_range = { PS, compute_len };
	char *filename = "tmpshmcstat";
	struct cachestat cs;
	bool ret = true;
	unsigned long num_pages = compute_len / PS;
	int fd = shm_open(filename, O_CREAT | O_RDWR, 0600);

	if (fd < 0) {
		ksft_print_msg("Unable to create shmem file.\n");
		ret = false;
		goto out;
	}

	if (ftruncate(fd, filesize)) {
		ksft_print_msg("Unable to truncate shmem file.\n");
		ret = false;
		goto close_fd;
	}

	if (!write_exactly(fd, filesize)) {
		ksft_print_msg("Unable to write to shmem file.\n");
		ret = false;
		goto close_fd;
	}

	syscall_ret = syscall(__NR_cachestat, fd, &cs_range, &cs, 0);

	if (syscall_ret) {
		ksft_print_msg("Cachestat returned non-zero.\n");
		ret = false;
		goto close_fd;
	} else {
		print_cachestat(&cs);
		if (cs.nr_cache + cs.nr_evicted != num_pages) {
			ksft_print_msg(
				"Total number of cached and evicted pages is off.\n");
			ret = false;
		}
	}

close_fd:
	shm_unlink(filename);
out:
	return ret;
}

int main(void)
{
	int ret;

	ksft_print_header();

	ret = syscall(__NR_cachestat, -1, NULL, NULL, 0);
	if (ret == -1 && errno == ENOSYS)
		ksft_exit_skip("cachestat syscall not available\n");

	ksft_set_plan(NR_TESTS);

	if (ret == -1 && errno == EBADF) {
		ksft_test_result_pass("bad file descriptor recognized\n");
		ret = 0;
	} else {
		ksft_test_result_fail("bad file descriptor ignored\n");
		ret = 1;
	}

	for (int i = 0; i < 5; i++) {
		const char *dev_filename = dev_files[i];

		if (test_cachestat(dev_filename, false, false, false,
			4, O_RDONLY, 0400) == KSFT_PASS)
			ksft_test_result_pass("cachestat works with %s\n", dev_filename);
		else {
			ksft_test_result_fail("cachestat fails with %s\n", dev_filename);
			ret = 1;
		}
	}

	if (test_cachestat("tmpfilecachestat", true, true,
		false, 4, O_CREAT | O_RDWR, 0600) == KSFT_PASS)
		ksft_test_result_pass("cachestat works with a normal file\n");
	else {
		ksft_test_result_fail("cachestat fails with normal file\n");
		ret = 1;
	}

	switch (test_cachestat("tmpfilecachestat", true, true,
		true, 4, O_CREAT | O_RDWR, 0600)) {
	case KSFT_FAIL:
		ksft_test_result_fail("cachestat fsync fails with normal file\n");
		ret = KSFT_FAIL;
		break;
	case KSFT_PASS:
		ksft_test_result_pass("cachestat fsync works with a normal file\n");
		break;
	case KSFT_SKIP:
		ksft_test_result_skip("tmpfilecachestat is on tmpfs\n");
		break;
	}

	if (test_cachestat_shmem())
		ksft_test_result_pass("cachestat works with a shmem file\n");
	else {
		ksft_test_result_fail("cachestat fails with a shmem file\n");
		ret = 1;
	}

	return ret;
}
