/* 
 * tclMacFile.c --
 *
 *      This file implements the channel drivers for Macintosh
 *	files.  It also comtains Macintosh version of other Tcl
 *	functions that deal with the file system.
 *
 * Copyright (c) 1995-1996 Sun Microsystems, Inc.
 *
 * See the file "license.terms" for information on usage and redistribution
 * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
 *
 * SCCS: @(#) tclMacFile.c 1.49 96/10/10 10:11:36
 */

/*
 * Note: This code eventually needs to support async I/O.  In doing this
 * we will need to keep track of all current async I/O.  If exit to shell
 * is called - we shouldn't exit until all asyc I/O completes.
 */

#include "tclInt.h"
#include "tclPort.h"
#include "tclMacInt.h"
#include <Aliases.h>
#include <Errors.h>
#include <Processes.h>
#include <Strings.h>
#include <Types.h>
#include <MoreFiles.h>
#include <MoreFilesExtras.h>
#include <FSpCompat.h>

/*
 * The following are flags returned by GetOpenMode.  They
 * are or'd together to determine how opening and handling
 * a file should occur.
 */

#define TCL_RDONLY		(1<<0)
#define TCL_WRONLY		(1<<1)
#define TCL_RDWR		(1<<2)
#define TCL_CREAT		(1<<3)
#define TCL_TRUNC		(1<<4)
#define TCL_APPEND		(1<<5)
#define TCL_ALWAYS_APPEND	(1<<6)
#define TCL_EXCL		(1<<7)
#define TCL_NOCTTY		(1<<8)
#define TCL_NONBLOCK		(1<<9)
#define TCL_RW_MODES 		(TCL_RDONLY|TCL_WRONLY|TCL_RDWR)

/*
 * This structure describes per-instance state of a 
 * macintosh file based channel.
 */

typedef struct FileState {
    short fileRef;		/* Macintosh file reference number. */
    Tcl_Channel fileChan;	/* Pointer to the channel for this file. */
    int appendMode;		/* Flag to tell if in O_APPEND mode or not. */
    int volumeRef;		/* Flag to tell if in O_APPEND mode or not. */
} FileState;

/*
 * The variable below caches the name of the current working directory
 * in order to avoid repeated calls to getcwd.  The string is malloc-ed.
 * NULL means the cache needs to be refreshed.
 */

static char *currentDir =  NULL;

/*
 * Static routines for this file:
 */

static int		FileBlockMode _ANSI_ARGS_((ClientData instanceData,
			    int mode));
static int		FileClose _ANSI_ARGS_((ClientData instanceData,
			    Tcl_Interp *interp));
static Tcl_File		FileGet _ANSI_ARGS_((ClientData instanceData,
		            int direction));
static int		FileInput _ANSI_ARGS_((ClientData instanceData,
			    char *buf, int toRead, int *errorCode));
static int		FileOutput _ANSI_ARGS_((ClientData instanceData,
			    char *buf, int toWrite, int *errorCode));
static int		FileReady _ANSI_ARGS_((ClientData instanceData,
		            int mask));
static int		FileSeek _ANSI_ARGS_((ClientData instanceData,
			    long offset, int mode, int *errorCode));
static void		FileWatch _ANSI_ARGS_((ClientData instanceData,
		            int mask));
static int		GetOpenMode _ANSI_ARGS_((Tcl_Interp *interp,
        		    char *string));
static Tcl_Channel	OpenFileChannel _ANSI_ARGS_((char *fileName, int mode, 
			    int permissions, int *errorCodePtr));

/*
 * This variable describes the channel type structure for file based IO.
 */

static Tcl_ChannelType fileChannelType = {
    "file",			/* Type name. */
    FileBlockMode,		/* Set blocking or
                                 * non-blocking mode.*/
    FileClose,			/* Close proc. */
    FileInput,			/* Input proc. */
    FileOutput,			/* Output proc. */
    FileSeek,			/* Seek proc. */
    NULL,			/* Set option proc. */
    NULL,			/* Get option proc. */
    FileWatch,			/* Initialize notifier. */
    FileReady,			/* Are there events? */
    FileGet			/* Get Tcl_Files out of channel. */
};


/*
 *----------------------------------------------------------------------
 *
 * TclChdir --
 *
 *	Change the current working directory.
 *
 * Results:
 *	The result is a standard Tcl result.  If an error occurs and 
 *	interp isn't NULL, an error message is left in interp->result.
 *
 * Side effects:
 *	The working directory for this application is changed.  Also
 *	the cache maintained used by TclGetCwd is deallocated and
 *	set to NULL.
 *
 *----------------------------------------------------------------------
 */

int
TclChdir(interp, dirName)
    Tcl_Interp *interp;		/* If non NULL, used for error reporting. */
    char *dirName;     		/* Path to new working directory. */
{
    FSSpec spec;
    OSErr err;
    Boolean isFolder;
    long dirID;

    if (currentDir != NULL) {
	ckfree(currentDir);
	currentDir = NULL;
    }

    err = FSpLocationFromPath(strlen(dirName), dirName, &spec);
    if (err != noErr) {
	errno = ENOENT;
	goto chdirError;
    }
    
    err = FSpGetDirectoryID(&spec, &dirID, &isFolder);
    if (err != noErr) {
	errno = ENOENT;
	goto chdirError;
    }

    if (isFolder != true) {
	errno = ENOTDIR;
	goto chdirError;
    }

    err = FSpSetDefaultDir(&spec);
    if (err != noErr) {
	switch (err) {
	    case afpAccessDenied:
		errno = EACCES;
		break;
	    default:
		errno = ENOENT;
	}
	goto chdirError;
    }

    return TCL_OK;
    chdirError:
    if (interp != NULL) {
	Tcl_AppendResult(interp, "couldn't change working directory to \"",
		dirName, "\": ", Tcl_PosixError(interp), (char *) NULL);
    }
    return TCL_ERROR;
}

/*
 *----------------------------------------------------------------------
 *
 * TclGetCwd --
 *
 *	Return the path name of the current working directory.
 *
 * Results:
 *	The result is the full path name of the current working
 *	directory, or NULL if an error occurred while figuring it
 *	out.  If an error occurs and interp isn't NULL, an error
 *	message is left in interp->result.
 *
 * Side effects:
 *	The path name is cached to avoid having to recompute it
 *	on future calls;  if it is already cached, the cached
 *	value is returned.
 *
 *----------------------------------------------------------------------
 */

char *
TclGetCwd(interp)
    Tcl_Interp *interp;		/* If non NULL, used for error reporting. */
{
    FSSpec theSpec;
    int length;
    Handle pathHandle = NULL;
    
    if (currentDir == NULL) {
	if (FSpGetDefaultDir(&theSpec) != noErr) {
	    if (interp != NULL) {
		interp->result = "error getting working directory name";
	    }
	    return NULL;
	}
	if (FSpPathFromLocation(&theSpec, &length, &pathHandle) != noErr) {
	    if (interp != NULL) {
		interp->result = "error getting working directory name";
	    }
	    return NULL;
	}
	HLock(pathHandle);
	currentDir = (char *) ckalloc((unsigned) (length + 1));
	strcpy(currentDir, *pathHandle);
	HUnlock(pathHandle);
	DisposeHandle(pathHandle);	
    }
    return currentDir;
}

/*
 *----------------------------------------------------------------------
 *
 * Tcl_WaitPid --
 *
 *	Fakes a call to wait pid.
 *
 * Results:
 *	Always returns -1.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

int
Tcl_WaitPid(pid, statPtr, options)
    int pid;
    int *statPtr;
    int options;
{
    return -1;
}

/*
 *----------------------------------------------------------------------
 *
 * TclCreateTempFile --
 *
 *	This function creates a temporary file initialized with an
 *	optional string, and returns a file handle with the file pointer
 *	at the beginning of the file.
 *
 * Results:
 *	A handle to a file.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

Tcl_File
TclCreateTempFile(contents, namePtr)
    char *contents;		/* String to write into temp file, or NULL. */
    Tcl_DString *namePtr;	/* If non-NULL, pointer to initialized 
				 * DString that is filled with the name of 
				 * the temp file that was created. */
{
    char fileName[L_tmpnam];
    Tcl_File file;
    int length = (contents == NULL) ? 0 : strlen(contents);

    tmpnam(fileName);
    file = TclOpenFile(fileName, O_RDWR|O_CREAT|O_TRUNC);
    unlink(fileName);

    if ((file != NULL) && (length > 0)) {
	int fd = (int)Tcl_GetFileInfo(file, NULL);
	while (1) {
	    if (write(fd, contents, length) != -1) {
		break;
	    } else if (errno != EINTR) {
		close(fd);
		Tcl_FreeFile(file);
		return NULL;
	    }
	}
	lseek(fd, 0, SEEK_SET);
    }
    if (namePtr != NULL) {
	Tcl_DStringAppend(namePtr, fileName, -1);
    }
    return file;
}

/*
 *----------------------------------------------------------------------
 *
 * Tcl_FindExecutable --
 *
 *	This procedure computes the absolute path name of the current
 *	application, given its argv[0] value.  However, this
 *	implementation doesn't use of need the argv[0] value.  NULL
 *	may be passed in its place.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The variable tclExecutableName gets filled in with the file
 *	name for the application, if we figured it out.  If we couldn't
 *	figure it out, Tcl_FindExecutable is set to NULL.
 *
 *----------------------------------------------------------------------
 */

void
Tcl_FindExecutable(argv0)
    char *argv0;		/* The value of the application's argv[0]. */
{
    ProcessSerialNumber psn;
    ProcessInfoRec info;
    Str63 appName;
    FSSpec fileSpec;
    int pathLength;
    Handle pathName = NULL;
    OSErr err;
    
    GetCurrentProcess(&psn);
    info.processInfoLength = sizeof(ProcessInfoRec);
    info.processName = appName;
    info.processAppSpec = &fileSpec;
    GetProcessInformation(&psn, &info);

    if (tclExecutableName != NULL) {
	ckfree(tclExecutableName);
	tclExecutableName = NULL;
    }
    
    err = FSpPathFromLocation(&fileSpec, &pathLength, &pathName);

    tclExecutableName = (char *) ckalloc((unsigned) pathLength + 1);
    HLock(pathName);
    strcpy(tclExecutableName, *pathName);
    HUnlock(pathName);
    DisposeHandle(pathName);	
}

/*
 *----------------------------------------------------------------------
 *
 * TclGetUserHome --
 *
 *	This function takes the passed in user name and finds the
 *	corresponding home directory specified in the password file.
 *
 * Results:
 *	On a Macintosh we always return a NULL.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

char *
TclGetUserHome(name, bufferPtr)
    char *name;			/* User name to use to find home directory. */
    Tcl_DString *bufferPtr;	/* May be used to hold result.  Must not hold
				 * anything at the time of the call, and need
				 * not even be initialized. */
{
    return NULL;
}

/*
 *----------------------------------------------------------------------
 *
 * TclMatchFiles --
 *
 *	This routine is used by the globbing code to search a
 *	directory for all files which match a given pattern.
 *
 * Results: 
 *	If the tail argument is NULL, then the matching files are
 *	added to the interp->result.  Otherwise, TclDoGlob is called
 *	recursively for each matching subdirectory.  The return value
 *	is a standard Tcl result indicating whether an error occurred
 *	in globbing.
 *
 * Side effects:
 *	None.
 *
 *---------------------------------------------------------------------- */

int
TclMatchFiles(interp, separators, dirPtr, pattern, tail)
    Tcl_Interp *interp;		/* Interpreter to receive results. */
    char *separators;		/* Directory separators to pass to TclDoGlob. */
    Tcl_DString *dirPtr;	/* Contains path to directory to search. */
    char *pattern;		/* Pattern to match against. */
    char *tail;			/* Pointer to end of pattern.  Tail must
				 * point to a location in pattern. */
{
    char *dirName, *patternEnd = tail;
    char savedChar;
    int result = TCL_OK;
    int baseLength = Tcl_DStringLength(dirPtr);
    CInfoPBRec pb;
    OSErr err;
    FSSpec dirSpec;
    Boolean isDirectory;
    long dirID;
    short itemIndex;
    Str255 fileName;
    

    /*
     * Make sure that the directory part of the name really is a
     * directory.
     */

    dirName = dirPtr->string;
    FSpLocationFromPath(strlen(dirName), dirName, &dirSpec);
    err = FSpGetDirectoryID(&dirSpec, &dirID, &isDirectory);
    if ((err != noErr) || !isDirectory) {
	return TCL_OK;
    }

    /*
     * Now open the directory for reading and iterate over the contents.
     */

    pb.hFileInfo.ioVRefNum = dirSpec.vRefNum;
    pb.hFileInfo.ioDirID = dirID;
    pb.hFileInfo.ioNamePtr = (StringPtr) fileName;
    pb.hFileInfo.ioFDirIndex = itemIndex = 1;
    
    /*
     * Clean up the end of the pattern and the tail pointer.  Leave
     * the tail pointing to the first character after the path separator
     * following the pattern, or NULL.  Also, ensure that the pattern
     * is null-terminated.
     */

    if (*tail == '\\') {
	tail++;
    }
    if (*tail == '\0') {
	tail = NULL;
    } else {
	tail++;
    }
    savedChar = *patternEnd;
    *patternEnd = '\0';

    while (1) {
	pb.hFileInfo.ioFDirIndex = itemIndex;
	pb.hFileInfo.ioDirID = dirID;
	err = PBGetCatInfoSync(&pb);
	if (err != noErr) {
	    break;
	}

	/*
	 * Now check to see if the file matches.  If there are more
	 * characters to be processed, then ensure matching files are
	 * directories before calling TclDoGlob. Otherwise, just add
	 * the file to the result.
	 */

	p2cstr(fileName);
	if (Tcl_StringMatch((char *) fileName, pattern)) {
	    Tcl_DStringSetLength(dirPtr, baseLength);
	    Tcl_DStringAppend(dirPtr, (char *) fileName, -1);
	    if (tail == NULL) {
		if ((dirPtr->length > 1) &&
			(strchr(dirPtr->string+1, ':') == NULL)) {
		    Tcl_AppendElement(interp, dirPtr->string+1);
		} else {
		    Tcl_AppendElement(interp, dirPtr->string);
		}
	    } else if ((pb.hFileInfo.ioFlAttrib & ioDirMask) != 0) {
		Tcl_DStringAppend(dirPtr, ":", 1);
		result = TclDoGlob(interp, separators, dirPtr, tail);
		if (result != TCL_OK) {
		    break;
		}
	    }
	}
	
	itemIndex++;
    }
    *patternEnd = savedChar;

    return result;
}

/*
 *----------------------------------------------------------------------
 *
 * TclMacStat --
 *
 *	This function replaces the library version of stat.  The stat
 *	function provided by most Mac compiliers is rather broken and
 *	incomplete.
 *
 * Results:
 *	See stat documentation.
 *
 * Side effects:
 *	See stat documentation.
 *
 *----------------------------------------------------------------------
 */

int
TclMacStat(path, buf)
    char *path;
    struct stat *buf;
{
    HFileInfo fpb;
    HVolumeParam vpb;
    OSErr err;
    FSSpec fileSpec;
    Boolean isDirectory;
    long dirID;
    
    err = FSpLocationFromPath(strlen(path), path, &fileSpec);
    if (err != noErr) {
	errno = TclMacOSErrorToPosixError(err);
	return -1;
    }
    
    /*
     * Fill the fpb & vpb struct up with info about file or directory.
     */
     
    FSpGetDirectoryID(&fileSpec, &dirID, &isDirectory);
    vpb.ioVRefNum = fpb.ioVRefNum = fileSpec.vRefNum;
    vpb.ioNamePtr = fpb.ioNamePtr = fileSpec.name;
    if (isDirectory) {
	fpb.ioDirID = fileSpec.parID;
    } else {
	fpb.ioDirID = dirID;
    }

    fpb.ioFDirIndex = 0;
    err = PBGetCatInfoSync((CInfoPBPtr)&fpb);
    if (err == noErr) {
	vpb.ioVolIndex = 0;
	err = PBHGetVInfoSync((HParmBlkPtr)&vpb);
	if (err == noErr && buf != NULL) {
	    /* 
	     * Files are always readable by everyone.
	     */
	     
	    buf->st_mode = S_IRUSR | S_IRGRP | S_IROTH;

	    /* 
	     * Use the Volume Info & File Info to fill out stat buf.
	     */
	    if (fpb.ioFlAttrib & 0x10) {
		buf->st_mode |= S_IFDIR;
		buf->st_nlink = 2;
	    } else {
		buf->st_nlink = 1;
		if (fpb.ioFlFndrInfo.fdFlags & 0x8000) {
		    buf->st_mode |= S_IFLNK;
		} else {
		    buf->st_mode |= S_IFREG;
		}
	    }
	    if ((fpb.ioFlAttrib & 0x10) || (fpb.ioFlFndrInfo.fdType == 'APPL')) {
	    	/*
	    	 * Directories and applications are executable by everyone.
	    	 */
	    	 
	    	buf->st_mode |= S_IXUSR | S_IXGRP | S_IXOTH;
	    }
	    if ((fpb.ioFlAttrib & 0x01) == 0){
	    	/* 
	    	 * If not locked, then everyone has write acces.
	    	 */
	    	 
	        buf->st_mode |= S_IWUSR | S_IWGRP | S_IWOTH;
	    }
	    buf->st_ino = fpb.ioDirID;
	    buf->st_dev = fpb.ioVRefNum;
	    buf->st_uid = -1;
	    buf->st_gid = -1;
	    buf->st_rdev = 0;
	    buf->st_size = fpb.ioFlLgLen;
	    buf->st_atime = buf->st_mtime = fpb.ioFlMdDat;
	    buf->st_ctime = fpb.ioFlCrDat;
	    buf->st_blksize = vpb.ioVAlBlkSiz;
	    buf->st_blocks = (buf->st_size + buf->st_blksize - 1) / buf->st_blksize;
	}
    }

    if (err != noErr) {
	errno = TclMacOSErrorToPosixError(err);
    }
    
    return (err == noErr ? 0 : -1);
}

/*
 *----------------------------------------------------------------------
 *
 * TclMacReadlink --
 *
 *	This function replaces the library version of readlink.
 *
 * Results:
 *	See readlink documentation.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

int
TclMacReadlink(path, buf, size)
    char *path;
    char *buf;
    int size;
{
    HFileInfo fpb;
    OSErr err;
    FSSpec fileSpec;
    Boolean isDirectory;
    Boolean wasAlias;
    long dirID;
    char fileName[256];
    char *end;
    Handle theString = NULL;
    int pathSize;

    /*
     * Remove ending colons if they exist.
     */
    while ((strlen(path) != 0) && (path[strlen(path) - 1] == ':')) {
	path[strlen(path) - 1] = NULL;
    }

    if (strchr(path, ':') == NULL) {
	strcpy(fileName, path);
	path = NULL;
    } else {
	end = strrchr(path, ':') + 1;
	strcpy(fileName, end);
	*end = NULL;
    }
    c2pstr(fileName);
    
    /*
     * Create the file spec for the directory of the file
     * we want to look at.
     */
    if (path != NULL) {
	err = FSpLocationFromPath(strlen(path), path, &fileSpec);
	if (err != noErr) {
	    errno = EINVAL;
	    return -1;
	}
    } else {
	FSMakeFSSpecCompat(0, 0, NULL, &fileSpec);
    }
    
    /*
     * Fill the fpb struct up with info about file or directory.
     */
    FSpGetDirectoryID(&fileSpec, &dirID, &isDirectory);
    fpb.ioVRefNum = fileSpec.vRefNum;
    fpb.ioDirID = dirID;
    fpb.ioNamePtr = (StringPtr) fileName;

    fpb.ioFDirIndex = 0;
    err = PBGetCatInfoSync((CInfoPBPtr)&fpb);
    if (err != noErr) {
	errno = TclMacOSErrorToPosixError(err);
	return -1;
    } else {
	if (fpb.ioFlAttrib & 0x10) {
	    errno = EINVAL;
	    return -1;
	} else {
	    if (fpb.ioFlFndrInfo.fdFlags & 0x8000) {
		/*
		 * The file is a link!
		 */
	    } else {
		errno = EINVAL;
		return -1;
	    }
	}
    }
    
    /*
     * If we are here it's really a link - now find out
     * where it points to.
     */
    err = FSMakeFSSpecCompat(fileSpec.vRefNum, dirID, (StringPtr) fileName, &fileSpec);
    if (err == noErr) {
	err = ResolveAliasFile(&fileSpec, true, &isDirectory, &wasAlias);
    }
    if ((err == fnfErr) || wasAlias) {
	err = FSpPathFromLocation(&fileSpec, &pathSize, &theString);
	if ((err != noErr) || (pathSize > size)) {
	    DisposeHandle(theString);
	    errno = ENAMETOOLONG;
	    return -1;
	}
    } else {
    	errno = EINVAL;
	return -1;
    }

    strncpy(buf, *theString, pathSize);
    DisposeHandle(theString);
    
    return pathSize;
}

/*
 *----------------------------------------------------------------------
 *
 * TclMacAccess --
 *
 *	This function replaces the library version of access.  The
 *	access function provided by most Mac compiliers is rather 
 *	broken or incomplete.
 *
 * Results:
 *	See access documentation.
 *
 * Side effects:
 *	See access documentation.
 *
 *----------------------------------------------------------------------
 */

int
TclMacAccess(path, mode)
    const char *path;
    int mode;
{
    HFileInfo fpb;
    HVolumeParam vpb;
    OSErr err;
    FSSpec fileSpec;
    Boolean isDirectory;
    long dirID;
    int full_mode = 0;

    err = FSpLocationFromPath(strlen(path), (char *) path, &fileSpec);
    if (err != noErr) {
	errno = TclMacOSErrorToPosixError(err);
	return -1;
    }
    
    /*
     * Fill the fpb & vpb struct up with info about file or directory.
     */
    FSpGetDirectoryID(&fileSpec, &dirID, &isDirectory);
    vpb.ioVRefNum = fpb.ioVRefNum = fileSpec.vRefNum;
    vpb.ioNamePtr = fpb.ioNamePtr = fileSpec.name;
    if (isDirectory) {
	fpb.ioDirID = fileSpec.parID;
    } else {
	fpb.ioDirID = dirID;
    }

    fpb.ioFDirIndex = 0;
    err = PBGetCatInfoSync((CInfoPBPtr)&fpb);
    if (err == noErr) {
	vpb.ioVolIndex = 0;
	err = PBHGetVInfoSync((HParmBlkPtr)&vpb);
	if (err == noErr) {
	    /* 
	     * Use the Volume Info & File Info to determine
	     * access information.  If we have got this far
	     * we know the directory is searchable or the file
	     * exists.  (We have F_OK)
	     */

	    /*
	     * Check to see if the volume is hardware or
	     * software locked.  If so we arn't W_OK.
	     */
	    if (mode & W_OK) {
		if ((vpb.ioVAtrb & 0x0080) || (vpb.ioVAtrb & 0x8000)) {
		    errno = EROFS;
		    return -1;
		}
		if (fpb.ioFlAttrib & 0x01) {
		    errno = EACCES;
		    return -1;
		}
	    }
	    
	    /*
	     * Directories are always searchable and executable.  But only 
	     * files of type 'APPL' are executable.
	     */
	    if (!(fpb.ioFlAttrib & 0x10) && (mode & X_OK)
	    	&& (fpb.ioFlFndrInfo.fdType != 'APPL')) {
		return -1;
	    }
	}
    }

    if (err != noErr) {
	errno = TclMacOSErrorToPosixError(err);
	return -1;
    }
    
    return 0;
}

/*
 *----------------------------------------------------------------------
 *
 * TclMacFOpenHack --
 *
 *	This function replaces fopen.  It supports paths with alises.
 *	Note, remember to undefine the fopen macro!
 *
 * Results:
 *	See fopen documentation.
 *
 * Side effects:
 *	See fopen documentation.
 *
 *----------------------------------------------------------------------
 */

#undef fopen
FILE *
TclMacFOpenHack(path, mode)
    const char *path;
    const char *mode;
{
    OSErr err;
    FSSpec fileSpec;
    Handle pathString = NULL;
    int size;
    FILE * f;
    
    err = FSpLocationFromPath(strlen(path), (char *) path, &fileSpec);
    if ((err != noErr) && (err != fnfErr)) {
	return NULL;
    }
    err = FSpPathFromLocation(&fileSpec, &size, &pathString);
    if ((err != noErr) && (err != fnfErr)) {
	return NULL;
    }
    
    HLock(pathString);
    f = fopen(*pathString, mode);
    HUnlock(pathString);
    DisposeHandle(pathString);
    return f;
}

/*
 *----------------------------------------------------------------------
 *
 * Tcl_OpenFileChannel --
 *
 *	Open an File based channel on Unix systems.
 *
 * Results:
 *	The new channel or NULL. If NULL, the output argument
 *	errorCodePtr is set to a POSIX error.
 *
 * Side effects:
 *	May open the channel and may cause creation of a file on the
 *	file system.
 *
 *----------------------------------------------------------------------
 */

Tcl_Channel
Tcl_OpenFileChannel(interp, fileName, modeString, permissions)
    Tcl_Interp *interp;			/* Interpreter for error reporting;
                                         * can be NULL. */
    char *fileName;			/* Name of file to open. */
    char *modeString;			/* A list of POSIX open modes or
                                         * a string such as "rw". */
    int permissions;			/* If the open involves creating a
                                         * file, with what modes to create
                                         * it? */
{
    Tcl_Channel chan;
    int mode;
    char *nativeName;
    Tcl_DString buffer;
    int errorCode;
    
    mode = GetOpenMode(interp, modeString);
    if (mode == -1) {
	return NULL;
    }

    nativeName = Tcl_TranslateFileName(interp, fileName, &buffer);
    if (nativeName == NULL) {
	return NULL;
    }

    chan = OpenFileChannel(nativeName, mode, permissions, &errorCode);
    Tcl_DStringFree(&buffer);

    if (chan == NULL) {
	Tcl_SetErrno(errorCode);
	if (interp != (Tcl_Interp *) NULL) {
            Tcl_AppendResult(interp, "couldn't open \"", fileName, "\": ",
                    Tcl_PosixError(interp), (char *) NULL);
        }
	return NULL;
    }
    
    return chan;
}

/*
 *----------------------------------------------------------------------
 *
 * OpenFileChannel--
 *
 *	Opens a Macintosh file and creates a Tcl channel to control it.
 *
 * Results:
 *	A Tcl channel.
 *
 * Side effects:
 *	Will open a Macintosh file.
 *
 *----------------------------------------------------------------------
 */

static Tcl_Channel
OpenFileChannel(fileName, mode, permissions, errorCodePtr)
    char *fileName;			/* Name of file to open. */
    int mode;				/* Mode for opening file. */
    int permissions;			/* If the open involves creating a
                                         * file, with what modes to create
                                         * it? */
    int *errorCodePtr;			/* Where to store error code. */
{
    int channelPermissions;
    Tcl_Channel chan;
    char macPermision;
    FSSpec fileSpec;
    OSErr err;
    short fileRef;
    FileState *fileState;
    char channelName[64];
    
    /*
     * Note we use fsRdWrShPerm instead of fsRdWrPerm which allows shared
     * writes on a file.  This isn't common on a mac but is common with 
     * Windows and UNIX and the feature is used by Tcl.
     */

    switch (mode & (TCL_RDONLY | TCL_WRONLY | TCL_RDWR)) {
	case TCL_RDWR:
	    channelPermissions = (TCL_READABLE | TCL_WRITABLE);
	    macPermision = fsRdWrShPerm;
	    break;
	case TCL_WRONLY:
	    /*
	     * Mac's fsRdPerm permission actually defaults to fsRdWrPerm because
	     * the Mac OS doesn't realy support write only access.  We explicitly
	     * set the permission fsRdWrShPerm so that we can have shared write
	     * access.
	     */
	    channelPermissions = TCL_WRITABLE;
	    macPermision = fsRdWrShPerm;
	    break;
	case TCL_RDONLY:
	default:
	    channelPermissions = TCL_READABLE;
	    macPermision = fsRdPerm;
	    break;
    }
     
    err = FSpLocationFromPath(strlen(fileName), fileName, &fileSpec);
    if ((err != noErr) && (err != fnfErr)) {
	*errorCodePtr = errno = TclMacOSErrorToPosixError(err);
	Tcl_SetErrno(errno);
	return NULL;
    }

    if ((err == fnfErr) && (mode & TCL_CREAT)) {
	err = HCreate(fileSpec.vRefNum, fileSpec.parID, fileSpec.name, 'MPW ', 'TEXT');
	if (err != noErr) {
	    *errorCodePtr = errno = TclMacOSErrorToPosixError(err);
	    Tcl_SetErrno(errno);
	    return NULL;
	}
    } else if ((mode & TCL_CREAT) && (mode & TCL_EXCL)) {
        *errorCodePtr = errno = EEXIST;
	Tcl_SetErrno(errno);
        return NULL;
    }

    err = HOpenDF(fileSpec.vRefNum, fileSpec.parID, fileSpec.name, macPermision, &fileRef);
    if (err != noErr) {
	*errorCodePtr = errno = TclMacOSErrorToPosixError(err);
	Tcl_SetErrno(errno);
	return NULL;
    }

    if (mode & TCL_TRUNC) {
	SetEOF(fileRef, 0);
    }
    
    sprintf(channelName, "file%d", (int) fileRef);
    fileState = (FileState *) ckalloc((unsigned) sizeof(FileState));
    chan = Tcl_CreateChannel(&fileChannelType, channelName, 
	(ClientData) fileState, channelPermissions);
    if (chan == (Tcl_Channel) NULL) {
	*errorCodePtr = errno = EFAULT;
	Tcl_SetErrno(errno);
	FSClose(fileRef);
	ckfree((char *) fileState);
        return NULL;
    }

    fileState->fileChan = chan;
    fileState->volumeRef = fileSpec.vRefNum;
    fileState->fileRef = fileRef;
    if (mode & TCL_ALWAYS_APPEND) {
	fileState->appendMode = true;
    } else {
	fileState->appendMode = false;
    }
        
    if ((mode & TCL_ALWAYS_APPEND) || (mode & TCL_APPEND)) {
        if (Tcl_Seek(chan, 0, SEEK_END) < 0) {
	    *errorCodePtr = errno = EFAULT;
	    Tcl_SetErrno(errno);
            Tcl_Close(NULL, chan);
            FSClose(fileRef);
            ckfree((char *) fileState);
            return NULL;
        }
    }
    
    return chan;
}

/*
 *----------------------------------------------------------------------
 *
 * FileBlockMode --
 *
 *	Set blocking or non-blocking mode on channel.  Macintosh files
 *	can never really be set to blocking or non-blocking modes.
 *	However, we don't generate an error - we just return success.
 *
 * Results:
 *	0 if successful, errno when failed.
 *
 * Side effects:
 *	Sets the device into blocking or non-blocking mode.
 *
 *----------------------------------------------------------------------
 */

static int
FileBlockMode(instanceData, mode)
    ClientData instanceData;		/* Unused. */
    int mode;				/* The mode to set. */
{
    return 0;
}

/*
 *----------------------------------------------------------------------
 *
 * FileClose --
 *
 *	Closes the IO channel.
 *
 * Results:
 *	0 if successful, the value of errno if failed.
 *
 * Side effects:
 *	Closes the physical channel
 *
 *----------------------------------------------------------------------
 */

static int
FileClose(instanceData, interp)
    ClientData instanceData;	/* Unused. */
    Tcl_Interp *interp;		/* Unused. */
{
    FileState *fileState = (FileState *) instanceData;
    int errorCode = 0;
    OSErr err;

    err = FSClose(fileState->fileRef);
    FlushVol(NULL, fileState->volumeRef);
    if (err != noErr) {
	errorCode = errno = TclMacOSErrorToPosixError(err);
	panic("error during file close");
    }

    ckfree((char *) fileState);
    Tcl_SetErrno(errorCode);
    return errorCode;
}

/*
 *----------------------------------------------------------------------
 *
 * FileGet --
 *
 *	Called from Tcl_GetChannelFile to retrieve Tcl_Files from inside
 *	a file based channel.
 *
 * Results:
 *	The appropriate Tcl_File or NULL if not present. 
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static Tcl_File
FileGet(instanceData, direction)
    ClientData instanceData;		/* The file state. */
    int direction;			/* Which Tcl_File to retrieve? */
{
    FileState *fileState = (FileState *) instanceData;

    if ((direction == TCL_READABLE) || (direction == TCL_WRITABLE)) {
	return (Tcl_File) fileState->fileRef;
    }
    return (Tcl_File) NULL;
}

/*
 *----------------------------------------------------------------------
 *
 * FileInput --
 *
 *	Reads input from the IO channel into the buffer given. Returns
 *	count of how many bytes were actually read, and an error indication.
 *
 * Results:
 *	A count of how many bytes were read is returned and an error
 *	indication is returned in an output argument.
 *
 * Side effects:
 *	Reads input from the actual channel.
 *
 *----------------------------------------------------------------------
 */

int
FileInput(instanceData, buffer, bufSize, errorCodePtr)
    ClientData instanceData;		/* Unused. */
    char *buffer;			/* Where to store data read. */
    int bufSize;			/* How much space is available
                                         * in the buffer? */
    int *errorCodePtr;			/* Where to store error code. */
{
    FileState *fileState = (FileState *) instanceData;
    OSErr err;
    long length = bufSize;

    *errorCodePtr = 0;
    errno = 0;
    err = FSRead(fileState->fileRef, &length, buffer);
    if ((err == noErr) || (err == eofErr)) {
	return length;
    } else {
	switch (err) {
	    case ioErr:
		*errorCodePtr = errno = EIO;
	    case afpAccessDenied:
		*errorCodePtr = errno = EACCES;
	    default:
		*errorCodePtr = errno = EINVAL;
	}
        return -1;	
    }
    *errorCodePtr = errno;
    return -1;
}

/*
 *----------------------------------------------------------------------
 *
 * FileOutput--
 *
 *	Writes the given output on the IO channel. Returns count of how
 *	many characters were actually written, and an error indication.
 *
 * Results:
 *	A count of how many characters were written is returned and an
 *	error indication is returned in an output argument.
 *
 * Side effects:
 *	Writes output on the actual channel.
 *
 *----------------------------------------------------------------------
 */

static int
FileOutput(instanceData, buffer, toWrite, errorCodePtr)
    ClientData instanceData;		/* Unused. */
    char *buffer;			/* The data buffer. */
    int toWrite;			/* How many bytes to write? */
    int *errorCodePtr;			/* Where to store error code. */
{
    FileState *fileState = (FileState *) instanceData;
    long length = toWrite;
    OSErr err;

    *errorCodePtr = 0;
    errno = 0;
    
    if (fileState->appendMode == true) {
	FileSeek(instanceData, 0, SEEK_END, errorCodePtr);
	*errorCodePtr = 0;
    }
    
    err = FSWrite(fileState->fileRef, &length, buffer);
    if (err == noErr) {
	err = FlushFile(fileState->fileRef);
    } else {
	*errorCodePtr = errno = TclMacOSErrorToPosixError(err);
	return -1;
    }
    return length;
}

/*
 *----------------------------------------------------------------------
 *
 * FileReady --
 *
 *	Called by the notifier to check whether events of interest are
 *	present on the channel.  On the Macintosh all files are always
 *	considered to be readable and writeable.
 *
 * Results:
 *	Returns OR-ed combination of TCL_READABLE, TCL_WRITABLE and
 *	TCL_EXCEPTION to indicate which events of interest are present.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static int
FileReady(instanceData, mask)
    ClientData instanceData;		/* The file state. */
    int mask;				/* Events of interest; an OR-ed
                                         * combination of TCL_READABLE,
                                         * TCL_WRITABLE and TCL_EXCEPTION. */
{
    return (TCL_READABLE | TCL_WRITABLE);
}

/*
 *----------------------------------------------------------------------
 *
 * FileSeek --
 *
 *	Seeks on an IO channel. Returns the new position.
 *
 * Results:
 *	-1 if failed, the new position if successful. If failed, it
 *	also sets *errorCodePtr to the error code.
 *
 * Side effects:
 *	Moves the location at which the channel will be accessed in
 *	future operations.
 *
 *----------------------------------------------------------------------
 */

static int
FileSeek(instanceData, offset, mode, errorCodePtr)
    ClientData instanceData;			/* Unused. */
    long offset;				/* Offset to seek to. */
    int mode;					/* Relative to where
                                                 * should we seek? */
    int *errorCodePtr;				/* To store error code. */
{
    FileState *fileState = (FileState *) instanceData;
    IOParam pb;
    OSErr err;

    *errorCodePtr = 0;
    pb.ioCompletion = NULL;
    pb.ioRefNum = fileState->fileRef;
    if (mode == SEEK_SET) {
	pb.ioPosMode = fsFromStart;
    } else if (mode == SEEK_END) {
	pb.ioPosMode = fsFromLEOF;
    } else if (mode == SEEK_CUR) {
	err = PBGetFPosSync((ParmBlkPtr) &pb);
	if (pb.ioResult == noErr) {
	    if (offset == 0) {
		return pb.ioPosOffset;
	    }
	    offset += pb.ioPosOffset;
	}
	pb.ioPosMode = fsFromStart;
    }
    pb.ioPosOffset = offset;
    err = PBSetFPosSync((ParmBlkPtr) &pb);
    if (pb.ioResult == noErr){
	return pb.ioPosOffset;
    } else if (pb.ioResult == eofErr) {
	long currentEOF, newEOF;
	long buffer, i, length;
	
	err = PBGetEOFSync((ParmBlkPtr) &pb);
	currentEOF = (long) pb.ioMisc;
	if (mode == SEEK_SET) {
	    newEOF = offset;
	} else if (mode == SEEK_END) {
	    newEOF = offset + currentEOF;
	} else if (mode == SEEK_CUR) {
	    err = PBGetFPosSync((ParmBlkPtr) &pb);
	    newEOF = offset + pb.ioPosOffset;
	}
	
	/*
	 * Write 0's to the new EOF.
	 */
	pb.ioPosOffset = 0;
	pb.ioPosMode = fsFromLEOF;
	err = PBGetFPosSync((ParmBlkPtr) &pb);
	length = 1;
	buffer = 0;
	for (i = 0; i < (newEOF - currentEOF); i++) {
	    err = FSWrite(fileState->fileRef, &length, &buffer);
	}
	err = PBGetFPosSync((ParmBlkPtr) &pb);
	if (pb.ioResult == noErr){
	    return pb.ioPosOffset;
	}
    }
    *errorCodePtr = errno = TclMacOSErrorToPosixError(err);
    return -1;
}

/*
 *----------------------------------------------------------------------
 *
 * FileWatch --
 *
 *	Initialize the notifier to watch Tcl_Files from this channel.
 *	This doesn't do anything on the Macintosh.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static void
FileWatch(instanceData, mask)
    ClientData instanceData;		/* The file state. */
    int mask;				/* Events of interest; an OR-ed
                                         * combination of TCL_READABLE,
                                         * TCL_WRITABLE and TCL_EXCEPTION. */
{
    Tcl_Time timeout = { 0, 0 };

    /*
     * Currently, files are always ready under the Macintosh,
     * so we just set a 0 timeout.  Since there s no notification
     * scheme - we just set the timeout time to zero.
     */

    Tcl_SetMaxBlockTime(&timeout);
}

/*
 *----------------------------------------------------------------------
 *
 * TclMacOSErrorToPosixError --
 *
 *	Given a Macintosh OSErr return the appropiate POSIX error.
 *
 * Results:
 *	A Posix error.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

int
TclMacOSErrorToPosixError(error)
    int error;	/* A Macintosh error. */
{
    switch (error) {
	case noErr:
	    return 0;
	case bdNamErr:
	    return ENAMETOOLONG;
	case afpObjectTypeErr:
	    return ENOTDIR;
	case fnfErr:
	case dirNFErr:
	    return ENOENT;
	case dupFNErr:
	    return EEXIST;
	case dirFulErr:
	case dskFulErr:
	    return ENOSPC;
	case fBsyErr:
	    return EBUSY;
	case tmfoErr:
	    return ENFILE;
	case fLckdErr:
	case permErr:
	case afpAccessDenied:
	    return EACCES;
	case wPrErr:
	case vLckdErr:
	    return EROFS;
	case badMovErr:
	    return EINVAL;
	case diffVolErr:
	    return EXDEV;
	default:
	    return EINVAL;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * GetOpenMode --
 *
 * Description:
 *	Computes a POSIX mode mask from a given string and also sets
 *	a flag to indicate whether the caller should seek to EOF during
 *	opening of the file.
 *
 * Results:
 *	On success, returns mode to pass to "open". If an error occurs, the
 *	returns -1 and if interp is not NULL, sets interp->result to an
 *	error message.
 *
 * Side effects:
 *	Sets the integer referenced by seekFlagPtr to 1 if the caller
 *	should seek to EOF during opening the file.
 *
 * Special note:
 *	This code is based on a prototype implementation contributed
 *	by Mark Diekhans.
 *
 *----------------------------------------------------------------------
 */

static int
GetOpenMode(interp, string)
    Tcl_Interp *interp;			/* Interpreter to use for error
					 * reporting - may be NULL. */
    char *string;			/* Mode string, e.g. "r+" or
					 * "RDONLY CREAT". */
{
    int mode, modeArgc, c, i, gotRW;
    char **modeArgv, *flag;

    /*
     * Check for the simpler fopen-like access modes (e.g. "r").  They
     * are distinguished from the POSIX access modes by the presence
     * of a lower-case first letter.
     */

    mode = 0;
    if (islower(UCHAR(string[0]))) {
	switch (string[0]) {
	    case 'r':
		mode = TCL_RDONLY;
		break;
	    case 'w':
		mode = TCL_WRONLY|TCL_CREAT|TCL_TRUNC;
		break;
	    case 'a':
		mode = TCL_WRONLY|TCL_CREAT|TCL_APPEND;
		break;
	    default:
		error:
                if (interp != (Tcl_Interp *) NULL) {
                    Tcl_AppendResult(interp,
                            "illegal access mode \"", string, "\"",
                            (char *) NULL);
                }
		return -1;
	}
	if (string[1] == '+') {
	    mode &= ~(TCL_RDONLY|TCL_WRONLY);
	    mode |= TCL_RDWR;
	    if (string[2] != 0) {
		goto error;
	    }
	} else if (string[1] != 0) {
	    goto error;
	}
        return mode;
    }

    /*
     * The access modes are specified using a list of POSIX modes
     * such as TCL_CREAT.
     */

    if (Tcl_SplitList(interp, string, &modeArgc, &modeArgv) != TCL_OK) {
        if (interp != (Tcl_Interp *) NULL) {
            Tcl_AddErrorInfo(interp,
                    "\n    while processing open access modes \"");
            Tcl_AddErrorInfo(interp, string);
            Tcl_AddErrorInfo(interp, "\"");
        }
        return -1;
    }
    
    gotRW = 0;
    for (i = 0; i < modeArgc; i++) {
	flag = modeArgv[i];
	c = flag[0];
	if ((c == 'R') && (strcmp(flag, "RDONLY") == 0)) {
	    mode = (mode & ~TCL_RW_MODES) | TCL_RDONLY;
	    gotRW = 1;
	} else if ((c == 'W') && (strcmp(flag, "WRONLY") == 0)) {
	    mode = (mode & ~TCL_RW_MODES) | TCL_WRONLY;
	    gotRW = 1;
	} else if ((c == 'R') && (strcmp(flag, "RDWR") == 0)) {
	    mode = (mode & ~TCL_RW_MODES) | TCL_RDWR;
	    gotRW = 1;
	} else if ((c == 'A') && (strcmp(flag, "APPEND") == 0)) {
	    mode |= TCL_ALWAYS_APPEND;
	} else if ((c == 'C') && (strcmp(flag, "CREAT") == 0)) {
	    mode |= TCL_CREAT;
	} else if ((c == 'E') && (strcmp(flag, "EXCL") == 0)) {
	    mode |= TCL_EXCL;
	} else if ((c == 'N') && (strcmp(flag, "NOCTTY") == 0)) {
	    mode |= TCL_NOCTTY;
	} else if ((c == 'N') && (strcmp(flag, "NONBLOCK") == 0)) {
	    mode |= TCL_NONBLOCK;
	} else if ((c == 'T') && (strcmp(flag, "TRUNC") == 0)) {
	    mode |= TCL_TRUNC;
	} else {
            if (interp != (Tcl_Interp *) NULL) {
                Tcl_AppendResult(interp, "invalid access mode \"", flag,
                        "\": must be RDONLY, WRONLY, RDWR, APPEND, CREAT",
                        " EXCL, NOCTTY, NONBLOCK, or TRUNC", (char *) NULL);
            }
	    ckfree((char *) modeArgv);
	    return -1;
	}
    }
    ckfree((char *) modeArgv);
    if (!gotRW) {
        if (interp != (Tcl_Interp *) NULL) {
            Tcl_AppendResult(interp, "access mode must include either",
                    " RDONLY, WRONLY, or RDWR", (char *) NULL);
        }
	return -1;
    }
    return mode;
}
