/* SPDX-License-Identifier: MIT */
/*
 * Description: tests linked requests failing during submission
 */
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <assert.h>

#include "liburing.h"

#define DRAIN_USER_DATA 42

static int test_underprep_fail(bool hardlink, bool drain, bool link_last,
			       int link_size, int fail_idx)
{
	const int invalid_fd = 42;
	int link_flags = IOSQE_IO_LINK;
	int total_submit = link_size;
	struct io_uring ring;
	struct io_uring_sqe *sqe;
	struct io_uring_cqe *cqe;
	char buffer[1];
	int i, ret, fds[2];

	if (drain)
		link_flags |= IOSQE_IO_DRAIN;
	if (hardlink)
		link_flags |= IOSQE_IO_HARDLINK;

	assert(fail_idx < link_size);
	assert(link_size < 40);

	/* create a new ring as it leaves it dirty */
	ret = io_uring_queue_init(8, &ring, 0);
	if (ret) {
		printf("ring setup failed\n");
		return -1;
	}
	if (pipe(fds)) {
		perror("pipe");
		return -1;
	}

	if (drain) {
		/* clog drain, so following reqs sent to draining */
		sqe = io_uring_get_sqe(&ring);
		io_uring_prep_read(sqe, fds[0], buffer, sizeof(buffer), 0);
		sqe->user_data = DRAIN_USER_DATA;
		sqe->flags |= IOSQE_IO_DRAIN;
		total_submit++;
	}

	for (i = 0; i < link_size; i++) {
		sqe = io_uring_get_sqe(&ring);
		if (i == fail_idx) {
			io_uring_prep_read(sqe, invalid_fd, buffer, 1, 0);
			sqe->ioprio = (short) -1;
		} else {
			io_uring_prep_nop(sqe);
		}

		if (i != link_size - 1 || !link_last)
			sqe->flags |= link_flags;
		sqe->user_data = i;
	}

	ret = io_uring_submit(&ring);
	if (ret != total_submit) {
		/* Old behaviour, failed early and under-submitted */
		if (ret == fail_idx + 1 + drain)
			goto out;
		fprintf(stderr, "submit failed: %d\n", ret);
		return -1;
	}

	if (drain) {
		/* unclog drain */
		ret = write(fds[1], buffer, sizeof(buffer));
		if (ret < 0) {
			perror("write");
			return 1;
		}
	}

	for (i = 0; i < total_submit; i++) {
		ret = io_uring_wait_cqe(&ring, &cqe);
		if (ret) {
			fprintf(stderr, "wait_cqe=%d\n", ret);
			return 1;
		}

		ret = cqe->res;
		if (cqe->user_data == DRAIN_USER_DATA) {
			if (ret != 1) {
				fprintf(stderr, "drain failed %d\n", ret);
				return 1;
			}
		} else if (cqe->user_data == fail_idx) {
			if (ret == 0 || ret == -ECANCELED) {
				fprintf(stderr, "half-prep req unexpected return %d\n", ret);
				return 1;
			}
		} else {
			if (ret != -ECANCELED) {
				fprintf(stderr, "cancel failed %d, ud %d\n", ret, (int)cqe->user_data);
				return 1;
			}
		}
		io_uring_cqe_seen(&ring, cqe);
	}
out:
	close(fds[0]);
	close(fds[1]);
	io_uring_queue_exit(&ring);
	return 0;
}

int main(int argc, char *argv[])
{
	int ret, link_size, fail_idx, i;

	if (argc > 1)
		return 0;

	/*
	 * hardlink, size=3, fail_idx=1, drain=false -- kernel fault
	 * link, size=3, fail_idx=0, drain=true -- kernel fault
	 * link, size=3, fail_idx=1, drain=true -- invalid cqe->res
	 */
	for (link_size = 0; link_size < 3; link_size++) {
		for (fail_idx = 0; fail_idx < link_size; fail_idx++) {
			for (i = 0; i < 8; i++) {
				bool hardlink = (i & 1) != 0;
				bool drain = (i & 2) != 0;
				bool link_last = (i & 4) != 0;

				ret = test_underprep_fail(hardlink, drain, link_last,
							  link_size, fail_idx);
				if (!ret)
					continue;

				fprintf(stderr, "failed %d, hard %d, drain %d,"
						"link_last %d, size %d, idx %d\n",
						ret, hardlink, drain, link_last,
						link_size, fail_idx);
				return 1;
			}
		}
	}

	return 0;
}
