about summary refs log tree commit diff
path: root/Src/Modules
diff options
context:
space:
mode:
authorTanaka Akira <akr@users.sourceforge.net>1999-04-15 18:09:05 +0000
committerTanaka Akira <akr@users.sourceforge.net>1999-04-15 18:09:05 +0000
commit9003d99d16c46b5679da7fcf1f2a41adef495ff9 (patch)
tree95244be534cc37c03a628068faf7712da6317196 /Src/Modules
parentf13624e0f8a3c28c90aa0ce8ee36b639a491e4a9 (diff)
downloadzsh-9003d99d16c46b5679da7fcf1f2a41adef495ff9.tar.gz
zsh-9003d99d16c46b5679da7fcf1f2a41adef495ff9.tar.xz
zsh-9003d99d16c46b5679da7fcf1f2a41adef495ff9.zip
zsh-3.1.5-pws-3 zsh-3.1.5-pws-3
Diffstat (limited to 'Src/Modules')
-rw-r--r--Src/Modules/example.c62
-rw-r--r--Src/Modules/stat.c77
-rw-r--r--Src/Modules/zftp.c2596
-rw-r--r--Src/Modules/zftp.mdd3
4 files changed, 2725 insertions, 13 deletions
diff --git a/Src/Modules/example.c b/Src/Modules/example.c
index 45ef3c28f..a71806c3a 100644
--- a/Src/Modules/example.c
+++ b/Src/Modules/example.c
@@ -49,6 +49,53 @@ bin_example(char *nam, char **args, char *ops, int func)
     return 0;
 }
 
+/**/
+static int
+cond_p_len(Conddef c, char **a)
+{
+    char *s1 = a[0], *s2 = a[1];
+
+    singsub(&s1);
+    untokenize(s1);
+    if (s2) {
+	singsub(&s2);
+	untokenize(s2);
+	return strlen(s1) == matheval(s2);
+    } else {
+	return !s1[0];
+    }
+}
+
+/**/
+static int
+cond_i_ex(Conddef c, char **a)
+{
+    char *s1 = a[0], *s2 = a[1];
+
+    singsub(&s1);
+    untokenize(s1);
+    singsub(&s2);
+    untokenize(s2);
+    return !strcmp("example", dyncat(s1, s2));
+}
+
+/**/
+static int
+ex_wrapper(List list, FuncWrap w, char *name)
+{
+    if (strncmp(name, "example", 7))
+	return 1;
+    else {
+	int ogd = opts[GLOBDOTS];
+
+	opts[GLOBDOTS] = 1;
+	runshfunc(list, w, name);
+	opts[GLOBDOTS] = ogd;
+
+	return 0;
+    }
+}
+
 /*
  * boot_example is executed when the module is loaded.
  */
@@ -57,11 +104,22 @@ static struct builtin bintab[] = {
     BUILTIN("example", 0, bin_example, 0, -1, 0, "flags", NULL),
 };
 
+static struct conddef cotab[] = {
+    CONDDEF("len", 0, 1, 2, cond_p_len),
+    CONDDEF("ex", CONDF_INFIX, 0, 0, cond_i_ex),
+};
+
+static struct funcwrap wrapper[] = {
+    WRAPDEF(ex_wrapper),
+};
+
 /**/
 int
 boot_example(Module m)
 {
-    return !addbuiltins(m->nam, bintab, sizeof(bintab)/sizeof(*bintab));
+    return !(addbuiltins(m->nam, bintab, sizeof(bintab)/sizeof(*bintab)) |
+	     addconddefs(m->nam, cotab, sizeof(cotab)/sizeof(*cotab)) |
+	     !addwrapper(m, wrapper));
 }
 
 #ifdef MODULE
@@ -71,6 +129,8 @@ int
 cleanup_example(Module m)
 {
     deletebuiltins(m->nam, bintab, sizeof(bintab)/sizeof(*bintab));
+    deleteconddefs(m->nam, cotab, sizeof(cotab)/sizeof(*cotab));
+    deletewrapper(m, wrapper);
     return 0;
 }
 #endif
diff --git a/Src/Modules/stat.c b/Src/Modules/stat.c
index 64664abaf..769b42b1a 100644
--- a/Src/Modules/stat.c
+++ b/Src/Modules/stat.c
@@ -34,11 +34,13 @@ enum statnum { ST_DEV, ST_INO, ST_MODE, ST_NLINK, ST_UID, ST_GID,
 		   ST_RDEV, ST_SIZE, ST_ATIM, ST_MTIM, ST_CTIM,
 		   ST_BLKSIZE, ST_BLOCKS, ST_READLINK, ST_COUNT };
 enum statflags { STF_NAME = 1,  STF_FILE = 2, STF_STRING = 4, STF_RAW = 8,
-		     STF_PICK = 16, STF_ARRAY = 32, STF_GMT = 64 };
+		     STF_PICK = 16, STF_ARRAY = 32, STF_GMT = 64,
+		     STF_HASH = 128 };
 static char *statelts[] = { "device", "inode", "mode", "nlink",
 				"uid", "gid", "rdev", "size", "atime",
 				"mtime", "ctime", "blksize", "blocks",
 				"link", NULL };
+#define HNAMEKEY "name"
 
 /**/
 static void
@@ -287,6 +289,8 @@ statprint(struct stat *sbuf, char *outbuf, char *fname, int iwhich, int flags)
  *        file names are returned as a separate array element, type names as
  *        prefix to element.  Note the formatting deliberately contains
  *        fewer frills when -A is used.
+ *  -H hash:  as for -A array, but returns a hash with the keys being those
+ *        from stat -l
  *  -F fmt: specify a $TIME-like format for printing times; the default
  *        is the (CTIME-like) "%a %b %e %k:%M:%S".  This option implies
  *        -s as it is not useful for numerical times.
@@ -305,6 +309,7 @@ static int
 bin_stat(char *name, char **args, char *ops, int func)
 {
     char **aptr, *arrnam = NULL, **array = NULL, **arrptr = NULL;
+    char *hashnam = NULL, **hash = NULL, **hashptr = NULL;
     int len, iwhich = -1, ret = 0, flags = 0, arrsize = 0, fd = 0;
     struct stat statbuf;
     int found = 0, nargs;
@@ -352,6 +357,16 @@ bin_stat(char *name, char **args, char *ops, int func)
 		    }
 		    flags |= STF_ARRAY;
 		    break;
+		} else if (*arg == 'H') {
+		    if (arg[1]) {
+			hashnam = arg+1;
+		    } else if (!(hashnam = *++args)) {
+			zerrnam(name, "missing parameter name\n",
+				NULL, 0);
+			return 1;
+		    }
+		    flags |= STF_HASH;
+		    break;
 		} else if (*arg == 'f') {
 		    char *sfd;
 		    ops['f'] = 1;
@@ -385,6 +400,15 @@ bin_stat(char *name, char **args, char *ops, int func)
 	}
     }
 
+    if ((flags & STF_ARRAY) && (flags & STF_HASH)) {
+    	/* We don't implement setting multiple variables at once */
+	zwarnnam(name, "both array and hash requested", NULL, 0);
+	return 1;
+	/* Alternate method would be to make -H blank arrnam etc etc *
+	 * and so get 'silent loss' of earlier choice, which would   *
+	 * be similar to stat -A foo -A bar filename                 */
+    }
+
     if (ops['l']) {
 	/* list types and return:  can also list to array */
 	if (arrnam) {
@@ -435,7 +459,7 @@ bin_stat(char *name, char **args, char *ops, int func)
     if (ops['g'])
 	flags |= STF_GMT;
 
-    if (!arrnam) {
+    if (!(arrnam || hashnam)) {
 	if (nargs > 1)
 	    flags |= STF_FILE;
 	if (!(flags & STF_PICK))
@@ -444,9 +468,20 @@ bin_stat(char *name, char **args, char *ops, int func)
 
     if (ops['N'] || ops['f'])
 	flags &= ~STF_FILE;
-    if (ops['T'])
+    if (ops['T'] || ops['H'])
 	flags &= ~STF_NAME;
 
+    if (hashnam) {
+    	if (nargs > 1) {
+	    zwarnnam(name, "only one file allowed with -H", NULL, 0);
+	    return 1;
+	}
+	arrsize = (flags & STF_PICK) ? 1 : ST_COUNT;
+	if (flags & STF_FILE)
+	    arrsize++;
+	hashptr = hash = (char **)zcalloc((arrsize+1)*2*sizeof(char *));
+    }
+
     if (arrnam) {
 	arrsize = (flags & STF_PICK) ? 1 : ST_COUNT;
 	if (flags & STF_FILE)
@@ -473,13 +508,20 @@ bin_stat(char *name, char **args, char *ops, int func)
 	if (flags & STF_FILE)
 	    if (arrnam)
 		*arrptr++ = ztrdup(*args);
-	    else
+	    else if (hashnam) {
+	    	*hashptr++ = ztrdup(HNAMEKEY);
+		*hashptr++ = ztrdup(*args);
+	    } else
 		printf("%s%s", *args, (flags & STF_PICK) ? " " : ":\n");
 	if (iwhich > -1) {
 	    statprint(&statbuf, outbuf, *args, iwhich, flags);
 	    if (arrnam)
 		*arrptr++ = ztrdup(outbuf);
-	    else
+	    else if (hashnam) {
+		/* STF_NAME explicitly turned off for ops['H'] above */
+	    	*hashptr++ = ztrdup(statelts[iwhich]);
+		*hashptr++ = ztrdup(outbuf);
+	    } else
 		printf("%s\n", outbuf);
 	} else {
 	    int i;
@@ -487,28 +529,39 @@ bin_stat(char *name, char **args, char *ops, int func)
 		statprint(&statbuf, outbuf, *args, i, flags);
 		if (arrnam)
 		    *arrptr++= ztrdup(outbuf);
-		else
+		else if (hashnam) {
+		    /* STF_NAME explicitly turned off for ops['H'] above */
+		    *hashptr++ = ztrdup(statelts[i]);
+		    *hashptr++ = ztrdup(outbuf);
+		} else
 		    printf("%s\n", outbuf);
 	    }
 	}
 	if (ops['f'])
 	    break;
 
-	if (!arrnam && args[1] && !(flags & STF_PICK))
+	if (!arrnam && !hashnam && args[1] && !(flags & STF_PICK))
 	    putchar('\n');
     }
 
     if (arrnam)
-	if (ret) {
-	    for (aptr = array; *aptr; aptr++)
-		zsfree(*aptr);
-	    zfree(array, arrsize+1);
-	} else {
+	if (ret)
+	    freearray(array);
+	else {
 	    setaparam(arrnam, array);
 	    if (errflag)
 		return 1;
 	}
 
+    if (hashnam)
+    	if (ret)
+	    freearray(hash);
+	else {
+	    sethparam(hashnam, hash);
+	    if (errflag)
+		return 1;
+	}
+
     return ret;
 }
 
diff --git a/Src/Modules/zftp.c b/Src/Modules/zftp.c
new file mode 100644
index 000000000..ca0843419
--- /dev/null
+++ b/Src/Modules/zftp.c
@@ -0,0 +1,2596 @@
+/*
+ * zftp.c - builtin FTP client
+ *
+ * This file is part of zsh, the Z shell.
+ *
+ * Copyright (c) 1998 Peter Stephenson
+ * All rights reserved.
+ *
+ * Permission is hereby granted, without written agreement and without
+ * license or royalty fees, to use, copy, modify, and distribute this
+ * software and to distribute modified versions of this software for any
+ * purpose, provided that the above copyright notice and the following
+ * two paragraphs appear in all copies of this software.
+ *
+ * In no event shall Peter Stephenson or the Zsh Development Group be liable
+ * to any party for direct, indirect, special, incidental, or consequential
+ * damages arising out of the use of this software and its documentation,
+ * even if Peter Stephenson and the Zsh Development Group have been advised of
+ * the possibility of such damage.
+ *
+ * Peter Stephenson and the Zsh Development Group specifically disclaim any
+ * warranties, including, but not limited to, the implied warranties of
+ * merchantability and fitness for a particular purpose.  The software
+ * provided hereunder is on an "as is" basis, and Peter Stephenson and the
+ * Zsh Development Group have no obligation to provide maintenance,
+ * support, updates, enhancements, or modifications.
+ *
+ */
+
+/*
+ * TODO:
+ *   can signal handling be improved?
+ *   error messages may need tidying up.
+ *   maybe we should block CTRL-c on some more operations,
+ *     otherwise you can get the connection closed prematurely.
+ *   some way of turning off progress reports when backgrounded
+ *     would be nice, but the shell doesn't make it easy to find that out.
+ *   the progress reports 100% a bit prematurely:  the data may still
+ *     be in transit, and we are stuck waiting for a message from the
+ *     server.  but there's really nothing else to do.  it's worst
+ *     with small files.
+ *   proxy/gateway connections if i knew what to do
+ *   options to specify e.g. a non-standard port
+ *   optimizing things out is hard in general when you don't know what
+ *     the shell's going to want, but they may be places to second guess
+ *     the user.  Some of the variables could be made special and so
+ *     only retrieve things like the current directory when necessary.
+ *     But it's much neater to have ordinary variables, which the shell
+ *     can manage without our interference, and getting the directory
+ *     just every time it changes isn't so bad.  The user can always
+ *     toggle the `Dumb' preference if it's feeling clever.
+ */
+#include "zftp.mdh"
+#include "zftp.pro"
+
+#include <sys/socket.h>
+#include <netdb.h>
+#include <netinet/in_systm.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <arpa/inet.h>
+/* it's a TELNET based protocol, but don't think I like doing this */
+#include <arpa/telnet.h>
+
+/* bet there are machines which have neither INADDR_NONE nor in_addr_t. */
+#ifndef INADDR_NONE
+#define INADDR_NONE (in_addr_t)-1
+#endif
+
+/*
+ * For FTP block mode
+ *
+ * The server on our AIX machine here happily accepts block mode, takes the
+ * first connection, then at the second complains that it's got nowhere
+ * to send data.  The same problem happens with ncftp, it's not just
+ * me.  And a lot of servers don't even support block mode. So I'm not sure
+ * how widespread the supposed ability to leave open the data fd between
+ * transfers.  Therefore, I've closed all connections after the transfer.
+ * But then what's the point in block mode?  I only implemented it because
+ * it says in RFC959 that you need it to be able to restart transfers
+ * later in the file.  However, it turns out that's not true for
+ * most servers --- but our AIX machine happily accepts the REST
+ * command and then dumps the whole file onto you.  Sigh.
+ *
+ * Note on block sizes:
+ * Not quite sure how to optimize this:  in principle
+ * we should handle blocks up to 65535 bytes, which
+ * is pretty big, and should presumably send blocks
+ * which are smaller to be on the safe side.
+ * Currently we send 32768 and use that also as
+ * the maximum to receive.  No-one's complained yet.  Of course,
+ * no-one's *used* it yet apart from me, but even so.
+ */
+
+struct zfheader {
+    char flags;
+    unsigned char bytes[2];
+};
+
+enum {
+    ZFHD_MARK = 16,		/* restart marker */
+    ZFHD_ERRS = 32,		/* suspected errors in block */
+    ZFHD_EOFB = 64,		/* block is end of record */
+    ZFHD_EORB = 128		/* block is end of file */
+};
+
+typedef int (*readwrite_t)(int, char *, size_t, int);
+
+struct zftpcmd {
+    const char *nam;
+    int (*fun) _((char *, char **, int));
+    int min, max, flags;
+};
+
+enum {
+    ZFTP_CONN  = 0x0001,	/* must be connected */
+    ZFTP_LOGI  = 0x0002,	/* must be logged in */
+    ZFTP_TBIN  = 0x0004,	/* set transfer type image */
+    ZFTP_TASC  = 0x0008,	/* set transfer type ASCII */
+    ZFTP_NLST  = 0x0010,	/* use NLST rather than LIST */
+    ZFTP_DELE  = 0x0020,	/* a delete rather than a make */
+    ZFTP_SITE  = 0x0040,	/* a site rather than a quote */
+    ZFTP_APPE  = 0x0080,	/* append rather than overwrite */
+    ZFTP_HERE  = 0x0100,	/* here rather than over there */
+    ZFTP_CDUP  = 0x0200,	/* CDUP rather than CWD */
+    ZFTP_REST  = 0x0400,	/* restart: set point in remote file */
+    ZFTP_RECV  = 0x0800		/* receive rather than send */
+};
+
+typedef struct zftpcmd *Zftpcmd;
+
+static struct zftpcmd zftpcmdtab[] = {
+    { "open", zftp_open, 0, 4, 0 },
+    { "params", zftp_params, 0, 4, 0 },
+    { "login", zftp_login, 0, 3, ZFTP_CONN },
+    { "user", zftp_login, 0, 3, ZFTP_CONN },
+    { "cd", zftp_cd, 1, 1, ZFTP_CONN|ZFTP_LOGI },
+    { "cdup", zftp_cd, 0, 0, ZFTP_CONN|ZFTP_LOGI|ZFTP_CDUP },
+    { "dir", zftp_dir, 0, -1, ZFTP_CONN|ZFTP_LOGI },
+    { "ls", zftp_dir, 0, -1, ZFTP_CONN|ZFTP_LOGI|ZFTP_NLST },
+    { "type", zftp_type, 0, 1, ZFTP_CONN|ZFTP_LOGI },
+    { "ascii", zftp_type, 0, 0, ZFTP_CONN|ZFTP_LOGI|ZFTP_TASC },
+    { "binary", zftp_type, 0, 0, ZFTP_CONN|ZFTP_LOGI|ZFTP_TBIN },
+    { "mode", zftp_mode, 0, 1, ZFTP_CONN|ZFTP_LOGI },
+    { "local", zftp_local, 0, -1, ZFTP_HERE },
+    { "remote", zftp_local, 1, -1, ZFTP_CONN|ZFTP_LOGI },
+    { "get", zftp_getput, 1, -1, ZFTP_CONN|ZFTP_LOGI|ZFTP_RECV },
+    { "getat", zftp_getput, 2, 2, ZFTP_CONN|ZFTP_LOGI|ZFTP_RECV|ZFTP_REST },
+    { "put", zftp_getput, 1, -1, ZFTP_CONN|ZFTP_LOGI },
+    { "putat", zftp_getput, 2, 2, ZFTP_CONN|ZFTP_LOGI|ZFTP_REST },
+    { "append", zftp_getput, 1, -1, ZFTP_CONN|ZFTP_LOGI|ZFTP_APPE },
+    { "appendat", zftp_getput, 2, 2, ZFTP_CONN|ZFTP_LOGI|ZFTP_APPE|ZFTP_REST },
+    { "delete", zftp_delete, 1, -1, ZFTP_CONN|ZFTP_LOGI },
+    { "mkdir", zftp_mkdir, 1, 1, ZFTP_CONN|ZFTP_LOGI },
+    { "rmdir", zftp_mkdir, 1, 1, ZFTP_CONN|ZFTP_LOGI|ZFTP_DELE },
+    { "rename", zftp_rename, 2, 2, ZFTP_CONN|ZFTP_LOGI },
+    { "quote", zftp_quote, 1, -1, ZFTP_CONN },
+    { "site", zftp_quote, 1, -1, ZFTP_CONN|ZFTP_SITE },
+    { "close", zftp_close, 0, 0, ZFTP_CONN },
+    { "quit", zftp_close, 0, 0, ZFTP_CONN },
+    { 0, 0, 0, 0}
+};
+
+static struct builtin bintab[] = {
+    BUILTIN("zftp", 0, bin_zftp, 1, -1, 0, NULL, NULL),
+};
+
+/*
+ * these are the non-special params to unset when a connection
+ * closes.  any special params are handled, well, specially.
+ * currently there aren't any, which is the way I like it.
+ */
+static char *zfparams[] = {
+    "ZFTP_HOST", "ZFTP_IP", "ZFTP_SYSTEM", "ZFTP_USER",
+    "ZFTP_ACCOUNT", "ZFTP_PWD", "ZFTP_TYPE", "ZFTP_MODE", NULL
+};
+
+/* flags for zfsetparam */
+
+enum {
+    ZFPM_READONLY = 0x01,	/* make parameter readonly */
+    ZFPM_IFUNSET  = 0x02,	/* only set if not already set */
+    ZFPM_INTEGER  = 0x04	/* passed pointer to long */
+};
+
+/*
+ * Basic I/O variables for control connection:
+ * zcfd != -1 is a flag that there is a connection open.
+ */
+static int zcfd = -1;
+static FILE *zcin;
+static struct sockaddr_in zsock;
+
+/*
+ * zcfinish = 0 keep going
+ *            1 line finished, alles klar
+ *            2 EOF
+ */
+static int zcfinish;
+/* zfclosing is set if zftp_close() is active */
+static int zfclosing;
+
+/*
+ * Now stuff for data connection
+ */
+static int zdfd = -1;
+static struct sockaddr_in zdsock;
+
+/*
+ * Stuff about last message:  last line of message and status code.
+ * The reply is also stored in $ZFTP_REPLY; we keep these separate
+ * for convenience.
+ */
+static char *lastmsg, lastcodestr[4];
+static int lastcode;
+
+/* flag for remote system is UNIX --- useful to know as things are simpler */
+static int zfis_unix, zfpassive_conn;
+
+/* remote system has size, mdtm commands */
+enum {
+    ZFCP_UNKN = 0,		/* dunno if it works on this server */
+    ZFCP_YUPP = 1,		/* it does */
+    ZFCP_NOPE = 2		/* it doesn't */
+};
+
+static int zfhas_size, zfhas_mdtm;
+
+/*
+ * We keep an fd open for communication between the main shell
+ * and forked off bits and pieces.  This allows us to know
+ * if something happend in a subshell:  mode changed, type changed,
+ * connection was closed.  If something too substantial happened
+ * in a subshell --- connection opened, ZFTP_USER and ZFTP_PWD changed
+ * --- we don't try to track it because it's too complicated.
+ */
+enum {
+    ZFST_ASCI = 0x00,		/* type for next transfer is ASCII */
+    ZFST_IMAG = 0x01,		/* type for next transfer is image */
+
+    ZFST_TMSK = 0x01,		/* mask for type flags */
+    ZFST_TBIT = 0x01,		/* number of bits in type flags */
+
+    ZFST_CASC = 0x00,		/* current type is ASCII - default */
+    ZFST_CIMA = 0x02,		/* current type is image */
+
+    ZFST_STRE = 0x00,		/* stream mode - default */
+    ZFST_BLOC = 0x04,		/* block mode */
+
+    ZFST_MMSK = 0x04,		/* mask for mode flags */
+
+    ZFST_LOGI = 0x08,		/* user logged in */
+    ZFST_NOPS = 0x10,		/* server doesn't understand PASV */
+    ZFST_NOSZ = 0x20,		/* server doesn't send `(XXXX bytes)' reply */
+    ZFST_TRSZ = 0x40,		/* tried getting 'size' from reply */
+    ZFST_CLOS = 0x80		/* connection closed */
+};
+#define ZFST_TYPE(x) (x & ZFST_TMSK)
+/*
+ * shift current type flags to match type flags: should be by
+ * the number of bits in the type flags
+ */
+#define ZFST_CTYP(x) ((x >> ZFST_TBIT) & ZFST_TMSK)
+#define ZFST_MODE(x) (x & ZFST_MMSK)
+
+static int zfstatfd = -1, zfstatus;
+
+/* Preferences, read in from the `zftp_prefs' array variable */
+enum {
+    ZFPF_SNDP = 0x01,		/* Use send port mode */
+    ZFPF_PASV = 0x02,		/* Try using passive mode */
+    ZFPF_DUMB = 0x04		/* Don't do clever things with variables */
+};
+
+/* The flags as stored internally. */
+int zfprefs;
+
+
+/* zfuserparams is the storage area for zftp_params() */
+char **zfuserparams;
+
+/*
+ * Bits and pieces for dealing with SIGALRM (and SIGPIPE, but that's
+ * easier).  The complication is that SIGALRM may already be handled
+ * by the user setting TMOUT and possibly setting their own trap --- in
+ * fact, it's always handled by the shell when it's interactive.  It's
+ * too difficult to use zsh's own signal handler --- either it would
+ * need rewriting to use a C function as a trap, or we would need a
+ * hack to make it callback via a hidden builtin from a function --- so
+ * just install our own, and use settrap() to restore the behaviour
+ * afterwards if necessary.  However, the more that could be done by
+ * the main shell code, the better I would like it.
+ *
+ * Since we don't want to go through the palaver of changing between
+ * the main zsh signal handler and ours every time we start or stop the
+ * alarm, we keep the flag zfalarmed set to 1 while zftp is rigged to
+ * handle alarms.  This is tested at the end of bin_zftp(), which is
+ * the entry point for all functions, and that restores the original
+ * handler for SIGALRM.  To turn off the alarm temporarily in the zftp
+ * code we then just call alarm(0).
+ *
+ * If we could rely on having select() or some replacement, we would
+ * only need the alarm during zftp_open().
+ */
+
+/* flags for alarm set, alarm gone off */
+int zfalarmed, zfdrrrring;
+/* remember old alarm status */
+time_t oaltime;
+unsigned int oalremain;
+
+/*
+ * Where to jump to when the alarm goes off.  This is much
+ * easier than fiddling with error flags at every turn.
+ * Since we don't expect too many alarm's, the simple setjmp()
+ * mechanism should be good enough.
+ *
+ * gcc -O gives apparently spurious `may be clobbered by longjmp' warnings.
+ */
+jmp_buf zfalrmbuf;
+
+/* The signal handler itself */
+
+/**/
+static RETSIGTYPE
+zfhandler(int sig)
+{
+    if (sig == SIGALRM) {
+	zfdrrrring = 1;
+#ifdef ETIMEDOUT		/* just in case */
+	errno = ETIMEDOUT;
+#else
+	errno = EIO;
+#endif
+	longjmp(zfalrmbuf, 1);
+    }
+    DPUTS(1, "zfhandler caught incorrect signal");
+}
+
+/* Set up for an alarm call */
+
+/**/
+static void
+zfalarm(int tmout)
+{
+    zfdrrrring = 0;
+    /*
+     * We need to do this even if tmout is zero, since there may
+     * be a non-zero timeout set in the main shell which we don't
+     * want to go off.  This could be argued the other way, since
+     * if we don't get that it's probably harmless.  But this looks safer.
+     */
+    if (zfalarmed) {
+	alarm(tmout);
+	return;
+    }
+    signal(SIGALRM, zfhandler);
+    oalremain = alarm(tmout);
+    if (oalremain)
+	oaltime = time(NULL);
+    /*
+     * We'll leave sigtrapped, sigfuncs and TRAPXXX as they are; since the
+     * shell's handler doesn't get the signal, they don't matter.
+     */
+    zfalarmed = 1;
+}
+
+/* Set up for a broken pipe */
+
+/**/
+static void
+zfpipe()
+{
+    /* Just ignore SIGPIPE and rely on getting EPIPE from the write. */
+    signal(SIGPIPE, SIG_IGN);
+}
+
+/* Unset the alarm, see above */
+
+/**/
+static void
+zfunalarm()
+{
+    if (oalremain) {
+	/*
+	 * The alarm was previously set, so set it back, adjusting
+	 * for the time used.  Mostly the alarm was set to zero
+	 * beforehand, so it would probably be best to reinstall
+	 * the proper signal handler before resetting the alarm.
+	 *
+	 * I love the way alarm() uses unsigned int while time_t
+	 * is probably something completely different.
+	 */
+	time_t tdiff = time(NULL) - oaltime;
+	alarm(oalremain < tdiff ? 1 : oalremain - tdiff);
+    } else
+	alarm(0);
+    if (sigtrapped[SIGALRM] || interact) {
+	if (sigfuncs[SIGALRM] || !sigtrapped[SIGALRM])
+	    install_handler(SIGALRM);
+	else
+	    signal_ignore(SIGALRM);
+    } else
+	signal_default(SIGALRM);
+    zfalarmed = 0;
+}
+
+/* Restore SIGPIPE handling to its usual status */
+
+/**/
+static void
+zfunpipe()
+{
+    if (sigtrapped[SIGPIPE]) {
+	if (sigfuncs[SIGPIPE])
+	    install_handler(SIGPIPE);
+	else
+	    signal_ignore(SIGPIPE);
+    } else
+	signal_default(SIGPIPE);
+}
+
+/*
+ * Same as movefd(), but don't mark the fd in the zsh tables,
+ * because we only want it closed by zftp.  However, we still
+ * need to shift the fd's out of the way of the user-visible 0-9.
+ */
+
+/**/
+static int
+zfmovefd(int fd)
+{
+    if (fd != -1 && fd < 10) {
+#ifdef F_DUPFD
+	int fe = fcntl(fd, F_DUPFD, 10);
+#else
+	int fe = zfmovefd(dup(fd));
+#endif
+	close(fd);
+	fd = fe;
+    }
+    return fd;
+}
+
+/*
+ * set a non-special parameter.
+ * if ZFPM_IFUNSET, don't set if it already exists.
+ * if ZFPM_READONLY, make it readonly, but only when creating it.
+ * if ZFPM_INTEGER, val pointer is to long (NB not int), don't free.
+ */
+/**/
+static void
+zfsetparam(char *name, void *val, int flags)
+{
+    Param pm = NULL;
+    int type = (flags & ZFPM_INTEGER) ? PM_INTEGER : PM_SCALAR;
+
+    if (!(pm = (Param) paramtab->getnode(paramtab, name))
+	|| (pm->flags & PM_UNSET)) {
+	/*
+	 * just make it readonly when creating, in case user
+	 * *really* knows what they're doing
+	 */
+	if ((pm = createparam(name, type)) && (flags & ZFPM_READONLY))
+	    pm->flags |= PM_READONLY;
+    } else if (flags & ZFPM_IFUNSET) {
+	pm = NULL;
+    }
+    if (!pm || PM_TYPE(pm->flags) != type) {
+	/* parameters are funny, you just never know */
+	if (type == PM_SCALAR)
+	    zsfree((char *)val);
+	return;
+    }
+    if (type == PM_INTEGER)
+	pm->sets.ifn(pm, *(long *)val);
+    else
+	pm->sets.cfn(pm, (char *)val);
+}
+
+/*
+ * Unset a ZFTP parameter when the connection is closed.
+ * We only do this with connection-specific parameters.
+ */
+
+/**/
+static void
+zfunsetparam(char *name)
+{
+    Param pm;
+
+    if ((pm = (Param) paramtab->getnode(paramtab, name))) {
+	pm->flags &= ~PM_READONLY;
+	unsetparam_pm(pm, 0, 1);
+    }
+}
+
+/*
+ * Join command and arguments to make a proper TELNET command line.
+ * New line is in permanent storage.
+ */
+
+/**/
+static char *
+zfargstring(char *cmd, char **args)
+{
+    int clen = strlen(cmd) + 3;
+    char *line, **aptr;
+
+    for (aptr = args; *aptr; aptr++)
+	clen += strlen(*aptr) + 1;
+    line = zalloc(clen);
+    strcpy(line, cmd);
+    for (aptr = args; *aptr; aptr++) {
+	strcat(line, " ");
+	strcat(line, *aptr);
+    }
+    strcat(line, "\r\n");
+
+    return line;
+}
+
+/*
+ * get a line on the control connection according to TELNET rules
+ * Return status is first digit of FTP reply code
+ */
+
+/**/
+static int
+zfgetline(char *ln, int lnsize, int tmout)
+{
+    int ch, added = 0;
+    /* current line point */
+    char *pcur = ln, cmdbuf[3];
+
+    zcfinish = 0;
+    /* leave room for null byte */
+    lnsize--;
+    /* in case we return unexpectedly before getting anything */
+    ln[0] = '\0';
+
+    if (setjmp(zfalrmbuf)) {
+	alarm(0);
+	zwarnnam("zftp", "timeout getting response", NULL, 0);
+	return 5;
+    }
+    zfalarm(tmout);
+
+    /*
+     * We need to be more careful about errors here; we
+     * should do the stuff with errflag and so forth.
+     * We should probably holdintr() here, since if we don't
+     * get the message, the connection is going to be messed up.
+     * But then we get `frustrated user syndrome'.
+     */
+    for (;;) {
+	ch = fgetc(zcin);
+
+	switch(ch) {
+	case EOF:
+	    if (ferror(zcin) && errno == EINTR) {
+		clearerr(zcin);
+		continue;
+	    }
+	    zcfinish = 2;
+	    break;
+
+	case '\r':
+	    /* always precedes something else */
+	    ch = fgetc(zcin);
+	    if (ch == EOF) {
+		zcfinish = 2;
+		break;
+	    }
+	    if (ch == '\n') {
+		zcfinish = 1;
+		break;
+	    }
+	    if (ch == '\0') {
+		ch = '\r';
+		break;
+	    }
+	    /* not supposed to get here */
+	    ch = '\r';
+	    break;
+
+	case '\n':
+	    /* not supposed to get here */
+	    zcfinish = 1;
+	    break;
+
+	case IAC:
+	    /*
+	     * oh great, now it's sending TELNET commands.  try
+	     * to persuade it not to.
+	     */
+	    ch = fgetc(zcin);
+	    switch (ch) {
+	    case WILL:
+	    case WONT:
+		ch = fgetc(zcin);
+		/* whatever it wants to do, stop it. */
+		cmdbuf[0] = (char)IAC;
+		cmdbuf[1] = (char)DONT;
+		cmdbuf[2] = ch;
+		write(zcfd, cmdbuf, 3);
+		continue;
+
+	    case DO:
+	    case DONT:
+		ch = fgetc(zcin);
+		/* well, tough, we're not going to. */
+		cmdbuf[0] = (char)IAC;
+		cmdbuf[1] = (char)WONT;
+		cmdbuf[2] = ch;
+		write(zcfd, cmdbuf, 3);
+		continue;
+
+	    case EOF:
+		/* strange machine. */
+		zcfinish = 2;
+		break;
+
+	    default:
+		break;
+	    }
+	    break;
+	}
+	
+	if (zcfinish)
+	    break;
+	if (added < lnsize) {
+	    *pcur++ = ch;
+	    added++;
+	}
+	/* junk it if we don't have room, but go on reading */
+    }
+
+    alarm(0);
+
+    *pcur = '\0';
+    /* if zcfinish == 2, at EOF, return that, else 0 */
+    return (zcfinish & 2);
+}
+
+/*
+ * Get a whole message from the server.  A dash after
+ * the first line code means keep reading until we get
+ * a line with the same code followed by a space.
+ *
+ * Note that this returns an FTP status code, the first
+ * digit of the reply.  There is also a pseudocode, 6, which
+ * means `there's no point trying anything, just yet'.
+ * We return it either if the connection is closed, or if
+ * we got a 530 (user not logged in), in which case whatever
+ * you're trying to do isn't going to work.
+ */
+
+/**/
+static int 
+zfgetmsg()
+{
+    char line[256], *ptr, *verbose;
+    int stopit, printing = 0, tmout;
+
+    if (zcfd == -1)
+	return 5;
+    if (!(verbose = getsparam("ZFTP_VERBOSE")))
+	verbose = "";
+    zsfree(lastmsg);
+    lastmsg = NULL;
+
+    tmout = getiparam("ZFTP_TMOUT");
+
+    zfgetline(line, 256, tmout);
+    ptr = line;
+    if (zfdrrrring || !isdigit((int)*ptr) || !isdigit((int)ptr[1]) || 
+	!isdigit((int)ptr[2])) {
+	/* timeout, or not talking FTP.  not really interested. */
+	zcfinish = 2;
+	if (!zfclosing)
+	    zfclose();
+	lastmsg = ztrdup("");
+	strcpy(lastcodestr, "000");
+	zfsetparam("ZFTP_REPLY", ztrdup(lastmsg), ZFPM_READONLY);
+	return 6;
+    }
+    strncpy(lastcodestr, ptr, 3);
+    ptr += 3;
+    lastcodestr[3] = '\0';
+    lastcode = atoi(lastcodestr);
+    zfsetparam("ZFTP_CODE", ztrdup(lastcodestr), ZFPM_READONLY);
+    stopit = (*ptr++ != '-');
+
+    if (strchr(verbose, lastcodestr[0])) {
+	/* print the whole thing verbatim */
+	printing = 1;
+	fputs(line, stderr);
+    }  else if (strchr(verbose, '0') && !stopit) {
+	/* print multiline parts with the code stripped */
+	printing = 2;
+	fputs(ptr, stderr);
+    }
+    if (printing)
+	fputc('\n', stderr);
+
+    while (zcfinish != 2 && !stopit) {
+	zfgetline(line, 256, tmout);
+	ptr = line;
+	if (zfdrrrring) {
+	    line[0] = '\0';
+	    break;
+	}
+
+	if (!strncmp(lastcodestr, line, 3)) {
+	    if (line[3] == ' ') {
+		stopit = 1;
+		ptr += 4;
+	    } else if (line[3] == '-')
+		ptr += 4;
+	} else if (!strncmp("    ", line, 4))
+	    ptr += 4;
+
+	if (printing == 2) {
+	    if (!stopit) {
+		fputs(ptr, stderr);
+		fputc('\n', stderr);
+	    }
+	} else if (printing) {
+	    fputs(line, stderr);
+	    fputc('\n', stderr);
+	}
+    }
+
+    if (printing)
+	fflush(stderr);
+
+    /* The internal message is just the text. */
+    lastmsg = ztrdup(ptr);
+    /*
+     * The parameter is the whole thing, including the code.
+     */
+    zfsetparam("ZFTP_REPLY", ztrdup(line), ZFPM_READONLY);
+    /*
+     * close the connection here if zcfinish == 2, i.e. EOF,
+     * or if we get a 421 (service not available, closing connection),
+     * but don't do it if it's expected (zfclosing set).
+     */
+    if ((zcfinish == 2 || lastcode == 421) && !zfclosing) {
+	zcfinish = 2;		/* don't need to tell server */
+	zfclose();
+	/* unexpected, so tell user */
+	zwarnnam("zftp", "remote server has closed connection", NULL, 0);
+	return 6;		/* pretend it failed, because it did */
+    }
+    if (lastcode == 530) {
+	/* user not logged in */
+	return 6;
+    }
+    /*
+     * May as well handle this here, though it's pretty uncommon.
+     * A 120 is something like "service ready in nnn minutes".
+     * It means we just hang around waiting for another reply.
+     */
+    if (lastcode == 120) {
+	zwarnnam("zftp", "delay expected, waiting: %s", lastmsg, 0);
+	return zfgetmsg();
+    }
+
+    /* first digit of code determines success, failure, not in the mood... */
+    return lastcodestr[0] - '0';
+}
+
+
+/*
+ * Send a command and get the reply.
+ * The command is expected to have the \r\n already tacked on.
+ * Returns the status code for the reply.
+ */
+
+/**/
+static int
+zfsendcmd(char *cmd)
+{
+    /*
+     * We use the fd directly; there's no point even using
+     * stdio with line buffering, since we always send the
+     * complete line in one string anyway.
+     */
+    int ret, tmout;
+
+    if (zcfd == -1)
+	return 5;
+    tmout = getiparam("ZFTP_TMOUT");
+    if (setjmp(zfalrmbuf)) {
+	alarm(0);
+	zwarnnam("zftp", "timeout sending message", NULL, 0);
+	return 5;
+    }
+    zfalarm(tmout);
+    ret = write(zcfd, cmd, strlen(cmd));
+    alarm(0);
+
+    if (ret <= 0) {
+	zwarnnam("zftp send", "failed sending control message", NULL, 0);
+	return 5;		/* FTP status code */
+    }
+
+    return zfgetmsg();
+}
+
+
+/* Set up a data connection, return 1 for failure, 0 for success */
+
+/**/
+static int
+zfopendata(char *name)
+{
+    if (!(zfprefs & (ZFPF_SNDP|ZFPF_PASV))) {
+	zwarnnam(name, "Must set preference S or P to transfer data", NULL, 0);
+	return 1;
+    }
+    zdfd = zfmovefd(socket(AF_INET, SOCK_STREAM, 0));
+    if (zdfd < 0) {
+	zwarnnam(name, "can't get data socket: %e", NULL, errno);
+	return 1;
+    }
+
+    zdsock = zsock;
+    zdsock.sin_family = AF_INET;
+
+    if (!(zfstatus & ZFST_NOPS) && (zfprefs & ZFPF_PASV)) {
+	char *ptr;
+	int i, nums[6], err;
+	unsigned char iaddr[4], iport[2];
+
+	if (zfsendcmd("PASV\r\n") == 6)
+	    return 1;
+	else if (lastcode >= 500 && lastcode <= 504) {
+	    /*
+	     * Fall back to send port mode.  That will
+	     * test the preferences for whether that's OK.
+	     */
+	    zfstatus |= ZFST_NOPS;
+	    zfclosedata();
+	    return zfopendata(name);
+	}
+	/*
+	 * OK, now we need to know what port we're looking at,
+	 * which is cunningly concealed in the reply.
+	 * lastmsg already has the reply code expunged.
+	 */
+	for (ptr = lastmsg; *ptr; ptr++)
+	    if (isdigit(*ptr))
+		break;
+	if (sscanf(ptr, "%d,%d,%d,%d,%d,%d",
+		   nums, nums+1, nums+2, nums+3, nums+4, nums+5) != 6) {
+	    zwarnnam(name, "bad response to PASV: %s", lastmsg, 0);
+	    zfclosedata();
+	    return 1;
+	}
+	for (i = 0; i < 4; i++)
+	    iaddr[i] = STOUC(nums[i]);
+	iport[0] = STOUC(nums[4]);
+	iport[1] = STOUC(nums[5]);
+
+	memcpy(&zdsock.sin_addr, iaddr, sizeof(iaddr));
+	memcpy(&zdsock.sin_port, iport, sizeof(iport));
+
+	/* we should timeout this connect */
+	do {
+	    err = connect(zdfd, (struct sockaddr *)&zdsock, sizeof(zdsock));
+	} while (err && errno == EINTR && !errflag);
+
+	if (err) {
+	    zwarnnam(name, "connect failed: %e", NULL, errno);
+	    zfclosedata();
+	    return 1;
+	}
+
+	zfpassive_conn = 1;
+    } else {
+	char portcmd[40];
+	unsigned char *addr, *port;
+	int ret, len;
+
+	if (!(zfprefs & ZFPF_SNDP)) {
+	    zwarnnam(name, "only sendport mode available for data", NULL, 0);
+	    return 1;
+	}
+
+	zdsock.sin_port = 0;	/* to be set by bind() */
+	len = sizeof(zdsock);
+	/* need to do timeout stuff and probably handle EINTR here */
+	if (bind(zdfd, (struct sockaddr *)&zdsock, sizeof(zdsock)) < 0)
+	    ret = 1;
+	else if (getsockname(zdfd, (struct sockaddr *)&zdsock, &len) < 0)
+	    ret = 2;
+	else if (listen(zdfd, 1) < 0)
+	    ret = 3;
+	else
+	    ret = 0;
+
+	if (ret) {
+	    zwarnnam(name, "failure on data socket: %s: %e",
+		     ret == 3 ? "listen" : ret == 2 ? "getsockname" : "bind",
+		     errno);
+	    zfclosedata();
+	    return 1;
+	}
+
+	addr = (unsigned char *) &zdsock.sin_addr;
+	port = (unsigned char *) &zdsock.sin_port;
+	sprintf(portcmd, "PORT %d,%d,%d,%d,%d,%d\r\n",
+		addr[0],addr[1],addr[2],addr[3],port[0],port[1]);
+	if (zfsendcmd(portcmd) >= 5) {
+	    zwarnnam(name, "port command failed", NULL, 0);
+	    zfclosedata();
+	    return 1;
+	}
+	zfpassive_conn = 0;
+    }
+
+    return 0;
+}
+
+/* Close the data connection. */
+
+/**/
+static void
+zfclosedata(void)
+{
+    if (zdfd == -1)
+	return;
+    close(zdfd);
+    zdfd = -1;
+}
+
+/*
+ * Set up a data connection and use cmd to initiate a transfer.
+ * The actual data fd will be zdfd; the calling routine
+ * must handle the data itself.
+ * rest is a REST command to specify starting somewhere other
+ * then the start of the remote file.
+ * getsize is non-zero if we want to try to find the number
+ * of bytes in the reply to a RETR command.
+ *
+ * Return 0 on success, 1 on failure.
+ */
+
+/**/
+static int
+zfgetdata(char *name, char *rest, char *cmd, int getsize)
+{
+    int len, newfd;
+
+    if (zfopendata(name))
+	return 1;
+
+    /*
+     * Set position in remote file for get/put.
+     * According to RFC959, the restart command needs something
+     * called a marker which has previously been put into the data.
+     * Luckily for the real world, UNIX machines just interpret this
+     * as an offset into the byte stream.
+     *
+     * This has to be sent immediately before the data transfer, i.e.
+     * after all mucking around with types and sizes and so on.
+     */
+    if (rest && zfsendcmd(rest) > 3) {
+	zfclosedata();
+	return 1;
+    }
+
+    if (zfsendcmd(cmd) > 2) {
+	zfclosedata();
+	return 1;
+    }
+    if (getsize || (!(zfstatus & ZFST_TRSZ) && !strncmp(cmd, "RETR", 4))) {
+	/*
+	 * See if we got something like:
+	 *   Opening data connection for nortypix.gif (1234567 bytes).
+	 * On the first RETR, always see if this works,  Then we
+	 * can avoid sending a special SIZE command beforehand.
+	 */
+	char *ptr = strstr(lastmsg, "bytes");
+	zfstatus |= ZFST_NOSZ|ZFST_TRSZ;
+	if (ptr) {
+	    while (ptr > lastmsg && !isdigit(*ptr))
+		ptr--;
+	    while (ptr > lastmsg && isdigit(ptr[-1]))
+		ptr--;
+	    if (isdigit(*ptr)) {
+		zfstatus &= ~ZFST_NOSZ;
+		if (getsize) {
+		    long sz = zstrtol(ptr, NULL, 10);
+		    zfsetparam("ZFTP_SIZE", &sz, ZFPM_READONLY|ZFPM_INTEGER);
+		}
+	    }
+	}
+    }
+
+    if (!zfpassive_conn) {
+	/*
+	 * the current zdfd is the socket we opened, but we need
+	 * to let the server set up a different fd for reading/writing.
+	 * then we can close the fd we were listening for a connection on.
+	 * don't expect me to understand this, i'm only the programmer.
+	 */
+
+	/* accept the connection */
+	len = sizeof(zdsock);
+	newfd = zfmovefd(accept(zdfd, (struct sockaddr *)&zdsock, &len));
+	zfclosedata();
+	if (newfd < 0) {
+	    zwarnnam(name, "unable to accept data.", NULL, 0);
+	    return 1;
+	}
+	zdfd = newfd;		/* this is now the actual data fd */
+    }
+
+
+    /* more options, just to look professional */
+#ifdef SO_LINGER
+    /*
+     * Since data can take arbitrary amounts of time to arrive,
+     * the socket can be made to hang around until it doesn't think
+     * anything is arriving.
+     *
+     * In BSD 4.3, you could only linger for infinity.  Don't
+     * know if this has changed.
+     */
+    {
+	struct linger li;
+
+	li.l_onoff = 1;
+	li.l_linger = 120;
+	setsockopt(zdfd, SOL_SOCKET, SO_LINGER, (char *)&li, sizeof(li));
+    }
+#endif
+#if defined(IP_TOS) && defined(IPTOS_THROUGHPUT)
+    /* try to get high throughput, snigger */
+    {
+	int arg = IPTOS_THROUGHPUT;
+	setsockopt(zdfd, IPPROTO_IP, IP_TOS, (char *)&arg, sizeof(arg));
+    }
+#endif
+#if defined(F_SETFD) && defined(FD_CLOEXEC)
+	/* If the shell execs a program, we don't want this fd left open. */
+	len = FD_CLOEXEC;
+	fcntl(zdfd, F_SETFD, &len);
+#endif
+
+    return 0;
+}
+
+/*
+ * Find out about a local or remote file and pass back the information.
+ *
+ * We could jigger this to use ls like ncftp does as a backup.
+ * But if the server is non-standard enough not to have SIZE and MDTM,
+ * there's a very good chance ls -l isn't going to do great things.
+ *
+ * if fd is >= 0, it is used for an fstat when remote is zero:
+ * this is because on a put we are taking input from fd 0.
+ */
+
+/**/
+static int
+zfstats(char *fnam, int remote, long *retsize, char **retmdtm, int fd)
+{
+    long sz = -1;
+    char *mt = NULL;
+    int ret;
+
+    if (retsize)
+	*retsize = -1;
+    if (retmdtm)
+	*retmdtm = NULL;
+    if (remote) {
+	char *cmd;
+	if ((zfhas_size == ZFCP_NOPE && retsize) ||
+	    (zfhas_mdtm == ZFCP_NOPE && retmdtm))
+	    return 2;
+
+	/*
+	 * File is coming from over there.
+	 * Make sure we get the type right.
+	 */
+	zfsettype(ZFST_TYPE(zfstatus));
+	if (retsize) {
+	    cmd = tricat("SIZE ", fnam, "\r\n");
+	    ret = zfsendcmd(cmd);
+	    zsfree(cmd);
+	    if (ret == 6)
+		return 1;
+	    else if (lastcode < 300) {
+		sz = zstrtol(lastmsg, 0, 10);
+		zfhas_size = ZFCP_YUPP;
+	    } else if (lastcode >= 500 && lastcode <= 504) {
+		zfhas_size = ZFCP_NOPE;
+		return 2;
+	    } else if (lastcode == 550)
+		return 1;
+	    /* if we got a 550 from SIZE, the file doesn't exist */
+	}
+
+	if (retmdtm) {
+	    cmd = tricat("MDTM ", fnam, "\r\n");
+	    ret = zfsendcmd(cmd);
+	    zsfree(cmd);
+	    if (ret == 6)
+		return 1;
+	    else if (lastcode < 300) {
+		mt = ztrdup(lastmsg);
+		zfhas_mdtm = ZFCP_YUPP;
+	    } else if (lastcode >= 500 && lastcode <= 504) {
+		zfhas_mdtm = ZFCP_NOPE;
+		return 2;
+	    } else if (lastcode == 550)
+		return 1;
+	}
+    } else {
+	/* File is over here */
+	struct stat statbuf;
+	struct tm *tm;
+	char tmbuf[20];
+
+	if ((fd == -1 ? stat(fnam, &statbuf) : fstat(fd, &statbuf)) < 0)
+	    return 1;
+	/* make sure it's long, since this has to be a pointer */
+	sz = statbuf.st_size;
+
+	if (retmdtm) {
+	    /* use gmtime() rather than localtime() for consistency */
+	    tm = gmtime(&statbuf.st_mtime);
+	    /*
+	     * FTP format for data is YYYYMMDDHHMMSS
+	     * Using tm directly is easier than worrying about
+	     * incompatible strftime()'s.
+	     */
+	    sprintf(tmbuf, "%04d%02d%02d%02d%02d%02d",
+		    tm->tm_year + 1900, tm->tm_mon+1, tm->tm_mday,
+		    tm->tm_hour, tm->tm_min, tm->tm_sec);
+	    mt = ztrdup(tmbuf);
+	}
+    }
+    if (retsize)
+	*retsize = sz;
+    if (retmdtm)
+	*retmdtm = mt;
+    return 0;
+}
+
+/* Set parameters to say what's coming */
+
+/**/
+static void
+zfstarttrans(char *nam, int recv, long sz)
+{
+    long cnt = 0;
+    /*
+     * sz = -1 signifies error getting size.  don't set ZFTP_SIZE if sz is
+     * zero, either: it probably came from an fstat() on a pipe, so it
+     * means we don't know and shouldn't tell the user porkies.
+     */
+    if (sz > 0)
+	zfsetparam("ZFTP_SIZE", &sz, ZFPM_READONLY|ZFPM_INTEGER);
+    zfsetparam("ZFTP_FILE", ztrdup(nam), ZFPM_READONLY);
+    zfsetparam("ZFTP_TRANSFER", ztrdup(recv ? "G" : "P"), ZFPM_READONLY);
+    zfsetparam("ZFTP_COUNT", &cnt, ZFPM_READONLY|ZFPM_INTEGER);
+}
+
+/* Tidy up afterwards */
+
+/**/
+static void
+zfendtrans()
+{
+    zfunsetparam("ZFTP_SIZE");
+    zfunsetparam("ZFTP_FILE");
+    zfunsetparam("ZFTP_TRANSFER");
+    zfunsetparam("ZFTP_COUNT");
+}
+
+/* Read with timeout if recv is set. */
+
+/**/
+static int
+zfread(int fd, char *bf, size_t sz, int tmout)
+{
+    int ret;
+
+    if (!tmout)
+	return read(fd, bf, sz);
+
+    if (setjmp(zfalrmbuf)) {
+	alarm(0);
+	zwarnnam("zftp", "timeout on network read", NULL, 0);
+	return -1;
+    }
+    zfalarm(tmout);
+
+    ret = read(fd, bf, sz);
+
+    /* we don't bother turning off the whole alarm mechanism here */
+    alarm(0);
+    return ret;
+}
+
+/* Write with timeout if recv is not set. */
+
+/**/
+static int
+zfwrite(int fd, char *bf, size_t sz, int tmout)
+{
+    int ret;
+
+    if (!tmout)
+	return write(fd, bf, sz);
+
+    if (setjmp(zfalrmbuf)) {
+	alarm(0);
+	zwarnnam("zftp", "timeout on network write", NULL, 0);
+	return -1;
+    }
+    zfalarm(tmout);
+
+    ret = write(fd, bf, sz);
+
+    /* we don't bother turning off the whole alarm mechanism here */
+    alarm(0);
+    return ret;
+}
+
+static int zfread_eof;
+
+/* Version of zfread when we need to read in block mode. */
+
+/**/
+static int
+zfread_block(int fd, char *bf, size_t sz, int tmout)
+{
+    int n;
+    struct zfheader hdr;
+    size_t blksz, cnt;
+    char *bfptr;
+    do {
+	/* we need the header */
+	do {
+	    n = zfread(fd, (char *)&hdr, sizeof(hdr), tmout);
+	} while (n < 0 && errno == EINTR);
+	if (n != 3 && !zfdrrrring) {
+	    zwarnnam("zftp", "failed to read FTP block header", NULL, 0);
+	    return n;
+	}
+	/* size is stored in network byte order */
+	if (hdr.flags & ZFHD_EOFB)
+	    zfread_eof = 1;
+	blksz = (hdr.bytes[0] << 8) | hdr.bytes[1];
+	if (blksz > sz) {
+	    /*
+	     * See comments in file headers
+	     */
+	    zwarnnam("zftp", "block too large to handle", NULL, 0);
+	    errno = EIO;
+	    return -1;
+	}
+	bfptr = bf;
+	cnt = blksz;
+	while (cnt) {
+	    n = zfread(fd, bfptr, cnt, tmout);
+	    if (n > 0) {
+		bfptr += n;
+		cnt -= n;
+	    } else if (n < 0 && (errflag || zfdrrrring || errno != EINTR))
+		return n;
+	    else
+		break;
+	}
+	if (cnt) {
+	    zwarnnam("zftp", "short data block", NULL, 0);
+	    errno = EIO;
+	    return -1;
+	}
+    } while ((hdr.flags & ZFHD_MARK) && !zfread_eof);
+    return (hdr.flags & ZFHD_MARK) ? 0 : blksz;
+}
+
+/* Version of zfwrite when we need to write in block mode. */
+
+/**/
+static int
+zfwrite_block(int fd, char *bf, size_t sz, int tmout)
+{
+    int n;
+    struct zfheader hdr;
+    size_t cnt;
+    char *bfptr;
+    /* we need the header */
+    do {
+	hdr.bytes[0] = (sz & 0xff00) >> 8;
+	hdr.bytes[1] = sz & 0xff;
+	hdr.flags = sz ? 0 : ZFHD_EOFB;
+	n = zfwrite(fd, (char *)&hdr, sizeof(hdr), tmout);
+    } while (n < 0 && errno == EINTR);
+    if (n != 3 && !zfdrrrring) {
+	zwarnnam("zftp", "failed to write FTP block header", NULL, 0);
+	return n;
+    }
+    bfptr = bf;
+    cnt = sz;
+    while (cnt) {
+	n = zfwrite(fd, bfptr, cnt, tmout);
+	if (n > 0) {
+	    bfptr += n;
+	    cnt -= n;
+	} else if (n < 0 && (errflag || zfdrrrring || errno != EINTR))
+	    return n;
+    }
+
+    return sz;
+}
+
+/*
+ * Move stuff from fdin to fdout, tidying up the data connection
+ * when finished.  The data connection could be either input or output:
+ * recv is 1 for receiving a file, 0 for sending.
+ *
+ * progress is 1 to use a progress meter.
+ * startat says how far in we're starting with a REST command.
+ *
+ * Since we're doing some buffering here anyway, we don't bother
+ * with a stdio layer.
+ */
+
+/**/
+static int
+zfsenddata(char *name, int recv, int progress, long startat)
+{
+#define ZF_BUFSIZE 32768
+#define ZF_ASCSIZE (ZF_BUFSIZE/2)
+    /* ret = 2 signals the local read/write failed, so send abort */
+    int n, ret = 0, gotack = 0, fdin, fdout, fromasc = 0, toasc = 0;
+    int rtmout = 0, wtmout = 0;
+    char lsbuf[ZF_BUFSIZE], *ascbuf = NULL, *optr;
+    long sofar = 0, last_sofar = 0;
+    readwrite_t read_ptr = zfread, write_ptr = zfwrite;
+    List l;
+
+    if (progress && (l = getshfunc("zftp_progress")) != &dummy_list) {
+	/*
+	 * progress to set up:  ZFTP_COUNT is zero.
+	 * We do this here in case we needed to wait for a RETR
+	 * command to tell us how many bytes are coming.
+	 */
+	doshfunc("zftp_progress", l, NULL, 0, 1);
+	/* Now add in the bit of the file we've got/sent already */
+	sofar = last_sofar = startat;
+    }
+    if (recv) {
+	fdin = zdfd;
+	fdout = 1;
+	rtmout = getiparam("ZFTP_TMOUT");
+	if (ZFST_CTYP(zfstatus) == ZFST_ASCI)
+	    fromasc = 1;
+	if (ZFST_MODE(zfstatus) == ZFST_BLOC)
+	    read_ptr = zfread_block;
+    } else {
+	fdin = 0;
+	fdout = zdfd;
+	wtmout = getiparam("ZFTP_TMOUT");
+	if (ZFST_CTYP(zfstatus) == ZFST_ASCI)
+	    toasc = 1;
+	if (ZFST_MODE(zfstatus) == ZFST_BLOC)
+	    write_ptr = zfwrite_block;
+    }
+
+    if (toasc)
+	ascbuf = zalloc(ZF_ASCSIZE);
+    zfpipe();
+    zfread_eof = 0;
+    while (!ret && !zfread_eof) {
+	n = (toasc) ? read_ptr(fdin, ascbuf, ZF_ASCSIZE, rtmout)
+	    : read_ptr(fdin, lsbuf, ZF_BUFSIZE, rtmout);
+	if (n > 0) {
+	    char *iptr;
+	    if (toasc) {
+		/* \n -> \r\n it shouldn't happen to a dog. */
+		char *iptr = ascbuf, *optr = lsbuf;
+		int cnt = n;
+		while (cnt--) {
+		    if (*iptr == '\n') {
+			*optr++ = '\r';
+			n++;
+		    }
+		    *optr++ = *iptr++;
+		}
+	    }
+	    if (fromasc && (iptr = memchr(lsbuf, '\r', n))) {
+		/* \r\n -> \n */
+		char *optr = iptr;
+		int cnt = n - (iptr - lsbuf);
+		while (cnt--) {
+		    if (*iptr != '\r' || iptr[1] != '\n') {
+			*optr++ = *iptr;
+		    } else
+			n--;
+		    iptr++;
+		}
+	    }
+	    optr = lsbuf;
+
+	    sofar += n;
+
+	    for (;;) {
+		/*
+		 * in principle, write can be interrupted after
+		 * safely writing some bytes, and will return the
+		 * number already written, which may not be the
+		 * complete buffer.  so make this robust.  they call me
+		 * `robustness stephenson'.  in my dreams.
+		 */
+		int newn = write_ptr(fdout, optr, n, wtmout);
+		if (newn == n)
+		    break;
+		if (newn < 0) {
+		    /*
+		     * The somewhat contorted test here (for write)
+		     * and below (for read) means:
+		     * real error if
+		     *  - errno is set and it's not just an interrupt, or
+		     *  - errflag is set, probably due to CTRL-c, or
+		     *  - zfdrrrring is set, due to the alarm going off.
+		     * print an error message if
+		     *  - not a timeout, since that was reported, and
+		     *    either
+		     *    - a non-interactive shell, where we don't
+		     *      know what happened otherwise
+		     *    - or both of
+		     *      - not errflag, i.e. CTRL-c or what have you,
+		     *        since the user probably knows about that, and
+		     *      - not a SIGPIPE, since usually people are
+		     *        silent about those when going to pagers
+		     *        (if you quit less or more in the middle
+		     *        and see an error message you think `I
+		     *        shouldn't have done that').
+		     *
+		     * If we didn't print an error message here,
+		     * and were going to do an abort (ret == 2)
+		     * because the error happened on the local end
+		     * of the connection, set ret to 3 and don't print
+		     * the 'aborting...' either.
+		     *
+		     * There must be a better way of doing this.
+		     */
+		    if (errno != EINTR || errflag || zfdrrrring) {
+			if (!zfdrrrring &&
+			    (!interact || (!errflag && errno != EPIPE))) {
+			    ret = recv ? 2 : 1;
+			    zwarnnam(name, "write failed: %e", NULL, errno);
+			} else
+			    ret = recv ? 3 : 1;
+			break;
+		    }
+		    continue;
+		}
+		optr += newn;
+		n -= newn;
+	    }
+	} else if (n < 0) {
+	    if (errno != EINTR || errflag || zfdrrrring) {
+		if (!zfdrrrring &&
+		    (!interact || (!errflag && errno != EPIPE))) {
+		    ret = recv ? 1 : 2;
+		    zwarnnam(name, "read failed: %e", NULL, errno);
+		} else
+		    ret = recv ? 1 : 3;
+		break;
+	    }
+	} else
+	    break;
+	if (!ret && sofar != last_sofar && progress &&
+	    (l = getshfunc("zftp_progress")) != &dummy_list) {
+	    zfsetparam("ZFTP_COUNT", &sofar, ZFPM_READONLY|ZFPM_INTEGER);
+	    doshfunc("zftp_progress", l, NULL, 0, 1);
+	    last_sofar = sofar;
+	}
+    }
+    zfunpipe();
+    /*
+     * At this point any timeout was on the data connection,
+     * so we don't need to force the control connection to close.
+     */
+    zfdrrrring = 0;
+    if (!errflag && !ret && !recv && ZFST_MODE(zfstatus) == ZFST_BLOC) {
+	/* send an end-of-file marker block */
+	ret = (zfwrite_block(fdout, lsbuf, 0, wtmout) < 0);
+    }
+    if (errflag || ret > 1) {
+	/*
+	 * some error occurred, maybe a keyboard interrupt, or
+	 * a local file/pipe handling problem.
+	 * send an abort.
+	 *
+	 * safest to block all signals here?  can get frustrating if
+	 * we're waiting for an abort.  don't I know.  let's start
+	 * off just by blocking SIGINT's.
+	 *
+	 * maybe the timeout for the abort should be shorter than
+	 * for normal commands.  and what about aborting after
+	 * we had a timeout on the data connection, is that
+	 * really a good idea?
+	 */
+	/* RFC 959 says this is what to send */
+	unsigned char msg[4] = { IAC, IP, IAC, SYNCH };
+
+	if (ret == 2)
+	    zwarnnam(name, "aborting data transfer...", NULL, 0);
+
+	holdintr();
+
+	/* the following is black magic, as far as I'm concerned. */
+	/* what are we going to do if it fails?  not a lot, actually. */
+	send(zcfd, (char *)msg, 3, 0);
+	send(zcfd, (char *)msg+3, 1, MSG_OOB);
+
+	zfsendcmd("ABOR\r\n");
+	if (lastcode == 226) {
+	    /*
+	     * 226 is supposed to mean the transfer got sent OK after
+	     * all, and the abort got ignored, at least that's what
+	     * rfc959 seems to be saying.  but in fact what can happen
+	     * is the transfer finishes (at least as far as the
+	     * server's concerned) and it's response is waiting, then
+	     * the abort gets sent, and we need to mop up a response to
+	     * that.  so actually in most cases we get two replies
+	     * anyway.  we could test if we had select() on all hosts.
+	     */
+	    /* gotack = 1; */
+	    /*
+	     * we'd better leave errflag, since we don't know
+	     * where it came from.  maybe the user wants to abort
+	     * a whole script or function.
+	     */
+	} else
+	    ret = 1;
+
+	noholdintr();
+    }
+	
+    if (toasc)
+	zfree(ascbuf, ZF_ASCSIZE);
+    zfclosedata();
+    if (!gotack && zfgetmsg() > 2)
+	ret = 1;
+    return ret != 0;
+}
+
+/* Open a new control connection, i.e. start a new FTP session */
+
+/**/
+static int
+zftp_open(char *name, char **args, int flags)
+{
+    struct in_addr ipaddr;
+    struct protoent *zprotop;
+    struct servent *zservp;
+    struct hostent *zhostp = NULL;
+    char **addrp, tbuf[2] = "X", *fname;
+    int err, len, tmout;
+
+    if (!*args) {
+	if (zfuserparams)
+	    args = zfuserparams;
+	else {
+	    zwarnnam(name, "no host specified", NULL, 0);
+	    return 1;
+	}
+    }
+
+    /*
+     * Close the existing connection if any.
+     * Probably this is the safest thing to do.  It's possible
+     * a `QUIT' will hang, though.
+     */
+    if (zcfd != -1)
+	zfclose();
+
+    /* this is going to give 0.  why bother? */
+    zprotop = getprotobyname("tcp");
+    zservp = getservbyname("ftp", "tcp");
+
+    if (!zprotop || !zservp) {
+	zwarnnam(name, "Somebody stole FTP!", NULL, 0);
+	return 1;
+    }
+
+    /* don't try talking to server yet */
+    zcfinish = 2;
+
+    /*
+     * This sets an alarm for the whole process, getting the host name
+     * as well as connecting.  Arguably you could time them out separately. 
+     */
+    tmout = getiparam("ZFTP_TMOUT");
+    if (setjmp(zfalrmbuf)) {
+	char *hname;
+	alarm(0);
+	if ((hname = getsparam("ZFTP_HOST")) && *hname) 
+	    zwarnnam(name, "timeout connecting to %s", hname, 0);
+	else
+	    zwarnnam(name, "timeout on host name lookup", NULL, 0);
+	zfclose();
+	return 1;
+    }
+    zfalarm(tmout);
+
+    /*
+     * Now this is what I like.  A library which provides decent higher
+     * level functions to do things like converting address types.  It saves
+     * so much trouble.  Pity about the rest of the network interface, though.
+     */
+    ipaddr.s_addr = inet_addr(args[0]);
+    if (ipaddr.s_addr != INADDR_NONE) {
+	/*
+	 * hmmm, we don't necessarily want to do this... maybe the
+	 * user is actively trying to avoid a bad nameserver.
+	 * perhaps better just to set ZFTP_HOST to the dot address, too.
+	 * that way shell functions know how it was opened.
+	 *
+	 * 	zhostp = gethostbyaddr(&ipaddr, sizeof(ipaddr), AF_INET);
+	 *
+	 * or, we could have a `no_lookup' flag.
+	 */
+	zfsetparam("ZFTP_HOST", ztrdup(args[0]), ZFPM_READONLY);
+	zsock.sin_family = AF_INET;
+    } else {
+	zhostp = gethostbyname(args[0]);
+	if (!zhostp || errflag) {
+	    /* should use herror() here if available, but maybe
+	     * needs configure test. on AIX it's present but not
+	     * in headers.
+	     */
+	    zwarnnam(name, "host not found: %s", args[0], 0);
+	    alarm(0);
+	    return 1;
+	}
+	zsock.sin_family = zhostp->h_addrtype;
+	zfsetparam("ZFTP_HOST", ztrdup(zhostp->h_name), ZFPM_READONLY);
+    }
+
+    zsock.sin_port = ntohs(zservp->s_port);
+    zcfd = zfmovefd(socket(zsock.sin_family, SOCK_STREAM, 0));
+    if (zcfd < 0) {
+	zwarnnam(name, "socket failed: %e", NULL, errno);
+	zfunsetparam("ZFTP_HOST");
+	alarm(0);
+	return 1;
+    }
+
+#if defined(F_SETFD) && defined(FD_CLOEXEC)
+    /* If the shell execs a program, we don't want this fd left open. */
+    len = FD_CLOEXEC;
+    fcntl(zcfd, F_SETFD, &len);
+#endif
+
+    /*
+     * now connect the socket.  manual pages all say things like `this is all
+     * explained oh-so-wonderfully in some other manual page'.  not.
+     */
+
+    err = 1;
+
+    if (ipaddr.s_addr != INADDR_NONE) {
+	/* dot address */
+	memcpy(&zsock.sin_addr, &ipaddr, sizeof(ipaddr));
+	do {
+	    err = connect(zcfd, (struct sockaddr *)&zsock, sizeof(zsock));
+	} while (err && errno == EINTR && !errflag);
+    } else {
+	/* host name: try all possible IP's */
+	for (addrp = zhostp->h_addr_list; *addrp; addrp++) {
+	    memcpy(&zsock.sin_addr, *addrp, zhostp->h_length);
+	    do {
+		err = connect(zcfd, (struct sockaddr *)&zsock, sizeof(zsock));
+	    } while (err && errno == EINTR && !errflag);
+	    /* you can check whether it's worth retrying here */
+	}
+    }
+
+    alarm(0);
+
+    if (err < 0) {
+	zwarnnam(name, "connect failed: %e", NULL, errno);
+	zfclose();
+	return 1;
+    }
+    zfsetparam("ZFTP_IP", ztrdup(inet_ntoa(zsock.sin_addr)), ZFPM_READONLY);
+    /* now we can talk to the control connection */
+    zcfinish = 0;
+
+    len = sizeof(zsock);
+    if (getsockname(zcfd, (struct sockaddr *)&zsock, &len) < 0) {
+	zwarnnam(name, "getsockname failed: %e", NULL, errno);
+	zfclose();
+	return 1;
+    }
+    /* nice to get some options right, ignore if they don't work */
+#ifdef SO_OOBINLINE
+    /*
+     * this says we want messages in line.  maybe sophisticated people
+     * do clever things with SIGURG.
+     */
+    len = 1;
+    setsockopt(zcfd, SOL_SOCKET, SO_OOBINLINE, (char *)&len, sizeof(len));
+#endif
+#if defined(IP_TOS) && defined(IPTOS_LOWDELAY)
+    /* for control connection we want low delay.  please don't laugh. */
+    len = IPTOS_LOWDELAY;
+    setsockopt(zcfd, IPPROTO_IP, IP_TOS, (char *)&len, sizeof(len));
+#endif
+
+    /*
+     * We use stdio with line buffering for convenience on input.
+     * On output, we can just dump a complete message to the fd via write().
+     */
+    zcin = fdopen(zcfd, "r");
+
+    if (!zcin) {
+	zwarnnam(name, "file handling error", NULL, 0);
+	zfclose();
+	return 1;
+    }
+
+#ifdef _IONBF
+    setvbuf(zcin, NULL, _IONBF, 0);
+#else
+    setlinebuf(zcin);
+#endif
+
+    /*
+     * now see what the remote server has to say about that.
+     */
+    if (zfgetmsg() >= 4) {
+	zfclose();
+	return 1;
+    }
+
+    zfis_unix = 0;
+    zfhas_size = zfhas_mdtm = ZFCP_UNKN;
+    zdfd = -1;
+    /* initial status: open, ASCII data, stream mode 'n' stuff */
+    zfstatus = 0;
+
+    /* open file for saving the current status */
+    fname = gettempname();
+    zfstatfd = open(fname, O_RDWR|O_CREAT, 0600);
+    DPUTS(zfstatfd == -1, "zfstatfd not created");
+#if defined(F_SETFD) && defined(FD_CLOEXEC)
+    /* If the shell execs a program, we don't want this fd left open. */
+    len = FD_CLOEXEC;
+    fcntl(zfstatfd, F_SETFD, &len);
+#endif
+    unlink(fname);
+
+    /* now find out what system we're connected to */
+    if (!(zfprefs & ZFPF_DUMB) && zfsendcmd("SYST\r\n") == 2) {
+	char *ptr = lastmsg, *eptr, *systype;
+	for (eptr = ptr; *eptr; eptr++)
+	    ;
+	systype = ztrduppfx(ptr, eptr-ptr);
+	if (!strncmp(systype, "UNIX Type: L8", 13)) {
+	    /*
+	     * Use binary for transfers.  This simple test saves much
+	     * hassle for all concerned, particularly me.
+	     */
+	    zfstatus |= ZFST_IMAG;
+	    zfis_unix = 1;
+	}
+	/*
+	 * we could set zfis_unix based just on the UNIX part,
+	 * but I don't really know the consequences of that.
+	 */
+	zfsetparam("ZFTP_SYSTEM", systype, ZFPM_READONLY);
+    } else if (zcfd == -1) {
+	/* final paranoid check */
+	return 1;
+    }
+	
+    tbuf[0] = (ZFST_TYPE(zfstatus) == ZFST_ASCI) ? 'A' : 'I';
+    zfsetparam("ZFTP_TYPE", ztrdup(tbuf), ZFPM_READONLY);
+    zfsetparam("ZFTP_MODE", ztrdup("S"), ZFPM_READONLY);
+    /* if remaining arguments, use them to log in. */
+    if (zcfd > -1 && *++args)
+	return zftp_login(name, args, flags);
+    /* if something wayward happened, connection was already closed */
+    return zcfd == -1;
+}
+
+/*
+ * Read a parameter string, with a prompt if reading from stdin.
+ * The returned string is on the heap.
+ * If noecho, turn off ECHO mode while reading.
+ */
+
+/**/
+static char *
+zfgetinfo(char *prompt, int noecho)
+{
+    int resettty = 0;
+    /* 256 characters should be enough, hardly worth allocating
+     * a password string byte by byte
+     */
+    char instr[256], *strret;
+    int len;
+
+    /*
+     * Only print the prompt if getting info from a tty.  Of
+     * course, we don't know if stderr has been redirected, but
+     * that seems a minor point.
+     */
+    if (isatty(0)) {
+	if (noecho) {
+	    /* hmmm... all this great big shell and we have to read
+	     * something with no echo by ourselves.
+	     * bin_read() is far to complicated for our needs.
+	     * we could use zread(), but that relies on static
+	     * variables, so someone doesn't want that to happen.
+	     *
+	     * this is modified from setcbreak() in utils.c,
+	     * except I don't see any point in using cbreak mode
+	     */
+	    struct ttyinfo ti;
+
+	    ti = shttyinfo;
+#ifdef HAS_TIO
+	    ti.tio.c_lflag &= ~ECHO;
+#else
+	    ti.sgttyb.sg_flags &= ~ECHO;
+#endif
+	    settyinfo(&ti);
+	    resettty = 1;
+	}
+	fflush(stdin);
+	fputs(prompt, stderr);
+	fflush(stderr);
+    }
+
+    fgets(instr, 256, stdin);
+    if (instr[len = strlen(instr)-1] == '\n')
+	instr[len] = '\0';
+
+    strret = dupstring(instr);
+
+    if (resettty) {
+	/* '\n' didn't get echoed */
+	fputc('\n', stdout);
+	fflush(stdout);
+    	settyinfo(&shttyinfo);
+    }
+
+    return strret;
+}
+
+/*
+ * set params for an open with no arguments.
+ * this allows easy re-opens.
+ */
+
+/**/
+static int
+zftp_params(char *name, char **args, int flags)
+{
+    char *prompts[] = { "Host: ", "User: ", "Password: ", "Account: " };
+    char **aptr, **newarr;
+    int i, j, len;
+
+    if (!*args) {
+	if (zfuserparams) {
+	    for (aptr = zfuserparams, i = 0; *aptr; aptr++, i++) {
+		if (i == 2) {
+		    len = strlen(*aptr);
+		    for (j = 0; j < len; j++)
+			fputc('*', stdout);
+		    fputc('\n', stdout);
+		} else
+		    fprintf(stdout, "%s\n", *aptr);
+	    }
+	}
+	return 0;
+    }
+    if (!strcmp(*args, "-")) {
+	if (zfuserparams)
+	    freearray(zfuserparams);
+	zfuserparams = 0;
+	return 0;
+    }
+    len = arrlen(args);
+    newarr = (char **)zcalloc((len+1)*sizeof(char *));
+    for (aptr = args, i = 0; *aptr && !errflag; aptr++, i++) {
+	char *str;
+	if (!strcmp(*aptr, "?"))
+	    str = zfgetinfo(prompts[i], i == 2);
+	else
+	    str = *aptr;
+	newarr[i] = ztrdup(str);
+    }
+    if (errflag) {
+	/* maybe user CTRL-c'd in the middle somewhere */
+	for (aptr = newarr; *aptr; aptr++)
+	    zsfree(*aptr);
+	zfree(newarr, len+1);
+	return 1;
+    }
+    if (zfuserparams)
+	freearray(zfuserparams);
+    zfuserparams = newarr;
+    return 0;
+}
+
+/* login a user:  often called as part of the open sequence */
+
+/**/
+static int
+zftp_login(char *name, char **args, int flags)
+{
+    char *ucmd, *passwd = NULL, *acct = NULL;
+    char *user;
+    int stopit;
+
+    if ((zfstatus & ZFST_LOGI) && zfsendcmd("REIN\r\n") >= 4)
+	return 1;
+
+    zfstatus &= ~ZFST_LOGI;
+    if (*args) {
+	user = *args++;
+    } else {
+	user = zfgetinfo("User: ", 0);
+    }
+
+    ucmd = tricat("USER ", user, "\r\n");
+    stopit = 0;
+
+    if (zfsendcmd(ucmd) == 6)
+	stopit = 2;
+
+    while (!stopit && !errflag) {
+	switch (lastcode) {
+	case 230: /* user logged in */
+	case 202: /* command not implemented, don't care */
+	    stopit = 1;
+	    break;
+
+	case 331: /* need password */
+	    if (*args)
+		passwd = *args++;
+	    else
+		passwd = zfgetinfo("Password: ", 1);
+	    zsfree(ucmd);
+	    ucmd = tricat("PASS ", passwd, "\r\n");
+	    if (zfsendcmd(ucmd) == 6)
+		stopit = 2;
+	    break;
+
+	case 332: /* need account */
+	case 532:
+	    if (*args)
+		acct = *args++;
+	    else
+		acct = zfgetinfo("Account: ", 0);
+	    zsfree(ucmd);
+	    ucmd = tricat("ACCT ", passwd, "\r\n");
+	    if (zfsendcmd(ucmd) == 6)
+		stopit = 2;
+	    break;
+
+	case 421: /* service not available, so closed anyway */
+	case 501: /* syntax error */
+	case 503: /* bad commands */
+	case 530: /* not logged in */
+	case 550: /* random can't-do-that */
+	default:  /* whatever, should flag this as bad karma */
+	    /* need more diagnostics here */
+	    stopit = 2;
+	    break;
+	}
+    }
+
+    zsfree(ucmd);
+    if (zcfd == -1)
+	return 1;
+    if (stopit == 2 || (lastcode != 230 && lastcode != 202)) {
+	zwarnnam(name, "login failed", NULL, 0);
+	return 1;
+    }
+
+    if (*args) {
+	int cnt;
+	for (cnt = 0; *args; args++)
+	    cnt++;
+	zwarnnam(name, "warning: %d comand arguments not used\n", NULL, cnt);
+    }
+    zfstatus |= ZFST_LOGI;
+    zfsetparam("ZFTP_USER", ztrdup(user), ZFPM_READONLY);
+    if (acct)
+	zfsetparam("ZFTP_ACCOUNT", ztrdup(acct), ZFPM_READONLY);
+
+    /*
+     * Get the directory.  This is possibly an unnecessary overhead, of
+     * course, but when you're being driven by shell functions there's
+     * just no way of telling.
+     */
+    return zfgetcwd();
+}
+
+/* do ls or dir on the remote directory */
+
+/**/
+static int
+zftp_dir(char *name, char **args, int flags)
+{
+    /* maybe should be cleverer about handling arguments */
+    char *cmd;
+    int ret;
+
+    /*
+     * RFC959 says this must be ASCII or EBCDIC, not image format.
+     * I rather suspect on a UNIX server we get away handsomely
+     * with doing everything, including this, as image.
+     */
+    zfsettype(ZFST_ASCI);
+
+    cmd = zfargstring((flags & ZFTP_NLST) ? "NLST" : "LIST", args);
+    ret = zfgetdata(name, NULL, cmd, 0);
+    zsfree(cmd);
+    if (ret)
+	return 1;
+
+    fflush(stdout);		/* since we're now using fd 1 */
+    return zfsenddata(name, 1, 0, 0);
+}
+
+/* change the remote directory */
+
+/**/
+static int
+zftp_cd(char *name, char **args, int flags)
+{
+    /* change directory --- enhance to allow 'zftp cdup' */
+    int ret;
+
+    if ((flags & ZFTP_CDUP) || !strcmp(*args, "..") ||
+	!strcmp(*args, "../")) {
+	ret = zfsendcmd("CDUP\r\n");
+    } else {
+	char *cmd = tricat("CWD ", *args, "\r\n");
+	ret = zfsendcmd(cmd);
+	zsfree(cmd);
+    }
+    if (ret > 2)
+	return 1;
+    /* sometimes the directory may be in the response. usually not. */
+    if (zfgetcwd())
+	return 1;
+
+    return 0;
+}
+
+/* get the remote directory */
+
+/**/
+static int
+zfgetcwd(void)
+{
+    char *ptr, *eptr;
+    int endc;
+    List l;
+
+    if (zfprefs & ZFPF_DUMB)
+	return 1;
+    if (zfsendcmd("PWD\r\n") > 2) {
+	zfunsetparam("ZFTP_PWD");
+	return 1;
+    }
+    ptr = lastmsg;
+    while (*ptr == ' ')
+	ptr++;
+    if (!*ptr)			/* ultra safe */
+	return 1;
+    if (*ptr == '"') {
+	ptr++;
+	endc = '"';
+    } else 
+	endc = ' ';
+    for (eptr = ptr; *eptr && *eptr != endc; eptr++)
+	;
+    zfsetparam("ZFTP_PWD", ztrduppfx(ptr, eptr-ptr), ZFPM_READONLY);
+
+    /*
+     * This isn't so necessary if we're going to have a shell function
+     * front end.  By putting it here, and in close when ZFTP_PWD is unset,
+     * we at least cover the bases.
+     */
+    if ((l = getshfunc("zftp_chpwd")) != &dummy_list)
+	doshfunc("zftp_chpwd", l, NULL, 0, 1);
+
+    return 0;
+}
+
+/*
+ * Set the type for the next transfer, usually image (binary) or ASCII.
+ */
+
+/**/
+static int
+zfsettype(int type)
+{
+    char buf[] = "TYPE X\r\n";
+    if (ZFST_TYPE(type) == ZFST_CTYP(zfstatus))
+	return 0;
+    buf[5] = (ZFST_TYPE(type) == ZFST_ASCI) ? 'A' : 'I';
+    if (zfsendcmd(buf) > 2)
+	return 1;
+    zfstatus &= ~(ZFST_TMSK << ZFST_TBIT);
+    /* shift the type left to set the current type bits */;
+    zfstatus |= type << ZFST_TBIT;
+    return 0;
+}
+
+/*
+ * Print or get a new type for the transfer.
+ * We don't actually set the type at this point.
+ */
+
+/**/
+static int
+zftp_type(char *name, char **args, int flags)
+{
+    char *str, nt, tbuf[2] = "A";
+    if (flags & (ZFTP_TBIN|ZFTP_TASC)) {
+	nt = (flags & ZFTP_TBIN) ? 'I' : 'A';
+    } else if (!(str = *args)) {
+	/*
+	 * Since this is supposed to be a low-level basis for
+	 * an FTP system, just print the single code letter.
+	 */
+	printf("%c\n", (ZFST_TYPE(zfstatus) == ZFST_ASCI) ? 'A' : 'I');
+	fflush(stdout);
+	return 0;
+    } else {
+	nt = toupper(*str);
+	/*
+	 * RFC959 specifies other types, but these are the only
+	 * ones we know what to do with.
+	 */
+	if (str[1] || (nt != 'A' && nt != 'B' && nt != 'I')) {
+	    zwarnnam(name, "transfer type %s not recognised", str, 0);
+	    return 1;
+	}
+	
+	if (nt == 'B')		/* binary = image */
+	    nt = 'I';
+    }
+
+    zfstatus &= ~ZFST_TMSK;
+    zfstatus |= (nt == 'I') ? ZFST_IMAG : ZFST_ASCI;
+    tbuf[0] = nt;
+    zfsetparam("ZFTP_TYPE", ztrdup(tbuf), ZFPM_READONLY);
+    return 0;
+}
+
+/**/
+static int
+zftp_mode(char *name, char **args, int flags)
+{
+    char *str, cmd[] = "MODE X\r\n";
+    int nt;
+
+    if (!(str = *args)) {
+	printf("%c\n", (ZFST_MODE(zfstatus) == ZFST_STRE) ? 'S' : 'B');
+	fflush(stdout);
+	return 0;
+    }
+    nt = str[0] = toupper(*str);
+    if (str[1] || (nt != 'S' && nt != 'B')) {
+	zwarnnam(name, "transfer mode %s not recognised", str, 0);
+	return 1;
+    }
+    cmd[5] = (char) nt;
+    if (zfsendcmd(cmd) > 2)
+	return 1;
+    zfstatus &= ZFST_MMSK;
+    zfstatus |= (nt == 'S') ? ZFST_STRE : ZFST_BLOC;
+    zfsetparam("ZFTP_MODE", ztrdup(str), ZFPM_READONLY);
+    return 0;
+}
+
+/**/
+static int
+zftp_local(char *name, char **args, int flags)
+{
+    int more = !!args[1], ret = 0, dofd = !*args;
+    while (*args || dofd) {
+	long sz;
+	char *mt;
+	int newret = zfstats(*args, !(flags & ZFTP_HERE), &sz, &mt,
+			     dofd ? 0 : -1);
+	if (newret == 2)	/* at least one is not implemented */
+	    return 2;
+	else if (newret) {
+	    ret = 1;
+	    if (mt)
+		zsfree(mt);
+	    args++;
+	    continue;
+	}
+	if (more) {
+	    fputs(*args, stdout);
+	    fputc(' ', stdout);
+	}
+	printf("%ld %s\n", sz, mt);
+	zsfree(mt);
+	if (dofd)
+	    break;
+	args++;
+    }
+    fflush(stdout);
+
+    return ret;
+}
+
+/*
+ * Generic transfer for get, put and append.
+ *
+ * Get sends all files to stdout, i.e. this is basically cat. It's up to a
+ * shell function driver to turn this into standard FTP-like commands.
+ *
+ * Put/append sends everything from stdin down the drai^H^H^Hata connection. 
+ * Slightly weird with multiple files in that it tries to read
+ * a separate complete file from stdin each time, which is
+ * only even potentially useful interactively.  But the only
+ * real alternative is just to allow one file at a time.
+ */
+
+/**/
+static int
+zftp_getput(char *name, char **args, int flags)
+{
+    int ret = 0, recv = (flags & ZFTP_RECV), getsize = 0, progress = 1;
+    char *cmd = recv ? "RETR " : (flags & ZFTP_APPE) ? "APPE " : "STOR ";
+    List l;
+
+    /*
+     * At this point I'd like to set progress to 0 if we're
+     * backgrounded, since it's hard for the user to find out.
+     * It turns out it's hard enough for us to find out.
+     * The problem is that zsh clears it's job table, so we
+     * just don't know if we're some forked shell in a pipeline
+     * somewhere or in the background.  This seems to me a problem.
+     */
+
+    zfsettype(ZFST_TYPE(zfstatus));
+
+    if (recv)
+	fflush(stdout);		/* since we may be using fd 1 */
+    for (; *args; args++) {
+	char *ln, *rest = NULL;
+	long startat = 0;
+	if (progress && (l = getshfunc("zftp_progress")) != &dummy_list) {
+	    long sz;
+	    /*
+	     * This calls the SIZE command to get the size for remote
+	     * files.  Some servers send the size with the reply to
+	     * the transfer command (i.e. RETR), in which
+	     * case we note the fact and don't call this
+	     * next time.  For that reason, the first call
+	     * of zftp_progress is delayed until zfsenddata().
+	     */
+	    if ((!(zfprefs & ZFPF_DUMB) &&
+		 (zfstatus & (ZFST_NOSZ|ZFST_TRSZ)) != ZFST_TRSZ)
+		|| !recv) {
+		/* the final 0 is a local fd to fstat if recv is zero */
+		zfstats(*args, recv, &sz, NULL, 0);
+		/* even if it doesn't support SIZE, it may tell us */
+		if (recv && sz == -1)
+		    getsize = 1;
+	    } else
+		getsize = 1;
+	    zfstarttrans(*args, recv, sz);
+	}
+
+	if (flags & ZFTP_REST) {
+	    startat = zstrtol(args[1], NULL, 10);
+	    rest = tricat("REST ", args[1], "\r\n");
+	}
+
+	ln = tricat(cmd, *args, "\r\n");
+	/* note zdfd doesn't exist till zfgetdata() creates it */
+	if (zfgetdata(name, rest, ln, getsize) ||
+	    zfsenddata(name, recv, progress, startat))
+	    ret = 1;
+	zsfree(ln);
+	if (progress && (l = getshfunc("zftp_progress")) != &dummy_list) {
+	    /* progress to finish: ZFTP_TRANSFER set to GF or PF */
+	    zfsetparam("ZFTP_TRANSFER", ztrdup(recv ? "GF" : "PF"),
+		       ZFPM_READONLY);
+	    doshfunc("zftp_progress", l, NULL, 0, 1);
+	}
+	if (rest) {
+	    zsfree(rest);
+	    args++;
+	}
+	if (errflag)
+	    break;
+    }
+    zfendtrans();
+    return ret;
+}
+
+/*
+ * Delete a list of files on the server.  We allow a list by analogy with
+ * `rm'.
+ */
+
+/**/
+static int
+zftp_delete(char *name, char **args, int flags)
+{
+    int ret = 0;
+    char *cmd, **aptr;
+    for (aptr = args; *aptr; aptr++) {
+	cmd = tricat("DELE ", *aptr, "\r\n");
+	if (zfsendcmd(cmd) > 2)
+	    ret = 1;
+	zsfree(cmd);
+    }
+    return ret;
+}
+
+/* Create or remove a directory on the server */
+
+/**/
+static int
+zftp_mkdir(char *name, char **args, int flags)
+{
+    int ret;
+    char *cmd = tricat((flags & ZFTP_DELE) ? "RMD " : "MKD ",
+		       *args, "\r\n");
+    ret = (zfsendcmd(cmd) > 2);
+    zsfree(cmd);
+    return ret;
+}
+
+/* Rename a file on the server */
+
+/**/
+static int
+zftp_rename(char *name, char **args, int flags)
+{
+    int ret;
+    char *cmd;
+
+    cmd = tricat("RNFR ", args[0], "\r\n");
+    ret = 1;
+    if (zfsendcmd(cmd) == 3) {
+	zsfree(cmd);
+	cmd = tricat("RNTO ", args[1], "\r\n");
+	if (zfsendcmd(cmd) == 2)
+	    ret = 0;
+    }
+    zsfree(cmd);
+    return ret;
+}
+
+/*
+ * Send random commands, either with SITE or literal.
+ * In the second case, the user better know what they're doing.
+ */
+
+/**/
+static int
+zftp_quote(char *name, char **args, int flags)
+{
+    int ret = 0;
+    char *cmd;
+
+    cmd = (flags & ZFTP_SITE) ? zfargstring("SITE", args)
+	: zfargstring(args[0], args+1);
+    ret = (zfsendcmd(cmd) > 2);
+    zsfree(cmd);
+
+    return ret;
+}
+
+/* Close the connection, ending the session */
+
+/**/
+static int
+zftp_close(char *name, char **args, int flags)
+{
+    char **aptr;
+    List l;
+    zfclosing = 1;
+    if (zcfinish != 2) {
+	/*
+	 * haven't had EOF from server, so send a QUIT and get the response.
+	 * maybe we should set a shorter timeout for this to avoid
+	 * CTRL-c rage.
+	 */
+	zfsendcmd("QUIT\r\n");
+    }
+    if (zcin)
+	fclose(zcin);
+    zcin = NULL;
+    close(zcfd);
+    zcfd = -1;
+
+    /* Write the final status in case this is a subshell */
+    zfstatus |= ZFST_CLOS;
+    lseek(zfstatfd, 0, 0);
+    write(zfstatfd, &zfstatus, sizeof(zfstatus));
+    close(zfstatfd);
+    zfstatfd = -1;
+
+    /* Unset the non-special parameters */
+    for (aptr = zfparams; *aptr; aptr++)
+	zfunsetparam(*aptr);
+
+    /* Now ZFTP_PWD is unset.  It's up to zftp_chpwd to notice. */
+    if ((l = getshfunc("zftp_chpwd")) != &dummy_list)
+	doshfunc("zftp_chpwd", l, NULL, 0, 1);
+
+    /* tidy up status variables, because mess is bad */
+    zfclosing = zfdrrrring = 0;
+
+    return 0;
+}
+
+/* Safe front end to zftp_close() from within the package */
+
+/**/
+static void
+zfclose(void)
+{
+    if (zcfd != -1)
+	zftp_close("zftp close", NULL, 0);
+}
+
+/* The builtin command frontend to the rest of the package */
+
+/**/
+static int
+bin_zftp(char *name, char **args, char *ops, int func)
+{
+    char fullname[11] = "zftp ";
+    char *cnam = *args++, *prefs, *ptr;
+    Zftpcmd zptr;
+    int n, ret;
+
+    for (zptr = zftpcmdtab; zptr->nam; zptr++)
+	if (!strcmp(zptr->nam, cnam))
+	    break;
+
+    if (!zptr->nam) {
+	zwarnnam(name, "no such subcommand: %s", cnam, 0);
+	return 1;
+    }
+
+    /* check number of arguments */
+    for (n = 0; args[n]; n++)
+	;
+    if (n < zptr->min || (zptr->max != -1 && n > zptr->max)) {
+	zwarnnam(name, "wrong no. of arguments for %s", cnam, 0);
+	return 1;
+    }
+
+    strcat(fullname, cnam);
+    if (zfstatfd != -1) {
+	/* Get the status in case it was set by a forked process */
+	int oldstatus = zfstatus;
+	lseek(zfstatfd, 0, 0);
+	read(zfstatfd, &zfstatus, sizeof(zfstatus));
+	if (zcfd != -1 && (zfstatus & ZFST_CLOS)) {
+	    /* got closed in subshell without us knowing */
+	    zcfinish = 2;
+	    zfclose();
+	} else {
+	    /*
+	     * fix up status types: unfortunately they may already
+	     * have been looked at between being changed in the subshell
+	     * and now, but we can't help that.
+	     */
+	    if (ZFST_TYPE(oldstatus) != ZFST_TYPE(zfstatus))
+		zfsetparam("ZFTP_TYPE",
+			   ztrdup(ZFST_TYPE(zfstatus) == ZFST_ASCI ?
+				  "A" : "I"), ZFPM_READONLY);
+	    if (ZFST_MODE(oldstatus) != ZFST_MODE(zfstatus))
+		zfsetparam("ZFTP_MODE",
+			   ztrdup(ZFST_MODE(zfstatus) == ZFST_BLOC ?
+				  "B" : "S"), ZFPM_READONLY);
+	}
+    }
+    if ((zptr->flags & ZFTP_CONN) && zcfd == -1) {
+	zwarnnam(fullname, "not connected.", NULL, 0);
+	return 1;
+    }
+
+    if ((prefs = getsparam("ZFTP_PREFS"))) {
+	zfprefs = 0;
+	for (ptr = prefs; *ptr; ptr++) {
+	    switch (toupper(*ptr)) {
+	    case 'S':
+		/* sendport */
+		zfprefs |= ZFPF_SNDP;
+		break;
+
+	    case 'P':
+		/*
+		 * passive
+		 * If we have already been told to use sendport mode,
+		 * we're never going to use passive mode.
+		 */
+		if (!(zfprefs & ZFPF_SNDP))
+		    zfprefs |= ZFPF_PASV;
+		break;
+
+	    case 'D':
+		/* dumb */
+		zfprefs |= ZFPF_DUMB;
+		break;
+
+	    default:
+		zwarnnam(name, "preference %c not recognized", NULL, *ptr);
+		break;
+	    }
+	}
+    }
+
+    ret = (*zptr->fun)(fullname, args, zptr->flags);
+
+    if (zfalarmed)
+	zfunalarm();
+    if (zfdrrrring) {
+	/* had a timeout, close the connection */
+	zcfinish = 2;		/* don't try sending QUIT */
+	zfclose();
+    }
+    if (zfstatfd != -1) {
+	/* Set the status in case another process needs to know */
+	lseek(zfstatfd, 0, 0);
+	write(zfstatfd, &zfstatus, sizeof(zfstatus));
+    }
+    return ret;
+}
+
+/* The load/unload routines required by the zsh library interface */
+
+/**/
+int
+boot_zftp(Module m)
+{
+    int ret;
+    if ((ret = addbuiltins(m->nam, bintab,
+			   sizeof(bintab)/sizeof(*bintab))) == 1) {
+	/* if successful, set some default parameters */
+	long tmout_def = 60;
+	zfsetparam("ZFTP_VERBOSE", ztrdup("450"), ZFPM_IFUNSET);
+	zfsetparam("ZFTP_TMOUT", &tmout_def, ZFPM_IFUNSET|ZFPM_INTEGER);
+	zfsetparam("ZFTP_PREFS", ztrdup("PS"), ZFPM_IFUNSET);
+	/* default preferences if user deletes variable */
+	zfprefs = ZFPF_SNDP|ZFPF_PASV;
+    }
+    return !ret;
+}
+
+#ifdef MODULE
+
+/**/
+int
+cleanup_zftp(Module m)
+{
+    /*
+     * There are various parameters hanging around, but they're
+     * all non-special so are entirely non-life-threatening.
+     */
+    zfclosedata();
+    zfclose();
+    zsfree(lastmsg);
+    if (zfuserparams)
+	freearray(zfuserparams);
+    deletebuiltins(m->nam, bintab, sizeof(bintab)/sizeof(*bintab));
+    return 0;
+}
+
+#endif
diff --git a/Src/Modules/zftp.mdd b/Src/Modules/zftp.mdd
new file mode 100644
index 000000000..83051ae54
--- /dev/null
+++ b/Src/Modules/zftp.mdd
@@ -0,0 +1,3 @@
+autobins="zftp"
+
+objects="zftp.o"