/*
 * HMC Drive FUSE Filesystem (FUSE.HMCDRVFS)
 *
 * Copyright IBM Corp. 2015, 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 <fuse.h>
#include <inttypes.h>
#include <langinfo.h>
#include <libgen.h>
#include <limits.h>
#include <locale.h>
#include <pthread.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <syslog.h>
#include <time.h>
#include <unistd.h>

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

#define HMCDRV_FUSE_LOGNAME	"hmcdrvfs" /* log prefix */
#define HMCDRV_FUSE_LOGHEAD	HMCDRV_FUSE_LOGNAME ": " /* log header */
#define HMCDRV_FUSE_FTPDEV	"/dev/hmcdrv" /* DIAG/SCLP FTP device */
#define HMCDRV_FUSE_MAXPATH	192 /* max. path length (including EOS) */
#define HMCDRV_FUSE_MAXFTPLEN	3 /* max. length of FTP cmd (dir/nls/get) */
#define HMCDRV_FUSE_OFSPATH	(HMCDRV_FUSE_MAXFTPLEN + 1) /* path in cmd */
#define HMCDRV_FUSE_MAXCMDLEN	(HMCDRV_FUSE_MAXPATH + HMCDRV_FUSE_OFSPATH)

/* internal increase (margin/offset) of cache timeout with respect to FUSE
 * options 'attr_timeout' and/or 'entry_timeout' (having default 1s)
 */
#define HMCDRV_FUSE_CACHE_TMOFS 30

/* size of file name/attributes cache (should be a prime number)
 */
#define HMCDRV_FUSE_CACHE_SIZE	2053

/* number of hash table lines inspected in a single garbage loop
 */
#define HMCDRV_FUSE_GARBAGE_MAX 128

/* max. size of FTP 'dir <path>' output chunk size
 */
#define HMCDRV_FUSE_DIRBUF_SIZE (64 * 1024)

/* max. string length in FTP 'dir <path>' output buffer
 */
#define HMCDRV_FUSE_DIRBUF_LEN	(HMCDRV_FUSE_DIRBUF_SIZE - 1)


/* pointer to path (token) in FTP command string associated with file 'fp'
 */
#define HMCDRV_FUSE_PATH(fp)	((fp)->ftpcmd + HMCDRV_FUSE_OFSPATH)



/*
 * DEBUG log macro (requires -DDEBUG, else empty)
 */
#ifdef DEBUG
#define HMCDRV_FUSE_DBGLOG(fmt, ...) syslog(LOG_ERR, (fmt), ##__VA_ARGS__)
#else
#define HMCDRV_FUSE_DBGLOG(fmt, ...) do {} while (0)
#endif /* DEBUG */


/*
 * log output to 'stderr' and optionally (if from a fork'ed process/thread)
 * to syslog
 */
#define HMCDRV_FUSE_LOG(sev, fmt, ...)				\
	hmcdrv_fuse_log((sev), HMCDRV_FUSE_LOGHEAD "%s: " fmt "\n",	\
			hmcdrv_fuse_logsevname[sev], ##__VA_ARGS__)


#define HMCDRV_FUSE_STR(x)	#x
#define HMCDRV_FUSE_STRINGER(x)	HMCDRV_FUSE_STR(x)
#define HMCDRV_FUSE_RELEASE	HMCDRV_FUSE_STRINGER(S390_TOOLS_RELEASE)

#define HMCDRV_FUSE_OPT(t, p, v) { t, offsetof(struct hmcdrv_fuse_opt, p), v }


/*
 * option key IDs (used on command line parsing)
 */
enum {
	HMCDRV_FUSE_OPTKEY_VERSION, /* "-v", "--version" */
	HMCDRV_FUSE_OPTKEY_HELP, /* "-h", "--help" */
	HMCDRV_FUSE_OPTKEY_RO, /* read-only, e.g. "-o ro" or "-r" */
	HMCDRV_FUSE_OPTKEY_RW, /* option associated with -wr (ignored) */
	HMCDRV_FUSE_OPTKEY_NATM, /* option "noatime" */
	HMCDRV_FUSE_OPTKEY_CTMO, /* cache timeout (entry/attr_timeout) */
	HMCDRV_FUSE_OPTKEY_NSUP, /* unsupported options, e.g. "subdir" */
};


/*
 * internal used FTP command IDs
 */
enum hmcdrv_fuse_cmdid {
	HMCDRV_FUSE_CMDID_NLS = 0,
	HMCDRV_FUSE_CMDID_DIR = 1,
	HMCDRV_FUSE_CMDID_GET = 2,
	HMCDRV_FUSE_CMDID_SIZE
};


/*
 * options/parameters to lookup at start
 */
struct hmcdrv_fuse_opt {
	unsigned int ro:1; /* option(s) "ro", "-r", etc. */
	unsigned int noatime:1; /* option "noatime" */

	char *hmctz; /* HMC timezone option "-o hmctz=TZ" */
	char *hmclang; /* HMC locale option "-o hmclang=LANG" */
};


/*
 * HMC drive FUSE context
 */
struct hmcdrv_fuse_context {
	struct hmcdrv_fuse_opt opt; /* some saved startup options */
	int fd; /* file descriptor of FTP device HMCDRV_FUSE_FTPDEV */
	char *path; /* mount point path (root) */
	struct stat st; /* mount point stat attributes */
	time_t ctmo; /* cache timeout (derived from entry/attr_timeout) */
	pthread_t tid; /* cache aging thread ID */
	pthread_mutex_t mutex; /* cache access mutex */
	pid_t pid; /* PID of main() */
	char *abmon[12]; /* abbreviated month name of HMC locale */
	int ablen[12]; /* length of each abbreviated month name */
};


/*
 * SE/HMC drive FUSE file private data
 */
struct hmcdrv_fuse_file {
	struct hmcdrv_fuse_file *next; /* collision list (equal hash) */
	struct stat st; /* stat structure of this file */
	char *symlnk; /* pointer to path name of symlink target (S_IFLNK) */
	time_t timeout; /* cache timeout for this file */
	size_t cmdlen; /* length of FTP command + path */
	char ftpcmd[0]; /* FTP command + path (max HMCDRV_FUSE_MAXCMDLEN) */
};


/*
 * all file attributes accumulated from interpreting tokens/fields of 'dir'
 * command listing
 */
struct hmcdrv_fuse_attr {
	struct stat *st; /* pointer to stat structure */
	struct tm ftm; /* local time */
	char *fname; /* (base-) name */
};


/*
 * prototype (forward) declaration for hmcdrv_cache_dir()
 */
static struct hmcdrv_fuse_file *hmcdrv_file_get(const char *path);


/*
 * log severity strings
 */
static const char *const hmcdrv_fuse_logsevname[] = {

	"Emergency", /* (0) LOG_EMERG */
	"Alert", /* (1) LOG_ALERT */
	"Critical", /* (2) LOG_CRIT */
	"Error", /* (3) LOG_ERR */
	"Warning", /* (4) LOG_WARNING */
	"Notice", /* (5) LOG_NOTICE */
	"Info", /* (6) LOG_INFO */
	"Debug" /* (7) DEBUG */
};


/*
 * file attributes cache
 */
static struct hmcdrv_fuse_file *hmcdrv_fuse_cache[HMCDRV_FUSE_CACHE_SIZE];


/*
 * context
 */
static struct hmcdrv_fuse_context hmcdrv_ctx = {
	.fd = -1,
	.ctmo = 1 + HMCDRV_FUSE_CACHE_TMOFS,
	.mutex = PTHREAD_MUTEX_INITIALIZER,
};


/*
 * log output from hmcdrvfs daemon
 */
static void hmcdrv_fuse_log(int sev, const char *fmt, ...)
{
	va_list argp;

	va_start(argp, fmt);
	vfprintf(stderr, fmt, argp); /* always to stderr */
	va_end(argp);

	if (hmcdrv_ctx.pid != getpid()) {
		struct fuse_context *fctx = fuse_get_context();

		if ((fctx != NULL) && (hmcdrv_ctx.pid != fctx->pid)) {
			va_start(argp, fmt);
			vsyslog(sev,
				fmt + (sizeof(HMCDRV_FUSE_LOGHEAD) - 1),
				argp);
			va_end(argp);
		}
	}
}


/*
 * calculate a hash value from a file path
 */
static unsigned int hmcdrv_hash_path(const char *path)
{
	unsigned int hash = ~0;
	int len = strlen(path);

	while (len > 0) {
		hash += (unsigned) *path;

		++path;
		--len;
	}

	hash %= HMCDRV_FUSE_CACHE_SIZE;
	return hash;
}


/*
 * restart of cache entry aging time
 */
static void hmcdrv_cache_trestart(struct hmcdrv_fuse_file *fp)
{
	fp->timeout = time(NULL) + hmcdrv_ctx.ctmo;
}


/*
 * update or free symlink information
 */
static void hmcdrv_cache_symlink(struct hmcdrv_fuse_file *fp,
				const char *symlink)
{
	if (fp->symlnk != NULL) {
		free(fp->symlnk);
		fp->symlnk = NULL;
	}

	if ((symlink != NULL) && (symlink[0] != '\0')) {
		fp->symlnk = malloc(HMCDRV_FUSE_MAXPATH);

		if (fp->symlnk != NULL) {
			util_strlcpy(fp->symlnk, symlink, HMCDRV_FUSE_MAXPATH);
			fp->symlnk[HMCDRV_FUSE_MAXPATH - 1] = '\0';
		}
	}
}



/*
 * refresh the attributes of file/directory 'path'
 * note: if there is no cache entry for this file, then create one
 */
static void hmcdrv_cache_refresh(const char *path, const struct stat *st,
				const char *symlink)
{
	struct hmcdrv_fuse_file **pbase; /* storage location of fp */
	struct hmcdrv_fuse_file *fp;
	unsigned int index;

	int pathlen = strlen(path);

	if ((pathlen == 0) || (pathlen >= HMCDRV_FUSE_MAXPATH)) {
		HMCDRV_FUSE_LOG(LOG_WARNING,
				"Invalid path length for '%s'", path);
		return;
	}

	index = hmcdrv_hash_path(path);
	pbase = &hmcdrv_fuse_cache[index];
	fp = *pbase;

	while (fp != NULL) {
		if (strcmp(HMCDRV_FUSE_PATH(fp), path) == 0) {
			fp->st = *st;		   /* update file info */
			hmcdrv_cache_symlink(fp, symlink);
			hmcdrv_cache_trestart(fp); /* restart aging */
			return;
		}

		pbase = &fp->next;
		fp = fp->next;
	}

	/* entry does not exist so far - create new one
	 */
	fp = malloc((offsetof(struct hmcdrv_fuse_file, ftpcmd) +
		     HMCDRV_FUSE_OFSPATH + 1) + pathlen);

	if (fp == NULL) {
		HMCDRV_FUSE_LOG(LOG_ERR, "Out of memory, %ld bytes",
				(offsetof(struct hmcdrv_fuse_file, ftpcmd) +
				 HMCDRV_FUSE_OFSPATH + 1) + pathlen);
	} else {
		*pbase = fp;
		fp->cmdlen = pathlen + HMCDRV_FUSE_OFSPATH;
		fp->st = *st;
		memcpy(HMCDRV_FUSE_PATH(fp), path, pathlen + 1);
		fp->symlnk = NULL;
		hmcdrv_cache_symlink(fp, symlink);
		hmcdrv_cache_trestart(fp);
		fp->next = NULL;
	}
}


/*
 * search for a file/directory path in cache
 */
static struct hmcdrv_fuse_file *hmcdrv_cache_find(const char *path)
{
	unsigned index = hmcdrv_hash_path(path);
	struct hmcdrv_fuse_file *fp = hmcdrv_fuse_cache[index];

	while (fp != NULL) {
		if (strcmp(HMCDRV_FUSE_PATH(fp), path) == 0) {
			hmcdrv_cache_trestart(fp);
			return fp;
		}

		fp = fp->next;
	}

	return NULL;
}


/*
 * check for aging timeout of cache entry at index
 */
static void hmcdrv_cache_expire(unsigned index, time_t now)
{
	struct hmcdrv_fuse_file **pbase; /* storage location of fp */
	struct hmcdrv_fuse_file *fp, *next;

	pbase = &hmcdrv_fuse_cache[index];
	fp = *pbase;

	/* iterate the collision list */
	while (fp != NULL) {
		next = fp->next;

		if ((fp->timeout <= now) &&
		    (strcmp(HMCDRV_FUSE_PATH(fp), "/") != 0)) {
			hmcdrv_cache_symlink(fp, NULL);
			*pbase = next;
			free(fp);
		} else {
			pbase = &fp->next;
		}

		fp = next;
	}
}


/*
 * cache expiry handler thread
 */
static void *hmcdrv_cache_aging(void *UNUSED(arg))
{
	unsigned int cnt;
	time_t now;

	unsigned index = 0;

	while (1) {
		sleep(1);

		pthread_mutex_lock(&hmcdrv_ctx.mutex);
		now = time(NULL);

		/* each second scan a number of cache entries
		 */
		for (cnt = 0; cnt < HMCDRV_FUSE_GARBAGE_MAX; ++cnt) {

			hmcdrv_cache_expire(index, now);

			if (++index == HMCDRV_FUSE_CACHE_SIZE)
				index = 0;
		}

		pthread_mutex_unlock(&hmcdrv_ctx.mutex);
	}

	return NULL;
}


/*
 * convert a FTP command ID into a string
 *
 * Note: If a command string is shorter than HMCDRV_FUSE_MAXFTPLEN then
 *       define it right-aligned (prefixed with spaces) in table 'cmdstr'.
 */
static void hmcdrv_ftp_str(enum hmcdrv_fuse_cmdid cmd, char *ftp)
{
	static const char *cmdstr[HMCDRV_FUSE_CMDID_SIZE] = {

#if (HMCDRV_FUSE_MAXFTPLEN != 3)
#error The length of strings in cmdstr[] must match HMCDRV_FUSE_MAXFTPLEN
#endif
		"nls", /* HMCDRV_FUSE_CMDID_NLS */
		"dir", /* HMCDRV_FUSE_CMDID_DIR */
		"get"  /* HMCDRV_FUSE_CMDID_GET */
	};

	strcpy(ftp, cmdstr[cmd]);
	ftp[HMCDRV_FUSE_MAXFTPLEN] = ' '; /* overwrite '\0' from strcpy() */
}


/*
 * FTP command execution via kernel device
 */
static ssize_t hmcdrv_ftp_transfer(struct hmcdrv_fuse_file *fp, char *buf,
				   size_t len, off_t offset)
{
	static off_t current_offset;
	static char last_ftpcmd[HMCDRV_FUSE_MAXCMDLEN];

	ssize_t retlen;

	/*
	 * First check if this is a sequential read from the same file.	 If
	 * so skip repositioning the files seek pointer and emitting a new
	 * command.
	 */
	if ((offset != current_offset) ||
	    (strncmp(fp->ftpcmd, last_ftpcmd, HMCDRV_FUSE_MAXCMDLEN) != 0)) {

		if ((lseek(hmcdrv_ctx.fd, offset, SEEK_END) < 0) ||
		    (write(hmcdrv_ctx.fd, fp->ftpcmd, fp->cmdlen) < 0)) {
			last_ftpcmd[0] = '\0';
			return -errno;
		}

		current_offset = offset;
	}

	retlen = read(hmcdrv_ctx.fd, buf, len);

	if (retlen < 0) {
		last_ftpcmd[0] = '\0';
		return -errno;
	}

	current_offset += retlen;
	util_strlcpy(last_ftpcmd, fp->ftpcmd, HMCDRV_FUSE_MAXCMDLEN);
	return retlen;
}


/*
 * FTP command assembly and execution
 */
static ssize_t hmcdrv_ftp_cmd(struct hmcdrv_fuse_file *fp,
			      enum hmcdrv_fuse_cmdid cmd,
			      char *buf, size_t len, off_t offset)
{
	if (fp == NULL)
		return -EBADF;

	hmcdrv_ftp_str(cmd, fp->ftpcmd);
	return hmcdrv_ftp_transfer(fp, buf, len, offset);
}



/*
 * returns a file path (from internal file structure) to a buffer,
 * with appending a slash (in case it is missing)
 */
static int hmcdrv_path_copy(struct hmcdrv_fuse_file *fp, char *dest)
{
	char *src = HMCDRV_FUSE_PATH(fp);
	int len = 0;

	while ((len < (HMCDRV_FUSE_MAXPATH - 1)) &&
	       (*src != '\0')) {

		*dest = *src;

		++len;
		++dest;
		++src;
	}

	if ((len > 0) && (*(dest - 1) != '/')) {
		*dest++ = '/';
		++len;
	}

	*dest = '\0';
	return len;
}


/*
 * convert a string into into a unsigned number,
 * returning 0 on success or -errno on error
 */
static int hmcdrv_parse_uint(const char *s, unsigned int *pval)
{
	errno = 0;
	*pval = strtoul(s, NULL, 10);
	return -errno;
}


/*
 * convert a string into into a signed number (with range check),
 * returning 0 on success or -errno on error
 */
static int hmcdrv_parse_int(const char *s, int vmin, int vmax, int *pval)
{
	errno = 0;
	*pval = strtol(s, NULL, 10);

	if (errno == 0) {
		if ((*pval >= vmin) && (*pval <= vmax))
			return 0;

		errno = ERANGE;
	}

	return -errno;
}


/*
 * convert an abbreviated month name (%b) into a number
 */
static int hmcdrv_parse_month(const char *s)
{
	int i;

	for (i = 0; i < 12; ++i) {
		if (strncasecmp(hmcdrv_ctx.abmon[i], s,
				hmcdrv_ctx.ablen[i]) == 0)
			return i;
	}

	return -1;
}


/*
 * return the file mode from a 'ls -l' like mode string
 */
static mode_t hmcdrv_parse_mode(const char *s)
{
	int i;
	mode_t type;
	mode_t mode = 0;

	switch (*s) {
	case 'd': /* directory */
		type = S_IFDIR;
		break;

	case 'b': /* block device */
		type = S_IFBLK;
		break;

	case 'c': /* character device */
		type = S_IFCHR;
		break;

	case 'l': /* symbolic link */
		type = S_IFLNK;
		break;

	case 'p': /* FIFO */
		type = S_IFIFO;
		break;

	case 's': /* socket */
		type = S_IFSOCK;
		break;

	case '-':
		type = S_IFREG;
		break;

	default:
		return 0;
	}

	for (i = 0; i < 9; ++i) {
		mode <<= 1;

		switch (*++s) {
		case 'r':
		case 'w':
		case 'x':
			mode |= 1;
			break;

		case '-':
			break;

		default:
			return 0;
		}

	}

	mode &= ~(S_IWUSR | S_IWGRP | S_IWOTH);
	return mode | type;
}


/*
 * convert a time string from format "%H:%M" into a 'struct tm' (without any
 * local time conversion)
 */
static int hmcdrv_parse_time(const char *s, struct tm *t)
{
	char *endptr;
	int hour, minute;

	errno = 0;
	hour = strtol(s, &endptr, 10);

	if ((errno == 0) && (endptr != s) &&
	    (hour >= 0) && (hour < 24)) {
		if (*endptr == ':') {
			s = ++endptr;
			errno = 0;
			minute = strtol(s, &endptr, 10);

			if ((errno == 0) && (endptr != s) &&
			    (minute >= 0) && (minute < 60)) {

				t->tm_hour = hour;
				t->tm_min = minute;

				return 0; /* success */
			}
		}
	}

	return -1;
}


/*
 * interpret token depending on current field number and
 * fill the attributes structure
 *
 * Return: 'field' incremented by 1 or INT_MAX on parse errors
 */
static int hmcdrv_parse_ntoken(int field, char *token,
			       struct hmcdrv_fuse_attr *attr)
{
	switch (field) {
	case 0: /* mode */
		attr->st->st_mode = hmcdrv_parse_mode(token);

		if (attr->st->st_mode == 0) /* error ? */
			field = INT_MAX - 1; /* stop */
		break;

	case 1: /* number of hard links to this file */
		errno = 0;
		attr->st->st_nlink = strtoul(token, NULL, 10);

		if (errno != 0)
			field = INT_MAX - 1;
		break;

	case 2: /* user ID */
		if (hmcdrv_parse_uint(token, &attr->st->st_uid) != 0) {
			attr->st->st_uid = hmcdrv_ctx.st.st_uid;
			HMCDRV_FUSE_LOG(LOG_WARNING,
					"Could not parse UID (will use %u)",
					attr->st->st_uid);
		}

		break;

	case 3: /* group ID */
		if (hmcdrv_parse_uint(token, &attr->st->st_gid) != 0) {
			attr->st->st_gid = hmcdrv_ctx.st.st_gid;
			HMCDRV_FUSE_LOG(LOG_WARNING,
					"Could not parse GID (will use %u)",
					attr->st->st_gid);
		}

		break;

	case 4: /* file size */
		errno = 0;
		attr->st->st_size = strtol(token, NULL, 10);

		if (errno != 0)
			field = INT_MAX - 1;
		break;

	case 5: /* month of year [0,11] */
		attr->ftm.tm_mon = hmcdrv_parse_month(token);
		break;

	case 6: /* day of month [1,31] */
		if (hmcdrv_parse_int(token, 1, 31, &attr->ftm.tm_mday) != 0) {
			field = INT_MAX - 1;
		} else {
			/*
			 * in case month is not in English language we've
			 * got an error in attr->ftm.tm_mon
			 */
			if (attr->ftm.tm_mon < 0) {
				attr->ftm.tm_mday = 1; /* YYYY-01-01 */
				attr->ftm.tm_mon = 0;
			}
		}

		break;

	case 7: /* %H:%M or %Y */
		if (hmcdrv_parse_time(token, &attr->ftm) != 0) {
			if (hmcdrv_parse_int(token, 1900, INT_MAX,
					     &attr->ftm.tm_year) != 0)
				field = INT_MAX - 1;
			else
				attr->ftm.tm_year -= 1900;
		}

		break;

	case 8: /* file name (rest of line) */
		attr->fname = token;
		break;


	default:
		break;
	}

	return ++field;
}


/*
 * parse filename and attributes from 'dir' output (a single line)
 *
 * Note: Because HMC command 'dir' always uses the HMC server local time the
 *       current timezone should be set correctly, based on startup option
 *       "-o hmctz=TZ".
 *
 * Return: pointer to start of next line (skipping any whitespace)
 *         or pointer to '\0'
 */
static char *hmcdrv_parse_line(char *line, char *namebuf,
			       int bufsize, struct stat *st,
			       char *symlink)
{
	int field = 0;
	struct hmcdrv_fuse_attr attr = {.fname = NULL, .st = st};

	memset(st, 0, sizeof(*st));
	namebuf[0] = '\0';

	/* set default values
	 */
	st->st_mtime = time(NULL);  /* see next line: gmtime_r() */
	gmtime_r(&st->st_mtime, &attr.ftm);   /* tm_year default */
	attr.ftm.tm_isdst = -1; /* mktime() should use "TZ" info */
	attr.ftm.tm_hour = attr.ftm.tm_min = attr.ftm.tm_sec = 0;

	while ((*line != '\0') && isspace(*line))
		++line; /* skip leading whitespace (incl. newlines) */


	while ((*line != '\0') && (*line != '\n') && (field < 9)) {

		while ((*line != '\0') &&
		       (*line != '\n') &&
		       isspace(*line))
			++line; /* skip spaces, but not newlines */

		if ((*line != '\0') && (*line != '\n')) {
			field = hmcdrv_parse_ntoken(field, line, &attr);

			while ((*line != '\0') && (*line != '\n')) {
				if (isspace(*line))
					break;
				if (field == 1 && isdigit(*line))
					break;
				++line; /* search end of field */
			}
		}
	} /* while */

	/* search end of line now (skips some fields in case of error)
	 */
	while ((*line != '\0') && (*line != '\n'))
		++line;

	/*
	 * If start of file name was found by parser, then indicate success
	 * returning the file attributes.  But do this only if the end of
	 * line was really found, else this seems to be an incomplete line
	 * in current chunk (fragment).
	 */
	if ((attr.fname != NULL) && (*line == '\n')) {
		char *arrow = NULL; /* " -> " pointer, when symlink */
		*line = '\0'; /* temporary set EOS for strncpy() */

		if (S_ISLNK(st->st_mode)) {
			arrow = strstr(attr.fname, " -> ");

			if (arrow != NULL)
				*arrow = '\0';
		}

		util_strlcpy(namebuf, attr.fname, bufsize);

		if (arrow == NULL) {
			symlink[0] = '\0';
		} else {
			util_strlcpy(symlink, arrow + 4, HMCDRV_FUSE_MAXPATH);
			*arrow = ' '; /* restore */
		}

		*line = '\n'; /* restore */
		st->st_mtime = mktime(&attr.ftm); /* UTC from local time */

		if (st->st_mtime == (time_t) -1)
			st->st_mtime = 0;

		st->st_atime = st->st_ctime = st->st_mtime;

		/* In contrast to FUSE documentation the members below must
		 * be set in stat structure.  For example 'du' uses them,
		 * because it shows the disk usage from used blocks and not
		 * the file size (unless option '--apparent-size' is given).
		 */
		st->st_blksize = 2048U; /* nearly all CD/DVDs have this */
		st->st_blocks = (st->st_size + 511U) / 512U;
	}


	while ((*line != '\0') && isspace(*line))
		++line; /* skip whitespace after EOL, incl. newlines */

	return line;
}


/*
 * performs a FTP 'dir', then scans/parses the output, fills the cache and
 * (optional) calls the FUSE filler function for every file in 'dir' listing
 */
static int hmcdrv_cache_dir(const char *dir, fuse_fill_dir_t filler, void *buf)
{
	char path[HMCDRV_FUSE_MAXPATH]; /* constructed path of a file */
	char symlink[HMCDRV_FUSE_MAXPATH]; /* target of symlink (S_IFLNK) */
	struct hmcdrv_fuse_file *fpdir; /* directory file pointer */
	struct stat st; /* attributes of file */
	char *dirbuf; /* 'dir' listing (chunk) buffer */
	char *next; /* 'next line' pointer in 'dir' buffer */
	char *line; /* current line start position in 'dir' buffer */
	int dirlen; /* number of characters in 'dir' buffer */
	int fraglen; /* fragment length in 'dir' buffer */
	char *fname; /* buffer pointer for basename of file */
	int len; /* maximum length of file basename (buffer space) */
	off_t offset; /* device 'dir' position */

	char *tzenv = NULL; /* "TZ" environment variable */

	fpdir = hmcdrv_file_get(dir);

	if (fpdir == NULL) {
		HMCDRV_FUSE_LOG(LOG_ERR,
				"Could not find directory '%s' in cache",
				dir);
		return -ENOENT;
	}

	dirbuf = malloc(HMCDRV_FUSE_DIRBUF_SIZE);

	if (dirbuf == NULL) {
		HMCDRV_FUSE_LOG(LOG_ERR,
				"Out of memory, %d bytes",
				HMCDRV_FUSE_DIRBUF_SIZE);
		return -ENOMEM;
	}

	/* set HMC timezone (optional), else host timezone remains active -
	 * possibly different from HMC timezone and so resulting in wrong
	 * file modification times (cf. mktime() in hmcdrv_parse_line())
	 */
	if (hmcdrv_ctx.opt.hmctz != NULL) { /* "-o hmctz=TZ" present ? */
		tzenv = getenv("TZ");

		if (tzenv != NULL)
			tzenv = strdup(tzenv);

		if (setenv("TZ", hmcdrv_ctx.opt.hmctz, 1) != 0) {
			HMCDRV_FUSE_LOG(LOG_ERR,
					"Could not set HMC timezone '%s': %s",
					hmcdrv_ctx.opt.hmctz,
					strerror(errno));

			if (tzenv != NULL) {
				setenv("TZ", tzenv, 1);
				free(tzenv);
			}

			free(dirbuf);
			return -ENOMEM;
		}

		tzset();
	}

	len = hmcdrv_path_copy(fpdir, path); /* get directory path */
	fname = path + len; /* start of basename in path buffer */
	len = HMCDRV_FUSE_MAXPATH - len; /* space for basename */
	HMCDRV_FUSE_DBGLOG("scanning '%s'...", path);

	/* do the 'dir' chunks and parse all
	 */
	dirlen = hmcdrv_ftp_cmd(fpdir, HMCDRV_FUSE_CMDID_DIR,
				dirbuf, HMCDRV_FUSE_DIRBUF_LEN, 0);
	next = dirbuf; /* first line at start of buffer */
	offset = dirlen;

	while (dirlen > 0) {

		next[dirlen] = '\0'; /* force end of string */

		/* parse all files in this 'dir' listing chunk
		 */
		do {
			line = next;

			/* parse next line and retrieve the file name
			 */
			next = hmcdrv_parse_line(line, fname, len,
						 &st, symlink);

			/* If parsing of 'dir' line was successful, then
			 * store file attributes and name in cache. Else
			 * skip this line because of incorrect syntax -
			 * possibly it is a fragment (at end of buffer) or
			 * this line does not hold any valid file info.
			 */
			if (*fname != '\0') {
				hmcdrv_cache_refresh(path, &st, symlink);

				if ((filler != NULL) &&
				    (filler(buf, fname, &st, 0) != 0))
					filler = NULL; /* stop filling */
#ifdef DEBUG
				strftime(symlink, sizeof(symlink),
					 "%c", gmtime(&st.st_mtime));
				HMCDRV_FUSE_DBGLOG(" * %s (size=%zu, mode=%o, uid=%u, gid=%u, time=%s)",
						   fname,
						   st.st_size, st.st_mode,
						   st.st_uid, st.st_gid,
						   symlink);
#endif
			}

		} while (*next != '\0');

		/* Try to read more bytes from the FTP device. But regard
		 * remaining chars from the last line, in case it could not
		 * completely parsed (in other words the line was a
		 * fragment, and has to be moved to start of buffer).
		 */
		if (*fname == '\0') { /* no valid filename? */
			fraglen = next - line;

			if (fraglen > 0)
				memcpy(dirbuf, line, fraglen);
		} else {
			fraglen = 0;
		}

		dirlen = hmcdrv_ftp_cmd(fpdir, HMCDRV_FUSE_CMDID_DIR,
					dirbuf + fraglen,
					HMCDRV_FUSE_DIRBUF_LEN - fraglen,
					offset);

		if (dirlen > 0) { /* on success continue */
			next = dirbuf;
			offset += dirlen;
			dirlen += fraglen;
		}
	}


	free(dirbuf);

	/* restore old host timezone
	 */
	if (hmcdrv_ctx.opt.hmctz != NULL) {
		if (tzenv != NULL) {
			putenv(tzenv);
			free(tzenv);
		} else {
			unsetenv("TZ");
		}

		tzset();
	}

	return dirlen;
}


/*
 * cache all files located in parent directory of 'dir',
 * so also caching the attributes of 'dir' again
 *
 * Return: 0 on success, else a negative error code (-ENOENT if 'dir' is "/")
 */
static int hmcdrv_cache_parent(const char *dir)
{
	char *tmp, *parent;

	if (strcmp(dir, "/") == 0) /* root has no parent */
		return -ENOENT;

	tmp = strdup(dir); /* need a copy because of dirname() */

	if (tmp == NULL) {
		HMCDRV_FUSE_LOG(LOG_ERR, "Out of memory, %zd bytes",
				strlen(dir) + 1);
		return -ENOMEM;
	}

	parent = dirname(tmp);
	HMCDRV_FUSE_DBGLOG("re-scanning parent '%s'...", parent);
	hmcdrv_cache_dir(parent, NULL, NULL);
	free(tmp);
	return 0;
}


/*
 * return the file attributes data pointer (or NULL on error)
 */
static struct hmcdrv_fuse_file *hmcdrv_file_get(const char *path)
{
	struct hmcdrv_fuse_file *fp = hmcdrv_cache_find(path);

	if (fp != NULL) /* path found in cache ? */
		return fp;

	/* in very, very rare cases we must scan the parent again,
	 * recursive up to the root directory
	 */
	if (hmcdrv_cache_parent(path) != 0)
		return NULL;

	/* because the mutex is taken by the caller, we MUST find this file
	 * now in cache (if not so then the HMC drive DVD may have changed
	 * without unmounting FUSE.HMCDRVFS)
	 */
	return hmcdrv_cache_find(path);
}


/*
 * obtain file status information (attributes) on FUSE.HMCDRVFS filesystem
 *
 * Note: The most important function which FUSE calls (very often).
 */
static int hmcdrv_fuse_getattr(const char *path, struct stat *stbuf)
{
	struct hmcdrv_fuse_file *fp;
	int rc = 0;

	pthread_mutex_lock(&hmcdrv_ctx.mutex);
	fp = hmcdrv_file_get(path);

	if (fp == NULL)
		rc = -ENOENT;
	else
		*stbuf = fp->st;

	pthread_mutex_unlock(&hmcdrv_ctx.mutex);
	return rc;
}


/*
 * get symlink target path
 */
static int hmcdrv_fuse_readlink(const char *path, char *buf, size_t size)
{
	struct hmcdrv_fuse_file *fp;
	int rc = 0;

	pthread_mutex_lock(&hmcdrv_ctx.mutex);
	fp = hmcdrv_file_get(path);

	if (fp == NULL) {
		rc = -ENOENT;
	} else {
		if (fp->symlnk == NULL) {
			rc = -ENOMEM;
		} else {
			if (!S_ISLNK(fp->st.st_mode)) {
				rc = -EINVAL;
			} else {
				util_strlcpy(buf, fp->symlnk, size);
			}
		}
	}

	pthread_mutex_unlock(&hmcdrv_ctx.mutex);
	return rc;
}


/*
 * open a directory on FUSE.HMCDRVFS filesystem
 */
static int hmcdrv_fuse_opendir(const char *UNUSED(path),
			       struct fuse_file_info *fi)
{
	if ((fi->flags & O_ACCMODE) != O_RDONLY)
		return -EACCES; /* all files are read-only */

	return 0;
}


/*
 * read a directory on FUSE.HMCDRVFS filesystem
 */
static int hmcdrv_fuse_readdir(const char *path, void *buf,
			       fuse_fill_dir_t filler, off_t UNUSED(offset),
			       struct fuse_file_info *UNUSED(fi))
{
	int ret;

	filler(buf, ".", NULL, 0);
	filler(buf, "..", NULL, 0);

	pthread_mutex_lock(&hmcdrv_ctx.mutex);
	ret = hmcdrv_cache_dir(path, filler, buf);
	pthread_mutex_unlock(&hmcdrv_ctx.mutex);
	return ret;
}


/*
 * open a file on FUSE.HMCDRVFS filesystem
 */
static int hmcdrv_fuse_open(const char *UNUSED(path), struct fuse_file_info *fi)
{
	if ((fi->flags & O_ACCMODE) != O_RDONLY)
		return -EACCES; /* all files are read-only */

	return 0;
}


/*
 * read a file on FUSE.HMCDRVFS filesystem
 */
static int hmcdrv_fuse_read(const char *path, char *buf, size_t size,
			    off_t offset, struct fuse_file_info *UNUSED(fi))
{
	struct hmcdrv_fuse_file *fp;
	int rc;

	pthread_mutex_lock(&hmcdrv_ctx.mutex);
	fp = hmcdrv_file_get(path);

	if (fp == NULL)
		rc = -ENOENT;
	else
		rc = hmcdrv_ftp_cmd(fp, HMCDRV_FUSE_CMDID_GET,
				    buf, size, offset);

	pthread_mutex_unlock(&hmcdrv_ctx.mutex);
	return rc;
}


/*
 * initialize FUSE.HMCDRVFS filesystem
 *
 * Note: calls fuse_exit() on errors
 *
 * Return: value to be passed in the private_data field of fuse_context to
 *         all file operations and as a parameter to the destroy() method
 */
static void *hmcdrv_fuse_init(struct fuse_conn_info *UNUSED(conn))
{
	pthread_mutexattr_t attr;

	memset(hmcdrv_fuse_cache, 0, sizeof(hmcdrv_fuse_cache));
	openlog(HMCDRV_FUSE_LOGNAME, LOG_PID, LOG_DAEMON);

	if (pthread_mutexattr_init(&attr) != 0)
		goto err_out;

	pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
	hmcdrv_ctx.fd = open(HMCDRV_FUSE_FTPDEV, O_RDWR);

	if (hmcdrv_ctx.fd < 0)
		goto err_dev;

	if (pthread_mutex_init(&hmcdrv_ctx.mutex, &attr) != 0)
		goto err_mutex;

	hmcdrv_cache_refresh("/", &hmcdrv_ctx.st, NULL); /* never expires */

	if (pthread_create(&hmcdrv_ctx.tid, NULL,
			   hmcdrv_cache_aging, NULL) == 0) {

		pthread_mutexattr_destroy(&attr);
		return &hmcdrv_ctx.tid;
	}

err_mutex:
	close(hmcdrv_ctx.fd);
err_dev:
	pthread_mutexattr_destroy(&attr);
err_out:
	HMCDRV_FUSE_LOG(LOG_ERR, "Initialization failed: %s",
			strerror(errno));
	closelog();
	fuse_exit(fuse_get_context()->fuse);
	return NULL;
}


/*
 * clean up FUSE.HMCDRVFS filesystem
 */
static void hmcdrv_fuse_exit(void *arg)
{
	struct hmcdrv_fuse_file *fp, *next;
	int i;

	if (arg != NULL)
		pthread_cancel(*(pthread_t *) arg);

	pthread_mutex_lock(&hmcdrv_ctx.mutex);

	for (i = 0; i < HMCDRV_FUSE_CACHE_SIZE; ++i) {
		fp = hmcdrv_fuse_cache[i];

		while (fp != NULL) {
			next = fp->next;
			hmcdrv_cache_symlink(fp, NULL);
			free(fp);
			fp = next;
		}
	}

	memset(hmcdrv_fuse_cache, 0, sizeof(hmcdrv_fuse_cache));
	pthread_mutex_destroy(&hmcdrv_ctx.mutex);
	closelog();

	if (hmcdrv_ctx.fd >= 0)
		close(hmcdrv_ctx.fd);
}


/*
 * FUSE.HMCDRVFS internal main function
 */
static int hmcdrv_fuse_main(struct fuse_args *args)
{
	static struct fuse_operations hmcdrv_fuse_op = {
		.init = hmcdrv_fuse_init,
		.destroy = hmcdrv_fuse_exit,
		.getattr = hmcdrv_fuse_getattr,
		.opendir = hmcdrv_fuse_opendir,
		.readdir = hmcdrv_fuse_readdir,
		.open = hmcdrv_fuse_open,
		.read = hmcdrv_fuse_read,
		.readlink = hmcdrv_fuse_readlink
	};


#if FUSE_VERSION >= 26
	return fuse_main(args->argc, args->argv, &hmcdrv_fuse_op, NULL);
#else
	return fuse_main(args->argc, args->argv, &hmcdrv_fuse_op);
#endif
}


/*
 * usage output
 */
static void hmcdrv_fuse_usage(const char *progname)
{
	fprintf(stdout,
		"Usage: %s MOUNTPOINT [OPTIONS]\n\n"
		"Use the %s command to read files from a HMC drive DVD.\n"
		"\n"
		"General options:\n"
		"    -o opt,[opt...]        Mount options\n"
		"    -h   --help            Print help, then exit\n"
		"    -v   --version         Print version, then exit\n"
		"\n"
		"Specific options:\n"
		"    -o hmclang=LANG        HMC speaks language LANG (see locale(1))\n"
		"    -o hmctz=TZ            HMC is in timezone TZ (see tzset(3))\n"
		"\n"
		"Attention:\n"
		"    The following general and FUSE specific mount options will\n"
		"    be ignored (most do not make sense on a read-only media):\n"
		"    --rw, -w, -o rw, -o atime, -o max_write=N, -o big_writes,\n"
		"    -o atomic_o_trunc, -o hard_remove, -o negative_timeout=T,\n"
		"    -o use_ino, -o readdir_ino, -o subdir=DIR\n"
		"\n",
		progname, progname);
}


/*
 * process the "-o hmclang=LANG" command line option (if there is one) and
 * prepare abbreviated month names (%b) in context structure
 *
 * Return: 0 on success, -1 on error
 */
static int hmcdrv_optproc_lang(void)
{
	static const nl_item nl_abmon[12] = {
		ABMON_1, ABMON_2, ABMON_3, ABMON_4, ABMON_5, ABMON_6,
		ABMON_7, ABMON_8, ABMON_9, ABMON_10, ABMON_11, ABMON_12};

	unsigned int i;
	char *lc = NULL; /* current LC_TIME locale */

	if (hmcdrv_ctx.opt.hmclang != NULL) {
		HMCDRV_FUSE_DBGLOG("option \"-o hmclang=%s\" detected",
				   hmcdrv_ctx.opt.hmclang);

		lc = setlocale(LC_TIME, NULL);

		if (setlocale(LC_TIME, hmcdrv_ctx.opt.hmclang) == NULL) {
			setlocale(LC_TIME, lc);
			return -1;
		}
	}

	for (i = 0; i < (sizeof(nl_abmon) / sizeof(nl_abmon[0])); ++i) {
		hmcdrv_ctx.abmon[i] = nl_langinfo(nl_abmon[i]);
		hmcdrv_ctx.ablen[i] = strlen(hmcdrv_ctx.abmon[i]);
	}

	if (lc)
		setlocale(LC_TIME, lc); /* restore LC_TIME locale */

	return 0;
}


/*
 * option parsing function
 */
static int hmcdrv_fuse_optproc(void *data, const char *arg,
			       int key, struct fuse_args *outargs)
{
	double tmo; /* timeout T in "-o entry/attr_timeout=T" */
	mode_t mask; /* umask() of caller */

	(void)outargs;

	switch (key) {
	case HMCDRV_FUSE_OPTKEY_NSUP:
		HMCDRV_FUSE_LOG(LOG_WARNING,
				"FUSE option \"%s\" ignored (unsupported)",
				arg);
		return 0; /* remove this option(s) */

	case HMCDRV_FUSE_OPTKEY_RW: /* write options (unsupported) */
		HMCDRV_FUSE_LOG(LOG_WARNING,
				"FUSE option \"%s\" ignored (r/o filesystem)",
				arg);
		return 0; /* remove this option(s) */

	case HMCDRV_FUSE_OPTKEY_RO: /* "-o ro" or "-r" */
		((struct hmcdrv_fuse_opt *)data)->ro = 1;
		return 1;

	case HMCDRV_FUSE_OPTKEY_NATM: /* "-o noatime" */
		((struct hmcdrv_fuse_opt *)data)->noatime = 1;
		return 1;

	case HMCDRV_FUSE_OPTKEY_CTMO: /* "-o entry/attr_timeout=T" */
		if (sscanf(arg, "%*[^=]=%lf", &tmo) == 1) {
			if (tmo < 1.0)
				tmo = 1.0;

			if (hmcdrv_ctx.ctmo <
			    (HMCDRV_FUSE_CACHE_TMOFS + (time_t)tmo))
				hmcdrv_ctx.ctmo =
					HMCDRV_FUSE_CACHE_TMOFS + (time_t)tmo;

			HMCDRV_FUSE_DBGLOG("option \"%s\" detected (cache timeout now is %" PRIdMAX " sec.)",
					   arg, (intmax_t) hmcdrv_ctx.ctmo);
		}

		return 1;

	case HMCDRV_FUSE_OPTKEY_VERSION:
		fprintf(stdout, HMCDRV_FUSE_LOGHEAD
			"HMC drive DVD file system, version %s\n"
			"Copyright IBM Corp. 2015, 2017\n",
			HMCDRV_FUSE_RELEASE);
		exit(EXIT_SUCCESS);

	case HMCDRV_FUSE_OPTKEY_HELP:
		hmcdrv_fuse_usage(outargs->argv[0]);

		/*
		 * Usage output needs to go to stdout to be consistent with
		 * coding guidelines. FUSE versions before 3.0.0 print help
		 * output to stderr. Redirect stderr to stdout here to enforce
		 * consistent behavior.
		 */
		fflush(stderr);
		dup2(STDOUT_FILENO, STDERR_FILENO);

		fuse_opt_add_arg(outargs, "-ho");
		hmcdrv_fuse_main(outargs);
		exit(EXIT_SUCCESS);

	case FUSE_OPT_KEY_NONOPT: /* normally the mount point */
		mask = umask(0);
		umask(mask);
		hmcdrv_ctx.st.st_uid = getgid();
		hmcdrv_ctx.st.st_gid = getuid();
		hmcdrv_ctx.st.st_ino = 0;
		hmcdrv_ctx.st.st_size = 0; /* unknown */
		hmcdrv_ctx.st.st_blksize = 2048U; /* DVD sector size */
		hmcdrv_ctx.st.st_nlink = 2; /* minimum */
		hmcdrv_ctx.st.st_mtime = time(NULL);
		hmcdrv_ctx.st.st_atime = hmcdrv_ctx.st.st_mtime;
		hmcdrv_ctx.st.st_ctime = hmcdrv_ctx.st.st_mtime;

		hmcdrv_ctx.st.st_mode =
			(S_IFDIR |
			 S_IXUSR | S_IRUSR |
			 S_IXGRP | S_IRGRP |
			 S_IXOTH | S_IROTH) &
			~(mask | (S_IWUSR | S_IWGRP | S_IWOTH));

		HMCDRV_FUSE_DBGLOG("mount point is %s (uid = %u, gid = %u)",
				   arg,
				   hmcdrv_ctx.st.st_uid,
				   hmcdrv_ctx.st.st_gid);
		return 1;


	default:
		HMCDRV_FUSE_DBGLOG("option \"%s\" passed to FUSE", arg);
		return 1; /* pass all other options to fuse_main() */
	}
}


/*
 * FUSE.HMCDRVFS entry function
 */
int main(int argc, char *argv[])
{
	static struct fuse_opt lookup_opt[] = {

		HMCDRV_FUSE_OPT("hmctz=%s", hmctz, 0),
		HMCDRV_FUSE_OPT("hmclang=%s", hmclang, 0),

		FUSE_OPT_KEY("ro", HMCDRV_FUSE_OPTKEY_RO),
		FUSE_OPT_KEY("-r", HMCDRV_FUSE_OPTKEY_RO),
		FUSE_OPT_KEY("--read-only", HMCDRV_FUSE_OPTKEY_RO),

		FUSE_OPT_KEY("noatime", HMCDRV_FUSE_OPTKEY_NATM),
		FUSE_OPT_KEY("entry_timeout=%lf", HMCDRV_FUSE_OPTKEY_CTMO),
		FUSE_OPT_KEY("attr_timeout=%lf", HMCDRV_FUSE_OPTKEY_CTMO),

		/* unsupported (ignored) */
		FUSE_OPT_KEY("subdir=%s", HMCDRV_FUSE_OPTKEY_NSUP),
		FUSE_OPT_KEY("use_ino", HMCDRV_FUSE_OPTKEY_NSUP),
		FUSE_OPT_KEY("readdir_ino", HMCDRV_FUSE_OPTKEY_NSUP),

		/* to be ignored on read-only filesystem */
		FUSE_OPT_KEY("rw", HMCDRV_FUSE_OPTKEY_RW),
		FUSE_OPT_KEY("-w", HMCDRV_FUSE_OPTKEY_RW),
		FUSE_OPT_KEY("--rw", HMCDRV_FUSE_OPTKEY_RW),
		FUSE_OPT_KEY("atime", HMCDRV_FUSE_OPTKEY_RW),
		FUSE_OPT_KEY("max_write=%u", HMCDRV_FUSE_OPTKEY_RW),
		FUSE_OPT_KEY("big_writes", HMCDRV_FUSE_OPTKEY_RW),
		FUSE_OPT_KEY("hard_remove", HMCDRV_FUSE_OPTKEY_RW),
		FUSE_OPT_KEY("atomic_o_trunc", HMCDRV_FUSE_OPTKEY_RW),
		FUSE_OPT_KEY("negative_timeout=%lf", HMCDRV_FUSE_OPTKEY_RW),

		/* options, that exit immediately */
		FUSE_OPT_KEY("-v", HMCDRV_FUSE_OPTKEY_VERSION),
		FUSE_OPT_KEY("--version", HMCDRV_FUSE_OPTKEY_VERSION),
		FUSE_OPT_KEY("-h", HMCDRV_FUSE_OPTKEY_HELP),
		FUSE_OPT_KEY("--help", HMCDRV_FUSE_OPTKEY_HELP),

		FUSE_OPT_END
	};

	int ret;
	struct fuse_args args = FUSE_ARGS_INIT(argc, argv);

	memset(&hmcdrv_ctx.opt, 0, sizeof(hmcdrv_ctx.opt));
	hmcdrv_ctx.pid = getpid();

	fuse_opt_parse(&args, &hmcdrv_ctx.opt, lookup_opt,
		       hmcdrv_fuse_optproc);

	/* the following options are required on a read-only media */
	if (!hmcdrv_ctx.opt.ro)
		fuse_opt_add_arg(&args, "-oro");

	if (!hmcdrv_ctx.opt.noatime)
		fuse_opt_add_arg(&args, "-onoatime");

	if (hmcdrv_optproc_lang() != 0) {
		HMCDRV_FUSE_LOG(LOG_ERR,
				"Unknown HMC language in '-o hmclang=%s'",
				hmcdrv_ctx.opt.hmclang);
		exit(EXIT_FAILURE);
	}

	/* notice that there is no way to check the timezone parameter for
	 * correct syntax, not in glibc as well as POSIX 1003.1
	 */
	if (hmcdrv_ctx.opt.hmctz != NULL)
		HMCDRV_FUSE_DBGLOG("option \"-o hmctz=%s\" detected",
				   hmcdrv_ctx.opt.hmctz);

	ret = hmcdrv_fuse_main(&args);
	fuse_opt_free_args(&args);

	return ret;
}
