/*
 *  iSCSI digest handling.
 *
 *  Copyright (C) 2004 - 2006 Xiranet Communications GmbH
 *                            <arne.redlich@xiranet.com>
 *  Copyright (C) 2007 - 2018 Vladislav Bolkhovitin
 *  Copyright (C) 2007 - 2018 Western Digital Corporation
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU General Public License
 *  as published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *  GNU General Public License for more details.
 */

#include <linux/types.h>
#include <linux/scatterlist.h>

#include "iscsi_trace_flag.h"
#include "iscsi.h"
#include "digest.h"
#include <linux/crc32c.h>

void digest_alg_available(int *val)
{
#if defined(CONFIG_LIBCRC32C_MODULE) || defined(CONFIG_LIBCRC32C)
	int crc32c = 1;
#else
	int crc32c = 0;
#endif

	if ((*val & DIGEST_CRC32C) && !crc32c) {
		PRINT_ERROR("CRC32C digest algorithm not available in kernel");
		*val |= ~DIGEST_CRC32C;
	}
}

/**
 * digest_init - initialize support for digest calculation.
 * @conn: ptr to connection to make use of digests
 * Returns: 0 on success, < 0 on error
 */
int digest_init(struct iscsi_conn *conn)
{
	if (!(conn->hdigest_type & DIGEST_ALL))
		conn->hdigest_type = DIGEST_NONE;

	if (!(conn->ddigest_type & DIGEST_ALL))
		conn->ddigest_type = DIGEST_NONE;

	return 0;
}

static __be32 evaluate_crc32_from_sg(struct scatterlist *sg, int nbytes, uint32_t padding)
{
	u32 crc = ~0;

#ifdef CONFIG_SCST_ISCSI_DEBUG_DIGEST_FAILURES
	if (((scst_random() % 100000) == 752)) {
		PRINT_INFO("%s", "Simulating digest failure");
		return 0;
	}
#endif

#if defined(CONFIG_LIBCRC32C_MODULE) || defined(CONFIG_LIBCRC32C)
	{
		int pad_bytes = ((nbytes + 3) & -4) - nbytes;

		while (nbytes > 0) {
			int d = min(nbytes, (int)(sg->length));

			crc = crc32c(crc, sg_virt(sg), d);
			nbytes -= d;
			sg++;
		}

		if (pad_bytes)
			crc = crc32c(crc, (u8 *)&padding, pad_bytes);
	}
#endif

	return (__force __be32)~cpu_to_le32(crc);
}

static __be32 digest_header(struct iscsi_pdu *pdu)
{
	struct scatterlist sg[2];
	unsigned int nbytes = sizeof(struct iscsi_hdr);
	int asize = (pdu->ahssize + 3) & -4;

	sg_init_table(sg, 2);

	sg_set_buf(&sg[0], &pdu->bhs, nbytes);
	if (pdu->ahssize) {
		sg_set_buf(&sg[1], pdu->ahs, asize);
		nbytes += asize;
	}
	EXTRACHECKS_BUG_ON((nbytes & 3) != 0);
	return evaluate_crc32_from_sg(sg, nbytes, 0);
}

static __be32 digest_data(struct iscsi_cmnd *cmd, u32 size, u32 offset, uint32_t padding)
{
	struct scatterlist *sg = cmd->sg;
	int idx, count;
	struct scatterlist saved_sg;
	__be32 crc;

	offset += sg[0].offset;
	idx = offset >> PAGE_SHIFT;
	offset &= ~PAGE_MASK;

	count = get_pgcnt(size, offset);

	TRACE_DBG("req %p, idx %d, count %d, sg_cnt %d, size %d, offset %d",
		  cmd, idx, count, cmd->sg_cnt, size, offset);
	sBUG_ON(idx + count > cmd->sg_cnt);

	saved_sg = sg[idx];
	sg[idx].offset = offset;
	sg[idx].length -= offset - saved_sg.offset;

	crc = evaluate_crc32_from_sg(sg + idx, size, padding);

	sg[idx] = saved_sg;
	return crc;
}

int digest_rx_header(struct iscsi_cmnd *cmnd)
{
	__be32 crc;

	crc = digest_header(&cmnd->pdu);
	if (unlikely(crc != cmnd->hdigest)) {
		PRINT_ERROR("%s", "RX header digest failed");
		return -EIO;
	}

	TRACE_DBG("RX header digest OK for cmd %p", cmnd);

	return 0;
}

void digest_tx_header(struct iscsi_cmnd *cmnd)
{
	cmnd->hdigest = digest_header(&cmnd->pdu);
	TRACE_DBG("TX header digest for cmd %p: %x", cmnd, cmnd->hdigest);
}

int digest_rx_data(struct iscsi_cmnd *cmnd)
{
	struct iscsi_cmnd *req;
	struct iscsi_data_out_hdr *req_hdr;
	u32 offset;
	__be32 crc;
	int res = 0;

	switch (cmnd_opcode(cmnd)) {
	case ISCSI_OP_SCSI_DATA_OUT:
		req = cmnd->cmd_req;
		if (unlikely(!req)) {
			/* It can be for prelim completed commands */
			req = cmnd;
			goto out;
		}
		req_hdr = (struct iscsi_data_out_hdr *)&cmnd->pdu.bhs;
		offset = be32_to_cpu(req_hdr->buffer_offset);
		break;

	default:
		req = cmnd;
		offset = 0;
	}

	/*
	 * We need to skip the digest check for prelim completed commands,
	 * because we use shared data buffer for them, so, most likely, the
	 * check will fail. Plus, for such commands we sometimes don't have
	 * sg_cnt set correctly (cmnd_prepare_get_rejected_cmd_data() doesn't
	 * do it).
	 */
	if (unlikely(req->prelim_compl_flags != 0))
		goto out;

	/*
	 * Temporary to not crash with write residual overflows. ToDo. Until
	 * that let's always have succeeded data digests for such overflows.
	 * In ideal, we should allocate additional one or more sg's for the
	 * overflowed data and free them here or on req release. It's quite
	 * not trivial for such virtually never used case, so let's do it,
	 * when it gets needed.
	 */
	if (unlikely(offset + cmnd->pdu.datasize > req->bufflen)) {
		PRINT_WARNING("Skipping RX data digest check for residual overflow command op %x (data size %d, buffer size %d)",
			      cmnd_hdr(req)->scb[0], offset + cmnd->pdu.datasize,
			      req->bufflen);
		goto out;
	}

	crc = digest_data(req, cmnd->pdu.datasize, offset, cmnd->conn->rpadding);

	if (unlikely(crc != cmnd->ddigest)) {
		PRINT_ERROR("RX data digest failed, stable pages disabled?");
		TRACE_MGMT_DBG("Calculated crc %x, ddigest %x, offset %d", crc,
			       cmnd->ddigest, offset);
		iscsi_dump_pdu(&cmnd->pdu);
		res = -EIO;
	} else {
		TRACE_DBG("RX data digest OK for cmd %p", cmnd);
	}

out:
	return res;
}

void digest_tx_data(struct iscsi_cmnd *cmnd)
{
	struct iscsi_data_in_hdr *hdr;
	u32 offset;

	TRACE_DBG("%s:%d req %p, own_sg %d, sg %p, sgcnt %d cmnd %p, own_sg %d, sg %p, sgcnt %d",
		  __func__, __LINE__,
		cmnd->parent_req, cmnd->parent_req->own_sg,
		cmnd->parent_req->sg, cmnd->parent_req->sg_cnt,
		cmnd, cmnd->own_sg, cmnd->sg, cmnd->sg_cnt);

	switch (cmnd_opcode(cmnd)) {
	case ISCSI_OP_SCSI_DATA_IN:
		hdr = (struct iscsi_data_in_hdr *)&cmnd->pdu.bhs;
		offset = be32_to_cpu(hdr->buffer_offset);
		break;
	default:
		offset = 0;
	}

	cmnd->ddigest = digest_data(cmnd, cmnd->pdu.datasize, offset, 0);
	TRACE_DBG("TX data digest for cmd %p: %x (offset %d, opcode %x)", cmnd,
		  cmnd->ddigest, offset, cmnd_opcode(cmnd));
}
