/* SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later */
// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp)
#include <libpldm/instance-id.h>
#include <libpldm/pldm.h>

#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>

#define BIT(i) (1UL << (i))

#define PLDM_TID_MAX	 256
#define PLDM_INST_ID_MAX 32

/* We need to track our allocations explicitly due to OFD lock merging/splitting
 */
struct pldm_tid_state {
	pldm_instance_id_t prev;
	uint32_t allocations;
};

struct pldm_instance_db {
	struct pldm_tid_state state[PLDM_TID_MAX];
	int lock_db_fd;
};

static inline int iid_next(pldm_instance_id_t cur)
{
	return (cur + 1) % PLDM_INST_ID_MAX;
}

LIBPLDM_ABI_STABLE
int pldm_instance_db_init(struct pldm_instance_db **ctx, const char *dbpath)
{
	struct pldm_instance_db *l_ctx;
	struct stat statbuf;
	int rc;

	/* Make sure the provided pointer was initialised to NULL. In the future
	 * if we stabilise the ABI and expose the struct definition the caller
	 * can potentially pass a valid pointer to a struct they've allocated
	 */
	if (!ctx || *ctx) {
		return -EINVAL;
	}

	/* Ensure the underlying file is sized for properly managing allocations
	 */
	rc = stat(dbpath, &statbuf);
	if (rc < 0) {
		return -EINVAL;
	}

	if (statbuf.st_size <
	    ((off_t)(PLDM_TID_MAX) * (off_t)(PLDM_INST_ID_MAX))) {
		return -EINVAL;
	}

	l_ctx = calloc(1, sizeof(struct pldm_instance_db));
	if (!l_ctx) {
		return -ENOMEM;
	}

	/* Initialise previous ID values so the next one is zero */
	for (int i = 0; i < PLDM_TID_MAX; i++) {
		l_ctx->state[i].prev = 31;
	}

	/* Lock database may be read-only, either by permissions or mountpoint
	 */
	l_ctx->lock_db_fd = open(dbpath, O_RDONLY | O_CLOEXEC);
	if (l_ctx->lock_db_fd < 0) {
		free(l_ctx);
		return -errno;
	}
	*ctx = l_ctx;

	return 0;
}

LIBPLDM_ABI_STABLE
int pldm_instance_db_init_default(struct pldm_instance_db **ctx)
{
	return pldm_instance_db_init(ctx,
				     "/usr/share/libpldm/instance-db/default");
}

LIBPLDM_ABI_STABLE
int pldm_instance_db_destroy(struct pldm_instance_db *ctx)
{
	if (!ctx) {
		return 0;
	}
	close(ctx->lock_db_fd);
	free(ctx);
	return 0;
}

static const struct flock pldm_instance_id_cfls = {
	.l_type = F_RDLCK,
	.l_whence = SEEK_SET,
	.l_len = 1,
};

static const struct flock pldm_instance_id_cflx = {
	.l_type = F_WRLCK,
	.l_whence = SEEK_SET,
	.l_len = 1,
};

static const struct flock pldm_instance_id_cflu = {
	.l_type = F_UNLCK,
	.l_whence = SEEK_SET,
	.l_len = 1,
};

LIBPLDM_ABI_STABLE
int pldm_instance_id_alloc(struct pldm_instance_db *ctx, pldm_tid_t tid,
			   pldm_instance_id_t *iid)
{
	uint8_t l_iid;

	if (!iid) {
		return -EINVAL;
	}

	l_iid = ctx->state[tid].prev;
	if (l_iid >= PLDM_INST_ID_MAX) {
		return -EPROTO;
	}

	while ((l_iid = iid_next(l_iid)) != ctx->state[tid].prev) {
		struct flock flop;
		off_t loff;
		int rc;

		/* Have we already allocated this instance ID? */
		if (ctx->state[tid].allocations & BIT(l_iid)) {
			continue;
		}

		/* Derive the instance ID offset in the lock database */
		loff = tid * PLDM_INST_ID_MAX + l_iid;

		/* Reserving the TID's IID. Done via a shared lock */
		flop = pldm_instance_id_cfls;
		flop.l_start = loff;
		rc = fcntl(ctx->lock_db_fd, F_OFD_SETLK, &flop);
		if (rc < 0) {
			if (errno == EAGAIN || errno == EINTR) {
				return -EAGAIN;
			}
			return -EPROTO;
		}

		/*
		 * If we *may* promote the lock to exclusive then this IID is
		 * only reserved by us. This is now our allocated IID.
		 *
		 * If we *may not* promote the lock to exclusive then this IID
		 * is also reserved on another file descriptor. Move on to the
		 * next IID index.
		 *
		 * Note that we cannot actually *perform* the promotion in
		 * practice because this is prevented by the lock database being
		 * opened O_RDONLY.
		 */
		flop = pldm_instance_id_cflx;
		flop.l_start = loff;
		rc = fcntl(ctx->lock_db_fd, F_OFD_GETLK, &flop);
		if (rc < 0) {
			if (errno == EAGAIN || errno == EINTR) {
				rc = -EAGAIN;
				goto release_cfls;
			}
			rc = -EPROTO;
			goto release_cfls;
		}

		/* F_UNLCK is the type of the lock if we could successfully
		 * promote it to F_WRLCK */
		if (flop.l_type == F_UNLCK) {
			ctx->state[tid].prev = l_iid;
			ctx->state[tid].allocations |= BIT(l_iid);
			*iid = l_iid;
			return 0;
		}

		if (flop.l_type != F_RDLCK) {
			rc = -EPROTO;
		}

	release_cfls:
		flop = pldm_instance_id_cflu;
		flop.l_start = loff;
		if (fcntl(ctx->lock_db_fd, F_OFD_SETLK, &flop) < 0) {
			if (errno == EAGAIN || errno == EINTR) {
				return -EAGAIN;
			}
			return -EPROTO;
		}

		if (rc < 0) {
			return rc;
		}
	}

	/* Failed to allocate an IID after a full loop. Make the caller try
	 * again */
	return -EAGAIN;
}

LIBPLDM_ABI_STABLE
int pldm_instance_id_free(struct pldm_instance_db *ctx, pldm_tid_t tid,
			  pldm_instance_id_t iid)
{
	struct flock flop;
	int rc;

	/* Trying to free an instance ID that is not currently allocated */
	if (!(ctx->state[tid].allocations & BIT(iid))) {
		return -EINVAL;
	}

	flop = pldm_instance_id_cflu;
	flop.l_start = tid * PLDM_INST_ID_MAX + iid;
	rc = fcntl(ctx->lock_db_fd, F_OFD_SETLK, &flop);
	if (rc < 0) {
		if (errno == EAGAIN || errno == EINTR) {
			return -EAGAIN;
		}
		return -EPROTO;
	}

	/* Mark the instance ID as no-longer allocated */
	ctx->state[tid].allocations &= ~BIT(iid);

	return 0;
}