/*
 * zipl - zSeries Initial Program Loader tool
 *
 * Functions to handle disk layout specific operations
 *
 * Copyright IBM Corp. 2001, 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 <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <sys/vfs.h>
#include <unistd.h>
#include <linux/fs.h>
#include <linux/major.h>
#include <linux/fiemap.h>
#include <linux/nvme_ioctl.h>
#include <linux/raid/md_u.h>
#include <assert.h>

#include "lib/util_proc.h"
#include "lib/util_sys.h"
#include "lib/util_libc.h"
#include "disk.h"
#include "error.h"
#include "install.h"
#include "job.h"
#include "misc.h"

/* from linux/hdregs.h */
#define HDIO_GETGEO		0x0301

#define DASD_IOCTL_LETTER	'D'
#define BIODASDINFO		_IOR(DASD_IOCTL_LETTER, 1, \
				     struct dasd_information)
#define DASD_PARTN_MASK		0x03
#define SCSI_PARTN_MASK		0x0f

/* Definitions for dasd device driver, taken from linux/include/asm/dasd.h */
struct dasd_information {
	unsigned int devno; 		/* S/390 devno */
	unsigned int real_devno; 	/* for aliases */
	unsigned int schid;		/* S/390 subchannel identifier */
	unsigned int cu_type : 16;	/* from SenseID */
	unsigned int cu_model : 8;	/* from SenseID */
	unsigned int dev_type : 16;	/* from SenseID */
	unsigned int dev_model : 8;	/* from SenseID */
	unsigned int open_count;
	unsigned int req_queue_len;
	unsigned int chanq_len; 	/* length of chanq */
	char type[4]; 			/* from discipline.name */
	unsigned int status; 		/* current device level */
	unsigned int label_block;	/* where to find the VOLSER */
	unsigned int FBA_layout; 	/* fixed block size (like AIXVOL) */
	unsigned int characteristics_size;
	unsigned int confdata_size;
	char characteristics[64];	/* from read_device_characteristics */
	char configuration_data[256];	/* from read_configuration_data */
};

static int
disk_determine_dasd_type(struct disk_info *data,
	struct dasd_information dasd_info)
{
	if (strncmp(dasd_info.type, "FBA ",4) == 0)
		data->type = disk_type_fba;
	else if (strncmp(dasd_info.type, "DIAG",4) == 0)
		data->type = disk_type_diag;
	else if (strncmp(dasd_info.type, "ECKD",4) == 0) {
		if (dasd_info.FBA_layout)
			data->type = disk_type_eckd_ldl;
		else
			data->type = disk_type_eckd_cdl;
	} else {
		error_reason("Unknown DASD type");
		return -1;
	}
	return 0;
}

static int
read_block_by_offset(int fd, int blksize, uint64_t offset, char *buffer)
{

	if (lseek(fd, offset, SEEK_SET) == -1) {
		/* Seek error. */
		error_text("Error: Could not seek to %llu: %s!\n",
			   (unsigned long long) offset);
		return -1;
	}

	misc_read(fd, buffer, blksize);

	return 0;
}

static int determine_virtblk_type(struct device_info *dev_info,
				  const struct stat *stats)
{
	struct disk_info *data = &dev_info->base[FIRST_MIRROR_ID];
	int fd, rc, shift, sb;
	char *device;
	char *buffer;

	rc = 0;
	buffer = (char *) malloc(data->phy_block_size);
	if (!buffer)
		return -1;

	/*
	 * The geo.start value reported for virtblk devices is based on a
	 * 512 byte blocksize.
	 * For DASD devices it is based on the real (most likely 4k) blocksize
	 * and the DASD device driver reports a shifted value.
	 * For virtblk devices we need to shift the value manually according to
	 * the physical blocksize of the device.
	 */
	shift = 0;
	for (sb = 512; sb < data->phy_block_size; sb = sb << 1)
		shift++;

	if (data->geo.heads == 15) {
		/* assume DASD */
		data->partnum = stats->st_rdev & DASD_PARTN_MASK;
		dev_info->device = stats->st_rdev & ~DASD_PARTN_MASK;

		rc = misc_temp_dev(dev_info->device, 1, &device);
		if (rc)
			goto out_err;

		fd = open(device, O_RDONLY);

		/* read 3rd record, containing the volume label */
		read_block_by_offset(fd, data->phy_block_size,
				     2 * data->phy_block_size, buffer);
		misc_ebcdic_to_ascii((unsigned char *) buffer,
				     (unsigned char *) buffer + 4);
		/* determine dasd type by label */
		if (!strncmp(buffer, "VOL1", 4)) {
			data->type = disk_type_eckd_cdl;
			data->geo.start >>= shift;
		} else if (!strncmp(buffer, "LNX1", 4)) {
			data->type = disk_type_eckd_ldl;
			data->geo.start >>= shift;
		} else if (!strncmp(buffer, "CMS1", 4)) {
			data->type = disk_type_eckd_ldl;
			data->geo.start >>= shift;
		} else {
			/* DASD label was not found,
			 * type has to be specified by hand
			 */
			error_text("Failed to read DASD label, "
				   "please specify type manually");
			rc = -1;
		}
		close(fd);
		misc_free_temp_dev(device);
	} else {
		data->type = disk_type_scsi;
		data->partnum = stats->st_rdev & SCSI_PARTN_MASK;
		dev_info->device = stats->st_rdev & ~SCSI_PARTN_MASK;
	}

out_err:
	free(buffer);
	return rc;
}

#define GET_TARGET_OR_GOTO_ERR()					\
	do {								\
		if (target_id < 0)					\
			goto err_inc;					\
		t = target_at(td, target_id);				\
		if (!t)							\
			goto err_exc;					\
	} while (0)

/**
 * Process a script output represented by FH and consisting
 * of pairs 'key=value' (each such pair is on a separate line).
 * Check its consistency and set the extracted target parameters
 * to the array of "targets" at TD.
 *
 * NOTE: this function defines specifications on valid output of
 * zipl helper scripts. See zipl-support-for-mirrored-devices.txt
 * for details. Before modifying this function, make sure that it
 * won't lead to format change.
 */
static int set_target_parameters(FILE *fh, struct job_target_data *td)
{
	int target_id = -1;
	struct target *t;
	char buffer[80];
	char value[40];
	int i;

	/* reset array of target parameters */
	memset(td->targets, 0, sizeof(td->targets));
	/*
	 * Process a stream of key-value pairs and complete the array of
	 * target parameters.
	 * Each key-value pair "targetbase=foo" title a group of parameters
	 * for a new target.
	 */
	while (fgets(buffer, 80, fh)) {
		if (sscanf(buffer, "targetbase=%s", value) == 1) {
			target_id++; /* new title */
			GET_TARGET_OR_GOTO_ERR();
			t->targetbase = misc_strdup(value);
			goto found;
		}
		if (sscanf(buffer, "targettype=%s", value) == 1) {
			GET_TARGET_OR_GOTO_ERR();
			type_from_target(value,	&t->targettype);
			goto found;
		}
		if (sscanf(buffer, "targetgeometry=%s", value) == 1) {
			GET_TARGET_OR_GOTO_ERR();
			t->targetcylinders = atoi(strtok(value, ","));
			t->targetheads = atoi(strtok(NULL, ","));
			t->targetsectors = atoi(strtok(NULL, ","));
			goto found;
		}
		if (sscanf(buffer, "targetblocksize=%s", value) == 1) {
			GET_TARGET_OR_GOTO_ERR();
			t->targetblocksize = atoi(value);
			goto found;
		}
		if (sscanf(buffer, "targetoffset=%s", value) == 1) {
			GET_TARGET_OR_GOTO_ERR();
			t->targetoffset = atol(value);
			goto found;
		}
		continue;
found:
		t->check_params++;
	}
	td->nr_targets = target_id + 1;
	/* Check for consistency */
	if (!td->nr_targets)
		/* Missed a title pair "targetbase=foo" in the stream */
		goto err_inc;
	/*
	 * Any group of "ECKD" type has to include targetgeometry
	 */
	for (i = 0; i < td->nr_targets; i++) {
		t = target_at(td, i);
		assert(t->check_params >= 4);
		if (disk_type_is_eckd(t->targettype) && t->check_params != 5)
			goto err_inc;
	}
	return 0;
err_inc:
	error_reason("%s", "Inconsistent script output");
	return -1;
err_exc:
	error_reason("%s", "Exceeded the maximum number of base disks");
	return -1;
}

/**
 * Set disk info using ready target parameters provided either by
 * user, or by script
 */
static int device_set_info_by_hint(struct job_target_data *td,
				   struct device_info *data, int fd)
{
	int majnum, minnum;
	struct stat stats;
	int i;

	assert(td->nr_targets != 0);
	/* set devno, type, phy_block_size */
	for (i = 0; i < td->nr_targets; i++) {
		data->base[i].devno = -1;
		data->base[i].type = get_targettype(td, i);
		data->base[i].phy_block_size = get_targetblocksize(td, i);

		if (sscanf(get_targetbase(td, i),
			   "%d:%d", &majnum, &minnum) == 2) {
			data->base[i].disk = makedev(majnum, minnum);
			data->base[i].targetbase_def = defined_as_device;
		} else {
			if (stat(get_targetbase(td, i), &stats)) {
				error_reason(strerror(errno));
				error_text("Could not get information for "
					   "file '%s'", get_targetbase(td, i));
				return -1;
			}
			if (!S_ISBLK(stats.st_mode)) {
				error_reason("Target base device '%s' is not "
					     "a block device",
					     get_targetbase(td, i));
				return -1;
			}
			data->base[i].disk = stats.st_rdev;
			data->base[i].targetbase_def = defined_as_name;
		}
		if (data->base[i].targetbase_def !=
		    data->base[0].targetbase_def) {
			error_reason("Target base disks are defined by different ways");
			return -1;
		}
		data->base[i].partnum = 0;
		if (data->base[i].type == disk_type_scsi &&
		    ioctl(fd, NVME_IOCTL_ID) >= 0)
			data->base[i].is_nvme = 1;
	}
	return 0;
}

/**
 * Calculate target parameters in the case when no hints were provided
 */
static int disk_set_info_auto(struct device_info *dev_info,
			      const struct stat *stats, int fd)
{
	struct disk_info *data = &dev_info->base[FIRST_MIRROR_ID];
	struct dasd_information dasd_info;

	if (ioctl(fd, BLKSSZGET, &data->phy_block_size)) {
		error_reason("Could not get blocksize");
		return -1;
	}
	if (!dev_info->drv_name) {
		/* Driver name cannot be read */
		if (ioctl(fd, BIODASDINFO, &dasd_info)) {
			data->devno = -1;
			if (data->geo.start) {
				/* SCSI partition */
				data->type = disk_type_scsi;
				data->partnum =
					stats->st_rdev & SCSI_PARTN_MASK;
				dev_info->device =
					stats->st_rdev & ~SCSI_PARTN_MASK;
			} else {
				/* SCSI disk */
				data->type = disk_type_scsi;
				data->partnum = 0;
				dev_info->device = stats->st_rdev;
			}
		} else {
			/* DASD */
			data->devno = dasd_info.devno;
			if (disk_determine_dasd_type(data, dasd_info))
				return -1;
			data->partnum = stats->st_rdev & DASD_PARTN_MASK;
			dev_info->device = stats->st_rdev & ~DASD_PARTN_MASK;
		}
	} else if (strcmp(dev_info->drv_name, UTIL_PROC_DEV_ENTRY_DASD) == 0) {
		/* Driver name is 'dasd' */
		if (ioctl(fd, BIODASDINFO, &dasd_info)) {
			error_reason("Could not determine DASD type");
			return -1;
		}
		data->devno = dasd_info.devno;
		if (disk_determine_dasd_type(data, dasd_info))
			return -1;
		data->partnum = stats->st_rdev & DASD_PARTN_MASK;
		dev_info->device = stats->st_rdev & ~DASD_PARTN_MASK;
	} else if (strcmp(dev_info->drv_name, UTIL_PROC_DEV_ENTRY_SD) == 0) {
		/* Driver name is 'sd' */
		data->devno = -1;
		data->type = disk_type_scsi;
		data->partnum = stats->st_rdev & SCSI_PARTN_MASK;
		dev_info->device = stats->st_rdev & ~SCSI_PARTN_MASK;

	} else if (strcmp(dev_info->drv_name,
			  UTIL_PROC_DEV_ENTRY_VIRTBLK) == 0) {
		/* Driver name is 'virtblk' */
		if (ioctl(fd, HDIO_GETGEO, &data->geo) != 0)
			perror("Could not retrieve disk geometry information.");
		if (ioctl(fd, BLKSSZGET, &data->phy_block_size) != 0)
			perror("Could not retrieve blocksize information.");

		if (determine_virtblk_type(dev_info, stats)) {
			error_reason("Virtblk device type not clearly "
				     "determined.");
			return -1;
		}
	} else if (strcmp(dev_info->drv_name,
			  UTIL_PROC_DEV_ENTRY_BLKEXT) == 0 &&
		   ioctl(fd, NVME_IOCTL_ID) >= 0) {
		/* NVMe path, driver name is 'blkext' */
		data->devno = -1;
		data->type = disk_type_scsi;
		data->is_nvme = 1;

		if (util_sys_dev_is_partition(stats->st_rdev)) {
			if (util_sys_get_base_dev(stats->st_rdev,
						  &dev_info->device))
				return -1;
			data->partnum = util_sys_get_partnum(stats->st_rdev);
			if (data->partnum == -1)
				return -1;
		} else {
			dev_info->device = stats->st_rdev;
			data->partnum = 0;
		}
	} else {
		/* Driver name is unknown */
		error_reason("Unsupported device driver '%s'",
			     dev_info->drv_name);
		return -1;
	}
	return 0;
}

/**
 * Evaluate and set source type
 */
static void set_source_type(struct job_target_data *td,
			    const char *drv_name, char **script_file)
{
	const char *script_prefix = util_libdir_path("zipl_helper.");
	struct stat script_stats;

	if (td->source == source_user) {
		/* do not reset user-specified target parameters */
		return;
	}
	/* Check if targetbase script is available */
	if (drv_name)
		misc_asprintf(script_file, "%s%s", script_prefix,
			      drv_name);
	else
		misc_asprintf(script_file, "%s", script_prefix);
	if (!stat(*script_file, &script_stats)) {
		/* target parameters to be evaluated by script */
		td->source = source_script;
		return;
	}
	td->source = source_auto;
}

static void set_driver_name(int fd, struct device_info *info, dev_t device)
{
	struct util_proc_dev_entry dev_entry;

	if (info->drv_name)
		/* already set */
		return;
	if (util_proc_dev_get_entry(device, 1, &dev_entry) == 0) {
		mdu_array_info_t array;

		if (strcmp(dev_entry.name, UTIL_PROC_DEV_ENTRY_BLKEXT) == 0 &&
		    ioctl(fd, GET_ARRAY_INFO, &array) >= 0)
			/*
			 * Driver name is 'blkext',
			 * it is actually an md-partition
			 */
			info->drv_name = misc_strdup(UTIL_PROC_DEV_ENTRY_MD);
		else
			info->drv_name = misc_strdup(dev_entry.name);
		util_proc_dev_free_entry(&dev_entry);
	} else {
		misc_warn_on_failed_pdge(device);
	}
}

static int run_targetbase_script(struct job_target_data *td,
				 char *script_file, struct stat *stats)
{
	char *ppn_cmd = NULL;
	FILE *fh;

	misc_asprintf(&ppn_cmd, "%s %d:%d", script_file,
		      major(stats->st_rdev), minor(stats->st_rdev));
	pr_debug("Run %s\n", ppn_cmd);
	fh = popen(ppn_cmd, "r");
	free(ppn_cmd);

	if (!fh) {
		error_reason("Failed to run popen(%s,\"r\",)");
		return -1;
	}
	/* translate the script output to target parameters */
	if (set_target_parameters(fh, td)) {
		pclose(fh);
		return -1;
	}
	switch (pclose(fh)) {
	case 0:
		/* success */
		return 0;
	case -1:
		error_reason("Failed to run pclose");
		return -1;
	default:
		error_reason("Script could not determine target "
			     "parameters");
		return -1;
	}
}

/**
 * Set disk geometry using target parameters provided either by
 * user, or by script.
 *
 * Note: geo.start contains a sector number offset measured in
 * physical blocks, not sectors (512 bytes)
 */
static int device_set_geometry_by_hint(struct job_target_data *td,
				       struct device_info *data)
{
	int i;

	assert(td->nr_targets != 0);
	for (i = 0; i < td->nr_targets; i++) {
		data->base[i].geo.heads = get_targetheads(td, i);
		data->base[i].geo.sectors = get_targetsectors(td, i);
		data->base[i].geo.cylinders = get_targetcylinders(td, i);
		data->base[i].geo.start = get_targetoffset(td, i);
	}
	return 0;
}

static int disk_set_geometry_auto(int fd, struct disk_info *info)
{
	if (ioctl(fd, HDIO_GETGEO, &info->geo)) {
		error_reason("Could not get disk geometry");
		return -1;
	}
	return 0;
}

/**
 * The final step of setting disk info.
 * Common for all source types
 *
 * DATA: disk info to be completed
 * Pre-condition: disk type is already known and set at DATA->type
 */
static int disk_set_info_complete(struct job_target_data *td,
				  struct disk_info *data,
				  struct stat *stats, int fd)
{
	long devsize;

	/* Get size of device in sectors (512 byte) */
	if (ioctl(fd, BLKGETSIZE, &devsize)) {
		error_reason("Could not get device size");
		return -1;
	}
	/* Check for valid CHS geometry data. */
	if (disk_type_is_eckd(data->type) && (data->geo.cylinders == 0 ||
	    data->geo.heads == 0 || data->geo.sectors == 0)) {
		error_reason("Invalid disk geometry (CHS=%d/%d/%d)",
			     data->geo.cylinders, data->geo.heads,
			     data->geo.sectors);
		return -1;
	}
	/* Convert device size to size in physical blocks */
	data->phy_blocks = devsize / (data->phy_block_size / 512);
	/*
	 * Adjust start on SCSI according to block_size.
	 * device-mapper devices, which are evaluated only
	 * in "source_script" mode, are skipped
	 */
	if (data->type == disk_type_scsi && td->source == source_auto)
		data->geo.start =
			data->geo.start / (data->phy_block_size / 512);
	if (data->partnum != 0)
		data->partition = stats->st_rdev;
	return 0;
}

static void device_get_alignment(struct device_info *dev_info,
				 struct job_target_data *td)
{
	struct disk_info *base;
	int i;

	for (i = 0; i < td->nr_targets; i++) {
		base = &dev_info->base[i];
		if (dev_info->align < base->phy_block_size)
			dev_info->align = base->phy_block_size;
	}
}

static int device_set_info_complete(struct device_info *dev_info,
				    struct job_target_data *td)
{
	struct util_proc_part_entry part_entry;

	/* Try to get device name */
	if (util_proc_part_get_entry(dev_info->device, &part_entry) == 0) {
		dev_info->name = misc_strdup(part_entry.name);
		util_proc_part_free_entry(&part_entry);
		if (!dev_info->name)
			return -1;
	}
	/* Initialize file system block size with invalid value */
	dev_info->fs_block_size = -1;
	device_get_alignment(dev_info, td);
	return 0;
}

/**
 * Prepare INFO required to perform IPL installation on physical disks
 * participating in the logical DEVICE.
 * Preparation is performed in 2 steps:
 *
 * 1. Find out a set of physical "base" disks participating in the
 *    logical DEVICE. For each found disk calculate "target" parameters
 *    (type, geometry, physical block size, data offset, etc) and store
 *    it in the array of "targets" of TD;
 * 2. Complete INFO using the found base disks and calculated target
 *    parameters.
 *
 * TD: optionally contains target parameters specified by user via
 * config file, or special "target options" of zipl tool.
 * If target parameters were specified by user, then the step 1 above
 * is skipped.

 * To exclude any user assumptions about the DEVICE, this function
 * should be called with TD pointing to a zeroed structure.
 *
 * DEVICE: logical, or physical device, optionally formatted with a
 * file system.
 */
int device_get_info(const char *device, struct job_target_data *td,
		    struct device_info **info)
{
	char *script_file = NULL;
	struct device_info *data;
	struct stat stats;
	int fd;
	int i;

	if (stat(device, &stats)) {
		error_reason(strerror(errno));
		return -1;
	}
	fd = open(device, O_RDONLY);
	if (fd == -1) {
		error_reason(strerror(errno));
		return -1;
	}
	data = (struct device_info *)misc_malloc(sizeof(struct device_info));
	if (!data)
		goto error;
	memset((void *)data, 0, sizeof(struct device_info));
	set_driver_name(fd, data, stats.st_rdev);
	set_source_type(td, data->drv_name, &script_file);
	switch (td->source) {
	case source_script:
		if (run_targetbase_script(td, script_file, &stats))
			goto error;
		/* target parameters were set by the script output */
		assert(target_parameters_are_set(td));

		if (device_set_geometry_by_hint(td, data))
			goto error;
		if (device_set_info_by_hint(td, data, fd))
			goto error;
		data->device = stats.st_rdev;
		break;
	case source_user:
		/*
		 * target parameters were specified by user via
		 * "target" options
		 */
		assert(target_parameters_are_set(td));

		if (device_set_geometry_by_hint(td, data))
			goto error;
		if (device_set_info_by_hint(td, data, fd))
			goto error;
		/*
		 * multiple base disks are not supported
		 * with this source type
		 */
		assert(td->nr_targets == 1);
		data->device = data->base[FIRST_MIRROR_ID].disk;
		break;
	case source_auto:
		/* no ready target parameters are available */
		if (disk_set_geometry_auto(fd, &data->base[FIRST_MIRROR_ID]))
			goto error;
		if (disk_set_info_auto(data, &stats, fd))
			goto error;
		/*
		 * multiple base disks are not supported
		 * with this source type
		 */
		data->base[FIRST_MIRROR_ID].disk = data->device;
		td->nr_targets = 1;
		break;
	default:
		assert(0);
	}
	for (i = 0; i < td->nr_targets; i++) {
		if (disk_set_info_complete(td, &data->base[i], &stats, fd))
			goto error;
	}
	if (device_set_info_complete(data, td))
		goto error;
	free(script_file);
	close(fd);
	*info = data;
	return 0;
error:
	free(script_file);
	close(fd);
	free(data);
	return -1;
}

static int disk_is_tape(const char *device)
{
	int fd, rc = 0;

	/* Check for tape */
	fd = open(device, O_RDWR);
	if (fd == -1)
		return 0;
	if (rewind_tape(fd) == 0)
		rc = 1;
	close(fd);
	return rc;
}

/**
 * Get "extended type" of base disk by logical DEVICE
 *
 * This function may fail for various reasons. E.g. in case when
 * DEVICE is not eligible for boot record installation (not a
 * partition, etc). In case of success the resulted disk type is
 * stored in EXT_TYPE.
 */
int dump_disk_get_ext_type(const char *device, struct disk_ext_type *ext_type)
{
	struct job_target_data tmp = {.source = source_unknown};
	struct device_info *dev_info;
	struct disk_info *info;

	if (disk_is_tape(device)) {
		ext_type->is_tape = 1;
		return 0;
	}
	if (device_get_info(device, &tmp, &dev_info))
		return -1;
	info = &dev_info->base[0];
	ext_type->type = info->type;
	ext_type->is_nvme = info->is_nvme;

	device_free_info(dev_info);
	free_target_data(&tmp);
	return 0;
}

int disk_type_is_tape(struct disk_ext_type *ext_type)
{
	return ext_type->is_tape;
}

int disk_type_is_scsi(struct disk_ext_type *ext_type)
{
	return ext_type->type == disk_type_scsi;
}

int disk_type_is_eckd_ldl(struct disk_ext_type *ext_type)
{
	return ext_type->type == disk_type_eckd_ldl;
}

int disk_type_is_nvme(struct disk_ext_type *ext_type)
{
	return ext_type->is_nvme;
}

int disk_type_is_eckd(disk_type_t type)
{
	return (type == disk_type_eckd_ldl ||
		type == disk_type_eckd_cdl);
}

/**
 * Retrieve and set block size of the file system which contains FILENAME
 */
int device_info_set_fs_block(const char *filename, struct device_info *dinfo)
{
	int blocksize;
	int fd;
	int rc;

	/* Retrieve file system block size */
	fd = open(filename, O_RDONLY);
	if (fd == -1) {
		error_reason(strerror(errno));
		return -1;
	}
	rc = ioctl(fd, FIGETBSZ, &blocksize);
	close(fd);
	if (rc == -1) {
		error_reason("Could not get file system block size for '%s'",
			     filename);
		return -1;
	}
	dinfo->fs_block_size = blocksize;
	return 0;
}

/**
 * Retrieve disk info of the device which contains FILENAME
 * and set the filesystem block size
 */
int device_get_info_from_file(const char *filename,
			      struct job_target_data *target,
			      struct device_info **info)
{
	struct stat stats;
	char *device;

	if (stat(filename, &stats)) {
		error_reason(strerror(errno));
		return -1;
	}
	if (misc_temp_dev(stats.st_dev, 1, &device))
		return -1;
	if (device_get_info(device, target, info)) {
		misc_free_temp_dev(device);
		return -1;
	}
	misc_free_temp_dev(device);
	return device_info_set_fs_block(filename, *info);
}

void device_free_info(struct device_info *info)
{
	if (!info)
		return;
	if (info->name)
		free(info->name);
	if (info->drv_name)
		free(info->drv_name);
	free(info);
}

/* Retrieve the physical blocknumber (block on disk) of the specified logical
 * block (block in file). FD provides the file descriptor, LOGICAL is the
 * logical block number. Upon success, return 0 and store the physical
 * blocknumber in the variable pointed to by PHYSICAL. Return non-zero
 * otherwise. */
static int
disk_get_blocknum(int fd, int fd_is_basedisk, blocknum_t logical,
		  blocknum_t *physical, int fs_block_size,
		  struct disk_info *disk_info)
{
	blocknum_t phy_per_fs;
	blocknum_t mapped;
	int subblock;

	/* No file system: partition or raw disk */
	if (fs_block_size == -1) {
		if (fd_is_basedisk)
			*physical = logical;
		else
			*physical = logical + disk_info->geo.start;
		return 0;
	}
	/*
	 * Get mapping in file system blocks
	 */
	phy_per_fs = fs_block_size / disk_info->phy_block_size;
	subblock = logical % phy_per_fs;

	if (fs_map(fd, logical * disk_info->phy_block_size,
		   &mapped, fs_block_size) != 0)
		return -1;
	if (mapped == 0) {
		/* This is a hole in the file */
		*physical = 0;
	} else {
		/* Convert file system block to physical */
		*physical = mapped * phy_per_fs + subblock;
		/* Add partition start */
		*physical += disk_info->geo.start;
	}
	return 0;
}


/* Return the cylinder on which the block number BLOCKNUM is stored on the
 * CHS device identified by INFO. */
int
disk_cyl_from_blocknum(blocknum_t blocknum, struct disk_info *info)
{
	return blocknum / (info->geo.heads * info->geo.sectors);
}


/* Return the head on which the block number BLOCKNUM is stored on the
 * CHS device identified by INFO. */
int
disk_head_from_blocknum(blocknum_t blocknum, struct disk_info *info)
{
	return (blocknum / info->geo.sectors) % info->geo.heads;
}


/* Return the sector on which the block number BLOCKNUM is stored on the
 * CHS device identified by INFO. */
int
disk_sec_from_blocknum(blocknum_t blocknum, struct disk_info *info)
{
	return blocknum % info->geo.sectors + 1;
}


/* Create a block pointer in memory at location PTR which represents the
 * given blocknumber BLOCKNUM. INFO provides information about the disk
 * layout. */
void
disk_blockptr_from_blocknum(disk_blockptr_t* ptr, blocknum_t blocknum,
			    struct disk_info *info)
{
	switch (info->type) {
	case disk_type_scsi:
	case disk_type_fba:
	case disk_type_diag:
		ptr->linear.block = blocknum;
		ptr->linear.size = info->phy_block_size;
		ptr->linear.blockct = 0;
		break;
	case disk_type_eckd_ldl:
	case disk_type_eckd_cdl:
		if (blocknum == 0) {
			/* Special case: zero blocknum will be expanded to
			 * size * (blockct+1) bytes of zeroes. */
			ptr->chs.cyl = 0;
			ptr->chs.head = 0;
			ptr->chs.sec = 0;
		} else {

			ptr->chs.cyl = disk_cyl_from_blocknum(blocknum, info);
			ptr->chs.head = disk_head_from_blocknum(blocknum,
								info);
			ptr->chs.sec = disk_sec_from_blocknum(blocknum, info);
		}
		ptr->chs.size = info->phy_block_size;
		ptr->chs.blockct = 0;
		break;
	}
}

/**
 * Write BYTECOUNT bytes of data from memory at location DATA as a block to
 * the file identified by file descriptor FD at the current position in that
 * file aligned on ALIGN block size boundary and make sure that at most
 * INFO->PHY_BLOCK_SIZE bytes are written. INFO provides information about
 * the disk layout. Upon success, store the pointer to the resulting disk
 * block to BLOCK (if BLOCK is not NULL) and return 0. Return non-zero
 * otherwise. On success OFFSET contains offset of the first written byte
 */
static int
disk_write_block_aligned_base(struct misc_fd *mfd, int is_base_disk,
			      const void *data, size_t bytecount,
			      disk_blockptr_t *block, int fs_block_size,
			      struct disk_info *info, int align, off_t *offset)
{
	blocknum_t current_block;
	blocknum_t blocknum;
	off_t current_pos;

	if (align == 0)
		align = info->phy_block_size;

	current_pos = lseek(mfd->fd, 0, SEEK_CUR);
	if (current_pos == -1) {
		error_text(strerror(errno));
		return -1;
	}
	/* Ensure block alignment of current file pos */

	if (current_pos % align != 0) {
		current_pos = lseek(mfd->fd, align - current_pos % align, SEEK_CUR);
		if (current_pos == -1) {
	       		error_text(strerror(errno));
			return -1;
		}
	}
	current_block = current_pos / info->phy_block_size;
	/* Ensure maximum size */
	if (bytecount > (size_t)info->phy_block_size)
		bytecount = info->phy_block_size;
	/* Write data block */
	if (misc_write_or_simulate(mfd, data, bytecount))
		return -1;
	if (block != NULL) {
		/* Store block pointer */
		if (disk_get_blocknum(mfd->fd, is_base_disk, current_block,
				      &blocknum, fs_block_size, info))
			return -1;
		disk_blockptr_from_blocknum(block, blocknum, info);
	}
	if (offset)
		*offset = current_pos;
	return 0;
}

int disk_write_block_aligned(struct misc_fd *mfd, const void *data,
			     size_t bytecount, disk_blockptr_t *block,
			     int fs_block_size, struct disk_info *info)
{
	return disk_write_block_aligned_base(mfd, 0, data, bytecount, block,
					     fs_block_size, info,
					     info->phy_block_size, NULL);
}

/**
 * Write BYTECOUNT bytes from memory at location BUFFER to the file identified
 * by file descriptor FD at the current position in that file aligned on ALIGN
 * block size boundary and return the list of pointers to the disk blocks that
 * make up the respective part of the file. Upon success return the number of
 * blocks, set BLOCKLIST to point to the uncompressed list, and store offset of
 * the first written byte in OFFSET (if OFFSET is not NULL). Return zero
 * otherwise.
 */
blocknum_t
disk_write_block_buffer_align(struct misc_fd *mfd, int fd_is_basedisk,
			      const void *buffer, size_t bytecount,
			      disk_blockptr_t **blocklist, int fs_block_size,
			      int align, struct disk_info *info, off_t *offset)
{
	blocknum_t count;
	blocknum_t i;
	size_t written;
	size_t chunk_size;
	off_t pos;
	int rc;

	count = (bytecount + info->phy_block_size - 1) / info->phy_block_size;
	*blocklist = (disk_blockptr_t *)util_zalloc(sizeof(disk_blockptr_t) *
						    count);

	if (*blocklist == NULL) {
		close(mfd->fd);
		return 0;
	}
	/* Build list */
	for (i=0, written=0; i < count; i++, written += chunk_size) {
		chunk_size = bytecount - written;
		if (chunk_size > (size_t) info->phy_block_size)
			chunk_size = info->phy_block_size;
		rc = disk_write_block_aligned_base(mfd, fd_is_basedisk,
					VOID_ADD(buffer, written),
					chunk_size, &(*blocklist)[i],
					fs_block_size,
					info,
					i == 0 ? align : info->phy_block_size,
					&pos);
		if (rc)
			return 0;
		if (offset != NULL && i == 0)
			*offset = pos;
	}
	return count;
}

blocknum_t
disk_write_block_buffer(struct misc_fd *mfd, int fd_is_basedisk,
			const void *buffer, size_t bytecount,
			disk_blockptr_t **blocklist, int fs_block_size,
			int align, struct disk_info *info)
{
	return disk_write_block_buffer_align(mfd, fd_is_basedisk, buffer,
					     bytecount, blocklist,
					     fs_block_size, align, info,
					     NULL);
}

/* Print device node. */
void
disk_print_devt(dev_t d)
{
	printf("%02x:%02x", major(d), minor(d));
}

void disk_print_devname(dev_t dev)
{
	struct util_proc_part_entry part_entry;

	if (!util_proc_part_get_entry(dev, &part_entry)) {
		printf("%s", part_entry.name);
		util_proc_part_free_entry(&part_entry);
	} else {
		disk_print_devt(dev);
	}
}

void prepare_footnote_ptr(int source, char *ptr)
{
	if (source == source_user || source == source_script)
		strcpy(ptr, " *)");
	else
		strcpy(ptr, "");
}

void print_footnote_ref(int source, const char *prefix)
{
	if (source == source_user)
		printf("%s*) Data provided by user.\n", prefix);
	else if (source == source_script)
		printf("%s*) Data provided by script.\n", prefix);
}

/* Return a name for a given disk TYPE. */
char *
disk_get_type_name(disk_type_t type)
{
	switch (type) {
	case disk_type_scsi:
		return "SCSI disk layout";
	case disk_type_fba:
		return "FBA disk layout";
	case disk_type_diag:
		return "DIAG disk layout";
	case disk_type_eckd_ldl:
		return "ECKD/linux disk layout";
	case disk_type_eckd_cdl:
		return "ECKD/compatible disk layout";
	default:
		return "Unknown disk type";
	}
}

/* Return IPL types supported for a given disk TYPE */
char *disk_get_ipl_type(disk_type_t type, int is_dump)
{
	switch (type) {
	case disk_type_scsi:
		return "LD-";
	case disk_type_fba:
	case disk_type_eckd_ldl:
		return "CCW-";
	case disk_type_eckd_cdl:
		return is_dump ? "LD-" : "CCW- and LD-";
	default:
		return "";
	}
}

/* Return non-zero for ECKD large volumes. */
int
disk_is_large_volume(struct disk_info *info)
{
	return (info->type == disk_type_eckd_ldl ||
		info->type == disk_type_eckd_cdl) &&
		info->geo.cylinders == 0xfffe;
}

static void disk_print_info(struct disk_info *info, int source)
{
	const char *prefix = "    ";
	char footnote[4] = "";

	prepare_footnote_ptr(source, footnote);
	printf("%sDisk............................: ", prefix);
	disk_print_devt(info->disk);
	if (info->targetbase_def == defined_as_device)
		printf("%s", footnote);
	printf("\n");
	if (info->partnum != 0) {
		printf("%sPartition.......................: ", prefix);
		disk_print_devt(info->partition);
		printf("\n");
	}
	if (((info->type == disk_type_fba) ||
	     (info->type == disk_type_diag) ||
	     (info->type == disk_type_eckd_ldl) ||
	     (info->type == disk_type_eckd_cdl)) &&
	     (source == source_auto)) {
		printf("%sDASD device number..............: %04x\n",
		       prefix, info->devno);
	}
	printf("%sType............................: disk %s\n",
	       prefix, (info->partnum != 0) ? "partition" : "device");
	printf("%sDisk layout.....................: %s%s\n",
	       prefix, disk_get_type_name(info->type), footnote);
	if (disk_type_is_eckd(info->type)) {
		printf("%sGeometry - heads................: %d%s\n",
		       prefix, info->geo.heads, footnote);
		printf("%sGeometry - sectors..............: %d%s\n",
		       prefix, info->geo.sectors, footnote);
		if (disk_is_large_volume(info)) {
			/* ECKD large volume. There is not enough information
			 * available in INFO to calculate disk cylinder size. */
			printf("%sGeometry - cylinders............: > 65534\n",
			       prefix);
		} else {
			printf("%sGeometry - cylinders............: %d%s\n",
			       prefix, info->geo.cylinders, footnote);
		}
	}
	printf("%sGeometry - start................: %ld%s\n",
	       prefix, info->geo.start, footnote);
	printf("%sPhysical block size.............: %d%s\n",
	       prefix, info->phy_block_size, footnote);
	printf("%sDisk size in physical blocks....: %ld\n",
	       prefix, (long)info->phy_blocks);
	print_footnote_ref(source, prefix);
}

void device_print_info(struct device_info *this, struct job_target_data *td)
{
	int i;

	printf("  Device............................: ");
	disk_print_devt(this->device);
	printf("\n");

	if (this->name) {
		printf("  Device name.......................: %s",
		       this->name);
		printf("\n");
	}
	if (this->drv_name) {
		printf("  Device driver name................: %s\n",
		       this->drv_name);
	}
	if (this->fs_block_size >= 0)
		printf("  File system block size............: %d\n",
		       this->fs_block_size);
	for (i = 0; i < td->nr_targets; i++) {
		printf("  Base %d:\n", i + 1);
		disk_print_info(&this->base[i], td->source);
	}
}

/* Check whether a block is a zero block which identifies a hole in a file.
 * Return non-zero if BLOCK is a zero block, 0 otherwise. */
int
disk_is_zero_block(disk_blockptr_t *block, struct disk_info *info)
{
	switch (info->type) {
	case disk_type_scsi:
	case disk_type_fba:
		return block->linear.block == 0;
	case disk_type_eckd_ldl:
	case disk_type_eckd_cdl:
		return (block->chs.cyl == 0) && (block->chs.head == 0) &&
		       (block->chs.sec == 0);
	default:
		break;
	}
	return 0;
}


#define DASD_MAX_LINK_COUNT	255
#define SCSI_MAX_LINK_COUNT	65535

/* Check whether two block pointers FIRST and SECOND can be merged into
 * one block pointer by increasing the block count field of the first
 * pointer. INFO provides information about the disk type. Return non-zero if
 * blocks can be merged, 0 otherwise. */
static int
can_merge_blocks(disk_blockptr_t* first, disk_blockptr_t* second,
		 struct disk_info *info)
{
	int max_count;

	/* Zero blocks can never be merged */
	if (disk_is_zero_block(first, info) || disk_is_zero_block(second, info))
		return 0;
	if (info->type == disk_type_scsi)
		max_count = SCSI_MAX_LINK_COUNT;
	else
		max_count = DASD_MAX_LINK_COUNT;
	switch (info->type) {
	case disk_type_scsi:
	case disk_type_fba:
		/* Check link count limits */
		if (((int) first->linear.blockct) +
		    ((int) second->linear.blockct) + 1 > max_count)
		       return 0;
		if (first->linear.block + first->linear.blockct + 1 ==
		    second->linear.block)
			return 1;
		break;
	case disk_type_eckd_ldl:
	case disk_type_eckd_cdl:
		/* Check link count limits */
		if (((int) first->chs.blockct) +
		    ((int) second->chs.blockct) + 1 > max_count)
		       return 0;
		if ((first->chs.cyl == second->chs.cyl) &&
		    (first->chs.head == second->chs.head) &&
		    (first->chs.sec + first->chs.blockct + 1 ==
		     second->chs.sec))
			return 1;
		break;
	case disk_type_diag:
		break;
	}
	return 0;
}


/* Merge two block pointers FIRST and SECOND into one pointer. The resulting
 * pointer is stored in FIRST. INFO provides information about the disk
 * type. */
static void
merge_blocks(disk_blockptr_t* first, disk_blockptr_t* second,
	     struct disk_info *info)
{
	switch (info->type) {
	case disk_type_scsi:
	case disk_type_fba:
		first->linear.blockct += second->linear.blockct + 1;
		break;
	case disk_type_eckd_ldl:
	case disk_type_eckd_cdl:
		first->chs.blockct += second->chs.blockct + 1;
		break;
	case disk_type_diag:
		/* Should not happen */
		break;
	}
}


/* Analyze COUNT elements in LIST and try to merge pointers to adjacent
 * blocks. INFO provides information about the disk type. Return the new
 * number of elements in the list. */
blocknum_t
disk_compact_blocklist(disk_blockptr_t* list, blocknum_t count,
		       struct disk_info *info)
{
	blocknum_t i;
	blocknum_t last;

	if (count < 2)
		return count;
	for (i=1, last=0; i < count; i++) {
		if (can_merge_blocks(&list[last], &list[i], info)) {
			merge_blocks(&list[last], &list[i], info);
		} else {
			list[++last] = list[i];
		}
	}
	return last + 1;
}

/**
 * Retrieve a list of pointers to the disk blocks that make up a continuous
 * region REG in a file specified by FILENAME. If REG is NULL, then retrieve
 * a list of pointers for the whole file.
 * Upon success, return the number of blocks and set BLOCKLIST to point to
 * the uncompacted list. INFO provides information about the device which
 * contains the file. Return zero otherwise
 */
blocknum_t
disk_get_blocklist_from_file(const char *filename, struct file_range *reg,
			     disk_blockptr_t **blocklist, int fs_block_size,
			     struct disk_info *info)
{
	struct stat stats;
	int fd;
	off_t off;
	size_t count;
	blocknum_t blk_off;
	blocknum_t blk_count;
	blocknum_t i;
	blocknum_t blocknum;

	fd = open(filename, O_RDONLY);
	if (fd == -1) {
		error_reason(strerror(errno));
		error_text("Could not open file '%s'", filename);
		return 0;
	}
	if (fstat(fd, &stats)) {
		error_reason(strerror(errno));
		error_text("Could not get information for file '%s'",
			   filename);
		close (fd);
		return 0;
	}
	if (reg) {
		/*
		 * case of not block-aligned offsets is not implemented
		 */
		assert(reg->offset % info->phy_block_size == 0);

		off = reg->offset;
		count = reg->len;
	} else {
		off = 0;
		count = stats.st_size;
	}
	assert(off < stats.st_size);

	if (off + count > (size_t)stats.st_size)
		count = stats.st_size - off;

	blk_off = off / info->phy_block_size;
	blk_count = ((blocknum_t) count +
		     info->phy_block_size - 1) / info->phy_block_size;

	*blocklist = (disk_blockptr_t *)util_zalloc(sizeof(disk_blockptr_t) *
						    blk_count);
	if (*blocklist == NULL) {
		close(fd);
		return 0;
	}
	/* Build list */
	for (i = 0; i < blk_count; i++) {
		if (disk_get_blocknum(fd, 0, blk_off + i, &blocknum,
				      fs_block_size, info)) {
			close(fd);
			return 0;
		}
		disk_blockptr_from_blocknum(&(*blocklist)[i], blocknum, info);
	}
	close(fd);
	return blk_count;
}

/* Check whether input device is in subchannel set 0.
 * Path to "dev" attribute containing the major/minor number depends on
 * whether option CONFIG_SYSFS_DEPRECATED is set or not */

int disk_check_subchannel_set(int devno, dev_t device, char* dev_name)
{
	struct dirent *direntp;
	DIR* fdd;
	static const char sys_bus_ccw_dev_filename[] = "/sys/bus/ccw/devices";
	char dev_file[PATH_MAX];
	char *buffer;
	int minor, major;

	snprintf(dev_file, PATH_MAX, "%s/0.0.%04x", sys_bus_ccw_dev_filename,
		 devno);
	fdd = opendir(dev_file);
	if (!fdd)
		goto out_with_warning;
	while ((direntp = readdir(fdd)))
		if (strncmp(direntp->d_name, "block:", 6) == 0)
			break;
	if (direntp != NULL)
		snprintf(dev_file, PATH_MAX, "%s/0.0.%04x/%s/dev",
			 sys_bus_ccw_dev_filename, devno, direntp->d_name);
	else {
		closedir(fdd);
		snprintf(dev_file, PATH_MAX, "%s/0.0.%04x/block",
			 sys_bus_ccw_dev_filename, devno);
		fdd = opendir(dev_file);
		if (!fdd)
			goto out_with_warning;
		while ((direntp = readdir(fdd)))
			if (strncmp(direntp->d_name, "dasd", 4) == 0)
				break;
		if (direntp == NULL)
			goto out_with_warning;
		snprintf(dev_file, PATH_MAX, "%s/0.0.%04x/block/%s/dev",
			 sys_bus_ccw_dev_filename, devno, direntp->d_name);
	}
	closedir(fdd);
	if (misc_read_special_file(dev_file, &buffer, NULL, 1))
		goto out_with_warning;
	if (sscanf(buffer, "%i:%i", &major, &minor) != 2) {
		free(buffer);
		goto out_with_warning;
	}
	free(buffer);
	if (makedev(major, minor) != device) {
		error_reason("Dump target '%s' must belong to "
			     "subchannel set 0.", dev_name);
		return -1;
	}
	return 0;
out_with_warning:
	fprintf(stderr, "Warning: Could not determine whether dump target %s "
		"belongs to subchannel set 0.\n", dev_name);
	return 0;
}
