/*
 * zgetdump - Tool for copying and converting System z dumps
 *
 * Single-volume SCSI dump tool
 *
 * Copyright IBM Corp. 2013, 2017
 *
 * 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 <linux/fs.h>
#include <stdio.h>

#include "lib/util_part.h"
#include "zgetdump.h"

/*
 * Single volume SCSI dump superblock
 */
struct scsi_dump_sb {
	uint64_t	magic;
	uint64_t	version;
	uint64_t	part_start;
	uint64_t	part_size;
	uint64_t	dump_off;
	uint64_t	dump_size;
	uint64_t	csum_off;
	uint64_t	csum_size;
	uint64_t	csum;
} __attribute((packed));

/*
 * File local static data
 */
static struct {
	struct scsi_dump_sb	sb;
	int			blk_size;
} l;

/*
 * zipl on-disc / bootloader structs from zipl includes
 */
struct scsi_blockptr {
	uint64_t	blockno;
	uint16_t	size;
	uint16_t	blockct;
	uint8_t		reserved[4];
} __attribute__((__packed__));

enum component_entry_type {
	component_execute	= 0x01,
	component_load		= 0x02
};

struct component_entry {
	struct scsi_blockptr	data;
	uint8_t			pad[7];
	uint8_t			component_type;
	union {
		uint64_t	load_address;
		uint64_t	load_psw;
	} address;
} __attribute__((__packed__));

enum component_header_type {
	component_header_ipl	= 0x00,
	component_header_dump	= 0x01
};

struct component_header {
	uint8_t	magic[4];
	uint8_t	type;
	uint8_t	reserved[27];
} __attribute__((__packed__));

struct boot_info {
	char		magic[4];
	uint8_t		version;
	uint8_t		bp_type;
	uint8_t		dev_type;
	uint8_t		flags;
	uint64_t	sb_off;
} __attribute__ ((packed));

struct scsi_mbr {
	char			magic[4];
	uint32_t		version;
	uint8_t			reserved1[8];
	struct scsi_blockptr	blockptr;
	uint8_t			reserved2[0x50];
	struct boot_info	boot_info;
} __attribute__((__packed__));

#define BOOT_INFO_VERSION		1
#define BOOT_INFO_MAGIC			"zIPL"
#define BOOT_INFO_DEV_TYPE_SCSI		0x02
#define BOOT_INFO_BP_TYPE_DUMP		0x01

/*
 * Check the zIPL magic number
 */
static int check_zipl_magic(void *buf)
{
	if (memcmp(buf, "zIPL", 4))
		return -1;
	return 0;
}

/*
 * Final step looks into program to find dump flag
 */
static int check_dump_program(struct scsi_blockptr *blockptr)
{
	uint64_t off = blockptr->blockno * l.blk_size;
	struct component_header header;

	zg_seek(g.fh, off, ZG_CHECK_ERR);
	zg_read(g.fh, &header, sizeof(header), ZG_CHECK_ERR);
	if (check_zipl_magic(&header.magic))
		return -1;
	return (header.type == component_header_dump) ? 0 : -1;
}

/*
 * Parse program table, see zipl docu to understand table structures
 */
static int check_program_table(uint64_t blockno)
{
	struct scsi_blockptr entries[l.blk_size / sizeof(struct scsi_blockptr)];
	unsigned int i;

	/* Entry 0, holds the magic, entry 1 the default */
	zg_seek(g.fh, blockno * l.blk_size, ZG_CHECK);
	zg_read(g.fh, &entries, l.blk_size, ZG_CHECK);
	if (check_zipl_magic(&entries[0]))
		return -1;
	for (i = 1; i < l.blk_size / sizeof(struct scsi_blockptr); i++) {
		if (entries[i].blockno == 0)
			break;
		if (check_dump_program(&entries[i]) == 0)
			return 0;
	}
	return -1;
}

/*
 * Check magic number and checksum of superblock at given offset
 */
static int check_sb(void)
{
	char buf[l.sb.csum_size];

	if (l.sb.magic != 0x5a46435044554d50ULL) /* ZFCPDUMP */
		return -1;
	/*
	 * Verify checksum
	 */
	zg_seek(g.fh, l.sb.part_start + l.sb.csum_off, ZG_CHECK);
	zg_read(g.fh, &buf, sizeof(buf), ZG_CHECK);
	if (zg_csum_partial(&buf, l.sb.csum_size, 0x12345678) != l.sb.csum)
		return -1;
	return 0;
}

/*
 * Check the SCSI dump boot info
 */
static int check_boot_info(struct boot_info *info)
{
	if (memcmp(&info->magic, BOOT_INFO_MAGIC, sizeof(info->magic)))
		return -1;
	if (info->dev_type != BOOT_INFO_DEV_TYPE_SCSI)
		return -1;
	if (info->bp_type != BOOT_INFO_BP_TYPE_DUMP)
		return -1;
	zg_seek(g.fh, info->sb_off, ZG_CHECK);
	zg_read(g.fh, &l.sb, sizeof(l.sb), ZG_CHECK);
	return check_sb();
}

/*
 * Detect if the bootmap contains ZFCPDUMP.
 * Walks through bootmap structs to find the dump flag
 */
static int dt_scsi_init(void)
{
	struct scsi_mbr mbr;

	zg_read(g.fh, &mbr, sizeof(mbr), ZG_CHECK);
	if (zg_ioctl(g.fh, BLKSSZGET, &l.blk_size, "BLKSSZGET", ZG_CHECK_NONE))
		return -1;
	if (check_zipl_magic(mbr.magic))
		return -1;
	if (check_program_table(mbr.blockptr.blockno))
		return -1;
	if (check_boot_info(&mbr.boot_info))
		return -1;
	dt_arch_set(DFI_ARCH_64);
	dt_version_set(l.sb.version);
	return 0;
}

/*
 * Print partition information for dump device
 */
static void dt_scsi_info(void)
{
	int part_num, part_ext;
	size_t start, cnt;

	start = l.sb.part_start / l.blk_size;
	cnt = l.sb.part_size / l.blk_size;
	part_num = util_part_search_fh(g.fh->fh, start, cnt, l.blk_size,
				       &part_ext);

	STDERR("Partition info:\n");
	if (part_num > 0)
		STDERR("  Partition number..: %d\n", part_num);
	else
		STDERR("  Partition number..: unknown\n");
	STDERR("  Maximum dump size.: %llu MB\n",
	       (unsigned long long) TO_MIB(l.sb.dump_size));
}

/*
 * Single-volume SCSI DT operations
 */
struct dt dt_scsi = {
	.desc	= "Single-volume SCSI dump tool",
	.init	= dt_scsi_init,
	.info	= dt_scsi_info,
};
