ftp.nice.ch/pub/next/graphics/convertors/Convert_MacPaint.NIHS.bs.tar.gz#/Convert_MacPaint/Source/shared.subproj/File.m

This is File.m in view mode; [Download] [Up]

/***********************************************************************\
Common class for accessing files in all Convert programs
Copyright (C) 1993 David John Burrowes

This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 1, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

The author, David John Burrowes, can be reached at:
	davidjohn@kira.net.netcom.com
	David John Burrowes
	1926 Ivy #10
	San Mateo, CA 94403-1367
\***********************************************************************/

/*
====================================================================
This is the implementation file for the File class.  Full documentation for this class can be found in the File.rtf file.  I will not duplicate all that fine information here.
	This is $Revision: 1.8 $ of this file
	It was last modified by $Author: death $ on $Date: 93/04/04 23:44:26 $
Note that this file was created while using the New Century Schoolbook Roman typeface.  You may find that some things line up strangely if you don't use that family.
 * $Log:	File.m,v $
Revision 1.8  93/04/04  23:44:26  death
Sun Apr  4 23:44:26 PDT 1993

Revision 1.7  93/01/10  15:07:54  death
Sun Jan 10 15:07:54 PST 1993

Revision 1.6  92/07/26  13:58:06  death
Update so all works with the font converter...


Revision 1.4  92/04/05  22:51:36  death
Miscelaneous revisions.  This is the last version of version 1.

Revision 1.3  92/03/29  12:36:14  death
Bug in the open method. If we failed, we returned OK, if we succed, we reported failure.

Revision 1.2  92/03/29  12:29:06  death
Oops.  Managed to check in the wrong version.  this one has the code reflecting the new location of the init methods 

Revision 1.1  92/03/29  12:18:44  death
Initial revision

 *====================================================================
*/

//
//	Import our own definition
//
#import "File.h"

#import <stdio.h>	
#import <string.h>
#import <stdlib.h>
#import <sys/param.h>	// for maxpathlen
#import <libc.h>			// for getwd
#import <sys/file.h>		// for open()
#import	<streams/streams.h>	// for streams stuff (exception codes)
#include <sys/time.h>		// for gettimeofday
#if (NSmajor == 3)
#	import	<errno.h>
#endif

@implementation File

//
//	@@@ bugs... doesn't deal with links properly.  =(
//

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		CreateAndOpenFor
//	Parameters:	the means of accessing the specified file
//	Returns:		self
//	Stores:		none
//	Description:
//		This method will try to create a new file and open it.  The file is presumed
//		to be the one whose path is stored in the instance variables of this object.
//		If the file already exists, or if an error occurrs when trying to creat it,
//		an error is stored.  Otherwise the file is opened, and the object is ready for use.
//	Bugs:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- CreateAndOpenFor: (AccessType) access
{
	//
	//	If the object is working with a file already give an error.
	//	otherwise, do our stuff.
	//
	if (TheFile != NULL)
		[self StoreErrorCode: ERR_SOMEFILEOPEN
			AndText: "Object already in use for another file."];
	{
		if ([self FileExists: FileName] == YES)
			[self StoreErrorCode: ERR_FILEEXISTS
				AndText: "Can't create the requested file. It already exists."];
		else
		{
			[self CreateFile: FileName];
			if ([self GetErrorCode] != ERR_OK)
				[self StoreErrorCode: ERR_CREATEFAIL
					AndText: "Can't create the requested file."];
			else
				[self OpenWithAccess: access];
				// Error info stored by this method.
		}
	}
	return self;
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		OpenExistingFor
//	Parameters:	the means of accessing the specified file
//	Returns:		self
//	Stores:		none
//	Description:
//		This method will try to open an existing file.  The file is presumed
//		to be the one whose path is stored in the instance variables of this object.
//		If the file does not exist,  an error is stored. Otherwise the file is opened,
//		and the object is ready for use.
//	Bugs:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- OpenExistingFor: (AccessType) access
{
	//
	//	If the object is working with a file already give an error.
	//	otherwise, do our stuff.
	//
	if (TheFile != NULL)
		[self StoreErrorCode: ERR_SOMEFILEOPEN
			AndText: "Object already in use for another file."];
	{
 		if ([self FileExists: FileName] == NO)
			[self StoreErrorCode: ERR_FILEDOESNTEXIST
				AndText: "Can't locate the requested file."];
		else
			[self OpenWithAccess: access];
			// Error info stored by this method.
	}
	return self;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		ClearAndOpenExistingFor
//	Parameters:	the means of accessing the specified file
//	Returns:		self
//	Stores:		none
//	Description:
//		This method will try to open an existing fil, and then clear (truncate) it.
//		The file is presumed to be the one whose path is stored in the instance
//		variables of this object. If the file does not exist, or if an error occurrs while
//		clearing it,  an error is stored. Otherwise the file is opened,
//		and the object is ready for use.
//	Bugs:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- ClearAndOpenExistingFor: (AccessType) access
{
	//
	//	If the object is working with a file already give an error.
	//	otherwise, do our stuff.
	//
	if (TheFile != NULL)
		[self StoreErrorCode: ERR_SOMEFILEOPEN
			AndText: "Object already in use for another file."];
	{
 		if ([self FileExists: FileName] == NO)
			[self StoreErrorCode: ERR_FILEDOESNTEXIST
				AndText: "Can't locate the requested file."];
		else
		{
			[self ClearFile: FileName];
			if ([self GetErrorCode] != ERR_OK)
				[self StoreErrorCode: ERR_CLEARFAIL
					AndText: "Can't clear the file!"];
			else
				[self OpenWithAccess: access];
				// Error info stored by this method.
		}
	}
	return self;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		ClearAndOpenFor
//	Parameters:	the means of accessing the specified file
//	Returns:		self
//	Stores:		none
//	Description:
//		This method will try to open a file.  If it doesn't exist already, it will create it.
//		if it does, then it will clear it (truncate).  The file is then opened if all went well
//		The file is presumed to be the one whose path is stored in the instance
//		variables of this object. 
//	Bugs:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- ClearAndOpenFor: (AccessType) access
{
	//
	//	If the object is working with a file already give an error.
	//	otherwise, do our stuff.
	//
	if (TheFile != NULL)
		[self StoreErrorCode: ERR_SOMEFILEOPEN
			AndText: "Object already in use for another file."];
	{
 		if ([self FileExists: FileName] == NO)
		{
			[self CreateFile: FileName];
			if ([self GetErrorCode] != ERR_OK)
				[self StoreErrorCode: ERR_CREATEFAIL
					AndText: "Can't create the requested file."];
		}
		else
		{
			[self ClearFile: FileName];
			if ([self GetErrorCode] != ERR_OK)
				[self StoreErrorCode: ERR_CLEARFAIL
					AndText: "Can't clear the file!"];
		}
		//
		//	The file exists and is of 0 length.  open it now.
		//
		if ([self GetErrorCode] == ERR_OK)
			[self OpenWithAccess: access];
	}
	return self;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		OpenFor
//	Parameters:	the means of accessing the specified file
//	Returns:		self
//	Stores:		none
//	Description:
//		This method will try to open a file.  If it doesn't exist already, it will create it.
//		The file is then opened if all went well.
//		The file is presumed to be the one whose path is stored in the instance
//		variables of this object. 
//	Bugs:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- OpenFor: (AccessType) access
{
	//
	//	If the object is working with a file already give an error.
	//	otherwise, do our stuff.
	//
	if (TheFile != NULL)
		[self StoreErrorCode: ERR_SOMEFILEOPEN
			AndText: "Object already in use for another file."];
	{
 		if ([self FileExists: FileName] == NO)
		{
			[self CreateFile: FileName];
			if ([self GetErrorCode] != ERR_OK)
				[self StoreErrorCode: ERR_CREATEFAIL
					AndText: "Can't create the requested file."];
		}
		else
				[self StoreErrorCode: ERR_OK  AndText: "File exists.  all is peachy."];
		//
		//	The file exists.  open it now.
		//
		if ([self GetErrorCode] == ERR_OK)
			[self OpenWithAccess: access];
	}
	return self;
}



//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		OpenWithAccess:
//	Parameters:	the means of accessing the specified file
//	Returns:		self
//	Stores:		none
//	Description:
//		This is a lower level opening routine.  It should probably never be called from
//		the outside, but instead be called by the (presently) five opening routines
//		above this in the listing.
//		It's purpose is simple: open the file that this object is going to work with.
//		If the file does'nt exist, it returns an error.  Thus, it is up to the caller to
//		assure that the file exists and is in th proper form before calling this.
//	Bugs:
//	Not accounting for the fact that I may not have *permission* (666 kinda thing)
//	to do any of these...
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- OpenWithAccess: (AccessType) operation
{
	struct stat *statBuf;
	[self ResetResults];
	
	//
	//	Make sure that the file exists, and that we aren't already working with
	//	an opened file.  If all is good, open it appropriately
	//
	if (([self FileExists: FileName] == YES) && (TheFile == NULL))
	{
		switch (operation)
		{
			case FILE_READ:
				TheFile = NXMapFile(FileName, NX_READONLY);
				break;
			case FILE_WRITE:
				TheFile = NXMapFile(FileName, NX_WRITEONLY);
				break;
			case	FILE_READWRITE:
				TheFile = NXMapFile(FileName, NX_READWRITE);
				break;
			case FILE_APPEND:
				TheFile = NXMapFile(FileName, NX_WRITEONLY);
				//
				//	Appending must be at the end of the file...
				//	@@@ do we need + 1 here?
				//
				[self MoveTo: [self FileSize]];
				break;
			default:
				[self StoreErrorCode: ERR_BADACCESS
					AndText: "An invalid access operation was requested. No file opened."];
				break;
		}
	}
	//
	//	If the file wasn't opened, or some other problem arose, generate a negative reply.
	//
	if ((TheFile == NULL) && ([self GetErrorCode] != ERR_BADACCESS))
		[self StoreErrorCode: ERR_CANTOPEN
				AndText: "The specified file could not be opened (bad permissions?  Bad path?)"];
	else
	{
		AccessMode = operation;
		//
		//	Store the real inode now, in case we could not get it during the init.
		//
		[self	FileInfo];
		if ([self GetErrorCode] != ERR_OK)
			TheInode = 0;
		else
		{
			statBuf = (struct stat *) [self GetPointerFrom: SECOND_RESULT];
			TheInode = statBuf->st_ino;
		}
	}
	//
	//	Store whether the file was opened or not
	//
	if ((TheFile != NULL) && ([self GetErrorCode] == ERR_OK))
	{
		FileIsOpen = YES;
		FileLocation = fileAtStart;
	}

	return self;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		initAndUse:
//	Parameters:	none
//	Returns:		self
//	Stores:		none
//	Description:
//		This funky method is a pretty standard init method.  It sets up all our instance
//		variables, and does nothing more.
//	Bugs:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- initAndUse: (roCString) pathname
{
	struct stat *statbuf;
	
	[super init];
	FileName = [self GetFullPathFrom: pathname];
	AccessMode = FILE_NOACCESS;
	TheFile = NULL;
	//
	//	Get our inode, if possible.
	//
	[self	FileInfo];
	if ([self GetErrorCode] != ERR_OK)
	{
		//  @@@ Maybe this shouldn't be this way...
		[self   StoreErrorCode: ERR_OK  AndText: "Hmmmmmm"];
		TheInode = 0;
	}
	else
	{
		statbuf = (struct stat *) [self GetPointerFrom: SECOND_RESULT];
		TheInode = statbuf->st_ino;
	}

	FileIsOpen = NO;
	FileLocation = fileAtStart;

	return self;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		initAndUseTemporary
//	Parameters:	none
//	Returns:		self
//	Stores:		none
//	Description:
//		This method is used when one wishes to have a temporary file to use.
//		Calling this method will cause a temporary file to be created somwhere.
//		one can then use it as a completely ordinary file with this object.
//	Bugs:
//		At the moment, this is implemented with the tmpname call.  Because we
//		ignore the TMP_MAX constant (the object shouldn't be tracking how many
//		temp files are open in the program, and the program shouldn't know we're
//		making use of tmpnam), we run the risk of opening too many temp files.
//		Thus, this should be re-implemented ourselves sometime (perhaps to:
//		/tmp/FileObj_TempXXXXXXXX, where X is a number-thing that will actually
//		be a date from a date object, or some something more unique...
//	History
//		93.01.03	djb	Added call to stat, because it seems you have to call tmpnam repeatedly
//					to get your unique name.  YICK!  Please move to the above.  Then
//					replaced with intermediate hack using a pretty unique name using the
//					above and the process id, seconds since 1970, and microseconds.
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- initAndUseTemporary
{
	struct timeval tp;
	struct timezone tzp;
	[super init];

	FileName = NewCString(63);
	gettimeofday(&tp, &tzp);
	//	93.01.24	djb	Added the 'long' designation to the %d's at the NS3 compiler's suggestion.
	sprintf(FileName, "/tmp/TempFileDataP%dS%ldU%ld",
		getpid(), tp.tv_sec, tp.tv_usec);

/*
	This approach does not work.  Nor do previsu simpler ones.
	FileName = NewCString(L_tmpnam);
	ctr = 0;
	do
	{
		FileName = tmpnam(FileName);
		result = stat(FileName, &mystat);
		ctr++;
	}
	while ((errno != 2) && (ctr < TMP_MAX));
*/

	AccessMode = FILE_NOACCESS;
	TheFile = NULL;
	FileIsOpen = NO;
	FileLocation = fileAtStart;
	return self;
}


//============================================================
//============================================================


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		ClearFile:
//	Parameters:	a CString that contains a path to a file
//	Returns:		self
//	Stores:		none
//	Description:
//		This little kludgey method just clears the specified file.  If anything bad
//		happens, it stores an error code and text.  At the moment, it truncates the file
//		by opening it with the O_TRUNC flag to the open() routine.  yech.  This
//		implementation had BETTER remain transparent to this and all subclasses,
//		for it's sure to change at some point!
//	Bugs:
//		Ickly implementation.
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- ClearFile: (CString) pathname
{
	int fd;
	
	errno = 0;
	fd =open(pathname, O_RDONLY|O_TRUNC, 666);
	if (errno != 0)
		[self StoreErrorCode: ERR_CLEARFAIL AndText: "The open() routine didn't O_TRUNC it!"];
	close (fd);
	return self;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		CreateFile:
//	Parameters:	a CString that contains a path to a file
//	Returns:		self
//	Stores:		none
//	Description:
//		This little kludgey method just creates the specified file.  If anything bad
//		happens, it stores an error code and text.  At the moment, it creates the file
//		by opening it with the O_CREAT flag to the open() routine.  yech.  This
//		implementation had BETTER remain transparent to this and all subclasses,
//		for it's sure to change at some point!
//	Bugs:
//		Ickly implementation.
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- CreateFile: (CString) pathname
{
	int fd;
	
	errno = 0;
	fd =open(pathname, O_RDONLY|O_CREAT, 0666);
	if (errno != 0)
		[self StoreErrorCode: ERR_CREATEFAIL AndText: "The open() routine didn't create it!"];
	else
		[self StoreErrorCode: ERR_OK AndText: "The file was created"];
	close (fd);
	return self;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		FileExists: 
//	Parameters:	a CString that contains a path to a file
//	Returns:		True if the file exists
//	Stores:		the return value
//	Description:
//		This method has only one purpose: it returns YES if the specified file
//		exists, and NO otherwise.  We determine this by whether stat() reports no
//		errors when trying to get info about the file.
//	Bugs:
//		Also, we always return an ERR_OK value...  certianly *something* can go wrong?
//		stat() is sensitifive to 8 bit character names.  All I can say is that I find this
//		to be an unacceptable drawback.  However, I haven't the time or energy right
//		now to get around this (and for all I know, the doc is just wrong).  
//	History
//		93.07.18	djb	Revised to use stat(), finally!
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (Boolean) FileExists: (CString) pathname
{
	struct stat statBuf;
	Integer	statCode;

	statCode = stat(pathname, &statBuf);
	if (statCode != 0)
	{
		[self StoreErrorCode: ERR_OK AndText: "The file does not exist or can not be reached"];
		return NO;
	}
	else
	{
		[self StoreErrorCode: ERR_OK AndText: "The file does indeed exist"];
		return YES;
	}
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		GetFullPathFrom
//	Parameters:	a CString that contains a full or partial path to a file
//	Returns:		a new CString containing the full path to the file
//	Stores:		the new CString
//	Description:
//		The purpose of this string is to take a full or partial pathname (i.e.
//		/me/Library/file.ext  or Library/file.ext or file.ext), and return the full
//		pathname to the file.
//		If we were passed a full pathname (starts with the / character), then we
//		make a full copy of it.
//		otherwise, use the function getwd to get the path to the current directory,
//		and fuse the two together.
//		Note that we do not reset the results here.  Since this is an internal
//		method, the caller, in the object, may not want things cleared.
//	Bugs:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (CString) GetFullPathFrom: (roCString) pathname
{
	char	rootedpath[MAXPATHLEN];
	CString	fullpath;
	char*	pathresult;

	//
	//	If the path is a complete path, then just copy it literally.
	//
	if (pathname[0] == '/')
	{
		fullpath = (CString) malloc(strlen(pathname) + 1);
		strcpy(fullpath, pathname);
		[self StoreErrorCode: ERR_OK AndText: "Got filename!"];
	}
	else
	{
		//
		//	Otherwise, we have a partial path.  Get path to our current location
		//	(presumably where the partial path starts from), and fuse the two
		//	(if getwd() returns an error, something is wrong, and so we risk returning
		//	just the partial path)
		//
	 	pathresult = getwd(rootedpath);
		if (*pathresult != 0)
		{
			fullpath = (CString) malloc(strlen(rootedpath)+1+strlen(pathname) + 1);
			sprintf(fullpath, "%s/%s", rootedpath, pathname);
			[self StoreErrorCode: ERR_OK AndText: "Got filename!"];
		}
		else
		{
			fullpath = (CString) malloc(strlen(pathname) + 1);
			strcpy(fullpath, pathname);
			[self StoreErrorCode: ERR_STRANGEPATH AndText: "Pathname may be corrupted!"];
		}
	}
	return fullpath;
}

//============================================================
//============================================================


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		Close
//	Parameters:	a flag to indicate whether we should save the file
//	Returns:		self
//	Stores:		none
//	Description:
//		This closes the file, discarding any changes.
//	Bugs:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- Close
{
	[self ResetResults];

	if (TheFile != NULL)
	{
		NXCloseMemory(TheFile, NX_FREEBUFFER);
		[self StoreErrorCode: ERR_OK AndText: "Closed right up"];
	}
	else
		[self StoreErrorCode: ERR_FILENOTOPEN AndText: "No file open to close!"];

	FileIsOpen = NO;
	FileLocation = fileAtStart;

	return self;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		CloseAndSave
//	Parameters:	none
//	Returns:		self
//	Stores:		none
//	Description:
//		This closes the file,saving its contents before doing so.
//	Bugs:
//		how about some checking to make sure we dumped it all OK??
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- CloseAndSave
{
	[self ResetResults];

	if (TheFile != NULL)
	{
		if (AccessMode != FILE_READ)
			NXSaveToFile(TheFile, FileName);
		NXCloseMemory(TheFile, NX_FREEBUFFER);
		[self StoreErrorCode: ERR_OK AndText: "Closed right up"];
	}
	else
		[self StoreErrorCode: ERR_FILENOTOPEN AndText: "No file open to close!"];

	FileIsOpen = NO;
	FileLocation = fileAtStart;

	return self;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		CloseAndDelete
//	Parameters:	none
//	Returns:		self
//	Stores:		the error we got back from the system routine.  (don't depend on this)
//	Description:
//		This closes the file, and then deletes it.
//	Bugs:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- CloseAndDelete
{
	Integer status;
	[self ResetResults];
	
	[self Close];
	status = remove(FileName);
	if (status != 0)
	{
		[self StoreErrorCode: ERR_CANTDELETE AndText: "Could not delete file. unknown why"];
		[self StoreInteger: status];
	}
	else
		[self StoreErrorCode: ERR_OK AndText: "Deleted the file just fine"];

	FileIsOpen = NO;
	FileLocation = fileAtStart;

	return self;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		free:
//	Parameters:	none
//	Returns:		self
//	Stores:		none
//	Description:
//		This frees the object, which involves closing our connection to the physical
//		file, WITHOUT SAVING, and then disposing of our instance variables,
//		and then freeing our parent.
//	Bugs:
//		The call to close is commented out here because it's causing trouble if you
//		say: [myFile CloseAndSave] ;  [myFile free];  Streams problem.  we SHOULD
//		be somehow nulling the stream, in the Close methods, and closing here only if
//		it isn't null.
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- free
{
	//[self Close];
	free(FileName);
	return [super free];
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		AdvanceBytes:
//	Parameters:	the number of bytes we should advance
//	Returns:		self
//	Stores:		none
//	Description:
//		This changes our current position in the file by advancing us up a specified number
//		of bytes in it. We add an exception handler partially for the ease so we don't
//		have to do the error checking ourselves.  Partially because it allows us to
//		check for other exceptions and kinda trap for them.  And partially for the
//		novelty of it.
//	Bugs:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- AdvanceBytes: (FilePosDelta) byteLoc;
{
	[self ResetResults];

	if (TheFile == NULL)
		[self StoreErrorCode: ERR_FILENOTOPEN  AndText: "The file wasn't even open!"];
	else
	{
		//
		//	Try to advance the requested number of bytes.  If an exception occurrs,
		//	and it's a seek error, just stick us at the end quietly.  Otherwise, store
		//	an error code and leave things as they are.  
		//
		NX_DURING
			NXSeek(TheFile, byteLoc, NX_FROMCURRENT);
			[self StoreErrorCode: ERR_PEACHY AndText: "We found what we NXsaught."];
		NX_HANDLER
			switch(NXLocalHandler.code)
			{
				case NX_illegalSeek:
					NXSeek(TheFile, 0, NX_FROMEND);
					[self StoreErrorCode: ERR_OK AndText: "Moved to the end."];
					FileLocation = fileAtEOF;
					break;
				default :
					[self StoreErrorCode: ERR_BADADVANCE
						AndText: "A unknown exception occurred when advancing."];
					break;
			}
		NX_ENDHANDLER
	}
	return self;
}



//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		BackupBytes:
//	Parameters:	the number of bytes we should backup
//	Returns:		self
//	Stores:		none
//	Description:
//		This changes our current position in the file by backing us up a specified number
//		of bytes in it.  We set up an exception handler to deal with the case if the
//		user tries to seek before the beginning.  We could, perhaps, do the traping
//		ourselves (compute current loc, distance from beginning, whether the byteLoc
//		falls within that request... etc), but this seems easier, and deals with any other
//		unexpected exceptions too.
//	Bugs:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- BackupBytes: (FilePosDelta) byteLoc;
{
	[self ResetResults];

	if (TheFile == NULL)
		[self StoreErrorCode: ERR_FILENOTOPEN  AndText: "The file wasn't even open!"];
	else
	{
		//
		//	Try to backup the requested number of bytes.  If an exception occurrs,
		//	and it's a seek error, just stick us at the beginning quietly.  Otherwise, store
		//	an error code and leave things as they are.  
		//
		NX_DURING
			NXSeek(TheFile, -byteLoc, NX_FROMCURRENT);
			[self StoreErrorCode: ERR_OK AndText: "We found what we NXsaught."];
		NX_HANDLER
			switch(NXLocalHandler.code)
			{
				case NX_illegalSeek:
					NXSeek(TheFile, 0, NX_FROMSTART);
					[self StoreErrorCode: ERR_OK AndText: "Moved to beginning."];
					FileLocation = fileAtEOF;
					break;
				default :
					[self StoreErrorCode: ERR_BADBACKUP
						AndText: "A unknown exception occurred when backing up."];
					break;
			}
		NX_ENDHANDLER
	}
	return self;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		GetBasename
//	Parameters:	none
//	Returns:		the copy of a string containing the current filename's basename
//	Stores:		A pointer to the same string we return
//	Description:
//		This simply  extracts the basename of the current file name.  The basename is
//		the part of the name that comes after thedirectory names, but before the extension.
//		If we are using the file  /foo/bar/hotfile.rtf, this returns hotfile
//	Bugs:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (CString) GetBasename
{
	CString	lastSlash, lastDot, tempName, finish, start;
	Integer	nameSize;
	[self ResetResults];
	//
	//	Determine the length of the basename by figuring out where its start is
	//	(either right after the /, or at the start) and the end (before . or at the end)
	//
	lastSlash = strrchr(FileName, '/');
	lastDot = strrchr(FileName, '.');
	if (lastSlash == NullCString)
		start = FileName;
	else
		start = &lastSlash[1];
	
	if ((lastDot == NullCString) || (lastDot < lastSlash))
		finish = &FileName[strlen(FileName)];
	else
		finish = lastDot;

	nameSize = finish - start;
	//
	//	Allocate the proper space for the basename, store the text, and return it.
	//
	tempName = (CString) malloc(nameSize+1);
	strncpy(tempName, start, nameSize);
	tempName[nameSize] = EndOfCString;
	
	[self StoreErrorCode: ERR_OK AndText: "Located whatever basename was to be found."];
	[self StoreCString: tempName];
	return tempName;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		GetCurrentPosition
//	Parameters:	none
//	Returns:		A position in the file
//	Stores:		A position in the file as an Integer
//	Description:
//		this returns a FilePos value which indicates a current position in the file, as
//		a count of the number of bytes since the beginning.
//	Bugs:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (FilePos) GetCurrentPosition
{
	FilePos	pos;
	[self ResetResults];
	if (TheFile == NULL)
	{
		[self StoreErrorCode: ERR_FILENOTOPEN AndText: "No position in an unopened file."];
		pos =  0;
	}
	else
	{
		pos =  NXTell(TheFile);
		[self StoreErrorCode: ERR_OK AndText: "Got the file position."];
	}
	[self StoreInteger: pos];
	return pos;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		GetExtension
//	Parameters:	none
//	Returns:		the copy of a string containing the current filename's extension
//	Stores:		A pointer to the same string we return
//	Description:
//		This simply  extracts the extension of the current file name.  The extension is
//		the part of the name that comes after the last . in the filename.  If there is no
//		extension, this will simply return an empty string.
//		If we are using the file  /foo/bar/hotfile.rtf, this returns rtf
//	Bugs:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (CString) GetExtension
{
	//
	//	Determine where the last / and . are. We need the / to be sure we don't get a . further
	//	up in the path somewhere.
	//
	CString	lastSlash, lastDot, start, tempName;

	[self ResetResults];
	//
	// 	Locate the last . in the file name, and the last /.  If we found no dot, or it came
	//	before the slash in the name, then there is no extension, so start with a null CString.
	//	otherwise, set the start of the exension to just after the last dot.
	//	
	lastSlash = strrchr(FileName, '/');
	lastDot = strrchr(FileName, '.');
	if ((lastDot == NullCString) || (lastDot < lastSlash))
		start = NullCString;
	else
		start = &lastDot[1];
	//
	//	Allocate space for the extension in a temporary string, and copy it into
	//	TempName.
	//
	tempName = (CString) malloc(strlen(start)+1);
	strcpy(tempName, start);
	[self StoreErrorCode: ERR_OK AndText: "Located whatever extension was to be found."];
	[self StoreCString: tempName];
	return tempName;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		GetFilename
//	Parameters:	none
//	Returns:		the copy of a string containing the full filename, without 
//	Stores:		A pointer to the same string we return
//	Description:
//	this returns the path to the file that we are using without including the file name
//	itselfor the slash before the filename.  If there are no /'s (and thus, oddly, no directories
//	refered to), a null string is returned. If we are using the file  /foo/bar/hotfile.rtf, this
//	returns hotfile.rtf
//	Bugs:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (CString) GetFilename
{
	CString	start;
	CString	tempName;
	[self ResetResults];
	//
	//	Locate the slash before the Filename, and make start point after it
	//	(which happens to be the start of our filename)
	//
	start = strrchr(FileName, '/');
	if (start != NullCString)
		start = &start[1];
	else
		start = FileName;
	//
	//	Allocate the proper space for the filename, copy the text, and return it.
	//
	tempName = (CString) malloc(strlen(start)+1);
	strcpy(tempName,  start);
	[self StoreErrorCode: ERR_OK  AndText: "Extracted the filename"];
	[self StoreCString: tempName];
	return tempName;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		GetDirectory
//	Parameters:	none
//	Returns:		the copy of a string containing the pathname to this directory
//	Stores:		A pointer to the same string we return
//	Description:
//	this returns the path to the file that we are using without including the file name
//	itselfor the slash before the filename.  If there are no /'s (and thus, oddly, no directories
//	refered to), a null string is returned.  If we are using the filen  /foo/bar/hotfile.rtf, this returns
//	/foo/bar
//	Bugs:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (CString) GetDirectory
{
	//
	//	Declare a temporary string to store the file name, clear the results, and copy
	//	the actual file name into the temp string.
	//
	CString	lastSlash;
	CString	tempName = (CString) malloc(strlen(FileName)+1);
	[self ResetResults];
	strcpy(tempName, FileName);
	//
	//	Locate the last slash in the path name.  If we find it, make it the end of the string instad,
	//	otherwise clear the whole string.  Return it in any case.
	//
	lastSlash = strrchr(tempName, '/');
	if (lastSlash != NullCString)
	{
		lastSlash[0] = EndOfCString;
		[self StoreErrorCode:  ERR_OK AndText: "Got the directory"];
	}
	else
	{
		tempName[0]=EndOfCString;
		[self StoreErrorCode:  ERR_NODIRECTORY AndText: "There were no directory names"];
	}
	[self StoreCString: tempName];
	return tempName;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		GetPathname:
//	Parameters:	none
//	Returns:		the copy of a string containing the pathname to this file
//	Stores:		A pointer to the same string we return
//	Description:
//		This merely makes a copy of the string that we have stored for the pathname
//		and returns it to the caller.  Given the filename  /foo/bar/hotfile.rtf, this returns
//		/foo/bar/hotfile.rtf
//	Bugs:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (CString) GetPathname
{
	CString	tempName;
	[self ResetResults];
	//
	//	Allocate the proper space for the pahtname, store the text, and return it.
	//
	tempName = (CString) malloc(strlen(FileName)+1);
	strcpy(tempName, FileName);
	[self StoreErrorCode: ERR_OK AndText: "Gotcha your path"];
	[self StoreCString: tempName];
	return tempName;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		MoveTo:
//	Parameters:	the byte position we should move to in the file
//	Returns:		self
//	Stores:		The position we moved to
//	Description:
//		This changes our position in the file to the position byteLoc bytes from the
//		beginning of the file.  The new position in the file is stored before we return.
//		Deal with exceptions being raised around the NXSeek code.  If an error occurs,
//		then we will assume that it's because we tried to move past the eof, and so we
//		set the position at the end of a file.  if an unknown exception occurs, do nothing.
//	Bugs:
//		I don't know if the assumption that an NX_illegalSeek exception necessarily means
//		we tried to go past the end of the stream.  If not, there's problems here.
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (id) MoveTo: (PositiveInteger) byteLoc
{
	Boolean result;
	[self ResetResults];
	
	if (TheFile == NULL)
		[self StoreErrorCode: ERR_FILENOTOPEN
			AndText: "The file wasn't even open!  How d'ya expect me to move around in it?!"];
	else if (AccessMode == FILE_APPEND)
		[self StoreErrorCode: ERR_FILEAPPENDONLY
			AndText: "Can not change position in a file set for only appending"];
	else
	{
		//
		//	Try to move the requested number of bytes.  If an exception occurrs,
		//	and it's a seek error, just stick us at the end quietly (assume the
		//	error was due to running over the end.  Otherwise, store
		//	an error code and leave things as they are.  
		//
		NX_DURING
			NXSeek(TheFile, byteLoc, NX_FROMSTART);
			[self StoreErrorCode: ERR_OK AndText: "Moved OK"];
		NX_HANDLER
			switch(NXLocalHandler.code)
			{
				case NX_illegalSeek:
					NXSeek(TheFile, 0, NX_FROMEND);
					[self StoreErrorCode: ERR_OK AndText: "Moved to the end."];
					FileLocation = fileAtEOF;
					break;
				default :
					[self StoreErrorCode: ERR_BADADVANCE
						AndText: "A unknown exception occurred when advancing."];
					break;
			}
		NX_ENDHANDLER

		result = NXAtEOS(TheFile);
		if (result != NO)
			FileLocation = fileAtEOF;
		else if ( NXTell(TheFile) == 0)
			FileLocation = fileAtStart;
		else
			FileLocation = fileInMiddle;


		[self StorePositiveInteger: NXTell(TheFile)];
	}
	return self;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		ReadByte
//	Parameters:	none
//	Returns:		the byte we read
//	Stores:		The byte we read
//				YES if EOF was encountered
//	Description:
//		This reads a byte from our file. If all goes well, we return the byte.  Otherwise,
//		we return 0 and store an error
//	Bugs:
//		BOOL NXAtEOS(NXStream *stream)
//		Says it should not be used on streams open for writing.  What about read/write?
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (Byte) ReadByte
{
	Byte byteIn = 0;
	Boolean	result;
	
	[self ResetResults];

	if (TheFile == NULL)
		[self StoreErrorCode: ERR_FILENOTOPEN AndText: "File was not properly opened"];
	else if ((AccessMode == FILE_WRITE) || (AccessMode == FILE_APPEND))
		[self StoreErrorCode: ERR_FILEWRITEONLY
			AndText: "Can not read from a file set for only appending or writing"];
	else
	{
		result = NXAtEOS(TheFile);
		if (result != NO)
		{
			[self StoreErrorCode: ERR_EOF AndText: "End of file found"];
			FileLocation = fileAtEOF;
			[self StorePositiveInteger: 0];
			[self PutBoolean: YES Into: SECOND_RESULT];
		}
		else
		{
			byteIn = NXGetc(TheFile);
			[self StoreErrorCode: ERR_OK AndText: "Read the byte"];
			[self StorePositiveInteger: byteIn];
			[self PutBoolean: NO Into: SECOND_RESULT];
		}
	}
	return byteIn;
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		Read:BytesInto:
//	Parameters:	the number of bytes we are to read
//				the buffer that we are to read info
//	Returns:		self
//	Stores:		The number of bytes we succesfully read
//				YES if we found EOF at the end of the current read
//	Description:
//		This attempts to read in numBytes bytes from our file into the buffer.  It does no
//		checking to assure that the buffer is of the proper size.  After reading the bytes, it
//		stores the number of bytes it succeeded in reading.  If the number of bytes written
//		is not equal to the number requested, an error is stored.
//	Bugs:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (id) Read: (PositiveInteger) numBytes BytesInto: (ByteString) buffer
{
	PositiveInteger	BytesRead;
	Boolean			result;
	
	[self ResetResults];

	if (TheFile == NULL)
		[self StoreErrorCode: ERR_FILENOTOPEN AndText: "File was not properly opened"];
	else if ((AccessMode == FILE_WRITE) || (AccessMode == FILE_APPEND))
		[self StoreErrorCode: ERR_FILEWRITEONLY
			AndText: "Can not read from a file set for only appending or writing"];
	else
	{
		result = NXAtEOS(TheFile);
		if (result != NO)
		{
			[self StoreErrorCode: ERR_EOF AndText: "End of file found"];
			FileLocation = fileAtEOF;
			[self StorePositiveInteger: 0];
			[self PutBoolean: YES Into: SECOND_RESULT];
		}
		else
		{
			BytesRead = NXRead(TheFile, buffer, numBytes);
			//
			//	Deal with the consequences of our actions
			//
			if (BytesRead != numBytes)
				[self StoreErrorCode: ERR_READINGERROR AndText: "Read the wrong number of bytes"];
			else
				[self StoreErrorCode: ERR_OK AndText: "Read the bytes."];
			[self StorePositiveInteger: BytesRead];
			[self PutBoolean: NO Into: SECOND_RESULT];
		}
	}
	return self;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		WriteByte
//	Parameters:	the byte we are to write
//	Returns:		self
//	Stores:		The byte we think we wrote
//	Description:
//		This writes a specified byte to our file.  It then checks to make sure we wrote
//		that byte.  If we failed to do that, we generate an error.
//	Bugs:
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (id) WriteByte: (Byte) theByte
{
	PositiveInteger byteOut;
	
	[self ResetResults];

	if (TheFile == NULL)
		[self StoreErrorCode: ERR_FILENOTOPEN AndText: "File was not properly opened"];
	else if (AccessMode == FILE_READ)
		[self StoreErrorCode: ERR_FILEREADONLY AndText: "Can not write to read-only file"];
	else
	{
		byteOut = NXPutc(TheFile, theByte);
		if (byteOut == theByte) 
			[self StoreErrorCode: ERR_OK AndText: "Wrote the byte"];
		else
			[self StoreErrorCode: ERR_WROTEBADCHAR AndText: "Wrote the wrong byte"];
		[self StorePositiveInteger: byteOut];
	}
	return self;
}



//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Routine:		Write:BytesFrom:
//	Parameters:	the number of bytes we are to write
//				the buffer that we are to write from
//	Returns:		self
//	Stores:		The number of bytes we succesfully wrote
//	Description:
//		This writes a specified number of bytes from the buffer we have been passed to
//		the file that we manipulate.  If we fail to write all the bytes requested, we store an
//		error.  We always store the number of bytes we wrote.
//	Bugs:
//		We never flush the data out (NXFlush does naught for a memory stream)
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (id) Write: (PositiveInteger) numBytes BytesFrom: (ByteString) buffer
{
	PositiveInteger	bytesWrote;
	
	[self ResetResults];

	if (TheFile == NULL)
		[self StoreErrorCode: ERR_FILENOTOPEN AndText: "File was not properly opened"];
	else if (AccessMode == FILE_READ)
		[self StoreErrorCode: ERR_FILEREADONLY AndText: "Can not write to read-only file"];
	else
	{
		bytesWrote = NXWrite(TheFile, buffer, numBytes);
		//
		//	Deal with the consequences of our actions
		//
		if (bytesWrote != numBytes)
			[self StoreErrorCode: ERR_WRITINGERROR
				AndText: "Wrote the wrong number of bytes"];
		else
			[self StoreErrorCode: ERR_OK AndText: "Wrote the bytes."];
		[self StorePositiveInteger: bytesWrote];
	}
	return self;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Method:		FileSize
//	Parameters:	none
//	Returns:		An integer reflecting the size of the file.  Set to 0 if an error occured
//	Stores:		The return value, if all went well, otherwise nothing
//	Description:
//		This determines how many bytes long the file that this object manipulates is,
//		and returns this number to the caller.
//	Bugs:
//		Only usable if the file has been opened...
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- (PositiveInteger) FileSize
{
	PositiveInteger	currentLoc, eofLoc;
	
	[self ResetResults];
	if (TheFile == NULL)
	{
		[self StoreErrorCode: ERR_FILENOTOPEN AndText: "File was not properly opened."];
		eofLoc = 0;
		[self StorePositiveInteger: 0];
	}
	else
	{
		currentLoc = NXTell(TheFile);
		NXSeek(TheFile, 0, NX_FROMEND);
		eofLoc = NXTell(TheFile);
		NXSeek(TheFile, currentLoc, NX_FROMSTART);
		[self StoreErrorCode: ERR_OK AndText: "Obtained size of file."];
		[self StorePositiveInteger: eofLoc];
	}
	return eofLoc;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Method:		FileInfo
//	Parameters:	none
//	Returns:		self
//	Stores:		the access mode is stored in return position 1
//				a pointer to an OS-dependant structure is in 2
//	Description:
//		This returns information to the caller that isn't accessable through other
//		methods.  At this point, it returns the access method for the file, currently being
//		used.  It also returns a pointer to a stat structure (see the man page for stat).
//		The stat structure may be replaced by a better structure in a future
//		version
//	Bugs:
//		info like file size won't be accurate to the caller.
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-  FileInfo
{
	 struct stat *statbuf = (struct stat *) malloc(sizeof(struct stat));
	
	if (stat(FileName, statbuf) != 0)
		[self StoreErrorCode: ERR_CANTGETINFO AndText: "Could not get machine dep. info"];
	else
	{
		[self StoreErrorCode: ERR_OK AndText: "The info is yours!"];
		[self PutInteger: AccessMode Into: FIRST_RESULT];
		[self CopyPointer: (Pointer) statbuf WithLength: sizeof(struct stat) Into: SECOND_RESULT];
         }
	free((char*) statbuf);
	return self;
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//	Method:		SameFileAs
//	Parameters:	the path to another file
//	Returns:		A boolean value.  yes indicates that the pathname refers to the same
//				file this object is accessing.  no means either they are different or
//				the other file does not exist. (or that this one doesn't exist)
//	Stores:		This boolean.
//	Description:
//		Used to determine if some other file is the same as this one..  Pass the name of
//		the other file with a path, and this will check to see it if it refers to the same
//		file that it is using.  Note that this doesn't understand symbolic links, really.
//	Bugs:
//		Inadequately deals with the negative cases where one or both files might not
//		actually exist.
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-  (Boolean) SameFileAs: (CString) someOtherFile
{
	 struct stat *statbuf = (struct stat *) malloc(sizeof(struct stat));
	CString 	pathToFile = [self GetFullPathFrom:  someOtherFile];
	ErrorCode result;
	Boolean answer = NO;
	
	if (stat(pathToFile, statbuf) != 0)
		result = -1;
	else
	{
		if (TheInode == statbuf->st_ino)
			result = 0;
		else
			result = 1;
	}
	FreeCString(pathToFile);
	//	92.01.01	djb	Added freeing of statbuf to remove a memory leak.
	free((char*) statbuf);
	
	switch (result)
	{
		case -1:
			[self StoreErrorCode: ERR_OK AndText: "The file does not even exist!"];
			answer = NO;
			break;
		case 0:
			[self StoreErrorCode: ERR_OK AndText: "The info is yours!"];
			answer = YES;
			break;
		case 1:
			[self StoreErrorCode: ERR_OK AndText: "The info is yours!"];
			answer = NO;
			break;
	}
	[self StoreBoolean: answer];
	return answer;
}



@end

These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.