/*
 * s390-tools/zipl/src/disk.c
 *   Functions to handle disk layout specific operations.
 *
 * Copyright (C) 2001-2003 IBM Deutschland Entwicklung GmbH, IBM Corporation
 *
 * Author(s): Carsten Otte <cotte@de.ibm.com>
 *            Peter Oberparleiter <Peter.Oberparleiter@de.ibm.com>
 */

#include "disk.h"

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/vfs.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

/* Need to define this to prevent conflict between sys/vfs.h and linux/fs.h */
#define _LINUX_VFS_H
#include <linux/fs.h>

#include "error.h"
#include "misc.h"
#include "procpart.h"


#define DASD_MIN_API_VERSION	0
#define DASD_IOCTL_LETTER	'D'
#define DASDAPIVER		_IOR(DASD_IOCTL_LETTER, 0, int)
#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 */
};

int
disk_get_info(const char* device, struct disk_info** info)
{
	struct stat stats;
	struct procpart_entry entry;
	struct dasd_information dasd_info;
	struct disk_info *data;
	int fd;
	int rc;
	int dasd_api;
	long devsize;

	/* Get file information */
	if (stat(device, &stats)) {
		error_reason(strerror(errno));
		return -1;
	}
	/* Open device file */
	fd = open(device, O_RDWR);
	if (fd == -1) {
		error_reason(strerror(errno));
		return -1;
	}
	/* Check DASD driver API version */
	rc = ioctl(fd, DASDAPIVER, &dasd_api);
	if (rc == -1)
		dasd_api = -1;
	else if (rc) {
		error_reason("Unexpected driver response (%d)", rc);
		close(fd);
		return -1;
	}
	/* Check API version */
	if ((dasd_api < DASD_MIN_API_VERSION) && (dasd_api != -1)) {
		error_reason("Unsupported DASD driver API version (%d)",
			     dasd_api);
		close(fd);
		return -1;
	}
	/* Get memory for result */
	data = (struct disk_info *) misc_malloc(sizeof(struct disk_info));
	if (data == NULL) {
		close(fd);
		return -1;
	}
	memset((void *) data, 0, sizeof(struct disk_info));
	/* Get disk geometry. Note: geo.start contains a sector number
	 * offset measured in physical blocks, not sectors (512 bytes) */
	if (ioctl(fd, HDIO_GETGEO, &data->geo)) {
		error_reason("Could not get disk geometry");
		free(data);
		close(fd);
		return -1;
	}
	/* Get DASD information in case of DASD device */
	if (dasd_api != -1) {
		if (ioctl(fd, BIODASDINFO, &dasd_info)) {
			error_reason("Could not get information from DASD "
				     "driver");
			free(data);
			close(fd);
			return -1;
		}
		data->devno = dasd_info.devno;
	} else
		data->devno = -1;
	/* Determine disk type */
	if (dasd_api == -1)
		/* FIXME: SCSI check? */
		data->type = disk_type_scsi;
	else 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_classic;
		else
			data->type = disk_type_eckd_compatible;
	} else
		data->type = disk_type_unknown;
	/* Get physical block size */
	if (ioctl(fd, BLKSSZGET, &data->phy_block_size)) {
		error_reason("Could not get blocksize");
		free(data);
		close(fd);
		return -1;
	}
	/* Get size of device in sectors (512 byte) */
	if (ioctl(fd, BLKGETSIZE, &devsize)) {
		error_reason("Could not get device size");
		close(fd);
		free(data);
		return -1;
	}
	/* Convert device size to size in physical blocks */
	data->phy_blocks = devsize / (data->phy_block_size / 512);
	/* Set device and partition devno */
	if (data->type == disk_type_scsi) {
		data->partnum = stats.st_rdev & SCSI_PARTN_MASK;
		data->device = stats.st_rdev & ~SCSI_PARTN_MASK;
	} else {
		data->partnum = stats.st_rdev & DASD_PARTN_MASK;
		data->device = stats.st_rdev & ~DASD_PARTN_MASK;
	}
	if (data->partnum != 0)
		data->partition = stats.st_rdev;
	/* Try to get device name */
	if (procpart_get_entry(data->device, &entry) == 0) {
		data->name = misc_strdup(entry.name);
		procpart_free_entry(&entry);
		if (data->name == NULL) {
			close(fd);
			free(data);
			return -1;
		}
	}
	/* There is no easy way to find out whether there is a file system
	 * on this device, so we set the respective block size to an invalid
	 * value. */
	data->fs_block_size = -1;
	close(fd);
	*info = data;
	return 0;
}


int
disk_get_info_from_file(const char* filename, struct disk_info** info)
{
	struct stat stats;
	char* device;
	int blocksize;
	int fd;
	int rc;

	if (stat(filename, &stats)) {
		error_reason(strerror(errno));
		return -1;
	}
	/* 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;
	}
	/* Create temporary device file */
	rc = misc_temp_dev(stats.st_dev, 1, &device);
	if (rc)
		return -1;
	/* Get device info */
	rc = disk_get_info(device, info);
	if (rc == 0)
		(*info)->fs_block_size = blocksize;
	/* Clean up */
	misc_free_temp_dev(device);
	return rc;
}


void
disk_free_info(struct disk_info* info)
{
	if (info->name)
		free(info->name);
	free(info);
}


#ifndef REISERFS_SUPER_MAGIC
#define REISERFS_SUPER_MAGIC	0x52654973
#endif /* not REISERFS_SUPER_MAGIC */

#ifndef REISERFS_IOC_UNPACK
#define REISERFS_IOC_UNPACK	_IOW(0xCD,1,long)
#endif /* not REISERFS_IOC_UNPACK */

/* 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. */
int
disk_get_blocknum(int fd, blocknum_t logical, blocknum_t* physical,
		  struct disk_info* info)
{
	struct statfs buf;
	blocknum_t phy_per_fs;
	int mapped;
	int subblock;

	/* Get file system type */
	if (fstatfs(fd, &buf)) {
		error_reason(strerror(errno));
		return -1;
	}
	/* Files on ReiserFS need unpacking */
	if (buf.f_type == REISERFS_SUPER_MAGIC) {
		if (ioctl (fd, REISERFS_IOC_UNPACK, 1) == ENOSPC) {
			error_reason("Could not unpack ReiserFS file");
			return -1;
		}
	}
	/* Get mapping in file system blocks */
	phy_per_fs = info->fs_block_size / info->phy_block_size;
	mapped = logical / phy_per_fs;
	subblock = logical % phy_per_fs;
	if (ioctl(fd, FIBMAP, &mapped)) {
		error_reason("Could not get file mapping");
		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 += 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_classic:
	case disk_type_eckd_compatible:
		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;
	case disk_type_unknown:
		/* Should not happen */
		break;
	}
}


/* Write BYTECOUNT bytes of data from memory at location DATA as a block to
 * the file identified by file descriptor FD. Make sure that the data is
 * aligned on a block size boundary and that at most INFO->PHY_BLOCK_SIZE
 * bytes are written. INFO provides information about the disk layout. Upon
 * success, return 0 and store the pointer to the resulting disk block to BLOCK
 * (if BLOCK is not NULL). Return non-zero otherwise. */
int
disk_write_block_aligned(int fd, const void* data, size_t bytecount,
			 disk_blockptr_t* block, struct disk_info* info)
{
	blocknum_t current_block;
	blocknum_t blocknum;
	off_t current_pos;
	int align;

	current_pos = lseek(fd, 0, SEEK_CUR);
	if (current_pos == -1) {
		error_text(strerror(errno));
		return -1;
	}
	/* Ensure block alignment of current file pos */
	align = info->phy_block_size;
	if (current_pos % align != 0) {
		current_pos = lseek(fd, align - current_pos % align, SEEK_CUR);
	       	if (current_pos == -1) {
	       		error_text(strerror(errno));
			return -1;
		}
	}
	current_block = current_pos / align;
	/* Ensure maximum size */
	if (bytecount > (size_t) align)
		bytecount = align;
	/* Write data block */
	if (misc_write(fd, data, bytecount))
		return -1;
	if (block != NULL) {
		/* Store block pointer */
		if (disk_get_blocknum(fd, current_block, &blocknum, info))
			return -1;
		disk_blockptr_from_blocknum(block, blocknum, info);
	}
	return 0;
}


/* Write BYTECOUNT bytes from memory at location BUFFER to the file identified
 * by file descriptor FD 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 and set BLOCKLIST to point to the uncompressed list. Return zero
 * otherwise. Note that the data is written to a file position which is aligned
 * on a block size boundary. */
blocknum_t
disk_write_block_buffer(int fd, const void* buffer, size_t bytecount,
			disk_blockptr_t** blocklist,
			struct disk_info* info)
{
	disk_blockptr_t* list;
	blocknum_t count;
	blocknum_t i;
	size_t written;
	size_t chunk_size;
	int rc;

	count = (bytecount + info->phy_block_size - 1) / info->phy_block_size;
	list = (disk_blockptr_t *) misc_malloc(sizeof(disk_blockptr_t) *
					       count);
	if (list == NULL) {
		close(fd);
		return 0;
	}
	memset((void *) list, 0, sizeof(disk_blockptr_t) * count);
	/* 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(fd, VOID_ADD(buffer, written),
				chunk_size,
				&list[i], info);
		if (rc) {
			free(list);
			return 0;
		}
	}
	*blocklist = list;
	return count;
}


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


/* 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";
	case disk_type_fba:
		return "FBA";
	case disk_type_diag:
		return "DIAG";
	case disk_type_eckd_classic:
		return "ECKD/linux disk layout";
	case disk_type_eckd_compatible:
		return "ECKD/compatible disk layout";
	default:
		return "Unknown disk type";
	}
}


/* Print textual representation of INFO contents. */
void
disk_print_info(struct disk_info* info)
{
	printf("  Device..........................: ");
	disk_print_devt(info->device);
	printf("\n");
	if (info->partnum != 0) {
		printf("  Partition.......................: ");
		disk_print_devt(info->partition);
		printf("\n");
	}
	if (info->name) {
		printf("  Device name.....................: %s\n",
		       info->name);
	}
	if ((info->type == disk_type_fba) ||
	    (info->type == disk_type_diag) ||
	    (info->type == disk_type_eckd_classic) ||
	    (info->type == disk_type_eckd_compatible)) {
		printf("  DASD device number..............: %04x\n",
		       info->devno);
	}
	printf("  Type............................: disk %s\n",
	       (info->partnum != 0) ? "partition" : "device");
	printf("  Disk layout.....................: %s\n",
	       disk_get_type_name(info->type));
	printf("  Geometry - heads................: %d\n",
	       info->geo.heads);
	printf("  Geometry - sectors..............: %d\n",
	       info->geo.sectors);
	printf("  Geometry - cylinders............: %d\n",
	       info->geo.cylinders);
	printf("  Geometry - start................: %ld\n",
	       info->geo.start);
	if (info->fs_block_size >= 0)
		printf("  File system block size..........: %d\n",
		       info->fs_block_size);
	printf("  Physical block size.............: %d\n",
	       info->phy_block_size);
	printf("  Device size in physical blocks..: %ld\n",
	       (long) info->phy_blocks);
}


/* 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_classic:
	case disk_type_eckd_compatible:
		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_classic:
	case disk_type_eckd_compatible:
		/* 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:
	case disk_type_unknown:
		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_classic:
	case disk_type_eckd_compatible:
		first->chs.blockct += second->chs.blockct + 1;
		break;
	case disk_type_diag:
	case disk_type_unknown:
		/* 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 the file
 * specified by FILENAME. 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, disk_blockptr_t** blocklist,
			     struct disk_info* info)
{
	disk_blockptr_t* list;
	struct stat stats;
	int fd;
	blocknum_t 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;
	}
	/* Get number of blocks */
	count = ((blocknum_t) stats.st_size +
			info->phy_block_size - 1) / info->phy_block_size;
	if (count == 0) {
		error_reason("Could not read empty file '%s'", filename);
		close(fd);
		return 0;
	}
	list = (disk_blockptr_t *) misc_malloc(sizeof(disk_blockptr_t) *
					       count);
	if (list == NULL) {
		close(fd);
		return 0;
	}
	memset((void *) list, 0, sizeof(disk_blockptr_t) * count);
	/* Build list */
	for (i=0; i < count; i++) {
		if (disk_get_blocknum(fd, i, &blocknum, info)) {
			free(list);
			close(fd);
			return 0;
		}
		disk_blockptr_from_blocknum(&list[i], blocknum, info);
	}
	close(fd);
	*blocklist = list;
	return count;
}
