/*
 * vmur - Work with z/VM spool file queues (reader, punch, printer)
 *
 * Copyright IBM Corp. 2007, 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 <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <libgen.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <libgen.h>
#include <signal.h>
#include <iconv.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/sysmacros.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <ctype.h>
#include <linux/types.h>

#include "lib/vmdump.h"
#include "lib/zt_common.h"
#include "lib/util_libc.h"
#include "lib/vmcp.h"

#include "vmur.h"

#define	CP_PREFIX_LEN	11

/* Program name */
static char *prog_name;

/* Short description */
static const char tool_name[] =
	"vmur: Control virtual reader, punch, and printer";

/* Copyright notice */
static const char copyright_notice[] = "Copyright IBM Corp. 2007, 2017";

/*
 * Structure vmur is used to store the command line parameters and
 * other information needed at different places
 */
struct vmur {
	char  spoolid[5];
	int   spoolid_specified;
	char  spoolfile_name[9];
	int   spoolfile_name_specified;
	char  spoolfile_type[9];
	int   spoolfile_type_specified;
	char  devnode[PATH_MAX];
	int   devnode_specified;
	char  node[9];
	int   node_specified;
	char  file_name[PATH_MAX];
	int   file_name_specified;
	char  queue[4];
	int   queue_specified;
	char  user[9];
	int   user_specified;
	int   blocked_separator;
	int   blocked_padding;
	int   blocked_specified;
	int   rdr_specified;
	int   text_specified;
	int   force_specified;
	int   stdout_specified;
	int   hold_specified;
	int   convert_specified;
	enum  ur_action action;
	int   devno;
	int   ur_reclen;
	int   file_reclen;
	enum spoolfile_fmt spoolfile_fmt;
	struct sigaction sigact;
	iconv_t iconv;
	int   lock_fd;
	/* ur device spool state */
	char  spool_restore_cmd[MAXCMDLEN];
	int   spool_restore_needed;
	char  spool_class;
	int   spool_class_specified;
	char  spool_dest[9];
	int   spool_dest_specified;
	char  spool_form[9];
	int   spool_form_specified;
	char  spool_dist[9];
	int   spool_dist_specified;
} vmur_info;

/*
 * Print version information.
 */
static void print_version (void)
{
	printf("%s version %s\n", tool_name, RELEASE_STRING);
	printf("%s\n", copyright_notice);
}

/*
 * Convert string to to_upper
 */
static char *to_upper(char *str)
{
	char *ptr = str;

	while (*ptr) {
		*ptr = toupper(*ptr);
		ptr++;
	}
	return str;
}

/*
 * Convert string to valid CP spool file name
 */
static char *to_valid_cpname(char *str)
{
	if (strlen(str) > 8)
		str[8] = 0;

	while (*str) {
		if (!isprint(*str) || isspace(*str))
			*str = '_';
		str++;
	}
	return str;
}

/*
 * Convert string to valid Linux file name
 */
static char *to_valid_linux_name(char *str)
{
	while (*str) {
		if (*str == '/')
			*str = '_';
		str++;
	}
	return str;
}

/*
 * Print out usage text
 */

static char HELP_TEXT[] =
"Usage: vmur receive [OPTIONS] [SPOOLID] [FILE]\n"
"       vmur punch   [OPTIONS] [FILE]\n"
"       vmur print   [OPTIONS] [FILE]\n"
"       vmur purge   [OPTIONS] [SPOOLID]\n"
"       vmur order   [OPTIONS] [SPOOLID]\n"
"       vmur list    [OPTIONS] [SPOOLID]\n"
"\n"
"Control virtual reader, punch, and printer. Available commands are:\n"
"  * REceive: Receive spool files from reader queue\n"
"  * PUNch:   Punch a file to punch queue\n"
"  * PRint:   Print a file to printer queue\n"
"  * PURge:   Purge spool files\n"
"  * ORder:   Order spool file\n"
"  * LIst:    List spool files\n"
"\n"
"General options:\n"
"\n"
"-h, --help               Print this help, then exit.\n"
"-v, --version            Print version information, then exit.\n"
"\n"
"Options of 'receive' command:\n"
"\n"
"-d, --device             Device node of the VM virtual reader.\n"
"                         If omitted, /dev/vmrdr-0.0.000c is assumed.\n"
"-t, --text               Indicates text data causing EBCDIC to ASCII\n"
"                         conversion.\n"
"-b, --blocked            Use blocked mode.\n"
"-c, --convert            Specifies to convert VMDUMP file into a format\n"
"                         appropriate for further analysis with (l)crash.\n"
"-O, --stdout             Write spool file to stdout.\n"
"-f, --force              Overwrite files without prompt.\n"
"-H, --hold               Hold spool file in reader after receive.\n"
"-C, --class              Specify the spool class to match a reader file.\n"
"\n"
"Options for 'punch' and 'print' command:\n"
"\n"
"-d, --device             Device node of the VM virtual punch or printer.\n"
"                         If omitted, /dev/vmpun-0.0.000d or\n"
"                         /dev/vmprt-0.0.000e is assumed, respectively.\n"
"-t, --text               Indicates text data causing ASCII to EBCDIC\n"
"                         conversion.\n"
"-b, --blocked            Use blocked mode.\n"
"-r, --rdr                Indicates to transfer file from punch/printer\n"
"                         to reader.\n"
"-u, --user               Transfer file to user's reader.\n"
"                         If omitted: Your guest machine's reader.\n"
"-n, --node               Remote node to send the file to.\n"
"                         If omitted: Your local VM node.\n"
"-N, --name               Name of new spool file.\n"
"-f, --force              Convert file name to valid spool file name\n"
"                         automatically without prompt.\n"
"-C, --class              Spool class to be assigned to the created spool file.\n"
"    --form               Form to be assigned to the created spool file.\n"
"    --dest               Destination to be assigned to the created spool file.\n"
"    --dist               Distribution code for the resulting spool file.\n"
"\n"
"Options for 'purge' command:\n"
"\n"
"-f, --force              Purge without prompt.\n"
"\n"
"Options for 'order', 'list', and 'purge' commands:\n"
"\n"
"-q, --queue              Target queue for command. Possible queues are:\n"
"                         'rdr' (default), 'pun' and 'prt'.\n";

static void usage(void)
{
	printf("%s", HELP_TEXT);
}

/*
 * Signal handler
 */
static void set_signal_handler(struct vmur *info,
			       void (*handler) (int, siginfo_t *, void *))
{
	info->sigact.sa_flags = (SA_NODEFER | SA_SIGINFO | SA_RESETHAND);
	info->sigact.sa_sigaction = handler;

	if (sigemptyset(&info->sigact.sa_mask) < 0)
		goto fail;
	if (sigaction(SIGINT, &info->sigact, NULL) < 0)
		goto fail;
	if (sigaction(SIGTERM, &info->sigact, NULL) < 0)
		goto fail;

	return;
fail:
	ERR_EXIT("Could not initialize signal handler (errno = %i)\n", errno);
}

/*
 * Strip leading CP header from error message
 */
static void strip_cperr(char *buf)
{
	unsigned int i;

	for (i = 0; i < strlen(buf) - CP_PREFIX_LEN; i++) {
		if (strncmp(buf + i, "HCP", 3) == 0) {
			int offs = i + CP_PREFIX_LEN;
			memmove(buf, buf + offs, strlen(buf) - offs);
			/* terminate string and remove newline */
			buf[strlen(buf) - offs - 1] = 0;
			return;
		}
	}
}

/*
 * Handle CP error message and exit
 */
static void cperr_exit(char *cpcmd, int cprc, char *buf)
{
	if (strlen(buf) <= CP_PREFIX_LEN)
		ERR_EXIT("CP command '%s' failed with rc=%i\n", cpcmd, cprc);

	strip_cperr(buf);
	ERR_EXIT("%s\n", buf);
}

static void _cpcmd(char *cpcmd, char **resp, int *rc, int retry, int upper)
{
	struct vmcp_parm cp;
	char cmd[MAXCMDLEN];
	int ret;

	strcpy(cmd, cpcmd);
	cp.cpcmd = cmd;
	cp.do_upper = upper;
	cp.buffer_size = VMCP_DEFAULT_BUFSZ;

retry:
	ret = vmcp(&cp);

	switch (ret) {
	case VMCP_ERR_OPEN:
		ERR_EXIT("Could not issue CP command: \"%s\"\n"
			 "Ensure that vmcp kernel module is loaded!\n", cmd);

	case VMCP_ERR_SETBUF:
		goto fail;

	case VMCP_ERR_WRITE:
		goto fail;

	case VMCP_ERR_GETCODE:
		goto fail;

	case VMCP_ERR_GETSIZE:
		goto fail;

	case VMCP_ERR_READ:
		goto fail;

	case VMCP_ERR_TOOSMALL:
		if (retry) {
			cp.buffer_size = cp.response_size;
			free(cp.response);
			goto retry;
		}
		ERR_EXIT("Not enough buffer space (%u/%u) for CP "
			 "command '%s'.\nSorry, please issue command "
			 " on your 3270 console!\n", cp.response_size,
			 cp.buffer_size, cmd);
	}

	if (rc == NULL) {
		if (cp.cprc != 0) {
			/* caller wants us to handle the error */
			cperr_exit(cmd, cp.cprc, cp.response);
		}
	} else {
		*rc = cp.cprc;
	}

	if (resp)
		*resp = cp.response;
	else
		free(cp.response);
	return;

fail:
	ERR_EXIT("CP command '%s' failed.\n", cmd);
}

/*
 * Issue   CP command:
 * @cpcmd: CP command to be issued.
 * @resp:  CP command response on success, an error message on error.
 * @rc:    CP return code.
 * @retry: retry = 1 -> CP command can be retried.
 * This function converts the command string to uppercase.
 */
static void cpcmd(char *cpcmd, char **resp, int *rc, int retry)
{
	_cpcmd(cpcmd, resp, rc, retry, 1);
}

/*
 * Issue   CP command:
 * @cpcmd: CP command to be issued.
 * @resp:  CP command response on success, an error message on error.
 * @rc:    CP return code.
 * @retry: retry = 1 -> CP command can be retried.
 * This function does not convert the command string to uppercase.
 */
static void cpcmd_cs(char *cpcmd, char **resp, int *rc, int retry)
{
	_cpcmd(cpcmd, resp, rc, retry, 0);
}

/*
 * Extract minor from sysfs file
 */
static int get_minor(char *path)
{
	FILE *fd;
	char buf[20];
	int major, minor, rc;

	fd = fopen(path, "r");
	if (!fd)
		ERR_EXIT("Could not open %s (err = %i)\n", path, errno);
	rc = fread(buf, sizeof(buf), 1, fd);
	if (rc == -1)
		ERR_EXIT("Could not read %s (err = %i)\n", path, errno);
	fclose(fd);

	if (sscanf(buf, "%i:%i", &major, &minor) != 2)
		ERR_EXIT("Malformed content of %s: %s\n", path, buf);

	return minor;
}

/*
 * Find device number of ur device
 */
static int get_ur_devno(int minor)
{
	struct dirent *direntp;
	char dev_file[PATH_MAX];
	DIR *fd;
	char bus_id[9] = {};
	int devno;

	errno = 0;
	fd = opendir(SYSFS_CLASS_DIR);
	if (!fd)
		ERR_EXIT("Could not open %s (err = %i)\n", SYSFS_CLASS_DIR,
			 errno);
	while ((direntp = readdir(fd))) {
		if (strcmp(direntp->d_name, ".") == 0)
			continue;
		if (strcmp(direntp->d_name, "..") == 0)
			continue;
		sprintf(dev_file, "%s/%s/dev", SYSFS_CLASS_DIR,
			direntp->d_name);
		if (get_minor(dev_file) == minor) {
			/* extract device id from <vmxxx-0.0.yyyy> */
			memcpy(bus_id, &direntp->d_name[6], 8);
			goto found;
		}
	};
	if (errno != 0)
		ERR_EXIT("Could not read %s (err = %i)\n",
			    SYSFS_CLASS_DIR, errno);
	else
		ERR_EXIT("Device is not online\n");
found:
	if (sscanf(bus_id, "0.0.%x", &devno) != 1)
		ERR_EXIT("Could not extract device number from %s\n",
			 direntp->d_name);
	closedir(fd);
	return devno;
}

/*
 * Extract major/minor from device node
 */
static dev_t get_node_dev(char *devnode)
{
	struct stat stat_info;

	if (stat(devnode, &stat_info)) {
		ERR("Unable to get status for '%s': %s\n", devnode,
		    strerror(errno));
		ERR_EXIT("Please check if device is online!\n");
	}

	return stat_info.st_rdev;
}

/*
 * Extract major of vmur driver
 */
static int get_driver_major(unsigned long *major)
{
	FILE *fh;
	char string[PROC_DEVICES_FILE_WIDTH];
	char last_string[PROC_DEVICES_FILE_WIDTH];

	fh = fopen(PROC_DEVICES_FILE, "r");
	if (!fh) {
		ERR("WARNING: Cannot check for vmur in file %s.\n%s\n",
		    PROC_DEVICES_FILE, strerror(errno));
		return -1; /* check not possible, just continue */
	}
	while (fscanf(fh, "%s", string) != EOF) {
		if (strcmp(string, "vmur") == 0) {
			fclose(fh);
			*major = atoi(last_string);
			return 0;
		} else {
			strcpy(last_string, string);
		}
	}
	fclose(fh);
	ERR_EXIT("Unit record device driver not loaded.\n");
}

/*
 * Copy printable characters
 */
static void strncpy_graph(char *dest, const char *src, size_t len)
{
	size_t n;

	for (n = 0; n < len; n++) {
		if (!isgraph(src[n]))
			break;
		dest[n] = src[n];
	}
	dest[n] = '\0';
}

/*
 * Create CP command to restore spooling options
 */
static void save_spool_options(struct vmur *info)
{
	char cmd[MAXCMDLEN], *resp;
	char cl, value[9], *tmp;
	int n;

	/* Retrieve spooling options for ur device */
	sprintf(cmd, "QUERY VIRTUAL %X", info->devno);
	cpcmd(cmd, &resp, NULL, 0);

	/* Prepare CP spool restore command */
	n = sprintf(info->spool_restore_cmd, "SPOOL %X", info->devno);

	/* Save the CLASS value if required */
	if (info->spool_class_specified) {
		cl = resp[13];
		n += sprintf(info->spool_restore_cmd + n, " CL %c", cl);
	}

	/* Save FORM value if required */
	if (info->spool_form_specified) {
		tmp = strstr(resp, "FORM ");
		if (tmp == NULL)
			ERR_EXIT("Could not retrieve value for the FORM "
				 "spooling option\n");
		strncpy_graph(value, tmp + 5, 8);
		n += sprintf(info->spool_restore_cmd + n, " FORM %s", value);
	}

	/* Save DEST value if required */
	if (info->spool_dest_specified) {
		tmp = strstr(resp, "DEST ");
		if (tmp == NULL)
			ERR_EXIT("Could not retrieve value for the DEST "
				 "spooling option\n");
		strncpy_graph(value, tmp + 5, 8);
		n += sprintf(info->spool_restore_cmd + n, " DEST %s", value);
	}

	/* Save DIST value if required */
	if (info->spool_dist_specified) {
		tmp = strstr(resp, "DIST ");
		if (tmp == NULL)
			ERR_EXIT("Could not retrieve value for the DIST "
				 "spooling option\n");
		strncpy_graph(value, tmp + 5, 8);
		n += sprintf(info->spool_restore_cmd + n, " DIST %s", value);
	}

	free(resp);
	++info->spool_restore_needed;
}

/*
 * Restore saved spooling options for a ur device
 */
static void restore_spool_options(struct vmur *info)
{
	if (!info->spool_restore_needed)
		return;

	cpcmd(info->spool_restore_cmd, NULL, NULL, 0);
	--info->spool_restore_needed;
}

/*
 * Returns non-zero if spool options have to be saved, changed, and restored,
 * otherwise zero is returned.
 */
static int require_spool_setup(struct vmur *info)
{
	return !!(info->spool_class_specified ||
		  info->spool_form_specified ||
		  info->spool_dest_specified ||
		  info->spool_dist_specified);
}

/*
 * Setup spooling options for a ur device
 */
static void setup_spool_options(struct vmur *info)
{
	char cmd[MAXCMDLEN];
	int n, rc;

	/*
	 * Check if spool options must be changed.  If so, save the current
	 * spool option values and restore them at program exit.
	 */
	if (!require_spool_setup(info))
		return;

	/* Save spool options */
	save_spool_options(info);

	/* Change spool options */
	n = sprintf(cmd, "SPOOL %X", info->devno);
	if (info->spool_class_specified)
		n += sprintf(cmd + n, " CLASS %c", info->spool_class);
	if (info->spool_form_specified)
		n += sprintf(cmd + n, " FORM %s", info->spool_form);
	if (info->spool_dest_specified)
		n += sprintf(cmd + n, " DEST %s", info->spool_dest);
	if (info->spool_dist_specified)
		n += sprintf(cmd + n, " DIST %s", info->spool_dist);

	cpcmd(cmd, NULL, &rc, 0);
	if (rc)
		ERR_EXIT("Could not set spooling options (rc=%i)\n", rc);
}

/*
 * Setup and check ur device
 */
static void setup_ur_device(struct vmur *info)
{
	unsigned long driver_major;
	dev_t node_dev;
	int rc;

	/*
	 * Few vmur commands do not require a particular device node and,
	 * therefore, no device setup is required.
	 */
	if (!strlen(info->devnode))
		return;

	node_dev = get_node_dev(info->devnode);
	rc = get_driver_major(&driver_major);
	if ((rc == 0) && (driver_major != major(node_dev)))
		ERR_EXIT("'%s' is not a unit record device.\n",
			 info->devnode);
	info->devno = get_ur_devno(minor(node_dev));
}

/*
 * initialize the vmur info structure
 */
static void init_info(struct vmur *info)
{
	memset(info, 0, sizeof(struct vmur));
	strcpy(info->queue, "rdr");
	info->lock_fd = -1;
}

/*
 * set positional spoolid parameter
 */
static void set_spoolid(struct vmur *info, char **argv, int argc, int optind,
			int mandatory)
{
	char *str;

	if (argc <= optind) {
		if (mandatory)
			ERR_EXIT("No spool id specified.\n");
		else
			return;
	}

	if ((argc > optind + 1) && (info->action != RECEIVE))
		ERR_EXIT("More than one spool id specified.\n");

	str = argv[optind];

	if (strlen(str) > 4)
		goto invalid;

	while (*str) {
		if (!isdigit(*str))
			goto invalid;
		str++;
	}

	strcpy(info->spoolid, argv[optind]);
	++info->spoolid_specified;
	return;

invalid:
	ERR_EXIT("Spoolid must be a decimal number in range 0-9999\n");
}

/*
 * set positional file parameter
 */
static void set_file(struct vmur *info, char **argv, int argc, int optind)
{
	if (argc <= optind)
		return;

	if (argc > optind + 1)
		ERR_EXIT("More than one file specified.\n");

	strncpy(info->file_name, argv[optind], sizeof(info->file_name) - 1);
	++info->file_name_specified;
}

/*
 * Set queue: rdr, pun or prt
 */
static void set_queue(struct vmur *info, char *queue)
{
	if (strcmp(queue, "rdr") == 0)
		strcpy(info->queue, "rdr");
	else if (strcmp(queue, "pun") == 0)
		strcpy(info->queue, "pun");
	else if (strcmp(queue, "prt") == 0)
		strcpy(info->queue, "prt");
	else
		ERR_EXIT("Invalid queue: %s\n", queue);
	return;
}

/*
 * Set block mode and the separator and padding byte
 */
static void set_blocked(struct vmur *info, char *blocked)
{
	if (sscanf(blocked, "0x%x,0x%x", &info->blocked_separator,
		   &info->blocked_padding) != 2)
		goto fail;
	if (info->blocked_separator > 255)
		goto fail;
	if (info->blocked_padding > 255)
		goto fail;
	++info->blocked_specified;
	return;
fail:
	ERR_EXIT("Invalid blocked parameter. It must have the format "
		 "'0xSS,0xPP'.\n");
}

/*
 * Set spool class value
 */
static void set_spool_class(struct vmur *info, const char *val, int is_rdr)
{
	char cl = val[0];

	if (strlen(val) > 1 || (!isalnum(cl) && cl != '*'))
		ERR_EXIT("Class must be one of A through Z, 0 through 9, "
			 "or an asterisk (*)\n");
	if (cl == '*' && !is_rdr)
		ERR_EXIT("The asterisk (*) class is only valid for readers\n");
	info->spool_class = toupper(cl);
	++info->spool_class_specified;
}

/*
 * Parse the command line: General options
 */
static void check_std_opts(int opt)
{
	switch (opt) {
	case 'v':
		print_version();
		exit(0);
	case 'h':
		usage();
		exit(0);
	}
}

static void std_usage_exit(void)
{
	fprintf(stderr, "Try '%s --help' for more information.\n", prog_name);
	exit(1);
}

/*
 * Parse the command line: Receive command
 */
static void parse_opts_receive(struct vmur *info, int argc, char *argv[])
{
	int opt, index;
	static struct option long_options[] = {
		{ "version",     no_argument,       NULL, 'v'},
		{ "help",        no_argument,       NULL, 'h'},
		{ "text",        no_argument,       NULL, 't'},
		{ "stdout",      no_argument,       NULL, 'O'},
		{ "force",       no_argument,       NULL, 'f'},
		{ "hold",        no_argument,       NULL, 'H'},
		{ "convert",     no_argument,       NULL, 'c'},
		{ "device",      required_argument, NULL, 'd'},
		{ "blocked",     required_argument, NULL, 'b'},
		{ "class",       required_argument, NULL, 'C'},
		{ 0,             0,                 0,    0  }
	};
	static const char option_string[] = "vhtOfHcd:b:C:";

	strcpy(info->devnode, VMRDR_DEVICE_NODE);
	while (1) {
		opt = getopt_long(argc, argv, option_string,
				  long_options, &index);
		if (opt == -1)
			break;
		check_std_opts(opt);
		switch (opt) {
		case 't':
			++info->text_specified;
			break;
		case 'd':
			++info->devnode_specified;
			strncpy(info->devnode, optarg,
				sizeof(info->devnode) - 1);
			break;
		case 'O':
			++info->stdout_specified;
			break;
		case 'b':
			set_blocked(info, optarg);
			break;
		case 'f':
			++info->force_specified;
			break;
		case 'H':
			++info->hold_specified;
			break;
		case 'c':
			++info->convert_specified;
			break;
		case 'C':
			set_spool_class(info, optarg, 1);
			break;
		default:
			std_usage_exit();
		}
	}

	set_spoolid(info, argv, argc, optind + 1, 1);
	set_file(info, argv, argc, optind + 2);

	CHECK_SPEC_MAX(info->text_specified, 1, "text");
	CHECK_SPEC_MAX(info->devnode_specified, 1, "devnode");
	CHECK_SPEC_MAX(info->file_name_specified, 1, "file");
	CHECK_SPEC_MAX(info->blocked_specified, 1, "blocked");
	CHECK_SPEC_MAX(info->force_specified, 1, "force");
	CHECK_SPEC_MAX(info->hold_specified, 1, "hold");
	CHECK_SPEC_MAX(info->stdout_specified, 1, "stdout");
	CHECK_SPEC_MAX(info->convert_specified, 1, "convert");
	CHECK_SPEC_MAX(info->spool_class_specified, 1, "class");

	if (info->stdout_specified && info->file_name_specified)
		ERR_EXIT("File name not allowed, when --stdout specified!\n");
	if (info->blocked_specified + info->text_specified +
	    info->convert_specified > 1)
		ERR_EXIT("Conflicting options: -b, -t and -c are mutually "
			 "exclusive.\n");
	if (!info->spool_class_specified)
		set_spool_class(info, "*", 1);
}

/*
 * Validate VM userID
 */
static int invalid_userid(char *operand)
{
	if (strlen(operand) > 8)
		return 1;
	while (*operand) {
		if (!isalnum(*operand) &&
		    (strchr("@#$_-", *operand) == NULL))
			return 1;
		operand++;
	}
	return 0;
}

/*
 * Validate CP command operand such as nodeID or name/type
 */
static int invalid_operand(char *operand)
{
	if (strlen(operand) > 8)
		return 1;
	while (*operand) {
		if (!isprint(*operand) || isspace(*operand))
			return 1;
		operand++;
	}
	return 0;
}

/*
 * Check whether user is in the CP directory
 */
static int check_local_user(const char *user)
{
	char cmd[MAXCMDLEN];
	int cprc;

	strcpy(cmd, "LINK ");
	strcat(cmd, user);
	cpcmd(cmd, NULL, &cprc, 0);
	if ((cprc == 53)	/* user not in CP directory */
	    || (cprc == 20))	/* Userid missing or invalid */
		return 1;
	else
		return 0;
}

/*
 * Set spool file name/type in vmur structure and validate name/type
 */
static void set_spoolfile_name(struct vmur *info, char *name)
{
	char spoolfile_name[PATH_MAX], spoolfile_type[PATH_MAX] = {};
	int i, flag = 0;

	strcpy(spoolfile_name, name);

	if (strlen(spoolfile_name) == 0)
		ERR_EXIT("Empty spool file name is invalid\n");

	/* check for period delimiting name from type */
	if (spoolfile_name[strlen(spoolfile_name) - 1] == '.')
		flag = 1; /* name/type string ends with period */
	for (i = strlen(spoolfile_name) - 1; i > 0; i--) {
		if (spoolfile_name[i] == '.') {
			if (flag && (spoolfile_name[i-1] == '.'))
				continue;
			else
				break;
		}
	}
	if ((i > 0) && (i < (int) strlen(spoolfile_name) - 1)) {
		strcpy(spoolfile_type, spoolfile_name + i + 1);
		spoolfile_name[i] = 0;
		++info->spoolfile_type_specified;
	}

	if (info->force_specified) {
		/* adjust spool file name, in order to have a valid one */
		to_valid_cpname(spoolfile_name);
		if (info->spoolfile_type_specified)
			to_valid_cpname(spoolfile_type);
		goto out;
	}
	if (invalid_operand(spoolfile_name) || invalid_operand(spoolfile_type))
		goto invalid;
out:
	strcpy(info->spoolfile_name, spoolfile_name),
	++info->spoolfile_name_specified;
	if (info->spoolfile_type_specified)
		strcpy(info->spoolfile_type, spoolfile_type);
	return;

invalid:
	ERR_EXIT("Malformed spool file name: %s\n"
		 "Specify --force, if the name should be converted "
		 "automatically.\n", name);
}

/*
 * Parse the command line: punch command
 */
static void parse_opts_punch_print(struct vmur *info, int argc, char *argv[])
{
	int opt, index;
	char *spoolfile_name = NULL;
	static struct option long_options[] = {
		{ "version",     no_argument,       NULL, 'v'},
		{ "help",        no_argument,       NULL, 'h'},
		{ "text",        no_argument,       NULL, 't'},
		{ "rdr",         no_argument,       NULL, 'r'},
		{ "force",       no_argument,       NULL, 'f'},
		{ "user",        required_argument, NULL, 'u'},
		{ "node",        required_argument, NULL, 'n'},
		{ "device",      required_argument, NULL, 's'},
		{ "blocked",     required_argument, NULL, 'b'},
		{ "name",        required_argument, NULL, 'N'},
		{ "class",       required_argument, NULL, 'C'},
		{ "dest",        required_argument, NULL, 'D'},
		{ "form",        required_argument, NULL, 'F'},
		{ "dist",	 required_argument, NULL, 'I'},
		{ 0,             0,                 0,    0  }
	};
	static const char option_string[] = "vhtrfu:n:d:b:N:C:";

	if (info->action == PUNCH) {
		strcpy(info->devnode, VMPUN_DEVICE_NODE);
		info->ur_reclen = VMPUN_RECLEN;
	} else {
		strcpy(info->devnode, VMPRT_DEVICE_NODE);
		info->ur_reclen = VMPRT_RECLEN;
	}

	while (1) {
		opt = getopt_long(argc, argv, option_string,
				  long_options, &index);
		if (opt == -1)
			break;
		check_std_opts(opt);
		switch (opt) {
		case 'd':
			++info->devnode_specified;
			strcpy(info->devnode, optarg);
			break;
		case 'N':
			++info->spoolfile_name_specified;
			spoolfile_name = optarg;
			break;
		case 'r':
			++info->rdr_specified;
			break;
		case 'f':
			++info->force_specified;
			break;
		case 't':
			++info->text_specified;
			break;
		case 'u':
			++info->user_specified;
			if (invalid_userid(optarg))
				ERR_EXIT("Invalid userid: %s\n", optarg);
			else
				strcpy(info->user, optarg);
			break;
		case 'b':
			set_blocked(info, optarg);
			break;
		case 'n':
			++info->node_specified;
			if (invalid_operand(optarg))
				ERR_EXIT("Invalid node specified.\n");
			else
				strcpy(info->node, optarg);
			break;
		case 'C':
			set_spool_class(info, optarg, 0);
			break;
		case 'D':
			++info->spool_dest_specified;
			if (invalid_operand(optarg))
				ERR_EXIT("Invalid destination: %s\n", optarg);
			else
				strcpy(info->spool_dest, optarg);
			break;
		case 'F':
			++info->spool_form_specified;
			if (invalid_operand(optarg))
				ERR_EXIT("Invalid form: %s\n", optarg);
			else
				strcpy(info->spool_form, optarg);
			break;
		case 'I':
			++info->spool_dist_specified;
			if (invalid_operand(optarg))
				ERR_EXIT("Invalid distribution code: %s\n", optarg);
			else
				strcpy(info->spool_dist, optarg);
			break;
		default:
			std_usage_exit();
		}
	}

	CHECK_SPEC_MAX(info->rdr_specified, 1, "rdr");
	CHECK_SPEC_MAX(info->force_specified, 1, "force");
	CHECK_SPEC_MAX(info->user_specified, 1, "user");
	CHECK_SPEC_MAX(info->file_name_specified, 1, "file");
	CHECK_SPEC_MAX(info->spoolfile_name_specified, 1, "name");
	CHECK_SPEC_MAX(info->user_specified, 1, "user");
	CHECK_SPEC_MAX(info->node_specified, 1, "node");
	CHECK_SPEC_MAX(info->blocked_specified, 1, "blocked");
	CHECK_SPEC_MAX(info->spool_class_specified, 1, "class");
	CHECK_SPEC_MAX(info->spool_form_specified, 1, "form");
	CHECK_SPEC_MAX(info->spool_dest_specified, 1, "dest");
	CHECK_SPEC_MAX(info->spool_dist_specified, 1, "dist");

	if (info->user_specified && !info->rdr_specified)
		ERR_EXIT("--user without --rdr specified\n");
	if (info->node_specified && !info->user_specified)
		ERR_EXIT("--node without --user specified\n");
	if ((info->user_specified && !info->node_specified)
	    && check_local_user(info->user))
		ERR_EXIT("Invalid userid: %s\n", info->user);
	if (info->node_specified && check_local_user(RSCS_USERID))
		ERR_EXIT("Invalid RSCS userid: %s\n", info->node);
	if (info->blocked_specified && info->text_specified)
		ERR_EXIT("Conflicting options: -b together with -t "
			 "specified\n");

	set_file(info, argv, argc, optind + 1);

	if (info->spoolfile_name_specified)
		set_spoolfile_name(info, spoolfile_name);
	else if (info->file_name_specified)
		set_spoolfile_name(info, basename(info->file_name));
	else
		ERR_EXIT("No name for spool file specified!\n");
}

/*
 * Parse the command line: Purge command
 */
static void parse_opts_purge(struct vmur *info, int argc, char *argv[])
{
	int opt, index;
	static struct option long_options[] = {
		{ "version",     no_argument,       NULL, 'v'},
		{ "help",        no_argument,       NULL, 'h'},
		{ "force",       no_argument,       NULL, 'f'},
		{ "queue",       no_argument,       NULL, 'q'},
		{ "class",       required_argument, NULL, 'C'},
		{ "dest",        required_argument, NULL, 'D'},
		{ "form",        required_argument, NULL, 'F'},
		{ 0,             0,                 0,    0  }
	};
	static const char option_string[] = "fvhq:C:";

	while (1) {
		opt = getopt_long(argc, argv, option_string,
				  long_options, &index);
		if (opt == -1)
			break;
		check_std_opts(opt);
		switch (opt) {
		case 'f':
			++info->force_specified;
			break;
		case 'q':
			++info->queue_specified;
			set_queue(info, optarg);
			break;
		case 'C':
			set_spool_class(info, optarg, 0);
			break;
		case 'D':
			++info->spool_dest_specified;
			if (invalid_operand(optarg))
				ERR_EXIT("Invalid destination: %s\n", optarg);
			else
				strcpy(info->spool_dest, optarg);
			break;
		case 'F':
			++info->spool_form_specified;
			if (invalid_operand(optarg))
				ERR_EXIT("Invalid form: %s\n", optarg);
			else
				strcpy(info->spool_form, optarg);
			break;
		default:
			std_usage_exit();
		}
	}
	CHECK_SPEC_MAX(info->force_specified, 1, "force");
	CHECK_SPEC_MAX(info->queue_specified, 1, "queue");
	CHECK_SPEC_MAX(info->spool_class_specified, 1, "class");
	CHECK_SPEC_MAX(info->spool_form_specified, 1, "form");
	CHECK_SPEC_MAX(info->spool_dest_specified, 1, "dest");
	set_spoolid(info, argv, argc, optind + 1, 0);
}

/*
 * Parse the command line: Order command
 */
static void parse_opts_order(struct vmur *info, int argc, char *argv[])
{
	int opt, index;
	static struct option long_options[] = {
		{ "version",     no_argument,       NULL, 'v'},
		{ "help",        no_argument,       NULL, 'h'},
		{ "queue",       required_argument, NULL, 'q'},
		{ 0,             0,                 0,    0  }
	};
	static const char option_string[] = "vhq:";

	while (1) {
		opt = getopt_long(argc, argv, option_string,
				  long_options, &index);
		if (opt == -1)
			break;
		check_std_opts(opt);
		switch (opt) {
		case 'q':
			++info->queue_specified;
			set_queue(info, optarg);
			break;
		default:
			std_usage_exit();
		}
	}
	CHECK_SPEC_MAX(info->queue_specified, 1, "queue");
	set_spoolid(info, argv, argc, optind + 1, 1);
}

/*
 * Parse the command line: List command
 */
static void parse_opts_list(struct vmur *info, int argc, char *argv[])
{
	int opt, index;
	static struct option long_options[] = {
		{ "version",     no_argument,       NULL, 'v'},
		{ "help",        no_argument,       NULL, 'h'},
		{ "queue",       required_argument, NULL, 'q'},
		{ 0,             0,                 0,    0  }
	};
	static const char option_string[] = "vhq:";

	while (1) {
		opt = getopt_long(argc, argv, option_string,
				  long_options, &index);
		if (opt == -1)
			break;
		check_std_opts(opt);
		switch (opt) {
		case 'q':
			++info->queue_specified;
			set_queue(info, optarg);
			break;
		default:
			std_usage_exit();
		}
	}
	CHECK_SPEC_MAX(info->queue_specified, 1, "queue");
	set_spoolid(info, argv, argc, optind + 1, 0);
}

/*
 * Parse the command line: Default options
 */
static void parse_opts_default(int argc, char *argv[])
{
	int opt, index;
	static struct option long_options[] = {
		{ "version",     no_argument,       NULL, 'v'},
		{ "help",        no_argument,       NULL, 'h'},
		{ 0,             0,                 0,    0  }
	};
	static const char option_string[] = "vh";

	while (1) {
		opt = getopt_long(argc, argv, option_string,
				  long_options, &index);
		if (opt == -1)
			break;
		check_std_opts(opt);
		std_usage_exit();
	}
}

/*
 * Parse action strings
 */
static int is_action(enum ur_action action, char *str)
{
	char action_str[80] = {};

	if (strlen(str) < ur_action_prefix_len[action])
		return 0;
	if (strlen(str) > strlen(ur_action_str[action]))
		return 0;

	strncpy(action_str, ur_action_str[action], strlen(str));

	if (strcasecmp(str, action_str) != 0)
		return 0;

	return 1;
}

/*
 * Get action string and set action field in info structure
 */
static int set_action(struct vmur *info, char *str)
{
	int action;

	for (action = 0; action < LAST; action++) {
		if (is_action((enum ur_action) action, str)) {
			info->action = (enum ur_action) action;
			return 0;
		}
	}
	return -EINVAL;
}

/*
 * The toplevel parameter parsing function
 */
static void parse_opts(struct vmur *info, int argc, char *argv[])
{
	if (argc == 1)
		ERR_EXIT("Missing command\n");

	if (set_action(info, argv[1])) {
		parse_opts_default(argc, argv);
		ERR("Unknown command '%s'\n", argv[1]);
		std_usage_exit();
	}

	switch (info->action) {
	case RECEIVE:
		return parse_opts_receive(info, argc, argv);
	case PUNCH:
		return parse_opts_punch_print(info, argc, argv);
	case PRINT:
		return parse_opts_punch_print(info, argc, argv);
	case PURGE:
		return parse_opts_purge(info, argc, argv);
	case ORDER:
		return parse_opts_order(info, argc, argv);
	case LIST:
		return parse_opts_list(info, argc, argv);
	default:
		ERR_EXIT("Internal error. Unknown action: %i\n", info->action);
	}
}

/*
 * Check if spool file has hold state "NONE"
 */
static void check_hold_state(char *spoolid)
{
	char cmd[MAXCMDLEN];
	char *response;

	sprintf(cmd, "QUERY READER * %s ALL SHORTDATE", spoolid);
	cpcmd(cmd, &response, 0, 0);
	response[114] = 0;
	if (strcmp(&response[110], "NONE") != 0)
		ERR_EXIT("Could not receive spool file %s: hold state = %s\n",
			 spoolid, &response[110]);
	free(response);
}

/*
 * Issue CP command ORDER RDR <spoolid>.
 * If ORDER fails, the CP command response is as follows:
 *      NO FILES ORDERED
 * HCPxxxnnnt <message text>
 * If ORDER returns successfully, CP command CHANGE RDR <spoolid> NOHOLD
 * is issued.
 */
static void order_change_reader_file(struct vmur *info)
{
	char cmd[MAXCMDLEN];

	sprintf(cmd, "ORDER * READER %s", info->spoolid);
	cpcmd(cmd, NULL, NULL, 0);
	sprintf(cmd, "CHANGE * READER %s NOHOLD", info->spoolid);
	cpcmd(cmd, NULL, NULL, 0);

	check_hold_state(info->spoolid);
}

/*
 * Issue CP command CLOSE RDR
 */
static void close_reader(struct vmur *info, const char *hold)
{
	char cmd[MAXCMDLEN];

	sprintf(cmd, "CLOSE %X %s", info->devno, hold);
	cpcmd(cmd, NULL, NULL, 0);
}

/*
 *  Check whether reader can handle class of reader spool file
 */
static int check_class(struct vmur *info)
{
	char cmd[MAXCMDLEN];
	char device_class, file_class;
	char *buf;

	sprintf(cmd, "QUERY VIRTUAL %X", info->devno);
	cpcmd(cmd, &buf, NULL, 0);
	device_class = buf[13];
	free(buf);
	if (device_class == '*') /* reader device can handle any class */
		return 0;

	sprintf(cmd, "QUERY RDR * %s ALL SHORTDATE", info->spoolid);
	cpcmd(cmd, &buf, NULL, 0);
	file_class = buf[91];
	free(buf);
	if (file_class != device_class)
		return 1;
	return 0;
}

/*
 * Get file name for spoolid from reader
 */
static int get_filename_from_reader(struct vmur *info)
{
	char cmd[MAXCMDLEN];
	char name[9] = {0};
	char type[9] = {0};
	char *buf;

	sprintf(cmd, "QUERY RDR * %s ALL SHORTDATE", info->spoolid);
	cpcmd(cmd, &buf, NULL, 0);
	memcpy(name, &buf[130], 8);
	util_strstrip(name);
	memcpy(type, &buf[140], 8);
	util_strstrip(type);
	free(buf);
	if (strlen(name) == 0) {
		ERR("Please specify name to receive "
		    "unnamed spool file: %s\n", info->spoolid);
		return 1;
	}

	to_valid_linux_name(name);
	to_valid_linux_name(type);

	if (strlen(type) == 0)
		strcpy(info->file_name, name);
	else
		sprintf(info->file_name, "%s.%s", name, type);
	info->file_name_specified = 1;
	return 0;
}

/*
 * Serialize access to unit record devices and their related spool file queues:
 * open lock file and apply lock
 */
static void acquire_lock(struct vmur *info)
{
	char failed_action[10] = {};

	info->lock_fd = open(LOCK_FILE, O_RDONLY | O_CREAT, S_IRUSR);
	if (info->lock_fd == -1) {
		ERR("WARNING: Unable to open lock file %s, continuing "
		    "without any serialization.\n", LOCK_FILE);
		return;
	}
	if (flock(info->lock_fd, LOCK_EX | LOCK_NB) == -1) {
		switch (info->action) {
		case RECEIVE:
			strcpy(failed_action, "received");
			break;
		case PUNCH:
			strcpy(failed_action, "punched");
			break;
		case PRINT:
			strcpy(failed_action, "printed");
			break;
		case PURGE:
			strcpy(failed_action, "purged");
			break;
		case ORDER:
			strcpy(failed_action, "ordered");
			break;
		default:
			strcpy(failed_action, "processed");
		}
		ERR("A concurrent instance of vmur is already active."
		    " No file %s.\n", failed_action);
		exit(EBUSY);
	}
}

/*
 * Release lock and close lock file
 */
static void release_lock(struct vmur *info)
{
	flock(info->lock_fd, LOCK_UN);
	close(info->lock_fd);
	info->lock_fd = -1;
}

/*
 * Check if file already exists. If yes, ask if it should be overwritten.
 */
static void check_overwrite(struct vmur *info)
{
	char buf[5] = {};
	char *str;
	struct stat stat_info;

	if (info->force_specified)
		return;

	if (stat(info->file_name, &stat_info))
		return;

	if (S_ISDIR(stat_info.st_mode)) {
		ERR("Cannot overwrite directory '%s'\n", info->file_name);
		exit(1);
	}

	fprintf(stderr, "%s: Overwrite '%s'? ", prog_name, info->file_name);
	str = fgets(buf, sizeof(buf), stdin);
	if (!str)
		exit(1);
	if (strcasecmp(buf, "y\n") == 0)
		return;
	if (strcasecmp(buf, "yes\n") == 0)
		return;
	close_reader(info, "HOLD");
	exit(0);
}

/*
 * Get format of spool file: Check VMDUMP and NETDATA flags,
 *                           set record length of file
 */
enum spoolfile_fmt get_spoolfile_fmt(struct vmur *info,
				     struct splink_page *buf)
{
	struct splink_record *rec;
	char netdata_id[5] = {0xc9, 0xd5, 0xd4, 0xd9, 0xf0}; /* EBCDIC: INMR0 */
	unsigned int i;

	rec = (struct splink_record *) &buf->data;

	if (buf->magic != 0)
		return TYPE_VMDUMP;

	info->file_reclen = buf->rec_len;

	for (i = 0; i < buf->data_recs; i++) {
		if (rec->ccw.flag & CCW_IMMED_FLAG) {
			rec = (struct splink_record *) ((char *) rec +
							sizeof(rec->ccw));
			continue; /* skip immediate CCWs */
		}
		if ((rec->data.flag & IS_CONTROL_RECORD) &&
		    (memcmp(rec->data.magic, netdata_id, 5) == 0))
			return TYPE_NETDATA;
		rec = (struct splink_record *) ((char *) rec + rec->record_len);
	}

	return TYPE_NORMAL;
}

/*
 * Convert record for text mode: Do EBCDIC->ASCII translation
 */
static int convert_text(struct vmur *info, struct splink_record *rec,
			char **out_ptr)
{
	size_t in_count = rec->ccw.data_len;
	size_t out_count = rec->ccw.data_len;
	char *data_ptr = (char *) &rec->data;
	int rc;

	if ((rec->ccw.data_len == 1) && (data_ptr[0] == 0x40))
		goto out; /* one blank -> just a newline */

	rc = iconv(info->iconv, &data_ptr, &in_count, out_ptr, &out_count);
	if ((rc == -1) || (in_count != 0)) {
		ERR("Code page translation EBCDIC-ASCII failed\n");
		return -1;
	}

out:
	**out_ptr = ASCII_LF;
	*out_ptr += 1;
	return 0;
}

/*
 * Convert record for binary mode: Fill up missing 0x40 bytes
 */
static void convert_binary(struct vmur *info, struct splink_record *rec,
			  char **out_ptr)
{
	int residual;

	memcpy(*out_ptr, &rec->data, rec->ccw.data_len);
	*out_ptr += rec->ccw.data_len;

	/* Since CP removed trailing EBCDIC blanks, we have */
	/* to insert them again */

	residual = info->file_reclen - rec->ccw.data_len;
	memset(*out_ptr, 0x40, residual);
	*out_ptr += residual;
}

/*
 * Convert record for blocked mode: remove padding bytes and add separator
 */
static void convert_blocked(struct vmur *info, struct splink_record *rec,
			   char **out_ptr)
{
	int residual, i;

	memcpy(*out_ptr, &rec->data, rec->ccw.data_len);
	*out_ptr += rec->ccw.data_len;

	/* Since CP removed trailing EBCDIC blanks, we have */
	/* to insert them again */

	residual = info->file_reclen - rec->ccw.data_len;
	memset(*out_ptr, 0x40, residual);
	*out_ptr += residual;

	/* Now remove trailing padding bytes */

	for (i = 0; i < info->file_reclen; i++) {
		if (*(*out_ptr - 1) != info->blocked_padding)
			break;
		*out_ptr -= 1;
	}

	/* ... and insert separator */

	**out_ptr = info->blocked_separator;
	*out_ptr += 1;
}

/*
 * Extract data from VM spool file data blocks (SPLINK).
 */
ssize_t convert_sfdata(struct vmur *info, struct splink_page *in, char *out)
{
	struct splink_record *rec;
	char *out_ptr = out;
	unsigned int i, rc = 0;

	rec = (struct splink_record *) &in->data;

	for (i = 0; i < in->data_recs; i++) {
		if (rec->ccw.opcode == NOP) {
			rec = (struct splink_record *) ((char *) rec +
				rec->record_len);
			continue; /* skip NOP CCWs */
		} else if (rec->ccw.flag & CCW_IMMED_FLAG) {
			rec = (struct splink_record *) ((char *) rec +
				sizeof(rec->ccw));
			continue; /* skip immediate CCWs */
		}
		if (info->text_specified) {
			rc = convert_text(info, rec, &out_ptr);
			if (rc)
				return rc;
		} else if (info->blocked_specified) {
			convert_blocked(info, rec, &out_ptr);
		} else
			convert_binary(info, rec, &out_ptr);
		rec = (struct splink_record *) ((char *) rec + rec->record_len);
	}
	return out_ptr - out;
}

/*
 * Write normal spool file data.
 */
int write_normal(struct vmur *info, struct splink_page *sfdata, int count,
		 int fho)
{
	char *outbuf;
	int len, i;

	for (i = 0; i < count; i++) {
		struct splink_page *data;

		data = &sfdata[i];
		outbuf = (char *) malloc((info->file_reclen + 1) *
					 data->data_recs);
		if (!outbuf) {
			ERR("Out of memory\n");
			return -ENOMEM;
		}
		len = convert_sfdata(info, data, outbuf);
		if (len < 0) {
			ERR("Data conversion failed\n");
			return -EINVAL;
		}
		if (write(fho, outbuf, len) == -1) {
			ERR("Write to file %s failed: %s\n", info->file_name,
			    strerror(errno));
			return -errno;
		}
		free(outbuf);
	}

	return 0;
}

/*
 * Write vmdump data.
 */
int write_vmdump(struct vmur *info, struct splink_page *sfdata, int count,
		 int fho)
{
	if (write(fho, sfdata, count * sizeof(sfdata[0])) == -1) {
		ERR("Write to file %s failed: %s\n", info->file_name,
		    strerror(errno));
		return -errno;
	}

	return 0;
}

/*
 * Clean up and restore spool options
 */
static void cleanup_atexit_fn(void)
{
	restore_spool_options(&vmur_info);
	release_lock(&vmur_info);
}

/*
 * Close VM virtual reader in case of a signal e.g. CTRL-C
*/
static void ur_receive_sig_handler(int UNUSED(sig), siginfo_t *UNUSED(sip),
				   void *UNUSED(p))
{
	close_reader(&vmur_info, "HOLD");
	cleanup_atexit_fn();
	ERR_EXIT("Operation terminated, spool file received incompletely.\n");
}

/*
 * Receive reader file.
 */
static void ur_receive(struct vmur *info)
{
	struct splink_page sfdata[READ_BLOCKS];
	enum spoolfile_fmt type;
	int fhi, fho = STDOUT_FILENO, count;
	int rc;

	if (check_class(info))
		ERR_EXIT("Reader device class does not match the specified "
			 "spool file class\n");
	close_reader(info, "HOLD");
	order_change_reader_file(info);

	fhi = open(info->devnode, O_RDONLY | O_NONBLOCK);
	if (fhi == -1)
		ERR_EXIT("Could not open device %s\n%s\n", info->devnode,
			 strerror(errno));

	set_signal_handler(info, ur_receive_sig_handler);

	if (!info->stdout_specified) {
		if (!info->file_name_specified &&
		    get_filename_from_reader(info))
			goto fail;
		check_overwrite(info);
	}

	/* Read first block and check spool file format */

	count = read(fhi, &sfdata, READ_BLOCKS * sizeof(sfdata[0]));
	if (count == -1) {
		ERR("Could not read from device %s\n%s\n", info->devnode,
		    strerror(errno));
		goto fail;
	}

	type = get_spoolfile_fmt(info, &sfdata[0]);
	if (info->convert_specified) {
		if (type != TYPE_VMDUMP) {
			ERR("Reader file %s does not have VMDUMP format, "
			    "conversion not possible.\n", info->spoolid);
			goto fail;
		} else {
			close(fhi);
			if (info->stdout_specified)
				rc = vmdump_convert(info->devnode, NULL,
						    prog_name);
			else
				rc = vmdump_convert(info->devnode,
						    info->file_name, prog_name);
			if (rc)
				goto fail;
			else
				goto vm_convert_done;
		}
	}
	if (type == TYPE_VMDUMP)
		ERR("INFO: Reader file %s has VMDUMP format.\n", info->spoolid);
	if (type == TYPE_NETDATA)
		ERR("INFO: Reader file %s has NETDATA format.\n",
		    info->spoolid);

	if (type != TYPE_VMDUMP) {
		int spoolid = atoi(info->spoolid);

		if (spoolid != sfdata->spoolid) {
			check_hold_state(info->spoolid);
			ERR_EXIT("Could not receive spool file %s. Spoolid "
				 "mismatch (%i)\n", info->spoolid,
				 sfdata->spoolid);
		}
	}

	/* read spool file data, convert it, and write it to output file or
	 * stdout */
	if (!info->stdout_specified) {
		fho = open(info->file_name, O_WRONLY | O_CREAT | O_TRUNC,
			   S_IRUSR | S_IWUSR);
		if (fho == -1) {
			ERR("Could not open file %s\n%s\n", info->file_name,
			    strerror(errno));
			goto fail;
		}
	}
	while (count != 0) {
		int blocks;

		blocks = count / sizeof(sfdata[0]);
		if (type == TYPE_VMDUMP)
			rc = write_vmdump(info, &sfdata[0], blocks, fho);
		else
			rc = write_normal(info, &sfdata[0], blocks, fho);

		if (rc)
			goto fail;

		/* read next records */
		count = read(fhi, &sfdata, READ_BLOCKS * sizeof(sfdata[0]));
		if (count == -1) {
			ERR("Could not read from device %s\n%s\n",
			    info->devnode, strerror(errno));
			goto fail;
		}
	}
	if (fho != STDOUT_FILENO)
		close(fho);
	close(fhi);
vm_convert_done:
	if (info->hold_specified)
		close_reader(info, "HOLD");
	else
		close_reader(info, "NOHOLD NOKEEP");
	return;

fail:
	close_reader(info, "HOLD");
	exit(1);
}

/*
 * Issue CP command CLOSE PUNCH
 */
static void close_ur_device(struct vmur *info)
{
	char cmd[MAXCMDLEN], spoolid[5] = {}, *response;
	int cprc;

	if (info->node_specified) {
		sprintf(cmd, "SPOOL %X NOCONT", info->devno);
		cpcmd(cmd, NULL, NULL, 0);
	}
	if (info->rdr_specified) {
		sprintf(cmd, "CLOSE %X TO ", info->devno);
		if (info->node_specified)
			strcat(cmd, RSCS_USERID);
		else if (info->user_specified)
			strcat(cmd, to_upper(info->user));
		else
			strcat(cmd, "*");
		strcat(cmd, " RDR");
	} else {
		sprintf(cmd, "CLOSE %X", info->devno);
	}
	strcat(cmd, " NAME ");
	strcat(cmd, info->spoolfile_name);
	if (info->spoolfile_type_specified) {
		strcat(cmd, " ");
		strcat(cmd, info->spoolfile_type);
	}
	cpcmd_cs(cmd, &response, &cprc, 0);
	memcpy(spoolid, &response[9], 4);
	free(response);
	if (cprc == 439) {
		if (info->action == PUNCH)
			sprintf(cmd, "PURGE * PUN %s", spoolid);
		else
			sprintf(cmd, "PURGE * PRT %s", spoolid);
		cpcmd(cmd, NULL, NULL, 0);
		ERR_EXIT("User %s spool fileid limit exceeded.\n"
			 , info->user);
	}
	if (info->rdr_specified) {
		if (info->node_specified)
			printf("Reader file with spoolid %s created and "
			       "transferred to %s.\n", spoolid, RSCS_USERID);
		else if (info->user_specified)
			printf("Reader file with spoolid %s created and "
			       "transferred to %s.\n", spoolid, info->user);
		else
			printf("Reader file with spoolid %s created.\n",
			       spoolid);
	} else if (info->action == PUNCH)
		printf("Punch file with spoolid %s created.\n", spoolid);
	else
		printf("Printer file with spoolid %s created.\n", spoolid);
}

/*
 * Issue CP command CLOSE PUNCH
 */
static void close_ur_device_simple(struct vmur *info)
{
	char cmd[MAXCMDLEN];

	sprintf(cmd, "CLOSE %X", info->devno);
	cpcmd(cmd, NULL, NULL, 0);
}

/*
 * Issue CP command CLOSE PUNCH PURGE
 */
static void close_ur_device_purge(struct vmur *info)
{
	char cmd[MAXCMDLEN];

	if (info->node_specified) {
		sprintf(cmd, "SPOOL %X NOCONT", info->devno);
		cpcmd(cmd, NULL, NULL, 0);
	}
	sprintf(cmd, "CLOSE %X PURGE", info->devno);
	cpcmd(cmd, NULL, NULL, 0);
}

/*
 * Issue "CP QUERY VIRTUAL devno" to retrieve punch device information
 * Exit, if devno has CONT status
 */
static int is_punch_cont(struct vmur *info)
{
	char *buf;
	char cmd[MAXCMDLEN];
	int rc = 0;

	sprintf(cmd, "QUERY VIRTUAL %X", info->devno);
	cpcmd(cmd, &buf, NULL, 0);
	if (!strncmp(buf + 15, "  CONT", 6))
		rc = 1;
	free(buf);
	return rc;
}

/*
 * Provide tag information for RSCS
 */
static void rscs_punch_setup(struct vmur *info)
{
	char cmd[MAXCMDLEN];

	sprintf(cmd, "SPOOL %X CONT", info->devno);
	cpcmd(cmd, NULL, NULL, 0);
	sprintf(cmd, "TAG DEV %X %s %s", info->devno, info->node, info->user);
	cpcmd(cmd, NULL, NULL, 0);
	return;
}

/*
 * Purge punch file in case of a signal e.g. CTRL-C
 */
static void ur_write_sig_handler(int UNUSED(sig), siginfo_t *UNUSED(sip),
				 void *UNUSED(p))
{
	close_ur_device_purge(&vmur_info);
	cleanup_atexit_fn();
	ERR_EXIT("Operation terminated, no spool file created.\n");
}

/*
 * Read on line from fd not including newline
 */
static int read_line(int fd, char *buf, int len, int lf)
{
	int offs = 0;

	memset(buf, 0, len);
	do {
		int rc;

		rc = read(fd, buf + offs, 1);
		if (rc < 0)
			return -EIO;
		if (rc == 0)
			return -ENODATA;
		if (*(buf + offs) == lf)
			goto found;
		offs++;
	} while (offs < len);

	return -EINVAL;

found:
	*(buf + offs) = 0;
	return offs;
}

/*
 * Read text file for punch/print
 */
static int read_text_file(struct vmur *info, int fd, char *out_buf, size_t len)
{
	unsigned int pos = 0;
	static int line = 1;
	char sep, pad;
	char *buf;
	int rc;

	sep = '\n';
	pad = ' ';

	buf = (char *) malloc(info->ur_reclen + 1);
	if (!buf)
		return -ENOMEM;

	do {
		int line_len;
		size_t rec_len, out_len;
		char *in_ptr, *out_ptr;

		line_len = read_line(fd, buf, info->ur_reclen  + 1, sep);
		if (line_len == -ENODATA) {
			break;
		} else if (line_len == -EINVAL) {
			ERR("Input line %i too long. Unit record length"
			    " must not exceed %i\n", line, info->ur_reclen);
			goto fail;
		} else if (line_len < 0) {
			ERR("Read failed: %s", strerror(errno));
			goto fail;
		}
		line++;
		memset(buf + line_len, pad, info->ur_reclen - line_len);
		rec_len = out_len = info->ur_reclen;
		in_ptr = buf;
		out_ptr = &out_buf[pos];
		rc = iconv(info->iconv, &in_ptr, &rec_len, &out_ptr, &out_len);
		if ((rc == -1) || (out_len != 0)) {
			ERR("Code page conversion failed at line %i\n", line);
			goto fail;
		}
		pos += info->ur_reclen;
	} while (pos < len);
	free(buf);
	return pos;
fail:
	free(buf);
	return -1;
}

/*
 * Read blocked file for punch/print
 */
static int read_blocked_file(struct vmur *info, int fd, char *out_buf,
			     size_t len)
{
	unsigned int pos = 0;
	static int line = 1;
	char sep, pad;
	int line_len;
	char *buf;

	sep = info->blocked_separator;
	pad = info->blocked_padding;

	buf = (char *) malloc(info->ur_reclen + 1);
	if (!buf)
		return -ENOMEM;

	do {
		line_len = read_line(fd, buf, info->ur_reclen  + 1, sep);
		if (line_len == -ENODATA) {
			break;
		} else if (line_len == -EINVAL) {
			ERR("Input line %i too long. Unit record length"
			    " must not exceed %i\n", line, info->ur_reclen);
			goto fail;
		} else if (line_len < 0) {
			ERR("Read failed: %s", strerror(errno));
			goto fail;
		}
		line++;
		memset(buf + line_len, pad, info->ur_reclen - line_len);
		memcpy(&out_buf[pos], buf, info->ur_reclen);
		pos += info->ur_reclen;
	} while (pos < len);
	free(buf);
	return pos;
fail:
	free(buf);
	return -1;
}

/*
 * Read file for punch/print
 */
static int read_input_file(struct vmur *info, int fd, char *out_buf, size_t len)
{
	int rc;

	if (info->text_specified) {
		rc = read_text_file(info, fd, out_buf, len);
	} else if (info->blocked_specified) {
		rc = read_blocked_file(info, fd, out_buf, len);
	} else {
		rc = read(fd, out_buf, len);
		if (rc == -1) {
			ERR("Could not read file %s\n%s\n", info->file_name,
			    strerror(errno));
			return -EIO;
		}
	}
	return rc;
}

/*
 * Write function for punch and printer
 */
static void ur_write(struct vmur *info)
{
	int fhi, fho, residual, anything_written = 0;
	char *sfdata;
	ssize_t count;

	/* close punch preventively */
	close_ur_device_simple(info);

	/* Check punch. If punch is spooled CONT, exit */
	if (is_punch_cont(info))
		ERR_EXIT("Virtual punch device %X is spooled CONT.\n",
			 info->devno);

	sfdata = (char *) malloc(info->ur_reclen * VMUR_REC_COUNT);
	if (!sfdata)
		ERR_EXIT("Could allocate memory for buffer (%i)\n",
			    info->ur_reclen);

	/* Open Linux file */
	if (info->file_name_specified) {
		fhi = open(info->file_name, O_RDONLY);
		if (fhi == -1)
			ERR_EXIT("Could not open file %s\n%s\n",
				 info->file_name, strerror(errno));
	} else {
		fhi = STDIN_FILENO;
	}

	if (info->node_specified)
		rscs_punch_setup(info);

	/* Open UR device */
	fho = open(info->devnode, O_WRONLY | O_NONBLOCK);
	if (fho == -1) {
		ERR("Could not open device %s\n%s\n", info->devnode,
		    strerror(errno));
		goto fail;
	}

	set_signal_handler(info, ur_write_sig_handler);

	/* read linux file data, and write it to VM punch device */
	do {
		count = read_input_file(info, fhi, sfdata,
					info->ur_reclen * VMUR_REC_COUNT);
		if (count < 0)
			goto fail;
		else if (count == 0)
			break; /* EOF */

		residual = (info->ur_reclen - (count % info->ur_reclen))
			% info->ur_reclen;
		memset(sfdata + count, 0, residual);
		if (write(fho, sfdata, count + residual) == -1) {
			ERR("Could not write on device %s (%s)\n",
			    info->devnode, strerror(errno));
			if (errno == EIO)
				ERR("Spool file limit exceeded or spool space "
				    "full?\n");
			goto fail;
		} else
			anything_written = 1;
	} while (1);

	close(fho);
	if (anything_written)
		close_ur_device(info);
	else
		ERR_EXIT("No spool file created - probably empty input.\n");
	free(sfdata);
	if (fhi != STDIN_FILENO)
		close(fhi);
	return;
fail:
	close_ur_device_purge(info);
	exit(1);
}

/*
 * Ask if file should be purged, if -f is not specified
 */
static void ur_purge_question(struct vmur *info)
{
	char buf[5] = {};
	char *str;

	if (info->force_specified)
		return;

	fprintf(stderr, "%s: purge selected %s file(s)? ", prog_name,
		info->queue);
	/*
	 * Release the vmur session lock while waiting for user input.  It is
	 * safe to release the lock here, because the ur_purge operation does
	 * not modify spool options.
	 */
	release_lock(info);
	str = fgets(buf, sizeof(buf), stdin);
	acquire_lock(info);
	if (!str)
		exit(1);
	if (strcasecmp(buf, "y\n") == 0)
		return;
	if (strcasecmp(buf, "yes\n") == 0)
		return;
	exit(0);
}

/*
 * Purge spool file
 */
static int ur_purge(struct vmur *info)
{
	char *buf, cmd[MAXCMDLEN];
	int n, m;

	ur_purge_question(info);

	/* Prepare the CP PURGE command */
	n = m = sprintf(cmd, "PURGE * %s", info->queue);

	/* Add selection criteria to match spool files */
	if (info->spoolid_specified)
		n += sprintf(cmd + n, " %s", info->spoolid);
	if (info->spool_class_specified)
		n += sprintf(cmd + n, " CLASS %c", info->spool_class);
	if (info->spool_form_specified)
		n += sprintf(cmd + n, " FORM %s", info->spool_form);
	if (info->spool_dest_specified)
		n += sprintf(cmd + n, " DEST %s", info->spool_dest);

	/* Purge all files if no spoolid or any of the class, form, or dest
	 * option is specified.
	 */
	if (n == m)
		n += sprintf(cmd + n, " ALL");

	cpcmd(cmd, &buf, NULL, 0);
	ERR("%s", buf);
	free(buf);
	return 0;
}

/*
 * Order spool file to top of the queue
 */
static int ur_order(struct vmur *info)
{
	char cmd[MAXCMDLEN];

	sprintf(cmd, "ORDER * %s %s", info->queue, info->spoolid);
	cpcmd(cmd, NULL, NULL, 0);
	return 0;
}

/*
 * List spool files
 */
static int ur_list(struct vmur *info)
{
	char *buf;
	char cmd[MAXCMDLEN];

	if (info->spoolid_specified)
		sprintf(cmd, "QUERY %s * %s ALL", info->queue, info->spoolid);
	else
		sprintf(cmd, "QUERY %s * ALL", info->queue);

	cpcmd(cmd, &buf, NULL, 1);
	printf("%s", buf);
	free(buf);
	return 0;
}

/*
 * Initialize iconv: "from" -> "to"
 */
static void setup_iconv(struct vmur *info, const char *from, const char *to)
{
	info->iconv = iconv_open(to, from);
	if (info->iconv == ((iconv_t) -1))
		ERR_EXIT("Could not initialize conversion table %s->%s.\n",
			 from, to);
}

int main(int argc, char **argv)
{
	/* Set name of program */
	prog_name = basename(argv[0]);

	/* Set default values */
	init_info(&vmur_info);

	/* Parse command line options and check syntax */
	parse_opts(&vmur_info, argc, argv);

	/* Register cleanup function */
	if (atexit(cleanup_atexit_fn))
		ERR_EXIT("Could not set up vmur session cleanup\n");

	/* Acquire a lock to serialize concurrent vmur invocations */
	acquire_lock(&vmur_info);

	/* Retrieve ur device number */
	setup_ur_device(&vmur_info);

	switch (vmur_info.action) {
	case RECEIVE:
		/* Setup spool options */
		setup_spool_options(&vmur_info);
		if (vmur_info.text_specified)
			setup_iconv(&vmur_info, EBCDIC_CODE_PAGE,
				    ASCII_CODE_PAGE);
		ur_receive(&vmur_info);
		break;
	case PUNCH:
	case PRINT:
		/* Setup spool options */
		setup_spool_options(&vmur_info);
		if (vmur_info.text_specified)
			setup_iconv(&vmur_info, ASCII_CODE_PAGE,
				    EBCDIC_CODE_PAGE);
		ur_write(&vmur_info);
		break;
	case PURGE:
		ur_purge(&vmur_info);
		break;
	case ORDER:
		ur_order(&vmur_info);
		break;
	case LIST:
		ur_list(&vmur_info);
		break;
	default:
		ERR("Internal error: unknown action '%i'\n", vmur_info.action);
		return -EINVAL;
	}
	return 0;
}
