/* mirrirdir.c - duplicate two directories to every detail
   This has nothing to do with cryptography.
   Copyright (C) 1998 Paul Sheer

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
   02111-1307, USA.
 */

#define _MIRROR_DIR_C 1

#include "mostincludes.h"
#include <stdarg.h>
#include <signal.h>
#include "mirrordir.h"
#include "exclude.h"
#include "diffie/parse.h"
#include "vfs/vfs.h"
#include "cmdlineopt.h"
#include "netrc.h"
#include "sys/socket.h"
#include "zlib/zlib.h"
#include "diffie/diffie-socket.h"
#include "diffie/z-socket.h"
#include "diffie/field.h"
#include "diffie/arc.h"
#include "diffie/compat.h"
#include "mad.h"

#ifdef HAVE_GETTIMEOFDAY
#define NICE_MESS "[--nice <num>]"
#else
#define NICE_MESS " "
#endif

#define UNDELETABLE_FILE "--keep-me"

#ifdef WIN32
#define SCRIPT_HOME "ftp:\\\\encrypt.obsidian.co.za\\pub\\mirrordir\\etc"
#else
#define SCRIPT_HOME "ftp://encrypt.obsidian.co.za/pub/mirrordir"
#endif

void tar_init (char *tar_file_name);
void tar_shut (void);
int tar_append (char *file, char *arch_name, struct stat *s);


char *control_dir = PATH_SEP_STR;
char *mirror_dir = 0;
char **control_files = 0;
char *progname;

int interupted = 0;
int terminate = 0;

pid_t my_pid = 0;

/* options - see usage */
int verbose = 0;
int access_times = 0;
int always_write = 0;
int only_erase = 0;
int rest_access = 0;
#ifndef WIN32
#ifdef HAVE_GETTIMEOFDAY
int nice_to_cpu = 0;
#endif
#endif
int strict_locking = 0;
int strict_mtimes = 0;
int mtime_threshold = 0;
int no_mtimes = 0;
int dont_remove = 0;
int only_test = 0;
int handle_hardlinks = 1;
int erase_directories = 1;
int show_help = 0;
int show_version = 0;
char *backup_extension = 0;
int backup_howmany = 0;
int copy_mode = 0;
int recurs_mode = 0;
int secure_login = 0;
int allow_chmod = 1;
int allow_chown = 1;
int allow_netrc = 1; /*EK*/
int ignore_size = 0;
int secure = 0;
int follow_symlinks = 0;
int key_size = 512;
time_t time_offset = 0;
extern int ftpfs_option_allow_empty_directories;
extern int ftpfs_use_passive_connections;
int download_scripts = 0;
int tar_output = 0;
char *tar_file_name = 0;
extern int tar_block_size;
int compress_mode = 0;
int gzip_backups = 0;
int case_insensitive_filenames = 0;
int files_to_lower = 0;
int files_to_upper = 0;
int test_login = 0;
int read_password_from_stdin = 0;
int no_warn_first_login = 0;
extern int ftpfs_literal_password;

char *ftpfs_password = 0;
extern char *ftpfs_proxy_host;
unsigned long allow_permissions_mask = 0xFFFFFFFFUL;
time_t outdate_time = 0;

char **control_mirror_directories = 0;

void *free_me[256];
unsigned char free_num = 0;

/* internal errno */
#define DIRECTORY_NOT_CREATED 1
int mirrordir_errno = 0;

/* updated constantly */
time_t current_time;

char *start_file = 0;

int (*my_stat) (char *, struct stat *) = 0;

#define UNITS_BYTES	'B'
#define UNITS_KILO	'K'
#define UNITS_MEGA	'M'
#define UNITS_GIGA	'G'
int sum_units;

unsigned long block_size = 0;

unsigned long total_blocks = 0;
unsigned long blocks_written = 0;
unsigned long blocks_allowed = 0;

#define BLOCK_NOT_USED			-1
#define BLOCK_BEFORE_START		0
#define BLOCK_IN_RANGE			1
#define BLOCK_FULL			2

int block_pos = BLOCK_NOT_USED;


int real_bad_error = 0;
int ignore_next_exclude = 0;

struct exclude_list {
    char *name;
#define EXCLUDE_REGEXP		1
#define EXCLUDE_GLOB		2
#define EXCLUDE_LITERAL		3
#define EXCLUDE_FILELIST	4
#define EXCLUDE_FUNCTION	5
    int type;
    int ignore;			/* file/path not to be excluded, but to be overlooked completely */
    regex_t regexp;
    FastSearch filelist;
    void *compiled_script;
    Value *heap;
    char *c_script;
    struct exclude_list *next;
    struct exclude_list *prev;
} *exclude_list = 0;

struct directory_entry {
    struct stat stat;
    char *name;
};

#ifndef HAVE_STRDUP
char *strdup (const char *s)
{
    char *p;
    p = malloc (strlen (s) + 1);
    if (!p)
	return 0;
    strcpy (p, s);
    return p;
}
#endif

/* {{{ reporting */

void progmess (char *s, char *par)
{
    fprintf (stderr, "%s: %s: %s\n", progname, s, par);
}

void verbmess (char *s, char *par)
{
    if (par)
	printf ("%s: ---verbose--- %s: %s\n", progname, s, par);
    else
	printf ("%s: ---verbose--- %s\n", progname, s);
}

void infomess (char *s, char *par)
{
    printf ("%s: %s: %s\n", progname, s, par);
}

void progmess_strerror (char *s, char *p)
{
    real_bad_error++;
#ifdef HAVE_STRERROR
    fprintf (stderr, "%s: %s: %s: %s\n", progname, s, p, strerror (mc_errno));
#else
    fprintf (stderr, "%s: %s: %s: %d\n", progname, s, p, mc_errno);
#endif
}

/* }}} reporting */

static inline int my_strcmp (char *p, char *q)
{
    if (case_insensitive_filenames)
	return strcasecmp (p, q);
    return strcmp (p, q);
}

static inline int my_strncmp (char *p, char *q, size_t n)
{
    if (case_insensitive_filenames)
	return strncasecmp (p, q, n);
    return strncmp (p, q, n);
}

#ifdef WIN32

void be_nice_to_cpu (void)
{
    return;
}

#else

void be_nice_to_cpu (void)
{
#ifdef HAVE_GETTIMEOFDAY
    static long double s1 = 0.0, s2;
    struct timeval t;

    if (!nice_to_cpu)
	return;

    gettimeofday (&t, 0);
    s2 = (long double) t.tv_usec / 1000000.0 + (long double) t.tv_sec;

    if (s1 > 0.0) {
	s2 = (long double) (s2 - s1) * 1000000.0 * (long double) nice_to_cpu;
	if (verbose > 1) {
	    char s[12];
	    sprintf (s, "%d", (int) s2);
	    verbmess ("sleeping microseconds", s);
	}
	usleep ((unsigned long) s2);
    }
    gettimeofday (&t, 0);
    s1 = (long double) t.tv_usec / 1000000.0 + (long double) t.tv_sec;
#endif
}

#endif

/* result must be free'd, path of "" is treated as PATH_SEP_STR */
char *join (char *path, char *file)
{
    char *r;
    int l;

    if (!*path)
	path = PATH_SEP_STR;
    while (*file == PATH_SEP)
	file++;

    l = strlen (path);

    r = malloc (l + strlen (file) + 2);
    strcpy (r, path);
    if (path[l - 1] != PATH_SEP)
	strcat (r, PATH_SEP_STR);
    strcat (r, file);
    return r;
}

void check_interrupt (void);

void check_keep_me (char *name)
{
    if (!*name)
	return;
    if (!my_strcmp (name, UNDELETABLE_FILE) || !my_strcmp (name + 1, UNDELETABLE_FILE)) {
	progmess ("aborting on finding a directory containing an undeletable file", name);
	exit (1);
    }
}


#ifdef WIN32
struct directory_list *vomit_read_list (char *path)
{
    struct directory_list *dl, *next;
    struct _finddata_t find;
    char *p;
    int h;
    p = malloc (strlen (path) + 10);
    strcpy (p, path);
    strcat (p, "\\*");
    h = _findfirst (p, &find);
    if (h < 0) {
	progmess_strerror ("unable to open directory", path);
	free (p);
	return 0;
    }
/* want at least one entry */
    dl = malloc (sizeof (struct directory_list));
    memset (dl, 0, sizeof (struct directory_list));
    dl->name = 0;
    do {
	check_interrupt ();
	next = malloc (sizeof (struct directory_list));
	memset (next, 0, sizeof (struct directory_list));
	next->prev = dl;
	if (dl)
	    dl->next = next;
	dl = next;
	dl->name = strdup (find.name);
	if (verbose > 1)
	    verbmess ("\tdirectory read", dl->name);
	if (!strcmp ("..", dl->name) || !strcmp (".", dl->name)) {
	    free (dl->name);
	    dl->name = 0;
	} else {
	    int a;
	    char *p;
	    time_t last_year;
	    p = join (path, dl->name);
	    dl->stat.st_atime = find.time_access;
	    dl->stat.st_mtime = find.time_write;
	    dl->stat.st_ctime = find.time_create;
	    dl->stat.st_mode = 0;
	    a = GetFileAttributes (p);
	    if (a < 0) {
		verbmess ("Could not get file attributes - winging it", p);
		dl->stat.st_mode |= S_IRWXU;
		dl->stat.st_mode |= (find.attrib & _A_SUBDIR) ? S_IFDIR : S_IFREG;
		if (find.attrib & _A_RDONLY)
		    dl->stat.st_mode &= ~S_IWUSR;
	    } else {
		dl->stat.st_mode = mode_mingw32_to_unix (a);
	    }
	    free (p);
	    dl->stat.st_size = find.size;
	    last_year = current_time - 365 * 24 * 60 * 60;
	    if (S_ISDIR (dl->stat.st_mode)) {
		dl->stat.st_atime = last_year;
		dl->stat.st_mtime = last_year;
		dl->stat.st_ctime = last_year;
	    } else {
		if (!dl->stat.st_atime)
		    dl->stat.st_atime = last_year;
		if (!dl->stat.st_mtime)
		    dl->stat.st_mtime = last_year;
		if (!dl->stat.st_ctime)
		    dl->stat.st_ctime = last_year;
	    }
	}
    } while (_findnext (h, &find) >= 0);
    _findclose (h);
    rewind (dl);
    free (p);
    return dl;
}
#endif


int is_special_prefix (char *path);

#ifdef HAVE_MAD
struct directory_list *mad_read_list (char *path, char *file, int line)
#define read_list(x) mad_read_list(x,__FILE__,__LINE__)
#else
/* returns linked list of directory entries */
struct directory_list *read_list (char *path)
#endif
{
    struct directory_list *dl = 0, *next;
    DIR *dir;
    int stat_with_readdir = 0;
    struct dirent *r;
    time_t sub_time = 0;

    if (is_special_prefix (path)) {
	sub_time = time_offset;
    }
#ifdef WIN32
    else {
	return vomit_read_list (path);
    }
#endif

    dir = mc_opendir (path);

    if (!strncmp (path, "mc:" PATH_SEP_STR PATH_SEP_STR, 5))
	stat_with_readdir = 1;
    if (!dir) {
	progmess_strerror ("unable to open directory", path);
	return 0;
    }

#ifdef HAVE_MAD
    dl = mad_alloc (sizeof (struct directory_list), file, line);
#else
    dl = malloc (sizeof (struct directory_list));
#endif
    memset (dl, 0, sizeof (struct directory_list));
    dl->name = (char *) strdup ("..");

    do {
	int l;
	r = mc_readdir (dir);
	check_interrupt ();
#ifdef HAVE_MAD
	next = mad_alloc (sizeof (struct directory_list), file, line);
#else
	next = malloc (sizeof (struct directory_list));
#endif
	memset (next, 0, sizeof (struct directory_list));
	next->prev = dl;
	if (dl)
	    dl->next = next;
	dl = next;
	if (r) {
	    l = NAMLEN (r);
	    dl->name = malloc (l + 1);
	    memcpy (dl->name, r->d_name, l);
	    dl->name[l] = '\0';
	    if (stat_with_readdir) {
		char *p;
		p = join (path, dl->name);
		if ((*my_stat) (p, &dl->stat)) {
		    progmess_strerror ("error trying to stat file", p);
		    free (dl->name);
		    dl->name = 0;
		}
		dl->stat.st_atime -= sub_time;
		dl->stat.st_mtime -= sub_time;
		dl->stat.st_ctime -= sub_time;	/* <-- we don't use this though */
		check_interrupt ();
		free (p);
	    }
	}
    } while (r);

/* go to start of list, list will not be empty because of . and .. */
    rewind (dl);

    for (;;) {
	if (dl->name) {
	    if (strcmp ("..", dl->name) && strcmp (".", dl->name)) {
		if (!stat_with_readdir) {
		    char *p;
		    p = join (path, dl->name);
		    if ((*my_stat) (p, &dl->stat)) {
			progmess_strerror ("error trying to stat file", p);
			free (dl->name);
			dl->name = 0;
		    }
		    dl->stat.st_atime -= sub_time;
		    dl->stat.st_mtime -= sub_time;
		    dl->stat.st_ctime -= sub_time;	/* <-- we don't use this though */
		    check_interrupt ();
		    free (p);
		}
	    } else {
		free (dl->name);
		dl->name = 0;
	    }
	}
	if (!dl->next)
	    break;
	dl = dl->next;
    }

    rewind (dl);
    mc_closedir (dir);
    return dl;
}

void free_list (struct directory_list *dl)
{
    if (!dl)
	return;
    rewind (dl);
    while (dl) {
	struct directory_list *next;
	next = dl->next;
	if (dl->name)
	    free (dl->name);
	free (dl);
	dl = next;
    }
}

void remove_list_entry (struct directory_list *dl)
{
    if (!dl->prev) {
	dl->next->prev = 0;	/* begining of list */
    } else if (!dl->next)
	dl->prev->next = 0;	/* end of list */
    else {
	dl->prev->next = dl->next;
	dl->next->prev = dl->prev;
    }
    if (dl->name)
	free (dl->name);
    dl->name = 0;
    free (dl);
}

struct directory_list *find_list_entry (struct directory_list *dl, char *name)
{
    rewind (dl);
    while (dl) {
	if (dl->name)
	    if (!my_strcmp (dl->name, name))
		return dl;
	dl = dl->next;
    }
    return 0;
}

void remove_name_entry (struct directory_list *dl, char *name)
{
    dl = find_list_entry (dl, name);
    if (dl)
	remove_list_entry (dl);
}

void create_dir (char *path, struct stat *s)
{
    if (verbose)
	verbmess ("creating directory", path);
    if (only_test) {
	mirrordir_errno = DIRECTORY_NOT_CREATED;
    } else {
	if (mc_mkdir (path, s->st_mode))
	    progmess_strerror ("error trying create directory", path);
	if (allow_chown)
	    if (mc_chown (path, s->st_uid, s->st_gid))
		progmess_strerror ("error trying chown/chgrp directory", path);
	if (allow_chmod)
	    if (mc_chmod (path, s->st_mode))
		progmess_strerror ("unable to chmod directory", path);
    }
}

int remove_file_quiet (char *path)
{
    if (!only_test) {
#ifdef WIN32
	chmod (path, 0777);
#endif
	if (mc_unlink (path)) {
	    progmess_strerror ("unable to remove file", path);
	    return 1;
	}
    }
    return 0;
}

int remove_file (char *path)
{
    if (verbose)
	verbmess ("removing file", path);
    return remove_file_quiet (path);
}

/* result must NOT be free'd */
char *add_backup_extension (char *path, int i)
{
    static char *r = 0;
    int l;
    if (r)
	free (r);
    l = strlen (path);
    r = malloc (l + strlen (backup_extension) + 16);
    memcpy (r, path, l);
    sprintf (r + l, backup_extension, i);
    return r;
}

int remove_dir (char *path);

void utime_check (time_t modified_time, time_t access_time, char *path, int force)
{
    struct utimbuf t;
    time_t add_time = 0;
    if (is_special_prefix (path))
	add_time = time_offset;
    if (!only_test || force) {
	t.actime = access_time + add_time;
	t.modtime = modified_time + add_time;
	if (mc_utime (path, &t))
	    progmess_strerror ("unable to change mtime/atime of file", path);
    }
}

char *file_name_without_path (char *s)
{
    char *f;
    f = strrchr (s, PATH_SEP);
    if (f)
	return f + 1;
    return s;
}

/* BUG: this does not report when files cannot be removed */
int rename_file_dir (char *path, struct directory_list *dl)
{
    char *p;
    struct directory_list *d;
    int i;
    p = add_backup_extension (path, backup_howmany);
    if (verbose)
	verbmess ("renaming file/dir", path);
    if ((d = find_list_entry (dl, file_name_without_path (p)))) {
	if (S_ISDIR (d->stat.st_mode)) {
	    if (remove_dir (p))
		return 1;
	} else {
	    if (remove_file (p))
		return 1;
	}
    }
    if (backup_howmany > 1)
	for (i = backup_howmany; i > 1; i--) {
	    p = (char *) strdup (add_backup_extension (path, i - 1));
	    if ((d = find_list_entry (dl, file_name_without_path (p)))) {
		if (!only_test)
		    if (mc_rename (p, add_backup_extension (path, i))) {
			progmess_strerror ("error trying to rename file/dir", p);
			free (p);
			return 1;
		    }
	    }
	    free (p);
	}
    if (!only_test) {
	p = add_backup_extension (path, 1);
	if (is_hardlink (&dl->stat) || (S_ISREG (dl->stat.st_mode) && gzip_backups)) {
	    if (copy_regular_file (path, p, &dl->stat, 0))
		return 1;
	    if (remove_file_quiet (path))
		return 1;
	} else if (mc_rename (path, p)) {
	    progmess_strerror ("error trying to rename file/dir", p);
	    return 1;
	}
    }
    time (&current_time);
#ifdef WIN32
    if (!S_ISDIR (dl->stat.st_mode))
#endif
    utime_check (current_time, current_time, p, 0);
    return 0;
}

int is_a_backup_file_name (char *name)
{
    char *p, *q;
    if (!backup_extension)
	return 0;
    p = name + strlen (name);
    q = backup_extension + strlen (backup_extension);
    while ((unsigned long) p > (unsigned long) name) {
	p--;
	q--;
	if (*q == 'd') {
	    if ((unsigned long) q > (unsigned long) backup_extension) {
		if (*(q - 1) == '%') {
		    int i;
		    q--;
		    if (!this_is_a_digit (*p))
			return 0;
		    while (this_is_a_digit (*p)) {
			if ((unsigned long) p == (unsigned long) name)
			    return 0;
			p--;
		    }
		    p++;
		    i = atoi (p);
		    if (i <= 0 || i > backup_howmany)
			return 0;
		} else if (*q != *p)
		    return 0;
	    } else if (*q != *p)
		return 0;
	} else if (*q != *p)
	    return 0;
	if ((unsigned long) q == (unsigned long) backup_extension)
	    return 1;
    }
    return 0;
}

int is_an_old_backup (struct stat *s)
{
    if (outdate_time) {
	time (&current_time);
	if ((time_t) s->st_mtime + outdate_time < (time_t) current_time)
	    return 1;
	else
	    return 0;
    }
    return 0;
}

/* removes all files or directories in a list */
int remove_list (char *path, struct directory_list *dl)
{
    int r = 0;
    rewind (dl);
    for (;;) {
	if (dl->name)
	    check_keep_me (dl->name);
	if (!dl->next)
	    break;
	dl = dl->next;
    }
    for (;;) {
	if (dl->name) {
	    int old, bname;
	    old = is_an_old_backup (&dl->stat);
	    bname = is_a_backup_file_name (dl->name);
	    if (!(!old && bname)) {
		char *p;
		p = join (path, dl->name);
		if (backup_extension && !bname)
		    if (rename_file_dir (p, dl))
			return 1;
		if (S_ISDIR (dl->stat.st_mode))
		    r += remove_dir (p);
		else
		    r += remove_file (p);
		free (p);
	    }
	}
	if (!dl->prev)
	    break;
	dl = dl->prev;
    }
    return r;
}

int remove_dir (char *path)
{
    struct directory_list *dl;
    int r = 0;
    dl = read_list (path);
    if (!dl)
	return 1;
    rewind (dl);
    if (dl->next->next)	/* list has only two entries : `.' and `..' */
	if (!erase_directories) {
	    progmess ("aborting on trying to erase a non-empty directory", path);
	    exit (1);
	}
    r = remove_list (path, dl);
    free_list (dl);
    if (verbose)
	verbmess ("removing directory", path);
    if (!only_test) {
#ifdef WIN32
	chmod (path, 0777);
#endif
	if (mc_rmdir (path)) {
	    progmess_strerror ("unable to remove directory", path);
	    r++;
	}
    }
    return r;
}

void set_vfs_temp_dir (char *p, char *q)
{
    char *d;
    *((char *) strrchr ((d = (char *) strdup (q)), PATH_SEP) + 1) = '\0';	/* there will always be a / in a full path */
    mc_setctl (p, MCCTL_SETTMPDIR, d);
    free (d);
}

void wait_for_shared_lock (int f, off_t offset, int size)
{
#ifdef F_RDLCK
    struct flock l;
    l.l_type = F_RDLCK;
    l.l_whence = SEEK_SET;
    l.l_start = offset;
    l.l_len = size;
    l.l_pid = my_pid;
    fcntl (f, F_SETLKW, &l);
#endif
}

static inline int ends_in_dot_gz (char *file)
{
    if (strlen (file) <= 3)
	return 0;
    if (my_strcmp (file + strlen (file) - 3, ".gz"))
	return 0;
    return 1;
}

int copy_regular_file (char *p, char *q, struct stat *s, struct stat *t)
{
    long f = 0, g = 0;
    char buf[COPY_BUF_SIZE];
    off_t togo, offset;
    int gzip = 0;

    if (gzip_backups) {
	if (ends_in_dot_gz (q) && !ends_in_dot_gz (p))
	    gzip = 1;
	if (ends_in_dot_gz (p) && !ends_in_dot_gz (q))
	    gzip = -1;		/* decompressing is not actually used */
    }
    check_interrupt ();

    set_vfs_temp_dir (p, q);

    if (verbose) {
	if (only_erase)
	    verbmess ("truncating file", q);
	else
	    verbmess ("copying file", q);
    }
    mad_check (__FILE__, __LINE__);
    if (!only_test) {
	f = gzip < 0 ? (long) gzopen (p, "rb") : (long) mc_open (p, O_RDONLY);
	if (!f || f == -1) {
	    progmess_strerror ("unable to open control file for reading", p);
	    real_bad_error++;
	    return 1;
	}
	if (strict_locking && !(gzip < 0))
	    if (!is_special_prefix (p))
		wait_for_shared_lock ((int) f, 0, s->st_size);	/* lock the whole file */
    }
    if (!only_test) {
	if (t) {
	    if (!(t->st_mode & S_IWUSR) || !(t->st_mode & S_IRUSR))
		mc_chmod (q, t->st_mode | S_IWUSR | S_IRUSR);
	} else {
	    mc_chmod (q, S_IWUSR | S_IRUSR);
	}
	g = gzip > 0 ? (long) gzopen (q, "wb") : (long) mc_open (q, O_CREAT | O_WRONLY | O_TRUNC, s->st_mode);
	if (!g || g == -1) {
	    progmess_strerror ("unable to open file for writing", q);
	    f = gzip < 0 ? (long) gzclose ((gzFile) f) : (long) mc_close ((int) f);
	    return 1;
	}
    }
    togo = s->st_size;
    mad_check (__FILE__, __LINE__);

    offset = 0;
    if (!only_test && !only_erase) {
	while (togo > 0) {
	    off_t count, c;
	    char *b;
	    be_nice_to_cpu ();
	    do {
		count = gzip < 0 ? gzread ((gzFile) f, buf, COPY_BUF_SIZE) : mc_read ((int) f, buf, COPY_BUF_SIZE);
	    } while (count < 0 && errno == EINTR);
	    if (count <= 0) {
		if (!count)
		    progmess ("error trying to read from file: premature end of file", p);
		else
		    progmess_strerror ("error trying to read from file", p);
		f = gzip < 0 ? gzclose ((gzFile) f) : mc_close ((int) f);
		g = gzip > 0 ? gzclose ((gzFile) g) : mc_close ((int) g);
		return 1;
	    }
	    b = buf;
	    togo -= count;
	    offset += count;
	    while (count > 0) {
		do {
		    c = gzip > 0 ? gzwrite ((gzFile) g, b, count) : mc_write ((int) g, b, count);
		} while (c < 0 && errno == EINTR);
		if (c <= 0) {
		    if (!count)
			progmess ("error trying to write to file: no bytes written", q);
		    else
			progmess_strerror ("error trying to write to file", q);
		    f = gzip < 0 ? gzclose ((gzFile) f) : mc_close ((int) f);
		    g = gzip > 0 ? gzclose ((gzFile) g) : mc_close ((int) g);
		    return 1;
		}
		b += c;
		count -= c;
	    }
	}
    }
    mad_check (__FILE__, __LINE__);
    if (!only_test) {
	f = gzip < 0 ? gzclose ((gzFile) f) : mc_close ((int) f);	/* locks are removed when we close the file */
	if (gzip > 0 ? gzclose ((gzFile) g) : mc_close ((int) g))
	    progmess_strerror ("error trying to close file", q);
	if (!(gzip > 0))
	    mc_setctl (p, MCCTL_REMOVELOCALCOPY, 0);
	if (!(gzip < 0))
	    mc_setctl (q, MCCTL_REMOVELOCALCOPY, 0);

/* chown comes first because it changes the sticky bit */
	if (allow_chown)
	    if (mc_chown (q, s->st_uid, s->st_gid))
		progmess_strerror ("error trying chown/chgrp regular file", q);
/* now reset the permissions in case the sticky bit has changed */
	if (allow_chmod)
	    if (mc_chmod (q, s->st_mode))
		progmess_strerror ("unable to chmod regular file", q);
    }
#ifdef WIN32
    if (!S_ISDIR (s->st_mode))
#endif
    if (rest_access)
	utime_check (s->st_mtime, s->st_atime, p, 1);
#ifdef WIN32
    if (!S_ISDIR (s->st_mode))
#endif
    utime_check (s->st_mtime, s->st_atime, q, 0);
    check_interrupt ();
    mad_check (__FILE__, __LINE__);
    return 0;
}

int copy_symlink (char *p, char *q, int force)
{
    char pbuf[MAX_PATH_LEN + 1], qbuf[MAX_PATH_LEN + 1];
    int pl, ql;
    pl = mc_readlink (p, pbuf, MAX_PATH_LEN);
    if (pl < 0) {
	progmess_strerror ("error trying read symlink", p);
	return 0;
    }
    pbuf[pl] = '\0';
    if (!force) {
	ql = mc_readlink (q, qbuf, MAX_PATH_LEN);
	if (ql < 0) {
	    progmess_strerror ("error trying read symlink", q);
	    return 0;
	}
	qbuf[ql] = '\0';
	force = my_strcmp (pbuf, qbuf);
	if (force) {
	    if (only_erase && verbose)
		verbmess ("removing symlink", q);
	    if (remove_file_quiet (q))
		return 1;
	}
    }
    if (force) {
	if (verbose && !only_erase)
	    verbmess ("creating symlink", q);
	if (!only_test && !only_erase) {
	    if (mc_symlink (pbuf, q))
		progmess_strerror ("error trying create symlink", q);
	    return 1;
	}
    }
    return 0;
}

void make_special (char *path, struct stat *s)
{
    if (verbose)
	verbmess ("creating device file", path);
    if (!only_test && !only_erase) {
	if (mc_mknod (path, s->st_mode, s->st_rdev))
	    progmess_strerror ("error trying create device file", path);
    }
/* mknod does not seem to set user and group permissions */
/* chown comes first because it changes the sticky bit */
    if (!only_test && !only_erase) {
	if (allow_chown)
	    if (mc_chown (path, s->st_uid, s->st_gid))
		progmess_strerror ("error trying chown/chgrp device file", path);
	if (allow_chmod)
	    if (mc_chmod (path, s->st_mode))
		progmess_strerror ("unable to chmod device file", path);
    }
    utime_check (s->st_mtime, s->st_atime, path, 0);
}

/* returns zero if the directory in which the file was listed was not modified */
int copy_file (char *p, char *q, struct stat *p_stat, struct stat *q_stat)
{
    mode_t m = p_stat->st_mode;
    if (S_ISREG (m)) {
	if (only_erase && !q_stat)
	    return 0;
	copy_regular_file (p, q, p_stat, q_stat);
    } else if (S_ISLNK (m)) {
	return copy_symlink (p, q, (q_stat == 0));	/* hack to not read a symlink if it does not yet exist on the mirror side */
    } else if (S_IS_SPECIAL (m)) {
	make_special (q, p_stat);
    } else {
	real_bad_error++;
	progmess ("what the hell kind of file is this?", p);
    }
    return 0;
}

static inline int time_out_range (time_t tcontrol, time_t tmirror)
{
    return tmirror < tcontrol - mtime_threshold || tmirror > tcontrol + mtime_threshold;
}

int compare_times (time_t tcontrol, time_t tmirror)
{
    if (always_write)
	return 1;
    if (no_mtimes)
	return 0;
    if (strict_mtimes)
	return time_out_range (tcontrol, tmirror);
    return tmirror < tcontrol - mtime_threshold;	/* i.e. the mirror file is *older* than the control file */
}

/* returns zero if the directory in which the file was listed was not modified */
int copy_hardlink_or_file (char *p, char *q, struct stat *p_stat, struct stat *q_stat)
{
    int r = 0;
    if (is_hardlink (p_stat)) {
	if (find_hardlink (p_stat)) {
	    if (!q_stat) {	/* the mirror file not existant */
		if (verbose)
		    verbmess ("copying file as hardlink", q);
		create_link (q);
		reduce_hardlink ();
		return 1;	/* directory modified */
	    } else if (!correct_link (q_stat)) {
/* the file has no hardlinks or is linked to the wrong place */
		if (remove_file_quiet (q))
		    return 1;
		if (verbose)
		    verbmess ("overwriting file as hardlink", q);
		create_link (q);
		reduce_hardlink ();
		return 1;	/* directory modified */
	    }
	    reduce_hardlink ();
	    return 0;		/* directory not modified */
	}
    }
    r |= copy_file (p, q, p_stat, q_stat);
/* if we got to here, then the hardlink is not found in the list */
    if (is_hardlink (p_stat)) {
	struct stat s;
	if (!q_stat) {
	    r = 1;
	    mc_stat (q, &s);
	    q_stat = &s;
	}
	add_hardlink (q, p_stat, q_stat);	/* must be comming accross this family of links
						   for the first time, so create a new list entry */
    }
    return r | !q_stat;		/* directory not modified */
}

/*
   remove() can remove anything except a directory, hence the reasoning
   in separating the file operations from the directory operations at a
   high level. Returns 1 if a modification was made that would change the
   modified time of the directory.
 */
int duplicate (char *p, char *q, struct directory_entry *d, struct directory_list *list)
{
    mode_t m = d->stat.st_mode;

    if (list)
	if (!is_hardlink (&d->stat) && is_hardlink (&list->stat)) {
	    if (verbose > 1)
		verbmess ("hardlink changed to non-hardlink", q);
	    list = 0;
	    remove_file_quiet (q);
	}

    if (!list) {
	/* the file or directory was not found in the mirror list */
	if (S_ISDIR (m)) {
	    if (only_erase)
		return 0;
	    create_dir (q, &d->stat);
	    return 1;
	} else {
	    if (verbose > 1)
		verbmess ("creating new file", q);
	    copy_hardlink_or_file (p, q, &d->stat, 0);
	    return 1;
	}
    }				/* else { */
    /* check if a directory has become a file and is hence in the way */
    if (S_ISDIR (list->stat.st_mode) && !S_ISDIR (m)) {
	if (verbose > 1)
	    verbmess ("replacing directory with file", q);
	if (backup_extension) {
	    if (rename_file_dir (q, list))
		return 1;
	} else {
	    if (remove_dir (q))	/* recursively */
		return 1;
	}
	copy_hardlink_or_file (p, q, &d->stat, 0);
	return 1;
    }
    if (S_ISREG (m) || S_IS_SPECIAL (m)) {
	int r = 0, copy = 0, add_hlink = 0;
	if ((S_IFMT & m) != (S_IFMT & list->stat.st_mode)) {
	    copy = 1;
	    r = 1;
	    if (remove_file_quiet (q))
		return 1;
	    if (S_IS_SPECIAL (m))
	    	if (verbose > 1)
		    verbmess ("updating device file that has changed type", q);
	}
/* regular files must be rewritten if they have been modified */
	if (S_ISREG (m))
	    if (compare_times (d->stat.st_mtime, list->stat.st_mtime) || (d->stat.st_size != list->stat.st_size && !ignore_size)) {
		if (verbose > 1)
		    verbmess ("updating modified file", q);
		if (is_special_prefix (q)) {	/* with a local fs, rewriting a file is possible - not so with ftp so we remove it first */
		    r = 1;
		    if (remove_file_quiet (q))
			return 1;
		}
		copy = 1;
	    }

/* ...or if they are linked to the wrong file */
	if (is_hardlink (&d->stat)) {
	    if (find_hardlink (&d->stat)) {
		if (correct_link (&list->stat))
		    copy = 0;	/* a hardlink would have already been copied - also no need to back it up */
		else
		    copy = 1;
	    } else {
		add_hlink = 1;
	    }
	}
	if (copy) {
	    if (S_ISREG (m) && backup_extension) {	/* do not backup device files */
		r = 1;
		if (rename_file_dir (q, list))
		    return 1;
		r |= copy_hardlink_or_file (p, q, &d->stat, 0);
	    } else {
		r |= copy_hardlink_or_file (p, q, &d->stat, &list->stat);
	    }
	    return r;		/* we can return now - add_hardlink would have been run by copy_hardlink above */
	} else if (is_hardlink (&d->stat)) {
	    if (add_hlink)
		add_hardlink (q, &d->stat, &list->stat);	/* must be coming accross this family of links
								   for the first time, so create a new list entry */
	    else
		reduce_hardlink ();
	}
    } else if (S_ISLNK (m)) {
	if ((S_IFMT & m) != (S_IFMT & list->stat.st_mode)) {
	    if (remove_file_quiet (q))
		return 1;
	    copy_hardlink_or_file (p, q, &d->stat, 0);
	    return 1;
	} else {
	    return copy_hardlink_or_file (p, q, &d->stat, &list->stat);
	}
    } else if (S_ISDIR (m)) {
/* check if a file has become a directory */
	if (!S_ISDIR (list->stat.st_mode)) {
	    if (backup_extension && S_ISREG (list->stat.st_mode))
		if (rename_file_dir (q, list))
		    return 1;
	    if (remove_file (q))
		return 1;
	    if (only_erase)
		return 0;
	    create_dir (q, &d->stat);
	    return 1;
	}
    } else {
	real_bad_error++;
	progmess ("what the hell kind of file is this?", p);
	return 0;
    }

/*
   If we get here, the mirror file/directory is of the same type,
   and just needs to have its permissions, ownership and times updated.
 */

    if (S_ISLNK (m))
	return 0;		/* no need to worry about links */
/* check if ownership is identical */
/* chown comes first because it changes the sticky bit */
    if (list->stat.st_uid != d->stat.st_uid || list->stat.st_gid != d->stat.st_gid) {
	if (verbose && allow_chown)
	    verbmess ("changing ownership", q);
	if (!only_test) {
	    if (allow_chown)
		if (mc_chown (q, d->stat.st_uid, d->stat.st_gid))
		    progmess_strerror ("unable to chown/chgrp dir/file", q);
	}
    }
/* check permissions */
    if (list->stat.st_mode != d->stat.st_mode) {
	if (verbose && allow_chmod)
	    verbmess ("changing permissions", q);
	if (!only_test) {
	    if (allow_chmod)
		if (mc_chmod (q, d->stat.st_mode))
		    progmess_strerror ("unable to chmod dir/file", q);
	}
    }
/* modified and access times */
    if (!S_ISDIR (m)) {		/* these must be handled seperately since access times are always changed */
	if ((time_out_range (list->stat.st_atime, d->stat.st_atime) && access_times) || time_out_range (list->stat.st_mtime, d->stat.st_mtime)) {
	    if (verbose)
		verbmess ("changing mtime/atime", q);
	    utime_check (d->stat.st_mtime, d->stat.st_atime, q, 0);
	}
    }
    return 0;
}

int excluded (char *p, struct stat *s)
{
    char *q;
    struct exclude_list *l;
    if (!exclude_list)
	return PATH_INCLUDED;
    rewind (exclude_list);
    q = file_name_without_path (p);
    l = exclude_list;
    while (l) {
	int ignore;
	ignore = l->ignore ? PATH_IGNORED : PATH_EXCLUDED;
	switch (l->type) {
	case EXCLUDE_LITERAL:
	    if (!my_strcmp (p, l->name)) {		/* it only occurs once, so we can remove it */
		free (l->name);
		l->name = 0;
		if (!l->prev && !l->next) {
		    exclude_list = 0;
		    free (l);
		    return ignore;
		}
		if (!l->prev) {
		    l->next->prev = 0;
		} else if (!l->next)
		    l->prev->next = 0;
		else {
		    l->prev->next = l->next;
		    l->next->prev = l->prev;
		}
		if ((unsigned long) l == (unsigned long) exclude_list)
		    exclude_list = l->next;
		free (l);
		return ignore;
	    }
	    break;
	case EXCLUDE_REGEXP:
	    if (!regexec (&l->regexp, p, 0, NULL, 0))
		return ignore;
	    break;
	case EXCLUDE_GLOB:
	    if (!regexec (&l->regexp, q, 0, NULL, 0))
		return ignore;
	    break;
	case EXCLUDE_FILELIST:
	    if (fastsearch_islisted (&l->filelist, p))
		return ignore;
	    break;
	case EXCLUDE_FUNCTION:{
		int r;
		struct stated_file f;
		f.name = p;
		f.stat = *s;
		r = parser_evaluate (l->compiled_script, (void *) &f);
		if (r == PATH_UNKNOWN)
		    break;
		return r;
	    }
	}
	l = l->next;
    }
    return PATH_INCLUDED;
}

unsigned long blocks_to_kilobytes (unsigned long blocks)
{
    if (block_size > 1024)
	return (unsigned long) blocks *(block_size / 1024);
    return (unsigned long) blocks / (1024 / block_size);
}

unsigned long bytes_to_blocks (off_t bytes)
{
    unsigned long file_blocks;
    file_blocks = bytes / block_size;
    if (bytes % block_size)
	file_blocks++;
    return file_blocks;
}

/* returns non-zero if in range */
int block_range (char *p, struct directory_entry *d)
{
    int r = 0;
    unsigned long file_blocks;

    file_blocks = bytes_to_blocks (d->stat.st_size);
    if (is_hardlink (&d->stat))
	if (find_hardlink (&d->stat))
	    file_blocks = 1;	/* this isn't true, but I have a feeling thats its a good idea */

    total_blocks += file_blocks;
    switch (block_pos) {
    case BLOCK_NOT_USED:
	blocks_written += file_blocks;
	r = 1;
	break;
    case BLOCK_BEFORE_START:
	r = 0;
	if (!start_file)
	    goto in_range;
	if (!my_strcmp (start_file, p)) {
	  in_range:
	    blocks_written += file_blocks;
	    r = 1;
	    block_pos = BLOCK_IN_RANGE;
	} else {
	    if (S_ISDIR (d->stat.st_mode)) {
		if (!my_strncmp (start_file, p, strlen (p)) && start_file[strlen (p)] == PATH_SEP) {
		    blocks_written += file_blocks;
		    r = 1;
		}
	    }
	}
	break;
    case BLOCK_IN_RANGE:
	if (blocks_written + file_blocks <= blocks_allowed || !blocks_allowed) {
	    blocks_written += file_blocks;
	    r = 1;
	} else {
	    infomess ("filled up all blocks - first file/dir not mirrored", p);
	    block_pos = BLOCK_FULL;
	    r = 0;
	}
	break;
    case BLOCK_FULL:
	r = 0;
	break;
    }
    if (file_blocks >= blocks_allowed && blocks_allowed) {
	real_bad_error++;
	progmess ("single file to large to fit within total bytes", p);
    }
    if (verbose > 2) {
	char s[128];
	sprintf (s, "%s file/dir, consumes %lukB: running total %lukB: mirrored total %lukB", \
	r ? "included" : "excluded", blocks_to_kilobytes (file_blocks), \
		 blocks_to_kilobytes (total_blocks), blocks_to_kilobytes (blocks_written));
	verbmess (s, p);
    }
    return r;
}

int subdir_present (char *full_path, char *high_path, struct directory_list *dl)
{
    int l;
    char *r;
    char name[MAX_PATH_LEN];
    l = strlen (high_path);
    if (l > strlen (full_path))	/* not applicable */
	return 1;
    if (my_strncmp (full_path, high_path, l) || full_path[l] != PATH_SEP)	/* not applicable */
	return 1;
    strcpy (name, full_path + l + 1);
    r = strchr (name, PATH_SEP);
    if (r)
	*r = '\0';
    if (find_list_entry (dl, name))
	return 1;
    return 0;
}

static inline char *convert_case (char *p)
{
    char *r;
    r = p = (char *) strdup (p);
    if (files_to_upper) {
	for (; *p; p++)
	    *p = (*p >= 'a' && *p <= 'z') ? (*p - ('a' - 'A')) : *p;
    } else if (files_to_lower) {
	for (; *p; p++)
	    *p = (*p >= 'A' && *p <= 'Z') ? (*p + ('a' - 'A')) : *p;
    }
    return r;
}

int tar_append (char *file, char *arch_name, struct stat *s);

/* returns non-zero if a modification was made that would change the modified time of the directory */
int recurs_dir (char *control, char *mirror,...)
{
    char **files = 0;

    va_list ap;
    int r = 0;
    struct directory_list *dl = 0;
    struct directory_list *dm = 0;

    if (control) {
	dl = read_list (control);
    } else {
	va_start (ap, mirror);
	files = va_arg (ap, char **);
	va_end (ap);
    }
    if (start_file)
	if (!subdir_present (start_file, control, dl)) {
	    progmess ("starting file or one of its components was not found, aborting immediately", start_file);
	    exit (1);
	}
    if (mirror && !tar_output)
	dm = read_list (mirror);
    mad_check (__FILE__, __LINE__);
    if ((control && !dl) || (mirror && !dm && !tar_output)) {
	if (dm)
	    free_list (dm);
	return -1;
    }

    if (control) {
	mc_setctl (control, MCCTL_SETDIRTIMEOUT, (void *) 3600);
	mc_setctl (control, MCCTL_QUICKFILEREAD, (void *) 0);
    }
    mad_check (__FILE__, __LINE__);
    if (mirror && !tar_output)
	mc_setctl (mirror, MCCTL_SETDIRTIMEOUT, (void *) 3600);

    be_nice_to_cpu ();

    for (;;) {
	struct directory_list *next = 0;
	struct directory_list f;
	if (control) {
	    next = dl->next;
	} else {
	    dl = &f;
	    if (!*files)
		break;
	    mad_check (__FILE__, __LINE__);
	    mc_setctl (*files, MCCTL_SETDIRTIMEOUT, (void *) 3600);
	    mc_setctl (*files, MCCTL_QUICKFILEREAD, (void *) 0);
	    mad_check (__FILE__, __LINE__);
	    if ((*my_stat) (*files, &dl->stat)) {
		progmess_strerror ("error trying to stat file", *files);
		real_bad_error++;
		dl->name = 0;
	    } else {
		dl->name = file_name_without_path (*files);
		if (is_special_prefix (*files)) {
		    dl->stat.st_atime -= time_offset;
		    dl->stat.st_mtime -= time_offset;
		    dl->stat.st_ctime -= time_offset;
		}
	    }
	    dl->next = 0;
	    dl->prev = 0;
	}
	if (dl->name) {
	    char *p, *q = 0;
	    int e;
	    check_interrupt ();
	    if (control) {
		p = join (control, dl->name);
	    } else {
		p = (char *) strdup (*files);
	    }
	    e = excluded (p, &dl->stat);
	    if (e == PATH_IGNORED) {
		/* ignoring this entry, so remove it from the mirror list */
		if (dm)
		    remove_name_entry (dm, dl->name);
	    } else if (e == PATH_INCLUDED) {	/* excluded files must not count toward the running total of blocks */
		if (block_range (p, (struct directory_entry *) dl)) {
		    char *i = 0;
		    struct directory_list *d = 0;
		    int m;
		    if (tar_output) {
			mirrordir_errno = 0;
			q = 0;
			m = tar_append (p, p, &dl->stat);
			r += m;
		    } else if (mirror) {
			d = find_list_entry (dm, i = convert_case (dl->name));
			if (d)
			    q = join (mirror, d->name);	/* for filenames that differ in case */
			else
			    q = join (mirror, i);
			mirrordir_errno = 0;
			m = duplicate (p, q, (struct directory_entry *) dl, d);
			r += m;
		    } else {
			q = 0;
		    }
		    if (S_ISDIR (dl->stat.st_mode)) {
			if (mirrordir_errno != DIRECTORY_NOT_CREATED) {
			    int reset_perms_control = 0, reset_perms_mirror = 0;
			    if (0500 != (dl->stat.st_mode & 0500)) {
				reset_perms_control = 1;
				mc_chmod (p, dl->stat.st_mode | 0500);
			    }
			    if (q && 0700 != (dl->stat.st_mode & 0700)) {
				reset_perms_mirror = 1;
				mc_chmod (q, 0777);
			    }
			    m = recurs_dir (p, q);
			    if (reset_perms_control)
				mc_chmod (p, dl->stat.st_mode);
			    if (q && reset_perms_mirror)
				mc_chmod (q, dl->stat.st_mode);
			    mc_setctl (p, MCCTL_FLUSHDIR, 0);	/* we aren't going to go in there again, so free memory */
			    if (q)
				mc_setctl (q, MCCTL_FLUSHDIR, 0);
			    if (m == -1) {
				real_bad_error++;
				progmess ("error trying to mirror directory", p);
				m = 0;
			    }
			    if (d) {
				if (S_ISDIR (d->stat.st_mode)) {
				    if (access_times || m || time_out_range (dl->stat.st_mtime, d->stat.st_mtime)) {
					if (verbose > 1 || (verbose && (m \
									||(time_out_range (dl->stat.st_atime, d->stat.st_atime) \
							&&access_times) \
									||time_out_range (dl->stat.st_mtime, d->stat.st_mtime)) \
					    )) {
					    char mess[64] = "changing directory";
					    if (time_out_range (dl->stat.st_atime, d->stat.st_atime) && access_times)
						strcat (mess, " atime");
					    else if (verbose > 1)
						strcat (mess, " atime/mtime");
					    if (time_out_range (dl->stat.st_mtime, d->stat.st_mtime))
						strcat (mess, " mtime");
					    if (strlen (mess) > 20)	/* hack to never print a changing nothing message */
						verbmess (mess, q ? q : "");
					}
#ifndef WIN32
					if (q)
					    utime_check (dl->stat.st_mtime, dl->stat.st_atime, q, 0);
#endif
				    }
				} else {
#ifndef WIN32
				    if (q)
					utime_check (dl->stat.st_mtime, dl->stat.st_atime, q, 0);
#endif
				}
			    } else {
#ifndef WIN32
				if (q)
				    utime_check (dl->stat.st_mtime, dl->stat.st_atime, q, 0);
#endif
			    }
			    r += m;
			}
#ifndef WIN32
			if (rest_access) {
			    if (verbose > 1)
				verbmess ("changing directory mtime/atime", p);
			    utime_check (dl->stat.st_mtime, dl->stat.st_atime, p, 1);
			}
#endif
		    }
		    if (dm)
	/* checked this entry, so remove it from the mirror list */
	/* must remove the actual file we copied above, not the first case insensitive one */
			remove_name_entry (dm, i ? i : dl->name);
		    if (i)
			free (i);
		    if (q)
			free (q);
		}
	    }
	    free (p);
	}
	if (control) {
	    if (!next)
		break;
	    dl = next;
	} else {
	    files++;
	}
    }

    if (!dont_remove && mirror)
	remove_list (mirror, dm);

    if (dm)
	free_list (dm);
    if (control)
	free_list (dl);
    return r;
}


/* {{{ command line processing */

void usage (int exit_code)
{
char **q;
char *p[256] = 
{"",
"Usage:",
"    mirrordir -[abBdDFGhklMmNOopRrsTtvVXz] <control> <mirror>",
"    copydir -[abBdeFGhklMmNOopRrsTtvVXz] <path> [<path> ...] <dest>",
"    recursdir -[abBdeFGhklMmNOopRrsTtvVXz] <path> [<path> ...]",
"                                          [--tar-file <file>]",
"    pslogin [-K [512|768|1024|1536]] [--test-login]",
"                                    mc:" PATH_SEP_STR PATH_SEP_STR "[user@]host[:port][/path]",
"",
"But usually just",
"    mirrordir [--exclude <path>] <control> <mirror>",
"    copydir <path> [<path> ...] <dest>",
"    recursdir <path> -C 'if (!glob (\"*.c\", FILE)) printf (\"%s\\n\", PATH);'",
"    recursdir <path> --tar-file <file>",
"    pslogin mc:" PATH_SEP_STR PATH_SEP_STR "host" PATH_SEP_STR,
"",
"Mirrordir makes a minimal set of changes to the directory <mirror> to",
"make it identical to the directory <control>. Mirrordir dives into",
"subdirectories recursively and duplicates all types of files exactly.",
"Mirrordir is a DANGEROUS command because it deletes sub-directories",
"recursively as required to make <mirror> identical to <control>.",
"",
"Copydir is equivalent to",
"    mirrordir -c --no-erase-directories --keep-files ...",
"Recursdir is equivalent to",
"    mirrordir --recurs-mode ...",
"Pslogin is a secure encrypted login program. It is equivalent to",
"    mirrordir --login-mode --secure ...",
"",
"",
"      --recurs-mode             do not mirror, only recurse - useful with -C",
"  -c  --copy-mode               copy multiple files on the command line as",
"                                allowed by cp - this is the default for",
"                                copydir and implies --keep-files",
"      --login-mode              login on a remote server running secure-mcserv",
"",
"  -a, --access-times            duplicate even access times of <control>",
"  -m, --strict-mtimes           copy files if modified times differ at all",
"      --no-mtimes               do not look at the mtime of a file when",
"                                trying to decide whether to copy it",
"      --ignore-size             do not look at the size of a file when",
"                                trying to decide whether to copy it",
"  -d, --mtime-threshold <sec>   set the allowable error in in mtime",
"      --time-offset [[+]|-][H]H[:MM]    time offset of any non-local directory",
"  -A, --always-write            overwrite all, regardless of mtime or size",
"  -r, --restore-access          restore access times of <control>",
"      --no-chown                never set ownerships of a file",
"      --no-chmod                never set permissions of a file",
"  -D, --only-delete             do nothing that will consume more disk space",
"",
"  -e, --erase-directories       if a directory and file conflict, then erase",
"                                the directory - this is the default for",
"                                mirrordir",
"      --no-erase-directories    if a directory and file conflict, then",
"                                abort - this is the default for copydir",
"",
"  -b, --backup-extension <ext>  with this extension, backup changed files",
"  -N, --num-backups <num>       backup levels, <ext> must have a %d in it",
"  -O, --backup-outdate <sec>    erase backup files older than <sec> seconds",
"",
"  -B, --block-size <bytes>      device's block size for space calculations",
"  -M, --max-bytes <num>[K|M|G]  max bytes to mirror, then abort with filename",
"  -s, --starting-file <path>    start mirroring only after reading <path>",
"",
"  [-i, --ignore-next-exclude]        the next exclude option means to leave",
"                                     the path completely unchanged",
"  [-i] -X, --exclude <path>          exclude full path matching <path>",
"  [-i] -F, --exclude-from <file>     read list of files to exclude from <file>",
"  [-i] -G, --exclude-glob <expr>     exclude file/dir names matching <expr>",
"  [-i] -R, --exclude-regexp <expr>   exclude full paths matching <expr>",
"       -C, --exclude-script <expr>   exclude, include, or ignore file based on",
"                                     return code of C script <expr>. If <expr>",
"                                     is a filename, reads C script therefrom.",
"",
"      --gzip-backups            compress backups to gzip format to save space",
"",
"  -h, --help                    print help and exit",
"  -v, --verbose                 give details of modifications being done",
"  -V, --version                 print version and exit",
"",
"  -k, --keep-files              do not erase files unless they conflict - this",
"                                is the default for copydir",
"  -l, --no-hard-links           treat hardlinks as regular files",
"      --follow-symlinks         treat symlinks as regular files",
"  -L, --strict-locking		 create shared read locks when reading files",
"",
"      --case-insensitive        look at filenames case insesitively",
"      --to-upper                convert all new filenames to upper or...",
"      --to-lower                lower case",
"",
"  --no-use-passive-connections  don't use passive ftp connections",
"",
"  -p, --password <password>     ftp, pslogin or mc:// password",
"  -P, --password-exact <passwd> never prepend a `-' to anonymous password",
"      --test-login              do not login, just return exit status",
"      --no-warn-first-login     don't warn on first pslogin ever to a machine",
"      --read-password-from-stdin  used for non-interactive invocation",
"      --netrc                   consult ~/.netrc (this is the default)",
"      --no-netrc                don't consult ~/.netrc",
"",
"      --secure                  enable strong encryption with key exchanges",
"  -K, --key-size <bits>         use encryption key size of <n>",
"",
"      --allow-empty-ftp-dirs    default",
"      --no-allow-empty-ftp-dirs if an ftp directory is empty, report error",
"",
"      --proxy-host <host>       set hsc ftp proxy for downloads",
"",
"      --tar-file <file>         for recursdir, creates tar file. use | as",
"                                first character to pipe to a command",
"      --tar-block-size <N>      set blocksize to N x 512",
"",
"  -t, --dry-run, --test-only    output merely what would be done",
"",
"      --nice <num>              consume less cpu time, 0 < num << 50",
"                                not supported on some systems.",
"",
"  -z, --gzip                    compress traffic in both directions - for",
"                                mcfs only",
"",
"All paths may be URL's, but at the moment only ftp:" PATH_SEP_STR PATH_SEP_STR " and mc:",
"are supported.",
"",
"Please send comments, suggestions and bug reports to",
"    mirrordir@mail.obsidian.co.za",
"", 0};

    if (exit_code) {
	progmess ("mirrordir: error on command-line", "try `mirrordir --help'");
	exit (exit_code);
    }

    for (q = p; *q; q++)
	puts (*q);

    exit (0);
}

char cwd[MAX_PATH_LEN + 1];

#ifdef HAVE_MAD
char *mad_pathdup (char *p, char *file, int line)
#else
char *pathdup (char *p)
#endif
{
    char *q, *r, *f = 0;

    if (!is_special_prefix (p)) {
#ifdef WIN32
	if (*p == PATH_SEP) {
	    f = r = malloc (strlen (p) + 10);
	    strncpy (r, cwd, 2);
	    strcpy (r + 2, p);
	    p = r;
	} else if (strlen (p) >= 2 && p[0] >= 'A' && p[0] <= 'Z' && p[1] == ':') {
/* HACK: change C:humphrey to C:\humphrey for simplicity */
	    f = r = malloc (strlen (p) + 10);
	    strncpy (r, p, 2);
	    strcpy (r + 2, PATH_SEP_STR);
	    strcat (r, p + 2);
	    p = r;
	} else {
/* change humphrey to C:\my\current\working\dir\humphrey */
	    f = r = malloc (strlen (cwd) + strlen (p) + 2);
	    strcpy (r, cwd);
	    strcat (r, PATH_SEP_STR);
	    strcat (r, p);
	    p = r;
	}
#else
	if (*p != PATH_SEP) {
	    f = r = malloc (strlen (cwd) + strlen (p) + 2);
	    strcpy (r, cwd);
	    strcat (r, PATH_SEP_STR);
	    strcat (r, p);
	    p = r;
	}
#endif
    }

#ifdef HAVE_MAD
    r = q = mad_alloc (strlen (p) + 2, file, line);
#else
    r = q = malloc (strlen (p) + 2);
#endif
    memset (r, 0, strlen (p) + 2);
    if (is_special_prefix (p)) {
	while (*p != ':')
	    *q++ = *p++;
	*q++ = *p++;
	if (!strncmp (p, PATH_SEP_STR PATH_SEP_STR, 2)) {
	    *q++ = *p++;
	    *q++ = *p++;
	}
    }
    for (;;) {
	if (!*p) {
	    *q = '\0';
	    break;
	}
	if (*p != PATH_SEP) {
	    *q++ = *p++;
	} else {
	    while (*p == PATH_SEP) {
		*q = PATH_SEP;
		if (!strncmp (p, PATH_SEP_STR "." PATH_SEP_STR, 3) || !strcmp (p, PATH_SEP_STR "."))
		    p++;
		else if (!strncmp (p, PATH_SEP_STR ".." PATH_SEP_STR, 4) || !strcmp (p, PATH_SEP_STR "..")) {
		    p += 2;
		    *q = ' ';
		    q = strrchr (r, PATH_SEP);
		    if (!q) {
			q = r;
			*q = PATH_SEP;
		    }
		}
		p++;
	    }
	    q++;
	}
    }
/* get rid of trailing / */
    if (r[0] && r[1])
	if (*--q == PATH_SEP)
	    *q = '\0';
    if (f)
	free (f);
    return r;
}

struct exclude_list *add_to_exclude_list (struct exclude_list *exclude, char *arg, int prepend_cwd)
{
    struct exclude_list *next;
    next = malloc (sizeof (struct exclude_list));
    memset (next, 0, sizeof (struct exclude_list));
    next->prev = exclude;
    if (exclude)
	exclude->next = next;
    exclude = next;
    if (arg) {
	if (prepend_cwd)
	    exclude->name = pathdup (arg);
	else
	    exclude->name = (char *) strdup (arg);
    }
    return exclude;
}

void free_exclude_list (void)
{
    struct exclude_list *l;
    if (!exclude_list)
	return;
    rewind (exclude_list);
    l = exclude_list;
    while (l) {
	struct exclude_list *t;
	t = l;
	l = l->next;
	if (t->compiled_script) {
	    parser_free (t->compiled_script, t->heap);
	    t->compiled_script = 0;
	    free (t->heap);
	    t->heap = 0;
	    if (t->c_script)
		free (t->c_script);
	    t->c_script = 0;
	}
	if (t->name)
	    free (t->name);
	if (t->filelist.paths)
	    fastsearch_shut (&t->filelist);
	memset (t, 0, sizeof (struct exclude_list));
	free (t);
    }
    exclude_list = 0;
}

/* result must be free'd */
char *glob_translate (char *s)
{
    int bracket = 0;
    char *g, *p;
    p = g = malloc (strlen (s) * 2 + 5);
    *p++ = '^';
    while (*s) {
	switch (*s) {
	case '*':
	    *p++ = '.';
	    *p = '*';
	    break;
	case '?':
	    *p = '.';
	    break;
	case '{':
	    bracket++;
	    *p = '(';
	    break;
	case '}':
	    bracket--;
	    *p = ')';
	    break;
	case '.':
	    *p++ = '\\';
	    *p = '.';
	    break;
	case '\\':
	    *p = *s;
	    if (s[1])
		*++p = *++s;
	    break;
	case ',':
	    if (bracket > 0) {
		*p = '|';
		break;
	    }
	default:
	    *p = *s;
	    break;
	}
	p++;
	s++;
    }
    *p++ = '$';
    *p = '\0';
    return g;
}

int opt_password_callback (int a, char *s)
{
    ftpfs_password = (char *) strdup (s);
    memset (s, 0, strlen (s));
    return 0;
}

static int opt_time_offset_callback (int a, char *s)
{
    int sign = 1;
    int hours = 0, minutes = 0;
    if (*s == '+') {
	s++;
    } else if (*s == '-') {
	sign = -1;
	s++;
    }
    if (this_is_a_digit (s[1])) {
	hours = (s[0] - '0') * 10;
	hours += (s[1] - '0');
	s += 2;
    } else if (this_is_a_digit (s[0])) {
	hours = (s[0] - '0');
	s++;
    } else
	goto parse_error;
    if (*s == ':') {
	s++;
	if (this_is_a_digit (s[0]) && this_is_a_digit (s[1]) && !s[2]) {
	    minutes = (s[0] - '0') * 10;
	    minutes += (s[1] - '0');
	} else
	    goto parse_error;
    } else if (*s)
	goto parse_error;
    time_offset = (hours * 60 * 60 + minutes * 60) * sign;
    return 0;
  parse_error:
    progmess ("error parsing time string, example", "use +02:30 for 2 and 1/2 hours");
    return 1;
}

static char *load_file (char *f)
{
    char *r, *result;
    int fd;
    struct stat s;
    if (stat (f, &s))
	return 0;
    if (s.st_size < 3)
	return 0;
    result = r = malloc (s.st_size + 1);
    fd = open (f, O_RDONLY);
    result[s.st_size] = '\0';
    while (s.st_size > 0) {
	int count;
	count = read (fd, r, s.st_size);
	if (count < 0 && errno == EINTR)
	    continue;
	if (count <= 0)
	    break;
	s.st_size -= count;
	r += count;
    }
    close (fd);
    return result;
}

static int opt_exclude_callback (int a, char *s)
{
    char *f = 0;
    char *p;
    Value *l = 0;
    switch (a) {
    case 'C':
	if (strchr (s, ';')) {
	    p = (char *) parser_compile (s, l = malloc (256 * sizeof (Value)));
	} else {
	    f = load_file (s);
	    if (!f) {
		progmess ("error loading", s);
		return 1;
	    }
	    p = (char *) parser_compile (f, l = malloc (256 * sizeof (Value)));
	}
	if (!p) {
	    progmess ("error compiling C function", s);
	    if (l)
		free (l);
	    return 1;
	}
	exclude_list = add_to_exclude_list (exclude_list, 0, 0);
	exclude_list->type = EXCLUDE_FUNCTION;
	exclude_list->ignore = 0;
	exclude_list->compiled_script = p;
	exclude_list->heap = l;
	exclude_list->c_script = f;
	ignore_next_exclude = 0;
	break;
    case 'F':
	exclude_list = add_to_exclude_list (exclude_list, 0, 0);
	exclude_list->type = EXCLUDE_FILELIST;
	exclude_list->ignore = (ignore_next_exclude != 0);
	ignore_next_exclude = 0;
	fastsearch_init (&exclude_list->filelist, s);
	break;
    case 'X':
	exclude_list = add_to_exclude_list (exclude_list, s, 1);
	exclude_list->type = EXCLUDE_LITERAL;
	exclude_list->ignore = (ignore_next_exclude != 0);
	ignore_next_exclude = 0;
	break;
    case 'G':
	exclude_list = add_to_exclude_list (exclude_list, 0, 0);
	p = glob_translate (s);
	exclude_list->type = EXCLUDE_GLOB;
	exclude_list->ignore = (ignore_next_exclude != 0);
	ignore_next_exclude = 0;
	if (regcomp (&exclude_list->regexp, p, REG_EXTENDED | REG_NOSUB | (case_insensitive_filenames ? REG_ICASE : 0))) {
	    progmess ("error converting glob expression to regular expression", s);
	    return 1;
	}
	if (p)
	    free (p);
	break;
    case 'R':
	exclude_list = add_to_exclude_list (exclude_list, 0, 0);
	exclude_list->type = EXCLUDE_REGEXP;
	exclude_list->ignore = (ignore_next_exclude != 0);
	ignore_next_exclude = 0;
	if (regcomp (&exclude_list->regexp, s, REG_EXTENDED | REG_NOSUB | (case_insensitive_filenames ? REG_ICASE : 0))) {
	    progmess ("error compiling regular expression", s);
	    return 1;
	}
	break;
    }
    return 0;
}

int calc_bytes (char *s)
{
    int c;

    if (!this_is_a_digit (s[0])) {
	progmess ("--max-bytes reguires an integer", s);
	return 1;
    }
    blocks_allowed = atol (s);
    c = s[strlen (s) - 1];
    switch (c) {
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
	sum_units = UNITS_BYTES;
	blocks_allowed /= block_size;
	break;
    case 'K':
    case 'k':
	sum_units = UNITS_KILO;
	blocks_allowed <<= 10;
	blocks_allowed /= block_size;
	break;
    case 'M':
    case 'm':
	sum_units = UNITS_MEGA;
	blocks_allowed <<= 20;
	blocks_allowed /= block_size;
	break;
    case 'G':
    case 'g':
	sum_units = UNITS_GIGA;
	blocks_allowed <<= 20;
	blocks_allowed /= block_size;
	blocks_allowed <<= 10;
	break;
    default:
	progmess ("unknown unit, only B, K, M, or G allowed", s);
	return 1;
    }
    if (blocks_allowed < 1) {
	progmess ("--max-bytes allowed must be more than 0", s);
	return 1;
    }
    block_pos = BLOCK_BEFORE_START;
    return 0;
}


int pars_opts (int argc, char **argv)
{
    int i;
    double outdate_time_float = 0.0;
    char *max_str = 0;

    struct prog_options arg_opts[] =
    {
/* 0*/	{' ', "", "", ARG_STRINGS, 0, 0, 0},
/* 1*/	{'a', "", "--access-times", ARG_SET, 0, 0, &access_times},
/* 2*/	{'A', "", "--always-write", ARG_SET, 0, 0, &always_write},
/* 3*/	{'b', "", "--backup-extension", ARG_STRING, &backup_extension, 0, 0},
/* 4*/	{'B', "", "--block-size", ARG_INT, 0, 0, &block_size},
/* 5*/	{'c', "", "--copy-mode", ARG_SET, 0, 0, &copy_mode},
/* 6*/	{'d', "", "--mtime-threshold", ARG_INT, 0, 0, &mtime_threshold},
/* 7*/	{'D', "", "--only-delete", ARG_SET, 0, 0, &only_erase},
/* 8*/	{ 0 , "", "--no-erase-directories", ARG_CLEAR, 0, 0, &erase_directories},
/* 9*/	{'e', "", "--erase-directories", ARG_SET, 0, 0, &erase_directories},
/*10*/	{'F', "", "--exclude-from", ARG_CALLBACK, 0, 0, (void *) opt_exclude_callback},
/* 1*/	{'G', "", "--exclude-glob", ARG_CALLBACK, 0, 0, (void *) opt_exclude_callback},
/* 2*/	{'h', "", "--help", ARG_ADD, 0, 0, &show_help},
/* 3*/	{'k', "", "--keep-files", ARG_SET, 0, 0, &dont_remove},
/* 4*/	{'l', "", "--no-hard-links", ARG_CLEAR, 0, 0, &handle_hardlinks},
/* 5*/	{'M', "", "--max-bytes", ARG_STRING, /* &max_str */ 0, 0, 0},
/* 6*/	{'m', "", "--strict-mtimes", ARG_SET, 0, 0, &strict_mtimes},
/* 7*/	{'N', "", "--num-backups", ARG_INT, 0, 0, &backup_howmany},
/* 8*/	{'O', "", "--backup-outdate", ARG_DOUBLE, 0, 0, /* &outdate_time_float */ 0},
/* 9*/	{'p', "", "--password", ARG_CALLBACK, 0, 0, (void *) opt_password_callback},
/*20*/	{'R', "", "--exclude-regexp", ARG_CALLBACK, 0, 0, (void *) opt_exclude_callback},
/* 1*/	{'r', "", "--restore-access", ARG_SET, 0, 0, &rest_access},
/* 2*/	{'s', "", "--starting-file", ARG_STRING, &start_file, 0, 0},
/* 3*/	{'S', "", "--suffix", ARG_STRING, &backup_extension, 0, 0},
/* 4*/	{'t', "--dry-run", "--test-only", ARG_SET, 0, 0, &only_test},
/* 5*/	{'C', "", "--exclude-script", ARG_CALLBACK, 0, 0, (void *) opt_exclude_callback},
/* 6*/	{'v', "", "--verbose", ARG_ADD, 0, 0, &verbose},
/* 7*/	{'V', "", "--version", ARG_SET, 0, 0, &show_version},
/* 8*/	{'X', "", "--exclude", ARG_CALLBACK, 0, 0, (void *) opt_exclude_callback},
#ifndef WIN32
#ifdef HAVE_GETTIMEOFDAY
	{0, "", "--nice", ARG_INT, 0, 0, &nice_to_cpu},
#endif
#endif
	{0, "", "--no-mtimes", ARG_SET, 0, 0, &no_mtimes},
	{0, "", "--no-chmod", ARG_CLEAR, 0, 0, &allow_chmod},
	{0, "", "--no-chown", ARG_CLEAR, 0, 0, &allow_chown},
	{0, "", "--no-netrc", ARG_CLEAR, 0, 0, &allow_netrc},
	{0, "", "--netrc", ARG_SET, 0, 0, &allow_netrc},
	{0, "", "--ignore-size", ARG_SET, 0, 0, &ignore_size},
	{0, "", "--allow-empty-ftp-dirs", ARG_SET, 0, 0, &ftpfs_option_allow_empty_directories},
	{0, "", "--no-allow-empty-ftp-dirs", ARG_CLEAR, 0, 0, &ftpfs_option_allow_empty_directories},
	{'L', "", "--strict-locking", ARG_SET, 0, 0, &strict_locking},
	{'i', "", "--ignore-next-exclude", ARG_ADD, 0, 0, &ignore_next_exclude},
	{0, "", "--secure", ARG_ADD, 0, 0, &secure},
	{'l', "", "--follow-symlinks", ARG_SET, 0, 0, &follow_symlinks},
	{0, "", "--time-offset", ARG_CALLBACK, 0, 0, (void *) opt_time_offset_callback},
	{'K', "", "--key-size", ARG_INT, 0, 0, &key_size},
	{0, "", "--recurs-mode", ARG_SET, 0, 0, &recurs_mode},
	{0, "", "--login-mode", ARG_SET, 0, 0, &secure_login},
	{0, "", "--proxy-host", ARG_STRING, &ftpfs_proxy_host, 0, 0},
	{0, "", "--tar-block-size", ARG_INT, 0, 0, &tar_block_size},
	{0, "", "--tar-file", ARG_STRING, &tar_file_name, 0, 0},
	{'z', "", "--gzip", ARG_SET, 0, 0, &compress_mode},
	{0, "", "--gzip-backups", ARG_SET, 0, 0, &gzip_backups},
	{0, "--for-Robert-Seese", "--case-insensitive", ARG_SET, 0, 0, &case_insensitive_filenames},
	{0, "", "--to-lower", ARG_SET, 0, 0, &files_to_lower},
	{0, "", "--to-upper", ARG_SET, 0, 0, &files_to_upper},
	{0, "", "--no-use-passive-connections", ARG_CLEAR, 0, 0, &ftpfs_use_passive_connections},
	{0, "", "--test-login", ARG_SET, 0, 0, &test_login},
	{0, "", "--no-warn-first-login", ARG_SET, 0, 0, &no_warn_first_login},
	{0, "", "--read-password-from-stdin", ARG_SET, 0, 0, &read_password_from_stdin},
	{0, "", "--mirror-mode", ARG_IGNORE, 0, 0, 0},
	{'P', "", "--password-exact", ARG_SET, 0, 0, &ftpfs_literal_password},
	{0, 0, 0, 0, 0, 0, 0}
    };

    arg_opts[15].str = &max_str;
    arg_opts[18].option = &outdate_time_float;

    control_mirror_directories = malloc (argc * sizeof(char *));
    arg_opts[0].strs = control_mirror_directories;
    memset (control_mirror_directories, 0, argc * sizeof(char *));

#ifdef HAVE_GETCWD
    getcwd (cwd, MAX_PATH_LEN);
#else
    getwd (cwd);
#endif

    if (strcmp (progname, "mirrordir") && strcmp (progname, "lt-mirrordir") && strncmp (progname, "MIRR", 4) && strcmp (argv[1] ? argv[1] : "", "--mirror-mode")) {
/* defaults for copydir */
	dont_remove = 1;
	copy_mode = 1;
	erase_directories = 0;
    }
    if (!strcmp (progname, "recursdir") || !strncmp (progname, "RECU", 4)) {
/* defaults for recursdir */
	recurs_mode = 1;
    }
    if (!strcmp (progname, "pslogin")) {
/* defaults for secure_login */
	secure = 1;
	secure_login = 1;
	recurs_mode = 1;
    }

    i = get_cmdline_options (argc, argv, arg_opts);

    if (secure_login) {
	recurs_mode = 1;
    }

    if (recurs_mode) {
	dont_remove = 1;
	copy_mode = 1;
	erase_directories = 0;
	recurs_mode = 1;
	if (tar_file_name)
	    tar_output = 1;
    } else {
	if (tar_file_name) {
	    progmess ("you can only use --tar-file with recursdir or --recurs-mode", "--tar-file");
	    return 1;
	}
    }

    if (show_help)
	usage (0);

    if (show_version) {
#ifdef HAVE_BUILTIN_ARC
	printf ("Mirrordir version " VERSION " (International - builtin encryption)\n");
#else
	printf ("Mirrordir version " VERSION " (US - slow script encryption)\n");
#endif
	exit (0);
    }

    if (i) {
	progmess ("error on cmdline near", argv[i]);
	return 1;
    }

    if (!control_mirror_directories[0] || (!control_mirror_directories[1] && !recurs_mode))
	return 1;
    if (secure_login)
	if (control_mirror_directories[1])
	    return 1;
    if (copy_mode) {
	if (recurs_mode) {
	    char **t;
	    for (t = control_mirror_directories;*t;t++)
		free_me[free_num++] = *t = (char *) strdup (*t);
	    mirror_dir = 0;
	    control_files = control_mirror_directories;
	} else {
	    char **t;
	    for (t = control_mirror_directories;*t;t++)
		free_me[free_num++] = *t = pathdup (*t);
	    t--;
	    mirror_dir = *t;
	    control_files = control_mirror_directories;
	    *t = 0;
	}
    } else {
	if (control_mirror_directories[2])
	    return 1;
	free_me[free_num++] = control_dir = pathdup (control_mirror_directories[0]);
	free_me[free_num++] = mirror_dir = pathdup (control_mirror_directories[1]);
    }

    if (verbose && time_offset) {
	char s[12];
	sprintf (s, "%ld", time_offset);
	verbmess ("time offset in seconds", s);
    }

/* now apply some rules */
    if (follow_symlinks) {
	my_stat = mc_stat;
	handle_hardlinks = 0;
    }

    if (copy_mode)
	dont_remove = 1;

    if (access_times)
	rest_access = 1;

    if (backup_extension)
	if (!backup_howmany)
	    backup_howmany = 1;

    if (backup_howmany)
	if (!backup_extension) {
	    if (gzip_backups)
		backup_extension = (char *) strdup (".OLD-%d.gz");
	    else
		backup_extension = (char *) strdup (".OLD-%d");
	}

    if (start_file)
	if (block_pos == BLOCK_NOT_USED)
	    block_pos = BLOCK_BEFORE_START;

    if (!block_size)
	block_size = 1024;

/* finally process some remaining args */
    outdate_time = (time_t) outdate_time_float;

    if (max_str)
	if (calc_bytes (max_str))
	    return 1;

    key_size = field_type (key_size);
    if (!key_size) {
	progmess ("unsupported key-size", "--key-size");
	return 1;
    }

    return 0;
}

/* }}} command line processing */

/* {{{ vfs callbacks */

void message_callback (char *p,...)
{
    static int last_len = 0;
    char s[1024];
    va_list pa;
    if (!verbose)
	return;
    va_start (pa, p);
    vsprintf (s, p, pa);
    va_end (pa);
    p = s;
    while ((p = strchr (p, '\n')))
	*p = ' ';
    if (strstr (s, "etting file") || strstr (s, "toring file") ||
	strstr (s, "eading FTP directory") || strstr (s, "ot listing")) {
	int i;
	i = last_len - strlen (s);
	if (i < 0)
	    i = 0;
	printf ("%s%.*s", s, i, "                                                                                                   ");
	last_len = strlen (s);
	fflush (stdout);
	printf ("\r");
    } else {
	verbmess (s, 0);
    }
}

char *anonymous_password_callback (char *host)
{
    char *p;
    /* password explicitly specified on commandline takes precedence */
    if (ftpfs_password)
	return ftpfs_password;
    /* then have a look at ~/.netrc */
    if (allow_netrc) {
	p = lookup_netrc (host, "anonymous");
	if (p) {		/*EK */
	    if (verbose)
		verbmess ("found password for anonymous in ~/.netrc for", host);
	    return p;
	}
    }
    return NULL;
}

char *password_callback (char *a, char *b, char *c, char *host, char *user)
{				/*EK */
    char *p;

    /* password explicitly specified on commandline takes precedence */
    if (ftpfs_password)
	return ftpfs_password;
    /* then have a look at ~/.netrc */
    if (allow_netrc
	&& (p = lookup_netrc (host, user))) {	/*EK */
	if (verbose)
	    verbmess ("found password in ~/.netrc for", host);
	return p;
    }
    if (read_password_from_stdin) {
	char s[20], *q;
	memset (s, 0, 20);
	read (0, s, 16);
	q = strchr (s, '\n');
	if (q)
	    *q = '\0';
	return (char *) strdup (s);
    }
    /* if everything else fails, then ask */
    return (char *) strdup ((char *) getpass (b));
}

void check_interrupt (void)
{
    if (terminate) {
	if (verbose) {
	    char s[12];
	    sprintf (s, "%d", terminate);
	    verbmess ("terminating on signal", s);
	}
	vfs_shut ();
	exit (1);
    }
}

int gotinterrupt_callback (void)
{
    int t;
    t = interupted;
    interupted = 0;
    return t;
}

/* }}} vfs callbacks */

/* {{{ signal handling */

#if (RETSIGTYPE==void)
#define handler_return return
#else
#define handler_return return 1
#endif

static RETSIGTYPE quit_handler (int x)
{
    interupted = 1;
    terminate = x;
    handler_return;
}

static RETSIGTYPE term_handler (int x)
{
    exit (1);
    handler_return;
}

/* }}} signal handling */

/* {{{ main function */

static int warn_callback (char *f, void *x)
{
    char c[20] = "";
    if (no_warn_first_login)
	return 0;
    printf (\
    "You are attempting to connect to a host that you appear to have\n" \
     "never connected to before as this user on this machine. If you\n" \
    "know that you have connected to this host in the past then this\n" \
      "could mean that an infiltration is being attempted. Otherwise\n" \
    "you are advised to copy the hosts " PUBLIC_KEY_DIR " file via a\n" \
    "secure channel (stiffy disk) to\n\t%s\nbefore connected to this\n" \
	    "host for the first time. Type y to continue.\n", f);
    read (0, c, 20);
    if (c[0] == 'y' || c[0] == 'Y')
	return 0;
    printf ("Aborting.\n");
    exit (1);
    return 1; /* prevents warning */
}

int mcfs_open_login (char *path, int test_login);
void add_functions (void);

int *debug = 0;

#define SCRIPT_ACCEPT_MIN_SIZE 80

void do_download_scripts (char *argv_0)
{
    struct stat st;
    char c[20] = "";
    memset (&st, 0, sizeof (struct stat));
    if (stat (SCRIPT_ACCEPT, &st)) {
	progmess ("Cannot locate script " SCRIPT_ACCEPT, "Mirrordir has not been installed properly");
	exit (1);
    }
    if (download_scripts)
	goto do_download;
    if (st.st_size < SCRIPT_ACCEPT_MIN_SIZE) {
	int f;
	printf ("Mirrordir has detected that " SCRIPT_ACCEPT " is truncated.\n");
	printf ("This means that you have a US version of this software.\n");
	printf ("Would you like to download all needed scripts now via ftp?\n");
	read (0, c, 20);
	if (c[0] == 'y' || c[0] == 'Y') {
	    char t[4096], *q;
	  do_download:
	    if (getuid () || geteuid ()) {
		printf ("You need to be root to run do this\n");
		exit (1);
	    }
	    argv_0 = (char *) strdup (argv_0);
	    q = strrchr (argv_0, PATH_SEP);
	    if (q)
		q[1] = '\0';
	    else
		argv_0[0] = '\0';
#ifdef HAVE_BUILTIN_ARC
	    sprintf (t, "%s%s -ckv %s" PATH_SEP_STR "%s %s" PATH_SEP_STR "%s %s",
		     argv_0,
		     "mirrordir",
		     SCRIPT_HOME, SCRIPT_ACCEPT,
		     SCRIPT_HOME, SCRIPT_CONNECT,
		     KEY_DIR);
#else
	    sprintf (t, "%s%s -ckv %s" PATH_SEP_STR "%s %s" PATH_SEP_STR "%s %s" PATH_SEP_STR "%s %s" PATH_SEP_STR "%s %s",
		     argv_0,
		     "mirrordir",
		     SCRIPT_HOME, SCRIPT_ACCEPT,
		     SCRIPT_HOME, SCRIPT_CONNECT,
		     SCRIPT_HOME, ARC_INIT_SCRIPT,
		     SCRIPT_HOME, ARC_ENCRYPT_SCRIPT,
		     KEY_DIR);
	    free (argv_0);
#endif
	    printf ("%s\n", t);
	    f = open (SCRIPT_ACCEPT, O_TRUNC | O_WRONLY);
	    write (f, "                                                                                 ", 80);
	    close (f);
	    system (t);
	    stat (SCRIPT_CONNECT, &st);
	    if (st.st_size < SCRIPT_ACCEPT_MIN_SIZE || download_scripts) {
		printf ("One of your scripts is still truncated - download must have failed.\n");
		printf ("This message will not display in the future. Use\n");
		printf ("\tmirrordir --download-scripts\nto force a download.\n");
		exit (1);
	    }
	} else {
	    f = open (SCRIPT_ACCEPT, O_TRUNC | O_WRONLY);
	    write (f, "                                                                                 ", 80);
	    close (f);
	    printf ("You have not answered yes. Encryption technology will not work.\n");
	    printf ("This message will not display in the future. Use\n");
	    printf ("\tmirrordir --download-scripts\nto force a download.\n");
	}
    }
}



int main (int argc, char **argv)
{
    char *p;
    int r;

    if (argc > 1)
	if (argv[1])
	    if (!strcmp (argv[1], "--download-scripts")) {
		download_scripts = 1;
		do_download_scripts (argv[0]);
		exit (0);
	    }

    diffie_add_functions ();
    add_functions ();
    memset (free_me, 0, sizeof (void *) * 256);

    p = strrchr (argv[0], PATH_SEP);
    progname = (char *) strdup (p ? p + 1 : argv[0]);

    parser_init ();
    my_pid = getpid ();

#ifdef SIGQUIT
    signal (SIGQUIT, term_handler);
#endif
    signal (SIGINT, quit_handler);
    signal (SIGTERM, term_handler);

    my_stat = mc_lstat;

    if (pars_opts (argc, argv))
	usage (1);

    z_socket_set_compression (compress_mode);
    if (compress_mode) {
	if (verbose)
	    verbmess ("compressed connection", 0);
    }
    if (secure) {
	do_download_scripts (argv[0]);
	if (verbose)
	    verbmess ("encrypted connection", 0);
	arc_socket_set_flags (ARC_SOCKET_FLAGS_NONCRYPTO_OFF | key_size);
	arc_socket_add_warning_callback (warn_callback, 0);
    } else
	arc_socket_set_flags (ARC_SOCKET_FLAGS_CRYPTO_OFF);

    if (copy_mode) {
	char **n;
	if (!recurs_mode)
	    for (n = control_files; *n; n++) {
		if (!my_strcmp (mirror_dir, *n)) {
		    progmess ("copying a path to the same path", *n);
		    usage (1);
		}
	    }
    } else {
	if (!my_strcmp (mirror_dir, control_dir)) {
	    progmess ("mirror and control directories are the same path", control_dir);
	    usage (1);
	}
    }

    vfs_set_message_callback (message_callback);
    vfs_set_getpasswd_callback (password_callback);
    vfs_set_getanonpasswd_callback (anonymous_password_callback);
    vfs_set_gotinterrupt_callback (gotinterrupt_callback);

    vfs_init ();
    ftpfs_init_passwd ();

    if (recurs_mode || copy_mode) {
	if (!expand_glob (&control_files)) {
	    progmess ("error on cmdline near", "no files match glob expressions");
	    exit (RETURN_VALUE_ERROR);
	}
    }

    if (secure_login) {
	int login_failed;
	char s[MAX_PATH_LEN];
	if (strncmp (control_files[0], "mc:" PATH_SEP_STR PATH_SEP_STR, 5)) {
	    sprintf (s, "mc:" PATH_SEP_STR PATH_SEP_STR "%s", control_files[0]);
	} else {
	    sprintf (s, "%s", control_files[0]);
	}
	login_failed = mcfs_open_login (s, test_login);
	if (!test_login) {
	    progmess ("cannot login to", control_files[0]);
	    mad_finalize (__FILE__, __LINE__);
	    exit (1);
	}
	if (login_failed)
	    exit (1);
	else
	    exit (0);
    } else if (tar_output) {
	tar_init (tar_file_name);
	r = recurs_dir (0, "." PATH_SEP_STR, control_files);
	tar_shut ();
    } else if (recurs_mode) {
	r = recurs_dir (0, 0, control_files);
    } else if (copy_mode) {
	r = recurs_dir (0, mirror_dir, control_files);
    } else {
	r = recurs_dir (control_dir, mirror_dir);
    }
    vfs_shut ();

    if (!secure_login && verbose && handle_hardlinks)
	print_hardlinks ();

    if (!secure_login && verbose) {
	char s[16];
	sprintf (s, "%lukB", blocks_to_kilobytes (blocks_written));
	infomess ("Total mirrored", s);
    }
    free (progname);
    if (control_mirror_directories)
	free (control_mirror_directories);
    while (free_num--)
	if (free_me[free_num])
	    free (free_me[free_num]);
    free_all_hardlinks ();
    free_exclude_list ();
    parser_shut ();

    mad_finalize (__FILE__, __LINE__);
/* FIXME: should other return values be reported. Most people won't now how to read them anyway */
    if (r == -1 || real_bad_error)
	return RETURN_VALUE_ERROR;
    return RETURN_VALUE_SUCCESS;
}

/* }}} main function */


