// SPDX-License-Identifier: LGPL-2.1-or-later
/*
 * This file is part of libnvme.
 * Copyright (c) 2020 Western Digital Corporation or its affiliates.
 *
 * Authors: Keith Busch <keith.busch@wdc.com>
 * 	    Chaitanya Kulkarni <chaitanya.kulkarni@wdc.com>
 */
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <linux/fs.h>

#include <sys/ioctl.h>
#include <sys/stat.h>

#include <ccan/build_assert/build_assert.h>
#include <ccan/endian/endian.h>
#include <ccan/minmax/minmax.h>

#include <shared/compiler-attributes-util.h>

#include <libnvme.h>

#include "private.h"
#include "loopback.h"

static int nvme_verify_chr(struct libnvme_transport_handle *hdl)
{
	static struct stat nvme_stat;
	int err = fstat(hdl->fd, &nvme_stat);

	if (err < 0)
		return -errno;

	if (!S_ISCHR(nvme_stat.st_mode))
		return -EINVAL;
	return 0;
}

__shr_public int libnvme_reset_subsystem(
		struct libnvme_transport_handle *hdl)
{
	int ret;

	ret = nvme_verify_chr(hdl);
	if (ret)
		return ret;

	ret = ioctl(hdl->fd, LIBNVME_IOCTL_SUBSYS_RESET);
	if (ret < 0)
		return -errno;
	return ret;
}

__shr_public int libnvme_reset_ctrl(struct libnvme_transport_handle *hdl)
{
	int ret;

	ret = nvme_verify_chr(hdl);
	if (ret)
		return ret;

	ret = ioctl(hdl->fd, LIBNVME_IOCTL_RESET);
	if (ret < 0)
		return -errno;
	return ret;
}

__shr_public int libnvme_rescan_ns(struct libnvme_transport_handle *hdl)
{
	int ret;

	ret = nvme_verify_chr(hdl);
	if (ret)
		return ret;

	ret = ioctl(hdl->fd, LIBNVME_IOCTL_RESCAN);
	if (ret < 0)
		return -errno;
	return ret;
}

__shr_public int libnvme_get_nsid(
		struct libnvme_transport_handle *hdl, __u32 *nsid)
{
	__u32 tmp;

	errno = 0;
	tmp = ioctl(hdl->fd, LIBNVME_IOCTL_ID);
	if (errno)
		return -errno;

	*nsid = tmp;
	return 0;
}

__shr_public int libnvme_update_block_size(
		struct libnvme_transport_handle *hdl, int block_size)
{
	int ret;
	libnvme_fd_t fd = libnvme_transport_handle_get_fd(hdl);

	ret = ioctl(fd, BLKBSZSET, &block_size);
	if (ret < 0)
		return -errno;

	ret = ioctl(fd, BLKRRPART);
	if (ret < 0)
		return -errno;

	return 0;
}

/*
 * The 64 bit version is the preferred version to use, but for backwards
 * compatibility keep a 32 version.
 */
static int ioctl_passthru32(struct libnvme_transport_handle *hdl,
		unsigned long ioctl_cmd, struct libnvme_passthru_cmd *cmd)
{
	struct linux_passthru_cmd32 cmd32 = {};
	void *user_data;
	int err = 0;

	user_data = hdl->submit_entry(hdl, cmd);
	if (hdl->ctx->dry_run)
		goto out;

	memcpy(&cmd32, cmd, offsetof(struct linux_passthru_cmd32, result));
	cmd32.result = 0;

	do {
		err = ioctl(hdl->fd, ioctl_cmd, &cmd32);
		if (err >= 0)
			break;
		err = -errno;
	} while (hdl->decide_retry(hdl, cmd, err));

out:
	cmd->result = cmd32.result;
	hdl->submit_exit(hdl, cmd, err, user_data);
	return err;
}

/*
 * supported since kernel 5.4, see
 * 65e68edce0db ("nvme: allow 64-bit results in passthru commands")
 */
static int ioctl_passthru64(struct libnvme_transport_handle *hdl,
		unsigned long ioctl_cmd, struct libnvme_passthru_cmd *cmd)
{
	void *user_data;
	int err = 0;

	user_data = hdl->submit_entry(hdl, cmd);
	if (hdl->ctx->dry_run)
		goto out;

	do {
		/*
		 * struct nvme_passtrhu_cmd is identically to struct
		 * linux_passthru_cmd64, thus just pass it in directly.
		 */
		err = ioctl(hdl->fd, ioctl_cmd, cmd);
		if (err >= 0)
			break;
		err = -errno;
	} while (hdl->decide_retry(hdl, cmd, err));

out:
	hdl->submit_exit(hdl, cmd, err, user_data);
	return err;
}

static int ioctl_io_passthru(struct libnvme_transport_handle *hdl,
		struct libnvme_passthru_cmd *cmd)
{
	int err;

	if (hdl->ioctl_io_state == IOCTL_STATE_IOCTL64)
		return ioctl_passthru64(hdl,
			LIBNVME_IOCTL_IO64_CMD, cmd);

	if (hdl->ioctl_io_state == IOCTL_STATE_IOCTL32 ||
			!hdl->ctx->ioctl_probing)
		goto do_ioctl32;

	err = ioctl_passthru64(hdl, LIBNVME_IOCTL_IO64_CMD, cmd);
	if (err >= 0 || err != -ENOTTY) {
		hdl->ioctl_io_state = IOCTL_STATE_IOCTL64;
		return err;
	}

	hdl->ioctl_io_state = IOCTL_STATE_IOCTL32;

do_ioctl32:
	return ioctl_passthru32(hdl, LIBNVME_IOCTL_IO_CMD, cmd);
}

static int ioctl_admin_passthru(struct libnvme_transport_handle *hdl,
		struct libnvme_passthru_cmd *cmd)
{
	int err;

	if (hdl->ioctl_admin_state == IOCTL_STATE_IOCTL64)
		return ioctl_passthru64(hdl,
				LIBNVME_IOCTL_ADMIN64_CMD, cmd);

	if (hdl->ioctl_admin_state == IOCTL_STATE_IOCTL32 ||
			!hdl->ctx->ioctl_probing)
		goto do_ioctl32;

	err = ioctl_passthru64(hdl, LIBNVME_IOCTL_ADMIN64_CMD, cmd);
	if (err >= 0 || err != -ENOTTY) {
		hdl->ioctl_admin_state = IOCTL_STATE_IOCTL64;
		return err;
	}

	hdl->ioctl_admin_state = IOCTL_STATE_IOCTL32;

do_ioctl32:
	if (cmd->opcode == nvme_admin_fabrics)
		return -ENOTSUP;

	return ioctl_passthru32(hdl, LIBNVME_IOCTL_ADMIN_CMD, cmd);
}

__shr_public int libnvme_exec_admin_passthru(
		struct libnvme_transport_handle *hdl,
		struct libnvme_passthru_cmd *cmd)
{
	struct libnvme_passthru_completion completion;
	int err;

	if (!hdl)
		return -ENODEV;

	if (!cmd->timeout_ms && hdl->timeout)
		cmd->timeout_ms = hdl->timeout;

	if (hdl->type != LIBNVME_TRANSPORT_HANDLE_TYPE_DIRECT)
		goto no_uring;

	if (hdl->uring_state == LIBNVME_IO_URING_STATE_NOT_AVAILABLE)
		goto no_uring;

	err = libnvme_submit_admin_passthru(hdl, cmd, NULL);
	if (err) {
		if (err == -ENOTSUP)
			goto no_uring;

		return err;
	}

	err = libnvme_reap_passthru(hdl, &completion);
	if (err)
		return err;

	return completion.status;

no_uring:
	switch (hdl->type) {
	case LIBNVME_TRANSPORT_HANDLE_TYPE_DIRECT:
		return ioctl_admin_passthru(hdl, cmd);
	case LIBNVME_TRANSPORT_HANDLE_TYPE_MI:
		return libnvme_mi_admin_admin_passthru(hdl, cmd);
	case LIBNVME_TRANSPORT_HANDLE_TYPE_LOOPBACK:
		return __libnvme_loopback_admin_passthru(hdl, cmd);
	default:
		break;
	}

	return -ENOTSUP;
}

__shr_public int libnvme_exec_io_passthru(
		struct libnvme_transport_handle *hdl,
		struct libnvme_passthru_cmd *cmd)
{
	struct libnvme_passthru_completion completion;
	int err;

	if (!hdl)
		return -ENODEV;

	if (!cmd->timeout_ms && hdl->timeout)
		cmd->timeout_ms = hdl->timeout;

	if (hdl->type != LIBNVME_TRANSPORT_HANDLE_TYPE_DIRECT)
		goto no_uring;

	if (hdl->uring_state == LIBNVME_IO_URING_STATE_NOT_AVAILABLE)
		goto no_uring;

	err = libnvme_submit_io_passthru(hdl, cmd, NULL);
	if (err) {
		if (err == -ENOTSUP)
			goto no_uring;

		return err;
	}

	err = libnvme_reap_passthru(hdl, &completion);
	if (err)
		return err;

	return completion.status;

no_uring:
	switch (hdl->type) {
	case LIBNVME_TRANSPORT_HANDLE_TYPE_DIRECT:
		return ioctl_io_passthru(hdl, cmd);
	case LIBNVME_TRANSPORT_HANDLE_TYPE_LOOPBACK:
		return __libnvme_loopback_io_passthru(hdl, cmd);
	default:
		break;
	}

	return -ENOTSUP;
}
