
/********************************************************************
 *
 *	File:		dirent.c
 *	Created:	7/3/93		By:	George T. Talbot
 *	Purpose:	Implements UNIX-like directory reading for the Macintosh.
 *
 *	Modifications:
 *
 *	Notes:
 *			1) These routines will NOT work under A/UX.
 *			2) WD = working directory
 *			3) CD = change directory
 *			4) FS = file system
 *			5) Mac filesystems allow spaces as part of pathnames!
 *			6) All routines which return a path use the default Macintosh path separator,
 *			   a colon (":").
 *
********************************************************************/

#include "dirent.h"
#include <string.h>
#include <Errors.h>
#include <Memory.h>
#include <Strings.h>

#define USEDOTDOT
#define USEUNIXTOMACPATH

#ifdef USEDOTDOT
#include "StringUtils.h"
#endif

#ifdef USEUNIXTOMACPATH
#include "DrvSystem.h"
#endif

OSErr	dd_errno;				/*	Global errno to check after calls to dirent routines	*/
char	*dd_separator = ":";	/*	If you're feeling brave, change this to "/"	*/
int		dd_xform_seps = true;

char *getwd(char *path);
char *pathdir(DIR *dirp, char *path);

/****************************************************************************************
 *
 *	This function, given a Macintosh-style pathname, will open a directory to that path.
 *	NOTES:	1)	passing in nil will get you the current directory.
 *			2)	".:", "..:" & "Å:" are supported at the beginning of paths ONLY
 *				by this routine.
 *			3)	"/" will be turned into ":" by this routine.
 *
 *	Calls:			PBHGetVol(), PBHGetCatInfo(), PBHSetVol(), hopendir(), CtoPstr()
 *	Called By:		<general purpose>
 *	Globals Used:	dd_errno
 *	Parameters:		pointer to C-string pathname or nil for current directory
 *	Returns:		pointer to directory management block or nil & dd_errno will be set
 *
 ****************************************************************************************/

DIR	*opendir(char *dirname)
	{
	WDPBRec			pb;
	CInfoPBRec		cpb;
	short			vRefNum;
	long			dirID;
	char			*dname;
	DIR				*temp;
	char			path_temp[MAXPATHLEN+1];	/*	Temporary area for building pathname	*/

	/*	Save the current path	*/
	pb.ioCompletion	= nil;
	pb.ioNamePtr	= nil;
	
	if ((dd_errno = PBHGetVol(&pb, false)) != 0)
		return nil;

	vRefNum	= pb.ioWDVRefNum;
	dirID	= pb.ioWDDirID;

	/*	dname points to the desired pathname	*/
	dname	= dirname;

	/*	If no pathname was passed in, or there are no ".", ".." or "Å" special directory
	 *	names, then handle the pathname as normal.
	 */
	if (dirname == nil)
		goto opendir_fallthrough;

	/*	If there's not '.', '..' or 'Å', fall through	*/
	if ((dirname[0] != '.') && (dirname[0] != 'Å'))
		goto opendir_fallthrough;

	/*	If there's a 'Å', treat it like '..'	*/
	if (dirname[0] == 'Å')
		{
		dname = &(dirname[1]);
		goto path_dotdot;
		}

	/*	If the pathname has "." (current directory) in front of it...	*/
	if (dirname[1] != '.')
		{
		/*	Skip over the "." and fall through	*/
		dname	= &(dirname[1]);
		goto opendir_fallthrough;
		}

	/*	Skip over the ".."	*/
	dname	= &(dirname[2]);

path_dotdot:
	/*	If we get here, the directory has ".." in front of it...	*/
	
	/*	First, get the directory info on the current directory.  We do this so
	 *	that we can get the directory's parent
	 */
	cpb.dirInfo.ioCompletion	= nil;
	cpb.dirInfo.ioNamePtr		= (StringPtr)path_temp;	/* Unused, but must be set because of
												 * bug in Apple File Sharing.
												 */
	cpb.dirInfo.ioVRefNum		= vRefNum;
	cpb.dirInfo.ioFDirIndex		= -1;
	cpb.dirInfo.ioDrDirID		= dirID;

	if ((dd_errno = PBGetCatInfo(&cpb, false)) != 0)
		return nil;

	/*	Temporarily CD to the parent directory	*/
	pb.ioCompletion				= nil;
	pb.ioNamePtr				= nil;
	pb.ioVRefNum				= pb.ioWDVRefNum;
	pb.ioWDDirID				= cpb.dirInfo.ioDrParID;
	
	if ((dd_errno = PBHSetVol(&pb, false)) != 0)
		return nil;

	/*	This is the common code for all three cases above	*/
opendir_fallthrough:
	/*	If the pathname is too long (this is a Macintosh FS constraint), then return	*/
	if (strlen(dname) > MAXPATHLEN)
		{
		/*	Set the error	*/
		dd_errno	= bdNamErr;
		temp		= nil;
		
		/*	Go to the common exit, where we CD back to the saved WD	*/
		goto opendir_exit;
		}

	/*	If this call was passed a pathname	*/
	if (dname != nil)
		{
		/*	Copy the pathname into a temp	*/
		strcpy(path_temp, dname);

#ifdef USEUNIXTOMACPATH
		if (dd_xform_seps)		
			UnixToMacPath(path_temp, MAXPATHLEN);
#endif	
		
		/*	Turn it into a Pascal string for the Mac FS	*/
		CtoPstr(path_temp);

		/*	Change any "/" to ":" for the Mac FS	*/
		if (dd_xform_seps)		
			{
#ifndef USEUNIXTOMACPATH
			int i;
			
			for (i=1; i<= path_temp[0]; ++i)
				if (path_temp[i] == '/')
					path_temp[i] = ':';
#endif					
			}

		/*	Try and open the directory	*/
		temp = hopendir(path_temp, 0, 0);
		}
	else
		/*	If this call wasn't passed a pathname, then we call hopendir() with nil to
		 *	tell it to open the current working directory.
		 */
		temp = hopendir(nil, 0, 0);

	/*	This is the common exit code which restores the current WD	*/
opendir_exit:
	pb.ioCompletion				= nil;
	pb.ioNamePtr				= nil;
	pb.ioVRefNum				= vRefNum;
	pb.ioWDDirID				= dirID;
	
	if ((dd_errno = PBHSetVol(&pb, false)) != 0)
		{
		/*	If this call failed, then get rid of the structures created by hopendir()	*/
		closedir(temp);
		return nil;
		}

	return temp;
	}

/****************************************************************************************
 *
 *	This function actually opens the directory.  If you feel brave, you can call it.
 *	If you pass in a dirname, then set vRefNum and dirID to 0.  All named opens are
 *	relative to the current WD.  If you pass in vRefNum and dirID, then don't bother
 *	passing in a name.  This routine WILL CHANGE YOUR CURRENT WORKING DIRECTORY!
 *
 *	Calls:			NewHandle(), PBHGetCatInfo(), PBHSetVol(), PtoCstr(), BlockMove(),
 *					DisposHandle(), MoveHHi(), HLock(), MemError()
 *	Called By:		opendir(), and you if you feel brave.
 *	Globals Used:	dd_errno
 *	Parameters:		pointer to Pascal-string pathname, vRefNum, dirID of desired
 *					directory.  If you pass in a WDRefNum as the vRefNum, set dirID to 0
 *	Returns:		pointer to directory management block or nil & dd_errno will be set
 *
 ****************************************************************************************/

DIR	*hopendir(char *dirname, short vRefNum, long dirID)
	{
	DIR				**curh, *cur;
	CInfoPBRec		cpb;
	WDPBRec			pb;
	Str63			name;

	/*	Get memory for the directory structure	*/
	curh	= (DIR **) NewHandle(sizeof(DIR));

	/*	Did we get it?	*/
	if (curh == nil)
		{
		dd_errno	= MemError();
		return nil;
		}

	/*	Move it high and lock it	*/
	MoveHHi((Handle) curh);
	HLock((Handle) curh);
	cur		= *curh;

	/*	If we're supposed to open anything but the current directory, set the current
	 *	working directory to the desired directory.
	 */
	if ((dirname != nil) || (vRefNum != 0) || (dirID != 0))
		{
		pb.ioCompletion				= nil;
		pb.ioNamePtr				= (StringPtr) dirname;
		pb.ioVRefNum				= vRefNum;
		pb.ioWDDirID				= dirID;
		
		if ((dd_errno = PBHSetVol(&pb, false)) != 0)
			goto failure_exit;
		}

	cur->dd_buf	= nil;
	
	/*	Get info on the desired directory (its name, etc.)	*/
	cpb.dirInfo.ioCompletion	= nil;
	cpb.dirInfo.ioNamePtr		= name;
	cpb.dirInfo.ioVRefNum		= vRefNum;
	cpb.dirInfo.ioFDirIndex		= -1;
	cpb.dirInfo.ioDrDirID		= dirID;
	
	if ((dd_errno = PBGetCatInfo(&cpb, false)) != 0)
		goto failure_exit;

	/*	Save the directory info	*/
	cur->dir_fsp.vRefNum	= vRefNum;
	cur->dd_fd				= cpb.dirInfo.ioDrDirID;
	cur->dd_parent			= cpb.dirInfo.ioDrParID;

	BlockMove(name, cur->dir_fsp.name, sizeof(Str63));

	/*	Convert the name to a C-style string	*/
	PtoCstr(cur->dir_fsp.name);

	/*	Set up our directory structure to read the first entry	*/
#ifdef USEDOTDOT	
	cur->dd_off				= 0;
	cur->dd_numents		= cpb.dirInfo.ioDrNmFls + 1;		/* Account for the .. */
#else
	cur->dd_off				= 1;
	cur->dd_numents			= cpb.dirInfo.ioDrNmFls;
#endif
	cur->dd_cached			= false;

	return cur;

	/*	This code is branched-to in case of error.  It frees up the memory and returns.	*/
failure_exit:
	DisposeHandle((Handle) curh);
	return nil;
	}

/****************************************************************************************
 *
 *	This function returns the index of the directory entry to be next read.
 *
 *	Calls:			nothing
 *	Called By:		<general purpose>
 *	Globals Used:	none
 *	Parameters:		pointer to the directory management block
 *	Returns:		index of the next directory entry to be read.
 *
 ****************************************************************************************/

long	telldir(DIR *dirp)
	{
	if (dirp->dd_off > dirp->dd_numents)
		return -1;
	else
#ifdef USEDOTDOT	
		return dirp->dd_off-1;
#else
		return dirp->dd_off-1;	/* The -1 is because Macs start at 1 & not 0 for dir index,
								 * and this is a little more POSIX.
								 */
#endif
	}

/****************************************************************************************
 *
 *	This function closes the directory opened with opendir() or hopendir()
 *
 *	Calls:			DisposHandle(), RecoverHandle()
 *	Called By:		<general purpose>
 *	Globals Used:	none
 *	Parameters:		pointer to the directory management block
 *	Returns:		0 (always successful)
 *
 ****************************************************************************************/

int	closedir(DIR *dirp)
	{
	struct dirent	**cur;
	
	/*	Dispose of any directory entries read in.	*/
	cur	= dirp->dd_buf;
	
	dd_errno	= noErr;

	while (cur)
		{
		struct dirent	**next;
		
		next	= (*cur)->next;
		
		DisposHandle((Handle) cur);
		
		if (dd_errno == noErr)
			dd_errno	= MemError();

		cur		= next;
		}

	/*	Dispose of the directory managment block	*/
	DisposHandle(RecoverHandle((Ptr) dirp));

	if (dd_errno == noErr)
		dd_errno	= MemError();

	return dd_errno?-1:0;
	}

/****************************************************************************************
 *
 *	This function sets the index of the next-read directory entry.  It will also search
 *	the list of read entries so that an entry won't be read from disk more than once.
 *
 *	Calls:			nothing
 *	Called By:		<general purpose>
 *	Globals Used:	none
 *	Parameters:		pointer to the directory management block, index of directory
 *	Returns:		nothing
 *
 ****************************************************************************************/

void	seekdir(DIR *dirp, long loc)
	{
	struct dirent	**cur;

#ifdef USEDOTDOT	
	dirp->dd_off		= loc+1;		/* Index to the next directory */
#else
	dirp->dd_off		= loc+1;	/* The +1 is because the Mac indexes directories
									 		* from 1 and not 0 and we want to be a little bit
									 		* POSIX									 		*/
#endif
	/*	Search through the entries that we've read already	*/
	cur	= dirp->dd_buf;
	
	while (cur)
		{
		/*	If we find the entry that we've seeked to, set up so that readdir() will
		 *	return this one instead of reading a new one.
		 */
		if (loc == (*cur)->d_off)
			{
			dirp->dd_cached		= true;
			dirp->dd_cache_hint	= cur;

			return;
			}

		cur	= (*cur)->next;
		}

	/*	If we didn't find it, then tell readdir() to get the entry from the FS	*/
	dirp->dd_cached	= false;
	}

/****************************************************************************************
 *
 *	This function will read the next directory entry from disk.  It will return nil and
 *	set dd_errno to noErr when the end of the directory is reached.  It will avoid
 *	reading directory entries from disk more than once.
 *
 *	Calls:			nothing
 *	Called By:		<general purpose>
 *	Globals Used:	none
 *	Parameters:		pointer to the directory management block
 *	Returns:		pointer to directory entry or nil if an error occurred and dd_errno
 *					will be set.  If the last entry has already been read, this will
 *					return nil and dd_errno will be set to noErr.
 *
 ****************************************************************************************/

struct dirent	*readdir(DIR *dirp)
	{
	CInfoPBRec		cpb;
	struct dirent	**meh, *me;
	
	/*	If the entry has been read already, then return the already present entry	*/
	if (dirp->dd_cached)
		me	= *(dirp->dd_cache_hint);
	else
		/*	Otherwise, read it from disk...	*/
		{
		/*	Past the end of the directory?	*/
		if (dirp->dd_off > dirp->dd_numents)
			{
			dd_errno	= noErr;
			return nil;
			}

		/*	Allocate space for a new entry	*/
		meh	= (struct dirent **) NewHandle(sizeof(struct dirent));
		
		/*	Enough memory?	*/
		if (meh == nil)
			{
			dd_errno	= MemError();
			return nil;
			}

		/*	Lock the entry	*/
		MoveHHi((Handle) meh);
		HLock((Handle) meh);

		me	= *meh;

		/*	Get the entry's info from disk	*/
		me->fsp.name[0]				= 0;

		cpb.dirInfo.ioCompletion	= nil;
		cpb.dirInfo.ioNamePtr		= me->fsp.name;
		cpb.dirInfo.ioVRefNum		= dirp->dir_fsp.vRefNum;
		cpb.dirInfo.ioFDirIndex		= dirp->dd_off;
		cpb.dirInfo.ioDrDirID		= dirp->dd_fd;

#ifdef USEDOTDOT	
		if (cpb.dirInfo.ioFDirIndex == 0) {
			cpb.dirInfo.ioResult = noErr;
			pstrcpy(cpb.dirInfo.ioNamePtr, "\p..");
		}
		else
#endif		
		if ((dd_errno = PBGetCatInfo(&cpb, false)) != 0)
			{
			DisposHandle((Handle) meh);
			return nil;
			}
	
		/*	Set up the dirent structure	*/
#ifdef USEDOTDOT
		me->d_off			= dirp->dd_off-1;
#else
		me->d_off			= dirp->dd_off-1;
#endif	
		me->fsp.vRefNum		= cpb.dirInfo.ioVRefNum;
		me->d_fileno		= cpb.dirInfo.ioDrDirID;
		me->d_parent		= cpb.dirInfo.ioDrParID;
		
		/*	C strings only!	*/
		PtoCstr(me->fsp.name);

		/*	Add it to the list for this directory	*/
		me->next			= dirp->dd_buf;
		
		dirp->dd_buf		= meh;
		}

	/*	Seek to the next entry	*/
	seekdir(dirp, dirp->dd_off);

	/*	Return what we've found	*/
	return me;
	}

/****************************************************************************************
 *
 *	This function will give an absolute pathname to a given directory.
 *
 *	Calls:			NewPtr(), DisposPtr(), PBGetCatInfo()
 *	Called By:		<general purpose>
 *	Globals Used:	none
 *	Parameters:		vRefNum and startDirID of desired path, pointer to path name storage,
 *					length of path name storage, pointer to C-string separator.
 *	Returns:		bdNamErr if the path would overflow the storage,
 *					some other error code if something else happened,
 *					or noErr on success.
 *
 ****************************************************************************************/

OSErr	hgetwd(short vRefNum, long startDirID, char *path, int max_path_len, char *sep)
	{
	long		curDirID;
	OSErr		err;
	CInfoPBRec	pb;
	Str63		name;
	char		*temp_path;

	/*	Start with an empty path	*/
	path[0]	= 0;

	/*	Get memory for a temporary path	*/
	temp_path	= (char *) NewPtr(max_path_len);
	
	if (temp_path == nil)
		return MemError();

	/*	Start at the given directory	*/
	curDirID	= startDirID;

	do	{
		/*	Get cat info for the current directory	*/
		name[0]	= 0;

		pb.dirInfo.ioCompletion	= nil;
		pb.dirInfo.ioNamePtr	= name;
		pb.dirInfo.ioVRefNum	= vRefNum;
		pb.dirInfo.ioFDirIndex	= -1;
		pb.dirInfo.ioDrDirID	= curDirID;
		
		if ((err = PBGetCatInfo(&pb, false)) != 0)
			{
			DisposPtr((Ptr) temp_path);
			return err;
			}

		/*	Convert name to a C string	*/
		PtoCstr(name);

		/*	Check that we don't overflow storage	*/
		if ((strlen((char *)name) + strlen(path) + strlen(sep)) >= max_path_len)
			{
			DisposPtr((Ptr) temp_path);
			return bdNamErr;
			}

		/*	Prepend the name and separator	*/
		strcpy(temp_path, path);
		strcpy(path, (char *)name);
		strcat(path, sep);
		strcat(path, temp_path);

		/*	Move "up" one directory	*/
		curDirID	= pb.dirInfo.ioDrParID;
		}
	/*	Until we hit the root directory	*/
	while (pb.dirInfo.ioDrDirID != fsRtDirID);

	/*	Get rid of our temp storage and return	*/
	DisposPtr((Ptr) temp_path);

	return MemError();
	}

/****************************************************************************************
 *
 *	This function will change the current working directory.
 *
 *	Calls:			opendir(), closedir(), PBHSetVol()
 *	Called By:		<general purpose>
 *	Globals Used:	none
 *	Parameters:		C-string pathname.
 *	Returns:		-1 on failure, 0 on success.  Sets dd_errno on failure.
 *
 ****************************************************************************************/
#if LATER			/* This function is implemented by CW6 unix features */
int	chdir(char *path)
	{
	DIR		*d;
	short	vRefNum;
	long	dirID;
	WDPBRec	pb;

	/*	Open the directory	*/
	d	= opendir(path);
	
	if (d == nil)
		return -1;

	/*	Get the Mac FS identification for this directory	*/
	vRefNum	= d->dd_volume;
	dirID	= d->dd_fd;
	
	/*	Close the directory	*/
	closedir(d);

	/*	CD to the new directory	*/
	pb.ioCompletion	= nil;
	pb.ioNamePtr	= nil;
	pb.ioVRefNum	= vRefNum;
	pb.ioWDDirID	= dirID;
	
	dd_errno = PBHSetVol(&pb, false);
	
	return dd_errno?-1:0;
	}
#endif 		/* LATER */


/****************************************************************************************
 *
 *	This function will get the current working directory's path.
 *
 *	Calls:			PBHGetVol(), hgetwd()
 *	Called By:		<general purpose>
 *	Globals Used:	none
 *	Parameters:		pointer to a buffer of MAXPATHLEN bytes.
 *	Returns:		pointer to the buffer on success, on failure, nil and dd_errno will
 *					be set.
 *
 ****************************************************************************************/

char *getwd(char *path)
	{
	WDPBRec	pb;

	/*	Get the current working directory	*/
	pb.ioCompletion	= nil;
	pb.ioNamePtr	= nil;
	
	if ((dd_errno = PBHGetVol(&pb, false)) != 0)
		return nil;

	/*	Transform it into a path	*/
	if ((dd_errno = hgetwd(pb.ioWDVRefNum, pb.ioWDDirID, path, MAXPATHLEN-1, dd_separator)) != 0)
		return nil;

	return path;
	}

/****************************************************************************************
 *
 *	This function will get the path to a given (already opened) directory.
 *
 *	Calls:			hgetwd()
 *	Called By:		<general purpose>
 *	Globals Used:	none
 *	Parameters:		pointer to a buffer of MAXPATHLEN bytes.
 *	Returns:		pointer to the buffer on success, on failure, nil and dd_errno will
 *					be set.
 *
 ****************************************************************************************/

char *pathdir(DIR *dirp, char *path)
	{
	if ((dd_errno = hgetwd(dirp->dd_volume, dirp->dd_fd, path, MAXPATHLEN-1, dd_separator)) != 0)
		return nil;

	return path;
	}
