aboutsummaryrefslogblamecommitdiffstats
path: root/ftp.c
blob: 4a2befac6b883230a9515aa6c22b64934025a2c9 (plain) (tree)
1
2
3
4
5
6
7
8
                                                      





                   
                 


























                                                  
                       







































































                                                                    
                                    



                                     
                                                            





                                               
                                                                               


                                              


                                                                        

                                                        

                                                                           












                                          

                                                                              













                                                                          
                                                                              
































                                                               

                                     




                                 
                                   
                                          
                                                                    
                  
                                










                                                           
                                                      




                                                                     
                                                                  
                                                                          










                                                           
                                                          














                                                                     
           

















                                              
           

 















                                                               
                                    


            




                                                               
                  







                                
                  
                                         










                          

                                 
                            



             
                                                      


            













                                                               


                                                  
                  



                                            





                                  
                     







                                                               
                                                  






                                                        


                               

 
   















                              
                







                                                       
                    



                   

                                                                       









                         
                                   


                        
             

                      
                   
                             
                              



                         

                             











                                                               




                                        


                          

                        
                                                                       



                                                                               
                                              


                              
                                                                  















                                                     
                                                     


                             
                                                                     


                                              
                                              
 

                                              

                  
                                                           


                                    
                    
     
                                                   

                       
                                             

                                                                
                                        

                                                

                                    
     
                  


                       

                                    
                    

           
                            



















                                  
                                      

                                                
                                                            

              
                                                            



                                   
                                              
                                         
                                                  
                               
                                                                


              
                                                                       
         

                                                
            
                                                  



                                       
                            








                                                  
                                   



                                                                                
                                                   













                                                            
                                                                              




                                            
                                   


                     
                                                                         
                          

                                                                          





















                                                            

                                                                           


























                                                            
                                                      
                                                        





















                                                          
                                                                         











                                                            
                                                      
                                                                  



                                                         

                          
                     










                                                       
                                                                                                 





                                  











                                                    












                                              
                                






                                                                            
                














































                                                                      
                                                                         




































                                                  
                         





                                 
                        






































                                                    

                                     
     
                        
 
/* $Id: ftp.c,v 1.19 2002/12/19 15:29:05 ukai Exp $ */
#include <stdio.h>
#include <pwd.h>
#include <Str.h>
#include <ctype.h>
#include <signal.h>
#include <setjmp.h>
#include <time.h>

#include "fm.h"
#include "html.h"
#include "myctype.h"

#ifdef DEBUG
#include <malloc.h>
#endif				/* DEBUG */

#include <sys/socket.h>
#if defined(FTPPASS_HOSTNAMEGEN) || defined(INET6)
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#endif

typedef struct _FTP {
    FILE *rcontrol;
    FILE *wcontrol;
    FILE *data;
} *FTP;

#define FtpError(status) ((status)<0)
#define FTPDATA(ftp) ((ftp)->data)

typedef int STATUS;

static FTP current_ftp;

static Str
read_response1(FTP ftp)
{
    char c;
    Str buf = Strnew();
    while (1) {
	c = getc(ftp->rcontrol);
	if (c == '\r') {
	    c = getc(ftp->rcontrol);
	    if (c == '\n') {
		Strcat_charp(buf, "\r\n");
		break;
	    }
	    else {
		Strcat_char(buf, '\r');
		Strcat_char(buf, c);
	    }
	}
	else if (c == '\n') {
	    Strcat_charp(buf, "\r\n");
	    break;
	}
	else if (feof(ftp->rcontrol))
	    break;
	else
	    Strcat_char(buf, c);
    }
    return buf;
}

Str
read_response(FTP ftp)
{
    Str tmp;

    tmp = read_response1(ftp);
    if (feof(ftp->rcontrol)) {
	return tmp;
    }
    if (tmp->ptr[3] == '-') {
	/* RFC959 4.2 FTP REPLIES */
	/* multi-line response start */
	/* 
	 * Thus the format for multi-line replies is that the
	 * first line will begin with the exact required reply
	 * code, followed immediately by a Hyphen, "-" (also known 
	 * as Minus), followed by text.  The last line will begin
	 * with the same code, followed immediately by Space <SP>, 
	 * optionally some text, and the Telnet end-of-line code. */
	while (1) {
	    tmp = read_response1(ftp);
	    if (feof(ftp->rcontrol)) {
		break;
	    }
	    if (IS_DIGIT(tmp->ptr[0])
		&& IS_DIGIT(tmp->ptr[1])
		&& IS_DIGIT(tmp->ptr[2])
		&& tmp->ptr[3] == ' ') {
		break;
	    }
	}
    }
    return tmp;
}

int
FtpLogin(FTP * ftp_return, char *host, char *user, char *pass)
{
    Str tmp;
    FTP ftp = New(struct _FTP);
    int fd;
    *ftp_return = current_ftp = ftp;
    fd = openSocket(host, "ftp", 21);
    if (fd < 0)
	return -1;
#ifdef FTPPASS_HOSTNAMEGEN
    if (ftppass_hostnamegen && !strcmp(user, "anonymous")) {
	size_t n = strlen(pass);

	if (n > 0 && pass[n - 1] == '@') {
	    struct sockaddr_in sockname;
	    int socknamelen = sizeof(sockname);

	    if (!getsockname(fd, (struct sockaddr *)&sockname, &socknamelen)) {
		struct hostent *sockent;
		Str tmp2 = Strnew_charp(pass);

		if ((sockent = gethostbyaddr((char *)&sockname.sin_addr,
					     sizeof(sockname.sin_addr),
					     sockname.sin_family)))
		    Strcat_charp(tmp2, sockent->h_name);
		else
		    Strcat_m_charp(tmp2, "[", inet_ntoa(sockname.sin_addr),
				   "]", NULL);

		pass = tmp2->ptr;
	    }
	}
    }
#endif
    ftp->rcontrol = fdopen(fd, "rb");
    ftp->wcontrol = fdopen(dup(fd), "wb");
    ftp->data = NULL;
    tmp = read_response(ftp);
    if (atoi(tmp->ptr) != 220)
	return -1;
    if (fmInitialized) {
	message(Sprintf("Sending FTP username (%s) to remote server.", user)->
		ptr, 0, 0);
	refresh();
    }
    tmp = Sprintf("USER %s\r\n", user);
    fwrite(tmp->ptr, tmp->length, sizeof(char), ftp->wcontrol);
    fflush(ftp->wcontrol);
    tmp = read_response(ftp);
    /*
     * Some ftp daemons(e.g. publicfile) return code 230 for user command.
     */
    if (atoi(tmp->ptr) == 230)
	goto succeed;
    if (atoi(tmp->ptr) != 331)
	return -1;
    if (fmInitialized) {
	message(Sprintf("Sending FTP password to remote server.")->ptr, 0, 0);
	refresh();
    }
    tmp = Sprintf("PASS %s\r\n", pass);
    fwrite(tmp->ptr, tmp->length, sizeof(char), ftp->wcontrol);
    fflush(ftp->wcontrol);
    tmp = read_response(ftp);
    if (atoi(tmp->ptr) != 230)
	return -1;
  succeed:
    return 0;
}

int
FtpBinary(FTP ftp)
{
    Str tmp;
    fwrite("TYPE I\r\n", 8, sizeof(char), ftp->wcontrol);
    fflush(ftp->wcontrol);
    tmp = read_response(ftp);
    if (atoi(tmp->ptr) != 200)
	return -1;
    return 0;
}

int
ftp_pasv(FTP ftp)
{
    int n1, n2, n3, n4, p1, p2;
    int data_s;
    char *p;
    Str tmp;
    int family;
#ifdef INET6
    struct sockaddr_storage sockaddr;
    int sockaddrlen, port;
    unsigned char d1, d2, d3, d4;
    char abuf[INET6_ADDRSTRLEN];
#endif

#ifdef INET6
    sockaddrlen = sizeof(sockaddr);
    if (getpeername(fileno(ftp->wcontrol),
		    (struct sockaddr *)&sockaddr, &sockaddrlen) < 0)
	return -1;
    family = sockaddr.ss_family;
#else
    family = AF_INET;
#endif
    switch (family) {
#ifdef INET6
    case AF_INET6:
	fwrite("EPSV\r\n", 6, sizeof(char), ftp->wcontrol);
	fflush(ftp->wcontrol);
	tmp = read_response(ftp);
	if (atoi(tmp->ptr) != 229)
	    return -1;
	for (p = tmp->ptr + 4; *p && *p != '('; p++) ;
	if (*p == '\0')
	    return -1;
	if (sscanf(++p, "%c%c%c%d%c", &d1, &d2, &d3, &port, &d4) != 5
	    || d1 != d2 || d1 != d3 || d1 != d4)
	    return -1;
	if (getnameinfo((struct sockaddr *)&sockaddr, sockaddrlen,
			abuf, sizeof(abuf), NULL, 0, NI_NUMERICHOST) != 0)
	    return -1;
	tmp = Sprintf("%s", abuf);
	data_s = openSocket(tmp->ptr, "", port);
	break;
#endif
    case AF_INET:
	fwrite("PASV\r\n", 6, sizeof(char), ftp->wcontrol);
	fflush(ftp->wcontrol);
	tmp = read_response(ftp);
	if (atoi(tmp->ptr) != 227)
	    return -1;
	for (p = tmp->ptr + 4; *p && !IS_DIGIT(*p); p++) ;
	if (*p == '\0')
	    return -1;
	sscanf(p, "%d,%d,%d,%d,%d,%d", &n1, &n2, &n3, &n4, &p1, &p2);
	tmp = Sprintf("%d.%d.%d.%d", n1, n2, n3, n4);
	data_s = openSocket(tmp->ptr, "", p1 * 256 + p2);
	break;
    default:
	return -1;
    }
    if (data_s < 0)
	return -1;
    ftp->data = fdopen(data_s, "rb");
    return 0;
}

static void
ftp_fclose(FTP ftp)
{
    int control_closed = 0;

    if (ftp->rcontrol != NULL) {
	fclose(ftp->rcontrol);
	ftp->rcontrol = NULL;
	control_closed = 1;
    }
    if (ftp->wcontrol != NULL) {
	fclose(ftp->wcontrol);
	ftp->wcontrol = NULL;
	control_closed = 1;
    }
    if (control_closed && ftp->data != NULL) {
	fclose(ftp->data);
	ftp->data = NULL;
    }
    return;
}

int
FtpCwd(FTP ftp, char *path)
{
    Str tmp;

    tmp = Sprintf("CWD %s\r\n", path);
    fwrite(tmp->ptr, tmp->length, sizeof(char), ftp->wcontrol);
    fflush(ftp->wcontrol);
    tmp = read_response(ftp);
    if (tmp->ptr[0] == '5') {
	return -1;
    }
    return 0;
}

int
FtpOpenReadBody(FTP ftp, char *path)
{
    Str tmp;

    tmp = Sprintf("RETR %s\r\n", path);
    fwrite(tmp->ptr, tmp->length, sizeof(char), ftp->wcontrol);
    fflush(ftp->wcontrol);
    tmp = read_response(ftp);
    if (tmp->ptr[0] == '5') {
	return -1;
    }
    return 0;
}

int
FtpOpenRead(FTP ftp, char *path)
{
    if (ftp_pasv(ftp) < 0)
	return -1;
    if (FtpOpenReadBody(ftp, path) < 0) {
	fclose(ftp->data);
	ftp->data = NULL;
	return -1;
    }
    return 0;
}

int
Ftpfclose(FILE * f)
{
    fclose(f);
    if (f == current_ftp->data)
	current_ftp->data = NULL;
    ftp_fclose(current_ftp);
    return 0;
}

int
FtpDataBody(FTP ftp, char *cmd, char *arg, char *mode)
{
    Str tmp;

    tmp = Sprintf(cmd, arg);
    Strcat_charp(tmp, "\r\n");
    fwrite(tmp->ptr, tmp->length, sizeof(char), ftp->wcontrol);
    fflush(ftp->wcontrol);
    tmp = read_response(ftp);
    if (tmp->ptr[0] == '5') {
	fclose(ftp->data);
	ftp->data = NULL;
	return -1;
    }
    return 0;
}

int
FtpData(FTP ftp, char *cmd, char *arg, char *mode)
{
    if (ftp_pasv(ftp) < 0)
	return -1;

    return FtpDataBody(ftp, cmd, arg, mode);
}

time_t
getFtpModtime(FTP ftp, char *path)
{
    Str tmp;
    char *p;
    struct tm tm;
    time_t t, lt, gt;

    memset(&tm, 0, sizeof(struct tm));
    tmp = Sprintf("MDTM %s\r\n", path);
    fwrite(tmp->ptr, tmp->length, sizeof(char), ftp->wcontrol);
    fflush(ftp->wcontrol);
    tmp = read_response(ftp);
    if (atoi(tmp->ptr) != 213)
	return -1;
    for (p = tmp->ptr + 4; *p && *p == ' '; p++) ;
    if (sscanf(p, "%04d%02d%02d%02d%02d%02d",
	       &tm.tm_year, &tm.tm_mon, &tm.tm_mday,
	       &tm.tm_hour, &tm.tm_min, &tm.tm_sec) < 6)
	return -1;
    tm.tm_year -= 1900;
    tm.tm_mon--;
    t = mktime(&tm);
    lt = mktime(localtime(&t));
    gt = mktime(gmtime(&t));
    return t + (lt - gt);
}

int
FtpClose(FTP ftp)
{
    Str tmp;

    fclose(ftp->data);
    ftp->data = NULL;
    tmp = read_response(ftp);
    if (atoi(tmp->ptr) != 226)
	return -1;
    return 0;
}

int
FtpBye(FTP ftp)
{
    Str tmp;
    int ret_val;

    fwrite("QUIT\r\n", 6, sizeof(char), ftp->wcontrol);
    fflush(ftp->wcontrol);
    tmp = read_response(ftp);
    if (atoi(tmp->ptr) != 221)
	ret_val = -1;
    else
	ret_val = 0;
    ftp_fclose(ftp);
    return ret_val;
}


static int ex_ftpdir_name_size_date(char *, char **, char **, char **);
static int ftp_system(FTP);

#define	SERVER_NONE	0
#define	UNIXLIKE_SERVER	1

#define	FTPDIR_NONE	0
#define	FTPDIR_DIR	1
#define	FTPDIR_LINK	2
#define	FTPDIR_FILE	3

FILE *
openFTP(ParsedURL *pu, URLFile *uf)
{
    Str tmp2 = Strnew();
    Str tmp3 = Strnew();
    STATUS s;
    char *user = NULL;
    char *pass = NULL;
    Str pwd = NULL;
    int add_auth_cookie_flag;
    char *realpathname = NULL;
#ifdef JP_CHARSET
    char code = '\0', ic;
    Str pathStr;
#endif

    add_auth_cookie_flag = 0;
    if (pu->user == NULL && pu->pass == NULL) {
	Str uname, pwd;
	if (find_auth_user_passwd(pu, NULL, &uname, &pwd, 0)) {
	    if (uname)
		user = uname->ptr;
	    if (pwd)
		pass = pwd->ptr;
	}
    }
    if (user)
	/* do nothing */ ;
    else if (pu->user)
	user = pu->user;
    else {
	Strcat_charp(tmp3, "anonymous");
	user = tmp3->ptr;
    }
    if (pass)
	/* do nothing */ ;
    else if (pu->pass)
	pass = pu->pass;
    else if (pu->user) {
	pwd = find_auth_cookie(pu->host, pu->port, pu->file, pu->user);
	if (pwd == NULL) {
	    if (fmInitialized) {
		term_raw();
		pwd = Strnew_charp(inputLine("Password: ", NULL, IN_PASSWORD));
		pwd = Str_conv_to_system(pwd);
		term_cbreak();
	    }
	    else {
		pwd = Strnew_charp((char *)getpass("Password: "));
	    }
	    add_auth_cookie_flag = 1;
	}
	pass = pwd->ptr;
    }
    else if (ftppasswd != NULL && *ftppasswd != '\0')
	pass = ftppasswd;
    else {
	struct passwd *mypw = getpwuid(getuid());
	if (mypw == NULL)
	    Strcat_charp(tmp2, "anonymous");
	else
	    Strcat_charp(tmp2, mypw->pw_name);
	Strcat_char(tmp2, '@');
	pass = tmp2->ptr;
    }
    s = FtpLogin(&current_ftp, pu->host, user, pass);
    if (FtpError(s))
	return NULL;
    if (add_auth_cookie_flag)
	add_auth_cookie(pu->host, pu->port, pu->file, pu->user, pwd);
    if (pu->file == NULL || *pu->file == '\0')
	goto ftp_dir;
    else
	realpathname = file_unquote(pu->file);

    if (pu->file[strlen(pu->file) - 1] == '/')
	goto ftp_dir;

    /* Get file */
    uf->modtime = getFtpModtime(current_ftp, realpathname);
    FtpBinary(current_ftp);
    if (ftp_pasv(current_ftp) < 0) {
	FtpBye(current_ftp);
	return NULL;
    }
    s = FtpOpenReadBody(current_ftp, realpathname);
    if (!FtpError(s)) {
#ifdef JP_CHARSET
	pathStr = Strnew_charp(realpathname);
	if ((ic = checkShiftCode(pathStr, code)) != '\0') {
	    pathStr = conv_str(pathStr, (code = ic), InnerCode);
	    realpathname = pathStr->ptr;
	}
#endif				/* JP_CHARSET */
	pu->file = realpathname;
	return FTPDATA(current_ftp);
    }
    goto ftp_dir1;

    /* Get directory */
  ftp_dir:
    if (ftp_pasv(current_ftp) < 0) {
	FtpBye(current_ftp);
	return NULL;
    }
  ftp_dir1:
    pu->scheme = SCM_FTPDIR;
    return NULL;
}

Str
readFTPDir(ParsedURL *pu)
{
    Str FTPDIRtmp = Strnew();
    Str host;
    Str curdir;
    char *fn;
    char *qdir;
    char **flist;
    int i, nfile, nfile_max = 100;
    int sv_type;
    STATUS s;
    char *realpathname = NULL;
    Str tmp2 = Strnew();

    if (current_ftp->data == NULL)
	return FTPDIRtmp;
    sv_type = ftp_system(current_ftp);
    if (pu->file == NULL || *pu->file == '\0') {
	if (sv_type == UNIXLIKE_SERVER) {
	    s = FtpDataBody(current_ftp, "LIST", NULL, "r");
	}
	else {
	    s = FtpDataBody(current_ftp, "NLST", NULL, "r");
	}
	curdir = Strnew_charp("/");
    }
    else {
	realpathname = file_unquote(pu->file);
	if (sv_type == UNIXLIKE_SERVER) {
	    s = FtpCwd(current_ftp, realpathname);
	    if (!FtpError(s)) {
		s = FtpDataBody(current_ftp, "LIST", NULL, "r");
	    }
	}
	else {
	    s = FtpDataBody(current_ftp, "NLST %s", realpathname, "r");
	}
	if (realpathname[0] == '/')
	    curdir = Strnew_charp(realpathname);
	else
	    curdir = Sprintf("/%s", realpathname);
	if (Strlastchar(curdir) != '/')
	    Strcat_char(curdir, '/');
    }
    if (FtpError(s)) {
	FtpBye(current_ftp);
	return NULL;
    }
    host = Strnew_charp("ftp://");
    if (pu->user) {
	Strcat_m_charp(host, pu->user, "@", NULL);
    }
    Strcat_charp(host, pu->host);
    if (Strlastchar(host) == '/')
	Strshrink(host, 1);
    qdir = html_quote(curdir->ptr);
    FTPDIRtmp =
	Sprintf
	("<html><head><title>%s%s</title></head><body><h1>Index of %s%s</h1>\n",
	 host->ptr, qdir, host->ptr, qdir);
    curdir = Strnew_charp(file_quote(curdir->ptr));
    qdir = curdir->ptr;
    tmp2 = Strdup(curdir);
    if (Strcmp_charp(curdir, "/") != 0) {
	Strshrink(tmp2, 1);
	while (Strlastchar(tmp2) != '/' && tmp2->length > 0)
	    Strshrink(tmp2, 1);
    }
    if (sv_type == UNIXLIKE_SERVER) {
	Strcat_charp(FTPDIRtmp, "<pre><a href=\"");
    }
    else {
	Strcat_charp(FTPDIRtmp, "<ul><li><a href=\"");
    }
    Strcat_m_charp(FTPDIRtmp, host->ptr,
		   html_quote(tmp2->ptr), "\">[Upper Directory]</a>\n", NULL);

    flist = New_N(char *, nfile_max);
    nfile = 0;
    if (sv_type == UNIXLIKE_SERVER) {
	char *name, *date, *size, *type_str;
	int ftype, max_len, len, j;
	Str line_tmp;

	max_len = 0;
	while (tmp2 = Strfgets(FTPDATA(current_ftp)), tmp2->length > 0) {
	    Strchop(tmp2);
	    if ((ftype =
		 ex_ftpdir_name_size_date(tmp2->ptr, &name, &date, &size))
		== FTPDIR_NONE) {
		continue;
	    }
	    if (!strcmp(".", name) || !strcmp("..", name)) {
		continue;
	    }
	    len = strlen(name);
	    if (!len)
		continue;
	    if (ftype == FTPDIR_DIR) {
		len++;
		type_str = "/";
	    }
	    else if (ftype == FTPDIR_LINK) {
		len++;
		type_str = "@";
	    }
	    else {
		type_str = "";
	    }
	    if (max_len < len)
		max_len = len;
	    line_tmp =
		Sprintf("%s%s %-12.12s %6.6s", name, type_str, date, size);
	    flist[nfile++] = line_tmp->ptr;
	    if (nfile == nfile_max) {
		nfile_max *= 2;
		flist = New_Reuse(char *, flist, nfile_max);
	    }
	}
	qsort(flist, nfile, sizeof(char *), strCmp);
	for (j = 0; j < nfile; j++) {
	    fn = flist[j];
	    date = fn + strlen(fn) - 20;
	    if (*(date - 1) == '/') {
		ftype = FTPDIR_DIR;
		*(date - 1) = '\0';
	    }
	    else if (*(date - 1) == '@') {
		ftype = FTPDIR_LINK;
		*(date - 1) = '\0';
	    }
	    else {
		ftype = FTPDIR_FILE;
		*date = '\0';
	    }
	    date++;
	    len = strlen(fn);
	    Strcat_m_charp(FTPDIRtmp, "<a href=\"",
			   host->ptr,
			   qdir,
			   html_quote(file_quote(fn)),
			   "\">", html_quote(fn), NULL);
	    if (ftype == FTPDIR_DIR) {
		Strcat_charp(FTPDIRtmp, "/");
		len++;
	    }
	    else if (ftype == FTPDIR_LINK) {
		Strcat_charp(FTPDIRtmp, "@");
		len++;
	    }
	    Strcat_charp(FTPDIRtmp, "</a>");
	    for (i = len; i <= max_len; i++) {
		if ((max_len % 2 + i) % 2) {
		    Strcat_charp(FTPDIRtmp, ".");
		}
		else {
		    Strcat_charp(FTPDIRtmp, " ");
		}
	    }
	    Strcat_m_charp(FTPDIRtmp, date, "\n", NULL);
	}
	Strcat_charp(FTPDIRtmp, "</pre></body></html>\n");
    }
    else {
	while (tmp2 = Strfgets(FTPDATA(current_ftp)), tmp2->length > 0) {
	    Strchop(tmp2);
	    flist[nfile++] = mybasename(tmp2->ptr);
	    if (nfile == nfile_max) {
		nfile_max *= 2;
		flist = New_Reuse(char *, flist, nfile_max);
	    }
	}
	qsort(flist, nfile, sizeof(char *), strCmp);
	for (i = 0; i < nfile; i++) {
	    fn = flist[i];
	    Strcat_m_charp(FTPDIRtmp, "<li><a href=\"",
			   host->ptr, qdir,
			   html_quote(file_quote(fn)),
			   "\">", html_quote(fn), "</a>\n", NULL);
	}
	Strcat_charp(FTPDIRtmp, "</ul></body></html>\n");
    }

    FtpClose(current_ftp);
    FtpBye(current_ftp);
    return FTPDIRtmp;
}

static int
ftp_system(FTP ftp)
{
    int sv_type = SERVER_NONE;
    Str tmp;

    fwrite("SYST\r\n", 6, sizeof(char), ftp->wcontrol);
    fflush(ftp->wcontrol);
    tmp = read_response(ftp);
    if (strstr(tmp->ptr, "UNIX") != NULL || !strncmp(tmp->ptr + 4, "Windows_NT", 10)) {	/* :-) */
	sv_type = UNIXLIKE_SERVER;
    }

    return (sv_type);
}

#define XD_CTOD(c) {\
  if (c >= '0' && c <= '9') {\
    c -= (unsigned char)'0';\
  } else if (c >= 'a' && c <= 'f') {\
    c = c - (unsigned char)'a' + (unsigned char)10;\
  } else if (c >= 'A' && c <= 'F') {\
    c = c - (unsigned char)'A' + (unsigned char)10;\
  } else {\
    goto skip;\
  }\
}

#define EX_SKIP_SPACE(cp) {\
  while (IS_SPACE(*cp) && *cp != '\0') cp++;\
  if (*cp == '\0') {\
    goto done;\
  }\
}
#define EX_SKIP_NONE_SPACE(cp) {\
  while (!IS_SPACE(*cp) && *cp != '\0') cp++;\
  if (*cp == '\0') {\
    goto done;\
  }\
}

static Str size_int2str(clen_t);

static int
ex_ftpdir_name_size_date(char *line, char **name, char **date, char **sizep)
{
    int ftype = FTPDIR_NONE;
    char *cp, *endp;
    Str date_str, name_str, size_str;
    clen_t size;

    if (strlen(line) < 11) {
	goto done;
    }
    /* skip permission */
    if (!IS_SPACE(line[10])) {
	goto done;
    }
    cp = line + 11;

    /* skip link count */
    EX_SKIP_SPACE(cp)
	while (IS_DIGIT(*cp) && *cp != '\0')
	cp++;
    if (!IS_SPACE(*cp) || *cp == '\0') {
	goto done;
    }
    cp++;

    /* skip owner string */
    EX_SKIP_SPACE(cp)
	EX_SKIP_NONE_SPACE(cp)
	cp++;

    /* skip group string */
    EX_SKIP_SPACE(cp)
	EX_SKIP_NONE_SPACE(cp)
	cp++;

    /* extract size */
    EX_SKIP_SPACE(cp)
	size = 0;
    while (*cp && IS_DIGIT(*cp)) {
	size = size * 10 + *(cp++) - '0';
    }
    if (*cp == '\0') {
	goto done;
    }

    /* extract date */
    EX_SKIP_SPACE(cp)
	if (IS_ALPHA(cp[0]) && IS_ALPHA(cp[1]) && IS_ALPHA(cp[2])
	    && IS_SPACE(cp[3])
	    && (IS_SPACE(cp[4]) || IS_DIGIT(cp[4])) && IS_DIGIT(cp[5])
	    && IS_SPACE(cp[6])
	    && (IS_SPACE(cp[7]) || IS_DIGIT(cp[7])) && IS_DIGIT(cp[8])
	    && (cp[9] == ':' || IS_DIGIT(cp[9]))
	    && IS_DIGIT(cp[10]) && (IS_DIGIT(cp[11]) || IS_SPACE(cp[11]))
	    && IS_SPACE(cp[12])) {
	cp[12] = '\0';
	date_str = Strnew_charp(cp);
	cp += 13;
    }
    else {
	goto done;
    }

    /* extract file name */
    EX_SKIP_SPACE(cp)
	if (line[0] == 'l') {
	if ((endp = strstr(cp, " -> ")) == NULL) {
	    goto done;
	}
	*endp = '\0';
	size_str = Strnew_charp("-");
	ftype = FTPDIR_LINK;
    }
    else if (line[0] == 'd') {
	size_str = Strnew_charp("-");
	ftype = FTPDIR_DIR;
    }
    else {
	size_str = size_int2str(size);
	ftype = FTPDIR_FILE;
    }
    name_str = Strnew_charp(cp);
    *date = date_str->ptr;
    *name = name_str->ptr;
    *sizep = size_str->ptr;

  done:
    return (ftype);
}

static Str
size_int2str(clen_t size)
{
    Str size_str;
    int unit;
    double dtmp;
    char *size_format, *unit_str;

    dtmp = (double)size;
    for (unit = 0; unit < 3; unit++) {
	if (dtmp < 1024) {
	    break;
	}
	dtmp /= 1024;
    }
    if (!unit || dtmp > 100) {
	size_format = "%.0f%s";
    }
    else if (dtmp > 10) {
	size_format = "%.1f%s";
    }
    else {
	size_format = "%.2f%s";
    }
    switch (unit) {
    case 3:
	unit_str = "G";
	break;
    case 2:
	unit_str = "M";
	break;
    case 1:
	unit_str = "K";
	break;
    default:
	unit_str = "";
	break;
    }
    size_str = Sprintf(size_format, dtmp, unit_str);

    return (size_str);
}

void
closeFTP(FILE * f)
{
    if (f) {
	fclose(f);
	if (f == current_ftp->data)
	    current_ftp->data = NULL;
    }
    FtpBye(current_ftp);
}