/*************************************************************************
* findfile.c - find file with wild cards                     by Bob Alston
*                                                               12-26-91
* Use this program freely.  No warranty is intended or implied.
*
* Functions:   findfile()     find files and build list (gets list buffer)
*              findfget()     get files one at a time
*                             list buffer is released with free()
*
*     For AMIGA and MSDOS.
*
*     The two functions are used to find all files matching the
*     specified pathname.  The pathname may have AmigaDOS patterns
*     (wildcards for MSDOS) in both the filename and the directory names
*     (in the style of the unix shell).  The resulting list of pathnames
*     is optionally sorted alphabetically.
* 
* Example use (showing AMIGA patterns):
*
*     #include <stdio.h>
*     #define LEN    129
*     void   *buff;
*     char    filename[LEN];
*     int     count;
*     FILE   *fp;
*                                         / * build sorted list * /
*     buff = findfile("projects/#?/src/#?.c", LEN, 1, stdout, "FIND");
*     while (findfget(filename, buff)) {  / * get all files in list * /
*        fp = fopen(filename, "r");
*        ...
*     }
*     if (buff != NULL)
*        free(buff);
*
* Note:  The "#?" for the Amiga is like * for MSDOS.  Actually the #? for
*        the Amiga is identical to the * wildcard for unix where the period
*        has no special meaning.
*
*
* Conditional Compile Parameters
*
*   This program has been compiled and tested on the following systems
*   where these parameters are automatically defined.
*
*     AMIGA    (using Lattice, Inc. 5.02 compiler)
*     MSDOS    (using Microsoft, Inc. 6.0a compiler, older should also work)
*
*************************************************************************/
/*eject*/
#if AMIGA
#include <exec/types.h>                /* predede for NULL redefinition err */
#endif

#include <stdio.h>                     /* get headers */
#include <string.h>
#include <stdlib.h>
#include <dos.h>
#include "findfile.h"                  /* get prototypes */


/*************************************************************************
* the following definitions allow one set of code for both compilers
*************************************************************************/

#if AMIGA
#define FSEP      '/'                  /* dir/filename separator */
#define FILENAME  info->fib_FileName   /* filename in dos struct */
#define DIRTYPE   (info->fib_DirEntryType > 0)  /* type is dir */
#define DIRATTR   1                    /* attribute for finding dirs */
#define WILDCARDS "()?%#|"             /* AmigaDOS pattern chars */

#elif MSDOS

#define FSEP      '\\'                 /* dir/filename separator */
#define FILENAME  info->name           /* filename in dos struct */
#define DIRTYPE   (info->attrib & _A_SUBDIR)  /* type is dir */
                                       /* attribute for finding dirs */
                                       /* (like MSDOS manual says to do it) */
#define DIRATTR   (_A_HIDDEN | _A_SYSTEM | _A_SUBDIR)
#define WILDCARDS "*?"                 /* MSDOS wildcard chars */
#define FILEINFO  find_t               /* the Microsoft file info structure */

/* define the Lattice fuctions as macros to call Microsoft functions */
#define dfind(i,n,a)    _dos_findfirst(n,a,i)
#define dnext(i)        _dos_findnext(i)
#endif

/*eject*/
/*************************************************************************
* the list buffer struct
*     The list buffer which is returned to the caller, is used in these
*     programs as the following structure. It holds the list of file names
*     and other data used by the program that could be global data if the
*     program was restricted to one use at a time.  
*************************************************************************/

#define PATH_STR_LEN 25000             /* 25K buffer for path name stings */
#define FN_STR_LEN   12000             /* 12K buffer for file name stings */
#define FN_PTR_LEN   500               /* allow 500 path names */

/* buffer returned to user with complete list of path names */
struct buffer {
   char   str[PATH_STR_LEN];           /* path name string buffer */
   short  ix;                          /* index to file name string */
   char  *list[FN_PTR_LEN];            /* path name list of pointers */
   short  tot;                         /* total number of path names found */
   short  cur;                         /* index of current path name */
   short  max;                         /* max length of file pathnames */
   short  errtype;                     /* type of error that occured or 0 */
};

/* values for errtype in struct buffer (above) */
#define ERR_OUT_OF_MEM     1
#define ERR_CANT_FIND      2
#define ERR_TOO_MANY       3
#define ERR_NAME_TOO_LONG  4

/* buffer used internally for a list matching a single dir or file name */
struct files {
   char   str[FN_STR_LEN];             /* file name string buffer */
   short  ix;                          /* index to file name string */
};


/*************************************************************************
* local function prototypes
*************************************************************************/
static int compare(char **ptr1, char **ptr2);
static int build_fnl(char *in_startpath, char *in_endpath, struct buffer *g);
static int join_path(char *path, char *dir, char * file, struct buffer *g);
static int write_fnl(char *dir, char *file, struct buffer *g);
static struct files *find_level(char *path, int dirattr, struct buffer *g);
static char *get_level(struct files *f);

/*eject*/
/*************************************************************************
* findfile() - find files and build file name list into list buffer
*
* Inputs:         fname    = pathname (with wildcards) to find
*                 maxlen   = max length of pathname buffer.  A filename
*                            of maxlen-1 can be returned by findfget().
*                 sort     = sort option,  non 0 = sort,  0 = don't sort
*                 mfp      = FILE pointer for error messages, or NULL
*                 progname = pointer to program name string for error
*                            messages or NULL
*
* Return value:   Pointer to list buffer or
*                 NULL if error.
*
*                 If the return value is not NULL it must be passed to
*                 findfget() and released by calling the C library
*                 function free().
* 
* Note: If input name is a null string, then one null string will be
*       returned by findfget().
*************************************************************************/

void *findfile(char *fname, int maxlen, int sort, FILE *mfp, char *progname)
{
   struct buffer *g;                   /* the list buffer struct */
   short  split = 0;                   /* place to split filename */
   char   spstr[4];                    /* split fname string */

   if ((g = malloc(sizeof(struct buffer))) == NULL) { /* allocate buffer */
      if (mfp != NULL) {               /* if not enough memory */
         fputs("\n", mfp);
         if (progname != NULL) {
            fputs(progname, mfp);      /* display program name */
            fputs(": ERROR: ", mfp);
         }
         fputs("Out of memory\n", mfp);
      }
      return (void *)NULL;             /* error RETURN */
   }
   g->ix = g->tot = 0;                 /* init for building the list */
   g->max = (short)maxlen;             /* save max filename length */
   g->cur = 0;                         /* init to first file for findfget() */
   g->errtype = 0;                     /* init to no error */
   
   if (*fname != 0) {                  /* if any file name */
      if (fname[0] == FSEP)
         split = 1;                    /* num chars to split */
#if MSDOS                              /* in MSDOS we must skip the root \ */
      else if (fname[1] == ':' && fname[2] == FSEP)
         split = 3;                    /* split drive and root, "C:\" */
#endif
      if (split)                       /* if split necessary, copy start */
         strncpy(spstr, fname, split);
   }
   spstr[split] = 0;                   /* put string terminator */
   build_fnl(spstr, &fname[split], g); /* get file name list */
   if (g->tot == 0 && g->errtype == 0) /* if none found and no error */
      g->errtype = ERR_CANT_FIND;
   if (g->errtype != 0 ) {             /* if any error */
      if (mfp != NULL) {               /* if any message file */
         fputs("\n", mfp);
         if (progname != NULL) {
            fputs(progname, mfp);      /* display program name */
            fputs(": ERROR: ", mfp);
         }
         switch (g->errtype) {
            case ERR_OUT_OF_MEM:
               fputs("Out of memory\n", mfp);
               break;
            case ERR_CANT_FIND:
               fputs("Can't find file(s)", mfp);
               break;
            case ERR_TOO_MANY:
               fputs("Too many files,", mfp);
               break;
            case ERR_NAME_TOO_LONG:
               fputs("File name too long", mfp);
               break;
         }
         if (g->errtype != ERR_OUT_OF_MEM) {
            fputs(" \"", mfp);
            fputs(fname, mfp);         /* add filename */
            fputs("\"\n", mfp);
         }
      }
      free((void *)g);                 /* free the buffer */
      return (void *)NULL;             /* error RETURN */
   }
   if (sort && g->tot > 1)             /* if sort desired, do it */
      qsort((void *)g->list, g->tot, sizeof(char *), &compare);
   return (void *)g;                   /* return the list buffer */
}
/*eject*/
/*************************************************************************
* findfget() - get a file pathname from list until no more are found
*
* Input:          fname = the char array to store the returned pathname
*                 buff  = ptr to the list buffer returned from findinit()
*
* Return value:   1 if found or
*                 0 if not found (no more files) or buff was NULL
*
*                 If found, the file pathname is copied to the return fname.
*                 The max length was specified for findfile() above.
*************************************************************************/
int findfget(char *fname, void *buff)
{
   struct buffer *g;                   /* the list buffer struct */

   g = (struct buffer *)buff;          /* set global buffer pointer */
   if (g == NULL || g->cur >= g->tot)  /* if no more */
      return 0;                        /* return not found */
   strcpy(fname, g->list[g->cur++]);   /* return a file name */
   return 1;                           /* return found */
}


/*************************************************************************
* compare() - compare two file names
*************************************************************************/
static int compare(char **ptr1, char **ptr2)
{
   return stricmp(*ptr1, *ptr2);
}
/*eject*/
/*************************************************************************
* build_fnl() - build file name list from pathname with wild cards
*
* Input:          in_startpath = beginning part of the path or 0 length string
*                 in_endpath   = ending part of the path with filename
*
*                 Together the two input strings make the input pathname.
*                 The input pathname string has the form:
*
*                       [in_startpath/]in_endpath

*                 There may be wild cards in the file or directory names
*                 that are part of in_endpath.
*
* Return value:   0 if ok or
*                 -1 if error
*
* Other return:   See struct buffer.
*************************************************************************/

static int build_fnl(char *in_startpath, char *in_endpath, struct buffer *g)
{
   char *newend;                       /* end split from in_endpath */
   char *nextpath;                     /* built path for next level  */
   int   dirattr;                      /* 0 = find files, else find dirs */
   struct files *flist = NULL;         /* ptr to dir list at this level */
   int err = 0;                        /* non-0 is error for return */
   int wild_level = 1;                 /* non-0 is wild cards at this level */
   static char nullstring[] = "";      /* a null string */
   char *l_dirname;                    /* dir or file name found this level */

                                       /* no wildcards in the rest of path */
   if (strpbrk(in_endpath, WILDCARDS) == NULL)
      err = write_fnl(in_startpath, in_endpath, g); /* put this in the list */
   else {
      if ((nextpath = malloc(g->max)) == NULL) { /* alloc temp pathname */
         err = -1;
         g->errtype = ERR_OUT_OF_MEM;
      }
                                       /* if a separator */
      if ((newend = strchr(in_endpath, FSEP)) != NULL)
         *(newend++) = 0;              /* temporarily split the end pathname */
                                       /* make new start path */
      if (!err) 
         err = join_path(nextpath, in_startpath, in_endpath, g);
      dirattr = 0;                     /* assume attr = 0 for files */
      if (newend != NULL) {            /* if a path/file follows */
         if (strpbrk(in_endpath, WILDCARDS) == NULL) {
            wild_level = 0;            /* no wild cards at this level */
                                       /* go to next level */
            err = build_fnl(nextpath, newend, g);
         }
         dirattr = DIRATTR;            /* set attr for finding dirs */
         *(newend - 1) = FSEP;         /* put back file seperator */
      } else                           /* else point end at a null string */
         newend = nullstring;
      if (!err && wild_level) {        /* no errs and wild cards this level */
                                       /* find first dir or file */
         if ((flist = find_level(nextpath, dirattr, g)) == NULL)
            err = -1;                  /* if error finding files */
         while (!err) {
            l_dirname = get_level(flist);
            if (*l_dirname == 0)       /* if end */
               break;                  /* exit the loop */
            if (dirattr) {             /* if dir found */
                                       /* join in_path, found dir ==> next */
               if ((err = join_path(nextpath, in_startpath,
                                                   l_dirname, g)) == 0)
                                       /* recursively find next dir/file */
                  err = build_fnl(nextpath, newend, g);
            } else                     /* else file found, put in list */
               err = write_fnl(in_startpath, l_dirname, g);
         }  /* end while */
      }
      if (nextpath != NULL)
         free(nextpath);               /* free the temp pathname */
      if (flist != NULL)
         free(flist);                  /* free the list buffer */
   }
   return err;                         /* return error if any */
}


/*************************************************************************
* join_path() - join two path strings into one pathname
*
* Inputs:         path = ptr to output string
*                 dir  = 1st input string
*                 file = 2nd input string
*                 g    = ptr to the list buffer
*
* Return value:   0 if ok or
*                 -1 if error
*
* Other return:   if error, errtype in the buffer is set to the error
*************************************************************************/
static int join_path(char *path, char *dir, char *file, struct buffer *g)
{
   short dirl;                         /* dir length */
   static char fsepstr[] = { FSEP, 0 };

                                       /* if string too long */
   if ((dirl = strlen(dir)) + strlen(file) > g->max - 2) {
      g->errtype = ERR_NAME_TOO_LONG;  /* set error indicator */
      return -1;
   }
   strcpy (path, dir);                 /* put 1st string */
   if (*dir && *file)                  /* if both are present */
      if (dir[dirl-1] != FSEP)         /* if 1st is not root */
         strcat(path, fsepstr);        /* put separator */
   strcat(path, file);                 /* put 2nd string */
   return 0;
}


/*************************************************************************
* write_fnl() - write a pathname into the file list
*     The two input strings are joined into a pathname and put into the
*     file name string buffer.
*
* Inputs:         dir  = 1st input string
*                 file = 2nd input string
*                 g    = pointer to the list buffer
*
* Return value:   0 if ok or
*                 -1 if error
*
* Other return:   if error, errtype in the buffer is set to the error
*************************************************************************/
static int write_fnl(char *dir, char * file, struct buffer *g)
{
   char *sp;

                                       /* if no space in strings or pointers */
   if (strlen(dir) + strlen(file) + 2 + g->ix > PATH_STR_LEN ||
                                          g->tot >= FN_PTR_LEN) {
      g->errtype = ERR_TOO_MANY;       /* indicate out of space error */
      return -1;                       /* RETURN error */
   }
   sp = &g->str[g->ix];                /* get ptr for next string in list*/
   if (join_path(sp, dir, file, g) != 0) /* put string in the buffer */
      return -1;                       /* RETURN error */
   g->list[g->tot++] = sp;             /* set next in array of pointers */
   g->ix += strlen(sp) + 1;            /* bump string index to next */
   return 0;                           /* RETURN ok */
}
/*eject*/
/*************************************************************************
* find_level() - find matching dirs or files at this level
*
*     This routine makes a list of all dirs (or files) that match the
*     last dir (or file) in the input pathname.
*
* Input:          path    = path name to find
*                 dirattr = 0 if files or DIRATTR (system dependent)
*                 g       = ptr to list buff (for returning err code)
*
* Return value:   ptr to list (must be free'ed later) or
*                 NULL if error
*
*                 Use get_level() to get the names out of the list.
*************************************************************************/
static struct files *find_level(char *path, int dirattr, struct buffer *g)
{
   struct files *f;                    /* ptr to list */
   struct FILEINFO *info;              /* struct for dfind */
   int notfound;                       /* 0 = found,  1 = not found */
   int err = 0;                        /* non-0 is error for return */
   int i = 0;                          /* index into string array */

                                       /* allocate list struct */
   if ((f = malloc(sizeof(struct files))) == NULL)
      err = -1;
                                       /* alloc file information struct */
   if ((info = malloc(sizeof(struct FILEINFO))) == NULL)
      err = -1;
   if (err)                            /* if out of memory */
      g->errtype = ERR_OUT_OF_MEM;
   else {
      f->ix = 0;                       /* else init to start of string */
                                       /* find first match */
      notfound = dfind(info, path, dirattr);
   }
   while (!err && !notfound) {
      if (!dirattr || DIRTYPE) { /* if finding files or its a dir */
         if (strlen(FILENAME) + 1 + i > FN_STR_LEN) {
            g->errtype = ERR_TOO_MANY; /* indicate out of space error */
            err = -1;
         } else {
            strcpy(&f->str[i], FILENAME); /* put dir/file name in the list */
            strlwr(&f->str[i]);        /* make it lower case */
            i += strlen(FILENAME) + 1; /* bump string index to next */
         }
      }
      if (!err)
         notfound = dnext(info);       /* find next dir or file */
   }  /* end while */
   if (info != NULL)
      free(info);                      /* free dfind memory */
   if (err) {                          /* if any error */
      if (f != NULL)
         free(f);                      /* free list memory */
      return NULL;                     /* RETURN error */
   }
   f->str[i] = 0;                      /* put end null string on the list */
   return f;
}


/*************************************************************************
* get_level() - get a dir or file name from the list built by find_level()
*
* Input:          ptr to the list
*
* Return value:   ptr to next string (null string if end)
*************************************************************************/
static char *get_level(struct files *f)
{
   char *p;

   p = &f->str[f->ix];                 /* set pointer to return */
   if (*p)                             /* if not at end */
      f->ix += strlen(p) + 1;          /* bump to next string */
   return p;                           /* return ptr to the string */
}
