Import version 20001223.
[ftpparse.git] / ftpparse.c
1 /* ftpparse.c, ftpparse.h: library for parsing FTP LIST responses
2 20001223
3 D. J. Bernstein, djb@cr.yp.to
4 http://cr.yp.to/ftpparse.html
5
6 Commercial use is fine, if you let me know what programs you're using this in.
7
8 Currently covered formats:
9 EPLF.
10 UNIX ls, with or without gid.
11 Microsoft FTP Service.
12 Windows NT FTP Server.
13 VMS.
14 WFTPD.
15 NetPresenz (Mac).
16 NetWare.
17 MSDOS.
18
19 Definitely not covered: 
20 Long VMS filenames, with information split across two lines.
21 NCSA Telnet FTP server. Has LIST = NLST (and bad NLST for directories).
22 */
23
24 #include <time.h>
25 #include "ftpparse.h"
26
27 static long totai(long year,long month,long mday)
28 {
29   long result;
30   if (month >= 2) month -= 2;
31   else { month += 10; --year; }
32   result = (mday - 1) * 10 + 5 + 306 * month;
33   result /= 10;
34   if (result == 365) { year -= 3; result = 1460; }
35   else result += 365 * (year % 4);
36   year /= 4;
37   result += 1461 * (year % 25);
38   year /= 25;
39   if (result == 36524) { year -= 3; result = 146096; }
40   else { result += 36524 * (year % 4); }
41   year /= 4;
42   result += 146097 * (year - 5);
43   result += 11017;
44   return result * 86400;
45 }
46
47 static int flagneedbase = 1;
48 static time_t base; /* time() value on this OS at the beginning of 1970 TAI */
49 static long now; /* current time */
50 static int flagneedcurrentyear = 1;
51 static long currentyear; /* approximation to current year */
52
53 static void initbase(void)
54 {
55   struct tm *t;
56   if (!flagneedbase) return;
57
58   base = 0;
59   t = gmtime(&base);
60   base = -(totai(t->tm_year + 1900,t->tm_mon,t->tm_mday) + t->tm_hour * 3600 + t->tm_min * 60 + t->tm_sec);
61   /* assumes the right time_t, counting seconds. */
62   /* base may be slightly off if time_t counts non-leap seconds. */
63   flagneedbase = 0;
64 }
65
66 static void initnow(void)
67 {
68   long day;
69   long year;
70
71   initbase();
72   now = time((time_t *) 0) - base;
73
74   if (flagneedcurrentyear) {
75     day = now / 86400;
76     if ((now % 86400) < 0) --day;
77     day -= 11017;
78     year = 5 + day / 146097;
79     day = day % 146097;
80     if (day < 0) { day += 146097; --year; }
81     year *= 4;
82     if (day == 146096) { year += 3; day = 36524; }
83     else { year += day / 36524; day %= 36524; }
84     year *= 25;
85     year += day / 1461;
86     day %= 1461;
87     year *= 4;
88     if (day == 1460) { year += 3; day = 365; }
89     else { year += day / 365; day %= 365; }
90     day *= 10;
91     if ((day + 5) / 306 >= 10) ++year;
92     currentyear = year;
93     flagneedcurrentyear = 0;
94   }
95 }
96
97 /* UNIX ls does not show the year for dates in the last six months. */
98 /* So we have to guess the year. */
99 /* Apparently NetWare uses ``twelve months'' instead of ``six months''; ugh. */
100 /* Some versions of ls also fail to show the year for future dates. */
101 static long guesstai(long month,long mday)
102 {
103   long year;
104   long t;
105
106   initnow();
107
108   for (year = currentyear - 1;year < currentyear + 100;++year) {
109     t = totai(year,month,mday);
110     if (now - t < 350 * 86400)
111       return t;
112   }
113 }
114
115 static int check(char *buf,char *monthname)
116 {
117   if ((buf[0] != monthname[0]) && (buf[0] != monthname[0] - 32)) return 0;
118   if ((buf[1] != monthname[1]) && (buf[1] != monthname[1] - 32)) return 0;
119   if ((buf[2] != monthname[2]) && (buf[2] != monthname[2] - 32)) return 0;
120   return 1;
121 }
122
123 static char *months[12] = {
124   "jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"
125 } ;
126
127 static int getmonth(char *buf,int len)
128 {
129   int i;
130   if (len == 3)
131     for (i = 0;i < 12;++i)
132       if (check(buf,months[i])) return i;
133   return -1;
134 }
135
136 static long getlong(char *buf,int len)
137 {
138   long u = 0;
139   while (len-- > 0)
140     u = u * 10 + (*buf++ - '0');
141   return u;
142 }
143
144 int ftpparse(struct ftpparse *fp,char *buf,int len)
145 {
146   int i;
147   int j;
148   int state;
149   long size;
150   long year;
151   long month;
152   long mday;
153   long hour;
154   long minute;
155
156   fp->name = 0;
157   fp->namelen = 0;
158   fp->flagtrycwd = 0;
159   fp->flagtryretr = 0;
160   fp->sizetype = FTPPARSE_SIZE_UNKNOWN;
161   fp->size = 0;
162   fp->mtimetype = FTPPARSE_MTIME_UNKNOWN;
163   fp->mtime = 0;
164   fp->idtype = FTPPARSE_ID_UNKNOWN;
165   fp->id = 0;
166   fp->idlen = 0;
167
168   if (len < 2) /* an empty name in EPLF, with no info, could be 2 chars */
169     return 0;
170
171   switch(*buf) {
172     /* see http://pobox.com/~djb/proto/eplf.txt */
173     /* "+i8388621.29609,m824255902,/,\tdev" */
174     /* "+i8388621.44468,m839956783,r,s10376,\tRFCEPLF" */
175     case '+':
176       i = 1;
177       for (j = 1;j < len;++j) {
178         if (buf[j] == 9) {
179           fp->name = buf + j + 1;
180           fp->namelen = len - j - 1;
181           return 1;
182         }
183         if (buf[j] == ',') {
184           switch(buf[i]) {
185             case '/':
186               fp->flagtrycwd = 1;
187               break;
188             case 'r':
189               fp->flagtryretr = 1;
190               break;
191             case 's':
192               fp->sizetype = FTPPARSE_SIZE_BINARY;
193               fp->size = getlong(buf + i + 1,j - i - 1);
194               break;
195             case 'm':
196               fp->mtimetype = FTPPARSE_MTIME_LOCAL;
197               initbase();
198               fp->mtime = base + getlong(buf + i + 1,j - i - 1);
199               break;
200             case 'i':
201               fp->idtype = FTPPARSE_ID_FULL;
202               fp->id = buf + i + 1;
203               fp->idlen = j - i - 1;
204           }
205           i = j + 1;
206         }
207       }
208       return 0;
209     
210     /* UNIX-style listing, without inum and without blocks */
211     /* "-rw-r--r--   1 root     other        531 Jan 29 03:26 README" */
212     /* "dr-xr-xr-x   2 root     other        512 Apr  8  1994 etc" */
213     /* "dr-xr-xr-x   2 root     512 Apr  8  1994 etc" */
214     /* "lrwxrwxrwx   1 root     other          7 Jan 25 00:17 bin -> usr/bin" */
215     /* Also produced by Microsoft's FTP servers for Windows: */
216     /* "----------   1 owner    group         1803128 Jul 10 10:18 ls-lR.Z" */
217     /* "d---------   1 owner    group               0 May  9 19:45 Softlib" */
218     /* Also WFTPD for MSDOS: */
219     /* "-rwxrwxrwx   1 noone    nogroup      322 Aug 19  1996 message.ftp" */
220     /* Also NetWare: */
221     /* "d [R----F--] supervisor            512       Jan 16 18:53    login" */
222     /* "- [R----F--] rhesus             214059       Oct 20 15:27    cx.exe" */
223     /* Also NetPresenz for the Mac: */
224     /* "-------r--         326  1391972  1392298 Nov 22  1995 MegaPhone.sit" */
225     /* "drwxrwxr-x               folder        2 May 10  1996 network" */
226     case 'b':
227     case 'c':
228     case 'd':
229     case 'l':
230     case 'p':
231     case 's':
232     case '-':
233
234       if (*buf == 'd') fp->flagtrycwd = 1;
235       if (*buf == '-') fp->flagtryretr = 1;
236       if (*buf == 'l') fp->flagtrycwd = fp->flagtryretr = 1;
237
238       state = 1;
239       i = 0;
240       for (j = 1;j < len;++j)
241         if ((buf[j] == ' ') && (buf[j - 1] != ' ')) {
242           switch(state) {
243             case 1: /* skipping perm */
244               state = 2;
245               break;
246             case 2: /* skipping nlink */
247               state = 3;
248               if ((j - i == 6) && (buf[i] == 'f')) /* for NetPresenz */
249                 state = 4;
250               break;
251             case 3: /* skipping uid */
252               state = 4;
253               break;
254             case 4: /* getting tentative size */
255               size = getlong(buf + i,j - i);
256               state = 5;
257               break;
258             case 5: /* searching for month, otherwise getting tentative size */
259               month = getmonth(buf + i,j - i);
260               if (month >= 0)
261                 state = 6;
262               else
263                 size = getlong(buf + i,j - i);
264               break;
265             case 6: /* have size and month */
266               mday = getlong(buf + i,j - i);
267               state = 7;
268               break;
269             case 7: /* have size, month, mday */
270               if ((j - i == 4) && (buf[i + 1] == ':')) {
271                 hour = getlong(buf + i,1);
272                 minute = getlong(buf + i + 2,2);
273                 fp->mtimetype = FTPPARSE_MTIME_REMOTEMINUTE;
274                 initbase();
275                 fp->mtime = base + guesstai(month,mday) + hour * 3600 + minute * 60;
276               } else if ((j - i == 5) && (buf[i + 2] == ':')) {
277                 hour = getlong(buf + i,2);
278                 minute = getlong(buf + i + 3,2);
279                 fp->mtimetype = FTPPARSE_MTIME_REMOTEMINUTE;
280                 initbase();
281                 fp->mtime = base + guesstai(month,mday) + hour * 3600 + minute * 60;
282               }
283               else if (j - i >= 4) {
284                 year = getlong(buf + i,j - i);
285                 fp->mtimetype = FTPPARSE_MTIME_REMOTEDAY;
286                 initbase();
287                 fp->mtime = base + totai(year,month,mday);
288               }
289               else
290                 return 0;
291               fp->name = buf + j + 1;
292               fp->namelen = len - j - 1;
293               state = 8;
294               break;
295             case 8: /* twiddling thumbs */
296               break;
297           }
298           i = j + 1;
299           while ((i < len) && (buf[i] == ' ')) ++i;
300         }
301
302       if (state != 8)
303         return 0;
304
305       fp->size = size;
306       fp->sizetype = FTPPARSE_SIZE_BINARY;
307
308       if (*buf == 'l')
309         for (i = 0;i + 3 < fp->namelen;++i)
310           if (fp->name[i] == ' ')
311             if (fp->name[i + 1] == '-')
312               if (fp->name[i + 2] == '>')
313                 if (fp->name[i + 3] == ' ') {
314                   fp->namelen = i;
315                   break;
316                 }
317
318       /* eliminate extra NetWare spaces */
319       if ((buf[1] == ' ') || (buf[1] == '['))
320         if (fp->namelen > 3)
321           if (fp->name[0] == ' ')
322             if (fp->name[1] == ' ')
323               if (fp->name[2] == ' ') {
324                 fp->name += 3;
325                 fp->namelen -= 3;
326               }
327
328       return 1;
329   }
330
331   /* MultiNet (some spaces removed from examples) */
332   /* "00README.TXT;1      2 30-DEC-1996 17:44 [SYSTEM] (RWED,RWED,RE,RE)" */
333   /* "CORE.DIR;1          1  8-SEP-1996 16:09 [SYSTEM] (RWE,RWE,RE,RE)" */
334   /* and non-MutliNet VMS: */
335   /* "CII-MANUAL.TEX;1  213/216  29-JAN-1996 03:33:12  [ANONYMOU,ANONYMOUS]   (RWED,RWED,,)" */
336   for (i = 0;i < len;++i)
337     if (buf[i] == ';')
338       break;
339   if (i < len) {
340     fp->name = buf;
341     fp->namelen = i;
342     if (i > 4)
343       if (buf[i - 4] == '.')
344         if (buf[i - 3] == 'D')
345           if (buf[i - 2] == 'I')
346             if (buf[i - 1] == 'R') {
347               fp->namelen -= 4;
348               fp->flagtrycwd = 1;
349             }
350     if (!fp->flagtrycwd)
351       fp->flagtryretr = 1;
352     while (buf[i] != ' ') if (++i == len) return 0;
353     while (buf[i] == ' ') if (++i == len) return 0;
354     while (buf[i] != ' ') if (++i == len) return 0;
355     while (buf[i] == ' ') if (++i == len) return 0;
356     j = i;
357     while (buf[j] != '-') if (++j == len) return 0;
358     mday = getlong(buf + i,j - i);
359     while (buf[j] == '-') if (++j == len) return 0;
360     i = j;
361     while (buf[j] != '-') if (++j == len) return 0;
362     month = getmonth(buf + i,j - i);
363     if (month < 0) return 0;
364     while (buf[j] == '-') if (++j == len) return 0;
365     i = j;
366     while (buf[j] != ' ') if (++j == len) return 0;
367     year = getlong(buf + i,j - i);
368     while (buf[j] == ' ') if (++j == len) return 0;
369     i = j;
370     while (buf[j] != ':') if (++j == len) return 0;
371     hour = getlong(buf + i,j - i);
372     while (buf[j] == ':') if (++j == len) return 0;
373     i = j;
374     while ((buf[j] != ':') && (buf[j] != ' ')) if (++j == len) return 0;
375     minute = getlong(buf + i,j - i);
376
377     fp->mtimetype = FTPPARSE_MTIME_REMOTEMINUTE;
378     initbase();
379     fp->mtime = base + totai(year,month,mday) + hour * 3600 + minute * 60;
380
381     return 1;
382   }
383
384   /* MSDOS format */
385   /* 04-27-00  09:09PM       <DIR>          licensed */
386   /* 07-18-00  10:16AM       <DIR>          pub */
387   /* 04-14-00  03:47PM                  589 readme.htm */
388   if ((*buf >= '0') && (*buf <= '9')) {
389     i = 0;
390     j = 0;
391     while (buf[j] != '-') if (++j == len) return 0;
392     month = getlong(buf + i,j - i) - 1;
393     while (buf[j] == '-') if (++j == len) return 0;
394     i = j;
395     while (buf[j] != '-') if (++j == len) return 0;
396     mday = getlong(buf + i,j - i);
397     while (buf[j] == '-') if (++j == len) return 0;
398     i = j;
399     while (buf[j] != ' ') if (++j == len) return 0;
400     year = getlong(buf + i,j - i);
401     if (year < 50) year += 2000;
402     if (year < 1000) year += 1900;
403     while (buf[j] == ' ') if (++j == len) return 0;
404     i = j;
405     while (buf[j] != ':') if (++j == len) return 0;
406     hour = getlong(buf + i,j - i);
407     while (buf[j] == ':') if (++j == len) return 0;
408     i = j;
409     while ((buf[j] != 'A') && (buf[j] != 'P')) if (++j == len) return 0;
410     minute = getlong(buf + i,j - i);
411     if (hour == 12) hour = 0;
412     if (buf[j] == 'A') if (++j == len) return 0;
413     if (buf[j] == 'P') { hour += 12; if (++j == len) return 0; }
414     if (buf[j] == 'M') if (++j == len) return 0;
415
416     while (buf[j] == ' ') if (++j == len) return 0;
417     if (buf[j] == '<') {
418       fp->flagtrycwd = 1;
419       while (buf[j] != ' ') if (++j == len) return 0;
420     }
421     else {
422       i = j;
423       while (buf[j] != ' ') if (++j == len) return 0;
424       fp->size = getlong(buf + i,j - i);
425       fp->sizetype = FTPPARSE_SIZE_BINARY;
426       fp->flagtryretr = 1;
427     }
428     while (buf[j] == ' ') if (++j == len) return 0;
429
430     fp->name = buf + j;
431     fp->namelen = len - j;
432
433     fp->mtimetype = FTPPARSE_MTIME_REMOTEMINUTE;
434     initbase();
435     fp->mtime = base + totai(year,month,mday) + hour * 3600 + minute * 60;
436
437     return 1;
438   }
439
440   /* Some useless lines, safely ignored: */
441   /* "Total of 11 Files, 10966 Blocks." (VMS) */
442   /* "total 14786" (UNIX) */
443   /* "DISK$ANONFTP:[ANONYMOUS]" (VMS) */
444   /* "Directory DISK$PCSA:[ANONYM]" (VMS) */
445
446   return 0;
447 }