// SPDX-License-Identifier: GPL-2.0

/*
 * Copyright (c) 2018, DataDirect Networks Storage.
 *
 * General data integrity functions
 *
 * Author: Li Xi.
 */

#include <linux/blkdev.h>
#include <linux/crc-t10dif.h>
#include <asm/checksum.h>
#include <obd_class.h>
#include <obd_cksum.h>

#if IS_ENABLED(CONFIG_CRC_T10DIF)
__be16 obd_dif_crc_fn(void *data, unsigned int len)
{
	return cpu_to_be16(crc_t10dif(data, len));
}
EXPORT_SYMBOL(obd_dif_crc_fn);

__be16 obd_dif_ip_fn(void *data, unsigned int len)
{
	return (__force __be16)ip_compute_csum(data, len);
}
EXPORT_SYMBOL(obd_dif_ip_fn);

int obd_page_dif_generate_buffer(const char *obd_name, struct folio *folio,
				 __s32 pgno, __u32 start, __u32 length,
				 __be16 *guard_start, int guard_number,
				 int *used_number, int sector_size,
				 obd_dif_csum_fn *fn)
{
	unsigned int off = start;
	unsigned int end = start + length;
	void *addr;
	char *data_buf;
	__be16 *guard_buf = guard_start;
	unsigned int data_size;
	int guard_used = 0;
	int rc = 0;

	addr = kmap_local_folio(folio, (pgno > 0 ? pgno : 0) * PAGE_SIZE);
	data_buf = addr + start;
	while (off < end) {
		if (guard_used >= guard_number) {
			rc = -E2BIG;
			CERROR("%s: used %u >= guard %u, data %u+%u, sector_size %u: rc = %d\n",
			       obd_name, guard_used, guard_number, start,
			       length, sector_size, rc);
			goto out;
		}
		data_size = min(round_up(off + 1, sector_size), end) - off;
		*guard_buf = fn(data_buf, data_size);
		guard_buf++;
		guard_used++;
		data_buf += data_size;
		off += data_size;
	}
	*used_number = guard_used;
out:
	kunmap_local(addr);

	return rc;
}
EXPORT_SYMBOL(obd_page_dif_generate_buffer);

static int __obd_t10_performance_test(const char *obd_name,
				      enum cksum_types cksum_type,
				      struct folio *data_page,
				      int repeat_number)
{
	unsigned char cfs_alg = cksum_obd2cfs(OBD_CKSUM_T10_TOP);
	struct ahash_request *req;
	obd_dif_csum_fn *fn = NULL;
	unsigned int bufsize = 0;
	unsigned char *buffer;
	struct folio *__folio;
	__be16 *guard_start;
	int guard_number;
	int used_number = 0;
	int sector_size = 0;
	__u32 cksum;
	int rc = 0;
	int rc2;
	int used;
	int i;

	obd_t10_cksum2dif(cksum_type, &fn, &sector_size);
	if (!fn)
		return -EINVAL;

	__folio = folio_alloc(GFP_KERNEL, 0);
	if (IS_ERR_OR_NULL(__folio))
		return -ENOMEM;

	req = cfs_crypto_hash_init(cfs_alg, NULL, 0);
	if (IS_ERR(req)) {
		rc = PTR_ERR(req);
		CERROR("%s: unable to initialize checksum hash %s: rc = %d\n",
		       obd_name, cfs_crypto_hash_name(cfs_alg), rc);
		GOTO(out, rc);
	}

	buffer = kmap_local_folio(__folio, 0);
	guard_start = (__be16 *)buffer;
	guard_number = PAGE_SIZE / sizeof(*guard_start);
	for (i = 0; i < repeat_number; i++) {
		/* The left guard number should be able to hold checksums of a
		 * whole page
		 */
		rc = obd_page_dif_generate_buffer(obd_name, data_page, 0,
						  0, PAGE_SIZE,
						  guard_start + used_number,
						  guard_number - used_number,
						  &used, sector_size, fn);
		if (rc)
			break;

		used_number += used;
		if (used_number == guard_number) {
			cfs_crypto_hash_update_page(req, fpgptr(__folio), 0,
				used_number * sizeof(*guard_start));
			used_number = 0;
		}
	}
	kunmap_local(buffer);
	if (rc)
		GOTO(out_final, rc);

	if (used_number != 0)
		cfs_crypto_hash_update_page(req, fpgptr(__folio), 0,
			used_number * sizeof(*guard_start));

	bufsize = sizeof(cksum);
out_final:
	rc2 = cfs_crypto_hash_final(req, (unsigned char *)&cksum, &bufsize);
	rc = rc ? rc : rc2;
out:
	folio_put(__folio);

	return rc;
}

/**
 *  Array of T10PI checksum algorithm speed in MByte per second
 */
static int obd_t10_cksum_speeds[OBD_T10_CKSUM_MAX];

static enum obd_t10_cksum_type
obd_t10_cksum2type(enum cksum_types cksum_type)
{
	switch (cksum_type) {
	case OBD_CKSUM_T10IP512:
		return OBD_T10_CKSUM_IP512;
	case OBD_CKSUM_T10IP4K:
		return OBD_T10_CKSUM_IP4K;
	case OBD_CKSUM_T10CRC512:
		return OBD_T10_CKSUM_CRC512;
	case OBD_CKSUM_T10CRC4K:
		return OBD_T10_CKSUM_CRC4K;
	default:
		return OBD_T10_CKSUM_UNKNOWN;
	}
}

static const char *obd_t10_cksum_name(enum obd_t10_cksum_type index)
{
	/* Need to skip "crc32", "adler", "crc32c", "reserved" */
	return cksum_name[3 + index];
}

/**
 * Compute the speed of specified T10PI checksum type
 *
 * Run a speed test on the given T10PI checksum on buffer using a 1MB buffer
 * size. This is a reasonable buffer size for Lustre RPCs, even if the actual
 * RPC size is larger or smaller.
 *
 * The speed is stored internally in the obd_t10_cksum_speeds[] array, and
 * is available through the obd_t10_cksum_speed() function.
 *
 * This function needs to stay the same as cfs_crypto_performance_test() so
 * that the speeds are comparable. And this function should reflect the real
 * cost of the checksum calculation.
 *
 * \param[in] obd_name		name of the OBD device
 * \param[in] cksum_type	checksum type (OBD_CKSUM_T10*)
 */
static void obd_t10_performance_test(const char *obd_name,
				     enum cksum_types cksum_type)
{
	enum obd_t10_cksum_type index = obd_t10_cksum2type(cksum_type);
	const int buf_len = max(PAGE_SIZE, 1048576UL);
	unsigned long bcount;
	unsigned long start;
	unsigned long end;
	struct folio *folio;
	int rc = 0;
	void *buf;

	folio = folio_alloc(GFP_KERNEL, 0);
	if (IS_ERR_OR_NULL(folio)) {
		rc = -ENOMEM;
		goto out;
	}

	buf = kmap_local_folio(folio, 0);
	memset(buf, 0xAD, PAGE_SIZE);
	kunmap_local(buf);

	for (start = jiffies, end = start + cfs_time_seconds(1) / 4,
	     bcount = 0; time_before(jiffies, end) && rc == 0; bcount++) {
		rc = __obd_t10_performance_test(obd_name, cksum_type, folio,
						buf_len >> PAGE_SHIFT);
		if (rc)
			break;
	}
	end = jiffies;
	folio_put(folio);
out:
	if (rc) {
		obd_t10_cksum_speeds[index] = rc;
		CDEBUG(D_INFO, "%s: T10 checksum algorithm %s test error: "
		       "rc = %d\n", obd_name, obd_t10_cksum_name(index), rc);
	} else {
		unsigned long tmp;

		tmp = ((bcount * buf_len / jiffies_to_msecs(end - start)) *
		       1000) / (1024 * 1024);
		obd_t10_cksum_speeds[index] = (int)tmp;
		CDEBUG(D_CONFIG, "%s: T10 checksum algorithm %s speed = %d "
		       "MB/s\n", obd_name, obd_t10_cksum_name(index),
		       obd_t10_cksum_speeds[index]);
	}
}
#endif /* CONFIG_CRC_T10DIF */

int obd_t10_cksum_speed(const char *obd_name,
			enum cksum_types cksum_type)
{
#if IS_ENABLED(CONFIG_CRC_T10DIF)
	enum obd_t10_cksum_type index = obd_t10_cksum2type(cksum_type);

	if (unlikely(obd_t10_cksum_speeds[index] == 0)) {
		static DEFINE_MUTEX(obd_t10_cksum_speed_mutex);

		mutex_lock(&obd_t10_cksum_speed_mutex);
		if (obd_t10_cksum_speeds[index] == 0)
			obd_t10_performance_test(obd_name, cksum_type);
		mutex_unlock(&obd_t10_cksum_speed_mutex);
	}

	return obd_t10_cksum_speeds[index];
#else /* !CONFIG_CRC_T10DIF */
	return 0;
#endif /* !CONFIG_CRC_T10DIF */
}
EXPORT_SYMBOL(obd_t10_cksum_speed);
