/*
 * zipl - zSeries Initial Program Loader tool
 *
 * Common ECKD dump I/O functions
 *
 * Copyright IBM Corp. 2013, 2023
 *
 * s390-tools is free software; you can redistribute it and/or modify
 * it under the terms of the MIT license. See LICENSE for details.
 */

#include "dump/s390_dump.h"

#include "cio.h"
#include "eckd2dump.h"
#include "boot/error.h"
#include "boot/s390.h"
#include "stage2dump.h"

#define ECKD_CCW_LOCATE_RECORD		0x47
#define ECKD_CCW_LOCATE_RECORD_EXT	0x4b
#define ECKD_CCW_DEFINE_EXTENT		0x63
#define ECKD_CCW_WRITE_TRACK_DATA	0xa5
#define ECKD_CCW_READ_TRACK_DATA	0xa6
#define ECKD_CCW_WRITE_KD_MT		0x8d
#define ECKD_CCW_READ_KD_MT		0x8e

#define ECKD_BLK_PER_TRK		12

typedef unsigned long sector_t;

/*
 * For track I/O we can write 12 blocks per full track. Because I/O can start
 * at the end of a track with "tr_count" tracks we can guarantee to write:
 *
 * block_count = (tr_count - 1) * 12 + 1
 *
 * Therefore: tr_count >= (block_count + 11) / 12
 */
#define ECKD_BLK_PER_IO_TRK	128 /* 128 * 4 KB = up to 512 KB per I/O */
#define ECKD_CCW_MAX_COUNT_TRK	12  /* (128 + 11) / 12 = 12 track I/O CCWs */

/*
 * For record I/O we write one 4 KB block with one write CCW
 */
#define ECKD_BLK_PER_IO_REC	64  /* 64 * 4 KB = up to 256 KB per I/O */
#define ECKD_CCW_MAX_COUNT_REC	64  /* 64 record I/O CCWs */

unsigned long eckd_blk_max;	/* Maximum number of blocks per I/O */
struct eckd_device device;

static int track_io;

#define sector_div(n, b)( \
{ \
	int _res; \
	_res = (n) % (b); \
	(n) /= (b); \
	_res; \
} \
)

struct ch_t {
	uint16_t cyl;
	uint16_t head;
} __packed;

struct chr_t {
	uint16_t cyl;
	uint16_t head;
	uint8_t record;
} __packed;

static void set_ch_t(struct ch_t *geo, uint32_t cyl, uint8_t head)
{
	geo->cyl = (uint16_t) cyl;
	geo->head = cyl >> 16;
	geo->head <<= 4;
	geo->head |= head;
}

/*
 * Data for Locate Record Extended CCW
 */
struct LRE_eckd_data {
	struct {
		unsigned char orientation:2;
		unsigned char operation:6;
	} __packed operation;
	struct {
		unsigned char length_valid:1;
		unsigned char length_scope:1;
		unsigned char imbedded_ccw_valid:1;
		unsigned char check_bytes:2;
		unsigned char imbedded_count_valid:1;
		unsigned char reserved:1;
		unsigned char read_count_suffix:1;
	} __packed auxiliary;
	uint8_t imbedded_ccw;
	uint8_t count;
	struct ch_t seek_addr;
	struct chr_t search_arg;
	uint8_t sector;
	uint16_t length;
	uint8_t imbedded_count;
	uint8_t extended_operation;
	uint16_t extended_parameter_length;
	uint8_t extended_parameter[0];
} __packed;

static struct LRE_eckd_data lodata;

struct DE_eckd_data {
	struct {
		unsigned char perm:2;	/* Permissions on this extent */
		unsigned char reserved:1;
		unsigned char seek:2;	/* Seek control */
		unsigned char auth:2;	/* Access authorization */
		unsigned char pci:1;	/* PCI Fetch mode */
	} __packed mask;
	struct {
		unsigned char mode:2;	/* Architecture mode */
		unsigned char ckd:1;	/* CKD Conversion */
		unsigned char operation:3;	/* Operation mode */
		unsigned char cfw:1;	/* Cache fast write */
		unsigned char dfw:1;	/* DASD fast write */
	} __packed attributes;
	uint16_t blk_size;		/* Blocksize */
	uint16_t fast_write_id;
	uint8_t ga_additional;	/* Global Attributes Additional */
	uint8_t ga_extended;	/* Global Attributes Extended	*/
	struct ch_t beg_ext;
	struct ch_t end_ext;
} __packed;

static struct DE_eckd_data dedata;

/*
 * ORB /IRB
 */
static struct orb orb = {
	.intparm	= 0x0049504c,	/* Interruption Parameter */
	.fmt		= 0x1,		/* Use format 1 CCWs */
	.c64		= 0x1,		/* Use IDAs */
	.lpm		= 0xff,		/* Logical path mask */
};

static struct irb irb;

/*
 * CCW program and IDA list
 */
struct ccw_program_rw_trk {
	struct ccw1 wrtdccw[ECKD_CCW_MAX_COUNT_TRK];
	unsigned long ida_list[ECKD_CCW_MAX_COUNT_TRK * ECKD_BLK_PER_TRK];
};

struct ccw_program_rw_rec {
	struct ccw1 wrtdccw[ECKD_CCW_MAX_COUNT_REC];
	unsigned long ida_list[ECKD_CCW_MAX_COUNT_REC];
};

struct ccw_program {
	struct ccw1 deccw;
	struct ccw1 loccw;
	union {
		struct ccw_program_rw_trk trk;
		struct ccw_program_rw_rec rec;
	} rw;
} __packed __aligned(8);

static struct ccw_program ccw_program;

/*
 * Create the list of idal words for an address/length pair
 */
static unsigned long *idal_create_words(unsigned long *idaws, void *vaddr,
					unsigned int length,
					unsigned long zero_page)
{
	unsigned long paddr;
	unsigned int cidaw;

	paddr = __pa(vaddr);
	cidaw = ((paddr & (PAGE_SIZE-1)) + length + (PAGE_SIZE-1)) >> 12;
	if (zero_page)
		*idaws++ = page_is_valid(paddr) ? paddr : zero_page;
	else
		*idaws++ = paddr;
	paddr &= -PAGE_SIZE;
	while (--cidaw > 0) {
		paddr += PAGE_SIZE;
		if (zero_page)
			*idaws++ = page_is_valid(paddr) ? paddr : zero_page;
		else
			*idaws++ = paddr;
	}
	return idaws;
}

/*
 * Fill Locate Record (Extended) data
 */
static void fill_LRE_data(struct LRE_eckd_data *data, unsigned int trk,
			  unsigned int rec_on_trk, int count, int cmd,
			  unsigned int reclen, unsigned int tlf)
{
	memset(data, 0, sizeof(*data));
	/*
	 * note: meaning of count depends on the operation
	 *       for record based I/O it's the number of records, but for
	 *       track based I/O it's the number of tracks
	 */
	data->count = count;
	data->sector = 0xff;
	switch (cmd) {
	case ECKD_CCW_WRITE_KD_MT:
		data->auxiliary.length_valid = 0x1;
		data->length = reclen;
		data->operation.operation = 0x1;
		break;
	case ECKD_CCW_READ_KD_MT:
		data->auxiliary.length_valid = 0x1;
		data->length = reclen;
		data->operation.operation = 0x6;
		break;
	case ECKD_CCW_WRITE_TRACK_DATA:
		data->auxiliary.length_valid = 0x1;
		data->length = reclen;	/* not tlf, as one might think */
		data->operation.operation = 0x3f;
		data->extended_operation = 0x23;
		break;
	case ECKD_CCW_READ_TRACK_DATA:
		data->auxiliary.length_valid = 0x1;
		data->length = tlf;
		data->operation.operation = 0xc;
		break;
	}
	set_ch_t(&data->seek_addr, trk / device.num_heads,
		 trk % device.num_heads);
	data->search_arg.cyl = data->seek_addr.cyl;
	data->search_arg.head = data->seek_addr.head;
	data->search_arg.record = rec_on_trk;
}

/*
 * Fill Define Extend and Locate Record (Extended) data
 */
static void fill_DE_LRE_data(unsigned int trk, unsigned int totrk, int cmd,
			     unsigned int rec_on_trk, int count,
			     unsigned int blksize, unsigned int tlf)
{
	uint16_t heads, beghead, endhead;
	uint32_t begcyl, endcyl;

	memset(&dedata, 0, sizeof(dedata));
	switch (cmd) {
	case ECKD_CCW_READ_KD_MT:
		dedata.mask.perm = 0x1;
		break;
	case ECKD_CCW_WRITE_KD_MT:
		dedata.mask.perm = 0x2;
		break;
	case ECKD_CCW_READ_TRACK_DATA:
		dedata.mask.perm = 0x1;
		dedata.blk_size = 0;
		break;
	case ECKD_CCW_WRITE_TRACK_DATA:
		dedata.mask.perm = 0x2;
		dedata.blk_size = device.blk_size;
		break;
	}
	dedata.attributes.mode = 0x3;	/* ECKD */
	dedata.ga_extended |= 0x40; /* Regular Data Format Mode */

	heads = device.num_heads;
	begcyl = trk / heads;
	beghead = trk % heads;
	endcyl = totrk / heads;
	endhead = totrk % heads;

	set_ch_t(&dedata.beg_ext, begcyl, beghead);
	set_ch_t(&dedata.end_ext, endcyl, endhead);

	fill_LRE_data(&lodata, trk, rec_on_trk, count, cmd, blksize, tlf);
}

/*
 * Read or write ECKD blocks using record I/O
 */
static void io_block_rec(int cmd, unsigned long addr,
			 unsigned long zero_page,
			 sector_t first_rec, sector_t last_rec,
			 sector_t first_trk, sector_t last_trk,
			 unsigned int first_offs)
{
	unsigned int i, rec_count;
	unsigned long *idaws;
	struct ccw1 *ccw;

	rec_count = last_rec - first_rec + 1;
	idaws = ccw_program.rw.rec.ida_list;

	fill_DE_LRE_data(first_trk, last_trk, cmd, first_offs + 1, rec_count,
			 device.blk_size, 0);
	ccw = ccw_program.rw.rec.wrtdccw;
	for (i = 0; i < rec_count; i++) {
		/* Read/write ccw. */
		ccw[-1].flags |= CCW_FLAG_CC;
		ccw->cmd_code = cmd;
		ccw->count = device.blk_size;
		ccw->cda = __pa32(idaws);
		ccw->flags = CCW_FLAG_IDA;
		idaws = idal_create_words(idaws, (void *) addr, device.blk_size,
					  zero_page);
		ccw++;
		addr += device.blk_size;
	}
}

/*
 * Read or write ECKD blocks using track I/O
 */
static void io_block_trk(uint8_t cmd, unsigned long addr,
			 unsigned long zero_page,
			 sector_t first_rec, sector_t last_rec,
			 sector_t first_trk, sector_t last_trk,
			 unsigned int first_offs, unsigned int last_offs)
{
	unsigned int recoffs, count_to_trk_end, len_to_track_end, count;
	unsigned int blk_count, recid, trkid, trk_count, tlf;
	unsigned int idaw_len, seg_len, part_len;
	unsigned char new_track, end_idaw;
	unsigned long *idaws;
	char *dst, *idaw_dst;
	struct ccw1 *ccw;

	blk_count = last_rec - first_rec + 1;
	trk_count = last_trk - first_trk + 1;
	idaws = ccw_program.rw.trk.ida_list;
	recid = first_rec;
	dst = (char *) addr;
	new_track = 1;
	end_idaw = 0;
	len_to_track_end = 0;
	idaw_dst = NULL;
	idaw_len = 0;

	if (first_trk == last_trk)
		tlf = device.blk_size * (last_offs - first_offs + 1);
	else
		tlf = device.blk_size * (last_offs + 1);
	fill_DE_LRE_data(first_trk, last_trk, cmd, first_offs + 1, trk_count,
			 device.blk_size, tlf);

	ccw = ccw_program.rw.trk.wrtdccw;
	seg_len = blk_count * device.blk_size;
	while (seg_len) {
		if (new_track) {
			trkid = recid;
			recoffs = trkid % device.bpt;
			count_to_trk_end = device.bpt - recoffs;
			count = MIN((last_rec - recid + 1), count_to_trk_end);
			len_to_track_end = count * device.blk_size;
			ccw[-1].flags |= CCW_FLAG_CC;
			ccw->cmd_code = cmd;
			ccw->count = len_to_track_end;
			ccw->cda = __pa32(idaws);
			ccw->flags = CCW_FLAG_IDA;
			ccw++;
			recid += count;
			new_track = 0;
			if (!idaw_dst)
				idaw_dst = dst;
		}
		if (!idaw_dst)
			idaw_dst = dst;
		part_len = MIN(seg_len, len_to_track_end);
		seg_len -= part_len;
		dst += part_len;
		idaw_len += part_len;
		len_to_track_end -= part_len;
		if (!(__pa(idaw_dst + idaw_len) & (PAGE_SIZE - 1)))
			end_idaw = 1;
		if (!len_to_track_end) {
			new_track = 1;
			end_idaw = 1;
		}
		if (end_idaw) {
			idaws = idal_create_words(idaws, idaw_dst,
						  idaw_len, zero_page);
			idaw_dst = NULL;
			idaw_len = 0;
			end_idaw = 0;
		}
	}
}

/*
 * Read or write ECKD blocks
 */
static int io_block(uint8_t cmd, unsigned long first_blk, unsigned long addr,
		    unsigned long blk_count, unsigned long zero_page, int panic)
{
	sector_t first_trk, last_trk, first_rec, last_rec;
	unsigned int first_offs, last_offs;

	if (first_blk + blk_count - 1 > device.blk_end)
		panic(EMEM, "%s", "Device too small");

	/* Compute start track and end block on track */
	first_rec = first_trk = first_blk;
	first_offs = sector_div(first_trk, device.bpt);
	last_rec = last_trk = first_blk + blk_count - 1;
	last_offs = sector_div(last_trk, device.bpt);
	memset(&ccw_program.rw.trk.wrtdccw, 0,
	       sizeof(ccw_program.rw.trk.wrtdccw));

	if (track_io)
		io_block_trk(cmd, addr, zero_page, first_rec, last_rec,
			     first_trk, last_trk, first_offs, last_offs);
	else
		io_block_rec(cmd, addr, zero_page, first_rec, last_rec,
			     first_trk, last_trk, first_offs);
	return start_io(device.sid, &irb, &orb, panic);
}

/*
 * Write data to given block address
 */
void writeblock(unsigned long blk, unsigned long addr, unsigned long blk_count,
		unsigned long zero_page)
{
	int cmd = track_io ? ECKD_CCW_WRITE_TRACK_DATA : ECKD_CCW_WRITE_KD_MT;

	io_block(cmd, blk, addr, blk_count, zero_page, 1);
}

/*
 * Read data from given block address
 */
void readblock(unsigned long blk, unsigned long addr, unsigned long blk_count)
{
	int cmd = track_io ? ECKD_CCW_READ_TRACK_DATA : ECKD_CCW_READ_KD_MT;

	io_block(cmd, blk, addr, blk_count, 0, 1);
}

/*
 * Write len/device.blk_size blocks of data at start address starting from
 * given block and return the next free block number. The len value must be
 * a multiple of a block size (4096 bytes).
 */
unsigned long write_addr_range(unsigned long blk, unsigned long start,
			       unsigned long len, int print_progress)
{
	unsigned long addr, start_blk, blk_count, zero_page;

	/* Verify the range is a multiple of a block size */
	if (len % device.blk_size)
		panic(EINTERNAL, "Invalid address range: [%016x, %016x]",
		      start, start + len);
	addr = start;
	start_blk = blk;
	while (addr < start + len) {
		/* Remaining blocks to write */
		blk_count = m2b(len) - (blk - start_blk);
		blk_count = MIN(blk_count, eckd_blk_max);
		zero_page = get_zeroed_page();
		writeblock(blk, addr, blk_count, zero_page);
		free_page(zero_page);
		blk += blk_count;
		addr += b2m(blk_count);
		total_dump_size += b2m(blk_count);
		if (print_progress)
			progress_print(addr);
	}
	return blk;
}

/*
 * Write dump segment with the header to DASD and return the next free
 * block number
 */
unsigned long write_dump_segment(unsigned long blk,
				 struct df_s390_dump_segm_hdr *segm)
{
	unsigned long zero_page;

	/* Write the dump segment header itself (1 page) */
	zero_page = get_zeroed_page();
	writeblock(blk, (unsigned long)segm,
		   m2b(sizeof(struct df_s390_dump_segm_hdr)), zero_page);
	free_page(zero_page);
	blk += m2b(PAGE_SIZE);
	/* Write the dump segment */
	return write_addr_range(blk, segm->start, segm->len, PRINT_PROGRESS);
}

/*
 * Init ECKD common
 */
void stage2dump_eckd_init(void)
{
	unsigned long addr;
	int rc;

	ccw_program.deccw.cmd_code = ECKD_CCW_DEFINE_EXTENT;
	ccw_program.deccw.flags = CCW_FLAG_CC;
	ccw_program.deccw.count = sizeof(dedata);
	ccw_program.deccw.cda = __pa32(&dedata);

	ccw_program.loccw.cmd_code = ECKD_CCW_LOCATE_RECORD_EXT;
	ccw_program.loccw.flags = CCW_FLAG_CC;
	ccw_program.loccw.count = sizeof(lodata);
	ccw_program.loccw.cda = __pa32(&lodata);

	orb.cpa = __pa32(&ccw_program);
	track_io = 1;
	eckd_blk_max = ECKD_BLK_PER_IO_TRK;

	/* Probe Track I/O - Read first block of device */
	addr = get_zeroed_page();
	rc = io_block(ECKD_CCW_READ_TRACK_DATA, device.blk_start,
		      addr, 1, 0, 0);
	free_page(addr);

	/*
	 * If storage server does not support track I/O use record I/O fallback.
	 * Use Locate Record instead of Locate Record Extended in that case.
	 */
	if (rc) {
		printf("Using Record I/O");
		track_io = 0;
		eckd_blk_max = ECKD_BLK_PER_IO_REC;
		ccw_program.loccw.cmd_code = ECKD_CCW_LOCATE_RECORD;
		ccw_program.loccw.count = 0x10;
	}
}
