/******************************************************************************
 jDirUtil_UNIX.cc

	Directory utilities implemented for the UNIX System.

	Copyright © 1996 by Glenn W. Bach. All rights reserved.

 ******************************************************************************/

#include <jDirUtil.h>
#include <jFileUtil.h>
#include <jProcessUtil.h>
#include <JUNIXDirInfo.h>
#include <JUNIXDirEntry.h>
#include <JProgressDisplay.h>
#include <JString.h>
#include <JLatentPG.h>
#include <jGlobals.h>
#include <JStdError.h>
#include <pwd.h>
#include <jErrno.h>
#include <jMissingProto.h>
#include <jAssert.h>

/******************************************************************************
 JNameUsed

	Returns kTrue if the specified name exists. (file, directory, link, etc).

 ******************************************************************************/

JBoolean
JNameUsed
	(
	const JCharacter* name
	)
{
	struct stat info;
	return JConvertToBoolean( lstat(name, &info) == 0 );
}

/******************************************************************************
 JSameDirEntry

	Returns kTrue if the given names point to the same inode in the
	file system.

 ******************************************************************************/

JBoolean
JSameDirEntry
	(
	const JCharacter* name1,
	const JCharacter* name2
	)
{
	struct stat stbuf1, stbuf2;
	return JI2B(stat(name1, &stbuf1) == 0 &&
				stat(name2, &stbuf2) == 0 &&
				stbuf1.st_dev == stbuf2.st_dev &&
				stbuf1.st_ino == stbuf2.st_ino);
}

/******************************************************************************
 JGetModificationTime

	Returns the last time that the file was modified.
	Can return JFileDoesNotExist.

 ******************************************************************************/

JError
JGetModificationTime
	(
	const JCharacter*	name,
	time_t*				modTime
	)
{
	struct stat info;
	if (stat(name, &info) == 0)
		{
		*modTime = info.st_mtime;
		return JNoError();
		}

	*modTime = 0;

	const int err = jerrno();
	if (err == ENOENT)
		{
		return JFileDoesNotExist();
		}
	else
		{
		return JUnexpectedError(err);
		}
}

/******************************************************************************
 JGetPermissions

	Returns the access permissions for the specified file.
	Can return JFileDoesNotExist.

 ******************************************************************************/

JError
JGetPermissions
	(
	const JCharacter*	name,
	mode_t*				perms
	)
{
	struct stat info;
	if (stat(name, &info) == 0)
		{
		*perms = info.st_mode;
		return JNoError();
		}

	*perms = 0;

	const int err = jerrno();
	if (err == ENOENT)
		{
		return JFileDoesNotExist();
		}
	else
		{
		return JUnexpectedError(err);
		}
}

/******************************************************************************
 JSetPermissions

	Sets the access permissions for the specified file.

	Can return JAccessDenied, JFileSystemReadOnly, JSegFault, JNameTooLong,
	JFileDoesNotExist, JNoKernelMemory, JComponentNotDirectory,
	JPathContainsLoop.

 ******************************************************************************/

JError
JSetPermissions
	(
	const JCharacter*	name,
	const mode_t		perms
	)
{
	jclear_errno();
	if (chmod(name, perms) == 0)
		{
		return JNoError();
		}

	const int err = jerrno();
	if (err == EPERM || err == EACCES)
		{
		return JAccessDenied();
		}
	else if (err == EROFS)
		{
		return JFileSystemReadOnly();
		}
	else if (err == EFAULT)
		{
		return JSegFault();
		}
	else if (err == ENAMETOOLONG)
		{
		return JNameTooLong();
		}
	else if (err == ENOENT)
		{
		return JFileDoesNotExist();
		}
	else if (err == ENOMEM)
		{
		return JNoKernelMemory();
		}
	else if (err == ENOTDIR)
		{
		return JComponentNotDirectory();
		}
	else if (err == ELOOP)
		{
		return JPathContainsLoop();
		}
	else
		{
		return JUnexpectedError(err);
		}
}

/******************************************************************************
 JGetPermissionsString

	Converts the low 9 bits of the st_mode field from stat() to a string
	using the same format as ls.

 *****************************************************************************/

JString
JGetPermissionsString
	(
	const mode_t mode
	)
{
	JString modeString = "---------";
	if (mode & S_IRUSR)
		{
		modeString.SetCharacter(1, 'r');
		}
	if (mode & S_IWUSR)
		{
		modeString.SetCharacter(2, 'w');
		}
	if (mode & S_IXUSR)
		{
		modeString.SetCharacter(3, 'x');
		}
	if (mode & S_IRGRP)
		{
		modeString.SetCharacter(4, 'r');
		}
	if (mode & S_IWGRP)
		{
		modeString.SetCharacter(5, 'w');
		}
	if (mode & S_IXGRP)
		{
		modeString.SetCharacter(6, 'x');
		}
	if (mode & S_IROTH)
		{
		modeString.SetCharacter(7, 'r');
		}
	if (mode & S_IWOTH)
		{
		modeString.SetCharacter(8, 'w');
		}
	if (mode & S_IXOTH)
		{
		modeString.SetCharacter(9, 'x');
		}

	return modeString;
}

/******************************************************************************
 JDirectoryExists

	Returns kTrue if the specified directory exists.

 ******************************************************************************/

JBoolean
JDirectoryExists
	(
	const JCharacter* dirName
	)
{
	struct stat info;
	return JI2B(
			lstat(dirName, &info) == 0 &&
			stat( dirName, &info) == 0 &&
			S_ISDIR(info.st_mode) );
}

/******************************************************************************
 JDirectoryReadable

	Returns kTrue if the specified directory can be read from.

 ******************************************************************************/

JBoolean
JDirectoryReadable
	(
	const JCharacter* dirName
	)
{
	return JI2B( (getuid() == 0 && JDirectoryExists(dirName)) ||
				 access(dirName, R_OK) == 0 );
}

/******************************************************************************
 JDirectoryWritable

	Returns kTrue if the specified directory can be written to.

 ******************************************************************************/

JBoolean
JDirectoryWritable
	(
	const JCharacter* dirName
	)
{
	return JI2B( (getuid() == 0 && JDirectoryExists(dirName)) ||
				 access(dirName, W_OK) == 0 );
}

/******************************************************************************
 JCanEnterDirectory

	Returns kTrue if it is possible to make the specified directory
	the working directory.

 ******************************************************************************/

JBoolean
JCanEnterDirectory
	(
	const JCharacter* dirName
	)
{
	return JI2B( (getuid() == 0 && JDirectoryExists(dirName)) ||
				 access(dirName, X_OK) == 0 );
}

/******************************************************************************
 JCreateDirectory

	Creates the requested directory.

	Can return JDirectoryAlreadyExists, JSegFault, JAccessDenied, JNameTooLong,
	JBadPath, JComponentNotDirectory, JNoKernelMemory, JFileSystemReadOnly,
	JPathContainsLoop, JFileSystemFull.

 ******************************************************************************/

JError
JCreateDirectory
	(
	const JCharacter* dirName
	)
{
	return JCreateDirectory(dirName, 0755);
}

JError
JCreateDirectory
	(
	const JCharacter*	dirName,
	const int			mode
	)
{
	jclear_errno();
	if (mkdir(dirName, mode) == 0)
		{
		return JNoError();
		}

	const int err = jerrno();
	if (err == EEXIST)
		{
		return JDirectoryAlreadyExists();
		}
	else if (err == EFAULT)
		{
		return JSegFault();
		}
	else if (err == EACCES)
		{
		return JAccessDenied();
		}
	else if (err == ENAMETOOLONG)
		{
		return JNameTooLong();
		}
	else if (err == ENOENT)
		{
		return JBadPath();
		}
	else if (err == ENOTDIR)
		{
		return JComponentNotDirectory();
		}
	else if (err == ENOMEM)
		{
		return JNoKernelMemory();
		}
	else if (err == EROFS)
		{
		return JFileSystemReadOnly();
		}
	else if (err == ELOOP)
		{
		return JPathContainsLoop();
		}
	else if (err == ENOSPC)
		{
		return JFileSystemFull();
		}
	else
		{
		return JUnexpectedError(err);
		}
}

/******************************************************************************
 JRenameDirectory

	Renames the specified directory.

 ******************************************************************************/

JError
JRenameDirectory
	(
	const JCharacter* oldName,
	const JCharacter* newName
	)
{
	return JRenameFile(oldName, newName);
}

/******************************************************************************
 JChangeDirectory

	Changes the current working directory to the specified directory.

	Can return JAccessDenied, JSegFault, JNameTooLong, JBadPath,
	JNoKernelMemory, JComponentNotDirectory, JPathContainsLoop.

 ******************************************************************************/

JError
JChangeDirectory
	(
	const JCharacter* dirName
	)
{
	jclear_errno();
	if (chdir(dirName) == 0)
		{
		return JNoError();
		}

	const int err = jerrno();
	if (err == EPERM || err == EACCES)
		{
		return JAccessDenied();
		}
	else if (err == EFAULT)
		{
		return JSegFault();
		}
	else if (err == ENAMETOOLONG)
		{
		return JNameTooLong();
		}
	else if (err == ENOENT)
		{
		return JBadPath();
		}
	else if (err == ENOMEM)
		{
		return JNoKernelMemory();
		}
	else if (err == ENOTDIR)
		{
		return JComponentNotDirectory();
		}
	else if (err == ELOOP)
		{
		return JPathContainsLoop();
		}
	else
		{
		return JUnexpectedError(err);
		}
}

/******************************************************************************
 JRemoveDirectory

	Removes the specified directory.  This only works if the directory is empty.

	Can return JAccessDenied, JSegFault, JNameTooLong, JBadPath,
	JComponentNotDirectory, JDirectoryNotEmpty, JDirectoryBusy,
	JNoKernelMemory, JFileSystemReadOnly, JPathContainsLoop.

 ******************************************************************************/

JError
JRemoveDirectory
	(
	const JCharacter* dirName
	)
{
	jclear_errno();
	if (rmdir(dirName) == 0)
		{
		return JNoError();
		}

	const int err = jerrno();
	if (err == EPERM || err == EACCES)
		{
		return JAccessDenied();
		}
	else if (err == EFAULT)
		{
		return JSegFault();
		}
	else if (err == ENAMETOOLONG)
		{
		return JNameTooLong();
		}
	else if (err == ENOENT)
		{
		return JBadPath();
		}
	else if (err == ENOTDIR)
		{
		return JComponentNotDirectory();
		}
	else if (err == ENOTEMPTY)
		{
		return JDirectoryNotEmpty();
		}
	else if (err == EBUSY)
		{
		return JDirectoryBusy();
		}
	else if (err == ENOMEM)
		{
		return JNoKernelMemory();
		}
	else if (err == EROFS)
		{
		return JFileSystemReadOnly();
		}
	else if (err == ELOOP)
		{
		return JPathContainsLoop();
		}
	else
		{
		return JUnexpectedError(err);
		}
}

/******************************************************************************
 JKillDirectory

	Deletes the directory and everything in it.
	Returns kTrue if successful.

 ******************************************************************************/

JBoolean
JKillDirectory
	(
	const JCharacter* dirName
	)
{
	const JCharacter* argv[] = {"rm", "-rf", dirName, NULL};
	const JError err = JExecute(argv, sizeof(argv), NULL,
								kJIgnoreConnection, NULL,
								kJIgnoreConnection, NULL,
								kJTossOutput,       NULL);
	if (err.OK())
		{
		return JNegate( JNameUsed(dirName) );
		}
	else
		{
		return kFalse;
		}
}

/******************************************************************************
 JGetCurrentDirectory

	Returns the full path of the current working directory.

 ******************************************************************************/

JString
JGetCurrentDirectory()
{
	char buf[1024];
	char* result = getcwd(buf, 1024);

	JString dirName;
	if (result == buf)
		{
		dirName = buf;
		}
	else if (!JGetHomeDirectory(&dirName) || JChangeDirectory(dirName) != kJNoError)
		{
		dirName = "/";
		JChangeDirectory(dirName);
		}

	JAppendDirSeparator(&dirName);
	return dirName;
}

/******************************************************************************
 JGetHomeDirectory

	Returns kTrue if the current user has a home directory.

 ******************************************************************************/

JBoolean
JGetHomeDirectory
	(
	JString* homeDir
	)
{
	// try HOME environment variable

	char* envHomeDir = getenv("HOME");
	if (envHomeDir != NULL && JDirectoryExists(envHomeDir))
		{
		*homeDir = envHomeDir;
		JAppendDirSeparator(homeDir);
		return kTrue;
		}

	// try password information

	char* envUserName = getenv("USER");

	struct passwd* pw;
	if (envUserName != NULL)
		{
		pw = getpwnam(envUserName);
		}
	else
		{
		pw = getpwuid( getuid() );
		}

	if (pw != NULL && JDirectoryExists(pw->pw_dir))
		{
		*homeDir = pw->pw_dir;
		JAppendDirSeparator(homeDir);
		return kTrue;
		}

	// give up

	homeDir->Clear();
	return kFalse;
}

/******************************************************************************
 JGetHomeDirectory

	Returns kTrue if the specified user has a home directory.

 ******************************************************************************/

JBoolean
JGetHomeDirectory
	(
	const JCharacter*	user,
	JString*			homeDir
	)
{
	struct passwd* pw = getpwnam(user);
	if (pw != NULL && JDirectoryExists(pw->pw_dir))
		{
		*homeDir = pw->pw_dir;
		JAppendDirSeparator(homeDir);
		return kTrue;
		}
	else
		{
		homeDir->Clear();
		return kFalse;
		}
}

/******************************************************************************
 JGetTrueName

	Returns kTrue if name is a valid file or a valid directory.
	*trueName is the full, true path to name, without symbolic links.

 ******************************************************************************/

JBoolean
JGetTrueName
	(
	const JCharacter*	name,
	JString*			trueName
	)
{
	if (!JNameUsed(name))
		{
		return kFalse;
		}

	// check if it is a file

	if (JFileExists(name))
		{
		JString path;
		JString fileName = name;
		JIndex index;
		if (fileName.LocateLastSubstring("/", &index))
			{
			const JString origPath = fileName.GetSubstring(1, index);
			fileName.RemoveSubstring(1, index);

			const JString currPath = JGetCurrentDirectory();

			JError err = JChangeDirectory(origPath);
			assert( err.OK() );

			path = JGetCurrentDirectory();

			err = JChangeDirectory(currPath);
			assert( err.OK() );
			}
		else
			{
			path = JGetCurrentDirectory();
			}

		JAppendDirSeparator(&path);

		const JSize kBufferSize = 1024;
		static char buf [ kBufferSize ];
		const long linkNameSize = readlink(name, buf, kBufferSize-1);
		if (linkNameSize != -1)
			{
			buf [ linkNameSize ] = '\0';
			fileName = buf;
			if (buf[0] != '/')
				{
				fileName.Prepend(path);
				}
			return JGetTrueName(fileName, trueName);
			}
		else
			{
			*trueName  = path;
			*trueName += fileName;
			return kTrue;
			}
		}

	// check if it is a directory

	else if (JDirectoryExists(name))
		{
		const JString currPath = JGetCurrentDirectory();

		JError err = JChangeDirectory(name);
		assert( err.OK() );

		*trueName = JGetCurrentDirectory();
		JAppendDirSeparator(trueName);

		err = JChangeDirectory(currPath);
		assert( err.OK() );

		return kTrue;
		}

	// it doesn't exist

	else
		{
		trueName->Clear();
		return kFalse;
		}
}

/******************************************************************************
 JSearchSubdirs

	Recursively search for the given file or directory, starting from the
	specified directory.  We do this in two passes:
		1)  Search in the given directory
		2)  Search in the sub-directories

	This mirrors the user's search strategy and helps insure that the
	expected file is found, if there are duplicates.

	caseSensitive is used only if name does not include a partial path,
	because it is just too messy otherwise.

	If newName is not NULL, it is set to the name of the file that was
	found.  This is critical if !caseSensitive, but useless otherwise.

	A progress display is used if the search takes more than 3 seconds.
	If you do not pass one in, JNewPG() will be used to create one.

 ******************************************************************************/

static JBoolean JSearchSubdirs_private(
	const JCharacter* startPath, const JCharacter* name,
	const JBoolean isFile, const JBoolean caseSensitive,
	JString* path, JString* newName,
	JProgressDisplay& pg, JBoolean* cancelled);

JBoolean
JSearchSubdirs
	(
	const JCharacter*	startPath,
	const JCharacter*	name,
	const JBoolean		isFile,
	const JBoolean		caseSensitive,
	JString*			path,
	JString*			newName,
	JProgressDisplay*	userPG
	)
{
	assert( !JStringEmpty(startPath) );
	assert( !JStringEmpty(name) && name[0] != '/' );

	JLatentPG pg(100);
	if (userPG != NULL)
		{
		pg.SetPG(userPG, kFalse);
		}
	JString msg = "Searching for \"";
	msg += name;
	msg += "\"...";
	pg.VariableLengthProcessBeginning(msg, kTrue, kFalse);

	JBoolean cancelled = kFalse;
	const JBoolean found =
		JSearchSubdirs_private(startPath, name, isFile, caseSensitive,
							   path, newName, pg, &cancelled);
	if (!found)
		{
		path->Clear();
		if (newName != NULL)
			{
			newName->Clear();
			}
		}

	pg.ProcessFinished();
	return found;
}

JBoolean
JSearchSubdirs_private
	(
	const JCharacter*	startPath,
	const JCharacter*	name,
	const JBoolean		isFile,
	const JBoolean		caseSensitive,
	JString*			path,
	JString*			newName,
	JProgressDisplay&	pg,
	JBoolean*			cancelled
	)
{
	// checking this way covers partial path cases like "X11/Xlib.h"

	const JString fullName = JCombinePathAndName(startPath, name);
	if (( isFile && JFileExists(fullName)) ||
		(!isFile && JDirectoryExists(fullName)))
		{
		const JBoolean ok = JGetTrueName(startPath, path);
		assert( ok );
		if (newName != NULL)
			{
			*newName = name;
			}
		return kTrue;
		}

	JUNIXDirInfo* info;
	if (!JUNIXDirInfo::Create(startPath, &info))
		{
		return kFalse;
		}

	JBoolean found    = kFalse;
	const JSize count = info->GetEntryCount();

	// check each entry (if case sensitive, the initial check is enough)

	if (!caseSensitive)
		{
		for (JIndex i=1; i<=count; i++)
			{
			const JUNIXDirEntry& entry = info->GetEntry(i);

			if ((( isFile && entry.IsFile()) ||
				 (!isFile && entry.IsDirectory())) &&
				JStringCompare(name, entry.GetName(), caseSensitive) == 0)
				{
				const JBoolean ok = JGetTrueName(startPath, path);
				assert( ok );
				if (newName != NULL)
					{
					*newName = entry.GetName();
					}
				found = kTrue;
				break;
				}

			if (!pg.IncrementProgress())
				{
				*cancelled = kTrue;
				break;
				}
			}
		}

	// recurse on each directory

	if (!found && !(*cancelled))
		{
		for (JIndex i=1; i<=count; i++)
			{
			const JUNIXDirEntry& entry = info->GetEntry(i);

			if (entry.IsDirectory() && !entry.IsLink())
				{
				const JString newPath = entry.GetFullName();
				if (JSearchSubdirs_private(newPath, name, isFile,
										   caseSensitive, path, newName,
										   pg, cancelled))
					{
					found = kTrue;
					break;
					}
				}

			if (*cancelled || (caseSensitive && !pg.IncrementProgress()))
				{
				*cancelled = kTrue;
				break;
				}
			}
		}

	delete info;
	return found;
}

/******************************************************************************
 JCombinePathAndName

	Concatenates path and name, inserting the appropriate separator
	('/' for UNIX) if necessary.

 ******************************************************************************/

JString
JCombinePathAndName
	(
	const JCharacter* path,
	const JCharacter* name
	)
{
	assert( !JStringEmpty(path) );
	assert( !JStringEmpty(name) );

	JString file = path;
	if (file.GetLastCharacter() != '/' && name[0] != '/')
		{
		file.AppendCharacter('/');
		}
	file += name;
	JCleanPath(&file);
	return file;
}

/******************************************************************************
 JSplitPathAndName

	Splits fullName into a path and name.

	This function is only designed to split a path and file name.
	It asserts if the string that you pass in ends with the directory
	separator.  If fullName doesn't contain a directory separator, it
	returns kFalse and *path is the result of JGetCurrentDirectory().

 ******************************************************************************/

JBoolean
JSplitPathAndName
	(
	const JCharacter*	fullName,
	JString*			path,
	JString*			name
	)
{
	assert( !JStringEmpty(fullName) );

	JString pathAndName = fullName;
	assert( pathAndName.GetLastCharacter() != '/' );

	JIndex i;
	if (pathAndName.LocateLastSubstring("/", &i))
		{
		*path = pathAndName.GetSubstring(1,i);

		const JSize len = pathAndName.GetLength();
		assert( i < len );
		*name = pathAndName.GetSubstring(i+1, len);
		return kTrue;
		}
	else
		{
		*path = JGetCurrentDirectory();
		*name = pathAndName;
		return kFalse;
		}
}

/******************************************************************************
 JAppendDirSeparator

	Appends the appropriate separator ('/' for UNIX) to the end of *dirName,
	if neccessary.

 ******************************************************************************/

void
JAppendDirSeparator
	(
	JString* dirName
	)
{
	assert( !dirName->IsEmpty() );

	if (dirName->GetLastCharacter() != '/')
		{
		dirName->AppendCharacter('/');
		}
}

/******************************************************************************
 JStripTrailingDirSeparator

 ******************************************************************************/

void
JStripTrailingDirSeparator
	(
	JString* dirName
	)
{
	assert( !dirName->IsEmpty() );

	while (dirName->GetLength() > 1 &&
		   dirName->GetLastCharacter() == '/')
		{
		dirName->RemoveSubstring(dirName->GetLength(), dirName->GetLength());
		}
}

/******************************************************************************
 JCleanPath

	Removes fluff from the given path:

		//
		/./
		trailing /.

	We can't remove /x/../ because if x is a symlink, the result would not
	be the same directory.

	This is required to work for files and directories.

 ******************************************************************************/

void
JCleanPath
	(
	JString* path
	)
{
	if (path->EndsWith("/."))
		{
		path->RemoveSubstring(path->GetLength(), path->GetLength());
		}

	JIndex i;
	while (path->LocateSubstring("/./", &i))
		{
		path->RemoveSubstring(i, i+1);
		}

	while (path->LocateSubstring("//", &i))
		{
		path->RemoveSubstring(i, i);
		}
}

/******************************************************************************
 JIsRootDirectory

 ******************************************************************************/

JBoolean
JIsRootDirectory
	(
	const JCharacter* dirName
	)
{
	assert( dirName != NULL );
	return JI2B( dirName[0] == '/' && dirName[1] == '\0' );
}

/*****************************************************************************
 JConvertToAbsolutePath

	Attempts to convert 'path' to a full path.  Returns kTrue if successful.

	If path begins with '/', there is nothing to do.
	If path begins with '~', the user's home directory is inserted.
	Otherwise, if base is not NULL and not empty, it is prepended.
	Otherwise, the result of JGetCurrentDirectory() is prepended.

	As a final check, it calls JNameUsed() to check that the result exists.
	(This allows one to pass in a path+name as well as only a path.)

 ******************************************************************************/

JBoolean
JConvertToAbsolutePath
	(
	const JCharacter*	path,
	const JCharacter*	base,		// can be NULL
	JString*			result
	)
{
	assert( !JStringEmpty(path) && result != NULL );

	JBoolean ok = kTrue;
	if (path[0] == '/')
		{
		*result = path;
		}
	else if (path[0] == '~')
		{
		ok = JExpandHomeDirShortcut(path, result);
		}
	else if (!JStringEmpty(base))
		{
		*result = JCombinePathAndName(base, path);
		}
	else
		{
		const JString currDir = JGetCurrentDirectory();
		*result = JCombinePathAndName(currDir, path);
		}

	if (ok && JNameUsed(*result))
		{
		return kTrue;
		}
	else
		{
		result->Clear();
		return kFalse;
		}
}

/*****************************************************************************
 JConvertToRelativePath

	Converts 'path' to a path relative to 'base'.  Both inputs must be
	absolute paths.  'path' can include a file name on the end.

 ******************************************************************************/

JString
JConvertToRelativePath
	(
	const JCharacter* origPath,
	const JCharacter* origBase
	)
{
	// Check that they are both absolute paths.

	assert( origPath != NULL && origPath[0] == '/' &&
			origBase != NULL && origBase[0] == '/' );

	// Remove extra directory separators
	// and make sure that both paths end with one.

	JString path = origPath;
	JCleanPath(&path);

	JString base = origBase;
	JCleanPath(&base);
	JAppendDirSeparator(&base);

	// Find and remove the matching directories at the beginning.
	// The while loop backs us up so we only consider complete directory names.

	JBoolean hadTDS = kTrue;
	if (path.GetLastCharacter() != '/')
		{
		path.AppendCharacter('/');
		hadTDS = kFalse;
		}

	JSize matchLength = JCalcMatchLength(path, base);

	if (!hadTDS)
		{
		path.RemoveSubstring(path.GetLength(), path.GetLength());
		}

	while (base.GetCharacter(matchLength) != '/')
		{
		matchLength--;
		}
	assert( matchLength >= 1 );
	if (matchLength == 1)
		{
		return path;
		}

	if (matchLength > path.GetLength())
		{
		base.RemoveSubstring(matchLength, matchLength);
		matchLength--;
		}

	path.RemoveSubstring(1, matchLength);
	base.RemoveSubstring(1, matchLength);

	if (base.IsEmpty())
		{
		path.Prepend("./");
		return path;
		}

	// The number of remaining directory separators in base
	// is the number of levels to go up.

	JSize upCount = 0;

	const JSize baseLength = base.GetLength();
	for (JIndex i=1; i<=baseLength; i++)
		{
		if (base.GetCharacter(i) == '/')
			{
			upCount++;
			path.Prepend("../");
			}
		}
	assert( upCount > 0 );

	return path;
}

/*****************************************************************************
 JGetClosestDirectory

	If the directory does not exist, go as far down the
	directory tree as possible towards the specified directory.

	As an example, /usr/include/junk doesn't normally exist, so it will
	return /usr/include.

	If a partial path is passed in, it is assumed to be relative to the
	current working directory.  If the current working directory is valid,
	a partial path will be returned.

	If the path begins with ~ and the home directory exists, a ~ path
	will be returned.

 ******************************************************************************/

JString
JGetClosestDirectory
	(
	const JCharacter*	origDirName,
	const JBoolean		requireWrite
	)
{
	assert( !JStringEmpty(origDirName) );

	const JString workingDir = JGetCurrentDirectory();

	JString dirName = origDirName;
	JString homeDir;
	JSize homeLength;
	if (origDirName[0] == '~' &&
		!JExpandHomeDirShortcut(origDirName, &dirName, &homeDir, &homeLength))
		{
		return JString("/");
		}
	else if (origDirName[0] != '~' && origDirName[0] != '/')
		{
		dirName.Prepend(workingDir);
		}

	assert( dirName.GetFirstCharacter() == '/' );

	JString newDir, junkName;
	while (!JDirectoryExists(dirName) ||
		   !JCanEnterDirectory(dirName) ||
		   !JDirectoryReadable(dirName) ||
		   (requireWrite && !JDirectoryWritable(dirName)))
		{
		JStripTrailingDirSeparator(&dirName);
		if (JIsRootDirectory(dirName))
			{
			break;
			}
		JSplitPathAndName(dirName, &newDir, &junkName);
		dirName = newDir;
		}

	// convert back to partial path, if possible

	if (origDirName[0] == '~' &&
		dirName.BeginsWith(homeDir))
		{
		dirName.ReplaceSubstring(1, homeDir.GetLength(), origDirName, homeLength);
		}
	else if (origDirName[0] != '~' && origDirName[0] != '/' &&
			 dirName.GetLength() > workingDir.GetLength() &&
			 dirName.BeginsWith(workingDir))
		{
		dirName.RemoveSubstring(1, workingDir.GetLength());
		}

	return dirName;
}

/*****************************************************************************
 JExpandHomeDirShortcut

	If the given path begins with ~ or ~x, this is replaced by the appropriate
	home directory, if it exists.  Otherwise, kFalse is returned and *result
	is empty.

	If homeDir != NULL, it is set to the home directory that was specified
	by the ~.  If homeLength != NULL it is set to the number of characters
	at the start of path that specified the home directory.

	This function does not check that the resulting expanded path is valid.

	If path doesn't begin with ~, returns kTrue, *result = path, *homeDir
	is empty, and *homeLength = 0.

 ******************************************************************************/

JBoolean
JExpandHomeDirShortcut
	(
	const JCharacter*	path,
	JString*			result,
	JString*			homeDir,
	JSize*				homeLength
	)
{
	assert( !JStringEmpty(path) && result != NULL );

	JBoolean ok = kTrue;
	if (path[0] == '~' && path[1] == '\0')
		{
		ok = JGetHomeDirectory(result);
		if (ok && homeDir != NULL)
			{
			*homeDir = *result;
			}
		if (ok && homeLength != NULL)
			{
			*homeLength = 1;
			}
		}
	else if (path[0] == '~' && path[1] == '/')
		{
		ok = JGetHomeDirectory(result);
		if (ok && homeDir != NULL)
			{
			*homeDir = *result;
			}
		if (ok && homeLength != NULL)
			{
			*homeLength = 2;
			}
		if (ok && path[2] != '\0')
			{
			*result = JCombinePathAndName(*result, path+2);
			}
		}
	else if (path[0] == '~')
		{
		JString userName = path+1;
		JIndex i;
		const JBoolean found = userName.LocateSubstring("/", &i);
		if (found)
			{
			userName.RemoveSubstring(i, userName.GetLength());
			}

		ok = JGetHomeDirectory(userName, result);
		if (ok && homeDir != NULL)
			{
			*homeDir = *result;
			}
		if (ok && homeLength != NULL)
			{
			*homeLength = found ? i+1 : strlen(path);
			}
		if (ok && found && path[i+1] != '\0')
			{
			*result = JCombinePathAndName(*result, path+i+1);
			}
		}
	else
		{
		*result = path;
		if (homeDir != NULL)
			{
			homeDir->Clear();
			}
		if (homeLength != NULL)
			{
			*homeLength = 0;
			}
		}

	if (ok)
		{
		return kTrue;
		}
	else
		{
		result->Clear();
		if (homeDir != NULL)
			{
			homeDir->Clear();
			}
		if (homeLength != NULL)
			{
			*homeLength = 0;
			}
		return kFalse;
		}
}
