]> sjero.net Git - wget/blob - src/ftp-ls.c
[svn] Committed C. Frankel's SSL patch.
[wget] / src / ftp-ls.c
1 /* Parsing FTP `ls' output.
2    Copyright (C) 1995, 1996, 1997, 2000 Free Software Foundation, Inc.
3
4 This file is part of Wget.
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.  */
19
20 #include <config.h>
21
22 #include <stdio.h>
23 #include <stdlib.h>
24 #ifdef HAVE_STRING_H
25 # include <string.h>
26 #else
27 # include <strings.h>
28 #endif
29 #ifdef HAVE_UNISTD_H
30 # include <unistd.h>
31 #endif
32 #include <sys/types.h>
33 #include <ctype.h>
34 #include <errno.h>
35
36 #include "wget.h"
37 #include "utils.h"
38 #include "ftp.h"
39 #include "url.h"
40
41 /* Undef this if FTPPARSE is not available.  In that case, Wget will
42    still work with Unix FTP servers, which covers most cases.  */
43
44 #define HAVE_FTPPARSE
45
46 #ifdef HAVE_FTPPARSE
47 #include "ftpparse.h"
48 #endif
49
50 /* Converts symbolic permissions to number-style ones, e.g. string
51    rwxr-xr-x to 755.  For now, it knows nothing of
52    setuid/setgid/sticky.  ACLs are ignored.  */
53 static int
54 symperms (const char *s)
55 {
56   int perms = 0, i;
57
58   if (strlen (s) < 9)
59     return 0;
60   for (i = 0; i < 3; i++, s += 3)
61     {
62       perms <<= 3;
63       perms += (((s[0] == 'r') << 2) + ((s[1] == 'w') << 1) +
64                 (s[2] == 'x' || s[2] == 's'));
65     }
66   return perms;
67 }
68
69
70 /* Convert the Un*x-ish style directory listing stored in FILE to a
71    linked list of fileinfo (system-independent) entries.  The contents
72    of FILE are considered to be produced by the standard Unix `ls -la'
73    output (whatever that might be).  BSD (no group) and SYSV (with
74    group) listings are handled.
75
76    The time stamps are stored in a separate variable, time_t
77    compatible (I hope).  The timezones are ignored.  */
78 static struct fileinfo *
79 ftp_parse_unix_ls (const char *file, int ignore_perms)
80 {
81   FILE *fp;
82   static const char *months[] = {
83     "Jan", "Feb", "Mar", "Apr", "May", "Jun",
84     "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
85   };
86   int next, len, i, error, ignore;
87   int year, month, day;         /* for time analysis */
88   int hour, min, sec;
89   struct tm timestruct, *tnow;
90   time_t timenow;
91
92   char *line, *tok;             /* tokenizer */
93   struct fileinfo *dir, *l, cur; /* list creation */
94
95   fp = fopen (file, "rb");
96   if (!fp)
97     {
98       logprintf (LOG_NOTQUIET, "%s: %s\n", file, strerror (errno));
99       return NULL;
100     }
101   dir = l = NULL;
102
103   /* Line loop to end of file: */
104   while ((line = read_whole_line (fp)))
105     {
106       DEBUGP (("%s\n", line));
107       len = strlen (line);
108       /* Destroy <CR><LF> if present.  */
109       if (len && line[len - 1] == '\n')
110         line[--len] = '\0';
111       if (len && line[len - 1] == '\r')
112         line[--len] = '\0';
113
114       /* Skip if total...  */
115       if (!strncasecmp (line, "total", 5))
116         {
117           xfree (line);
118           continue;
119         }
120       /* Get the first token (permissions).  */
121       tok = strtok (line, " ");
122       if (!tok)
123         {
124           xfree (line);
125           continue;
126         }
127
128       cur.name = NULL;
129       cur.linkto = NULL;
130
131       /* Decide whether we deal with a file or a directory.  */
132       switch (*tok)
133         {
134         case '-':
135           cur.type = FT_PLAINFILE;
136           DEBUGP (("PLAINFILE; "));
137           break;
138         case 'd':
139           cur.type = FT_DIRECTORY;
140           DEBUGP (("DIRECTORY; "));
141           break;
142         case 'l':
143           cur.type = FT_SYMLINK;
144           DEBUGP (("SYMLINK; "));
145           break;
146         default:
147           cur.type = FT_UNKNOWN;
148           DEBUGP (("UNKOWN; "));
149           break;
150         }
151
152       if (ignore_perms)
153         {
154           switch (cur.type)
155             {
156             case FT_PLAINFILE:
157               cur.perms = 420;
158               break;
159             case FT_DIRECTORY:
160               cur.perms = 493;
161               break;
162             default:
163               cur.perms = 1023;
164             }
165           DEBUGP (("implicite perms %0o; ", cur.perms));
166         }
167        else
168          {
169            cur.perms = symperms (tok + 1);
170            DEBUGP (("perms %0o; ", cur.perms));
171          }
172
173       error = ignore = 0;       /* Erroneous and ignoring entries are
174                                    treated equally for now.  */
175       year = hour = min = sec = 0; /* Silence the compiler.  */
176       month = day = 0;
177       next = -1;
178       /* While there are tokens on the line, parse them.  Next is the
179          number of tokens left until the filename.
180
181          Use the month-name token as the "anchor" (the place where the
182          position wrt the file name is "known").  When a month name is
183          encountered, `next' is set to 5.  Also, the preceding
184          characters are parsed to get the file size.
185
186          This tactic is quite dubious when it comes to
187          internationalization issues (non-English month names), but it
188          works for now.  */
189       while ((tok = strtok (NULL, " ")))
190         {
191           --next;
192           if (next < 0)         /* a month name was not encountered */
193             {
194               for (i = 0; i < 12; i++)
195                 if (!strcmp (tok, months[i]))
196                   break;
197               /* If we got a month, it means the token before it is the
198                  size, and the filename is three tokens away.  */
199               if (i != 12)
200                 {
201                   char *t = tok - 2;
202                   long mul = 1;
203
204                   for (cur.size = 0; t > line && ISDIGIT (*t); mul *= 10, t--)
205                     cur.size += mul * (*t - '0');
206                   if (t == line)
207                     {
208                       /* Something is seriously wrong.  */
209                       error = 1;
210                       break;
211                     }
212                   month = i;
213                   next = 5;
214                   DEBUGP (("month: %s; ", months[month]));
215                 }
216             }
217           else if (next == 4)   /* days */
218             {
219               if (tok[1])       /* two-digit... */
220                 day = 10 * (*tok - '0') + tok[1] - '0';
221               else              /* ...or one-digit */
222                 day = *tok - '0';
223               DEBUGP (("day: %d; ", day));
224             }
225           else if (next == 3)
226             {
227               /* This ought to be either the time, or the year.  Let's
228                  be flexible!
229
230                  If we have a number x, it's a year.  If we have x:y,
231                  it's hours and minutes.  If we have x:y:z, z are
232                  seconds.  */
233               year = 0;
234               min = hour = sec = 0;
235               /* We must deal with digits.  */
236               if (ISDIGIT (*tok))
237                 {
238                   /* Suppose it's year.  */
239                   for (; ISDIGIT (*tok); tok++)
240                     year = (*tok - '0') + 10 * year;
241                   if (*tok == ':')
242                     {
243                       /* This means these were hours!  */
244                       hour = year;
245                       year = 0;
246                       ++tok;
247                       /* Get the minutes...  */
248                       for (; ISDIGIT (*tok); tok++)
249                         min = (*tok - '0') + 10 * min;
250                       if (*tok == ':')
251                         {
252                           /* ...and the seconds.  */
253                           ++tok;
254                           for (; ISDIGIT (*tok); tok++)
255                             sec = (*tok - '0') + 10 * sec;
256                         }
257                     }
258                 }
259               if (year)
260                 DEBUGP (("year: %d (no tm); ", year));
261               else
262                 DEBUGP (("time: %02d:%02d:%02d (no yr); ", hour, min, sec));
263             }
264           else if (next == 2)    /* The file name */
265             {
266               int fnlen;
267               char *p;
268
269               /* Since the file name may contain a SPC, it is possible
270                  for strtok to handle it wrong.  */
271               fnlen = strlen (tok);
272               if (fnlen < len - (tok - line))
273                 {
274                   /* So we have a SPC in the file name.  Restore the
275                      original.  */
276                   tok[fnlen] = ' ';
277                   /* If the file is a symbolic link, it should have a
278                      ` -> ' somewhere.  */
279                   if (cur.type == FT_SYMLINK)
280                     {
281                       p = strstr (tok, " -> ");
282                       if (!p)
283                         {
284                           error = 1;
285                           break;
286                         }
287                       cur.linkto = xstrdup (p + 4);
288                       DEBUGP (("link to: %s\n", cur.linkto));
289                       /* And separate it from the file name.  */
290                       *p = '\0';
291                     }
292                 }
293               /* If we have the filename, add it to the list of files or
294                  directories.  */
295               /* "." and ".." are an exception!  */
296               if (!strcmp (tok, ".") || !strcmp (tok, ".."))
297                 {
298                   DEBUGP (("\nIgnoring `.' and `..'; "));
299                   ignore = 1;
300                   break;
301                 }
302               /* Some FTP sites choose to have ls -F as their default
303                  LIST output, which marks the symlinks with a trailing
304                  `@', directory names with a trailing `/' and
305                  executables with a trailing `*'.  This is no problem
306                  unless encountering a symbolic link ending with `@',
307                  or an executable ending with `*' on a server without
308                  default -F output.  I believe these cases are very
309                  rare.  */
310               fnlen = strlen (tok); /* re-calculate `fnlen' */
311               cur.name = (char *)xmalloc (fnlen + 1);
312               memcpy (cur.name, tok, fnlen + 1);
313               if (fnlen)
314                 {
315                   if (cur.type == FT_DIRECTORY && cur.name[fnlen - 1] == '/')
316                     {
317                       cur.name[fnlen - 1] = '\0';
318                       DEBUGP (("trailing `/' on dir.\n"));
319                     }
320                   else if (cur.type == FT_SYMLINK && cur.name[fnlen - 1] == '@')
321                     {
322                       cur.name[fnlen - 1] = '\0';
323                       DEBUGP (("trailing `@' on link.\n"));
324                     }
325                   else if (cur.type == FT_PLAINFILE
326                            && (cur.perms & 0111)
327                            && cur.name[fnlen - 1] == '*')
328                     {
329                       cur.name[fnlen - 1] = '\0';
330                       DEBUGP (("trailing `*' on exec.\n"));
331                     }
332                 } /* if (fnlen) */
333               else
334                 error = 1;
335               break;
336             }
337           else
338             abort ();
339         } /* while */
340
341       if (!cur.name || (cur.type == FT_SYMLINK && !cur.linkto))
342         error = 1;
343
344       DEBUGP (("\n"));
345
346       if (error || ignore)
347         {
348           DEBUGP (("Skipping.\n"));
349           FREE_MAYBE (cur.name);
350           FREE_MAYBE (cur.linkto);
351           xfree (line);
352           continue;
353         }
354
355       if (!dir)
356         {
357           l = dir = (struct fileinfo *)xmalloc (sizeof (struct fileinfo));
358           memcpy (l, &cur, sizeof (cur));
359           l->prev = l->next = NULL;
360         }
361       else
362         {
363           cur.prev = l;
364           l->next = (struct fileinfo *)xmalloc (sizeof (struct fileinfo));
365           l = l->next;
366           memcpy (l, &cur, sizeof (cur));
367           l->next = NULL;
368         }
369       /* Get the current time.  */
370       timenow = time (NULL);
371       tnow = localtime (&timenow);
372       /* Build the time-stamp (the idea by zaga@fly.cc.fer.hr).  */
373       timestruct.tm_sec   = sec;
374       timestruct.tm_min   = min;
375       timestruct.tm_hour  = hour;
376       timestruct.tm_mday  = day;
377       timestruct.tm_mon   = month;
378       if (year == 0)
379         {
380           /* Some listings will not specify the year if it is "obvious"
381              that the file was from the previous year.  E.g. if today
382              is 97-01-12, and you see a file of Dec 15th, its year is
383              1996, not 1997.  Thanks to Vladimir Volovich for
384              mentioning this!  */
385           if (month > tnow->tm_mon)
386             timestruct.tm_year = tnow->tm_year - 1;
387           else
388             timestruct.tm_year = tnow->tm_year;
389         }
390       else
391         timestruct.tm_year = year;
392       if (timestruct.tm_year >= 1900)
393         timestruct.tm_year -= 1900;
394       timestruct.tm_wday  = 0;
395       timestruct.tm_yday  = 0;
396       timestruct.tm_isdst = -1;
397       l->tstamp = mktime (&timestruct); /* store the time-stamp */
398
399       xfree (line);
400     }
401
402   fclose (fp);
403   return dir;
404 }
405
406 static struct fileinfo *
407 ftp_parse_winnt_ls (const char *file)
408 {
409   FILE *fp;
410   int len;
411   int year, month, day;         /* for time analysis */
412   int hour, min, sec;
413   struct tm timestruct;
414
415   char *line, *tok;             /* tokenizer */
416   struct fileinfo *dir, *l, cur; /* list creation */
417
418   fp = fopen (file, "rb");
419   if (!fp)
420     {
421       logprintf (LOG_NOTQUIET, "%s: %s\n", file, strerror (errno));
422       return NULL;
423     }
424   dir = l = NULL;
425
426   /* Line loop to end of file: */
427   while ((line = read_whole_line (fp)))
428     {
429       DEBUGP (("%s\n", line));
430       len = strlen (line);
431       /* Destroy <CR><LF> if present.  */
432       if (len && line[len - 1] == '\n')
433         line[--len] = '\0';
434       if (len && line[len - 1] == '\r')
435         line[--len] = '\0';
436
437       /* Extracting name is a bit of black magic and we have to do it
438          before `strtok' inserted extra \0 characters in the line
439          string. For the moment let us just suppose that the name starts at
440          column 39 of the listing. This way we could also recognize
441          filenames that begin with a series of space characters (but who
442          really wants to use such filenames anyway?). */
443       if (len < 40) continue;
444       tok = line + 39;
445       cur.name = xstrdup(tok);
446       DEBUGP(("Name: '%s'\n", cur.name));
447
448       /* First column: mm-dd-yy */
449       tok = strtok(line, "-");
450       month = atoi(tok);
451       tok = strtok(NULL, "-");
452       day = atoi(tok);
453       tok = strtok(NULL, " ");
454       year = atoi(tok);
455       /* Assuming the epoch starting at 1.1.1970 */
456       if (year <= 70) year += 100;
457
458       /* Second column: hh:mm[AP]M */
459       tok = strtok(NULL,  ":");
460       hour = atoi(tok);
461       tok = strtok(NULL,  "M");
462       min = atoi(tok);
463       /* Adjust hour from AM/PM */
464       tok+=2;
465       if (*tok == 'P') hour += 12;
466       /* Listing does not contain value for seconds */
467       sec = 0;
468
469       DEBUGP(("YYYY/MM/DD HH:MM - %d/%02d/%02d %02d:%02d\n", 
470               year+1900, month, day, hour, min));
471       
472       /* Build the time-stamp (copy & paste from above) */
473       timestruct.tm_sec   = sec;
474       timestruct.tm_min   = min;
475       timestruct.tm_hour  = hour;
476       timestruct.tm_mday  = day;
477       timestruct.tm_mon   = month;
478       timestruct.tm_year  = year;
479       timestruct.tm_wday  = 0;
480       timestruct.tm_yday  = 0;
481       timestruct.tm_isdst = -1;
482       cur.tstamp = mktime (&timestruct); /* store the time-stamp */
483
484       DEBUGP(("Timestamp: %ld\n", cur.tstamp));
485
486       /* Third column: Either file length, or <DIR>. We also set the
487          permissions (guessed as 0644 for plain files and 0755 for
488          directories as the listing does not give us a clue) and filetype
489          here. */
490       tok = strtok(NULL, " ");
491       while (*tok == '\0')  tok = strtok(NULL, " ");
492       if (*tok == '<')
493         {
494           cur.type  = FT_DIRECTORY;
495           cur.size  = 0;
496           cur.perms = 493; /* my gcc does not like 0755 ?? */
497           DEBUGP(("Directory\n"));
498         }
499       else
500         {
501           cur.type  = FT_PLAINFILE;
502           cur.size  = atoi(tok);
503           cur.perms = 420; /* 0664 octal */
504           DEBUGP(("File, size %ld bytes\n", cur.size));
505         }
506
507       cur.linkto = NULL;
508
509       /* And put everything into the linked list */
510       if (!dir)
511         {
512           l = dir = (struct fileinfo *)xmalloc (sizeof (struct fileinfo));
513           memcpy (l, &cur, sizeof (cur));
514           l->prev = l->next = NULL;
515         }
516       else
517         {
518           cur.prev = l;
519           l->next = (struct fileinfo *)xmalloc (sizeof (struct fileinfo));
520           l = l->next;
521           memcpy (l, &cur, sizeof (cur));
522           l->next = NULL;
523         }
524
525       xfree(line);
526     }
527
528   fclose(fp);
529   return dir;
530 }
531
532
533 #ifdef HAVE_FTPPARSE
534
535 /* This is a "glue function" that connects the ftpparse interface to
536    the interface Wget expects.  ftpparse is used to parse listings
537    from servers other than Unix, like those running VMS or NT. */
538
539 static struct fileinfo *
540 ftp_parse_nonunix_ls (const char *file)
541 {
542   FILE *fp;
543   int len;
544
545   char *line;          /* tokenizer */
546   struct fileinfo *dir, *l, cur; /* list creation */
547
548   fp = fopen (file, "rb");
549   if (!fp)
550     {
551       logprintf (LOG_NOTQUIET, "%s: %s\n", file, strerror (errno));
552       return NULL;
553     }
554   dir = l = NULL;
555
556   /* Line loop to end of file: */
557   while ((line = read_whole_line (fp)))
558     {
559       struct ftpparse fp;
560
561       DEBUGP (("%s\n", line));
562       len = strlen (line);
563       /* Destroy <CR><LF> if present.  */
564       if (len && line[len - 1] == '\n')
565         line[--len] = '\0';
566       if (len && line[len - 1] == '\r')
567         line[--len] = '\0';
568
569       if (ftpparse(&fp, line, len))
570         {
571           cur.size = fp.size;
572           cur.name = (char *)xmalloc (fp.namelen + 1);
573           memcpy (cur.name, fp.name, fp.namelen);
574           cur.name[fp.namelen] = '\0';
575           DEBUGP (("%s\n", cur.name));
576           /* No links on non-UNIX systems */
577           cur.linkto = NULL;
578           /* ftpparse won't tell us correct permisions. So lets just invent
579              something. */
580           if (fp.flagtrycwd)
581             {
582               cur.type = FT_DIRECTORY;
583               cur.perms = 0755;
584             } 
585           else 
586             {
587               cur.type = FT_PLAINFILE;
588               cur.perms = 0644;
589             }
590           if (!dir)
591             {
592               l = dir = (struct fileinfo *)xmalloc (sizeof (struct fileinfo));
593               memcpy (l, &cur, sizeof (cur));
594               l->prev = l->next = NULL;
595             }
596           else 
597             {
598               cur.prev = l;
599               l->next = (struct fileinfo *)xmalloc (sizeof (struct fileinfo));
600               l = l->next;
601               memcpy (l, &cur, sizeof (cur));
602               l->next = NULL;
603             }
604           l->tstamp = fp.mtime;
605       }
606
607       xfree (line);
608     }
609
610   fclose (fp);
611   return dir;
612 }
613 #endif
614
615 /* This function switches between the correct parsing routine
616    depending on the SYSTEM_TYPE.  If system type is ST_UNIX, we use
617    our home-grown ftp_parse_unix_ls; otherwise, we use our interface
618    to ftpparse, also known as ftp_parse_nonunix_ls.  The system type
619    should be based on the result of the "SYST" response of the FTP
620    server.  */
621
622 struct fileinfo *
623 ftp_parse_ls (const char *file, const enum stype system_type)
624 {
625   switch (system_type)
626     {
627     case ST_UNIX:
628       return ftp_parse_unix_ls (file, FALSE);
629     case ST_WINNT:
630       {
631         /* Detect whether the listing is simulating the UNIX format */
632         FILE *fp;
633         int   c;
634         fp = fopen (file, "rb");
635         if (!fp)
636         {
637           logprintf (LOG_NOTQUIET, "%s: %s\n", file, strerror (errno));
638           return NULL;
639     }
640         c = fgetc(fp);
641         fclose(fp);
642         /* If the first character of the file is '0'-'9', it's WINNT
643            format. */
644         if (c >= '0' && c <='9')
645           return ftp_parse_winnt_ls (file);
646   else
647           return ftp_parse_unix_ls (file, TRUE);
648       }
649     default:
650 #ifdef HAVE_FTPPARSE
651       return ftp_parse_nonunix_ls (file);
652 #else
653       /* #### Maybe log some warning here? */ 
654       return ftp_parse_unix_ls (file);
655 #endif
656     }
657 }
658 \f
659 /* Stuff for creating FTP index. */
660
661 /* The function creates an HTML index containing references to given
662    directories and files on the appropriate host.  The references are
663    FTP.  */
664 uerr_t
665 ftp_index (const char *file, struct urlinfo *u, struct fileinfo *f)
666 {
667   FILE *fp;
668   char *upwd;
669   char *htclfile;               /* HTML-clean file name */
670
671   if (!opt.dfp)
672     {
673       fp = fopen (file, "wb");
674       if (!fp)
675         {
676           logprintf (LOG_NOTQUIET, "%s: %s\n", file, strerror (errno));
677           return FOPENERR;
678         }
679     }
680   else
681     fp = opt.dfp;
682   if (u->user)
683     {
684       char *tmpu, *tmpp;        /* temporary, clean user and passwd */
685
686       tmpu = CLEANDUP (u->user);
687       tmpp = u->passwd ? CLEANDUP (u->passwd) : NULL;
688       upwd = (char *)xmalloc (strlen (tmpu)
689                              + (tmpp ? (1 + strlen (tmpp)) : 0) + 2);
690       sprintf (upwd, "%s%s%s@", tmpu, tmpp ? ":" : "", tmpp ? tmpp : "");
691       xfree (tmpu);
692       FREE_MAYBE (tmpp);
693     }
694   else
695     upwd = xstrdup ("");
696   fprintf (fp, "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n");
697   fprintf (fp, "<html>\n<head>\n<title>");
698   fprintf (fp, _("Index of /%s on %s:%d"), u->dir, u->host, u->port);
699   fprintf (fp, "</title>\n</head>\n<body>\n<h1>");
700   fprintf (fp, _("Index of /%s on %s:%d"), u->dir, u->host, u->port);
701   fprintf (fp, "</h1>\n<hr>\n<pre>\n");
702   while (f)
703     {
704       fprintf (fp, "  ");
705       if (f->tstamp != -1)
706         {
707           /* #### Should we translate the months? */
708           static char *months[] = {
709             "Jan", "Feb", "Mar", "Apr", "May", "Jun",
710             "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
711           };
712           struct tm *ptm = localtime ((time_t *)&f->tstamp);
713
714           fprintf (fp, "%d %s %02d ", ptm->tm_year + 1900, months[ptm->tm_mon],
715                   ptm->tm_mday);
716           if (ptm->tm_hour)
717             fprintf (fp, "%02d:%02d  ", ptm->tm_hour, ptm->tm_min);
718           else
719             fprintf (fp, "       ");
720         }
721       else
722         fprintf (fp, _("time unknown       "));
723       switch (f->type)
724         {
725         case FT_PLAINFILE:
726           fprintf (fp, _("File        "));
727           break;
728         case FT_DIRECTORY:
729           fprintf (fp, _("Directory   "));
730           break;
731         case FT_SYMLINK:
732           fprintf (fp, _("Link        "));
733           break;
734         default:
735           fprintf (fp, _("Not sure    "));
736           break;
737         }
738       htclfile = html_quote_string (f->name);
739       fprintf (fp, "<a href=\"ftp://%s%s:%hu", upwd, u->host, u->port);
740       if (*u->dir != '/')
741         putc ('/', fp);
742       fprintf (fp, "%s", u->dir);
743       if (*u->dir)
744         putc ('/', fp);
745       fprintf (fp, "%s", htclfile);
746       if (f->type == FT_DIRECTORY)
747         putc ('/', fp);
748       fprintf (fp, "\">%s", htclfile);
749       if (f->type == FT_DIRECTORY)
750         putc ('/', fp);
751       fprintf (fp, "</a> ");
752       if (f->type == FT_PLAINFILE)
753         fprintf (fp, _(" (%s bytes)"), legible (f->size));
754       else if (f->type == FT_SYMLINK)
755         fprintf (fp, "-> %s", f->linkto ? f->linkto : "(nil)");
756       putc ('\n', fp);
757       xfree (htclfile);
758       f = f->next;
759     }
760   fprintf (fp, "</pre>\n</body>\n</html>\n");
761   xfree (upwd);
762   if (!opt.dfp)
763     fclose (fp);
764   else
765     fflush (fp);
766   return FTPOK;
767 }