]> sjero.net Git - wget/blob - src/ftp-ls.c
[svn] Committed Jan's ftpparse patch with Hrvoje's modifications.
[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)
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           free (line);
118           continue;
119         }
120       /* Get the first token (permissions).  */
121       tok = strtok (line, " ");
122       if (!tok)
123         {
124           free (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       cur.perms = symperms (tok + 1);
153       DEBUGP (("perms %0o; ", cur.perms));
154
155       error = ignore = 0;       /* Errnoeous and ignoring entries are
156                                    treated equally for now.  */
157       year = hour = min = sec = 0; /* Silence the compiler.  */
158       month = day = 0;
159       next = -1;
160       /* While there are tokens on the line, parse them.  Next is the
161          number of tokens left until the filename.
162
163          Use the month-name token as the "anchor" (the place where the
164          position wrt the file name is "known").  When a month name is
165          encountered, `next' is set to 5.  Also, the preceding
166          characters are parsed to get the file size.
167
168          This tactic is quite dubious when it comes to
169          internationalization issues (non-English month names), but it
170          works for now.  */
171       while ((tok = strtok (NULL, " ")))
172         {
173           --next;
174           if (next < 0)         /* a month name was not encountered */
175             {
176               for (i = 0; i < 12; i++)
177                 if (!strcmp (tok, months[i]))
178                   break;
179               /* If we got a month, it means the token before it is the
180                  size, and the filename is three tokens away.  */
181               if (i != 12)
182                 {
183                   char *t = tok - 2;
184                   long mul = 1;
185
186                   for (cur.size = 0; t > line && ISDIGIT (*t); mul *= 10, t--)
187                     cur.size += mul * (*t - '0');
188                   if (t == line)
189                     {
190                       /* Something is seriously wrong.  */
191                       error = 1;
192                       break;
193                     }
194                   month = i;
195                   next = 5;
196                   DEBUGP (("month: %s; ", months[month]));
197                 }
198             }
199           else if (next == 4)   /* days */
200             {
201               if (tok[1])       /* two-digit... */
202                 day = 10 * (*tok - '0') + tok[1] - '0';
203               else              /* ...or one-digit */
204                 day = *tok - '0';
205               DEBUGP (("day: %d; ", day));
206             }
207           else if (next == 3)
208             {
209               /* This ought to be either the time, or the year.  Let's
210                  be flexible!
211
212                  If we have a number x, it's a year.  If we have x:y,
213                  it's hours and minutes.  If we have x:y:z, z are
214                  seconds.  */
215               year = 0;
216               min = hour = sec = 0;
217               /* We must deal with digits.  */
218               if (ISDIGIT (*tok))
219                 {
220                   /* Suppose it's year.  */
221                   for (; ISDIGIT (*tok); tok++)
222                     year = (*tok - '0') + 10 * year;
223                   if (*tok == ':')
224                     {
225                       /* This means these were hours!  */
226                       hour = year;
227                       year = 0;
228                       ++tok;
229                       /* Get the minutes...  */
230                       for (; ISDIGIT (*tok); tok++)
231                         min = (*tok - '0') + 10 * min;
232                       if (*tok == ':')
233                         {
234                           /* ...and the seconds.  */
235                           ++tok;
236                           for (; ISDIGIT (*tok); tok++)
237                             sec = (*tok - '0') + 10 * sec;
238                         }
239                     }
240                 }
241               if (year)
242                 DEBUGP (("year: %d (no tm); ", year));
243               else
244                 DEBUGP (("time: %02d:%02d:%02d (no yr); ", hour, min, sec));
245             }
246           else if (next == 2)    /* The file name */
247             {
248               int fnlen;
249               char *p;
250
251               /* Since the file name may contain a SPC, it is possible
252                  for strtok to handle it wrong.  */
253               fnlen = strlen (tok);
254               if (fnlen < len - (tok - line))
255                 {
256                   /* So we have a SPC in the file name.  Restore the
257                      original.  */
258                   tok[fnlen] = ' ';
259                   /* If the file is a symbolic link, it should have a
260                      ` -> ' somewhere.  */
261                   if (cur.type == FT_SYMLINK)
262                     {
263                       p = strstr (tok, " -> ");
264                       if (!p)
265                         {
266                           error = 1;
267                           break;
268                         }
269                       cur.linkto = xstrdup (p + 4);
270                       DEBUGP (("link to: %s\n", cur.linkto));
271                       /* And separate it from the file name.  */
272                       *p = '\0';
273                     }
274                 }
275               /* If we have the filename, add it to the list of files or
276                  directories.  */
277               /* "." and ".." are an exception!  */
278               if (!strcmp (tok, ".") || !strcmp (tok, ".."))
279                 {
280                   DEBUGP (("\nIgnoring `.' and `..'; "));
281                   ignore = 1;
282                   break;
283                 }
284               /* Some FTP sites choose to have ls -F as their default
285                  LIST output, which marks the symlinks with a trailing
286                  `@', directory names with a trailing `/' and
287                  executables with a trailing `*'.  This is no problem
288                  unless encountering a symbolic link ending with `@',
289                  or an executable ending with `*' on a server without
290                  default -F output.  I believe these cases are very
291                  rare.  */
292               fnlen = strlen (tok); /* re-calculate `fnlen' */
293               cur.name = (char *)xmalloc (fnlen + 1);
294               memcpy (cur.name, tok, fnlen + 1);
295               if (fnlen)
296                 {
297                   if (cur.type == FT_DIRECTORY && cur.name[fnlen - 1] == '/')
298                     {
299                       cur.name[fnlen - 1] = '\0';
300                       DEBUGP (("trailing `/' on dir.\n"));
301                     }
302                   else if (cur.type == FT_SYMLINK && cur.name[fnlen - 1] == '@')
303                     {
304                       cur.name[fnlen - 1] = '\0';
305                       DEBUGP (("trailing `@' on link.\n"));
306                     }
307                   else if (cur.type == FT_PLAINFILE
308                            && (cur.perms & 0111)
309                            && cur.name[fnlen - 1] == '*')
310                     {
311                       cur.name[fnlen - 1] = '\0';
312                       DEBUGP (("trailing `*' on exec.\n"));
313                     }
314                 } /* if (fnlen) */
315               else
316                 error = 1;
317               break;
318             }
319           else
320             abort ();
321         } /* while */
322
323       if (!cur.name || (cur.type == FT_SYMLINK && !cur.linkto))
324         error = 1;
325
326       DEBUGP (("\n"));
327
328       if (error || ignore)
329         {
330           DEBUGP (("Skipping.\n"));
331           FREE_MAYBE (cur.name);
332           FREE_MAYBE (cur.linkto);
333           free (line);
334           continue;
335         }
336
337       if (!dir)
338         {
339           l = dir = (struct fileinfo *)xmalloc (sizeof (struct fileinfo));
340           memcpy (l, &cur, sizeof (cur));
341           l->prev = l->next = NULL;
342         }
343       else
344         {
345           cur.prev = l;
346           l->next = (struct fileinfo *)xmalloc (sizeof (struct fileinfo));
347           l = l->next;
348           memcpy (l, &cur, sizeof (cur));
349           l->next = NULL;
350         }
351       /* Get the current time.  */
352       timenow = time (NULL);
353       tnow = localtime (&timenow);
354       /* Build the time-stamp (the idea by zaga@fly.cc.fer.hr).  */
355       timestruct.tm_sec   = sec;
356       timestruct.tm_min   = min;
357       timestruct.tm_hour  = hour;
358       timestruct.tm_mday  = day;
359       timestruct.tm_mon   = month;
360       if (year == 0)
361         {
362           /* Some listings will not specify the year if it is "obvious"
363              that the file was from the previous year.  E.g. if today
364              is 97-01-12, and you see a file of Dec 15th, its year is
365              1996, not 1997.  Thanks to Vladimir Volovich for
366              mentioning this!  */
367           if (month > tnow->tm_mon)
368             timestruct.tm_year = tnow->tm_year - 1;
369           else
370             timestruct.tm_year = tnow->tm_year;
371         }
372       else
373         timestruct.tm_year = year;
374       if (timestruct.tm_year >= 1900)
375         timestruct.tm_year -= 1900;
376       timestruct.tm_wday  = 0;
377       timestruct.tm_yday  = 0;
378       timestruct.tm_isdst = -1;
379       l->tstamp = mktime (&timestruct); /* store the time-stamp */
380
381       free (line);
382     }
383
384   fclose (fp);
385   return dir;
386 }
387
388 #ifdef HAVE_FTPPARSE
389
390 /* This is a "glue function" that connects the ftpparse interface to
391    the interface Wget expects.  ftpparse is used to parse listings
392    from servers other than Unix, like those running VMS or NT. */
393
394 static struct fileinfo *
395 ftp_parse_nonunix_ls (const char *file)
396 {
397   FILE *fp;
398   int len;
399
400   char *line;          /* tokenizer */
401   struct fileinfo *dir, *l, cur; /* list creation */
402
403   fp = fopen (file, "rb");
404   if (!fp)
405     {
406       logprintf (LOG_NOTQUIET, "%s: %s\n", file, strerror (errno));
407       return NULL;
408     }
409   dir = l = NULL;
410
411   /* Line loop to end of file: */
412   while ((line = read_whole_line (fp)))
413     {
414       struct ftpparse fp;
415
416       DEBUGP (("%s\n", line));
417       len = strlen (line);
418       /* Destroy <CR><LF> if present.  */
419       if (len && line[len - 1] == '\n')
420         line[--len] = '\0';
421       if (len && line[len - 1] == '\r')
422         line[--len] = '\0';
423
424       if (ftpparse(&fp, line, len))
425         {
426           cur.size = fp.size;
427           cur.name = (char *)xmalloc (fp.namelen + 1);
428           memcpy (cur.name, fp.name, fp.namelen);
429           cur.name[fp.namelen] = '\0';
430           DEBUGP (("%s\n", cur.name));
431           /* No links on non-UNIX systems */
432           cur.linkto = NULL;
433           /* ftpparse won't tell us correct permisions. So lets just invent
434              something. */
435           if (fp.flagtrycwd)
436             {
437               cur.type = FT_DIRECTORY;
438               cur.perms = 0755;
439             } 
440           else 
441             {
442               cur.type = FT_PLAINFILE;
443               cur.perms = 0644;
444             }
445           if (!dir)
446             {
447               l = dir = (struct fileinfo *)xmalloc (sizeof (struct fileinfo));
448               memcpy (l, &cur, sizeof (cur));
449               l->prev = l->next = NULL;
450             }
451           else 
452             {
453               cur.prev = l;
454               l->next = (struct fileinfo *)xmalloc (sizeof (struct fileinfo));
455               l = l->next;
456               memcpy (l, &cur, sizeof (cur));
457               l->next = NULL;
458             }
459           l->tstamp = fp.mtime;
460       }
461
462       free (line);
463     }
464
465   fclose (fp);
466   return dir;
467 }
468 #endif
469
470 /* This function switches between the correct parsing routine
471    depending on the SYSTEM_TYPE.  If system type is ST_UNIX, we use
472    our home-grown ftp_parse_unix_ls; otherwise, we use our interface
473    to ftpparse, also known as ftp_parse_nonunix_ls.  The system type
474    should be based on the result of the "SYST" response of the FTP
475    server.  */
476
477 struct fileinfo *
478 ftp_parse_ls (const char *file, const enum stype system_type)
479 {
480   if (system_type == ST_UNIX)
481     {
482       return ftp_parse_unix_ls (file);
483     }
484   else
485     {
486 #ifdef HAVE_FTPPARSE
487       return ftp_parse_nonunix_ls (file);
488 #else
489       /* #### Maybe log some warning here? */ 
490       return ftp_parse_unix_ls (file);
491 #endif
492     }
493 }
494 \f
495 /* Stuff for creating FTP index. */
496
497 /* The function returns the pointer to the malloc-ed quoted version of
498    string s.  It will recognize and quote numeric and special graphic
499    entities, as per RFC1866:
500
501    `&' -> `&amp;'
502    `<' -> `&lt;'
503    `>' -> `&gt;'
504    `"' -> `&quot;'
505
506    No other entities are recognized or replaced.  */
507 static char *
508 html_quote_string (const char *s)
509 {
510   const char *b = s;
511   char *p, *res;
512   int i;
513
514   /* Pass through the string, and count the new size.  */
515   for (i = 0; *s; s++, i++)
516     {
517       if (*s == '&')
518         i += 4;                /* `amp;' */
519       else if (*s == '<' || *s == '>')
520         i += 3;                /* `lt;' and `gt;' */
521       else if (*s == '\"')
522         i += 5;                /* `quot;' */
523     }
524   res = (char *)xmalloc (i + 1);
525   s = b;
526   for (p = res; *s; s++)
527     {
528       switch (*s)
529         {
530         case '&':
531           *p++ = '&';
532           *p++ = 'a';
533           *p++ = 'm';
534           *p++ = 'p';
535           *p++ = ';';
536           break;
537         case '<': case '>':
538           *p++ = '&';
539           *p++ = (*s == '<' ? 'l' : 'g');
540           *p++ = 't';
541           *p++ = ';';
542           break;
543         case '\"':
544           *p++ = '&';
545           *p++ = 'q';
546           *p++ = 'u';
547           *p++ = 'o';
548           *p++ = 't';
549           *p++ = ';';
550           break;
551         default:
552           *p++ = *s;
553         }
554     }
555   *p = '\0';
556   return res;
557 }
558
559 /* The function creates an HTML index containing references to given
560    directories and files on the appropriate host.  The references are
561    FTP.  */
562 uerr_t
563 ftp_index (const char *file, struct urlinfo *u, struct fileinfo *f)
564 {
565   FILE *fp;
566   char *upwd;
567   char *htclfile;               /* HTML-clean file name */
568
569   if (!opt.dfp)
570     {
571       fp = fopen (file, "wb");
572       if (!fp)
573         {
574           logprintf (LOG_NOTQUIET, "%s: %s\n", file, strerror (errno));
575           return FOPENERR;
576         }
577     }
578   else
579     fp = opt.dfp;
580   if (u->user)
581     {
582       char *tmpu, *tmpp;        /* temporary, clean user and passwd */
583
584       tmpu = CLEANDUP (u->user);
585       tmpp = u->passwd ? CLEANDUP (u->passwd) : NULL;
586       upwd = (char *)xmalloc (strlen (tmpu)
587                              + (tmpp ? (1 + strlen (tmpp)) : 0) + 2);
588       sprintf (upwd, "%s%s%s@", tmpu, tmpp ? ":" : "", tmpp ? tmpp : "");
589       free (tmpu);
590       FREE_MAYBE (tmpp);
591     }
592   else
593     upwd = xstrdup ("");
594   fprintf (fp, "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n");
595   fprintf (fp, "<html>\n<head>\n<title>");
596   fprintf (fp, _("Index of /%s on %s:%d"), u->dir, u->host, u->port);
597   fprintf (fp, "</title>\n</head>\n<body>\n<h1>");
598   fprintf (fp, _("Index of /%s on %s:%d"), u->dir, u->host, u->port);
599   fprintf (fp, "</h1>\n<hr>\n<pre>\n");
600   while (f)
601     {
602       fprintf (fp, "  ");
603       if (f->tstamp != -1)
604         {
605           /* #### Should we translate the months? */
606           static char *months[] = {
607             "Jan", "Feb", "Mar", "Apr", "May", "Jun",
608             "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
609           };
610           struct tm *ptm = localtime ((time_t *)&f->tstamp);
611
612           fprintf (fp, "%d %s %02d ", ptm->tm_year + 1900, months[ptm->tm_mon],
613                   ptm->tm_mday);
614           if (ptm->tm_hour)
615             fprintf (fp, "%02d:%02d  ", ptm->tm_hour, ptm->tm_min);
616           else
617             fprintf (fp, "       ");
618         }
619       else
620         fprintf (fp, _("time unknown       "));
621       switch (f->type)
622         {
623         case FT_PLAINFILE:
624           fprintf (fp, _("File        "));
625           break;
626         case FT_DIRECTORY:
627           fprintf (fp, _("Directory   "));
628           break;
629         case FT_SYMLINK:
630           fprintf (fp, _("Link        "));
631           break;
632         default:
633           fprintf (fp, _("Not sure    "));
634           break;
635         }
636       htclfile = html_quote_string (f->name);
637       fprintf (fp, "<a href=\"ftp://%s%s:%hu", upwd, u->host, u->port);
638       if (*u->dir != '/')
639         putc ('/', fp);
640       fprintf (fp, "%s", u->dir);
641       if (*u->dir)
642         putc ('/', fp);
643       fprintf (fp, "%s", htclfile);
644       if (f->type == FT_DIRECTORY)
645         putc ('/', fp);
646       fprintf (fp, "\">%s", htclfile);
647       if (f->type == FT_DIRECTORY)
648         putc ('/', fp);
649       fprintf (fp, "</a> ");
650       if (f->type == FT_PLAINFILE)
651         fprintf (fp, _(" (%s bytes)"), legible (f->size));
652       else if (f->type == FT_SYMLINK)
653         fprintf (fp, "-> %s", f->linkto ? f->linkto : "(nil)");
654       putc ('\n', fp);
655       free (htclfile);
656       f = f->next;
657     }
658   fprintf (fp, "</pre>\n</body>\n</html>\n");
659   free (upwd);
660   if (!opt.dfp)
661     fclose (fp);
662   else
663     fflush (fp);
664   return FTPOK;
665 }