/*
 * to.c - changes to any directory based on pre-computed directory tree
 *
 * Bruno Costa - 30 Jan 91 - 18 Feb 91
 *
 * (Requires AmigaDOS 2.0)
 */

#include <exec/types.h>
#include <dos/stdio.h>
#include <clib/dos_protos.h>
#include <pragmas/dos_pragmas.h>
#include <clib/exec_protos.h>
#include <pragmas/exec_pragmas.h>

#include <ctype.h>
#include <string.h>
#include <stdlib.h>


#define MAXNAME 100		/* maximum size of full path name */
#define MAXPAT 50		/* maximum size of a parsed pattern */

extern struct Library *SysBase;	/* exec.library base */
struct Library *DOSBase;	/* dos.library base */

char fullprogname[MAXNAME];	/* [path] name of the program being run */
char *progname;			/* name of the program being run */

char *filebuf = NULL;		/* buffer where table file data is stored */
long bufsize = 0;		/* size in bytes of the above buffer */

char **nametbl = NULL;		/* array of strings with directory names */
char **pathtbl = NULL;		/* array of strings with full path names */
long tblsize = 0;		/* size of the above arrays */

BPTR tablefile;			/* handle to write to the table file */


#define USAGE_MSG "\x1b[32mto\x1b[31m v2.1 - \xa9 1991 by Bruno Costa\n" \
"usage: \x1b[1mto\x1b[0m [-b | <pattern>]\n"

#define HELP_MSG \
"  Changes to a directory which matches <pattern>. Note that <pattern>\n" \
"may not  include  a path  specification.   The search is  based  on a\n" \
"pre-computed  directory table that is saved to ':dir.table'.  If this\n" \
"table does not exist, it will be automatically created; otherwise the\n" \
"program will be instantly executed.  Using the  option -b  will force\n" \
"the reconstruction of the table for the current drive.\n"


/*
 *   Name  of  the  file  that  contains the table.  For each volume (disk,
 * partition,  etc.)  where  TO will be used this file must be present, and
 * will contain a listing of all the directories present in that volume.
 */
#define TABLE_NAME ":dir.table"

/*
 *   Table  file  header definitions.  FILE_ID is a small string present in
 * the  start  of the file to help avoiding confusions -- it is a hint that
 * we  are  reading  a file in the expected format.  ID_SIZE is the size in
 * bytes  of  this ID string.  HEADER_SIZE is the size of the header, which
 * contains  the  file  ID  and  a  long  int  representing  the  number of
 * directories listed in this file.
 */
#define FILE_ID "DIRT"
#define ID_SIZE (sizeof (FILE_ID) - 1)			/* discounts '\0' */
#define HEADER_SIZE (ID_SIZE + sizeof (tblsize))

#define SORRY_MSG "Sorry, you must upgrade to " \
                  "Workbench 2.0 to run this program.\n"

/*
 *   Names  by which this program is expected to be run.  Based on the name
 * by  which  the user calls the program, it will behave differently.  This
 * is  meant  to be used in the following way:  the executable is copied to
 * your  C:   directory  with  the  name CHANGER; then you make a soft link
 * named  BUILDER  to  it;  then you may call either command by the correct
 * name and the same code will be invoked, but behaving apropriately.  Note
 * that only one copy of the file will be taking up space on disk.
 */
#define BUILDER "mktotbl"
#define CHANGER "to"


/*
 *   Returns  TRUE if the FileInfoBlock structure pointed to
 * by fib refers to a directory.
 */
#define isdir(fib) (fib->fib_DirEntryType > 0)

/*
 * A new C language control structure ...
 */
#define loop for(;;)

/*
 * Returns TRUE if two given strings are the same (case-insensitive).
 */
#define strsame(str1,str2) (stricmp (str1, str2) == 0)



/*
 * functions defined in this file
 */

/* miscellaneous */
unsigned char *fullname (BPTR dirlock, unsigned char *name);
unsigned char *cd (unsigned char *dir);
long filesize (BPTR file);
void strlower (unsigned char *str);

/* table writing */
int opentable (void);
void closetable (void);
int addtotable (unsigned char *path);
int scandir (unsigned char *dirname);
int buildtable (void);

/* table reading */
int readheader (BPTR file);
int alloctables (void);
void freetables (void);
int loadtable (void);

/* Error handling */
void DOSopen2_0 (void);
void DOSclose2_0 (void);
void puterr (unsigned char *msg);
void fail (unsigned char *msg);



/*
 *   This  program  has essentially three main parts.  The first is a table
 * writer:  it scans all directories in a particular drive or partition and
 * saves  all directory names to a table file in that drive.  The second is
 * a  table reader:  it reads the table file produced by the first part and
 * stores  a  representation  of  it  in  memory.   The  third is the table
 * scanner,  that  simply  consults the table in memory to find a directory
 * that  matches a user specified pattern and changes to it.  The first and
 * second parts are below main(), in apropriately labeled sections, and the
 * third is inside main itself.
 */
void main (int argc, char *argv[])
{
 DOSopen2_0 ();

 GetProgramName (fullprogname, MAXNAME);
 progname = FilePart (fullprogname);		/* ignore path */

 if (strsame (progname, BUILDER))		/* behave as BUILDER */
 {
   if (!buildtable ())
     fail ("could not build table.\n");
 }
 else						/* behave as CHANGER */
 {
   if (argc != 2)		/* if incorrect # of args, prints help */
   {
     PutStr (USAGE_MSG);
     PutStr (HELP_MSG);
   }
   else				/* if one arg, do the trick */
   {
     if (strsame ("-b", argv[1]))
     {
       if (!buildtable ())
         fail ("could not build table.\n");
     }
     else
     {
       int found;
       char **p, **n, **q, **cdptr;
       char pat[MAXPAT];
       char cdname[MAXNAME];

       if (!loadtable ())
       {
         if (!buildtable ())
           fail ("could not build table.\n");

         if (!loadtable ())
           fail ("table file is corrupt!\n");
       }

       GetCurrentDirName (cdname, MAXNAME);

       /*
        *   If  we  are  at the root directory, we know that it is not in the
        * table  --  we  don't  need  to  search.   Otherwise, search for the
        * current directory in the pathnames table.
        */
       found = FALSE;
       if (cdname[strlen(cdname) - 1] != ':')	/* are we at the root ? */
       {
         for (n = nametbl, p = pathtbl, q = n + tblsize; n < q; n++, p++)
           if (strsame (*p, cdname))
           {
             found = TRUE;
             cdptr = n;
             break;
           }

         if (!found)
           puterr ("current directory is not in table!\n");
       }

       if (!found)
       {
         n = nametbl;		/*  if the current directory is not in the */
         p = pathtbl;		/* table, set up things so that the search */
         q = n + tblsize;	/* starts from the beginning of the table. */
         cdptr = n;
       }

       /*
        * Prepare a case-insensitive search (since table is all lowercase)
        */
       strlower (argv[1]);
       ParsePattern (argv[1], pat, MAXPAT);

       /*
        *   Searches  for a directory name that matches the pattern, starting
        * from  the  next  place  after  the current directory.  If there are
        * multiple  matches  of  the  same pattern in the table, this assures
        * that  the  user  will be able to cycle among them just by repeating
        * the  same command.  Note that we use the table in a cyclic fashion,
        * going  back  to  the  start if we reach the end of it -- the search
        * only  stops  after we come back to the current dir after finding no
        * other match.
        */
       found = FALSE;

       do
       {
         ++n, ++p;	/* these pointers always go along */

         if (n >= q)	/* close the circle  */
         {
           n = nametbl;
           p = pathtbl;
         }

         if (MatchPattern (pat, *n))
         {
           found = TRUE;
           break;
         }
       } while (n != cdptr);

       if (!found)
         fail ("no directory matches\n");

       if (!cd (*p))
         fail ("could not change directory\n");
     }
   }
 }

 DOSclose2_0 ();

 exit (RETURN_OK);
}


/*-------------------------    Table Writing    -------------------------*/

/*
 *   The table is saved in the following format:
 *
 *	name		size (bytes)	contents
 *	----		----		--------
 *	FileID		4		"DIRT" - identifies table file
 *	Ndirs		4		number of entries that follows
 *  E +--
 *  n |	PathLen		1		size of the following string
 *  t |	Path		PathLen		FULL path name of a directory
 *  r |	EndPath		1		0 - terminates the path string
 *  y +--
 *	...		...		...
 */

/*
 *   Opens  and  prepares  the  table  file  for  writing.  Returns TRUE if
 * successful.
 */
int opentable (void)
{
 tablefile = Open (TABLE_NAME, MODE_NEWFILE);
 if (!tablefile)
   return FALSE;

 /*
  * Use fully buffered I/O to minimize file manipulation overhead.
  */
 SetVBuf (tablefile, BUF_FULL, 4000);

 tblsize = 0;

 /*
  *   Write  a header to identify the file, followed by the total number of
  * directory  entries that follows.  Note that at this time this number is
  * not  yet  known,  so we write a 0 to reserve space for it, and patch it
  * with the corrected value later.
  */
 FWrite (tablefile, FILE_ID, ID_SIZE, 1);
 FWrite (tablefile, &tblsize, sizeof (tblsize), 1);

 return TRUE;
}


/*
 * Finishes the writing of the table file.
 */
void closetable (void)
{
 if (tblsize == 0)	/* an empty table does not make any sense */
 {
   Close (tablefile);
   DeleteFile (TABLE_NAME);
   puterr ("no directories found!\n");
 }
 else
 {
   /*
    * Write buffers to disk, to ensure a correct Seek() operation.
    */
   Flush (tablefile);

   /*
    *   Rewind  to  a  place  near the start of the file, where we reserved
    * space  for  the count of directory entries, and patch it with the now
    * known value.
    */
   if (Seek (tablefile, ID_SIZE, OFFSET_BEGINNING) == -1)
     puterr ("seek error!\n");
   else
     FWrite (tablefile, &tblsize, sizeof (tblsize), 1);

   Close (tablefile);
 }
}


/*
 *   Add  a  particular  directory,  given  by  its  full path name, to the
 * currently open table file.  Returns TRUE if successful.
 */
int addtotable (char *path)
{
 int pathlen = strlen (path) + 1;

 strlower (path);	/* table is all lowercase to be case insensitive */

 /*
  *   Write  a  string  containing  the  full  path  name  of  a particular
  * directory, including a terminating '\0' and preceded by a byte with its
  * length.
  */
 FPutC (tablefile, pathlen);
 FWrite (tablefile, path, pathlen, 1);

 ++tblsize;

 if (CheckSignal (SIGBREAKF_CTRL_C))	/* check for ^C abort */
   return FALSE;
 else
   return TRUE;
}


/*
 *   Recursive  function  to read directories.  For each directory given by
 * its  full  path  name,  it  reads  the contents of it adding each of the
 * directories  inside  it  to  the currently opened table file and calling
 * itself recursively.  Returns TRUE if successful.
 */
int scandir (char *dirname)
{
 int success = FALSE;
 BPTR lock;
 struct FileInfoBlock *info = malloc (sizeof (*info));

 if (!info)
   puterr ("out of mem!\n");
 else
 {
   lock = Lock (dirname, SHARED_LOCK);
   if (!lock)
     puterr ("could not lock directory!\n");
   else
   {
     if (!Examine (lock, info))
       puterr ("could not examine directory!\n");
     else if (isdir (info))
     {
       loop
       {
         if (!ExNext (lock, info))
           break;				/* if error, stop reading */
         else if (isdir (info))
         {
           char *name = fullname (lock, info->fib_FileName);

           if (!addtotable (name))
             break;

           if (!scandir (name))
             break;				/* if error, stop reading */
         }
       }
       if (IoErr () == ERROR_NO_MORE_ENTRIES)	/* if terminated normally */
         success = TRUE;
     }
     UnLock (lock);
   }
   free (info);
 }

 return success;
}


/*
 *   Builds  the  directory  table  for  the  drive  where this command was
 * executed  from.   Calls  scandir() to do the real work.  Returns TRUE if
 * successful.
 */
int buildtable (void)
{
 int success;

 puterr ("building table - please wait ...\n");

 if (success = opentable ())
 {
   success = scandir (":");
   closetable ();
 }

 return success;
}


/*-------------------------    Table Reading    -------------------------*/

/*
 *   Reads  the  header  of a table file, storing the useful information in
 * the  proper places.  It also checks if the file is in a suitable format.
 * Returns TRUE if successful.
 */
int readheader (BPTR file)
{
 char id[ID_SIZE + 1];

 bufsize = filesize (file) - HEADER_SIZE;
 if (bufsize > 0)
   if (Read (file, id, ID_SIZE) == ID_SIZE)
   {
     id[ID_SIZE] = '\0';
     if (strcmp (id, FILE_ID) == 0)
       if (Read (file, &tblsize, sizeof (tblsize)) == sizeof (tblsize))
         if (tblsize > 0)
           return TRUE;
   }

 return FALSE;
}


/*
 *   Allocates  the  tables  and  the buffer used to hold the table file in
 * memory.  Returns TRUE if successful.
 */
int alloctables (void)
{
 filebuf = malloc (bufsize);
 nametbl = calloc (tblsize + 1, sizeof (char *));
 pathtbl = calloc (tblsize + 1, sizeof (char *));

 if (filebuf && nametbl && pathtbl)	/* if allocation succeeded */
   return (TRUE);

 freetables ();
 return (FALSE);
}


/*
 *   Frees the tables and the buffer used to hold in memory the information
 * present in the table file.
 */
void freetables (void)
{
 if (filebuf)
   free (filebuf);
 if (nametbl)
   free (nametbl);
 if (pathtbl)
   free (pathtbl);
}


/*
 * Loads a table file in memory.
 */
int loadtable (void)
{
 BPTR f = Open (TABLE_NAME, MODE_OLDFILE);
 if (!f)
   puterr ("could not open table file\n");
 else
 {
   if (!readheader (f))
     puterr ("table is corrupt!\n");
   else
   {
     if (!alloctables ())
       puterr ("out of mem!\n");
     else
     {
       if (Read (f, filebuf, bufsize) != bufsize)
         puterr ("error reading table\n");
       else
       {
         int i = 0;
         char *p = filebuf, *q = p + bufsize;

         /*
          *   Set up the arrays of directory names and path names to point
          * to  the  proper  places  in  the  file  buffer, which contains
          * essentially a copy of the file contents.
          */
         while (p < q  &&  i <= tblsize)
         {
           pathtbl[i] = p + 1;
           nametbl[i] = FilePart (p + 1);
           p += *p + 1;
           ++i;
         }

         if (p == q)		/* if the loop terminated normally */
         {
           Close (f);
           return TRUE;		/* if successful, return here */
         }
       }
       freetables ();
     }
   }
   Close (f);
 }
 return FALSE;
}


/*-------------------------    Miscellaneous    -------------------------*/

/*
 *   Returns  a  pointer  to  a  static  buffer  (overwritten by each call)
 * containing  the  full  path name of file with a given name relative to a
 * given lock.
 */
char *fullname (BPTR dirlock, char *name)
{
 static char fullname[MAXNAME];

 NameFromLock (dirlock, fullname, MAXNAME);
 if (name)
   AddPart (fullname, name, MAXNAME);

 return fullname;
}


/*
 *   Changes  the  current  directory  to  the  one given by its path name.
 * Updates  the  proper  Process structure fields and returns the full path
 * name of the directory.
 */
char *cd (char *dir)
{
 BPTR newdir, olddir;
 static char namebuf[MAXNAME];

 newdir = Lock (dir, SHARED_LOCK);
 if (!newdir)
   return NULL;
 else
 {
   olddir = CurrentDir (newdir);
   if (olddir)
     UnLock (olddir);
   NameFromLock (newdir, namebuf, MAXNAME);
   SetCurrentDirName (namebuf);
   return namebuf;
 }
}


/*
 * Returns the size in bytes of a file given by a handle.
 */
long filesize (BPTR file)
{
 long size = -1;
 struct FileInfoBlock *fib = malloc (sizeof (*fib));

 if (fib)
 {
   if (ExamineFH (file, fib))
     size = fib->fib_Size;

   free (fib);
 }

 return size;
}


/*
 * Converts all uppercase characters in a string to lowercase.
 */
void strlower (char *str)
{
 for ( ; *str; str++)
   if (isupper (*str))
     *str = tolower (*str);
}


/*-------------------------    Error Handling   -------------------------*/

/*
 * Writes an error message, preceded by the program name.
 */
void puterr (char *msg)
{
 PutStr (progname);
 PutStr (": ");
 PutStr (msg);
}


/*
 * Writes an error message, and then exits with a failure code.
 */
void fail (char *msg)
{
 puterr (msg);
 DOSclose2_0 ();
 exit (RETURN_FAIL);
}


/*
 *   The  two  functions  below  are  used  to  assure that AmigaDOS 2.0 is
 * present  which  is  required for this program to work.  If it isn't, the
 * code  below tries to open the previous version of the library (1.3) just
 * to print a message, and exits.
 */
void DOSopen2_0 (void)
{
 DOSBase = OpenLibrary ("dos.library", 36);	/* requires 2.0 */
 if (DOSBase == NULL)
 {
   DOSBase = OpenLibrary ("dos.library", 33);	/* try opening 1.3 */
   if (DOSBase)
   {
     Write (Output (), SORRY_MSG, sizeof (SORRY_MSG));
     CloseLibrary (DOSBase);
   }
   exit (RETURN_FAIL);
 }
}


void DOSclose2_0 (void)
{
 if (DOSBase)
   CloseLibrary (DOSBase);
}
