/* ftpparse.c, ftpparse.h: library for parsing FTP LIST responses
   20001223
   D. J. Bernstein, djb@cr.yp.to
http://cr.yp.to/ftpparse.html

Commercial use is fine, if you let me know what programs you're using this in.

Currently covered formats:
EPLF.
UNIX ls, with or without gid.
Microsoft FTP Service.
Windows NT FTP Server.
VMS.
WFTPD.
NetPresenz (Mac).
NetWare.
MSDOS.

Definitely not covered:
var VMS filenames, with information split across two lines.
NCSA Telnet FTP server. Has LIST = NLST (and bad NLST for directories).
*/

//static var totai(var year,var month,var mday)
function totai(year, month, mday) {
  var result;
  if (month >= 2) {
    month -= 2;
  } else { month += 10; --year; }

  result = (mday - 1) * 10 + 5 + 306 * month;
  result /= 10;

  if (result === 365) { year -= 3; result = 1460; }
  else { result += 365 * (year % 4); }
  year /= 4;
  result += 1461 * (year % 25);
  year /= 25;
  if (result === 36524) { year -= 3; result = 146096; }
  else { result += 36524 * (year % 4); }
  year /= 4;
  result += 146097 * (year - 5);
  result += 11017;
  return result * 86400;
}

var flagneedbase = 1;
//time_t base; [> time() value on this OS at the beginning of 1970 TAI <]
var base = 0;
var now; /* current time */
var flagneedcurrentyear = 1;
var currentyear; /* approximation to current year */

//static void initbase(void)
function initBase() {
  struct tm *t;
  if (!flagneedbase) return;

  base = 0;
  t = gmtime(&base);
  base = -(
    totai( t->tm_year + 1900, t->tm_mon, t->tm_mday) +
    t->tm_hour * 3600 + t->tm_min * 60 + t->tm_sec
  );
  /* assumes the right time_t, counting seconds. */
  /* base may be slightly off if time_t counts non-leap seconds. */
  flagneedbase = 0;
}

static void initnow(void)
{
  var day;
  var year;

  initbase();
  now = time((time_t *) 0) - base;

  if (flagneedcurrentyear) {
    day = now / 86400;
    if ((now % 86400) < 0) --day;
    day -= 11017;
    year = 5 + day / 146097;
    day = day % 146097;
    if (day < 0) { day += 146097; --year; }
    year *= 4;
    if (day === 146096) { year += 3; day = 36524; }
    else { year += day / 36524; day %= 36524; }
    year *= 25;
    year += day / 1461;
    day %= 1461;
    year *= 4;
    if (day === 1460) { year += 3; day = 365; }
    else { year += day / 365; day %= 365; }
    day *= 10;
    if ((day + 5) / 306 >= 10) ++year;
    currentyear = year;
    flagneedcurrentyear = 0;
  }
}

/* UNIX ls does not show the year for dates in the last six months. */
/* So we have to guess the year. */
/* Apparently NetWare uses ``twelve months'' instead of ``six months''; ugh. */
/* Some versions of ls also fail to show the year for future dates. */
function guesstai(month, mday) {
  var year;
  var t;

  initnow();

  for (year = currentyear - 1;year < currentyear + 100;++year) {
    t = totai(year,month,mday);
    if (now - t < 350 * 86400) {
      return t;
    }
  }
}

static var check(char *buf,char *monthname)
{
  if ((buf[0] != monthname[0]) && (buf[0] != monthname[0] - 32)) return 0;
  if ((buf[1] != monthname[1]) && (buf[1] != monthname[1] - 32)) return 0;
  if ((buf[2] != monthname[2]) && (buf[2] != monthname[2] - 32)) return 0;
  return 1;
}

var months = [
  "jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"
];

static var getmonth(char *buf,var len)
{
  var i;
  if (len === 3)
    for (i = 0;i < 12;++i)
  if (check(buf,months[i])) return i;
  return -1;
}

static var getlong(char *buf,var len)
{
  var u = 0;
  while (len-- > 0)
    u = u * 10 + (*buf++ - '0');
  return u;
}

function ftpparse(buf) {
  var i;
  var j;
  var state;
  var size;
  var year;
  var month;
  var mday;
  var hour;
  var minute;

  var len = buf.length;
  var fp = {
    name : 0;
    namelen : 0;
    flagtrycwd : 0;
    flagtryretr : 0;
    sizetype : FTPPARSE_SIZE_UNKNOWN;
    size : 0;
    mtimetype : FTPPARSE_MTIME_UNKNOWN;
    mtime : 0;
    idtype : FTPPARSE_ID_UNKNOWN;
    id : 0;
    idlen : 0;
  }

  if (len < 2) {/* an empty name in EPLF, with no info, could be 2 chars */
    return 0;
  }

  switch(buf[0])) {
    /* see http://pobox.com/~djb/proto/eplf.txt */
    /* "+i8388621.29609,m824255902,/,\tdev" */
    /* "+i8388621.44468,m839956783,r,s10376,\tRFCEPLF" */
    case '+':
      i = 1;
    for (j = 1;j < len;++j) {
      if (buf[j] === 9) {
        fp.name = buf + j + 1;
        fp.namelen = len - j - 1;
        return 1;
      }
      if (buf[j] === ',') {
        switch(buf[i]) {
          case '/':
            fp.flagtrycwd = 1;
          break;
          case 'r':
            fp.flagtryretr = 1;
          break;
          case 's':
            fp.sizetype = FTPPARSE_SIZE_BINARY;
          fp.size = getlong(buf + i + 1,j - i - 1);
          break;
          case 'm':
            fp.mtimetype = FTPPARSE_MTIME_LOCAL;
          initbase();
          fp.mtime = base + getlong(buf + i + 1,j - i - 1);
          break;
          case 'i':
            fp.idtype = FTPPARSE_ID_FULL;
          fp.id = buf + i + 1;
          fp.idlen = j - i - 1;
        }
        i = j + 1;
      }
    }
    return 0;

    /* UNIX-style listing, without inum and without blocks */
    /* "-rw-r--r--   1 root     other        531 Jan 29 03:26 README" */
    /* "dr-xr-xr-x   2 root     other        512 Apr  8  1994 etc" */
    /* "dr-xr-xr-x   2 root     512 Apr  8  1994 etc" */
    /* "lrwxrwxrwx   1 root     other          7 Jan 25 00:17 bin -> usr/bin" */
    /* Also produced by Microsoft's FTP servers for Windows: */
    /* "----------   1 owner    group         1803128 Jul 10 10:18 ls-lR.Z" */
    /* "d---------   1 owner    group               0 May  9 19:45 Softlib" */
    /* Also WFTPD for MSDOS: */
    /* "-rwxrwxrwx   1 noone    nogroup      322 Aug 19  1996 message.ftp" */
    /* Also NetWare: */
    /* "d [R----F--] supervisor            512       Jan 16 18:53    login" */
    /* "- [R----F--] rhesus             214059       Oct 20 15:27    cx.exe" */
    /* Also NetPresenz for the Mac: */
    /* "-------r--         326  1391972  1392298 Nov 22  1995 MegaPhone.sit" */
    /* "drwxrwxr-x               folder        2 May 10  1996 network" */
    case 'b':
      case 'c':
      case 'd':
      case 'l':
      case 'p':
      case 's':
      case '-':

      if (buf === 'd') fp.flagtrycwd = 1;
    if (buf === '-') fp.flagtryretr = 1;
    if (buf === 'l') fp.flagtrycwd = fp.flagtryretr = 1;

    state = 1;
    i = 0;
    for (j = 1;j < len;++j)
    if ((buf[j] === ' ') && (buf[j - 1] != ' ')) {
      switch(state) {
        case 1: /* skipping perm */
        state = 2;
        break;
        case 2: /* skipping nlink */
        state = 3;
        if ((j - i === 6) && (buf[i] === 'f')) /* for NetPresenz */
          state = 4;
        break;
        case 3: /* skipping uid */
        state = 4;
        break;
        case 4: /* getting tentative size */
        size = getlong(buf + i,j - i);
        state = 5;
        break;
        case 5: /* searching for month, otherwise getting tentative size */
        month = getmonth(buf + i,j - i);
        if (month >= 0)
          state = 6;
        else
          size = getlong(buf + i,j - i);
        break;
        case 6: /* have size and month */
        mday = getlong(buf + i,j - i);
        state = 7;
        break;
        case 7: /* have size, month, mday */
        if ((j - i === 4) && (buf[i + 1] === ':')) {
          hour = getlong(buf + i,1);
          minute = getlong(buf + i + 2,2);
          fp.mtimetype = FTPPARSE_MTIME_REMOTEMINUTE;
          initbase();
          fp.mtime = base + guesstai(month,mday) + hour * 3600 + minute * 60;
        } else if ((j - i === 5) && (buf[i + 2] === ':')) {
          hour = getlong(buf + i,2);
          minute = getlong(buf + i + 3,2);
          fp.mtimetype = FTPPARSE_MTIME_REMOTEMINUTE;
          initbase();
          fp.mtime = base + guesstai(month,mday) + hour * 3600 + minute * 60;
        }
        else if (j - i >= 4) {
          year = getlong(buf + i,j - i);
          fp.mtimetype = FTPPARSE_MTIME_REMOTEDAY;
          initbase();
          fp.mtime = base + totai(year,month,mday);
        }
        else
          return 0;
        fp.name = buf + j + 1;
        fp.namelen = len - j - 1;
        state = 8;
        break;
        case 8: /* twiddling thumbs */
        break;
      }
      i = j + 1;
      while ((i < len) && (buf[i] === ' ')) ++i;
    }

    if (state != 8)
      return 0;

    fp.size = size;
    fp.sizetype = FTPPARSE_SIZE_BINARY;

    if (buf === 'l')
      for (i = 0;i + 3 < fp.namelen;++i)
    if (fp.name[i] === ' ')
      if (fp.name[i + 1] === '-')
        if (fp.name[i + 2] === '>')
          if (fp.name[i + 3] === ' ') {
            fp.namelen = i;
            break;
          }

          /* eliminate extra NetWare spaces */
          if ((buf[1] === ' ') || (buf[1] === '['))
            if (fp.namelen > 3)
              if (fp.name[0] === ' ')
                if (fp.name[1] === ' ')
                  if (fp.name[2] === ' ') {
                    fp.name += 3;
                    fp.namelen -= 3;
                  }

                  return 1;
  }

  /* MultiNet (some spaces removed from examples) */
  /* "00README.TXT;1      2 30-DEC-1996 17:44 [SYSTEM] (RWED,RWED,RE,RE)" */
  /* "CORE.DIR;1          1  8-SEP-1996 16:09 [SYSTEM] (RWE,RWE,RE,RE)" */
  /* and non-MutliNet VMS: */
  /* "CII-MANUAL.TEX;1  213/216  29-JAN-1996 03:33:12  [ANONYMOU,ANONYMOUS]   (RWED,RWED,,)" */
  for (i = 0;i < len;++i)
  if (buf[i] === ';')
  break;
  if (i < len) {
    fp.name = buf;
    fp.namelen = i;
    if (i > 4){
      if (buf[i - 4] === '.'){
        if (buf[i - 3] === 'D'){
          if (buf[i - 2] === 'I'){
            if (buf[i - 1] === 'R') {
              fp.namelen -= 4;
              fp.flagtrycwd = 1;
            }
          }
        }
      }
    }

    if (!fp.flagtrycwd){
      fp.flagtryretr = 1;
    }

    while (buf[i] != ' ') if (++i === len) return 0;
    while (buf[i] === ' ') if (++i === len) return 0;
    while (buf[i] != ' ') if (++i === len) return 0;
    while (buf[i] === ' ') if (++i === len) return 0;
    j = i;
    while (buf[j] != '-') if (++j === len) return 0;
    mday = getlong(buf + i,j - i);
    while (buf[j] === '-') if (++j === len) return 0;
    i = j;
    while (buf[j] != '-') if (++j === len) return 0;
    month = getmonth(buf + i,j - i);
    if (month < 0) return 0;
    while (buf[j] === '-') if (++j === len) return 0;
    i = j;
    while (buf[j] != ' ') if (++j === len) return 0;
    year = getlong(buf + i,j - i);
    while (buf[j] === ' ') if (++j === len) return 0;
    i = j;
    while (buf[j] != ':') if (++j === len) return 0;
    hour = getlong(buf + i,j - i);
    while (buf[j] === ':') if (++j === len) return 0;
    i = j;
    while ((buf[j] != ':') && (buf[j] != ' ')) if (++j === len) return 0;
    minute = getlong(buf + i,j - i);

    fp.mtimetype = FTPPARSE_MTIME_REMOTEMINUTE;
    initbase();
    fp.mtime = base + totai(year,month,mday) + hour * 3600 + minute * 60;

    return 1;
  }

  /* MSDOS format */
  /* 04-27-00  09:09PM       <DIR>          licensed */
  /* 07-18-00  10:16AM       <DIR>          pub */
  /* 04-14-00  03:47PM                  589 readme.htm */
  if ((buf >= '0') && (buf <= '9')) {
    i = 0;
    j = 0;
    while (buf[j] != '-') if (++j === len) return 0;
    month = getlong(buf + i,j - i) - 1;
    while (buf[j] === '-') if (++j === len) return 0;
    i = j;
    while (buf[j] != '-') if (++j === len) return 0;
    mday = getlong(buf + i,j - i);
    while (buf[j] === '-') if (++j === len) return 0;
    i = j;
    while (buf[j] != ' ') if (++j === len) return 0;
    year = getlong(buf + i,j - i);
    if (year < 50) year += 2000;
    if (year < 1000) year += 1900;
    while (buf[j] === ' ') if (++j === len) return 0;
    i = j;
    while (buf[j] != ':') if (++j === len) return 0;
    hour = getlong(buf + i,j - i);
    while (buf[j] === ':') if (++j === len) return 0;
    i = j;
    while ((buf[j] != 'A') && (buf[j] != 'P')) if (++j === len) return 0;
    minute = getlong(buf + i,j - i);
    if (hour === 12) hour = 0;
    if (buf[j] === 'A') if (++j === len) return 0;
    if (buf[j] === 'P') { hour += 12; if (++j === len) return 0; }
    if (buf[j] === 'M') if (++j === len) return 0;

    while (buf[j] === ' ') if (++j === len) return 0;
    if (buf[j] === '<') {
      fp.flagtrycwd = 1;
      while (buf[j] != ' ') if (++j === len) return 0;
    }
    else {
      i = j;
      while (buf[j] != ' ') if (++j === len) return 0;
      fp.size = getlong(buf + i,j - i);
      fp.sizetype = FTPPARSE_SIZE_BINARY;
      fp.flagtryretr = 1;
    }
    while (buf[j] === ' ') if (++j === len) return 0;

    fp.name = buf + j;
    fp.namelen = len - j;

    fp.mtimetype = FTPPARSE_MTIME_REMOTEMINUTE;
    initbase();
    fp.mtime = base + totai(year,month,mday) + hour * 3600 + minute * 60;

    return 1;
  }

  return 0;
}

