/*
   Virtual File System: FTP file system

   Copyright (C) 2015-2021
   The Free Software Foundation, Inc.

   Written by: Andrew Borodin <aborodin@vmail.ru>, 2013

   This file is part of the Midnight Commander.

   The Midnight Commander is free software: you can redistribute it
   and/or modify it under the terms of the GNU General Public License as
   published by the Free Software Foundation, either version 3 of the License,
   or (at your option) any later version.

   The Midnight Commander is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/** \file
 *  \brief Source: Virtual File System: FTP file system
 *  \author Andrew Borodin
 *  \date 2015
 *
 *  Parser of ftp long file list (reply to "LIST -la" command).
 *  Borrowed from lftp project (http://http://lftp.yar.ru/).
 *  Author of original lftp code: Alexander V. Lukyanov (lav@yars.free.net)
 */

#include <config.h>

#include <ctype.h>              /* isdigit() */
#include <stdio.h>              /* sscanf() */
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>           /* mode_t */
#include <time.h>
#include <unistd.h>
#include <sys/types.h>

#include "lib/global.h"

#include "lib/vfs/vfs.h"
#include "lib/vfs/utilvfs.h"

#include "ftpfs.h"

/*** global variables ****************************************************************************/

/*** file scope macro definitions ****************************************************************/

#define number_of_parsers 7

#define MINUTE (60)
#define HOUR   (60 * MINUTE)
#define DAY    (24 * HOUR)

#define NO_SIZE     ((off_t) (-1L))
#define NO_SIZE_YET ((off_t) (-2L))
#define NO_DATE     ((time_t) (-1L))
#define NO_DATE_YET ((time_t) (-2L))

#define FIRST_TOKEN strtok (line, " \t")
#define NEXT_TOKEN  strtok (NULL, " \t")
#define FIRST_TOKEN_R strtok_r (line, " \t", &next)
#define NEXT_TOKEN_R  strtok_r (NULL, " \t", &next)

#define ERR2 do { (*err)++; return FALSE; } while (FALSE)

/*** file scope type declarations ****************************************************************/

typedef enum
{
    UNKNOWN = 0,
    DIRECTORY,
    SYMLINK,
    NORMAL
} filetype;

typedef gboolean (*ftpfs_line_parser) (char *line, struct stat * s, char **filename,
                                       char **linkname, int *err);

/* formard declarations */
static gboolean ftpfs_parse_long_list_UNIX (char *line, struct stat *s, char **filename,
                                            char **linkname, int *err);
static gboolean ftpfs_parse_long_list_NT (char *line, struct stat *s, char **filename,
                                          char **linkname, int *err);
static gboolean ftpfs_parse_long_list_EPLF (char *line, struct stat *s, char **filename,
                                            char **linkname, int *err);
static gboolean ftpfs_parse_long_list_MLSD (char *line, struct stat *s, char **filename,
                                            char **linkname, int *err);
static gboolean ftpfs_parse_long_list_AS400 (char *line, struct stat *s, char **filename,
                                             char **linkname, int *err);
static gboolean ftpfs_parse_long_list_OS2 (char *line, struct stat *s, char **filename,
                                           char **linkname, int *err);
static gboolean ftpfs_parse_long_list_MacWebStar (char *line, struct stat *s, char **filename,
                                                  char **linkname, int *err);

/*** file scope variables ************************************************************************/

static time_t rawnow;
static struct tm now;

static ftpfs_line_parser line_parsers[number_of_parsers] = {
    ftpfs_parse_long_list_UNIX,
    ftpfs_parse_long_list_NT,
    ftpfs_parse_long_list_EPLF,
    ftpfs_parse_long_list_MLSD,
    ftpfs_parse_long_list_AS400,
    ftpfs_parse_long_list_OS2,
    ftpfs_parse_long_list_MacWebStar
};

/* --------------------------------------------------------------------------------------------- */
/*** file scope functions ************************************************************************/
/* --------------------------------------------------------------------------------------------- */

static inline uid_t
ftpfs_get_uid (const char *s)
{
    uid_t u;

    if (*s < '0' || *s > '9')
        u = vfs_finduid (s);
    else
        u = (uid_t) atol (s);

    return u;
}

/* --------------------------------------------------------------------------------------------- */

static inline gid_t
ftpfs_get_gid (const char *s)
{
    gid_t g;

    if (*s < '0' || *s > '9')
        g = vfs_findgid (s);
    else
        g = (gid_t) atol (s);

    return g;
}

/* --------------------------------------------------------------------------------------------- */

static void
ftpfs_init_time (void)
{
    time (&rawnow);
    now = *localtime (&rawnow);
}

/* --------------------------------------------------------------------------------------------- */

static int
guess_year (int month, int day, int hour, int minute)
{
    int year;

    (void) hour;
    (void) minute;

    year = now.tm_year + 1900;

    if (month * 32 + day > now.tm_mon * 32 + now.tm_mday + 6)
        year--;

    return year;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
parse_year_or_time (const char *year_or_time, int *year, int *hour, int *minute)
{
    if (year_or_time[2] == ':')
    {
        if (sscanf (year_or_time, "%2d:%2d", hour, minute) != 2)
            return FALSE;

        *year = -1;
    }
    else
    {
        if (sscanf (year_or_time, "%d", year) != 1)
            return FALSE;

        *hour = *minute = 0;
    }

    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

/* Converts struct tm to time_t, assuming the data in tm is UTC rather
   than local timezone (mktime assumes the latter).

   Contributed by Roger Beeman <beeman@cisco.com>, with the help of
   Mark Baushke <mdb@cisco.com> and the rest of the Gurus at CISCO.  */
static time_t
mktime_from_utc (const struct tm *t)
{
    struct tm tc;
    time_t tl, tb;

    memcpy (&tc, t, sizeof (struct tm));

    /* UTC times are never DST; if we say -1, we'll introduce odd localtime-
     * dependant errors. */

    tc.tm_isdst = 0;

    tl = mktime (&tc);
    if (tl == -1)
        return (-1);

    tb = mktime (gmtime (&tl));

    return (tl <= tb ? (tl + (tl - tb)) : (tl - (tb - tl)));
}

/* --------------------------------------------------------------------------------------------- */

static time_t
ftpfs_convert_date (const char *s)
{
    struct tm tm;
    int year, month, day, hour, minute, second;
    int skip = 0;
    int n;

    memset (&tm, 0, sizeof (tm));

    n = sscanf (s, "%4d%n", &year, &skip);

    /* try to workaround server's y2k bug *
     * I hope in the next 300 years the y2k bug will be finally fixed :) */
    if (n == 1 && year >= 1910 && year <= 1930)
    {
        n = sscanf (s, "%5d%n", &year, &skip);
        year = year - 19100 + 2000;
    }

    if (n != 1)
        return NO_DATE;

    n = sscanf (s + skip, "%2d%2d%2d%2d%2d", &month, &day, &hour, &minute, &second);

    if (n != 5)
        return NO_DATE;

    tm.tm_year = year - 1900;
    tm.tm_mon = month - 1;
    tm.tm_mday = day;
    tm.tm_hour = hour;
    tm.tm_min = minute;
    tm.tm_sec = second;

    return mktime_from_utc (&tm);
}

/* --------------------------------------------------------------------------------------------- */

/*
   -rwxr-xr-x   1 lav      root         4771 Sep 12  1996 install-sh
   -rw-r--r--   1 lav      root         1349 Feb  2 14:10 lftp.lsm
   drwxr-xr-x   4 lav      root         1024 Feb 22 15:32 lib
   lrwxrwxrwx   1 lav      root           33 Feb 14 17:45 ltconfig -> /usr/share/libtool/ltconfig

   NOTE: group may be missing.
 */

static gboolean
parse_ls_line (char *line, struct stat *s, char **filename, char **linkname)
{
    char *next = NULL;
    char *t;
    mode_t type, mode = 0;
    char *group_or_size;
    struct tm date;
    const char *day_of_month;
    gboolean year_anomaly = FALSE;
    char *name;

    /* parse perms */
    t = FIRST_TOKEN_R;
    if (t == NULL)
        return FALSE;

    if (!vfs_parse_filetype (t, NULL, &type))
        return FALSE;

    if (vfs_parse_fileperms (t + 1, NULL, &mode))
        mode |= type;

    s->st_mode = mode;

    /* link count */
    t = NEXT_TOKEN_R;
    if (t == NULL)
        return FALSE;
    s->st_nlink = atol (t);

    /* user */
    t = NEXT_TOKEN_R;
    if (t == NULL)
        return FALSE;

    s->st_uid = ftpfs_get_uid (t);

    /* group or size */
    group_or_size = NEXT_TOKEN_R;

    /* size or month */
    t = NEXT_TOKEN_R;
    if (t == NULL)
        return FALSE;
    if (isdigit ((unsigned char) *t))
    {
        /* it's size, so the previous was group: */
        long long size;
        int n;

        s->st_gid = ftpfs_get_gid (t);

        if (sscanf (t, "%lld%n", &size, &n) == 1 && t[n] == '\0')
            s->st_size = (off_t) size;
        t = NEXT_TOKEN_R;
        if (t == NULL)
            return FALSE;
    }
    else
    {
        /*  it was month, so the previous was size: */
        long long size;
        int n;

        if (sscanf (group_or_size, "%lld%n", &size, &n) == 1 && group_or_size[n] == '\0')
            s->st_size = (off_t) size;
    }

#ifdef HAVE_STRUCT_STAT_ST_BLKSIZE
    s->st_blksize = 512;
#endif
#ifdef HAVE_STRUCT_STAT_ST_BLOCKS
    s->st_blocks = (s->st_size + 511) / 512;
#endif

    memset (&date, 0, sizeof (date));

    if (!vfs_parse_month (t, &date))
        date.tm_mon = 0;

    day_of_month = NEXT_TOKEN_R;
    if (day_of_month == NULL)
        return FALSE;
    date.tm_mday = atoi (day_of_month);

    /* time or year */
    t = NEXT_TOKEN_R;
    if (t == NULL)
        return FALSE;
    date.tm_isdst = -1;
    date.tm_hour = date.tm_min = 0;
    date.tm_sec = 30;

    if (sscanf (t, "%2d:%2d", &date.tm_hour, &date.tm_min) == 2)
        date.tm_year = guess_year (date.tm_mon, date.tm_mday, date.tm_hour, date.tm_min) - 1900;
    else
    {
        if (day_of_month + strlen (day_of_month) + 1 == t)
            year_anomaly = TRUE;
        date.tm_year = atoi (t) - 1900;
        /* We don't know the hour.  Set it to something other than 0, or
         * DST -1 will end up changing the date. */
        date.tm_hour = 12;
        date.tm_min = 0;
        date.tm_sec = 0;
    }

    s->st_mtime = mktime (&date);
    /* Use resulting time value */
    s->st_atime = s->st_ctime = s->st_mtime;

    name = strtok_r (NULL, "", &next);
    if (name == NULL)
        return FALSE;

    /* there are ls which output extra space after year. */
    if (year_anomaly && *name == ' ')
        name++;

    if (!S_ISLNK (s->st_mode))
        *linkname = NULL;
    else
    {
        char *arrow;

        for (arrow = name; (arrow = strstr (arrow, " -> ")) != NULL; arrow++)
            if (arrow != name && arrow[4] != '\0')
            {
                *arrow = '\0';
                *linkname = g_strdup (arrow + 4);
                break;
            }
    }

    *filename = g_strdup (name);

    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
ftpfs_parse_long_list_UNIX (char *line, struct stat *s, char **filename, char **linkname, int *err)
{
    int tmp;
    gboolean ret;

    if (sscanf (line, "total %d", &tmp) == 1)
        return FALSE;

    if (strncasecmp (line, "Status of ", 10) == 0)
        return FALSE;           /* STAT output. */
    if (strchr ("bcpsD", line[0]) != NULL)      /* block, char, pipe, socket, Door. */
        return FALSE;

    ret = parse_ls_line (line, s, filename, linkname);
    if (!ret)
        (*err)++;

    return ret;
}

/* --------------------------------------------------------------------------------------------- */

/*
   07-13-98  09:06PM       <DIR>          aix
   07-13-98  09:06PM       <DIR>          hpux
   07-13-98  09:06PM       <DIR>          linux
   07-13-98  09:06PM       <DIR>          ncr
   07-13-98  09:06PM       <DIR>          solaris
   03-18-98  06:01AM              2109440 nlxb318e.tar
   07-02-98  11:17AM                13844 Whatsnew.txt
 */

static gboolean
ftpfs_parse_long_list_NT (char *line, struct stat *s, char **filename, char **linkname, int *err)
{
    char *t;
    int month, day, year, hour, minute;
    char am;
    struct tm tms;
    long long size;

    t = FIRST_TOKEN;
    if (t == NULL)
        ERR2;
    if (sscanf (t, "%2d-%2d-%2d", &month, &day, &year) != 3)
        ERR2;
    if (year >= 70)
        year += 1900;
    else
        year += 2000;

    t = NEXT_TOKEN;
    if (t == NULL)
        ERR2;
    am = 'A';                   /* AM/PM is optional */
    if (sscanf (t, "%2d:%2d%c", &hour, &minute, &am) < 2)
        ERR2;

    t = NEXT_TOKEN;
    if (t == NULL)
        ERR2;

    if (am == 'P')              /* PM - after noon */
    {
        hour += 12;
        if (hour == 24)
            hour = 0;
    }

    tms.tm_sec = 30;            /* seconds after the minute [0, 61]  */
    tms.tm_min = minute;        /* minutes after the hour [0, 59] */
    tms.tm_hour = hour;         /* hour since midnight [0, 23] */
    tms.tm_mday = day;          /* day of the month [1, 31] */
    tms.tm_mon = month - 1;     /* months since January [0, 11] */
    tms.tm_year = year - 1900;  /* years since 1900 */
    tms.tm_isdst = -1;


    s->st_mtime = mktime (&tms);
    /* Use resulting time value */
    s->st_atime = s->st_ctime = s->st_mtime;

    if (strcmp (t, "<DIR>") == 0)
        s->st_mode = S_IFDIR;
    else
    {
        s->st_mode = S_IFREG;
        if (sscanf (t, "%lld", &size) != 1)
            ERR2;
        s->st_size = (off_t) size;
    }

    t = strtok (NULL, "");
    if (t == NULL)
        ERR2;
    while (*t == ' ')
        t++;
    if (*t == '\0')
        ERR2;

    *filename = g_strdup (t);
    *linkname = NULL;

    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

/*
   +i774.71425,m951188401,/,       users
   +i774.49602,m917883130,r,s79126,        jgr_www2.exe

   starts with +
   comma separated
   first character of field is type:
   i - ?
   m - modification time
   / - means directory
   r - means plain file
   s - size
   up - permissions in octal
   \t - file name follows.
 */

static gboolean
ftpfs_parse_long_list_EPLF (char *line, struct stat *s, char **filename, char **linkname, int *err)
{
    size_t len;
    const char *b;
    const char *name = NULL;
    size_t name_len = 0;
    off_t size = NO_SIZE;
    time_t date = NO_DATE;
    long date_l;
    long long size_ll;
    gboolean dir = FALSE;
    gboolean type_known = FALSE;
    int perms = -1;
    const char *scan;
    ssize_t scan_len;

    len = strlen (line);
    b = line;

    if (len < 2 || b[0] != '+')
        ERR2;

    scan = b + 1;
    scan_len = len - 1;

    while (scan != NULL && scan_len > 0)
    {
        const char *comma;

        switch (*scan)
        {
        case '\t':             /* the rest is file name. */
            name = scan + 1;
            name_len = scan_len - 1;
            scan = NULL;
            break;
        case 's':
            if (sscanf (scan + 1, "%lld", &size_ll) != 1)
                break;
            size = size_ll;
            break;
        case 'm':
            if (sscanf (scan + 1, "%ld", &date_l) != 1)
                break;
            date = date_l;
            break;
        case '/':
            dir = TRUE;
            type_known = TRUE;
            break;
        case 'r':
            dir = FALSE;
            type_known = TRUE;
            break;
        case 'i':
            break;
        case 'u':
            if (scan[1] == 'p') /* permissions. */
                if (sscanf (scan + 2, "%o", (unsigned int *) &perms) != 1)
                    perms = -1;
            break;
        default:
            name = NULL;
            scan = NULL;
            break;
        }
        if (scan == NULL || scan_len == 0)
            break;

        comma = (const char *) memchr (scan, ',', scan_len);
        if (comma == NULL)
            break;

        scan_len -= comma + 1 - scan;
        scan = comma + 1;
    }

    if (name == NULL || !type_known)
        ERR2;

    *filename = g_strndup (name, name_len);
    *linkname = NULL;

    if (size != NO_SIZE)
        s->st_size = size;
    if (date != NO_DATE)
    {
        s->st_mtime = date;
        /* Use resulting time value */
        s->st_atime = s->st_ctime = s->st_mtime;
    }
    if (type_known)
        s->st_mode = dir ? S_IFDIR : S_IFREG;
    if (perms != -1)
        s->st_mode |= perms;    /* FIXME */

    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */
/*
   Type=cdir;Modify=20021029173810;Perm=el;Unique=BP8AAjJufAA; /
   Type=pdir;Modify=20021029173810;Perm=el;Unique=BP8AAjJufAA; ..
   Type=dir;Modify=20010118144705;Perm=e;Unique=BP8AAjNufAA; bin
   Type=dir;Modify=19981021003019;Perm=el;Unique=BP8AAlhufAA; pub
   Type=file;Size=12303;Modify=19970124132601;Perm=r;Unique=BP8AAo9ufAA; mailserv.FAQ
   modify=20161215062118;perm=flcdmpe;type=dir;UNIX.group=503;UNIX.mode=0700; directory-name
   modify=20161213121618;perm=adfrw;size=6369064;type=file;UNIX.group=503;UNIX.mode=0644; file-name
   modify=20120103123744;perm=adfrw;size=11;type=OS.unix=symlink;UNIX.group=0;UNIX.mode=0777; www
 */

static gboolean
ftpfs_parse_long_list_MLSD (char *line, struct stat *s, char **filename, char **linkname, int *err)
{
    const char *name = NULL;
    off_t size = NO_SIZE;
    time_t date = NO_DATE;
    const char *owner = NULL;
    const char *group = NULL;
    filetype type = UNKNOWN;
    int perms = -1;
    char *space;
    char *tok;

    space = strstr (line, "; ");
    if (space != NULL)
    {
        name = space + 2;
        *space = '\0';
    }
    else
    {
        /* NcFTPd does not put a semicolon after last fact, workaround it. */
        space = strchr (line, ' ');
        if (space == NULL)
            ERR2;
        name = space + 1;
        *space = '\0';
    }

    for (tok = strtok (line, ";"); tok != NULL; tok = strtok (NULL, ";"))
    {
        if (strcasecmp (tok, "Type=cdir") == 0
            || strcasecmp (tok, "Type=pdir") == 0 || strcasecmp (tok, "Type=dir") == 0)
        {
            type = DIRECTORY;
            continue;
        }
        if (strcasecmp (tok, "Type=file") == 0)
        {
            type = NORMAL;
            continue;
        }
        if (strcasecmp (tok, "Type=OS.unix=symlink") == 0)
        {
            type = SYMLINK;
            continue;
        }
        if (strncasecmp (tok, "Modify=", 7) == 0)
        {
            date = ftpfs_convert_date (tok + 7);
            continue;
        }
        if (strncasecmp (tok, "Size=", 5) == 0)
        {
            long long size_ll;

            if (sscanf (tok + 5, "%lld", &size_ll) == 1)
                size = size_ll;
            continue;
        }
        if (strncasecmp (tok, "Perm=", 5) == 0)
        {
            perms = 0;
            for (tok += 5; *tok != '\0'; tok++)
            {
                switch (g_ascii_tolower (*tok))
                {
                case 'e':
                    perms |= 0111;
                    break;
                case 'l':
                    perms |= 0444;
                    break;
                case 'r':
                    perms |= 0444;
                    break;
                case 'c':
                    perms |= 0200;
                    break;
                case 'w':
                    perms |= 0200;
                    break;
                default:
                    break;
                }
            }
            continue;
        }
        if (strncasecmp (tok, "UNIX.mode=", 10) == 0)
        {
            if (sscanf (tok + 10, "%o", (unsigned int *) &perms) != 1)
                perms = -1;
            continue;
        }
        if (strncasecmp (tok, "UNIX.owner=", 11) == 0)
        {
            owner = tok + 11;
            continue;
        }
        if (strncasecmp (tok, "UNIX.group=", 11) == 0)
        {
            group = tok + 11;
            continue;
        }
        if (strncasecmp (tok, "UNIX.uid=", 9) == 0)
        {
            if (owner == NULL)
                owner = tok + 9;
            continue;
        }
        if (strncasecmp (tok, "UNIX.gid=", 9) == 0)
        {
            if (group == NULL)
                group = tok + 9;
            continue;
        }
    }
    if (name == NULL || name[0] == '\0' || type == UNKNOWN)
        ERR2;

    *filename = g_strdup (name);
    *linkname = NULL;

    if (size != NO_SIZE)
        s->st_size = size;
    if (date != NO_DATE)
    {
        s->st_mtime = date;
        /* Use resulting time value */
        s->st_atime = s->st_ctime = s->st_mtime;
    }
    switch (type)
    {
    case DIRECTORY:
        s->st_mode = S_IFDIR;
        break;
    case SYMLINK:
        s->st_mode = S_IFLNK;
        break;
    case NORMAL:
        s->st_mode = S_IFREG;
        break;
    default:
        g_assert_not_reached ();
    }
    if (perms != -1)
        s->st_mode |= perms;    /* FIXME */
    if (owner != NULL)
        s->st_uid = ftpfs_get_uid (owner);
    if (group != NULL)
        s->st_uid = ftpfs_get_gid (group);

    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

/*
   ASUSER          8192 04/26/05 13:54:16 *DIR       dir/
   ASUSER          8192 04/26/05 13:57:34 *DIR       dir1/
   ASUSER        365255 02/28/01 15:41:40 *STMF      readme.txt
   ASUSER       8489625 03/18/03 09:37:00 *STMF      saved.zip
   ASUSER        365255 02/28/01 15:41:40 *STMF      unist.old
 */

static gboolean
ftpfs_parse_long_list_AS400 (char *line, struct stat *s, char **filename, char **linkname, int *err)
{
    char *t;
    char *user;
    long long size;
    int month, day, year, hour, minute, second;
    struct tm tms;
    time_t mtime;
    mode_t type;
    char *slash;

    t = FIRST_TOKEN;
    if (t == NULL)
        ERR2;
    user = t;

    t = NEXT_TOKEN;
    if (t == NULL)
        ERR2;
    if (sscanf (t, "%lld", &size) != 1)
        ERR2;

    t = NEXT_TOKEN;
    if (t == NULL)
        ERR2;
    if (sscanf (t, "%2d/%2d/%2d", &month, &day, &year) != 3)
        ERR2;
    if (year >= 70)
        year += 1900;
    else
        year += 2000;

    t = NEXT_TOKEN;
    if (t == NULL)
        ERR2;
    if (sscanf (t, "%2d:%2d:%2d", &hour, &minute, &second) != 3)
        ERR2;

    t = NEXT_TOKEN;
    if (t == NULL)
        ERR2;

    tms.tm_sec = second;        /* seconds after the minute [0, 61]  */
    tms.tm_min = minute;        /* minutes after the hour [0, 59] */
    tms.tm_hour = hour;         /* hour since midnight [0, 23] */
    tms.tm_mday = day;          /* day of the month [1, 31] */
    tms.tm_mon = month - 1;     /* months since January [0, 11] */
    tms.tm_year = year - 1900;  /* years since 1900 */
    tms.tm_isdst = -1;
    mtime = mktime (&tms);

    t = NEXT_TOKEN;
    if (t == NULL)
        ERR2;
    if (strcmp (t, "*DIR") == 0)
        type = S_IFDIR;
    else
        type = S_IFREG;

    t = strtok (NULL, "");
    if (t == NULL)
        ERR2;
    while (*t == ' ')
        t++;
    if (*t == '\0')
        ERR2;

    *linkname = NULL;

    slash = strchr (t, '/');
    if (slash != NULL)
    {
        if (slash == t)
            return FALSE;

        *slash = '\0';
        type = S_IFDIR;
        if (slash[1] != '\0')
        {
            *filename = g_strdup (t);
            s->st_mode = type;  /* FIXME */
            return TRUE;
        }
    }

    *filename = g_strdup (t);
    s->st_mode = type;
    s->st_size = (off_t) size;
    s->st_mtime = mtime;
    /* Use resulting time value */
    s->st_atime = s->st_ctime = s->st_mtime;
    s->st_uid = ftpfs_get_uid (user);

    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

/*
   0          DIR  06-27-96  11:57  PROTOCOL
   169               11-29-94  09:20  SYSLEVEL.MPT
 */

static gboolean
ftpfs_parse_long_list_OS2 (char *line, struct stat *s, char **filename, char **linkname, int *err)
{
    char *t;
    long long size;
    int month, day, year, hour, minute;
    struct tm tms;

    t = FIRST_TOKEN;
    if (t == NULL)
        ERR2;

    if (sscanf (t, "%lld", &size) != 1)
        ERR2;
    s->st_size = (off_t) size;

    t = NEXT_TOKEN;
    if (t == NULL)
        ERR2;
    s->st_mode = S_IFREG;
    if (strcmp (t, "DIR") == 0)
    {
        s->st_mode = S_IFDIR;
        t = NEXT_TOKEN;

        if (t == NULL)
            ERR2;
    }

    if (sscanf (t, "%2d-%2d-%2d", &month, &day, &year) != 3)
        ERR2;
    if (year >= 70)
        year += 1900;
    else
        year += 2000;

    t = NEXT_TOKEN;
    if (t == NULL)
        ERR2;
    if (sscanf (t, "%2d:%2d", &hour, &minute) != 3)
        ERR2;

    tms.tm_sec = 30;            /* seconds after the minute [0, 61]  */
    tms.tm_min = minute;        /* minutes after the hour [0, 59] */
    tms.tm_hour = hour;         /* hour since midnight [0, 23] */
    tms.tm_mday = day;          /* day of the month [1, 31] */
    tms.tm_mon = month - 1;     /* months since January [0, 11] */
    tms.tm_year = year - 1900;  /* years since 1900 */
    tms.tm_isdst = -1;
    s->st_mtime = mktime (&tms);
    /* Use resulting time value */
    s->st_atime = s->st_ctime = s->st_mtime;

    t = strtok (NULL, "");
    if (t == NULL)
        ERR2;
    while (*t == ' ')
        t++;
    if (*t == '\0')
        ERR2;
    *filename = g_strdup (t);
    *linkname = NULL;

    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
ftpfs_parse_long_list_MacWebStar (char *line, struct stat *s, char **filename,
                                  char **linkname, int *err)
{
    char *t;
    mode_t type, mode;
    struct tm date;
    const char *day_of_month;
    char *name;

    t = FIRST_TOKEN;
    if (t == NULL)
        ERR2;

    if (!vfs_parse_filetype (t, NULL, &type))
        ERR2;

    s->st_mode = type;

    if (!vfs_parse_fileperms (t + 1, NULL, &mode))
        ERR2;
    /* permissions are meaningless here. */

    /* "folder" or 0 */
    t = NEXT_TOKEN;
    if (t == NULL)
        ERR2;

    if (strcmp (t, "folder") != 0)
    {
        long long size;

        /* size? */
        t = NEXT_TOKEN;
        if (t == NULL)
            ERR2;
        /* size */
        t = NEXT_TOKEN;
        if (t == NULL)
            ERR2;
        if (!isdigit ((unsigned char) *t))
            ERR2;

        if (sscanf (t, "%lld", &size) == 1)
            s->st_size = (off_t) size;
    }
    else
    {
        /* ?? */
        t = NEXT_TOKEN;
        if (t == NULL)
            ERR2;
    }

    /* month */
    t = NEXT_TOKEN;
    if (t == NULL)
        ERR2;

    memset (&date, 0, sizeof (date));

    if (!vfs_parse_month (t, &date))
        ERR2;

    day_of_month = NEXT_TOKEN;
    if (day_of_month == NULL)
        ERR2;

    date.tm_mday = atoi (day_of_month);

    /* time or year */
    t = NEXT_TOKEN;
    if (t == NULL)
        ERR2;
    if (!parse_year_or_time (t, &date.tm_year, &date.tm_hour, &date.tm_min))
        ERR2;

    date.tm_isdst = -1;
    date.tm_sec = 30;
    if (date.tm_year == -1)
        date.tm_year = guess_year (date.tm_mon, date.tm_mday, date.tm_hour, date.tm_min) - 1900;
    else
        date.tm_hour = 12;

    s->st_mtime = mktime (&date);
    /* Use resulting time value */
    s->st_atime = s->st_ctime = s->st_mtime;

    name = strtok (NULL, "");
    if (name == NULL)
        ERR2;

    /* no symlinks on Mac, but anyway. */
    if (!S_ISLNK (s->st_mode))
        *linkname = NULL;
    else
    {
        char *arrow;

        for (arrow = name; (arrow = strstr (arrow, " -> ")) != NULL; arrow++)
            if (arrow != name && arrow[4] != '\0')
            {
                *arrow = '\0';
                *linkname = g_strdup (arrow + 4);
                break;
            }
    }

    *filename = g_strdup (name);

    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */
/*** public functions ****************************************************************************/
/* --------------------------------------------------------------------------------------------- */

GSList *
ftpfs_parse_long_list (struct vfs_class * me, struct vfs_s_inode * dir, GSList * buf, int *err_ret)
{
    int err[number_of_parsers];
    GSList *set[number_of_parsers];     /* arrays of struct vfs_s_entry */
    size_t i;
    GSList *bufp;
    ftpfs_line_parser guessed_parser = NULL;
    GSList **the_set = NULL;
    int *the_err = NULL;
    int *best_err1 = &err[0];
    int *best_err2 = &err[1];

    ftpfs_init_time ();

    if (err_ret != NULL)
        *err_ret = 0;

    memset (&err, 0, sizeof (err));
    memset (&set, 0, sizeof (set));

    for (bufp = buf; bufp != NULL; bufp = g_slist_next (bufp))
    {
        char *b = (char *) bufp->data;
        size_t blen;

        blen = strlen (b);

        if (b[blen - 1] == '\r')
        {
            b[blen - 1] = '\0';
            blen--;
        }

        if (blen == 0)
            continue;

        if (guessed_parser == NULL)
        {
            for (i = 0; i < number_of_parsers; i++)
            {
                struct vfs_s_entry *info;
                gboolean ok;
                char *tmp_line;
                int nlink;

                /* parser can clobber the line - work on a copy */
                tmp_line = g_strndup (b, blen);

                info = vfs_s_generate_entry (me, NULL, dir, 0);
                nlink = info->ino->st.st_nlink;
                ok = (*line_parsers[i]) (tmp_line, &info->ino->st, &info->name,
                                         &info->ino->linkname, &err[i]);
                if (ok && strchr (info->name, '/') == NULL)
                {
                    info->ino->st.st_nlink = nlink;     /* Ouch, we need to preserve our counts :-( */
                    set[i] = g_slist_prepend (set[i], info);
                }
                else
                    vfs_s_free_entry (me, info);

                g_free (tmp_line);

                if (*best_err1 > err[i])
                    best_err1 = &err[i];
                if (*best_err2 > err[i] && best_err1 != &err[i])
                    best_err2 = &err[i];

                if (*best_err1 > 16)
                    goto leave; /* too many errors with best parser. */
            }

            if (*best_err2 > (*best_err1 + 1) * 16)
            {
                i = (size_t) (best_err1 - err);
                guessed_parser = line_parsers[i];
                the_set = &set[i];
                the_err = &err[i];
            }
        }
        else
        {
            struct vfs_s_entry *info;
            gboolean ok;
            char *tmp_line;
            int nlink;

            /* parser can clobber the line - work on a copy */
            tmp_line = g_strndup (b, blen);

            info = vfs_s_generate_entry (me, NULL, dir, 0);
            nlink = info->ino->st.st_nlink;
            ok = guessed_parser (tmp_line, &info->ino->st, &info->name, &info->ino->linkname,
                                 the_err);
            if (ok && strchr (info->name, '/') == NULL)
            {
                info->ino->st.st_nlink = nlink; /* Ouch, we need to preserve our counts :-( */
                *the_set = g_slist_prepend (*the_set, info);
            }
            else
                vfs_s_free_entry (me, info);

            g_free (tmp_line);
        }
    }

    if (the_set == NULL)
    {
        i = best_err1 - err;
        the_set = &set[i];
        the_err = &err[i];
    }

  leave:
    for (i = 0; i < number_of_parsers; i++)
        if (&set[i] != the_set)
        {
            for (bufp = set[i]; bufp != NULL; bufp = g_slist_next (bufp))
                vfs_s_free_entry (me, VFS_ENTRY (bufp->data));

            g_slist_free (set[i]);
        }

    if (err_ret != NULL && the_err != NULL)
        *err_ret = *the_err;

    return the_set != NULL ? g_slist_reverse (*the_set) : NULL;
}

/* --------------------------------------------------------------------------------------------- */
