/*
 * Amanda, The Advanced Maryland Automatic Network Disk Archiver
 * Copyright (c) 1991-1998 University of Maryland at College Park
 * Copyright (c) 2007-2012 Zmanda, Inc.  All Rights Reserved.
 * Copyright (c) 2013-2016 Carbonite, Inc.  All Rights Reserved.
 * All Rights Reserved.
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation, and that the name of U.M. not be used in advertising or
 * publicity pertaining to distribution of the software without specific,
 * written prior permission.  U.M. makes no representations about the
 * suitability of this software for any purpose.  It is provided "as is"
 * without express or implied warranty.
 *
 * U.M. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL U.M.
 * BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * Authors: the Amanda Development Team.  Its members are listed in a
 * file named AUTHORS, in the root directory of this distribution.
 */
/*
 * $Id: amindexd.c,v 1.106 2006/07/25 18:27:57 martinea Exp $
 *
 * This is the server daemon part of the index client/server system.
 * It is assumed that this is launched from inetd instead of being
 * started as a daemon because it is not often used
 */

/*
** Notes:
** - this server will do very little until it knows what Amanda config it
**   is to use.  Setting the config has the side effect of changing to the
**   index directory.
** - XXX - I'm pretty sure the config directory name should have '/'s stripped
**   from it.  It is given to us by an unknown person via the network.
*/

#include "amanda.h"
#include "conffile.h"
#include "diskfile.h"
#include "clock.h"
#include "match.h"
#include "amindex.h"
#include "disk_history.h"
#include "list_dir.h"
#include "logfile.h"
#include "find.h"
#include "tapefile.h"
#include "amutil.h"
#include "amandad.h"
#include "pipespawn.h"
#include "sockaddr-util.h"
#include "amxml.h"

#include <grp.h>

#define DBG(i, ...) do {		\
	if ((i) <= debug_amindexd) {	\
	    g_debug(__VA_ARGS__);	\
	}				\
} while (0)

typedef struct REMOVE_ITEM
{
    char *filename;
    struct REMOVE_ITEM *next;
} REMOVE_ITEM;

/* state */
static int from_amandad;
static char local_hostname[MAX_HOSTNAME_LENGTH+1];	/* me! */
static char *dump_hostname = NULL;		/* machine we are restoring */
static char *disk_name;				/* disk we are restoring */
static char *qdisk_name = NULL;			/* disk we are restoring */
static char *target_date = NULL;
static char **storage_list = NULL;
static disklist_t disk_list;			/* all disks in cur config */
static find_result_t *output_find = NULL;
static g_option_t *g_options = NULL;
static file_lock *lock_index = NULL;

static int amindexd_debug = 0;

static REMOVE_ITEM *uncompress_remove = NULL;
					/* uncompressed files to remove */
static REMOVE_ITEM *compress_sorted_files = NULL;
					/* compress new sorted files */

static am_feature_t *our_features = NULL;
static am_feature_t *their_features = NULL;

static int get_pid_status(int pid, char *program, GPtrArray **emsg);
static REMOVE_ITEM *remove_files(REMOVE_ITEM *);
static REMOVE_ITEM *compress_files(REMOVE_ITEM *);
static char *uncompress_file(char *, char *, char *, int,
			     char *, GPtrArray **,
			     gboolean need_uncompress, gboolean need_sort);
static int process_ls_dump(char *, DUMP_ITEM *, int, GPtrArray **);

static size_t reply_buffer_size = 1;
static char *reply_buffer = NULL;
static char *amandad_auth = NULL;
static FILE *cmdin;
static FILE *cmdout;

static void reply_ptr_array(int, GPtrArray *);
static void reply(int, char *, ...) G_GNUC_PRINTF(2, 3);
static void lreply(int, char *, ...) G_GNUC_PRINTF(2, 3);
static void fast_lreply(int, char *, ...) G_GNUC_PRINTF(2, 3);
static am_host_t *is_dump_host_valid(char *);
static int is_disk_valid(char *);
static int check_and_load_config(char *);
static int build_disk_table(void);
static int disk_history_list(void);
static int is_dir_valid_opaque(char *);
static int opaque_ls(char *, int);
static void opaque_ls_one (DIR_ITEM *dir_item, am_feature_e marshall_feature,
			     int recursive);
static int tapedev_is(void);
static int are_dumps_compressed(void);
static char *amindexd_nicedate (char *datestamp);
static int cmp_date (const char *date1, const char *date2);
static char *get_index_name(char *dump_hostname, char *hostname,
			    char *diskname, char *timestamps, int level,
			    GPtrArray **emsg);
static int get_index_dir(char *dump_hostname, char *hostname, char *diskname);

int main(int, char **);


static int
get_pid_status(
    int         pid,
    char       *program,
    GPtrArray **emsg)
{
    int       status;
    amwait_t  wait_status;
    char     *msg;
    int       result = 1;

    status = waitpid(pid, &wait_status, 0);
    if (status < 0) {
	msg = g_strdup_printf(
		_("%s (%d) returned negative value: %s"),
		program, pid, strerror(errno));
	dbprintf("%s\n", msg);
	g_ptr_array_add(*emsg, msg);
	result = 0;
    } else {
	if (!WIFEXITED(wait_status)) {
	    msg = g_strdup_printf(
			_("%s exited with signal %d"),
			program, WTERMSIG(wait_status));
	    dbprintf("%s\n", msg);
	    g_ptr_array_add(*emsg, msg);
	    result = 0;
	} else if (WEXITSTATUS(wait_status) != 0) {
	    msg = g_strdup_printf(
			_("%s exited with status %d"),
			program, WEXITSTATUS(wait_status));
	    dbprintf("%s\n", msg);
	    g_ptr_array_add(*emsg, msg);
	    result = 0;
	}
    }
    return result;
}

static REMOVE_ITEM *
remove_files(
    REMOVE_ITEM *remove)
{
    REMOVE_ITEM *prev;

    if (file_lock_locked(lock_index)) {
	while(remove) {
	    dbprintf(_("removing index file: %s\n"), remove->filename);
	    unlink(remove->filename);
	    amfree(remove->filename);
	    prev = remove;
	    remove = remove->next;
	    amfree(prev);
	}
    }
    return remove;
}

static REMOVE_ITEM *
compress_files(
    REMOVE_ITEM *compress)
{
    REMOVE_ITEM *prev;
    pid_t        pid;

    if (file_lock_locked(lock_index)) {
	while(compress) {
	    dbprintf(_("compressing index file: %s\n"), compress->filename);

	    switch (pid = fork()) {
	    case -1:
		error("error: couldn't fork: %s", strerror(errno));
	    case 0:
		execlp(COMPRESS_PATH, COMPRESS_PATH, COMPRESS_BEST_OPT, compress->filename, NULL);
		error("error: couldn't exec %s: %s", COMPRESS_PATH, strerror(errno));
		/*NOTREACHED*/
	    default:
		break;
	    }
	    waitpid(pid, NULL, 0);
	    amfree(compress->filename);
	    prev = compress;
	    compress = compress->next;
	    amfree(prev);
	}
    }
    return compress;
}

/* find all matching entries in a dump listing */
/* return -1 if error */
static int
process_ls_dump(
    char *	dir,
    DUMP_ITEM *	dump_item,
    int		recursive,
    GPtrArray **emsg)
{
    char line[STR_SIZE], old_line[STR_SIZE];
    char *filename = NULL;
    char *dir_slash = NULL;
    FILE *fp;
    char *s;
    int ch;
    size_t len_dir_slash;

    old_line[0] = '\0';
    if (g_str_equal(dir, "/")) {
	dir_slash = g_strdup(dir);
    } else {
	dir_slash = g_strconcat(dir, "/", NULL);
    }

    filename = get_index_name(dump_hostname, dump_item->hostname, disk_name,
			      dump_item->date, dump_item->level, emsg);
    if (filename == NULL) {
	amfree(filename);
	amfree(dir_slash);
	return -1;
    }

    if((fp = fopen(filename,"r")) == NULL) {
	g_ptr_array_add(*emsg, g_strdup_printf("%s", strerror(errno)));
	amfree(dir_slash);
        amfree(filename);
	return -1;
    }

    len_dir_slash=strlen(dir_slash);

    while (fgets(line, STR_SIZE, fp) != NULL) {
	if (line[0] != '\0') {
	    if(strlen(line) > 0 && line[strlen(line)-1] == '\n')
		line[strlen(line)-1] = '\0';
	    if (g_str_has_prefix(line, dir_slash )) {
		if(!recursive) {
		    s = line + len_dir_slash;
		    ch = *s++;
		    while(ch && ch != '/')
			ch = *s++;/* find end of the file name */
		    if(ch == '/') {
			s++;
		    }
		    s[-1] = '\0';
		}
		if(!g_str_equal(line, old_line)) {
		    add_dir_list_item(dump_item, line);
		    strcpy(old_line, line);
		}
	    }
	}
    }
    afclose(fp);
    amfree(filename);
    amfree(dir_slash);
    return 0;
}

static void
reply_ptr_array(
    int n,
    GPtrArray *emsg)
{
    gpointer *p;

    if (emsg->len == 0)
	return;

    p = emsg->pdata;
    while (p != emsg->pdata + emsg->len -1) {
	fast_lreply(n, "%s", (char *)*p);
	p++;
    }
    reply(n, "%s", (char *)*p);
}

/* send a 1 line reply to the client */
static void reply(int n, char *fmt, ...)
{
    va_list args;
    int len;

    if(!reply_buffer)
	reply_buffer = g_malloc(reply_buffer_size);

    while(1) {
	arglist_start(args, fmt);
	len = g_vsnprintf(reply_buffer, reply_buffer_size, fmt, args);
	arglist_end(args);

	if (len > -1 && (size_t)len < reply_buffer_size-1)
	    break;

	reply_buffer_size *= 2;
	amfree(reply_buffer);
	reply_buffer = g_malloc(reply_buffer_size);
    }

    if (g_fprintf(cmdout,"%03d %s\r\n", n, reply_buffer) < 0)
    {
	dbprintf(_("! error %d (%s) in printf\n"), errno, strerror(errno));
	uncompress_remove = remove_files(uncompress_remove);
	compress_sorted_files = compress_files(compress_sorted_files);
	exit(1);
    }
    if (fflush(cmdout) != 0)
    {
	dbprintf(_("! error %d (%s) in fflush\n"), errno, strerror(errno));
	uncompress_remove = remove_files(uncompress_remove);
	compress_sorted_files = compress_files(compress_sorted_files);
	exit(1);
    }
    dbprintf(_("< %03d %s\n"), n, reply_buffer);
}

/* send one line of a multi-line response */
static void lreply(int n, char *fmt, ...)
{
    va_list args;
    int len;

    if(!reply_buffer)
	reply_buffer = g_malloc(reply_buffer_size);

    while(1) {
	arglist_start(args, fmt);
	len = g_vsnprintf(reply_buffer, reply_buffer_size, fmt, args);
	arglist_end(args);

	if (len > -1 && (size_t)len < reply_buffer_size-1)
	    break;

	reply_buffer_size *= 2;
	amfree(reply_buffer);
	reply_buffer = g_malloc(reply_buffer_size);
    }

    if (g_fprintf(cmdout,"%03d-%s\r\n", n, reply_buffer) < 0)
    {
	dbprintf(_("! error %d (%s) in printf\n"),
		  errno, strerror(errno));
	uncompress_remove = remove_files(uncompress_remove);
	compress_sorted_files = compress_files(compress_sorted_files);
	exit(1);
    }
    if (fflush(cmdout) != 0)
    {
	dbprintf(_("! error %d (%s) in fflush\n"),
		  errno, strerror(errno));
	uncompress_remove = remove_files(uncompress_remove);
	compress_sorted_files = compress_files(compress_sorted_files);
	exit(1);
    }

    dbprintf("< %03d-%s\n", n, reply_buffer);

}

/* send one line of a multi-line response */
static void fast_lreply(int n, char *fmt, ...)
{
    va_list args;
    int len;

    if(!reply_buffer)
	reply_buffer = g_malloc(reply_buffer_size);

    while(1) {
	arglist_start(args, fmt);
	len = g_vsnprintf(reply_buffer, reply_buffer_size, fmt, args);
	arglist_end(args);

	if (len > -1 && (size_t)len < reply_buffer_size-1)
	    break;

	reply_buffer_size *= 2;
	amfree(reply_buffer);
	reply_buffer = g_malloc(reply_buffer_size);
    }

    if (g_fprintf(cmdout,"%03d-%s\r\n", n, reply_buffer) < 0)
    {
	dbprintf(_("! error %d (%s) in printf\n"),
		  errno, strerror(errno));
	uncompress_remove = remove_files(uncompress_remove);
	compress_sorted_files = compress_files(compress_sorted_files);
	exit(1);
    }

    DBG(2, "< %03d-%s", n, reply_buffer);
}

/* see if hostname is valid */
/* valid is defined to be that there is an index directory for it */
/* also do a security check on the requested dump hostname */
/* to restrict access to index records if required */
/* return -1 if not okay */
static am_host_t *
is_dump_host_valid(
    char *	host)
{
    am_host_t   *ihost;
    disk_t      *diskp;

    if (get_config_name() == NULL) {
	reply(501, _("Must set config before setting host."));
	return NULL;
    }

    /* check that the config actually handles that host */
    ihost = lookup_host(host);
    if(ihost == NULL) {
	reply(501, _("Host %s is not in your disklist."), host);
	return NULL;
    }

    /* check if an index dir exist */
    if(get_index_dir(host, ihost->hostname, NULL)) {
	return ihost;
    }

    /* check if an index dir exist for at least one DLE */
    for(diskp = ihost->disks; diskp != NULL; diskp = diskp->hostnext) {
	if (get_index_dir(diskp->hostname, NULL, NULL)) {
	    return ihost;
	}
    }

    reply(501, _("No index records for host: %s. Have you enabled indexing?"),
	  host);
    return NULL;
}


static gboolean
is_disk_allowed(
    disk_t *disk)
{
    dumptype_t *dt = disk->config;
    host_limit_t *rl = NULL;
    char *peer;
    char *dle_hostname;
    GSList *iter;

    /* get the config: either for the DLE or the global config */
    if (dt) {
	if (dumptype_seen(dt, DUMPTYPE_RECOVERY_LIMIT)) {
	    g_debug("using recovery limit from DLE");
	    rl = dumptype_get_recovery_limit(dt);
	}
    }
    if (!rl) {
	if (getconf_seen(CNF_RECOVERY_LIMIT)) {
	    g_debug("using global recovery limit");
	    rl = getconf_recovery_limit(CNF_RECOVERY_LIMIT);
	}
    }
    if (!rl) {
	g_debug("no recovery limit found; allowing access");
	return TRUE;
    }

    peer = getenv("AMANDA_AUTHENTICATED_PEER");
    if (!peer || !*peer) {
	g_warning("DLE has a recovery-limit, but no authenticated peer name is "
		  "available; rejecting");
	return FALSE;
    }

    /* check same-host */
    dle_hostname = disk->host? disk->host->hostname : NULL;
    if (rl->same_host && dle_hostname) {
	if (0 == g_ascii_strcasecmp(peer, dle_hostname)) {
	    g_debug("peer matched same-host ('%s')", dle_hostname);
	    return TRUE;
	}
    }

    /* check server */
    if (rl->server) {
	char myhostname[MAX_HOSTNAME_LENGTH+1];
	if (gethostname(myhostname, MAX_HOSTNAME_LENGTH) == 0) {
	    myhostname[MAX_HOSTNAME_LENGTH] = '\0';
	    g_debug("server hostname: %s", myhostname);
	    if (0 == g_ascii_strcasecmp(peer, myhostname)) {
		g_debug("peer matched server ('%s')", myhostname);
		return TRUE;
	    }
	}
    }

    /* check the match list */
    for (iter = rl->match_pats; iter; iter = iter->next) {
	char *pat = iter->data;
	if (match_host(pat, peer))
	    return TRUE;
    }

    g_warning("peer '%s' does not match any of the recovery-limit restrictions; rejecting",
	    peer);

    return FALSE;
}

static int
is_disk_valid(
    char *disk)
{
    disk_t *idisk;
    char *qdisk;

    if (get_config_name() == NULL) {
	reply(501, _("Must set config,host before setting disk."));
	return -1;
    }
    else if (dump_hostname == NULL) {
	reply(501, _("Must set host before setting disk."));
	return -1;
    }

    /* check that the config actually handles that disk, and that recovery-limit allows it */
    idisk = lookup_disk(dump_hostname, disk);
    if(idisk == NULL || !is_disk_allowed(idisk)) {
	qdisk = quote_string(disk);
	reply(501, _("Disk %s:%s is not in the server's disklist."), dump_hostname, qdisk);
	amfree(qdisk);
	return -1;
    }

    /* assume an index dir already */
    if (get_index_dir(dump_hostname, idisk->hostname, disk) == 0) {
	qdisk = quote_string(disk);
	reply(501, _("No index records for disk: %s. Invalid?"), qdisk);
	amfree(qdisk);
	return -1;
    }

    return 0;
}


static int
check_and_load_config(
    char *	config)
{
    char *conf_diskfile;
    char *conf_tapelist;
    char *conf_indexdir;
    char *lock_file;
    struct stat dir_stat;

    /* check that the config actually exists */
    if (config == NULL) {
	reply(501, _("Must set config first."));
	return -1;
    }

    /* (re-)initialize configuration with the new config name */
    config_init_with_global(CONFIG_INIT_EXPLICIT_NAME, config);
    if (config_errors(NULL) >= CFGERR_ERRORS) {
	reply(501, _("Could not read config file for %s!"), config);
	return -1;
    }

    check_running_as(RUNNING_AS_DUMPUSER_PREFERRED);

    conf_diskfile = config_dir_relative(getconf_str(CNF_DISKFILE));
    read_diskfile(conf_diskfile, &disk_list);
    amfree(conf_diskfile);
    if (config_errors(NULL) >= CFGERR_ERRORS) {
	reply(501, _("Could not read disk file %s!"), conf_diskfile);
	return -1;
    }

    conf_tapelist = config_dir_relative(getconf_str(CNF_TAPELIST));
    if(read_tapelist(conf_tapelist)) {
	reply(501, _("Could not read tapelist file %s!"), conf_tapelist);
	amfree(conf_tapelist);
	return -1;
    }
    amfree(conf_tapelist);

    dbrename(get_config_name(), DBG_SUBDIR_SERVER);

    output_find = find_dump(&disk_list, 1);
    /* the 'w' here sorts by write timestamp, so that the first instance of
     * any particular datestamp/host/disk/level/part that we see is the one
     * written earlier */
    sort_find_result("DLKHspwB", &output_find);

    conf_indexdir = config_dir_relative(getconf_str(CNF_INDEXDIR));
    if (stat (conf_indexdir, &dir_stat) != 0 || !S_ISDIR(dir_stat.st_mode)) {
	reply(501, _("Index directory %s does not exist"), conf_indexdir);
	amfree(conf_indexdir);
	return -1;
    }

    /* remove previous lock */
    if (lock_index) {
	file_lock_unlock(lock_index);
	file_lock_free(lock_index);
	lock_index = NULL;
    }

    /* take a lock file to prevent concurent trim */
    lock_file = g_strdup_printf("%s/%s", conf_indexdir, "lock");
    lock_index = file_lock_new(lock_file);
    file_lock_lock_rd(lock_index);

    amfree(lock_file);
    amfree(conf_indexdir);

    return 0;
}


static int
build_disk_table(void)
{
    char *date;
    char *last_timestamp;
    char *last_storage;
    off_t last_filenum;
    int last_level;
    int last_partnum;
    find_result_t *find_output;

    if (get_config_name() == NULL) {
	reply(590, _("Must set config,host,disk before building disk table"));
	return -1;
    }
    else if (dump_hostname == NULL) {
	reply(590, _("Must set host,disk before building disk table"));
	return -1;
    }
    else if (disk_name == NULL) {
	reply(590, _("Must set disk before building disk table"));
	return -1;
    }

    clear_list();
    last_timestamp = NULL;
    last_storage = NULL;
    last_filenum = (off_t)-1;
    last_level = -1;
    last_partnum = -1;
    for(find_output = output_find;
	find_output != NULL; 
	find_output = find_output->next) {
	if(strcasecmp(dump_hostname, find_output->hostname) == 0 &&
	   g_str_equal(disk_name, find_output->diskname) &&
	   g_str_equal("OK", find_output->status) &&
	   g_str_equal("OK", find_output->dump_status)) {
	    /*
	     * The sort order puts holding disk entries first.  We want to
	     * use them if at all possible, so ignore any other entries
	     * for the same datestamp after we see a holding disk entry
	     * (as indicated by a filenum of zero).
	     */
	    if(last_timestamp &&
	       g_str_equal(find_output->timestamp, last_timestamp) &&
	       find_output->level == last_level && 
	       last_filenum == 0) {
		continue;
	    }
	    /* ignore duplicate partnum */
	    if (last_timestamp &&
	        g_str_equal(find_output->timestamp, last_timestamp) &&
	        find_output->level == last_level &&
	        find_output->partnum == last_partnum &&
	        (!am_has_feature(their_features, fe_amindexd_STORAGE) ||
		 g_str_equal(find_output->storage, last_storage))) {
		continue;
	    }
	    if (storage_list) {
		char **storage_l;
		gboolean found = FALSE;
		for (storage_l = storage_list; *storage_l != NULL; storage_l++) {
		     if (g_str_equal(find_output->storage, *storage_l))
			found = TRUE;
		}
		if (!found)
		    continue;
	    }
	    last_timestamp = find_output->timestamp;
	    last_storage = find_output->storage;
	    last_filenum = find_output->filenum;
	    last_level = find_output->level;
	    last_partnum = find_output->partnum;
	    date = amindexd_nicedate(find_output->timestamp);
	    add_dump(find_output->hostname, date, find_output->level,
		     find_output->storage, find_output->label, find_output->filenum,
		     find_output->partnum, find_output->totalparts);
	    dbprintf("- %s %d %s %lld %d %d\n",
		     date, find_output->level, 
		     find_output->label,
		     (long long)find_output->filenum,
		     find_output->partnum, find_output->totalparts);
	}
    }

    clean_dump();

    return 0;
}


static int
disk_history_list(void)
{
    DUMP_ITEM *item;
    char date[20];

    if (get_config_name() == NULL) {
	reply(502, _("Must set config,host,disk before listing history"));
	return -1;
    }
    else if (dump_hostname == NULL) {
	reply(502, _("Must set host,disk before listing history"));
	return -1;
    }
    else if (disk_name == NULL) {
	reply(502, _("Must set disk before listing history"));
	return -1;
    }

    lreply(200, _(" Dump history for config \"%s\" host \"%s\" disk %s"),
	  get_config_name(), dump_hostname, qdisk_name);

    for (item=first_dump(); item!=NULL; item=next_dump(item)){
        char *tapelist_str = marshal_tapelist(item->tapes, 1,
		am_has_feature(their_features, fe_amrecover_storage_in_marshall));

	strncpy(date, item->date, 20);
	date[19] = '\0';
	if(!am_has_feature(their_features,fe_amrecover_timestamp))
	    date[10] = '\0';

	if(am_has_feature(their_features, fe_amindexd_marshall_in_DHST)){
	    lreply(201, " %s %d %s", date, item->level, tapelist_str);
	}
	else{
	    lreply(201, " %s %d %s %lld", date, item->level,
		tapelist_str, (long long)item->file);
	}
	amfree(tapelist_str);
    }

    reply(200, _("Dump history for config \"%s\" host \"%s\" disk %s"),
	  get_config_name(), dump_hostname, qdisk_name);

    return 0;
}


/*
 * is the directory dir backed up - dir assumed complete relative to
 * disk mount point
 */
/* opaque version of command */
static int
is_dir_valid_opaque(
    char *dir)
{
    DUMP_ITEM *item;
    char line[STR_SIZE];
    FILE *fp;
    int last_level;
    char *ldir = NULL;
    char *filename = NULL;
    size_t ldir_len;
    GPtrArray *emsg = NULL;

    if (get_config_name() == NULL || dump_hostname == NULL || disk_name == NULL) {
	reply(502, _("Must set config,host,disk before asking about directories"));
	return -1;
    }
    else if (dump_hostname == NULL) {
	reply(502, _("Must set host,disk before asking about directories"));
	return -1;
    }
    else if (disk_name == NULL) {
	reply(502, _("Must set disk before asking about directories"));
	return -1;
    }
    else if (target_date == NULL) {
	reply(502, _("Must set date before asking about directories"));
	return -1;
    }
    /* scan through till we find first dump on or before date */
    for (item=first_dump(); item!=NULL; item=next_dump(item))
	if (cmp_date(item->date, target_date) <= 0)
	    break;

    if (item == NULL)
    {
	/* no dump for given date */
	reply(500, _("No dumps available on or before date \"%s\""), target_date);
	return -1;
    }

    if(g_str_equal(dir, "/")) {
	ldir = g_strdup(dir);
    } else {
	ldir = g_strconcat(dir, "/", NULL);
    }
    ldir_len = strlen(ldir);

    /* go back till we hit a level 0 dump */
    do
    {
	amfree(filename);
	emsg = g_ptr_array_new();
	filename = get_index_name(dump_hostname, item->hostname, disk_name,
				  item->date, item->level, &emsg);
	if (filename == NULL) {
	    reply_ptr_array(599, emsg);
	    amfree(ldir);
	    return -1;
	}
	g_ptr_array_free_full(emsg);
	dbprintf("f %s\n", filename);
	if ((fp = fopen(filename, "r")) == NULL) {
	    reply(599, _("System error: %s"), strerror(errno));
	    amfree(filename);
	    amfree(ldir);
	    return -1;
	}
	while (fgets(line, STR_SIZE, fp) != NULL) {
	    if (line[0] == '\0')
		continue;
	    if(strlen(line) > 0 && line[strlen(line)-1] == '\n')
		line[strlen(line)-1] = '\0';
	    if (strncmp(line, ldir, ldir_len) != 0) {
		continue;			/* not found yet */
	    }
	    amfree(filename);
	    amfree(ldir);
	    afclose(fp);
	    return 0;
	}
	afclose(fp);

	last_level = item->level;
	do
	{
	    item=next_dump(item);
	} while ((item != NULL) && (item->level >= last_level));
    } while (item != NULL);

    amfree(filename);
    amfree(ldir);
    reply(500, _("\"%s\" is an invalid directory"), dir);
    return -1;
}

static int
opaque_ls(
    char *	dir,
    int		recursive)
{
    DUMP_ITEM *dump_item;
    DIR_ITEM *dir_item;
    int level, last_level;
    GPtrArray *emsg = NULL;
    am_feature_e marshall_feature;

    if (recursive) {
        marshall_feature = fe_amindexd_marshall_in_ORLD;
    } else {
        marshall_feature = fe_amindexd_marshall_in_OLSD;
    }

    clear_dir_list();

    if (get_config_name() == NULL) {
	reply(502, _("Must set config,host,disk before listing a directory"));
	return -1;
    }
    else if (dump_hostname == NULL) {
	reply(502, _("Must set host,disk before listing a directory"));
	return -1;
    }
    else if (disk_name == NULL) {
	reply(502, _("Must set disk before listing a directory"));
	return -1;
    }
    else if (target_date == NULL) {
	reply(502, _("Must set date before listing a directory"));
	return -1;
    }

    /* scan through till we find first dump on or before date */
    for (dump_item=first_dump(); dump_item!=NULL; dump_item=next_dump(dump_item))
	if (cmp_date(dump_item->date, target_date) <= 0)
	    break;

    if (dump_item == NULL)
    {
	/* no dump for given date */
	reply(500, _("No dumps available on or before date \"%s\""), target_date);
	return -1;
    }

    /* get data from that dump */
    emsg = g_ptr_array_new();
    if (process_ls_dump(dir, dump_item, recursive, &emsg) == -1) {
	reply_ptr_array(599, emsg);
	g_ptr_array_free_full(emsg);
	return -1;
    }

    /* go back processing higher level dumps till we hit a level 0 dump */
    last_level = dump_item->level;
    while ((last_level != 0) && ((dump_item=next_dump(dump_item)) != NULL))
    {
	if (dump_item->level < last_level)
	{
	    last_level = dump_item->level;
	    if (process_ls_dump(dir, dump_item, recursive, &emsg) == -1) {
		reply_ptr_array(599, emsg);
		g_ptr_array_free_full(emsg);
		return -1;
	    }
	}
    }
    g_ptr_array_free_full(emsg);

    /* return the information to the caller */
    lreply(200, _(" Opaque list of %s"), dir);
    for(level=0; level < DUMP_LEVELS; level++) {
	for (dir_item = get_dir_list(); dir_item != NULL; 
	     dir_item = dir_item->next) {

	    if(dir_item->dump->level == level) {
		if (!am_has_feature(their_features, marshall_feature) &&
	            (num_entries(dir_item->dump->tapes) > 1 ||
	            dir_item->dump->tapes->numfiles > 1)) {
	            fast_lreply(501, _(" ERROR: Split dumps not supported"
				" with old version of amrecover."));
		    break;
		}
		else {
		    opaque_ls_one(dir_item, marshall_feature, recursive);
		}
	    }
	}
    }
    reply(200, _(" Opaque list of %s"), dir);

    clear_dir_list();
    return 0;
}

void opaque_ls_one(
    DIR_ITEM *	 dir_item,
    am_feature_e marshall_feature,
    int		 recursive)
{
    char date[20];
    char *tapelist_str;
    char *qtapelist_str;
    char *qpath;

    if (am_has_feature(their_features, marshall_feature)) {
	tapelist_str = marshal_tapelist(dir_item->dump->tapes, 1,
		 am_has_feature(their_features, fe_amrecover_storage_in_marshall));
    } else {
	tapelist_str = dir_item->dump->tapes->label;
    }

    if (am_has_feature(their_features, fe_amindexd_quote_label)) {
	qtapelist_str = quote_string(tapelist_str);
    } else {
	qtapelist_str = g_strdup(tapelist_str);
    }
    strncpy(date, dir_item->dump->date, 20);
    date[19] = '\0';
    if(!am_has_feature(their_features,fe_amrecover_timestamp))
	date[10] = '\0';

    qpath = quote_string(dir_item->path);
    if((!recursive && am_has_feature(their_features,
				     fe_amindexd_fileno_in_OLSD)) ||
       (recursive && am_has_feature(their_features,
				    fe_amindexd_fileno_in_ORLD))) {
	fast_lreply(201, " %s %d %s %lld %s",
		    date,
		    dir_item->dump->level,
		    qtapelist_str,
		    (long long)dir_item->dump->file,
		    qpath);
    }
    else {

	fast_lreply(201, " %s %d %s %s",
		    date, dir_item->dump->level,
		    qtapelist_str, qpath);
    }
    amfree(qpath);
    if(am_has_feature(their_features, marshall_feature)) {
	amfree(tapelist_str);
    }
    amfree(qtapelist_str);
}

/*
 * returns the value of changer or tapedev from the amanda.conf file if set,
 * otherwise reports an error
 */

static int
tapedev_is(void)
{
    char *result;

    /* check state okay to do this */
    if (get_config_name() == NULL) {
	reply(501, _("Must set config before asking about tapedev."));
	return -1;
    }

    /* use amrecover_changer if possible */
    if ((result = getconf_str(CNF_AMRECOVER_CHANGER)) != NULL  &&
        *result != '\0') {
	dbprintf(_("tapedev_is amrecover_changer: %s\n"), result);
	reply(200, "%s", result);
	return 0;
    }

    /* use changer if possible */
    if ((result = getconf_str(CNF_TPCHANGER)) != NULL  &&  *result != '\0') {
	dbprintf(_("tapedev_is tpchanger: %s\n"), result);
	reply(200, "%s", result);
	return 0;
    }

    /* get tapedev value */
    if ((result = getconf_str(CNF_TAPEDEV)) != NULL  &&  *result != '\0') {
	dbprintf(_("tapedev_is tapedev: %s\n"), result);
	reply(200, "%s", result);
	return 0;
    }

    dbprintf(_("No tapedev or tpchanger in config site.\n"));

    reply(501, _("Tapedev or tpchanger not set in config file."));
    return -1;
}


/* returns YES if dumps for disk are compressed, NO if not */
static int
are_dumps_compressed(void)
{
    GList  *dlist;
    disk_t *diskp = NULL;

    /* check state okay to do this */
    if (get_config_name() == NULL) {
	reply(501, _("Must set config,host,disk name before asking about dumps."));
	return -1;
    }
    else if (dump_hostname == NULL) {
	reply(501, _("Must set host,disk name before asking about dumps."));
	return -1;
    }
    else if (disk_name == NULL) {
	reply(501, _("Must set disk name before asking about dumps."));
	return -1;
    }

    /* now go through the list of disks and find which have indexes */
    for (dlist = disk_list.head; dlist != NULL; dlist = dlist->next) {
	diskp = dlist->data;
	if ((strcasecmp(diskp->host->hostname, dump_hostname) == 0)
		&& (g_str_equal(diskp->name, disk_name))) {
	    break;
	}
    }

    if (diskp == NULL) {
	reply(501, _("Couldn't find host/disk in disk file."));
	return -1;
    }

    /* send data to caller */
    if (diskp->compress == COMP_NONE)
	reply(200, "NO");
    else
	reply(200, "YES");

    return 0;
}

int
main(
    int		argc,
    char **	argv)
{
    char *line = NULL, *part = NULL;
    char *s;
    int ch;
    char *cmd_undo, cmd_undo_ch;
    socklen_t_equiv socklen;
    sockaddr_union his_addr;
    char *arg = NULL;
    char *cmd;
    size_t len;
    int user_validated = 0;
    char *errstr = NULL;
    char *pgm = "amindexd";		/* in case argv[0] is not set */
    char his_hostname[MAX_HOSTNAME_LENGTH];
    char *cfg_opt = NULL;

    glib_init();

    if (argc > 1 && argv && argv[1] && g_str_equal(argv[1], "--version")) {
	printf("amindexd-%s\n", VERSION);
	return (0);
    }

    /*
     * Configure program for internationalization:
     *   1) Only set the message locale for now.
     *   2) Set textdomain for all amanda related programs to "amanda"
     *      We don't want to be forced to support dozens of message catalogs.
     */  
    setlocale(LC_MESSAGES, "C");
    textdomain("amanda"); 

    safe_fd(DATA_FD_OFFSET, 2);
    openbsd_fd_inform();
    safe_cd();

    /*
     * When called via inetd, it is not uncommon to forget to put the
     * argv[0] value on the config line.  On some systems (e.g. Solaris)
     * this causes argv and/or argv[0] to be NULL, so we have to be
     * careful getting our name.
     */
    if (argc >= 1 && argv != NULL && argv[0] != NULL) {
	if((pgm = strrchr(argv[0], '/')) != NULL) {
	    pgm++;
	} else {
	    pgm = argv[0];
	}
    }

    set_pname(pgm);

    /* Don't die when child closes pipe */
    signal(SIGPIPE, SIG_IGN);

    dbopen(DBG_SUBDIR_SERVER);
    dbprintf(_("version %s\n"), VERSION);

    if(argv == NULL) {
	error("argv == NULL\n");
    }

    if (! (argc >= 1 && argv[0] != NULL)) {
	dbprintf(_("WARNING: argv[0] not defined: check inetd.conf\n"));
    }

    debug_dup_stderr_to_debug();

    /* initialize */

    argc--;
    argv++;

    if(argc > 0 && g_str_equal(*argv, "-t")) {
	amindexd_debug = 1;
	argc--;
	argv++;
    }

    if(argc > 0 && g_str_equal(*argv, "amandad")) {
	from_amandad = 1;
	argc--;
	argv++;
	if(argc > 0) {
	    amandad_auth = *argv;
	    argc--;
	    argv++;
	}
    }
    else {
	from_amandad = 0;
	safe_fd(dbfd(), 1);
    }

    if (argc > 0) {
	cfg_opt = *argv;
	argc--;
	argv++;
    }

    if(gethostname(local_hostname, sizeof(local_hostname)-1) == -1) {
	error(_("gethostname: %s"), strerror(errno));
	/*NOTREACHED*/
    }
    local_hostname[sizeof(local_hostname)-1] = '\0';

    /* now trim domain off name */
    s = local_hostname;
    ch = *s++;
    while(ch && ch != '.') ch = *s++;
    s[-1] = '\0';


    if(from_amandad == 0) {
	if(!amindexd_debug) {
	    /* who are we talking to? */
	    socklen = sizeof (his_addr);
	    if (getpeername(0, (struct sockaddr *)&his_addr, &socklen) == -1)
		error(_("getpeername: %s"), strerror(errno));

	    /* Try a reverse (IP->hostname) resolution, and fail if it does
	     * not work -- this is a basic security check */
	    if (getnameinfo((struct sockaddr *)&his_addr, SS_LEN(&his_addr),
			    his_hostname, sizeof(his_hostname),
			    NULL, 0,
			    0)) {
		error(_("getnameinfo(%s): hostname lookup failed"),
		      str_sockaddr(&his_addr));
		/*NOTREACHED*/
	    }
	}

	/* Set up the input and output FILEs */
	cmdout = stdout;
	cmdin = stdin;
    }
    else {
	/* read the REQ packet */
	for(; (line = pgets(stdin)) != NULL; free(line)) {
	    if(strncmp_const(line, "OPTIONS ") == 0) {
                if (g_options != NULL) {
		    dbprintf(_("REQ packet specified multiple OPTIONS.\n"));
                    free_g_options(g_options);
                }
		g_options = parse_g_options(line+8, 1);
		if(!g_options->hostname) {
		    g_options->hostname = g_malloc(MAX_HOSTNAME_LENGTH+1);
		    gethostname(g_options->hostname, MAX_HOSTNAME_LENGTH);
		    g_options->hostname[MAX_HOSTNAME_LENGTH] = '\0';
		}
	    }
	}
	amfree(line);

	if(amandad_auth && g_options->auth) {
	    if(strcasecmp(amandad_auth, g_options->auth) != 0) {
		g_printf(_("ERROR recover program ask for auth=%s while amindexd is configured for '%s'\n"),
		       g_options->auth, amandad_auth);
		error(_("amindexd: ERROR recover program ask for auth=%s while amindexd is configured for '%s'"),
		      g_options->auth, amandad_auth);
		/*NOTREACHED*/
	    }
	}
	/* send the REP packet */
	g_printf("CONNECT MESG %d\n", DATA_FD_OFFSET);
	g_printf("\n");
	fflush(stdout);
	fclose(stdin);
	fclose(stdout);

	cmdout = fdopen(DATA_FD_OFFSET + 0, "a");
	if (!cmdout) {
	    error(_("amindexd: Can't fdopen(%d): %s"), DATA_FD_OFFSET + 0, strerror(errno));
	    /*NOTREACHED*/
	}

	cmdin = fdopen(DATA_FD_OFFSET + 1, "r");
	if (!cmdin) {
	    error(_("amindexd: Can't fdopen(%d): %s"), DATA_FD_OFFSET + 1, strerror(errno));
	    /*NOTREACHED*/
	}
    }

    /* clear these so we can detect when the have not been set by the client */
    amfree(dump_hostname);
    amfree(qdisk_name);
    amfree(disk_name);
    amfree(target_date);

    our_features = am_init_feature_set();
    their_features = am_set_default_feature_set();

    if (cfg_opt != NULL && check_and_load_config(cfg_opt) != -1) { /* load the config */
	return 1;
    }

    reply(220, _("%s AMANDA index server (%s) ready."), local_hostname,
	  VERSION);

    user_validated = from_amandad;

    /* a real simple parser since there are only a few commands */
    while (1)
    {
	/* get a line from the client */
	while(1) {
	    if((part = pgets(cmdin)) == NULL) {
		if(errno != 0) {
		    dbprintf(_("? read error: %s\n"), strerror(errno));
		} else {
		    dbprintf(_("? unexpected EOF\n"));
		}
		if(line) {
		    dbprintf(_("? unprocessed input:\n"));
		    dbprintf("-----\n");
		    dbprintf("? %s\n", line);
		    dbprintf("-----\n");
		}
		amfree(line);
		amfree(part);
		amfree(arg);
		uncompress_remove = remove_files(uncompress_remove);
		compress_sorted_files = compress_files(compress_sorted_files);
		dbclose();
		return 1;		/* they hung up? */
	    }
	    strappend(line, part);	/* Macro: line can be null */
	    amfree(part);

	    if(amindexd_debug) {
		break;			/* we have a whole line */
	    }
	    if((len = strlen(line)) > 0 && line[len-1] == '\r') {
		line[len-1] = '\0';	/* zap the '\r' */
		break;
	    }
	    /*
	     * Hmmm.  We got a "line" from pgets(), which means it saw
	     * a '\n' (or EOF, etc), but there was not a '\r' before it.
	     * Put a '\n' back in the buffer and loop for more.
	     */
	    strappend(line, "\n");
	}

	dbprintf("> %s\n", line);

	if (arg != NULL)
	    amfree(arg);
	s = line;
	ch = *s++;

	skip_whitespace(s, ch);
	if(ch == '\0') {
	    reply(500, _("Command not recognised/incorrect: %s"), line);
	    amfree(line);
	    continue;
	}
	cmd = s - 1;

	skip_non_whitespace(s, ch);
	cmd_undo = s-1;				/* for error message */
	cmd_undo_ch = *cmd_undo;
	*cmd_undo = '\0';
	if (ch) {
	    skip_whitespace(s, ch);		/* find the argument */
	    if (ch) {
		arg = s-1;
		skip_quoted_string(s, ch);
		arg = unquote_string(arg);
	    }
	}

	amfree(errstr);
	if (!user_validated && g_str_equal(cmd, "SECURITY") && arg) {
	    user_validated = amindexd_debug ||
				check_security(
					(sockaddr_union *)&his_addr,
					arg, 0, &errstr, "amindexd");
	    if(user_validated) {
		reply(200, _("Access OK"));
		amfree(line);
		continue;
	    }
	}
	if (!user_validated) {  /* don't tell client the reason, just log it to debug log */
	    reply(500, _("Access not allowed"));
	    if (errstr) {
		dbprintf("%s\n", errstr);
		amfree(errstr);
	    }
	    break;
	}

	if (g_str_equal(cmd, "QUIT")) {
	    amfree(line);
	    break;
	} else if (g_str_equal(cmd, "HOST") && arg) {
	    am_host_t *lhost;
	    /* set host we are restoring */
	    s[-1] = '\0';
	    if ((lhost = is_dump_host_valid(arg)) != NULL) {
		GList  *dlist;
		disk_t *disk;
		gboolean allowed = FALSE;
		for (dlist = disk_list.head; dlist != NULL; dlist = dlist->next) {
		    disk = dlist->data;
		    if (g_str_equal(lhost->hostname, disk->host->hostname))
			allowed |= is_disk_allowed(disk);
		}
		if (allowed) {
		    g_free(dump_hostname);
		    dump_hostname = g_strdup(lhost->hostname);
		    reply(200, _("Dump host set to %s."), dump_hostname);
		    amfree(qdisk_name);		/* invalidate any value */
		    amfree(disk_name);		/* invalidate any value */
		} else {
		    reply(502, _("Can't recover hosts '%s'"), lhost->hostname);
		}
	    }
	    s[-1] = (char)ch;
	} else if (g_str_equal(cmd, "LISTHOST")) {
	    GList  *dlist, *dlistup;
	    disk_t *disk,
                   *diskdup;
	    int nbhost = 0,
                found = 0;
	    s[-1] = '\0';
	    if (get_config_name() == NULL) {
		reply(501, _("Must set config before listhost"));
	    } else {
		lreply(200, _(" List hosts for config %s"), get_config_name());
		for (dlist = disk_list.head; dlist != NULL; dlist = dlist->next) {
		    disk = dlist->data;
		    if (is_disk_allowed(disk)) {
			found = 0;
			for (dlistup = disk_list.head; dlistup->data != dlist->data; dlistup = dlistup->next) {
			    diskdup = dlistup->data;
			    if (g_str_equal(diskdup->host->hostname,
					    disk->host->hostname) &&
				is_disk_allowed(diskdup)) {
				found = 1;
				break;
			    }
			}
			if (!found) {
			    fast_lreply(201, " %s", disk->host->hostname);
			    nbhost++;
			}
		    }
		}
		if(nbhost > 0) {
		    reply(200, _(" List hosts for config %s"), get_config_name());
		}
		else {
		    reply(200, _("No hosts for config %s"), get_config_name());
		}
	    }
	    s[-1] = (char)ch;
	} else if (g_str_equal(cmd, "DISK") && arg) {
	    s[-1] = '\0';
	    if (is_disk_valid(arg) != -1) {
		g_free(disk_name);
		disk_name = g_strdup(arg);
		amfree(qdisk_name);
		qdisk_name = quote_string(disk_name);
		if (build_disk_table() != -1) {
		    reply(200, _("Disk set to %s."), qdisk_name);
		}
	    }
	    s[-1] = (char)ch;
	} else if (g_str_equal(cmd, "DLE")) {

	    if (!dump_hostname || !disk_name) {
		reply(200, "NODLE");
	    } else {
                disk_t *dp = lookup_disk(dump_hostname, disk_name);
                if (dp->line == 0) {
                    reply(200, "NODLE");
                } else {
                    char *b64disk;
                    gchar **errors;

                    b64disk = amxml_format_tag("disk", dp->name);
                    dp->host->features = their_features;

                    errors = validate_optionstr(dp);

                    if (errors) {
                        gchar **ptr;
                        for (ptr = errors; *ptr; ptr++)
                            g_debug("ERROR: %s:%s %s", dump_hostname, disk_name,
                                *ptr);
                        g_strfreev(errors);
                        reply(200, "NODLE");
                    } else {
                        GString *strbuf = g_string_new("<dle>\n");
                        char *l, *ql;
                        char *optionstr;

                        g_string_append_printf(strbuf,
                            "  <program>%s</program>\n", dp->program);

                        if (dp->application) {
                            application_t *application;
                            char *xml_app;

                            application = lookup_application(dp->application);
                            g_assert(application != NULL);
                            xml_app = xml_application(dp, application,
                                                      their_features);
                            g_string_append(strbuf, xml_app);
                            g_free(xml_app);
                        }

                        g_string_append_printf(strbuf, "  %s\n", b64disk);

                        if (dp->device) {
                            char *b64device = amxml_format_tag("diskdevice",
                                                               dp->device);
                            g_string_append_printf(strbuf, "  %s\n", b64device);
                            g_free(b64device);
                        }

                        optionstr = xml_optionstr(dp, 0);
                        g_string_append_printf(strbuf, "%s</dle>\n",
                            optionstr);
                        g_free(optionstr);

                        l = g_string_free(strbuf, FALSE);
                        ql = quote_string(l);
                        g_free(l);

                        reply(200, "%s", ql);
                        g_free(ql);
                    }
                    g_free(b64disk);
                }
	    }
	} else if (g_str_equal(cmd, "LISTDISK")) {
	    char *qname;
	    GList  *dlist;
	    disk_t *disk;
	    int nbdisk = 0;
	    s[-1] = '\0';
	    if (get_config_name() == NULL) {
		reply(501, _("Must set config, host before listdisk"));
	    }
	    else if (dump_hostname == NULL) {
		reply(501, _("Must set host before listdisk"));
	    }
	    else if(arg) {
		lreply(200, _(" List of disk for device %s on host %s"), arg,
		       dump_hostname);
		for (dlist = disk_list.head; dlist != NULL; dlist = dlist->next) {
		    disk = dlist->data;

		    if (strcasecmp(disk->host->hostname, dump_hostname) == 0 &&
		      ((disk->device && g_str_equal(disk->device, arg)) ||
		      (!disk->device && g_str_equal(disk->name, arg)))) {
			qname = quote_string(disk->name);
			fast_lreply(201, " %s", qname);
			amfree(qname);
			nbdisk++;
		    }
		}
		if(nbdisk > 0) {
		    reply(200, _("List of disk for device %s on host %s"), arg,
			  dump_hostname);
		}
		else {
		    reply(200, _("No disk for device %s on host %s"), arg,
			  dump_hostname);
		}
	    }
	    else {
		lreply(200, _(" List of disk for host %s"), dump_hostname);
		for (dlist = disk_list.head; dlist != NULL; dlist = dlist->next) {
		    disk = dlist->data;
		    if(strcasecmp(disk->host->hostname, dump_hostname) == 0) {
			qname = quote_string(disk->name);
			fast_lreply(201, " %s", qname);
			amfree(qname);
			nbdisk++;
		    }
		}
		if(nbdisk > 0) {
		    reply(200, _("List of disk for host %s"), dump_hostname);
		}
		else {
		    reply(200, _("No disk for host %s"), dump_hostname);
		}
	    }
	    s[-1] = (char)ch;
	} else if (g_str_equal(cmd, "SCNF") && arg) {
	    s[-1] = '\0';
	    if (check_and_load_config(arg) != -1) {    /* try to load the new config */
		amfree(dump_hostname);		/* invalidate any value */
		amfree(qdisk_name);		/* invalidate any value */
		amfree(disk_name);		/* invalidate any value */
		reply(200, _("Config set to %s."), get_config_name());
	    } /* check_and_load_config replies with any failure messages */
	    s[-1] = (char)ch;
	} else if (g_str_equal(cmd, "FEATURES") && arg) {
	    char *our_feature_string = NULL;
	    char *their_feature_string = NULL;
	    s[-1] = '\0';
	    am_release_feature_set(our_features);
	    am_release_feature_set(their_features);
	    our_features = am_init_feature_set();
	    our_feature_string = am_feature_to_string(our_features);
	    g_free(their_feature_string);
	    their_feature_string = g_strdup(arg);
	    their_features = am_string_to_feature(their_feature_string);
	    if (!their_features) {
		g_warning("Invalid client feature set '%s'", their_feature_string);
		their_features = am_set_default_feature_set();
	    }
	    reply(200, "FEATURES %s", our_feature_string);
	    amfree(our_feature_string);
	    amfree(their_feature_string);
	    s[-1] = (char)ch;
	} else if (g_str_equal(cmd, "DATE") && arg) {
	    s[-1] = '\0';
	    g_free(target_date);
	    target_date = g_strdup(arg);
	    reply(200, _("Working date set to %s."), target_date);
	    s[-1] = (char)ch;
	} else if (g_str_equal(cmd, "DHST")) {
	    (void)disk_history_list();
	} else if (g_str_equal(cmd, "OISD") && arg) {
	    if (is_dir_valid_opaque(arg) != -1) {
		reply(200, _("\"%s\" is a valid directory"), arg);
	    }
	} else if (g_str_equal(cmd, "OLSD") && arg) {
	    (void)opaque_ls(arg,0);
	} else if (g_str_equal(cmd, "ORLD") && arg) {
	    (void)opaque_ls(arg, 1);
	} else if (g_str_equal(cmd, "TAPE")) {
	    (void)tapedev_is();
	} else if (g_str_equal(cmd, "DCMP")) {
	    (void)are_dumps_compressed();
	} else if (g_str_equal(cmd, "STORAGE")) {
	    s[-1] = '\0';
	    g_strfreev(storage_list);
	    if (arg && *arg != '\0') {
		char **storage_l;
		char **storage_n;
		char *invalid_storage = NULL;

		storage_list = split_quoted_strings(arg);
		storage_n = storage_list;
		for (storage_l = storage_list; *storage_l != NULL; storage_l++) {
		    if (g_str_equal(*storage_l, "HOLDING") ||
			lookup_storage(*storage_l)) {
			*storage_n = *storage_l;
			storage_n++;
		    } else {
			char *qstorage = quote_string(*storage_l);
			if (invalid_storage) {
			    char *new_invalid_storage = g_strconcat(invalid_storage, " ", qstorage, NULL);
			    g_free(invalid_storage);
			    invalid_storage = new_invalid_storage;
			    g_free(qstorage);
			} else {
			    invalid_storage = qstorage;
			}
			g_free(*storage_l);
			*storage_l = NULL;
		    }
		}
		if (invalid_storage) {
		    reply(599, _("invalid storage: %s"), invalid_storage);
		    g_free(invalid_storage);
		    invalid_storage = NULL;
		} else {
		    reply(200, _("storage set to %s"), arg);
		}
	    } else {
		storage_list = NULL;
		reply(200, _("storage unset"));
	    }
	    sort_find_result_with_storage("DLKHspwB", storage_list, &output_find);
	    build_disk_table();
	    s[-1] = (char)ch;
	} else {
	    *cmd_undo = cmd_undo_ch;	/* restore the command line */
	    reply(500, _("Command not recognised/incorrect: %s"), cmd);
	}
	amfree(line);
    }
    amfree(arg);
    amfree(line);

    uncompress_remove = remove_files(uncompress_remove);
    compress_sorted_files = compress_files(compress_sorted_files);

    /* remove previous lock */
    if (lock_index) {
	file_lock_unlock(lock_index);
	file_lock_free(lock_index);
	lock_index = NULL;
    }

    free_find_result(&output_find);
    reply(200, _("Good bye."));
    dbclose();
    return 0;
}

static char *
amindexd_nicedate(
    char *	datestamp)
{
    static char nice[20];
    int year, month, day;
    int hours, minutes, seconds;
    char date[9], atime[7];
    int  numdate, numtime;

    strncpy(date, datestamp, 8);
    date[8] = '\0';
    numdate = atoi(date);
    year  = numdate / 10000;
    month = (numdate / 100) % 100;
    day   = numdate % 100;

    if(strlen(datestamp) <= 8) {
	g_snprintf(nice, sizeof(nice), "%4d-%02d-%02d",
		year, month, day);
    }
    else {
	strncpy(atime, &(datestamp[8]), 6);
	atime[6] = '\0';
	numtime = atoi(atime);
	hours = numtime / 10000;
	minutes = (numtime / 100) % 100;
	seconds = numtime % 100;

	g_snprintf(nice, sizeof(nice), "%4d-%02d-%02d-%02d-%02d-%02d",
		year, month, day, hours, minutes, seconds);
    }

    return nice;
}

static int
cmp_date(
    const char *	date1,
    const char *	date2)
{
    return strncmp(date1, date2, strlen(date2));
}

static int
get_index_dir(
    char *dump_hostname,
    char *hostname,
    char *diskname)
{
    struct stat  dir_stat;
    char        *fn;
    char        *s;
    char        *lower_hostname;

    lower_hostname = g_strdup(dump_hostname);
    for(s=lower_hostname; *s != '\0'; s++)
	*s = tolower(*s);

    fn = getindexfname(dump_hostname, diskname, NULL, 0);
    if (stat(fn, &dir_stat) == 0 && S_ISDIR(dir_stat.st_mode)) {
	amfree(lower_hostname);
	amfree(fn);
	return 1;
    }
    amfree(fn);
    if (hostname != NULL) {
	fn = getindexfname(hostname, diskname, NULL, 0);
	if (stat(fn, &dir_stat) == 0 && S_ISDIR(dir_stat.st_mode)) {
	    amfree(lower_hostname);
	    amfree(fn);
	    return 1;
	}
    }
    amfree(fn);
    fn = getindexfname(lower_hostname, diskname, NULL, 0);
    if (stat(fn, &dir_stat) == 0 && S_ISDIR(dir_stat.st_mode)) {
	amfree(lower_hostname);
	amfree(fn);
	return 1;
    }
    amfree(fn);
    if(diskname != NULL) {
	fn = getoldindexfname(dump_hostname, diskname, NULL, 0);
	if (stat(fn, &dir_stat) == 0 && S_ISDIR(dir_stat.st_mode)) {
	    amfree(lower_hostname);
	    amfree(fn);
	    return 1;
	}
	amfree(fn);
	if (hostname != NULL) {
	    fn = getoldindexfname(hostname, diskname, NULL, 0);
	    if (stat(fn, &dir_stat) == 0 && S_ISDIR(dir_stat.st_mode)) {
		amfree(lower_hostname);
		amfree(fn);
		return 1;
	    }
	}
	amfree(fn);
	fn = getoldindexfname(lower_hostname, diskname, NULL, 0);
	if (stat(fn, &dir_stat) == 0 && S_ISDIR(dir_stat.st_mode)) {
	    amfree(lower_hostname);
	    amfree(fn);
	    return 1;
	}
	amfree(fn);
    }
    amfree(lower_hostname);
    return -1;
}

static char *
get_index_name(
    char       *dump_hostname,
    char       *hostname,
    char       *diskname,
    char       *timestamps,
    int         level,
    GPtrArray **emsg)
{
    struct stat  dir_stat;
    char        *fn;
    char        *s;
    char        *lower_hostname;
    gboolean     need_uncompress = FALSE;
    gboolean     need_sort = FALSE;

    lower_hostname = g_strdup(dump_hostname);
    for(s=lower_hostname; *s != '\0'; s++)
	*s = tolower(*s);

    fn = getindex_sorted_fname(dump_hostname, diskname, timestamps, level);
    if (stat(fn, &dir_stat) == 0 && S_ISREG(dir_stat.st_mode)) {
	need_uncompress = FALSE;
	need_sort = FALSE;
	goto process_dump;
    }
    amfree(fn);

    fn = getindex_sorted_gz_fname(dump_hostname, diskname, timestamps, level);
    if (stat(fn, &dir_stat) == 0 && S_ISREG(dir_stat.st_mode)) {
	need_uncompress = TRUE;
	need_sort = FALSE;
	goto process_dump;
    }
    amfree(fn);

    fn = getindex_unsorted_fname(dump_hostname, diskname, timestamps, level);
    if (stat(fn, &dir_stat) == 0 && S_ISREG(dir_stat.st_mode)) {
	need_uncompress = FALSE;
	need_sort = TRUE;
	goto process_dump;
    }
    amfree(fn);

    fn = getindex_unsorted_gz_fname(dump_hostname, diskname, timestamps, level);
    if (stat(fn, &dir_stat) == 0 && S_ISREG(dir_stat.st_mode)) {
	need_uncompress = TRUE;
	need_sort = TRUE;
	goto process_dump;
    }
    amfree(fn);

    fn = getindexfname(dump_hostname, diskname, timestamps, level);
    if (stat(fn, &dir_stat) == 0 && S_ISREG(dir_stat.st_mode)) {
	need_uncompress = TRUE;
	need_sort = TRUE;
	goto process_dump;
    }
    amfree(fn);
    if(hostname != NULL) {
	fn = getindexfname(hostname, diskname, timestamps, level);
	if (stat(fn, &dir_stat) == 0 && S_ISREG(dir_stat.st_mode)) {
	    need_uncompress = TRUE;
	    need_sort = TRUE;
	    goto process_dump;
	}
	amfree(fn);
    }
    amfree(fn);
    fn = getindexfname(lower_hostname, diskname, timestamps, level);
    if (stat(fn, &dir_stat) == 0 && S_ISREG(dir_stat.st_mode)) {
	need_uncompress = TRUE;
	need_sort = TRUE;
	goto process_dump;
    }
    amfree(fn);
    if(diskname != NULL) {
	fn = getoldindexfname(dump_hostname, diskname, timestamps, level);
	if (stat(fn, &dir_stat) == 0 && S_ISREG(dir_stat.st_mode)) {
	    need_uncompress = TRUE;
	    need_sort = TRUE;
	    goto process_dump;
	}
	amfree(fn);
	if(hostname != NULL) {
	    fn = getoldindexfname(hostname, diskname, timestamps, level);
	    if (stat(fn, &dir_stat) == 0 && S_ISREG(dir_stat.st_mode)) {
		need_uncompress = TRUE;
		need_sort = TRUE;
		goto process_dump;
	    }
	}
	amfree(fn);
	fn = getoldindexfname(lower_hostname, diskname, timestamps, level);
	if (stat(fn, &dir_stat) == 0 && S_ISREG(dir_stat.st_mode)) {
	    need_uncompress = TRUE;
	    need_sort = TRUE;
	    goto process_dump;
	}
	amfree(fn);
    }
    amfree(lower_hostname);
    g_ptr_array_add(*emsg, g_strdup_printf(_("index file not found for host '%s' disk '%s' level '%d' date '%s'"), dump_hostname, diskname, level, timestamps));
    return NULL;

process_dump:
    amfree(lower_hostname);
    return uncompress_file(hostname, diskname, timestamps, level,
			   fn, emsg, need_uncompress, need_sort);
}

static char *
uncompress_file(
    char       *hostname,
    char       *diskname,
    char       *timestamps,
    int         level,
    char       *filename,
    GPtrArray **emsg,
    gboolean    need_uncompress,
    gboolean    need_sort)
{
    char *cmd = NULL;
    char *new_filename = NULL;
    struct stat stat_filename;
    int result;
    int pipe_from_gzip;
    int pipe_to_sort;
    int indexfd;
    int nullfd;
    int uncompress_errfd;
    int sort_errfd;
    char line[STR_SIZE];
    FILE *pipe_stream;
    pid_t pid_gzip = 0;
    pid_t pid_sort = 0;
    pid_t pid_index = 0;
    int        status;
    char      *msg;
    gpointer  *p;
    gpointer  *p_last;
    GPtrArray *uncompress_err = NULL;
    GPtrArray *sort_err = NULL;
    FILE      *uncompress_err_stream;
    FILE      *sort_err_stream;

    new_filename = getindex_unsorted_fname(hostname, diskname, timestamps, level);

    /* uncompress the file */
    result = stat(filename, &stat_filename);
    if (result == -1) {
	msg = g_strdup_printf(_("Source index file '%s' is inaccessible: %s"),
			     filename, strerror(errno));
	dbprintf("%s\n", msg);
	g_ptr_array_add(*emsg, msg);
	amfree(filename);
	return NULL;
    } else if(!S_ISREG((stat_filename.st_mode))) {
	    msg = g_strdup_printf(_("\"%s\" is not a regular file"), filename);
	    dbprintf("%s\n", msg);
	    g_ptr_array_add(*emsg, msg);
	    errno = -1;
	    amfree(filename);
	    amfree(cmd);
	    return NULL;
    }

    if (g_str_equal(filename, new_filename)) {
	return new_filename;
    }
    if (!need_uncompress && !need_sort) {
	return new_filename;
    }

#ifdef UNCOMPRESS_OPT
#  define PARAM_UNCOMPRESS_OPT UNCOMPRESS_OPT
#else
#  define PARAM_UNCOMPRESS_OPT skip_argument
#endif

    indexfd = open(new_filename, O_WRONLY|O_CREAT, 0600);
    if (indexfd == -1) {
	msg = g_strdup_printf(_("Can't open '%s' for writing: %s"),
			      filename, strerror(errno));
	dbprintf("%s\n", msg);
	g_ptr_array_add(*emsg, msg);
	amfree(filename);
	return NULL;
    }

    if (need_uncompress) {
	int  pipedef;
	int *out_fd;

	/* just use our stderr directly for the pipe's stderr; in
	 * main() we send stderr to the debug file, or /dev/null
	 * if debugging is disabled */

	/* start the uncompress process */
	putenv(g_strdup("LC_ALL=C"));
	nullfd = open("/dev/null", O_RDONLY);

	if (need_sort) {
	    pipedef = STDOUT_PIPE|STDERR_PIPE;
	    out_fd = &pipe_from_gzip;
	} else {
	    pipedef = STDERR_PIPE;
	    out_fd = &indexfd;
	}
	pid_gzip = pipespawn(UNCOMPRESS_PATH, pipedef, 0,
			     &nullfd, out_fd, &uncompress_errfd,
			     UNCOMPRESS_PATH, PARAM_UNCOMPRESS_OPT,
			     filename, NULL);
	aclose(nullfd);

	if (need_sort) {
	    pipe_stream = fdopen(pipe_from_gzip, "r");
	    if (pipe_stream == NULL) {
		msg = g_strdup_printf(_("Can't fdopen pipe from gzip: %s"),
				      strerror(errno));
		dbprintf("%s\n", msg);
		g_ptr_array_add(*emsg, msg);
		amfree(filename);
		aclose(indexfd);
		return NULL;
	    }
	} else {
	    aclose(indexfd);
	}
    } else {
	pipe_stream = fopen(filename, "r");
	if (pipe_stream == NULL) {
	    msg = g_strdup_printf(_("Can't fopen index file (%s): %s"),
			     filename, strerror(errno));
	    dbprintf("%s\n", msg);
	    g_ptr_array_add(*emsg, msg);
	    amfree(filename);
	    aclose(indexfd);
	    return NULL;
	}
    }

    if (need_sort) {
	/* start the sort process */
	putenv(g_strdup("LC_ALL=C"));

	if (getconf_seen(CNF_TMPDIR)) {
	    gchar *tmpdir = getconf_str(CNF_TMPDIR);
	    pid_sort = pipespawn(SORT_PATH, STDIN_PIPE|STDERR_PIPE, 0,
				 &pipe_to_sort, &indexfd, &sort_errfd,
				 SORT_PATH, "-T", tmpdir, NULL);
	} else {
	    pid_sort = pipespawn(SORT_PATH, STDIN_PIPE|STDERR_PIPE, 0,
				 &pipe_to_sort, &indexfd, &sort_errfd,
				 SORT_PATH, NULL);
	}
	aclose(indexfd);

	/* start a subprocess */
	/* send all ouput from uncompress process to sort process */
	pid_index = fork();
	switch (pid_index) {
	case -1:
	    msg = g_strdup_printf(
			_("fork error: %s"),
			strerror(errno));
	    dbprintf("%s\n", msg);
	    g_ptr_array_add(*emsg, msg);
	    unlink(filename);
	    amfree(filename);
	    break;
	default: break;
	case 0:
	    while (fgets(line, STR_SIZE, pipe_stream) != NULL) {
		if (line[0] != '\0') {
		    if (strchr(line,'/')) {
			full_write(pipe_to_sort,line,strlen(line));
		    }
		}
	    }
	    exit(0);
	}

	fclose(pipe_stream);
	aclose(pipe_to_sort);
    }

    if (need_uncompress) {
	uncompress_err_stream = fdopen(uncompress_errfd, "r");
	uncompress_err = g_ptr_array_new();
	if (!uncompress_err_stream) {
	    g_ptr_array_add(uncompress_err,
		    g_strdup_printf("Can't fdopen uncompress_err_stream: %s\n",
				    strerror(errno)));
	} else {
	    while (fgets(line, sizeof(line), uncompress_err_stream) != NULL) {
		if (strlen(line) > 0 && line[strlen(line)-1] == '\n')
		    line[strlen(line)-1] = '\0';
		g_ptr_array_add(uncompress_err, g_strdup_printf("  %s", line));
		dbprintf("Uncompress: %s\n", line);
	    }
	    fclose(uncompress_err_stream);
	}
    }

    if (need_sort) {
	sort_err_stream = fdopen(sort_errfd, "r");
	sort_err = g_ptr_array_new();
	if (!sort_err_stream) {
	    g_ptr_array_add(sort_err,
		    g_strdup_printf("Can't fdopen sort_err_stream: %s\n",
				    strerror(errno)));
	} else {
	    while (fgets(line, sizeof(line), sort_err_stream) != NULL) {
		if (strlen(line) > 0 && line[strlen(line)-1] == '\n')
		    line[strlen(line)-1] = '\0';
		g_ptr_array_add(sort_err, g_strdup_printf("  %s", line));
		dbprintf("Sort: %s\n", line);
	    }
	    fclose(sort_err_stream);
	}
    }

    if (need_uncompress) {
	status = get_pid_status(pid_gzip, UNCOMPRESS_PATH, emsg);
	if (status == 0 && filename) {
	    unlink(filename);
	    amfree(filename);
	}
	if (uncompress_err->len > 0) {
	    p_last = uncompress_err->pdata + uncompress_err->len;
	    for (p = uncompress_err->pdata; p < p_last ;p++) {
		g_ptr_array_add(*emsg, (char *)*p);
	    }
	}
	g_ptr_array_free(uncompress_err, TRUE);
    }

    if (need_sort) {
	status = get_pid_status(pid_index, "index", emsg);
	if (status == 0 && filename) {
	    unlink(filename);
	    amfree(filename);
	}

	status = get_pid_status(pid_sort, SORT_PATH, emsg);
	if (status == 0 && filename) {
	    unlink(filename);
	    amfree(filename);
	}
	if (sort_err->len > 0) {
	    p_last = sort_err->pdata + sort_err->len;
	    for (p = sort_err->pdata; p < p_last ;p++) {
		g_ptr_array_add(*emsg, (char *)*p);
	    }
	}
	g_ptr_array_free(sort_err, TRUE);
    }

    if (need_sort && new_filename && getconf_boolean(CNF_COMPRESS_INDEX)) {
	/* add at beginning */
	REMOVE_ITEM *compress_file;
	compress_file = (REMOVE_ITEM *)g_malloc(sizeof(REMOVE_ITEM));
	compress_file->filename = g_strdup(new_filename);
	compress_file->next = uncompress_remove;
	compress_sorted_files = compress_file;
    } else if (need_uncompress && new_filename && !getconf_boolean(CNF_COMPRESS_INDEX)) {
	/* add at beginning */
	REMOVE_ITEM *remove_file;
	remove_file = (REMOVE_ITEM *)g_malloc(sizeof(REMOVE_ITEM));
	remove_file->filename = g_strdup(new_filename);
	remove_file->next = uncompress_remove;
	uncompress_remove = remove_file;
    }

    amfree(cmd);
    return new_filename;
}

