/*===========================================================================*
 *                                                                           *
 *  sfldir.c - Directory handling functions                                  *
 *                                                                           *
 *  Copyright (c) 1991-2010 iMatix Corporation                               *
 *                                                                           *
 *  ------------------ GPL Licensed Source Code ------------------           *
 *  iMatix makes this software available under the GNU General               *
 *  Public License (GPL) license for open source projects.  For              *
 *  details of the GPL license please see www.gnu.org or read the            *
 *  file license.gpl provided in this package.                               *
 *                                                                           *
 *  This program 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.                      *
 *                                                                           *
 *  This program 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 in the file 'license.gpl'; if            *
 *  not, see <http://www.gnu.org/licenses/>.                                 *
 *                                                                           *
 *  You can also license this software under iMatix's General Terms          *
 *  of Business (GTB) for commercial projects.  If you have not              *
 *  explicitly licensed this software under the iMatix GTB you may           *
 *  only use it under the terms of the GNU General Public License.           *
 *                                                                           *
 *  For more information, send an email to info@imatix.com.                  *
 *  --------------------------------------------------------------           *
 *===========================================================================*/

#include "prelude.h"                    /*  Universal header file            */
#include "sfldate.h"                    /*  Date handling functions          */
#include "sfluid.h"                     /*  Uid/gid functions                */
#include "sflstr.h"                     /*  String functions                 */
#include "sfllist.h"                    /*  Linked-list functions            */
#include "sflmem.h"                     /*  Memory allocation functions      */
#include "sfllist.h"                    /*  Linked-list functions            */
#include "sflfile.h"                    /*  File-access functions            */
#include "sflcons.h"                    /*  Console display functions        */
#include "sflprint.h"                   /*  snprintf functions               */
#include "sfldir.h"                     /*  Prototypes for functions         */

/*  Static variables used globally in this source file only                  */

static char
    *sort_key;

/*  Function prototypes                                                      */

static Bool   populate_entry     (DIRST *dir);
static time_t get_six_months_ago (void);
static char  *format_mode        (DIRST *dir);
static char  *format_name        (DIRST *dir, Bool full);
static char  *format_time        (DIRST *dir);
static Bool  compare_dir         (LIST *p1, LIST *p2);
static Bool  path_delimiter      (char delim);


/*  ---------------------------------------------------------------------[<]-
    Function: open_dir

    Synopsis:
    Creates a directory stream and returns the first entry in the directory.
    The order of entries is arbitrary, and it is undefined whether you will
    get entries like '.' and '..' or not.  Returns TRUE if something was
    found, else FALSE.  If TRUE, fills-in the values in the directory stream
    block.  Use the same directory stream block for the read_dir and
    close_dir() functions.

    You must supply a DIRST block when calling open_dir().  If you do not
    supply a dir_name (i.e. it is NULL or ""), open_dir() assumes you want
    to read from the current working directory.  You must call close_dir()
    after an open_dir(), even if it fails.

    The strings in DIRST all point to static areas that may change after a
    further call to read_dir.  If you need persistent data (i.e. because
    you want to collect a set of DIRSTs and then sort them, call fix_dir()
    after each call to open_dir and read_dir.  You should then call
    free_dir() to release each DIRST when you are finished.
    ---------------------------------------------------------------------[>]-*/

Bool
open_dir (
    DIRST *dir,
    const char *dir_name)
{
    char
        *dir_spec,                      /*  Directory to search through      */
        *dir_spec_end;                  /*  Points to NULL in dir_spec       */
    
    errno = 0;

    ASSERT (dir != NULL);
    if (! dir)
        return FALSE;

    memset (dir, 0, sizeof (DIRST));

    /*  Copy and prepare the directory specification                         */
    dir_spec = mem_alloc (NAME_MAX);
    if (dir_name == NULL || *dir_name == 0)
        strcpy (dir_spec, DEFAULT_DIR);
    else
        strcpy(dir_spec, clean_path(dir_name));

    /*  Save the unmodified directory name for cleanup later                 */
    dir_spec_end = dir_spec + strlen (dir_spec);

    /*  Open directory stream or find first directory entry                  */
#if (defined (__UNIX__) || defined (__VMS_XOPEN) || defined (__OS2__))
    if (strnull (dir_spec))
        strcpy (dir_spec, "/");
#   if (defined (__OS2__))
    if (dir_spec_end [-1] == ':')                /*  Special case d: to d:\  */
        strcat (dir_spec, "\\");
#   endif
    if ((dir-> _dir_handle = opendir (dir_spec)) == NULL)

#elif (defined (WIN32))
    strcat (dir_spec, "\\*");
    if ((dir-> _dir_handle = (void *)FindFirstFileA (dir_spec, &dir-> _dir_entry))
                           == INVALID_HANDLE_VALUE)

#elif (defined (_MSC_VER))
    strcat (dir_spec, "\\*.*");
    if ((dir-> _dir_handle = (void *)_dos_findfirst (dir_spec, _A_NORMAL | _A_SUBDIR,
                                             &dir-> _dir_entry)) != 0)

#elif (defined (__TURBOC__))
    strcat (dir_spec, "\\*.*");
    if (findfirst (dir_spec, &dir-> _dir_entry, 255 - FA_LABEL) == -1)
#endif
      {
        mem_free (dir_spec);
        return (FALSE);                 /*  Could not open directory         */
      }

    /*  Save the directory name in directory stream structure                */
#if (defined (__MSDOS__) || defined (__OS2__))
    *dir_spec_end = '\0';               /*  Kill the \*.* again              */
#endif

    dir-> dir_name = dir_spec;          /*  Now owned by DIRST structure     */

    // normalize the path and filename
#ifdef GATES_FILESYSTEM
    strconvch (dir-> dir_name, PATHEND, '/');
    strconvch (dir-> file_name, PATHEND, '/');
#endif

#if (defined (__UNIX__) || defined (__VMS_XOPEN) || defined (__OS2__))
    /*  Under UNIX & VMS we still need to fetch the first file entry         */
    return (read_dir (dir));

#elif (defined (WIN32) || defined (_MSC_VER) || defined (__TURBOC__))
    /*  Under Win32 we have read an entry, so return those values            */
    return (populate_entry (dir));

#else

    return (FALSE);                     /*  Directory access not supported   */
#endif
}


/*  -------------------------------------------------------------------------
 *  populate_entry -- internal
 *
 *  Sets the various public fields in the directory stream from the system-
 *  specific values in the private fields.  Returns TRUE if okay, FALSE if
 *  there was a problem getting the file status information.
 */

static Bool
populate_entry (DIRST *dir)
{

#if (defined (WIN32))
    static char
        *default_user = "user";
#else
    char
        *full_path;                    /*  Full path name of the file        */
    struct stat
        stat_buf;                      /*  File status structure             */
    int
        rc;
#endif

    ASSERT (dir != NULL);
    if (dir-> _fixed)
        free_dir (dir);                /*  De-allocate old strings if reqd   */

    /*  Get name of file from directory structure                            */
#if (defined (__UNIX__) || defined (__VMS_XOPEN) || defined (__OS2__))
    dir-> file_name = dir-> _dir_entry-> d_name;

#elif (defined (WIN32))
    dir-> file_name = dir-> _dir_entry.cFileName;

#elif (defined (_MSC_VER))
    dir-> file_name = dir-> _dir_entry.name;

#elif (defined (__TURBOC__))
    dir-> file_name = dir-> _dir_entry.ff_name;
#endif

    // normalize the filename
#ifdef GATES_FILESYSTEM
    strconvch (dir-> file_name, PATHEND, '/');
#endif

#if (defined (__UNIX__) || defined (GATES_FILESYSTEM))
    /*  If the filename is . or .., skip this entry and do the next.  We     */
    /*  use a little bit of a recursive call to make this code simple.       */

    if (dir-> file_name [0] == '.' && (dir-> file_name [1] == '\0'
    || (dir-> file_name [1] == '.' &&  dir-> file_name [2] == '\0')))
        return (read_dir (dir));
#endif

    /*  Prepare full path and get file status information.  Most systems have
     *  some kind of stat() function; we call this, even if similar info is
     *  already available in the _dir_entry structures.  Except for Win32
     *  where we use the native Win32 API to support compilers which may not
     *  have all the C wrapper functions.
     */

#if (defined (WIN32))
    /*  Get file info using Win32 calls                                      */
    {
        unsigned long thi, tlo;
        double   dthi, dtlo;
        double   secs_since_1601;
        double   delta = 11644473600.;
        double   two_to_32 = 4294967296.;

        thi = dir-> _dir_entry.ftLastWriteTime.dwHighDateTime;
        tlo = dir-> _dir_entry.ftLastWriteTime.dwLowDateTime;
        dthi = (double) thi;
        dtlo = (double) tlo;
        secs_since_1601 = (dthi * two_to_32 + dtlo) / 1.0e7;

        dir-> file_time = (unsigned long) (secs_since_1601 - delta);
        dir-> file_size = dir->_dir_entry.nFileSizeLow;
        dir-> file_mode = 0;
        if (dir-> _dir_entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
            dir-> file_mode |= S_IFDIR;
        else
            dir-> file_mode |= S_IFREG;
        if (!(dir-> _dir_entry.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN))
            dir-> file_mode |= S_IREAD;
        if (!(dir-> _dir_entry.dwFileAttributes & FILE_ATTRIBUTE_READONLY))
            dir-> file_mode |= S_IWRITE;
        dir-> file_nlink = 1;
        dir-> owner = NULL;
        dir-> group = NULL;
    }

#else
    full_path = xstrcpy (NULL, dir-> dir_name, "/", dir-> file_name, NULL);
    rc = stat (full_path , &stat_buf);
    mem_free (full_path);
    if (rc == -1)
        return (FALSE);

    dir-> file_time  = stat_buf.st_mtime;   /*  Modification time            */
    dir-> file_size  = stat_buf.st_size;    /*  Size in bytes                */
    dir-> file_mode  = stat_buf.st_mode;    /*  UNIX-ish permissions         */
    dir-> file_nlink = stat_buf.st_nlink;   /*  Number of links to file      */

    /*  Get owner and group names                                            */
    dir-> owner = get_uid_name (stat_buf.st_uid);
    dir-> group = get_gid_name (stat_buf.st_gid);
#endif

    /*  Prepare DOS-ish permission bits                                      */
#if (defined (__UNIX__) || defined (__VMS_XOPEN) || defined (__OS2__))
    dir-> file_attrs = 0;
    if ((stat_buf.st_mode & S_IREAD)  == 0)
        dir-> file_attrs |= ATTR_HIDDEN;
    if ((stat_buf.st_mode & S_IWRITE) == 0)
        dir-> file_attrs |= ATTR_RDONLY;
    if ((stat_buf.st_mode & S_IFDIR)  != 0)
        dir-> file_attrs |= ATTR_SUBDIR;

#elif (defined (WIN32))
    dir-> file_attrs = 0;
    if (dir-> _dir_entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
        dir-> file_attrs |= ATTR_SUBDIR;
    if (dir-> _dir_entry.dwFileAttributes & FILE_ATTRIBUTE_READONLY)
        dir-> file_attrs |= ATTR_RDONLY;
    if (dir-> _dir_entry.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN)
        dir-> file_attrs |= ATTR_HIDDEN;
    if (dir-> _dir_entry.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM)
        dir-> file_attrs |= ATTR_SYSTEM;
    dir-> owner = dir-> group = default_user;

#elif (defined (_MSC_VER))
    dir-> file_attrs = (byte) dir-> _dir_entry.attrib & ATTR_MASK;

#elif (defined (__TURBOC__))
    dir-> file_attrs = (byte) dir-> _dir_entry.ff_attrib & ATTR_MASK;
#endif

    return (TRUE);                      /*  No errors                        */
}


/*  ---------------------------------------------------------------------[<]-
    Function: read_dir

    Synopsis:
    Reads the next entry from the directory stream.  Returns TRUE if there
    was more data to read; returns FALSE if there was nothing more to read.
    Updates the fields in the directory structure when it returns TRUE.
    The strings in DIRST all point to static areas that may change after a
    further call to read_dir.  If you need persistent data (i.e. because
    you want to collect a set of DIRSTs and then sort them, call fix_dir()
    after each call to open_dir and read_dir.  You should then call
    free_dir() to release each DIRST when you are finished.
    ---------------------------------------------------------------------[>]-*/

Bool
read_dir (
    DIRST *dir)
{
    ASSERT (dir != NULL);
    if (! dir)
        return FALSE;

#if (defined (__UNIX__) || defined (__VMS_XOPEN) || defined (__OS2__))
    if ((dir-> _dir_entry =
        (struct Dirent *) readdir (dir-> _dir_handle)) != NULL)
        return (populate_entry (dir));
    else

#elif (defined (WIN32))
    if (FindNextFileA (dir-> _dir_handle, &dir-> _dir_entry))
        return (populate_entry (dir));
    else

#elif (defined (_MSC_VER))
    if (_dos_findnext (&dir-> _dir_entry) == 0)
        return (populate_entry (dir));
    else

#elif (defined (__TURBOC__))
    if (findnext (&dir-> _dir_entry) == 0)
        return (populate_entry (dir));
    else
#endif

    return (FALSE);
}


/*  ---------------------------------------------------------------------[<]-
    Function: close_dir

    Synopsis:
    Close the directory stream, and free any allocated memory.  You should
    call this function when you are done reading a directory, or you will
    get memory leaks.  Returns TRUE if okay, FALSE if there was an error.
    ---------------------------------------------------------------------[>]-*/

Bool
close_dir (
    DIRST *dir)
{
    Bool
        rc;

    ASSERT (dir != NULL);
    if (! dir)
        return FALSE;

    if (dir-> dir_name)
        mem_free (dir-> dir_name);      /*  Free dynamically-allocated name  */

#if (defined (__UNIX__) || defined (__VMS_XOPEN) || defined (__OS2__))
    if (dir-> _dir_handle)
        rc = (closedir (dir-> _dir_handle) == 0);
    else
        rc = TRUE;                     /*  Already closed; we're done        */

#elif (defined (WIN32))
    rc = FindClose (dir-> _dir_handle);

#elif (defined (_MSC_VER))
    rc = TRUE;                          /*  No function to close a dir       */

#elif (defined (__TURBOC__))
    rc = TRUE;                          /*  No function to close a dir       */
#else

    rc = FALSE;                         /*  Directory access not supported   */
#endif

    return (rc);
}


/*  ---------------------------------------------------------------------[<]-
    Function: format_dir

    Synopsis:
    Formats the directory entry information using the same conventions as
    the UNIX 'ls -l' command.  Returns a static buffer that contains the
    the formatted string.  If the full argument is TRUE, adds cosmetic
    hints to indicate the file type; for instance '/' if the file is a
    directory, '*' if it is executable.
    ---------------------------------------------------------------------[>]-*/

char *
format_dir (
    DIRST *dir,
    Bool full)
{
    static char
        buffer [LINE_MAX];              /*  Formatted directory entry        */

    ASSERT (dir != NULL);
    snprintf (buffer, sizeof (buffer),
                      "%s %3d %-8.8s %-8.8s %8ld %s %s",
                      format_mode (dir),
                      (int) dir-> file_nlink,
                      dir-> owner,
                      dir-> group,
                      (long) dir-> file_size,
                      format_time (dir),
                      format_name (dir, full)
           );
    return (buffer);
}


/*  -------------------------------------------------------------------------
 *  format_mode -- internal
 *
 *  Returns a static string holding the ASCII representation of the UNIX-ish
 *  permission bits for the directory entry specified.
 */

static char *
format_mode (
    DIRST *dir)
{
    static char
        buffer [11],                    /*  duuugggooo + null                */
        *bufptr;                        /*  Pointer into buffer              */
    dbyte
        mode = dir-> file_mode;         /*  File mode bits                   */
    int
        field;                          /*  User, group, other               */

    bufptr = buffer;
    *bufptr++ = ((mode & S_IFDIR) ? 'd' : '-');
    for (field = 0; field < 3; field++)
      {
        *bufptr++ = ((mode & (S_IREAD  >> (field * 3))) ? 'r' : '-');
        *bufptr++ = ((mode & (S_IWRITE >> (field * 3))) ? 'w' : '-');
        *bufptr   = ((mode & (S_IEXEC  >> (field * 3))) ? 'x' : '-');

#if (defined (S_ISUID))
        if (field == 0 && (mode & S_ISUID))
            *bufptr = (*bufptr == 'x' ? 's' : 'S');
        else
        if (field == 1 && (mode & S_ISGID))
            *bufptr = (*bufptr == 'x' ? 's' : 'S');
#   if (defined (S_ISVTX))
        else
        if (field == 2 && (mode & S_ISVTX))
            *bufptr = (*bufptr == 'x' ? 't' : 'T');
#   endif
#endif
        ++bufptr;
    }
    *bufptr = '\0';
    return (buffer);
}


/*  -------------------------------------------------------------------------
 *  format_name -- internal
 *
 *  Returns a static string holding the cleaned-up filename for the directory
 *  entry specified.  Replaces any control characters by an octal string
 *  and appends '/' if the entry is a directory, or '*' if it is executable,
 *  if the full argument is TRUE.
 */

static char *
format_name (DIRST *dir, Bool full)
{
    static char
        buffer [NAME_MAX + 1],
        *fromptr,
        *bufptr;

    ASSERT (dir);

    bufptr = buffer;
    for (fromptr = dir-> file_name; *fromptr; bufptr++, fromptr++)
      {
        *bufptr = *fromptr;
        if ((byte) *bufptr < ' ')
          {
            sprintf (bufptr, "\\%03o", (byte) *fromptr);
            bufptr += strlen (bufptr) - 1;
          }
      }
    if (full)
      {
        if (dir-> file_mode & S_IFDIR)
           *bufptr++ = '/';
        else
        if (dir-> file_mode & S_IEXEC)
           *bufptr++ = '*';
      }
    *bufptr = '\0';
    return (buffer);
}


/*  -------------------------------------------------------------------------
 *  format_time -- internal
 *
 *  Returns a static string holding the ASCII representation of the date and
 *  time for the specified directory entry.  The format of the date and time
 *  depends on whether the file is older than 6 months, roughly:
 *
 *      Oct  9  1995        if older than 6 months
 *      Jan 12 09:55        if not older than 6 months
 */

static char *
format_time (DIRST *dir)
{
    static char
        buffer [13],                    /*  Returned string                  */
        *months = "Jan\0Feb\0Mar\0Apr\0May\0Jun\0Jul\0Aug\0Sep\0Oct\0Nov\0Dec";
    static time_t
        six_months_ago = 0;
    struct tm
        *file_time;

    ASSERT (dir);

    if (six_months_ago == 0)
        six_months_ago = get_six_months_ago ();

    file_time = safe_localtime (&dir-> file_time);
    strncpy (buffer, months + file_time-> tm_mon * 4, 4);

    if (dir-> file_time < six_months_ago
    ||  dir-> file_time > time (NULL))
        snprintf (buffer + 3, sizeof (buffer) - 3,
                              " %2d  %4d", file_time-> tm_mday,
                                           file_time-> tm_year + 1900);
    else
        snprintf (buffer + 3, sizeof (buffer) - 3,
                              " %2d %02d:%02d", file_time-> tm_mday,
                                                file_time-> tm_hour,
                                                file_time-> tm_min);
    return (buffer);
}


/*  -------------------------------------------------------------------------
 *  get_six_months_ago -- internal
 *
 *  Returns system clock 6 months ago.
 */

static time_t
get_six_months_ago ()
{
    int
        count,
        year,
        month;
    long
        days;
    time_t
        time_val;
    struct tm
        *time_now;

    time_val = time (NULL);
    time_now = safe_localtime (&time_val);
    month    = time_now-> tm_mon + 1;
    year     = time_now-> tm_year;

    days = 0;
    for (count = 0; count < 6; count++)
      {
        if (--month < 1)                /*  Start with last month            */
          {
            month = 12;
            year--;
          }
        switch (month)
          {
            case 2:                     /*  Feb                              */
                days += leap_year (year)? 29: 28;
                break;
            case 1:                     /*  Jan                              */
            case 3:                     /*  Mar                              */
            case 5:                     /*  May                              */
            case 7:                     /*  Jul                              */
            case 8:                     /*  Aug                              */
            case 10:                    /*  Oct                              */
            case 12:                    /*  Dec                              */
                days++;
            case 4:                     /*  Apr                              */
            case 6:                     /*  Jun                              */
            case 9:                     /*  Sep                              */
            case 11:                    /*  Nov                              */
                days += 30;
        }
    }
    /*  60 * 60 * 24 = 86400 sec / day                                       */
    return (time (NULL) - (long) (86400L * days));
}


/*  ---------------------------------------------------------------------[<]-
    Function: fix_dir

    Synopsis:
    Converts all strings in the DIRST into permenant values, by allocating
    heap memory for each string.  You must call this function if you intend
    to keep a set of DIRSTs, for searching or sorting.  You do not need to
    call fix_dir() if you handle each call to read_dir() independently.
    If you use fix_dir(), you must call free_dir() for each DIRST when you
    terminate.  Returns 0 if okay, -1 if there was insufficient memory or
    another fatal error.
    ---------------------------------------------------------------------[>]-*/

int
fix_dir (DIRST *dir)
{
    char
        *owner,
        *group,
        *file_name;

    /*  Allocate each string                                                 */
    owner     = mem_strdup (dir-> owner);
    group     = mem_strdup (dir-> group);
    file_name = mem_strdup (dir-> file_name);

    /*  If all okay, assign new strings and indicate everything okay         */
    if (owner && group && file_name)
      {
        dir-> owner     = owner;
        dir-> group     = group;
        dir-> file_name = file_name;
        dir-> _fixed    = TRUE;
        return (0);
      }
    else
      {
        /*  Otherwise patch things back the way they were                    */
        if (owner)
            mem_free (owner);
        if (group)
            mem_free (group);
        if (file_name)
            mem_free (file_name);
        return (-1);
      }
}


/*  ---------------------------------------------------------------------[<]-
    Function: free_dir

    Synopsis:
    Frees all strings used in the DIRST.  You should call this function to
    free space allocated by fix_dir().  If you try to call free_dir() for a
    DIRST that was not fixed, you will get an error feedback, and (if you
    compiled with DEBUG defined) an assertion fault.  Returns 0 if okay,
    -1 if there was an error.  After a call to free_dir(), do not try to
    access the strings in DIRST; they are all set to point to an empty
    string.
    ---------------------------------------------------------------------[>]-*/

int
free_dir (DIRST *dir)
{
    static char
        *empty = "";

    ASSERT (dir);
    ASSERT (dir-> _fixed);

    if (dir-> _fixed)
      {
        /*  Free allocated strings                                           */
        mem_free (dir-> owner);
        mem_free (dir-> group);
        mem_free (dir-> file_name);

        /*  Now point the strings to an empty string                         */
        dir-> owner     = empty;
        dir-> group     = empty;
        dir-> file_name = empty;

        /*  And mark the DIRST as no longer 'fixed'                          */
        dir-> _fixed    = FALSE;
        return (0);
      }
    else
        return (-1);
}


/*  ---------------------------------------------------------------------[<]-
    Function: load_dir_list

    Synopsis:
    Loads and sorts the contents of a directory.  Returns a LIST pointer to
    a linked list containing the directory entries.  Each LIST is a FILEINFO
    structure (mapped onto a LIST structure for purposes of manipulating the
    linked list).  You can ask for the directory list to be sorted in various
    ways; in this case subdirectory entries are always sorted first.  To
    specify the sort order you pass a string consisting of one or more of
    these characters, which are then used in order:
    <TABLE>
    n      Sort by ascending name.
    N      Sort by descending name.
    x      Sort by ascending extension.
    X      Sort by descending extension.
    t      Sort by ascending time and date.
    T      Sort by descending time and date.
    s      Sort by ascending size.
    S      Sort by descending size.
    </TABLE>
    If the sort string is NULL, no sort is carried out.
    ---------------------------------------------------------------------[>]-*/

LIST *
load_dir_list (
    const char *dir_name,
    const char *sort)
{
    LIST
        *file_list;                     /*  File list head                   */
    DIRST
        dir;
    Bool
        rc;
    int
        nbr_files = 0;

    file_list = mem_alloc (sizeof (LIST));
    if (!file_list)
        return (NULL);

    list_reset (file_list);             /*  Initialise file list             */

    /* Load directory                                                        */
    rc = open_dir (&dir, dir_name);
    while (rc)
      {
        add_dir_list (file_list, &dir);
        nbr_files++;
        rc = read_dir (&dir);
      }
    close_dir (&dir);

    if (sort && nbr_files > 1)
        sort_dir_list (file_list, sort);
    return (file_list);
}


/*  ---------------------------------------------------------------------[<]-
    Function: free_dir_list

    Synopsis:
    Frees all FILELIST blocks in the specified linked list.
    ---------------------------------------------------------------------[>]-*/

void
free_dir_list (LIST *file_list)
{
    LIST
        *node;

    ASSERT (file_list);
    for (node = file_list-> next;  node != file_list; node = node-> next) {
        free_dir (&((FILEINFO *) node)-> dir);
    }
    list_destroy (file_list);
    mem_free (file_list);
}


/*  ---------------------------------------------------------------------[<]-
    Function: sort_dir_list

    Synopsis: Sorts the directory list as specified.  Returns the number of
    items in the list.  To specify the sort order you pass a string holding
    one or more of these characters, which are then used in order:
    <TABLE>
    n      Sort by ascending name.
    N      Sort by descending name.
    x      Sort by ascending extension.
    X      Sort by descending extension.
    t      Sort by ascending time and date.
    T      Sort by descending time and date.
    s      Sort by ascending size.
    S      Sort by descending size.
    </TABLE>
    ---------------------------------------------------------------------[>]-*/

void
sort_dir_list (LIST *file_list, const char *sort)
{
    ASSERT (file_list);
    sort_key = (char *) sort;
    list_sort (file_list, compare_dir);
}


/*  ---------------------------------------------------------------------[<]-
    Function: add_dir_list

    Synopsis: Adds a node to the specified directory list.  Returns the
    address of the new node, or NULL if there was insufficient memory.
    ---------------------------------------------------------------------[>]-*/

FILEINFO *
add_dir_list (LIST *file_list, const DIRST *dir)
{
    FILEINFO
        *file_info;

    list_create (file_info, sizeof (FILEINFO));
    if (file_info)
      {
        list_relink_after (file_list-> prev, file_info);
        memcpy  (&file_info-> dir, dir, sizeof (DIRST));
        fix_dir (&file_info-> dir);
        file_info-> directory = (dir-> file_attrs & ATTR_SUBDIR) != 0;
      }
    return (file_info);
}


/*  -------------------------------------------------------------------------
 *  compare_dir -- internal
 *
 *  Compare two file info structures and return TRUE if SWAP is needed.
 *  Uses the global variable sort_key to determine sorting method.
 */

static Bool
compare_dir (LIST *p1, LIST *p2)
{
    Bool
        end  = FALSE,
        swap = FALSE;
    char
        *ext1,
        *ext2,
        *p;
    int
        comp;
    long
        date1,
        date2,
        time1,
        time2;

    ASSERT (p1);
    ASSERT (p2);

    if (((FILEINFO *) p1)-> directory != ((FILEINFO *) p2)-> directory)
        return (((FILEINFO *) p2)-> directory);

    for (p = sort_key;
            *p   != '\0'
         && end  == FALSE
         && swap == FALSE;
         p++)
      {
        switch (*p)
          {
            case 'n':
            case 'N':
                comp = lexcmp (((FILEINFO *) p1)-> dir.file_name,
                               ((FILEINFO *) p2)-> dir.file_name);
                if ((*p == 'n' && comp > 0)
                ||  (*p == 'N' && comp < 0))
                    swap = TRUE;
                else
                if (comp != 0)
                    end = TRUE;
                break;
            case 't':
            case 'T':
                /* Use date and time and not time_t for compare to minute,
                   not second                                                */
                date1 = timer_to_date (((FILEINFO *) p1)-> dir.file_time);
                date2 = timer_to_date (((FILEINFO *) p2)-> dir.file_time);
                if ((*p == 't' && date1 > date2)
                ||  (*p == 'T' && date1 < date2))
                    swap = TRUE;
                else
                if (date1 != date2)
                    end = TRUE;
                else
                  {
                    time1 = timer_to_time
                                (((FILEINFO *) p1)-> dir.file_time) / 10000;
                    time2 = timer_to_time
                                (((FILEINFO *) p2)-> dir.file_time) / 10000;
                    if ((*p == 't' && time1 > time2)
                    ||  (*p == 'T' && time1 < time2))
                        swap = TRUE;
                    else
                    if (time1 != time2)
                        end = TRUE;
                  }
                break;
            case 's':
            case 'S':
                if ((*p == 's' && ((FILEINFO *) p1)-> dir.file_size >
                                  ((FILEINFO *) p2)-> dir.file_size)
                ||  (*p == 'S' && ((FILEINFO *) p1)-> dir.file_size <
                                  ((FILEINFO *) p2)-> dir.file_size))
                    swap = TRUE;
                else
                if (((FILEINFO *) p1)-> dir.file_size
                !=  ((FILEINFO *) p2)-> dir.file_size)
                    end = TRUE;
                break;
            case 'x':
            case 'X':
                ext1 = strchr (((FILEINFO *) p1)-> dir.file_name, '.');
                ext2 = strchr (((FILEINFO *) p2)-> dir.file_name, '.');
                if (ext1 && ext2)
                  {
                    comp = lexcmp (ext1, ext2);
                    if ((*p == 'x' && comp > 0)
                    ||  (*p == 'X' && comp < 0))
                        swap = TRUE;
                    else
                    if (comp != 0)
                        end = TRUE;
                  }
                break;

          }
      }
    return (swap);
}


/*  ---------------------------------------------------------------------[<]-
    Function: resolve_path

    Synopsis: Accepts a path consisting of zero or more directory names and
    optionally a filename plus extension. Removes '.' and '..' if they occur
    in the path. '..' is resolved by also removing the preceding directory
    name, if any.  Returns a freshly-allocated string containing the
    resulting path.  The caller must free this string using mem_free() when
    finished with it.  The returned path may be empty.  Under OS/2 and DOS,
    treats '\' and '/' both as directory separators. A '..' directory at the
    start of the path resolves into nothing. If the input path started with
    '/', the returned path also does, else it does not.  For compatibility
    with DOS-based systems, '...' is treated as '../..', '....' as
    '../../..', and so on.
    ---------------------------------------------------------------------[>]-*/

char *
resolve_path (
    const char *old_path)
{
#if (defined (__UNIX__) || defined (GATES_FILESYSTEM) || defined (__VMS__))
    char
        *new_path,                      /*  Newly-allocated path             */
        *new_ptr,                       /*  Pointer into new_path            */
        last_char = '/';                /*  Start of path counts as delim    */
    int
        nbr_dots;                       /*  Size of '..', '...' specifier    */

    ASSERT (old_path);

    new_path = mem_strdup (old_path);
    new_ptr  = new_path;
    while (*old_path)
      {
        if (path_delimiter (last_char) && *old_path == '.')
          {
            /*  Handle one or more dots followed by a path delimiter         */
            nbr_dots = 0;               /*  Count number of dots             */
            while (old_path [nbr_dots] == '.')
                nbr_dots++;

            if (path_delimiter (old_path [nbr_dots]))
              {
                old_path += nbr_dots;   /*  Skip past dots                   */
                if (*old_path)
                    old_path++;         /*    and past / if any              */

                /*  Now backtrack in new path, dropping directories as       */
                /*  many times as needed (0 or more times)                   */
                while (nbr_dots > 1)
                  {
                    if (new_ptr > new_path + 1)
                      {
                        new_ptr--;      /*  Drop delimiter                   */
                        while (new_ptr > new_path)
                          {
                            if (path_delimiter (*(new_ptr - 1)))
                                break;  /*    and end after delimiter        */
                            new_ptr--;
                          }
                      }
                    else
                        break;          /*  At start of name - finish        */
                    nbr_dots--;
                  }
              }
            else
                /*  Handle '.something'                                      */
                last_char = *new_ptr++ = *old_path++;
          }
        else
            last_char = *new_ptr++ = *old_path++;
      }

    *new_ptr = '\0';                    /*  Terminate string nicely          */
    return (new_path);
#else

    return (mem_strdup (old_path));     /*  Path resolution not supported    */
#endif
}

static Bool
path_delimiter (char delim)
{
#if (defined (GATES_FILESYSTEM))
    if (delim == '\\' || delim == '/' || delim == '\0')
        return (TRUE);
    else
#elif (defined (__UNIX__) || defined (__VMS__))
    if (delim == '/' || delim == '\0')
        return (TRUE);
    else
#endif
        return (FALSE);
}


/*  ---------------------------------------------------------------------[<]-
    Function: locate_path

    Synopsis: Accepts a root directory and a path and locates the path with
    respect to the root.  If the path looks like an absolute directory,
    returns the path after cleaning it up.  Otherwise appends the path to
    the root, and returns the result.  In any case, the resulting directory
    does not need to exist.  Cleans-up the returned path by appending a '/'
    if necessary, and resolving any '..' subpaths.  The returned value is
    held in a freshly-allocated string which the caller must free using
    mem_free() when finished with..
    ---------------------------------------------------------------------[>]-*/

char *
locate_path (
    const char *root,
    const char *path)
{
#if (defined (__UNIX__) || defined (GATES_FILESYSTEM) || defined (__VMS__))
    char
        *new_path,                      /*  Returned path value              */
        *resolved;                      /*  and after .. resolution          */

    ASSERT (root);
    ASSERT (path);

#if (defined (GATES_FILESYSTEM))
    /*  Under MSDOS, OS/2, or Windows we have a full path if we have any of:
     *  /directory
     *  D:/directory
     *  the variations of those with backslashes.
     */
    if (path [0] == '\\'   || path [0] == '/'
    || (isalpha (path [0]) && path [1] == ':'
    && (path [2] == '\\'   || path [2] == '/')))

#else
    /*  Under UNIX or VMS we have a full path if the path starts with the
     *  directory marker.
     */
    if (path [0] == PATHEND)
#endif
        new_path = mem_strdup (path);   /*  Use path as supplied             */
    else
      {
        xstrcpy_debug ();
        if (path_delimiter (strlast (root)))
            new_path = xstrcpy (NULL, root, path, NULL);
        else
            new_path = xstrcpy (NULL, root, "/", path, NULL);
      }
    resolved = resolve_path (new_path);
    mem_free (new_path);
    /*  Append slash if necessary                                            */
    if (!path_delimiter (strlast (resolved)))
      {
        new_path = resolved;
        xstrcpy_debug ();
        resolved = xstrcpy (NULL, new_path, "/", NULL);
        mem_free (new_path);
      }
    return (resolved);
#else

    return (mem_strdup (path));         /*  Unknown system                   */
#endif
}


/*  ---------------------------------------------------------------------[<]-
    Function: clean_path

    Synopsis: Returns a clean directory name; i.e. resolves the path,
    removes a trailing slash unless the name consists just of '/'; on a
    MS-DOS file system, cleans-up a directory name consisting of a disk
    specifier.  The cleaned-up directory path is in a static area that is
    overwritten by each call.
    ---------------------------------------------------------------------[>]-*/

char *
clean_path (
    const char *path)
{
#if (defined (__UNIX__) || defined (GATES_FILESYSTEM) || defined (__VMS__))
    static char
        new_path [PATH_MAX + 1];        /*  Returned path value              */
    char
        *slash;

    strncpy (new_path, path, PATH_MAX);
    new_path [PATH_MAX] = '\0';
#   if (defined (GATES_FILESYSTEM))
    /*  For DOS filesystems, use only back slashes                           */
    strconvch (new_path, '/', '\\');
#   endif
    slash = strrchr (new_path, PATHEND);    /*  Find last slash              */

#   if (defined (GATES_FILESYSTEM))
    /*  If slash is last character in string, maybe squash it                */
    if (slash && slash [1] == '\0')
      {
        if (slash > new_path && slash [-1] != ':')
            *slash = '\0';
      }
    else                                /*  Turn X: into X:\                 */
    if (new_path [1] == ':' && new_path [2] == '\0')
      {
        new_path [2] = '\\';
        new_path [3] = '\0';
      }
#   else
    /*  If slash is last character in string, maybe squash it                */
    if (slash && slash [1] == '\0')
        if (slash > new_path)
            *slash = '\0';
#   endif
    return (new_path);
#else
    return ((char *) path);
#endif
}


/*  ---------------------------------------------------------------------[<]-
    Function: get_curdir

    Synopsis: Returns a buffer containing the current working directory.
    This buffer is allocated using the mem_alloc() function and should be
    freed using mem_free() when no longer needed.  Returns NULL if there
    was insufficient memory to allocate the buffer, or if the system does
    not provide the current working directory information.  Under Windows,
    replaces backslash characters by the UNIX-like slash.  Under OpenVMS,
    returns directory name in POSIX format.  Note that the directory name
    always ends in a slash (e.g. 'C:/' or 'C:/subdir/').
    ---------------------------------------------------------------------[>]-*/

char *
get_curdir (void)
{
    char
        *curdir;                        /*  String we get from the OS        */

    curdir = mem_alloc (PATH_MAX + 1);

#if (defined (__UNIX__) || defined (__OS2__))
    ASSERT (getcwd (curdir, PATH_MAX));

#elif (defined (__VMS__))
    getcwd (curdir, PATH_MAX, 0);

#elif (defined (WIN32))
    GetCurrentDirectoryA (PATH_MAX, curdir);
    strconvch (curdir, '\\', '/');

#elif (defined (GATES_FILESYSTEM))
    getcwd (curdir, PATH_MAX);
    strconvch (curdir, '\\', '/');

#else
    strclr (curdir);
#endif

    /*  The directory must always end in a slash                             */
    if (strlast (curdir) != '/')
        strcat (curdir, "/");

    return (curdir);
}


/*  ---------------------------------------------------------------------[<]-
    Function: set_curdir

    Synopsis: Sets the current working directory as specified.  Returns 0
    if the directory path was found; -1 if there was an error.  Under
    Windows, replaces '/' by '\' before changing directory, and switches
    to the specified disk if the path starts with a letter and ':'.  Does
    nothing if the path is NULL or empty.
    ---------------------------------------------------------------------[>]-*/

int
set_curdir (
    const char *path)
{
    int
        feedback = 0;

#if (defined (__UNIX__) || defined (__VMS_XOPEN) || defined (__OS2__))
    if (path && *path)
        feedback = chdir (path);

#elif (defined (GATES_FILESYSTEM))
    char
        *copy_path = mem_strdup (path);

    if (path == NULL || *path == '\0')
        return (0);                     /*  Do nothing if path is empty      */

    /*  MS-DOS compilers generally require a two-step process                */
    strconvch (copy_path, '/', '\\');

#   if (defined (WIN32))
    /* The drive letter does not need to be changed separately in Win32.    */
    feedback = !SetCurrentDirectoryA (copy_path);

#   elif (defined (__TURBOC__))
    feedback = chdir (copy_path);
    if (feedback == 0 && isalpha (path [0]) && path [1] == ':')
        setdisk (toupper (path [0]) - 'A');

#   elif (defined (_MSC_VER))
    feedback = chdir (copy_path);
    if (feedback == 0 && isalpha (path [0]) && path [1] == ':')
        _chdrive (toupper (path [0]) - 'A' + 1);

#   endif
    mem_strfree (&copy_path);
#else
    feedback = -1;
#endif

    return (feedback);
}


/*  ---------------------------------------------------------------------[<]-
    Function: file_matches

    Synopsis: Returns TRUE if the filename matches the pattern.  The pattern
    is a character string that can contain these 'wildcard' characters:
    ---------------------------------------------------------------------[>]-*/

Bool
file_matches (
    const char *filename,
    const char *pattern)
{
    char
        *pattern_ptr,                   /*  Points to pattern                */
        *filename_ptr;                  /*  Points to filename               */

    filename_ptr = (char *) filename;   /*  Start comparing file name        */
    pattern_ptr  = (char *) pattern;    /*  Start comparing file name        */
    FOREVER
      {
        /*  If we came to the end of the pattern and the filename, we have   */
        /*  successful match.                                                */
        if (*pattern_ptr == '\0' && *filename_ptr == '\0')
            return (TRUE);              /*  Have a match                     */

        /*  Otherwise, end of either is a failed match                       */
        if (*pattern_ptr == '\0' || *filename_ptr == '\0')
            return (FALSE);             /*  Match failed                     */

        /*  If the pattern character is '?', then we matched a char          */
        if (*pattern_ptr == '?'
#if (defined (NAMEFOLD))
        ||  toupper (*pattern_ptr) == toupper (*filename_ptr))
#else
        ||  *pattern_ptr == *filename_ptr)
#endif
          {
            pattern_ptr++;
            filename_ptr++;
          }
        else
        /*  If we have a '*', match as much of the filename as we can        */
        if (*pattern_ptr == '*')
          {
            pattern_ptr++;              /*  Try to match following char      */
            while (*filename_ptr && *filename_ptr != *pattern_ptr)
                filename_ptr++;
          }
        else
            return (FALSE);             /*  Match failed                     */
      }
}


/*  ---------------------------------------------------------------------[<]-
    Function: make_dir

    Synopsis: Create a new directory.  Returns 0 if the directory was created;
    -1 if there was an error.  Under Windows and OpenVMS, accepts directory
    names with '/'.  Will create multiple levels of directory if required.
    ---------------------------------------------------------------------[>]-*/

int
make_dir (
    const char *path_to_create)
{
    char
        *path,
        *slash;
    int
        rc = 0;

    path = mem_strdup (path_to_create); /*  Working copy                     */
#if (defined (GATES_FILESYSTEM))
    strconvch (path, '/', '\\');

    /*  Handle \\system\drive specially                                      */
    if (strprefixed (path, "\\\\"))     /*  Network drive name?              */
      {
        slash = strchr (path + 2, '\\');
        if (slash)
            slash = strchr (slash + 1, '\\');
      }
    else
#endif
    slash = strchr (path + 1, PATHEND);

    /*  Create each component of directory as required                       */
    FOREVER                             /*  Create any parent directories    */
      {
        if (slash)
            *slash = '\0';              /*  Cut at slash                     */

        if (!file_is_directory (path))
          {
#if (defined (__UNIX__) || defined (__VMS_XOPEN) || defined (__OS2__))
            rc = mkdir (path, 0775);    /*  User RWE Group RWE World RE      */

#elif (defined (WIN32))
            if (CreateDirectoryA (path, NULL))
                rc = 0;
            else
                rc = -1;

#elif (defined (GATES_FILESYSTEM))
            rc = mkdir (path);          /*  Protection?  What's that?        */
#else
            rc = -1;                    /*  Not a known system               */
#endif
            if (rc)                     /*  End if error                     */
                break;
          }
        if (slash == NULL)              /*  End if last directory            */
            break;
       *slash = PATHEND;                /*  Restore path name                */
        slash = strchr (slash + 1, PATHEND);
      }
    mem_strfree (&path);
    return (rc);
}


/*  ---------------------------------------------------------------------[<]-
    Function: remove_dir

    Synopsis: remove a directory.  Returns 0 if the directory could be
    removed; -1 if there was an error.  Under MS-DOS and OpenVMS accepts
    a directory name in UNIX format, i.e. containing '/' delimiters.  The
    directory must be empty to be removed.
    ---------------------------------------------------------------------[>]-*/

int
remove_dir (
    const char *path)
{
#if (defined (__UNIX__) || defined (__VMS_XOPEN) || defined (__OS2__))
    /*  Check that directory exists                                          */
    if (!file_is_directory (path))
        return (-1);

    return (rmdir (path));

#elif (defined (GATES_FILESYSTEM))
    int
        rc = 0;
    char
        *copy_path;

    /*  Check that directory exists                                          */
    if (!file_is_directory (path))
        return (-1);

    copy_path = mem_strdup (clean_path (path));
    if (copy_path)
      {
#   if (defined (WIN32))
        if (RemoveDirectoryA (copy_path))
            rc = 0;
        else
            rc = -1;
#   else
        rc = rmdir (copy_path);
#   endif
        mem_strfree (&copy_path);
      }
    return (rc);
#else
    return (-1);
#endif
}


/*  ---------------------------------------------------------------------[<]-
    Function: dir_usage

    Synopsis: Calculates the amount of disk space used by a directory, and
    optionally all directories below that.  If the total size is greater
    than 4Gb, returns an unspecified value.  Returns 0 if there was an error.
    ---------------------------------------------------------------------[>]-*/

qbyte
dir_usage (const char *path, Bool recurse)
{
    DIRST
        dir;
    qbyte
        usage = 0;
    char
        *full_dir;

    if (open_dir (&dir, path))
    do
      {
        if ((dir.file_attrs & ATTR_HIDDEN) != 0)
            ;   /*  Do nothing                                               */
        else
        if (recurse
        && (dir.file_attrs & ATTR_SUBDIR) != 0)
          {
            full_dir = locate_path (path, dir.file_name);
            usage += dir_usage (full_dir, TRUE);
            mem_free (full_dir);
          }
        else
            usage += dir.file_size;
      }
    while (read_dir (&dir));
    close_dir (&dir);
    return (usage);
}


/*  ---------------------------------------------------------------------[<]-
    Function: dir_files

    Synopsis: Calculates the number of files in a directory and optionally
    all directories below that.  Excludes directories from the count (thus,
    a directory containing only '.' and '..' contains 0 files.  Returns 0
    if there was an error.  Excludes hidden files.
    ---------------------------------------------------------------------[>]-*/

qbyte
dir_files (const char *path, Bool recurse)
{
    DIRST
        dir;
    qbyte
        files = 0;
    char
        *full_dir;

    if (open_dir (&dir, path))
    do
      {
        if ((dir.file_attrs & ATTR_HIDDEN) != 0)
            ;   /*  Do nothing                                               */
        else
        if (recurse
        && (dir.file_attrs & ATTR_SUBDIR) != 0)
          {
            full_dir = locate_path (path, dir.file_name);
            files += dir_files (full_dir, TRUE);
            mem_free (full_dir);
          }
        else
            files++;
      }
    while (read_dir (&dir));
    close_dir (&dir);
    return (files);
}

