/****************************************************************************
**
**  Name: unixopen.cpp
**
**  Description:
**    PC-NFS UNIX non-8.3 file name support.
**
**  $Log:   S:/tbird/mt2_186/cliulib/unixopen.cpv  $
** 
**    Rev 1.0   16 Dec 1996 16:12:32   Judy
** Initial revision.
** 
**    Rev 1.3   28 Jul 1993 17:47:08   john
** hack hack hack
** 
**    Rev 1.2   28 Jun 1993 09:56:38   doug
** errors consolidated in errtext/mkerrors.h
** 
**    Rev 1.1   09 Apr 1993 08:31:48   tom
** Added prototypes, fixed other compiler problems.
** 
**    Rev 1.0   06 Apr 1993 15:33:34   tom
** Initial revision.
**
**  $Header:   S:/tbird/mt2_186/cliulib/unixopen.cpv   1.0   16 Dec 1996 16:12:32   Judy  $
**
**  Copyright (C) 1993 Microtek International.  All rights reserved.
**
*****************************************************************************/

                       /****************************
                        *                          *
                        *   SYSTEM INCLUDE FILES   *
                        *                          *
                        ****************************/
#include <ctype.h>
#include <direct.h>
#include <dos.h>
#include <fcntl.h>
#include <io.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys\stat.h>

extern "C" {
#include <tklib.h>    // from PC-NFS toolkit, for get_nfs_path()
}

// C++ class includes
#include <array.h>
#include <assoc.h>
#include <dict.h>
#include <object.h>
#include <strng.h>

                       /****************************
                        *                          *
                        *    LOCAL INCLUDE FILES   *
                        *                          *
                        ****************************/

#ifndef _BASEWIND_
#include "basewind.h"
#endif

#ifndef _HOSTERRS_
extern "C" {
#include "hosterrs.h"
}
#endif

#ifndef _UNIXOPEN_
#include "unixopen.h"
#endif

                       /****************************
                        *                          *
                        *     LOCAL DEFINITIONS    *
                        *                          *
                        ****************************/
/* use with Borland C++ compiler */
#ifndef _BORLANDC_
#define _BORLANDC_
#endif

#define UNMANGLED extern "C"

#define BUFFER_SIZE_1 256
#define BUFFER_SIZE_2 512
#define BUFFER_SIZE_3 1024

#define INT int  // for compatibility with DOS and library funcs expecting int

#define CHAR_SLASH '/'
#define CHAR_BACKSLASH '\\'
#define CHAR_A 'A'
#define CHAR_Z 'Z'
#define CHAR_a 'a'
#define CHAR_z 'z'
#define CHAR_SPACE ' '
#define CHAR_TAB '\t'
#define NULL_TERMINATOR '\0'
#define CHAR_NEWLINE '\n'
#define STRING_EQUALS "="
#define STRING_SLASH "/"
#define STRING_BACKSLASH "\\"
#define STRING_COLON ":"
#define STRING_SPACE_TAB " \t"
#define STRING_SPACE_TAB_SLASH " \t/"
#define STRING_SPACE_TAB_BACKSLASH " \t\\"
#define STRING_SPACE_TAB_COLON " \t:"
#define STRING_SPACE_TAB_COLON_SLASH " \t:/"

// local definition for UnixOpenFile
#define CLEAN_UP_AND_QUIT(stat) \
   RestoreFileSystemState(currentWorkDirectory, saveDrive); \
   return(stat);

// define object classes, operations
#define ASSOCIATION_CLASS Association
#define DICTIONARY_CLASS Dictionary
#define STRING_CLASS String
#define DICTIONARY_ADD add
#define DICTIONARY_LOOKUP lookup
#define DICTIONARY_VALUE value
#define DICTIONARY_SIZE getItemsInContainer

// PC-NFS interrupt numbers, masks, file name
#define PCNFS_CHANGE_DIRECTORY 0x3b00
#define PCNFS_OPEN_FILE 0x3d00
#define PCNFS_CHECK_FOR_ERROR(status) \
   ((status) & 1)
#define DRIVES_FILE "drives.bat"
#define STRING_NET "NET"
#define STRING_USE "USE"

// Legal drive numbers
#define FIRST_DRIVE 1
#define LAST_DRIVE 26

// skip over whitespace
#define SKIP_WHITESPACE(stringPointer) \
   while(((stringPointer) != NULL) && (isspace((stringPointer)[0]))) { \
      ++stringPointer; \
   }

                       /****************************
                        *                          *
                        *    EXTERNAL VARIABLES    *
                        *                          *
                        ****************************/
PRIVATE DICTIONARY_CLASS *drivesDictionary; // key: directory, value: drive#
PRIVATE BOOLEAN dictionaryCreated = FALSE;  // true if dictionary created
extern "C" {
VOID InitDict(VOID);
}

                       /****************************
                        *                          *
                        *     LOCAL PROTOTYPES     *
                        *                          *
                        ****************************/

#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
ChangeDirectory(LPSTR directory);

#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
ChangeDrive (U16 driveNumber);

#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
DictionaryAdd(LPSTR key, U16 value);

#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
DictionaryInit(VOID);
#endif

#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
DictionaryLookup(LPSTR key, U16 *value);

#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
DictionarySize(U16 *size);

#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
FindDirectory(LPSTR pathName, U16 *driveNumber, LPSTR truncatedPathname);

#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
GetNextToken (LPSTR *string, LPSTR delimiters, LPSTR token);

#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
MapDriveLetterToNumber (CHAR driveLetter, U16 *driveNumber);

#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
OpenUnixFile (LPSTR fileName, S16 *fileHandle);

#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
OpenUnixPathname (LPSTR pathName, S16 *fileHandle);

#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
ParseNewFormat(LPSTR line, LPSTR directory);

#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
ParseOldFormat (LPSTR line, LPSTR directory);

#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
ProcessDrivesFile (VOID);

#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
ProcessDrivesLine (LPSTR line);

#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
RestoreFileSystemState (CHAR * directory, U16 driveNumber);

                       /****************************
                        *                          *
                        *      EXECUTABLE CODE     *
                        *                          *
                        ****************************/

VOID InitDict(VOID) {
   drivesDictionary = new(Dictionary);
}

/*****************************************************************************
**
**  ChangeDirectory
**
**  Description:
**     This sets the directory either to the root, or to a subdirectory
**     within the current directory.  This uses the hidden PC-NFS feature
**     to allow specifying directories longer than 8 characters.
**     NOTE:  The directory is assumed to be "simple", e.g. "dir", not
**       "dir1/dir2".
**
**  Algorithm:
**     prepend "=" to directory
**     set up registers
**     call intdosx to change directory
**     if error
**       display ER_CLIULIB_DIRECTORY_ATTACH_FAILED + directory
**       return ER_CLIULIB_DIRECTORY_ATTACH_FAILED
**     endif
**     return GOOD
**
**  Parameters:
**     directory (in):  Name of the directory to attach to.
**
*****************************************************************************/
#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
ChangeDirectory(LPSTR directory) {
   CHAR dirBuffer[BUFFER_SIZE_3];
   union REGS iregs, oregs;
   struct SREGS segregs;

   // prepend "=" to directory
   lstrcpy(dirBuffer, STRING_EQUALS);
   lstrcat(dirBuffer, directory);

   // set up registers and make call
   iregs.x.ax = PCNFS_CHANGE_DIRECTORY;
   iregs.x.dx = FP_OFF(dirBuffer);
   segregs.ds = segregs.es = FP_SEG(dirBuffer);
   intdosx(&iregs, &oregs, &segregs);

   // display message box if there's an error
   if (PCNFS_CHECK_FOR_ERROR(oregs.x.cflag)) {
      ErrDisplayString(ER_CLIULIB_DIRECTORY_ATTACH_FAILED, directory,
         CHECK_MODE);
      return (ER_CLIULIB_DIRECTORY_ATTACH_FAILED);
   }

   return (GOOD);
}

/*******************************************************************************
**  ChangeDrive
**
**  Description:
**     Change the current drive to the one specified.
**
**  Algorithm:
**     if driveNumber < 1 or driveNumber > 26
**       display ER_CLIULIB_ILLEGAL_DRIVE + driveNumber
**       return ER_CLIULIB_ILLEGAL_DRIVE
**     endif
**     _dos_setdrive(driveNumber);    < DOS >
**     < confirm operation >
**     if driveNumber != _dos_getdrive()   < DOS >
**       < it didn't work >
**       display ER_CLIULIB_DRIVE_ATTACH_FAILED + driveNumber
**       return ER_CLIULIB_DRIVE_ATTACH_FAILED
**     endif
**     return GOOD
**
**  Parameters:
**     driveNumber (in):  Drive number (1 -> A, 2 -> B, ..., 26 -> Z)
**
*****************************************************************************/
#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
ChangeDrive (U16 driveNumber) {
   CHAR buffer[20];
   U16 disk;  // current drive
   U16 maxdrives;  // max drive number

   // discard illegal input.
   if (driveNumber < FIRST_DRIVE || driveNumber > LAST_DRIVE) {
      sprintf(buffer, "%u", driveNumber);
      ErrDisplayString(ER_CLIULIB_ILLEGAL_DRIVE, buffer, CHECK_MODE);
      return (ER_CLIULIB_ILLEGAL_DRIVE);
   }

   // set drive; confirm successful operation.
   _dos_setdrive(driveNumber, (unsigned *)&maxdrives);
   _dos_getdrive((unsigned *)&disk);
   if (driveNumber != disk) {
      sprintf(buffer, "%u", driveNumber);
      ErrDisplayString(ER_CLIULIB_DRIVE_ATTACH_FAILED, buffer, CHECK_MODE);
      return (ER_CLIULIB_DRIVE_ATTACH_FAILED);
   }

   return(GOOD);
}

/*****************************************************************************
**
**  DictionaryAdd
**
**  Description:
**     Given a key and a value, this creates an association, and adds
**     the association to the drivesDictionary dictionary.
**
**  Parameters:
**     key (in):  the directory name used as a key.
**     value (in):  the drive number used as a value.
**
*****************************************************************************/
#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
DictionaryAdd(LPSTR key, U16 value) {
   CHAR dumbBuffer[2];  // buffer to store integer value

   // stuff integer into string
   dumbBuffer[0] = value + CHAR_A - 1;
   dumbBuffer[1] = NULL_TERMINATOR;

   // Create String objects
   String *keyString = new String(key);
   String *valueString = new String(dumbBuffer);

   // Create an association
   Association *dictionaryEntry = new Association(*keyString, *valueString);

   // Stuff into collection
   drivesDictionary->DICTIONARY_ADD(*dictionaryEntry);

   return (GOOD);
}

#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
DictionaryInit(VOID) {
   drivesDictionary->flush();

   return (GOOD);
}
#endif

/*****************************************************************************
**
**  DictionaryLookup
**
**  Description:
**     Given a key, this looks up the association in the drives collection,
**     and returns the value if successful.
**
**  Algorithm:
**
**  Parameters:
**     key (in):  the directory name used as a key.
**     value (out):  the retrieved drive number; 0 if key not found.
**
*****************************************************************************/
#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
DictionaryLookup(LPSTR key, U16 *value) {
   // Look up association; value is 0 if not found, drive number otherwise.
   Association& objectPointer =
      drivesDictionary->DICTIONARY_LOOKUP(*new String(key));
   if (objectPointer == NOOBJECT) {
      *value = 0;
   }
   else {
      String driveString((String&)objectPointer.DICTIONARY_VALUE());
      *value = *driveString - CHAR_A + 1;
   }

   return (GOOD);
}

/*****************************************************************************
**
**  DictionarySize
**
**  Description:
**     Return the number of entries in the drives collection.
**
**  Algorithm:
**
**  Parameters:
**     size (out):  number of entries in collection.
**
*****************************************************************************/
#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
DictionarySize(U16 *size) {
   // return size of collection
   *size = drivesDictionary->DICTIONARY_SIZE();

   return (GOOD);
}

/****************************************************************************
**
**  FindDirectory
**
**  Description:
**     Given a UNIX file path name, locate the entry in the drives.bat
**     collection which best matches the directory, and return the
**     PC-NFS drive letter, and truncated path name.  For example, if
**     the drives.bat file contained the entries:
**
**       NET USE R host:/usr3/abc/def
**       NET USE S host:/usr3/abc/def/ghi
**
**     the following are input and output:
**
**     input                           drive        truncated path
**     -----                           -----        --------- ----
**     /usr3/abc/def/file                R          /file
**     /usr3/abc/def/ghi/file            S          /file
**     /usr3/abc/def/ghi/jkl/file        S          /jkl/file
**     /usr3/def/file                    <error>
**
**  Algorithm:
**     pointer points to first char in pathName
**     while there is another slash in pathName
**       directory = beginning of string up to next slash
**       if directory in drives collection
**         matchFound = true
**         driveNumber = < from drivesDictionary >
**         slashPosition = this slash position
**       endif
**       move pointer beyond this slash
**     end while
**     if not matchFound
**       display ER_CLIULIB_UNIX_DIR_NOT_FOUND + pathName
**       return ER_CLIULIB_UNIX_DIR_NOT_FOUND
**     endif
**     < match found >
**     truncatedPathname = string starting with slashPosition
**     return GOOD
**
**  Parameters:
**     pathName (in):  Input path name.
**     driveNumber (out):  Drive number of matching PC-NFS directory.
**     truncatedPathname (out):  Revised path name wrt PC-NFS directory.
**
*****************************************************************************/
#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
FindDirectory(LPSTR pathName, U16 *driveNumber, LPSTR truncatedPathname) {
   LPSTR pointer;       // work pointer
   LPSTR slashPointer;  // most recent slash
   LPSTR matchPointer;  // most recent match
   CHAR buffer[BUFFER_SIZE_3];
   U16 tempDriveNumber;
   BOOLEAN matchFound = FALSE;  // match found in drivesDictionary?
   RETCODE status;
   U16 length;

   // make sure input is valid
   if ((!pathName) || (pathName[0] != CHAR_SLASH)) {
      ErrDisplayString(ER_CLIULIB_UNIX_DIR_NOT_FOUND, pathName, CHECK_MODE);
      return (ER_CLIULIB_UNIX_DIR_NOT_FOUND);
   }

   pointer = pathName;

   // while there is another slash in pathName, get the next directory
   // (_fstrchr returns null if "/" not found, pointer if found)
   // new: next line generates warning: "Possibly incorrect assignment"
   while (slashPointer = (LPSTR)_fstrchr(pointer, CHAR_SLASH)) {

      // single slash only valid as directory on first character
      if (slashPointer == pointer) {
         if (pointer == pathName) {
            // first character; already known to be a slash from above
            lstrcpy(buffer, STRING_SLASH);
         }
         else {
            ErrDisplayString(ER_CLIULIB_UNIX_DIR_NOT_FOUND,
               pathName, CHECK_MODE);
            return (ER_CLIULIB_UNIX_DIR_NOT_FOUND);
         }
      }
      else {
         // directory name from beginning to slash-1 goes into buffer
         length = lstrlen(pathName) - lstrlen(slashPointer);
         _fstrncpy(buffer, pathName, length);
         buffer[length] = NULL_TERMINATOR;
      }

      // see if directory in drives collection
      if ((status = DictionaryLookup(buffer, &tempDriveNumber)) != GOOD) {
         // error already displayed by DictionaryLookup
         return (status);
      }

      // if 0, no match
      if (tempDriveNumber != 0) {
         matchFound = TRUE;
         *driveNumber = tempDriveNumber;
         matchPointer = slashPointer;
      }

      // move pointer beyond this slash (guaranteed at least 1 char beyond
      // where slash was found).
      pointer = slashPointer + 1;
   }

   if (!matchFound) {
      ErrDisplayString(ER_CLIULIB_UNIX_DIR_NOT_FOUND, pathName, CHECK_MODE);
      return (ER_CLIULIB_UNIX_DIR_NOT_FOUND);
   }

   // if still OK, copy truncated path from beyond slash to end.
   lstrcpy(truncatedPathname, matchPointer);
   return (GOOD);
}

/****************************************************************************
**
**  GetNextToken
**
**  Description:
**     Given a string, and a string of delimiters, return the next token,
**     which is the string of non-delimiter characters up to but not
**     including the first delimiter.  If the first character in the string
**     is a delimiter character, the token is that single delimiter character.
**     The end of the string is an implied delimiter.
**
**  Algorithm:
**     if string is empty
**       token = NULL
**       return GOOD
**     endif
**     if first char in string is a delimiter
**       token = first char
**       return GOOD
**     endif
**     token = string up to next delimiter
**     return GOOD
**
**  Parameters:
**     string (in/out):  Pointer to string pointer where token is to be
**        taken from.  Upon return, points to first character after token.
**     delimiter (in):  String containing one or more delimiter characters.
**     token (out):  The next token.
**
*****************************************************************************/
#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
GetNextToken (LPSTR *string, LPSTR delimiters, LPSTR token) {
   int length;  // index into string where delimiter is

   // if string is empty, return NULL token
   if ((*string == NULL) || (lstrlen(*string) == 0)) {
      token = NULL;
      return(GOOD);
   }

   // find first delimiter
   length = _fstrcspn(*string, delimiters);

   // if first char in string is a delimiter, token is that character
   // (adjust length to copy 1 character); otherwise, copy up to delimiter.
   if (length == 0) {
      length = 1;
   }
   _fstrncpy(token, *string, length);
   token[length] = NULL_TERMINATOR;    // place terminator

   // advance string pointer
   *string += length;

   return (GOOD);
}

/****************************************************************************
**
**  MapDriveLetterToNumber
**
**  Description:
**     Map the drive letter to the corresponding drive number.
**     A, a -> 1; B, b -> 2;...; Z, z -> 26; all others are errors.
**
**  Algorithm:
**     if driveLetter outside A-Z, a-z
**       < don't display error; let it fail silently >
**       return ER_CLIULIB_ILLEGAL_DRIVE
**     endif
**     if driveLetter is upper case
**       driveNumber = driveLetter - 'A' + 1
**     else
**       driveNumber = driveLetter - 'a' + 1
**     endif
**     return GOOD
**
**  Parameters:
**     driveLetter (in):  Input drive letter.
**     driveNumber (out):  Corresponding drive number.
**
*****************************************************************************/
#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
MapDriveLetterToNumber (CHAR driveLetter, U16 *driveNumber) {
   // convert lower to upper case (does nothing if not lower case)
   driveLetter = toupper(driveLetter);

   // reject if driveLetter outside A-Z
   if ((driveLetter < CHAR_A) || (driveLetter > CHAR_Z)) {
      return (ER_CLIULIB_ILLEGAL_DRIVE);
   }

   // convert to number
   *driveNumber = driveLetter - CHAR_A + 1;

   return (GOOD);
}

/****************************************************************************
**
**  OpenUnixFile
**
**  Description:
**     Open a file within the current working directory using PC-NFS's
**     hidden ability to open a native UNIX file.
**
**  Algorithm:
**     prepend "=" to fileName
**     set up registers
**     call intdosx to open unix file
**     if error
**       display ER_CLIULIB_UNIX_FILE_NOT_FOUND + fileName
**       return ER_CLIULIB_UNIX_FILE_NOT_FOUND
**     endif
**     fileHandle = ax register
**     return GOOD
**
**  Parameters:
**     fileName (in):  Simple (contains no path information) name of the
**        file to be opened within the current directory.
**     fileHandle (out):  DOS file handle of the opened file.
**
*****************************************************************************/
#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
OpenUnixFile (LPSTR fileName, S16 *fileHandle) {
   union REGS iregs, oregs;
   struct SREGS segregs;
   CHAR fileNameBuffer[BUFFER_SIZE_3];

   lstrcpy(fileNameBuffer, STRING_EQUALS);
   lstrcat(fileNameBuffer, fileName);

   // set up and make call
   iregs.x.ax = PCNFS_OPEN_FILE;  // open, compatability mode
   iregs.x.dx = FP_OFF(fileNameBuffer);
   segregs.ds = segregs.es = FP_SEG(fileNameBuffer);
   intdosx(&iregs, &oregs, &segregs);

   // check return code
   if (PCNFS_CHECK_FOR_ERROR(oregs.x.cflag)) {
      ErrDisplayString(ER_CLIULIB_UNIX_FILE_NOT_FOUND, fileName, CHECK_MODE);
      return(ER_CLIULIB_UNIX_FILE_NOT_FOUND);
   }

   // get file handle
   *fileHandle = oregs.x.ax;

   return(GOOD);
}

/****************************************************************************
**
**  OpenUnixPathname
**
**  Description:
**     Open a file given the full path name using PC-NFS's ability to attach
**     to native UNIX directories, and open native UNIX files.
**
**  Algorithm:
**     pointer points to pathName
**     GetNextToken(pointer, delimiters: "/", token)
**     if error
**       return error
**     endif
**     if token <> "/"
**       display ER_CLIULIB_UNIX_FILE_NOT_FOUND + pathName
**       return ER_CLIULIB_UNIX_FILE_NOT_FOUND
**     endif
**     ChangeDirectory("/")
**     if error
**       display ER_CLIULIB_DIRECTORY_ATTACH_FAILED + "/"
**       return ER_CLIULIB_DIRECTORY_ATTACH_FAILED
**     endif
**     while there are slashes remaining in pathName
**       advance pointer by 1  // past slash
**       < get the directory name >
**       GetNextToken(pointer, delimiters: "/", token)
**       if error
**         return error
**       endif
**       ChangeDirectory(token)
**       if error
**         return error
**       endif
**       < get the slash >
**       GetNextToken(pointer, delimiters: "/", token)
**       if error
**         return error
**       endif
**       if token <> "/"
**         display ER_CLIULIB_UNIX_FILE_NOT_FOUND + pathName
**         return ER_CLIULIB_UNIX_FILE_NOT_FOUND
**       endif
**     end while
**     < remainder of pathName is now the file name >
**     GetNextToken(pointer, delimiters: "/", token)
**     if error
**       return error
**     endif
**     return results of OpenUnixFile(token, fileHandle)
**
**  Parameters:
**     pathName (in):  UNIX path name of file to be opened; assumes that the
**        file is on the current drive.
**     fileHandle (out):  DOS file handle of the opened file.
**
*****************************************************************************/
#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
OpenUnixPathname (LPSTR pathName, S16 *fileHandle) {
   LPSTR pointer;
   CHAR tokenBuffer[BUFFER_SIZE_3];
   LPSTR token;
   RETCODE status;

   token = tokenBuffer;
   pointer = pathName;

   // get next token
   if ((status = GetNextToken(&pointer, STRING_SLASH, token)) != GOOD) {
      return(status);
   }

   // error if token is null
   if (token == NULL) {
      ErrDisplayString(ER_CLIULIB_UNIX_FILE_NOT_FOUND, pathName, CHECK_MODE);
      return(ER_CLIULIB_UNIX_FILE_NOT_FOUND);
   }

   // consume "/"; mismatch indicates error
   if (lstrcmp(token, STRING_SLASH)) {
      ErrDisplayString(ER_CLIULIB_UNIX_FILE_NOT_FOUND, pathName, CHECK_MODE);
      return(ER_CLIULIB_UNIX_FILE_NOT_FOUND);
   }

   // change to root directory
   if (chdir(STRING_BACKSLASH)) {
      ErrDisplayString(ER_CLIULIB_DIRECTORY_ATTACH_FAILED, STRING_BACKSLASH,
         CHECK_MODE);
      return (ER_CLIULIB_DIRECTORY_ATTACH_FAILED);
   }

   // while there are slashes remaining in pathName, get next subdirectory
   // name, and change directory to that.
   while (_fstrchr(pointer, CHAR_SLASH)) {

      // get the directory name
      if ((status = GetNextToken(&pointer, STRING_SLASH, token)) != GOOD) {
         return(status);
      }

      // error if token is null
      if (token == NULL) {
         ErrDisplayString(ER_CLIULIB_UNIX_FILE_NOT_FOUND, pathName, CHECK_MODE);
         return(ER_CLIULIB_UNIX_FILE_NOT_FOUND);
      }

      // change directory
      if ((status = ChangeDirectory(token)) != GOOD) {
         // error already displayed
         return(status);
      }

      // get the slash
      if ((status = GetNextToken(&pointer, STRING_SLASH, token)) != GOOD) {
         return(status);
      }

      // error if token is null
      if (token == NULL) {
         ErrDisplayString(ER_CLIULIB_UNIX_FILE_NOT_FOUND, pathName, CHECK_MODE);
         return(ER_CLIULIB_UNIX_FILE_NOT_FOUND);
      }

      // token should be "/"; true indicates mismatch
      if (lstrcmp(token, STRING_SLASH)) {
         ErrDisplayString(ER_CLIULIB_UNIX_FILE_NOT_FOUND, pathName, CHECK_MODE);
         return(ER_CLIULIB_UNIX_FILE_NOT_FOUND);
      }
   }

   // remainder of pathName is now the file name; get it
   if ((status = GetNextToken(&pointer, STRING_SLASH, token)) != GOOD) {
      return(status);
   }

   // error if token is null
   if (token == NULL) {
      ErrDisplayString(ER_CLIULIB_UNIX_FILE_NOT_FOUND, pathName, CHECK_MODE);
      return(ER_CLIULIB_UNIX_FILE_NOT_FOUND);
   }

   // return results of OpenUnixFile
   return (OpenUnixFile(token, fileHandle));
}

/****************************************************************************
**
**  ParseNewFormat
**
**  Description:
**     This parses the host and directory portion of a new format line from
**     the nfs\drives.bat file.  The format of the line is:
**
**        NET USE X: host:/dir/dir/dir /parm1 /parm2 ...
**                   ------------------------------- ...
**     The caller has already parsed the "NET USE X:" portion of the line;
**     this function only receives the underlined portion of the line, and
**     returns the directory portion to the caller.  In the above example,
**     this would be "/dir/dir/dir".
**
**  Algorithm:
**     pointer points to line
**     advance pointer past spaces, tabs
**     GetNextToken(pointer, delimiters: (space, tab, ":", "/"), token)
**     if error
**       return error
**     endif
**     if token is not host name
**       < don't display error; let it fail silently >
**       return ER_CLIULIB_BAD_DRIVES_BAT_LINE
**     endif
**     GetNextToken(pointer, delimiters: (space, tab, ":", "/"), token)
**     if error
**       return error
**     endif
**     if token is not ":"
**       < don't display error; let it fail silently >
**       return ER_CLIULIB_BAD_DRIVES_BAT_LINE
**     endif
**     < remainder of line is ("/", "dir") pairs >
**     while true
**       < consume "/" >
**       GetNextToken(pointer, delimiters: (space, tab, "/"), token)
**       if error
**         return error
**       endif
**       if token is empty
**         break out of while loop
**       endif
**       if token is not "/"
**         < don't display error; let it fail silently >
**         return ER_CLIULIB_BAD_DRIVES_BAT_LINE
**       endif
**       < get directory name, add to directory path >
**       GetNextToken(pointer, delimiters: (space, tab, "/"), token)
**       if error
**         return error
**       endif
**       < build up directory path >
**       directory = directory + "/" + token
**     end while
**     return GOOD
**
**  Parameters:
**     line (in):  Remainder of new format drives.bat entry.
**     directory (out):  Directory portion of input line.
**
*****************************************************************************/
#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
ParseNewFormat(LPSTR line, LPSTR directory) {
   LPSTR pointer;
   RETCODE status;
   CHAR tokenBuffer[BUFFER_SIZE_3];
   LPSTR token;
   BOOLEAN okEntry = FALSE;

   pointer = line;
   token = tokenBuffer;

   // advance past spaces, tabs
   SKIP_WHITESPACE(pointer);

   // get host name
   if ((status = GetNextToken(&pointer,
         STRING_SPACE_TAB_COLON_SLASH, token)) != GOOD) {
      return(status);
   }

   // fail silently if token is null
   if (token == NULL) {
      return(ER_CLIULIB_BAD_DRIVES_BAT_LINE);
   }

   // is there a way to validate host names?  None for now.

   // get colon; error if bad return code or not a colon (fail silently).
   if ((status = GetNextToken(&pointer,
         STRING_SPACE_TAB_COLON_SLASH, token)) != GOOD) {
      return(status);
   }

   // fail silently if token is null
   if (token == NULL) {
      return(ER_CLIULIB_BAD_DRIVES_BAT_LINE);
   }

   if (lstrcmp(token, STRING_COLON)) {
      return(ER_CLIULIB_BAD_DRIVES_BAT_LINE);
   }

   // initialize directory string
   lstrcpy(directory, "");

   // remainder of relevant part ofline is ("/", "dir") pairs;
   // get directories, build up directory path string.
   while (TRUE) {
      // consume next "/"; if NULL token, we're done.
      if ((status = GetNextToken(&pointer,
            STRING_SPACE_TAB_SLASH, token)) != GOOD) {
         return(status);
      }
      if (token == NULL)
         break;

      // otherwise, this should be a slash
      if (lstrcmp(token, STRING_SLASH)) {
         break;
      }

      // get directory name, add to directory path
      if ((status = GetNextToken(&pointer,
            STRING_SPACE_TAB_SLASH, token)) != GOOD) {
         return(status);
      }
      if (token == NULL) {
         return(ER_CLIULIB_BAD_DRIVES_BAT_LINE);
      }

      // discard if this is a delimiter
      if ((lstrlen(token) == 1) &&
         ((token[0] == CHAR_SLASH) ||
          (token[0] == CHAR_SPACE) ||
          (token[0] == CHAR_TAB))) {
         return(ER_CLIULIB_BAD_DRIVES_BAT_LINE);
      }

      // otherwise, assume this is a valid directory name; save it.
      lstrcat(directory, STRING_SLASH);
      lstrcat(directory, token);
      okEntry = TRUE;
   }

   if (!okEntry)
      return (ER_CLIULIB_BAD_DRIVES_BAT_LINE);

   return (GOOD);
}

/****************************************************************************
**
**  ParseOldFormat
**
**  Description:
**     This parses the host and directory portion of a old format line from
**     the nfs\drives.bat file.  The format of the line is:
**
**        NET USE X: \\host\dir\dir\dir /parm1 /parm2 ...
**                   -------------------------------- ...
**
**     The caller has already parsed the "NET USE X:" portion of the line;
**     this function only receives the underlined portion of the line, and
**     returns the directory portion to the caller.  In the above example,
**     this would be "/dir/dir/dir".
**
**  Algorithm:
**     pointer points to line
**     advance pointer past spaces, tabs
**     < consume first "\" >
**     GetNextToken(pointer, delimiters: (space, tab, "\"), token)
**     if error
**       return error
**     endif
**     if token is not "\"
**       < don't display error; let it fail silently >
**       return ER_CLIULIB_BAD_DRIVES_BAT_LINE
**     endif
**     < consume second "\" >
**     GetNextToken(pointer, delimiters: (space, tab, "\"), token)
**     if error
**       return error
**     endif
**     if token is not "\"
**       < don't display error; let it fail silently >
**       return ER_CLIULIB_BAD_DRIVES_BAT_LINE
**     endif
**     < consume host name >
**     GetNextToken(pointer, delimiters: (space, tab, "\"), token)
**     if error
**       return error
**     endif
**     < remainder of line is ("\", dir) pairs >
**     while true
**       < consume "\" >
**       GetNextToken(pointer, delimiters: (space, tab, "\"), token)
**       if error
**         return error
**       endif
**       if token is NULL
**         break out of while loop
**       endif
**       if token is not "\"
**         < don't display error; let it fail silently >
**         return ER_CLIULIB_BAD_DRIVES_BAT_LINE
**       endif
**       < get directory name; add to directory path >
**       GetNextToken(pointer, delimiters: (space, tab, "\"), token)
**       if error
**         return error
**       endif
**       if token is not directory name
**         < don't display error; let it fail silently >
**         return ER_CLIULIB_BAD_DRIVES_BAT_LINE
**       endif
**       < build up directory path >
**       directory = directory + "/" + token
**     end while
**     return GOOD
**
**  Parameters:
**     line (in):  Remainder of old format drives.bat entry.
**     directory (out):  Directory portion of input line.
**
*****************************************************************************/
#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
ParseOldFormat (LPSTR line, LPSTR directory) {
   LPSTR pointer;
   RETCODE status;
   CHAR tokenBuffer[BUFFER_SIZE_3];
   LPSTR token;
   BOOLEAN okEntry = FALSE;

   pointer = line;
   token = tokenBuffer;

   // advance past spaces, tabs
   SKIP_WHITESPACE(pointer);

   // consume first "\"; error if not found
   if ((status = GetNextToken(&pointer,
         STRING_SPACE_TAB_BACKSLASH, token)) != GOOD) {
      return(status);
   }

   // fail silently if token is null
   if (token == NULL) {
      return(ER_CLIULIB_BAD_DRIVES_BAT_LINE);
   }

   if (lstrcmp(token, STRING_BACKSLASH)) {
      return(ER_CLIULIB_BAD_DRIVES_BAT_LINE);
   }

   // consume second "\"; error if not found
   if ((status = GetNextToken(&pointer,
         STRING_SPACE_TAB_BACKSLASH, token)) != GOOD) {
      return(status);
   }

   // fail silently if token is null
   if (token == NULL) {
      return(ER_CLIULIB_BAD_DRIVES_BAT_LINE);
   }

   if (lstrcmp(token, STRING_BACKSLASH)) {
      return(ER_CLIULIB_BAD_DRIVES_BAT_LINE);
   }

   // consume host name
   if ((status = GetNextToken(&pointer,
         STRING_SPACE_TAB_BACKSLASH, token)) != GOOD) {
      return(status);
   }

   // fail silently if token is null
   if (token == NULL) {
      return(ER_CLIULIB_BAD_DRIVES_BAT_LINE);
   }

   // is there a way to validate host names?  None for now.

   // initialize directory string
   lstrcpy(directory, "");

   // remainder of relevant part of line is ("\", dir) pairs;
   // get directories, build up directory path string.
   while (TRUE) {
      // consume "\"; if token is NULL, we're done
      if ((status = GetNextToken(&pointer,
            STRING_SPACE_TAB_BACKSLASH, token)) != GOOD) {
         return(status);
      }
      if (token == NULL)
         break;

      // otherwise, this should be a backslash
      if (lstrcmp(token, STRING_BACKSLASH)) {
         break;
      }

      // get directory name, add to directory path
      if ((status = GetNextToken(&pointer,
            STRING_SPACE_TAB_BACKSLASH, token)) != GOOD) {
         return(status);
      }
      if (token == NULL) {
         return(ER_CLIULIB_BAD_DRIVES_BAT_LINE);
      }

      // discard if this is a delimiter
      if ((lstrlen(token) == 1) &&
         ((token[0] == CHAR_BACKSLASH) ||
          (token[0] == CHAR_SPACE) ||
          (token[0] == CHAR_TAB))) {
         return(ER_CLIULIB_BAD_DRIVES_BAT_LINE);
      }

      // otherwise, assume this is a valid directory name; save it.
      lstrcat(directory, STRING_SLASH);
      lstrcat(directory, token);
      okEntry = TRUE;
   }

   if (!okEntry)
      return (ER_CLIULIB_BAD_DRIVES_BAT_LINE);

   return (GOOD);
}

/****************************************************************************
**
**  ProcessDrivesFile
**
**  Description:
**     This drives the processing of the nfs/drives.bat file, which contains
**     all of the mappings of native UNIX directories to PC-NFS drive
**     letters.  The drives.bat file is located and opened.  As each line
**     is read, the drive letter is determined, and the line is identified
**     as either new format or old format.  The directory mapping is
**     determined, and the (UNIX directory, drive number) pair is placed
**     in a collection.  The collection is used later for mapping UNIX
**     path names onto the corresponding PC-NFS drive and truncated
**     path name.
**
**  Algorithm:
**     < locate nfs drive >
**     nfsDrive = nfsdrive()   < pc-nfs toolkit >
**     if error
**       display ER_CLIULIB_DRIVES_BAT_NOT_FOUND
**       return ER_CLIULIB_DRIVES_BAT_NOT_FOUND
**     endif
**     < locate nfs directory >
**     nfsDirectory = get_nfs_path()   < pc-nfs toolkit >
**     if error
**       display ER_CLIULIB_DRIVES_BAT_NOT_FOUND
**       return ER_CLIULIB_DRIVES_BAT_NOT_FOUND
**     endif
**     open drives.bat file for read
**     if error
**       display ER_CLIULIB_DRIVES_BAT_OPEN_FAILED
**       return ER_CLIULIB_DRIVES_BAT_OPEN_FAILED
**     endif
**     initialize drives collection
**     status = GOOD
**     while not end of drives.bat file
**       read next line
**       if error
**         display ER_CLIULIB_DRIVES_BAT_READ_FAILED
**         status = ER_CLIULIB_DRIVES_BAT_READ_FAILED
**         break out of while loop
**       endif
**       status = ProcessDrivesLine(line)
**       if error
**         < error already displayed >
**         break out of while loop
**       endif
**     end while
**     close drives.bat file
**     return status
**
**  Parameters:
**     None.
**
*****************************************************************************/
#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
ProcessDrivesFile (VOID) {
   RETCODE status;
   CHAR buffer[BUFFER_SIZE_2];
   FILE *drivesBat;
   U16 newlinePosition;

   // get pathname for drives.bat file
   if (get_nfs_path(DRIVES_FILE, buffer, BUFFER_SIZE_2)) {
      ErrDisplayError(ER_CLIULIB_DRIVES_BAT_NOT_FOUND, CHECK_MODE);
      return (ER_CLIULIB_DRIVES_BAT_NOT_FOUND);
   }

   // open drives.bat file for read
   if ((drivesBat = fopen(buffer, "r")) == NULL) {
      ErrDisplayError(ER_CLIULIB_DRIVES_BAT_OPEN_FAILED, CHECK_MODE);
      return (ER_CLIULIB_DRIVES_BAT_OPEN_FAILED);
   }

   // process each line in the drives.bat file
   status = GOOD;
   while (!feof(drivesBat)) {
      if (fgets(buffer, BUFFER_SIZE_2, drivesBat) == NULL) {
         if (!feof(drivesBat)) {
            ErrDisplayError(ER_CLIULIB_DRIVES_BAT_READ_FAILED, CHECK_MODE);
            status = ER_CLIULIB_DRIVES_BAT_READ_FAILED;
         }
         break;
      }

      // overwrite newline with null terminator
      newlinePosition = lstrlen(buffer) - 1;
      if (buffer[newlinePosition] == CHAR_NEWLINE)
         buffer[newlinePosition] = NULL_TERMINATOR;

      // process the line
      if ((status = ProcessDrivesLine(buffer)) != GOOD) {
         // error already displayed
         break;
      }
   }

   // close drives.bat file
   fclose(drivesBat);

   return(status);
}

/****************************************************************************
**
**  ProcessDrivesLine
**
**  Description:
**     This method receives lines from the drives.bat file, determines the
**     PC-NFS drive letter, and determines whether the remainder of the line
**     is new or old style so that the appropriate function can be called.
**     When the directory name is returned, a (UNIX path, drive number)
**     pair is inserted into the drives collection.
**     If the line is not a PC-NFS drive line (.e.g. a printer declaration),
**     it is ignored.
**
**     Note:  This function and its callees do NOT display error messages.
**       If a callee detects an error condition, it will return the error
**       return code WITHOUT displaying a message; this function will then
**       ignore the line, and return GOOD.  If this function detects an error,
**       it will ignore the line, and return GOOD.  Only if a line is exactly
**       as expected will it be added to the drives collection.
**
**  Algorithm:
**     pointer points to line
**     advance pointer past spaces, tabs
**     < consume "NET" >
**     GetNextToken(pointer, delimiters: (space, tab), token)
**     if error
**       return error
**     endif
**     if token is not "NET"
**       < silently ignore line >
**       return GOOD
**     endif
**     advance pointer past spaces, tabs
**     < consume "USE" >
**     GetNextToken(pointer, delimiters: (space, tab), token)
**     if error
**       return error
**     endif
**     if token is not "USE"
**       < silently ignore line >
**       return GOOD
**     endif
**     advance pointer past spaces, tabs
**     < get drive letter; convert to drive number >
**     GetNextToken(pointer, delimiters: (space, tab, ":"), token)
**     if error
**       return error
**     endif
**     MapDriveLetterToNumber(token, driveNumber)
**     if error
**       < silently ignore line >
**       return GOOD
**     endif
**     < consume ":" >
**     GetNextToken(pointer, delimiters: (space, tab, ":"), token)
**     if error
**       return error
**     endif
**     if token is not ":"
**       < silently ignore line >
**       return GOOD
**     endif
**     advance pointer past spaces, tabs
**     < determine if old or new format >
**     if next char in line is "\"
**       < continue processing old format line >
**       status = ParseOldFormat(line, directory)
**     else
**       < continue processing new format line >
**       status = ParseNewFormat(line, directory)
**     endif
**     if status is not GOOD
**       < silently ignore line >
**       return GOOD
**     endif
**     insert (directory, driveNumber) into drives collection
**     if error
**       < silently ignore line >
**       return GOOD
**     endif
**     return GOOD
**
**  Parameters:
**     line (in):  The next line from the drives.bat file.
**
*****************************************************************************/
#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
ProcessDrivesLine (LPSTR line) {
   LPSTR pointer;
   RETCODE status;
   CHAR tokenBuffer[BUFFER_SIZE_3];
   LPSTR token;
   U16 driveNumber;
   LPSTR filenamePointer;

   pointer = line;
   token = tokenBuffer;

   // advance past spaces, tabs
   SKIP_WHITESPACE(pointer);

   // consume "NET"; error if bad return code or mismatch (fail silently).
   if (GetNextToken(&pointer, STRING_SPACE_TAB, token) != GOOD) {
      return(GOOD);
   }

   // fail silently if token is null
   if (token == NULL) {
      return(GOOD);
   }

   if (_fstricmp(token, STRING_NET)) {
      return(GOOD);
   }

   // advance past spaces, tabs
   SKIP_WHITESPACE(pointer);

   // consume "USE"; error if bad return code or mismatch (fail silently).
   if (GetNextToken(&pointer, STRING_SPACE_TAB, token) != GOOD) {
      return(GOOD);
   }

   // fail silently if token is null
   if (token == NULL) {
      return(GOOD);
   }

   if (_fstricmp(token, STRING_USE)) {
      return(GOOD);
   }

   // advance past spaces, tabs
   SKIP_WHITESPACE(pointer);

   // get drive letter; map letter to number; fail silently if error.
   if (GetNextToken(&pointer, STRING_SPACE_TAB_COLON, token) != GOOD) {
      return(GOOD);
   }

   // fail silently if token is null
   if (token == NULL) {
      return(GOOD);
   }

   if (lstrlen(token) != 1) {
      return(GOOD);
   }

   if (MapDriveLetterToNumber(tokenBuffer[0], &driveNumber) != GOOD) {
      return(GOOD);
   }

   // consume ":"
   if (GetNextToken(&pointer, STRING_SPACE_TAB_COLON, token) != GOOD) {
      return(GOOD);
   }

   // fail silently if token is null
   if (token == NULL) {
      return(GOOD);
   }

   if (lstrcmp(token, STRING_COLON)) {
      return(GOOD);
   }

   // advance past spaces, tabs
   SKIP_WHITESPACE(pointer);

   // re-use token buffer for returned directory name
   filenamePointer = tokenBuffer;

   // determine if old or new format
   if (_fstrncmp(pointer, STRING_BACKSLASH, 1)) {
      // continue processing new format line
      status = ParseNewFormat(pointer, filenamePointer);
   }
   else {
      // continue processing old format line
      status = ParseOldFormat(pointer, filenamePointer);
   }

   // silently reject if error
   if (status != GOOD) {
      return (GOOD);
   }

   // otherwise, add (directory, driveNumber) into drives collection -
   // don't care about add failure; will return GOOD anyway.
   (VOID)DictionaryAdd(filenamePointer, driveNumber);

   return (GOOD);
}

/****************************************************************************
**
**  RestoreFileSystemState
**
**  Description:
**     This first restores the current working directory, and then the
**     drive that existed prior to opening the PC-NFS file.
**
**  Algorithm:
**     chdir(directory)   <DOS>
**     < if error, PC-NFS directory name no longer in cache >
**     if error
**       ChangeDirectory("/")  < just change to root >
**     endif
**     < change drive >
**     ChangeDrive(driveNumber)
**     if error
**       < error already displayed by ChangeDirectory >
**       return error
**     endif
**     return GOOD
**
**  Parameters:
**     directory (in):  Working directory to be restored.
**     driveNumber (in):  Drive number to be restored.
**
*****************************************************************************/
#ifdef UNIT_TESTING
UNMANGLED RETCODE EXPORT
#else
RETCODE PRIVATE
#endif
RestoreFileSystemState (CHAR * directory, U16 driveNumber) {
   RETCODE status;

   // change to specified directory
   if (chdir(directory)) {
      // chdir failure, probably due to PC-NFS directory; set to root.
      // Don't bother with error code.
      chdir(STRING_BACKSLASH);
   }

   // change drive
   if ((status = ChangeDrive(driveNumber)) != GOOD) {
      // error already displayed by ChangeDirectory
      return(status);
   }

   return(GOOD);
}

/****************************************************************************
**
**  UnixOpenFile
**
**  Description:
**     This processes the nfs/drives.bat file if the drives collection does
**     not yet exist, maps the UNIX file name onto its PC-NFS drive and
**     truncated file name equivalent, and opens the file.
**
**  Algorithm:
**     if PC-NFS not running
**       display ER_CLIULIB_PCNFS_NOT_INSTALLED
**       return ER_CLIULIB_PCNFS_NOT_INSTALLED
**     endif
**     if drivesDictionary does not exist
**       ProcessDrivesFile()
**       if error
**           < error already displayed by ProcessDrivesFile >
**           return error
**       endif
**     endif
**     if no entries in drivesDictionary
**       display ER_CLIULIB_UNIX_DIR_NOT_FOUND
**       return ER_CLIULIB_UNIX_DIR_NOT_FOUND
**     endif
**     FindDirectory(fileName, driveNumber, pathName)
**     if error
**       < error already displayed by FindDirectory >
**       return error
**     endif
**     < save drive and directory for later restore >
**     saveDrive = _dos_getdrive()    < DOS >
**     saveDirectory = getcwd()     < DOS >
**     ChangeDrive(driveNumber)
**     if error
**       < error already displayed by ChangeDrive >
**       restoreFileSystemState(saveDrive, saveDirectory)
**       return error
**     endif
**     OpenUnixPathname(pathName, fileHandle)
**     if error
**       < error already displayed by OpenUnixPathname >
**       restoreFileSystemState(saveDrive, saveDirectory)
**       return error
**     endif
**     restoreFileSystemState(saveDrive, saveDirectory)
**     return GOOD
**
**  Parameters:
**     fileName (in):  Name of UNIX file to be opened.
**     fileHandle (out):  Handle of open file.
**
*****************************************************************************/
UNMANGLED RETCODE EXPORT
UnixOpenFile (LPSTR fileName, S16 *fileHandle) {
   RETCODE status;
   CHAR pathBuffer[BUFFER_SIZE_3];
   LPSTR pathName;
   U16 driveNumber;
   U16 saveDrive;
   CHAR currentWorkDirectoryBuffer[BUFFER_SIZE_3];
   CHAR *currentWorkDirectory;
   U16 size;

   // error if PC-NFS not running
   if (is_pc_nfs_installed() != 1) {
      ErrDisplayError(ER_CLIULIB_PCNFS_NOT_INSTALLED, CHECK_MODE);
      return (ER_CLIULIB_PCNFS_NOT_INSTALLED);
   }

   // create drivesDictionary if it does not exist
   if (!dictionaryCreated) {
      if ((status = ProcessDrivesFile()) != GOOD) {
         return (status);
      }
   }

   // if there are no entries in drivesDictionary, a match is impossible
   if ((status = DictionarySize(&size)) != GOOD) {
      return (status);
   }
   if (size == 0) {
      ErrDisplayError(ER_CLIULIB_UNIX_DIR_NOT_FOUND, CHECK_MODE);
      return (ER_CLIULIB_UNIX_DIR_NOT_FOUND);
   }

   // map fileName onto driveNumber, pathName
   pathName = pathBuffer;
   if ((status = FindDirectory(fileName, &driveNumber, pathName)) != GOOD) {
      // error already displayed by FindDirectory
      return(status);
   }

   // save drive for later restore
   _dos_getdrive((unsigned *)&saveDrive);

   // change the drive
   if ((status = ChangeDrive(driveNumber)) != GOOD) {
      // error already displayed by ChangeDrive
      return(status);
   }

   // save work directory for later restore (must set drive first)
   currentWorkDirectory = getcwd(currentWorkDirectoryBuffer, BUFFER_SIZE_3);

   // open the unix pathname
   if ((status = OpenUnixPathname(pathName, fileHandle)) != GOOD) {
      // error already displayed by OpenUnixPathname
      CLEAN_UP_AND_QUIT(status);
   }

   CLEAN_UP_AND_QUIT(GOOD);
}

/******************************** E O F ***********************************/
