/*
   Copyright (C) 2023 by zhenwei pi <pizhenwei@bytedance.com>

   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; either version 2 of the License, or
   (at your option) any later version.

   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.

   You should have received a copy of the GNU General Public License
   along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <inttypes.h>
#include <string.h>
#include <poll.h>
#include <getopt.h>
#include "iscsi.h"
#include "scsi-lowlevel.h"

/* MD5 related codes come from glibc with a few change(to avoid symbol conflict) */
# if __BYTE_ORDER == __BIG_ENDIAN
#  define WORDS_BIGENDIAN 1
# endif

#ifdef WORDS_BIGENDIAN
# define SWAP(n)							\
	(((n) << 24) | (((n) & 0xff00) << 8) | (((n) >> 8) & 0xff00) | ((n) >> 24))
#else
# define SWAP(n) (n)
#endif

typedef unsigned int libiscsi_md5_uint32;

struct libiscsi_md5_ctx {
	libiscsi_md5_uint32 A;
	libiscsi_md5_uint32 B;
	libiscsi_md5_uint32 C;
	libiscsi_md5_uint32 D;

	libiscsi_md5_uint32 total[2];
	libiscsi_md5_uint32 buflen;
	union
	{
		char buffer[128];
		libiscsi_md5_uint32 buffer32[32];
	};
};

/* These are the four functions used in the four steps of the MD5 algorithm
   and defined in the RFC 1321.  The first function is a little bit optimized
   (as found in Colin Plumbs public domain implementation).  */
/* #define FF(b, c, d) ((b & c) | (~b & d)) */
#define FF(b, c, d) (d ^ (b & (c ^ d)))
#define FG(b, c, d) FF (d, b, c)
#define FH(b, c, d) (b ^ c ^ d)
#define FI(b, c, d) (c ^ (b | ~d))

/* Process LEN bytes of BUFFER, accumulating context into CTX.
   It is assumed that LEN % 64 == 0.  */

static void
libiscsi_md5_process_block (const void *buffer, size_t len, struct libiscsi_md5_ctx *ctx)
{
	libiscsi_md5_uint32 correct_words[16];
	const libiscsi_md5_uint32 *words = buffer;
	size_t nwords = len / sizeof (libiscsi_md5_uint32);
	const libiscsi_md5_uint32 *endp = words + nwords;
	libiscsi_md5_uint32 A = ctx->A;
	libiscsi_md5_uint32 B = ctx->B;
	libiscsi_md5_uint32 C = ctx->C;
	libiscsi_md5_uint32 D = ctx->D;
	libiscsi_md5_uint32 lolen = len;

	/* First increment the byte count.  RFC 1321 specifies the possible
	   length of the file up to 2^64 bits.  Here we only compute the
	   number of bytes.  Do a double word increment.  */
	ctx->total[0] += lolen;
	ctx->total[1] += (len >> 31 >> 1) + (ctx->total[0] < lolen);

	/* Process all bytes in the buffer with 64 bytes in each round of
	   the loop.  */
	while (words < endp)
	{
		libiscsi_md5_uint32 *cwp = correct_words;
		libiscsi_md5_uint32 A_save = A;
		libiscsi_md5_uint32 B_save = B;
		libiscsi_md5_uint32 C_save = C;
		libiscsi_md5_uint32 D_save = D;

		/* First round: using the given function, the context and a constant
		   the next context is computed.  Because the algorithms processing
		   unit is a 32-bit word and it is determined to work on words in
		   little endian byte order we perhaps have to change the byte order
		   before the computation.  To reduce the work for the next steps
		   we store the swapped words in the array CORRECT_WORDS.  */

#define OP(a, b, c, d, s, T)						\
		do								\
		{								\
			a += FF (b, c, d) + (*cwp++ = SWAP (*words)) + T;		\
			++words;							\
			CYCLIC (a, s);						\
			a += b;							\
		}								\
		while (0)

		/* It is unfortunate that C does not provide an operator for
		   cyclic rotation.  Hope the C compiler is smart enough.  */
#define CYCLIC(w, s) (w = (w << s) | (w >> (32 - s)))

		/* Before we start, one word to the strange constants.
		   They are defined in RFC 1321 as

		   T[i] = (int) (4294967296.0 * fabs (sin (i))), i=1..64
		 */

		/* Round 1.  */
		OP (A, B, C, D,  7, 0xd76aa478);
		OP (D, A, B, C, 12, 0xe8c7b756);
		OP (C, D, A, B, 17, 0x242070db);
		OP (B, C, D, A, 22, 0xc1bdceee);
		OP (A, B, C, D,  7, 0xf57c0faf);
		OP (D, A, B, C, 12, 0x4787c62a);
		OP (C, D, A, B, 17, 0xa8304613);
		OP (B, C, D, A, 22, 0xfd469501);
		OP (A, B, C, D,  7, 0x698098d8);
		OP (D, A, B, C, 12, 0x8b44f7af);
		OP (C, D, A, B, 17, 0xffff5bb1);
		OP (B, C, D, A, 22, 0x895cd7be);
		OP (A, B, C, D,  7, 0x6b901122);
		OP (D, A, B, C, 12, 0xfd987193);
		OP (C, D, A, B, 17, 0xa679438e);
		OP (B, C, D, A, 22, 0x49b40821);

		/* For the second to fourth round we have the possibly swapped words
		   in CORRECT_WORDS.  Redefine the macro to take an additional first
		   argument specifying the function to use.  */
#undef OP
#define OP(f, a, b, c, d, k, s, T)					\
		do 								\
		{								\
			a += f (b, c, d) + correct_words[k] + T;			\
			CYCLIC (a, s);						\
			a += b;							\
		}								\
		while (0)

		/* Round 2.  */
		OP (FG, A, B, C, D,  1,  5, 0xf61e2562);
		OP (FG, D, A, B, C,  6,  9, 0xc040b340);
		OP (FG, C, D, A, B, 11, 14, 0x265e5a51);
		OP (FG, B, C, D, A,  0, 20, 0xe9b6c7aa);
		OP (FG, A, B, C, D,  5,  5, 0xd62f105d);
		OP (FG, D, A, B, C, 10,  9, 0x02441453);
		OP (FG, C, D, A, B, 15, 14, 0xd8a1e681);
		OP (FG, B, C, D, A,  4, 20, 0xe7d3fbc8);
		OP (FG, A, B, C, D,  9,  5, 0x21e1cde6);
		OP (FG, D, A, B, C, 14,  9, 0xc33707d6);
		OP (FG, C, D, A, B,  3, 14, 0xf4d50d87);
		OP (FG, B, C, D, A,  8, 20, 0x455a14ed);
		OP (FG, A, B, C, D, 13,  5, 0xa9e3e905);
		OP (FG, D, A, B, C,  2,  9, 0xfcefa3f8);
		OP (FG, C, D, A, B,  7, 14, 0x676f02d9);
		OP (FG, B, C, D, A, 12, 20, 0x8d2a4c8a);

		/* Round 3.  */
		OP (FH, A, B, C, D,  5,  4, 0xfffa3942);
		OP (FH, D, A, B, C,  8, 11, 0x8771f681);
		OP (FH, C, D, A, B, 11, 16, 0x6d9d6122);
		OP (FH, B, C, D, A, 14, 23, 0xfde5380c);
		OP (FH, A, B, C, D,  1,  4, 0xa4beea44);
		OP (FH, D, A, B, C,  4, 11, 0x4bdecfa9);
		OP (FH, C, D, A, B,  7, 16, 0xf6bb4b60);
		OP (FH, B, C, D, A, 10, 23, 0xbebfbc70);
		OP (FH, A, B, C, D, 13,  4, 0x289b7ec6);
		OP (FH, D, A, B, C,  0, 11, 0xeaa127fa);
		OP (FH, C, D, A, B,  3, 16, 0xd4ef3085);
		OP (FH, B, C, D, A,  6, 23, 0x04881d05);
		OP (FH, A, B, C, D,  9,  4, 0xd9d4d039);
		OP (FH, D, A, B, C, 12, 11, 0xe6db99e5);
		OP (FH, C, D, A, B, 15, 16, 0x1fa27cf8);
		OP (FH, B, C, D, A,  2, 23, 0xc4ac5665);

		/* Round 4.  */
		OP (FI, A, B, C, D,  0,  6, 0xf4292244);
		OP (FI, D, A, B, C,  7, 10, 0x432aff97);
		OP (FI, C, D, A, B, 14, 15, 0xab9423a7);
		OP (FI, B, C, D, A,  5, 21, 0xfc93a039);
		OP (FI, A, B, C, D, 12,  6, 0x655b59c3);
		OP (FI, D, A, B, C,  3, 10, 0x8f0ccc92);
		OP (FI, C, D, A, B, 10, 15, 0xffeff47d);
		OP (FI, B, C, D, A,  1, 21, 0x85845dd1);
		OP (FI, A, B, C, D,  8,  6, 0x6fa87e4f);
		OP (FI, D, A, B, C, 15, 10, 0xfe2ce6e0);
		OP (FI, C, D, A, B,  6, 15, 0xa3014314);
		OP (FI, B, C, D, A, 13, 21, 0x4e0811a1);
		OP (FI, A, B, C, D,  4,  6, 0xf7537e82);
		OP (FI, D, A, B, C, 11, 10, 0xbd3af235);
		OP (FI, C, D, A, B,  2, 15, 0x2ad7d2bb);
		OP (FI, B, C, D, A,  9, 21, 0xeb86d391);

		/* Add the starting values of the context.  */
		A += A_save;
		B += B_save;
		C += C_save;
		D += D_save;
	}

	/* Put checksum in context given as argument.  */
	ctx->A = A;
	ctx->B = B;
	ctx->C = C;
	ctx->D = D;
}
/* This array contains the bytes used to pad the buffer to the next
   64-byte boundary.  (RFC 1321, 3.1: Step 1)  */
static const unsigned char fillbuf[64] = { 0x80, 0 /* , 0, 0, ...  */ };

/* Initialize structure containing state of computation.
   (RFC 1321, 3.3: Step 3)  */
static void
libiscsi_md5_init_ctx (struct libiscsi_md5_ctx *ctx)
{
	ctx->A = 0x67452301;
	ctx->B = 0xefcdab89;
	ctx->C = 0x98badcfe;
	ctx->D = 0x10325476;

	ctx->total[0] = ctx->total[1] = 0;
	ctx->buflen = 0;
}

/* Put result from CTX in first 16 bytes following RESBUF.  The result
   must be in little endian byte order.

IMPORTANT: On some systems it is required that RESBUF is correctly
aligned for a 32 bits value.  */
static void *
libiscsi_md5_read_ctx (const struct libiscsi_md5_ctx *ctx, void *resbuf)
{
	((libiscsi_md5_uint32 *) resbuf)[0] = SWAP (ctx->A);
	((libiscsi_md5_uint32 *) resbuf)[1] = SWAP (ctx->B);
	((libiscsi_md5_uint32 *) resbuf)[2] = SWAP (ctx->C);
	((libiscsi_md5_uint32 *) resbuf)[3] = SWAP (ctx->D);

	return resbuf;
}

/* Process the remaining bytes in the internal buffer and the usual
   prolog according to the standard and write the result to RESBUF.

IMPORTANT: On some systems it is required that RESBUF is correctly
aligned for a 32 bits value.  */
static void *
libiscsi_md5_finish_ctx (struct libiscsi_md5_ctx *ctx, void *resbuf)
{
	/* Take yet unprocessed bytes into account.  */
	libiscsi_md5_uint32 bytes = ctx->buflen;
	size_t pad;

	/* Now count remaining bytes.  */
	ctx->total[0] += bytes;
	if (ctx->total[0] < bytes)
		++ctx->total[1];

	pad = bytes >= 56 ? 64 + 56 - bytes : 56 - bytes;
	memcpy (&ctx->buffer[bytes], fillbuf, pad);

	/* Put the 64-bit file length in *bits* at the end of the buffer.  */
	ctx->buffer32[(bytes + pad) / 4] = SWAP (ctx->total[0] << 3);
	ctx->buffer32[(bytes + pad + 4) / 4] = SWAP ((ctx->total[1] << 3)
			| (ctx->total[0] >> 29));

	/* Process last bytes.  */
	libiscsi_md5_process_block (ctx->buffer, bytes + pad + 8, ctx);

	return libiscsi_md5_read_ctx (ctx, resbuf);
}

static void
libiscsi_md5_process_bytes (const void *buffer, size_t len, struct libiscsi_md5_ctx *ctx)
{
	/* When we already have some bits in our internal buffer concatenate
	   both inputs first.  */
	if (ctx->buflen != 0)
	{
		size_t left_over = ctx->buflen;
		size_t add = 128 - left_over > len ? len : 128 - left_over;

		memcpy (&ctx->buffer[left_over], buffer, add);
		ctx->buflen += add;

		if (ctx->buflen > 64)
		{
			libiscsi_md5_process_block (ctx->buffer, ctx->buflen & ~63, ctx);

			ctx->buflen &= 63;
			/* The regions in the following copy operation cannot overlap.  */
			memcpy (ctx->buffer, &ctx->buffer[(left_over + add) & ~63],
					ctx->buflen);
		}

		buffer = (const char *) buffer + add;
		len -= add;
	}

	/* Process available complete blocks.  */
	if (len >= 64)
	{
		while (len > 64)
		{
			libiscsi_md5_process_block (memcpy (ctx->buffer, buffer, 64), 64, ctx);
			buffer = (const char *) buffer + 64;
			len -= 64;
		}
	}

	/* Move remaining bytes in internal buffer.  */
	if (len > 0)
	{
		size_t left_over = ctx->buflen;

		memcpy (&ctx->buffer[left_over], buffer, len);
		left_over += len;
		if (left_over >= 64)
		{
			libiscsi_md5_process_block (ctx->buffer, 64, ctx);
			left_over -= 64;
			memcpy (ctx->buffer, &ctx->buffer[64], left_over);
		}
		ctx->buflen = left_over;
	}
}

/* iSCSI codes */
#define MIN(a, b) (((a) < (b)) ? (a) : (b))

static const char *initiator = "iqn.2007-10.com.github:sahlberg:libiscsi:iscsi-md5sum";

static void print_help(const char *prog)
{
	fprintf(stderr, "Usage: %s [OPTION...] <iscsi-url>\n", prog);
	fprintf(stderr, "  -i, --initiator-name=iqn-name     Initiatorname to use\n");
	fprintf(stderr, "  -o, --offset                      "
			"Byte offset into the target from which to start calculating. "
			"The provided value must be aligned to the target sector size. "
			"The default value is zero.\n");
	fprintf(stderr, "  -l, --length                      "
			"The number of bytes to calculate (counting from the starting point). "
			"The provided value must be aligned to the target sector size. "
			"If the specified value extends past the end of the device, "
			"%s will stop at the device size boundary. "
			"The default value extends to the end of the device.\n", prog);
	fprintf(stderr, "  -d, --debug=integer               debug level (0=disabled)\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "Help options:\n");
	fprintf(stderr, "  -?, --help                        Show this help message\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "iSCSI URL format : %s\n", ISCSI_URL_SYNTAX);
	fprintf(stderr, "\n");
	fprintf(stderr, "<host> is either of:\n");
	fprintf(stderr, "  \"hostname\"       iscsi.example\n");
	fprintf(stderr, "  \"ipv4-address\"   10.1.1.27\n");
	fprintf(stderr, "  \"ipv6-address\"   [fce0::1]\n");

	exit(0);
}

void inquiry_block_limits(struct scsi_inquiry_block_limits *inq)
{
	printf("wsnz:%d\n", inq->wsnz);
	printf("maximum compare and write length:%" PRIu8 "\n", inq->max_cmp);
	printf("optimal transfer length granularity:%" PRIu16 "\n", inq->opt_gran);
	printf("maximum transfer length:%" PRIu32 "\n", inq->max_xfer_len);
	printf("optimal transfer length:%" PRIu32 "\n",inq->opt_xfer_len);
	printf("maximum prefetch xdread xdwrite transfer length:%" PRIu32 "\n", inq->max_prefetch);
	printf("maximum unmap lba count:%" PRIu32 "\n", inq->max_unmap);
	printf("maximum unmap block descriptor count:%" PRIu32 "\n", inq->max_unmap_bdc);
	printf("optimal unmap granularity:%" PRIu32 "\n", inq->opt_unmap_gran);
	printf("ugavalid:%d\n", inq->ugavalid);
	printf("unmap granularity alignment:%" PRIu32 "\n", inq->unmap_gran_align);
	printf("maximum write same length:%" PRIu64 "\n", inq->max_ws_len);
}

static unsigned long inquiry_xfer_len(struct iscsi_context *iscsi, int lun, unsigned int block_length)
{
	struct scsi_task *task;
	int full_size;
	struct scsi_inquiry_block_limits *inq;
	unsigned long max_xfer_len = 1024 * 1024;	/* default size 1M */

	/* See how big this inquiry data is */
	task = iscsi_inquiry_sync(iscsi, lun, 1, SCSI_INQUIRY_PAGECODE_BLOCK_LIMITS, 64);
	if (task == NULL || task->status != SCSI_STATUS_GOOD) {
		fprintf(stderr, "Inquiry command failed : %s\n", iscsi_get_error(iscsi));
		exit(EIO);
	}

	full_size = scsi_datain_getfullsize(task);
	if (full_size > task->datain.size) {
		scsi_free_scsi_task(task);

		/* we need more data for the full list */
		if ((task = iscsi_inquiry_sync(iscsi, lun, 1, SCSI_INQUIRY_PAGECODE_BLOCK_LIMITS, full_size)) == NULL) {
			fprintf(stderr, "Inquiry command failed : %s\n", iscsi_get_error(iscsi));
			exit(EIO);
		}
	}

	inq = scsi_datain_unmarshall(task);
	if (inq == NULL) {
		fprintf(stderr, "failed to unmarshall inquiry datain blob\n");
		exit(EIO);
	}

	if (inq->max_xfer_len)
		max_xfer_len = MIN(max_xfer_len, inq->max_xfer_len * block_length);

	scsi_free_scsi_task(task);

	return max_xfer_len;
}

static void pread16(struct iscsi_context *iscsi, int lun, void *buf, uint64_t lba, uint32_t datalen, unsigned int block_size)
{
	struct scsi_iovec iov = { .iov_base = buf, .iov_len = datalen };
	struct scsi_task *task;

	task = iscsi_read16_iov_sync(iscsi, lun, lba, datalen, block_size, 0, 0, 0, 0, 0, &iov, 1);
	if (task == NULL || task->status != SCSI_STATUS_GOOD) {
		fprintf(stderr, "read16 command failed : %s\n", iscsi_get_error(iscsi));
		exit(EIO);
	}

	scsi_free_scsi_task(task);
}

int main(int argc, char *argv[])
{
	struct iscsi_context *iscsi;
	char *url = NULL;
	struct iscsi_url *iscsi_url = NULL;
	int debug = 0;
	int option_index, c;
	unsigned int block_length;
	long long offset = 0, length = 0, capacity, max_xfer_len, end;
	struct scsi_task *task;
	struct scsi_readcapacity16 *rc16;
	struct libiscsi_md5_ctx ctx;
	unsigned char sum[16];
	unsigned char *buf;
	int ret = EINVAL;

	static struct option long_options[] = {
		{"offset",         required_argument,    NULL,        'o'},
		{"length",         required_argument,    NULL,        'l'},
		{"debug",          required_argument,    NULL,        'd'},
		{"help",           no_argument,          NULL,        'h'},
		{"initiator-name", required_argument,    NULL,        'i'},
		{0, 0, 0, 0}
	};

	while ((c = getopt_long(argc, argv, "o:l:d:i:h?", long_options,
					&option_index)) != -1) {
		switch (c) {
			case 'o':
				offset = strtoll(optarg, NULL, 0);
				break;
			case 'l':
				length = strtoll(optarg, NULL, 0);
				break;
			case 'd':
				debug = strtol(optarg, NULL, 0);
				break;
			case 'i':
				initiator = optarg;
				break;
			case 'h':
			case '?':
				print_help(argv[0]);
				break;
			default:
				fprintf(stderr, "Unrecognized option '%c'\n\n", c);
				print_help(argv[0]);
				break;
		}
	}

	iscsi = iscsi_create_context(initiator);
	if (iscsi == NULL) {
		fprintf(stderr, "Failed to create context\n");
		exit(EINVAL);
	}

	if (debug > 0) {
		iscsi_set_log_fn(iscsi, iscsi_log_to_stderr);
		iscsi_set_log_level(iscsi, debug);
	}

	if (argv[optind] != NULL) {
		url = strdup(argv[optind]);
	}
	if (url == NULL) {
		fprintf(stderr, "You must specify the URL\n");
		print_help(argv[0]);
	}
	iscsi_url = iscsi_parse_full_url(iscsi, url);

	free(url);

	if (iscsi_url == NULL) {
		fprintf(stderr, "Failed to parse URL: %s\n", 
				iscsi_get_error(iscsi));
		exit(EINVAL);
	}

	iscsi_set_session_type(iscsi, ISCSI_SESSION_NORMAL);
	iscsi_set_header_digest(iscsi, ISCSI_HEADER_DIGEST_NONE_CRC32C);

	if (iscsi_full_connect_sync(iscsi, iscsi_url->portal, iscsi_url->lun) != 0) {
		fprintf(stderr, "Login Failed. %s\n", iscsi_get_error(iscsi));
		goto out;
	}

	task = iscsi_readcapacity16_sync(iscsi, iscsi_url->lun);
	if (task == NULL || task->status != SCSI_STATUS_GOOD) {
		fprintf(stderr,"Failed to send readcapacity command\n");
		goto out;
	}

	rc16 = scsi_datain_unmarshall(task);
	if (rc16 == NULL) {
		fprintf(stderr,"Failed to unmarshall readcapacity16 data\n");
		goto out;
	}

	block_length = rc16->block_length;
	if (offset & (block_length - 1)) {
		fprintf(stderr,"Unaligned offset of %u\n", block_length);
		goto free_task;
	}

	capacity = block_length * (rc16->returned_lba + 1);
	if (offset > capacity) {
		fprintf(stderr,"Offset(%lld) exceeds capacity(%lld)\n", offset, capacity);
		goto free_task;
	}

	if (!length || (offset + length > capacity)) {
		length = block_length * (rc16->returned_lba + 1) - offset;
	}

	/* free readcapacity16 task */
	scsi_free_scsi_task(task);

	libiscsi_md5_init_ctx(&ctx);
	max_xfer_len = inquiry_xfer_len(iscsi, iscsi_url->lun, block_length);
	buf = calloc(1, max_xfer_len);
	for (end = offset + length; offset < end; offset += max_xfer_len) {
		long long _len = MIN(end - offset, max_xfer_len);
		pread16(iscsi, iscsi_url->lun, buf, offset / block_length, _len, block_length);
		libiscsi_md5_process_bytes(buf, _len, &ctx);
	}

	libiscsi_md5_finish_ctx(&ctx, sum);
	free(buf);

	/* show MD5 sum in HEX */
	for (size_t i = 0; i < sizeof(sum); i++)
		printf("%02x", sum[i]);

	printf("\n");

	ret = 0;
	goto out;

free_task:
	scsi_free_scsi_task(task);

out:
	iscsi_destroy_url(iscsi_url);
	iscsi_logout_sync(iscsi);
	iscsi_destroy_context(iscsi);

	return ret;
}
