/*
**  Copyright 2000-2004  University of Illinois Board of Trustees
**  Copyright 2000-2004  Mark D. Roth
**  All rights reserved.
**
**  dircache.c - FTP directory caching code
**
**  Mark D. Roth <roth@feep.net>
*/

#include <internal.h>

#include <time.h>
#include <errno.h>
#include <sys/param.h>

#ifdef STDC_HEADERS
# include <string.h>
# include <stdlib.h>
#endif


/* useful constants */
#define DIRCACHE_HASH_SIZE	256	/* size of directory hash */
#define DIRCACHE_FILEHASH_SIZE	128	/* size of directory contents hashes
					   (7-bit ASCII filenames) */


/******************************************************************************
*****  dir cache entry code
******************************************************************************/

/* dircache entry structure */
struct dcent
{
	char dc_name[MAXPATHLEN];	/* name of entry */
	time_t dc_timestamp;		/* time added to cache */
	enum dcent_types dc_type;	/* type of entry */
	union
	{
		fget_hash_t *dc_files_h;	/* directory contents */
		file_info_t *dc_fip;		/* individual file listing */
	} dc_data;
};
typedef struct dcent dcent_t;


/*
** _dcent_hashfunc() - hash function for directory cache entries
*/
static int
_dcent_hashfunc(char *key, int num_buckets)
{
        register unsigned result = 0;
	register int i;

	if (key == NULL)
		return 0;

	for (i = 0; *key != '\0' && i < 32; i++)
		result = result * 33U + *key++;

	return (result % num_buckets);
}


/*
** _dcent_free() - free a directory cache entry
*/
static void
_dcent_free(dcent_t *dcp)
{
	if (dcp->dc_type == DCENT_DIR)
	{
		if (dcp->dc_data.dc_files_h != NULL)
			fget_hash_free(dcp->dc_data.dc_files_h, free);
	}
	else
	{
		if (dcp->dc_data.dc_fip != NULL)
			free(dcp->dc_data.dc_fip);
	}

	free(dcp);
}


/*
** _dcent_new() - allocate a new directory cache entry
*/
static int
_dcent_new(dcent_t **dcpp, char *name, int type, void *data)
{
	/* allocate new cache entry */
	*dcpp = (dcent_t *)calloc(1, sizeof(dcent_t));
	if (*dcpp == NULL)
		return -1;

	/* set timestamp */
	(*dcpp)->dc_timestamp = time(NULL);

	/* set fields with caller-supplied data */
	strlcpy((*dcpp)->dc_name, name, sizeof((*dcpp)->dc_name));
	(*dcpp)->dc_type = type;

	if (type == DCENT_DIR)
		(*dcpp)->dc_data.dc_files_h = (fget_hash_t *)data;
	else
		(*dcpp)->dc_data.dc_fip = (file_info_t *)data;

	return 0;
}


/*
** _dcent_find_file() - find an entry in a files hash
** Returns:
**	1			success
**	0			failure
*/
static int
_dcent_find_file(fget_hash_t *file_h, char *file, file_info_t **fipp)
{
	fget_hashptr_t hp;

#ifdef DEBUG
	printf("==> _dcent_find_file(file_h=0x%lx, file=\"%s\", "
	       "fipp=0x%lx (0x%lx))\n", file_h, file, fipp, *fipp);
#endif

	if (file_h == NULL)
		return 0;

	fget_hashptr_reset(&hp);
	if (fget_hash_getkey(file_h, &hp, file, NULL) == 0)
		return 0;

	*fipp = (file_info_t *)fget_hashptr_data(&hp);
#ifdef DEBUG
	printf("<== _dcent_find_file(): returning \"%s\"\n",
	       (*fipp)->fi_filename);
#endif
	return 1;
}


/******************************************************************************
*****  dir cache code
******************************************************************************/

/*
** _dc_expire_dir() - remove a given directory from the dir cache
*/
static void
_dc_expire_dir(FTP *ftp, dcent_t *dcp, fget_listptr_t *lpp)
{
	fget_hashptr_t hp;

#ifdef DEBUG
	printf("==> _dc_expire_dir(ftp=0x%lx (%s), dcp=0x%lx (%s), "
	       "lpp=0x%lx)\n",
	       ftp, fget_netio_get_host(ftp->ftp_control),
	       dcp, dcp->dc_name, lpp);
#endif

	fget_hashptr_reset(&hp);
	if (fget_hash_getkey(ftp->ftp_dc_h, &hp, dcp, NULL) == 0)
		/* can't happen */
		return;

	fget_hash_del(ftp->ftp_dc_h, &hp);
	fget_list_del(ftp->ftp_dc_age_l, lpp);

#ifdef DEBUG
	printf("    _dc_expire_dir(): ftp->ftp_dc_size = %lu - %u\n",
	       ftp->ftp_dc_size, fget_hash_nents(dcp->dc_data.dc_files_h));
#endif
	if (dcp->dc_type == DCENT_DIR)
		ftp->ftp_dc_size -= fget_hash_nents(dcp->dc_data.dc_files_h);
	else
		ftp->ftp_dc_size--;	/* DCENT_FILE */

	_dcent_free(dcp);
}


/*
** _dc_expire() - keep dircache within specified size and time
*/
static void
_dc_expire(FTP *ftp)
{
	fget_listptr_t lp;
	dcent_t *dcp;
	time_t age;

#ifdef DEBUG
	printf("==> _dc_expire(ftp=0x%lx (%s))\n",
	       ftp, fget_netio_get_host(ftp->ftp_control));
	printf("    ftp->ftp_cache_maxsize = %lu\n",
	       (unsigned long)ftp->ftp_cache_maxsize);
	printf("    ftp->ftp_cache_expire = %ld\n",
	       (long)ftp->ftp_cache_expire);
#endif

	fget_listptr_reset(&lp);
	if (ftp->ftp_cache_maxsize != -1
	    && fget_list_next(ftp->ftp_dc_age_l, &lp) != 0)
	{
		/*
		** NOTE: we don't need to call fget_list_next() each
		** time, since fget_list_del() gets called in
		** _dc_expire_dir() and leaves the listptr pointing
		** to the next node
		*/
		while (fget_hash_nents(ftp->ftp_dc_h) > 0)
		{
			dcp = (dcent_t *)fget_listptr_data(&lp);

#ifdef DEBUG
			printf("    _dc_expire(): EXP 1: "
			       "dcp=0x%lx (%s), cache size=%ld\n",
			       dcp, dcp->dc_name, (long)ftp->ftp_dc_size);
#endif

			if (ftp->ftp_dc_size
			    <= (unsigned long)ftp->ftp_cache_maxsize)
				break;

			_dc_expire_dir(ftp, dcp, &lp);
		}
	}

	fget_listptr_reset(&lp);
	if (ftp->ftp_cache_expire != (time_t)-1
	    && fget_list_next(ftp->ftp_dc_age_l, &lp) != 0)
	{
		/*
		** (see note about fget_list_next() above)
		*/
		while (fget_hash_nents(ftp->ftp_dc_h) > 0)
		{
			dcp = (dcent_t *)fget_listptr_data(&lp);
			age = time(NULL) - dcp->dc_timestamp;

#ifdef DEBUG
			printf("    _dc_expire(): EXP 2: "
			       "dcp=0x%lx (%s), age=%ld\n",
			       dcp, dcp->dc_name, (long)age);
#endif

			if (age <= ftp->ftp_cache_expire)
				break;

			_dc_expire_dir(ftp, dcp, &lp);
		}
	}
}


/*
** _dc_insert() - insert a new entry into the directory cache
*/
static void
_dc_insert(FTP *ftp, dcent_t *dcp)
{
	/* update cache size */
	if (dcp->dc_type == DCENT_DIR)
		ftp->ftp_dc_size += fget_hash_nents(dcp->dc_data.dc_files_h);
	else
		ftp->ftp_dc_size++;	/* DCENT_FILE */

	/* add to cache */
	fget_hash_add(ftp->ftp_dc_h, dcp);
	fget_list_add(ftp->ftp_dc_age_l, dcp);
}


/*
** _dc_get() - find an entry in the directory hash
** Returns:
**	1			success
**	0			failure
*/
static int
_dc_get(fget_hash_t *dc_h, char *name, dcent_t **dcpp,
	enum dcent_types preferred_type)
{
	fget_hashptr_t hp;
	int i;

#ifdef DEBUG
	printf("==> _dc_get(dc_h=0x%lx, name=\"%s\", "
	       "dcpp=0x%lx (0x%lx))\n", dc_h, name, dcpp, *dcpp);
#endif

	if (dc_h == NULL)
	{
#ifdef DEBUG
		printf("<== _dc_get(): failed\n");
#endif
		return 0;
	}

	*dcpp = NULL;

	fget_hashptr_reset(&hp);
	while (fget_hash_getkey(dc_h, &hp, name, NULL) == 1)
	{
		*dcpp = (dcent_t *)fget_hashptr_data(&hp);

		/* this is the preferred type, so stop here */
		if ((*dcpp)->dc_type == preferred_type)
			break;

		/*
		** if we found something that is not the preferred type,
		** save it in *dcpp, but keep looking
		*/
	}

	if (*dcpp == NULL)
	{
#ifdef DEBUG
		printf("<== _dc_get(): failed\n");
#endif
		return 0;
	}

#ifdef DEBUG
	printf("<== _dc_get(): returning \"%s\" (%spreferred type)\n",
	       (*dcpp)->dc_name,
	       ((*dcpp)->dc_type == preferred_type
		? ""
		: "NOT "));
#endif
	return 1;
}


/******************************************************************************
*****  FTP list interface
******************************************************************************/

/*
** _dc_query_server() - add directory info to the dir cache
*/
static int
_dc_query_server(FTP *ftp, char *dir, dcent_t **dcpp, 
		 enum dcent_types preferred_type)
{
	fget_hash_t *files_h = NULL;
	fget_hashptr_t hp;
	file_info_t *fip;
	char path[MAXPATHLEN];

#ifdef DEBUG
	printf("==> _dc_query_server(ftp=0x%lx (%s), dir=\"%s\", "
	       "dcpp=0x%lx (0x%lx), preferred_type=%d)\n",
	       ftp, fget_netio_get_host(ftp->ftp_control),
	       dir, dcpp, *dcpp, preferred_type);
#endif

	files_h = fget_hash_new(DIRCACHE_FILEHASH_SIZE, NULL);
	if (files_h == NULL)
		return -1;

	if (_ftp_list(ftp, dir, files_h, preferred_type, 0) == -1)
	{
#ifdef DEBUG
		printf("<== _dc_query_server(): _ftp_list() failed "
		       "(errno = %d)\n", errno);
#endif
		fget_hash_free(files_h, free);
		return -1;
	}

#ifdef DEBUG
	printf("    _dc_query_server(): files_h = 0x%lx\n", files_h);
#endif

	/*
	** check if the server returned a full directory listing,
	** or if it just listed a single file
	*/
	fget_hashptr_reset(&hp);
	fget_hash_next(files_h, &hp);
	fip = (file_info_t *)fget_hashptr_data(&hp);
	if (fget_hash_nents(files_h) == 1
	    && fip != NULL
	    && strcmp(fip->fi_filename, dir) == 0)
	{
		/*
		** server listed a single file
		*/

		/* free hash */
		fget_hash_free(files_h, NULL);

		/* fix filename field to include only the basename */
		strlcpy(fip->fi_filename, basename(fip->fi_filename),
			sizeof(fip->fi_filename));

		/* create new entry */
		if (_dcent_new(dcpp, dir, DCENT_FILE, fip) == -1)
			return -1;
	}
	else
	{
		/*
		** it's a full directory listing
		*/

		/* create new entry */
		if (_dcent_new(dcpp, dir, DCENT_DIR, files_h) == -1)
			return -1;
	}

	/* insert new entry */
	_dc_insert(ftp, *dcpp);

#ifdef DEBUG
	printf("<== _dc_query_server(): *dcpp=0x%lx\n", *dcpp);
#endif
	return 0;
}


/******************************************************************************
*****  external interface to this object
******************************************************************************/

/*
** _ftp_dircache_init() - initialize directory cache
*/
void
_ftp_dircache_init(FTP *ftp)
{
	ftp->ftp_dc_h = fget_hash_new(DIRCACHE_HASH_SIZE,
				      (fget_hashfunc_t)_dcent_hashfunc);
	ftp->ftp_dc_age_l = fget_list_new(LIST_QUEUE, NULL);
}


/*
** _ftp_dircache_free() - free the entire directory cache
*/
void
_ftp_dircache_free(FTP *ftp)
{
	if (ftp->ftp_dc_h != NULL)
		fget_hash_free(ftp->ftp_dc_h, (fget_freefunc_t)_dcent_free);
	ftp->ftp_dc_h = NULL;
	if (ftp->ftp_dc_age_l != NULL)
		fget_list_free(ftp->ftp_dc_age_l, NULL);
	ftp->ftp_dc_age_l = NULL;
}


/*
** _ftp_dircache_find_file() - get file info from directory cache
** Returns:
**	0			success
**	-1 (and sets errno)	failure
**
** Side effects:
**	sets *fipp to point to the info for the requested file
**
** Notes:
**	caller MUST NOT MODIFY the data "returned" in *fipp
*/
int
_ftp_dircache_find_file(FTP *ftp, char *pathname, unsigned short flags,
		        file_info_t **fipp)
{
	dcent_t *parent_dcp = NULL;
	dcent_t *dcp = NULL;
	unsigned long level;
	char buf[MAXPATHLEN];
	char path[MAXPATHLEN];
	char path_dirname[MAXPATHLEN];
	char path_basename[MAXPATHLEN];
	file_info_t *fip;

#ifdef DEBUG
	printf("==> _ftp_dircache_find_file(ftp=0x%lx (%s), pathname=\"%s\", "
	       "flags=%d, fipp=0x%lx)\n",
	       ftp, fget_netio_get_host(ftp->ftp_control),
	       pathname, flags, fipp);
#endif

	/* run cache expiration */
	_dc_expire(ftp);

	if (strcmp(pathname, "/") == 0)
		strlcpy(path, pathname, sizeof(path));
	else
	{
		/*
		** allow last component of pathname to escape canonification
		** (this allows us to return data for "." and "..")
		*/
		_ftp_abspath(ftp, dirname(pathname), buf, sizeof(buf));
		snprintf(path, sizeof(path), "%s/%s", buf, basename(pathname));
	}

	for (level = 0;
	     level < MAXSYMLINKS;
	     level++)
	{
#ifdef DEBUG
		printf("    _ftp_dircache_find_file(): TOP OF LOOP: "
		       "level=%lu, path=\"%s\"\n",
		       level, path);
#endif

		/* reset at beginning of loop */
		parent_dcp = NULL;
		dcp = NULL;

		/* save dirname and basename */
		strlcpy(path_dirname, dirname(path), sizeof(path_dirname));
		strlcpy(path_basename, basename(path), sizeof(path_basename));

		/*
		** Step 1: check cache for parent directory
		**
		** Note: we skip this step for the root directory,
		**       since root doesn't really have a parent
		*/
		if (strcmp(path, "/") != 0
		    && _dc_get(ftp->ftp_dc_h, path_dirname, &parent_dcp,
			       DCENT_DIR) == 1)
		{
			switch (parent_dcp->dc_type)
			{
			case DCENT_FILE:
				fip = parent_dcp->dc_data.dc_fip;

				/*
				** if it's a symlink, it presumably
				** points to the real directory, but
				** it doesn't really help us.
				** so, we treat it as a cache miss.
				*/
				if (S_ISLNK(fip->fi_stat.fs_mode))
				{
					parent_dcp = NULL;
					break;
				}

				/*
				** if the parent is a DCENT_FILE but not
				** a symlink, then fail
				**
				** FIXME: is this too much of an assumption?
				*/
#ifdef DEBUG
				printf("<== _ftp_dircache_find_file(): "
				       "FAILED: ENOTDIR in step 1\n");
#endif
				errno = ENOTDIR;
				return -1;

			case DCENT_DIR:
				if (_dcent_find_file(parent_dcp->dc_data.dc_files_h,
						     path_basename, fipp) == 1)
					goto gotcha;
				break;

			default:
				/*
				** can't happen
				** treat this as if nothing was found
				*/
				parent_dcp = NULL;
				break;
			}

			/*
			** if we get here, the parent must be
			** a DCENT_DIR that does not contain an entry
			** for path_basename
			**
			** in this case, we just fall through and don't
			** worry about it, since we don't want to make
			** too many assumptions about what the server
			** may be hiding
			*/
		}

		/*
		** if we get here, the parent was either not
		** found in the cache, or it is a DCENT_DIR
		** with no path_basename entry
		*/

		/*
		** Step 2: check cache for entry with the full path
		*/
		if (_dc_get(ftp->ftp_dc_h, path, &dcp, DCENT_FILE) == 1)
		{
			switch (dcp->dc_type)
			{
			case DCENT_FILE:
				/* if it's a file, we've got our man */
				*fipp = dcp->dc_data.dc_fip;
				goto gotcha;

			case DCENT_DIR:
				/*
				** try checking whether the directory
				** contains a "." entry
				*/
				if (_dcent_find_file(dcp->dc_data.dc_files_h,
						     ".", fipp) == 1)
					goto gotcha;
				break;

			default:
				/*
				** can't happen
				** treat this as if nothing was found
				*/
				dcp = NULL;
				break;
			}

			/*
			** if we get here, the full path must be
			** a DCENT_DIR with no "." entry
			*/
		}

		/*
		** if we get here, the full path was either not
		** found in the cache, or it is a DCENT_DIR
		** with no "." entry
		*/

		/*
		** Step 3: get parent directory from server
		**         (if not already cached)
		**
		** Note: we skip this step for the root directory,
		**       since root doesn't really have a parent
		*/
		if (parent_dcp == NULL
		    && strcmp(path, "/") != 0)
		{
#ifdef DEBUG
			printf("    _ftp_dircache_find_file(): "
			       "getting parent dir from server\n");
#endif
			if (_dc_query_server(ftp, path_dirname,
					     &parent_dcp, DCENT_DIR) == -1)
			{
#ifdef DEBUG
				printf("<== _ftp_dircache_find_file(): "
				       "FAILED: no data from server\n");
#endif
				return -1;
			}

			switch (parent_dcp->dc_type)
			{
			case DCENT_FILE:
				/*
				** FIXME: should we check for symlinks here?
				** are there any cases where the server would
				** return a DCENT_FILE when we ask for a
				** DCENT_DIR?  (see _ftp_list() code)
				*/
#if 0
				fip = parent_dcp->dc_data.dc_fip;
				if (S_ISLNK(fip->fi_stat.fs_mode))
				{
				}
#endif

				/*
				** if the parent is a DCENT_FILE but not
				** a symlink, then fail
				**
				** FIXME: is this too much of an assumption?
				*/
#ifdef DEBUG
				printf("<== _ftp_dircache_find_file(): "
				       "FAILED: ENOTDIR in step 3\n");
#endif
				errno = ENOTDIR;
				return -1;

			case DCENT_DIR:
				/*
				** check for child's entry in parent directory
				*/
				if (_dcent_find_file(parent_dcp->dc_data.dc_files_h,
						     path_basename, fipp) == 1)
					goto gotcha;
				break;
	
			default:
				/*
				** can't happen
				*/
				break;
			}

			/*
			** if we get here, the parent must be
			** a DCENT_DIR with no path_basename entry
			*/
		}

		/*
		** Step 4: get full path from server
		**         (if not already cached)
		*/
		if (dcp == NULL)
		{
#ifdef DEBUG
			printf("    _ftp_dircache_find_file(): "
			       "getting full path from server\n");
#endif
			if (_dc_query_server(ftp, path, &dcp,
					     DCENT_FILE) == -1)
			{
#ifdef DEBUG
				printf("<== _ftp_dircache_find_file(): FAILED: "
				       "no data from server\n");
#endif
				return -1;
			}

			switch (dcp->dc_type)
			{
			case DCENT_FILE:
				/* if it's a file, we've got our man */
				*fipp = dcp->dc_data.dc_fip;
				goto gotcha;

			case DCENT_DIR:
				/* try checking the "." entry */
				if (_dcent_find_file(dcp->dc_data.dc_files_h,
						     ".", fipp) == 1)
					goto gotcha;
				break;

			default:
				/*
				** can't happen
				*/
				break;
			}

			/*
			** if we get here, the full path must be
			** a DCENT_DIR with no "." entry
			*/
		}

		/*
		** if we get through to here, then the path is a
		** DCENT_DIR with no "." entry, and we've exhausted
		** our options for getting info on the directory itself.
		**
		** one of two things is happening:
		**
		**  (1) the full path is a DCENT_DIR with no "." entry
		**      and the parent is a DCENT_DIR with no
		**      path_basename entry.
		**
		**      this "can't happen", but I wouldn't put it past
		**      some servers I've encountered...
		**
		**  (2) the path requested is "/" (i.e., no parent
		**      directory to check), and it is a DCENT_DIR
		**      with no "." entry.
		**
		**      this is ugly, but it's somewhat common on
		**      servers that don't grok ls(1) options
		**      as arguments to a LIST request.
		**
		** in either case, we know that the requested path does
		** actually exist, so we punt by making up some info.
		*/

		*fipp = (file_info_t *)calloc(1, sizeof(file_info_t));
		if (*fipp == NULL)
		{
#ifdef DEBUG
			printf("<== _ftp_dircache_find_file(): "
			       "FAILED while creating dummy directory "
			       "stub entry: calloc(): %s\n",
			       strerror(errno));
#endif
			return -1;
		}

		/*
		** all fields were initialized to 0 by calloc(),
		** so just set the ones we care about
		*/
		strlcpy((*fipp)->fi_filename, path_basename,
			sizeof((*fipp)->fi_filename));
		(*fipp)->fi_stat.fs_mode = S_IFDIR|0555;
		strlcpy((*fipp)->fi_stat.fs_username, "-1",
			sizeof((*fipp)->fi_stat.fs_username));
		strlcpy((*fipp)->fi_stat.fs_groupname, "-1",
			sizeof((*fipp)->fi_stat.fs_groupname));

		/* create new entry */
		if (_dcent_new(&dcp, path, DCENT_FILE, *fipp) == -1)
		{
			free(*fipp);
			*fipp = NULL;
#ifdef DEBUG
			printf("<== _ftp_dircache_find_file(): "
			       "FAILED while creating dummy directory "
			       "stub entry: _dcent_new(): %s\n",
			       strerror(errno));
#endif
			return -1;
		}

		/* insert new entry */
		_dc_insert(ftp, dcp);

#ifdef DEBUG
		printf("<== _ftp_dircache_find_file(): "
		       "returning dummy directory stub entry\n");
#endif
		return 0;

  gotcha:
		/*
		** now fipp points to the right thing, regardless of
		** whether it came from the cache or whether we
		** just got it from the server
		*/

		/*
		** if it's not a link, or if we're not following links,
		** then break out of the loop
		*/
		if (! S_ISLNK((*fipp)->fi_stat.fs_mode)
		    || BIT_ISSET(flags, DC_NOFOLLOWLINKS))
			break;

		/*
		** otherwise, follow symlink and try again
		*/
		if ((*fipp)->fi_linkto[0] != '/')
			snprintf(buf, sizeof(buf), "%s/%s",
				 path_dirname, (*fipp)->fi_linkto);
		else
			strlcpy(buf, (*fipp)->fi_linkto, sizeof(buf));
		fget_cleanpath(buf, path, sizeof(path));

#ifdef DEBUG
		printf("    _ftp_dircache_find_file(): "
		       "following symlink; new path is: \"%s\"\n",
		       path);
#endif
	}

	if (level >= MAXSYMLINKS)
	{
#ifdef DEBUG
		printf("<== _ftp_dircache_find_file(): FAILED: ELOOP\n");
#endif
		errno = ELOOP;
		return -1;
	}

#ifdef DEBUG
	printf("<== _ftp_dircache_find_file(): success\n");
#endif
	return 0;
}


/*
** _ftp_dircache_find_dir() - get contents of a directory
** Returns:
**	0			success
**	-1 (and sets errno)	failure
**
** Side effects:
**	sets *dir_hp to point to the contents of the directory
**
** Notes:
**	caller MUST NOT MODIFY the data "returned" in *dir_hp
*/
int
_ftp_dircache_find_dir(FTP *ftp, char *pathname, fget_hash_t **dir_hp)
{
	dcent_t *parent_dcp = NULL;
	dcent_t *dcp = NULL;
	char buf[MAXPATHLEN];
	char path[MAXPATHLEN];
	char path_dirname[MAXPATHLEN];
	char path_basename[MAXPATHLEN];
	file_info_t *fip = NULL;

#ifdef DEBUG
	printf("==> _ftp_dircache_find_dir(ftp=0x%lx (%s), pathname=\"%s\", "
	       "dir_hp=0x%lx)\n",
	       ftp, fget_netio_get_host(ftp->ftp_control), pathname, dir_hp);
#endif

	/* run cache expiration */
	_dc_expire(ftp);

	_ftp_abspath(ftp, pathname, path, sizeof(path));

	/* save dirname and basename */
	strlcpy(path_dirname, dirname(path), sizeof(path_dirname));
	strlcpy(path_basename, basename(path), sizeof(path_basename));

	/*
	** Step 1: check cache for full path
	*/
	if (_dc_get(ftp->ftp_dc_h, path, &dcp, DCENT_DIR) == 1)
	{
		switch (dcp->dc_type)
		{
		case DCENT_DIR:
			/* if it's a directory, we've got our man */
			goto gotcha;

		case DCENT_FILE:
			fip = dcp->dc_data.dc_fip;

			/*
			** if it's a directory stub or
			** a symlink (presumably to a directory),
			** we might still be able to get the
			** directory contents
			*/
			if (S_ISDIR(fip->fi_stat.fs_mode)
			    || S_ISLNK(fip->fi_stat.fs_mode))
				break;

			/*
			** otherwise, fail with ENOTDIR
			*/
#ifdef DEBUG
			printf("<== _ftp_dircache_find_dir(): "
			       "FAILED: ENOTDIR in step 1\n");
#endif
			errno = ENOTDIR;
			return -1;

		default:
			/*
			** can't happen
			*/
			break;
		}

		/*
		** if we get here, then the full path is DCENT_FILE
		** entry for a directory or a symlink
		*/
	}

	/*
	** if we get here, then the full path is either not
	** cached or is a DCENT_FILE entry for
	** a directory or a symlink
	*/

	/*
	** Step 2: check cache for parent directory
	**
	** Note: we skip this step for the root directory,
	**       since root doesn't really have a parent
	*/
	if (strcmp(path, "/") == 0
	    && _dc_get(ftp->ftp_dc_h, path_dirname, &parent_dcp,
		       DCENT_DIR) == 1)
	{
		switch (parent_dcp->dc_type)
		{
		case DCENT_DIR:
			/*
			** if it's a directory, search for
			** path_basename
			*/
			if (_dcent_find_file(parent_dcp->dc_data.dc_files_h,
					     path_basename, &fip) == 1)
			{
				if (S_ISDIR(fip->fi_stat.fs_mode)
				    || S_ISLNK(fip->fi_stat.fs_mode))
					break;

#ifdef DEBUG
				printf("<== _ftp_dircache_find_dir(): "
				       "FAILED: parent directory lists "
				       "path as a non-directory: "
				       "ENOTDIR in step 2\n");
#endif
				errno = ENOTDIR;
				return -1;
			}
			break;

		case DCENT_FILE:
			fip = dcp->dc_data.dc_fip;

			/*
			** if it's a directory stub or
			** a symlink (presumably to a directory),
			** we might still be able to get the
			** directory contents
			*/
			if (S_ISDIR(fip->fi_stat.fs_mode)
			    || S_ISLNK(fip->fi_stat.fs_mode))
				break;

			/*
			** otherwise, fail with ENOTDIR
			*/
#ifdef DEBUG
			printf("<== _ftp_dircache_find_dir(): "
			       "FAILED: parent directory found "
			       "as a DCENT_FILE: ENOTDIR in step 2\n");
#endif
			errno = ENOTDIR;
			return -1;

		default:
			/*
			** can't happen
			*/
			break;
		}

		/*
		** if we get here, then the full path is DCENT_FILE
		** entry for a directory or a symlink
		*/
	}

	/*
	** Step 3: get listing from server
	*/
	if (_dc_query_server(ftp, path, &dcp, DCENT_DIR) == -1)
	{
#ifdef DEBUG
		printf("<== _ftp_dircache_find_dir(): FAILED: "
		       "no data from server\n");
#endif
		return -1;
	}

	switch (dcp->dc_type)
	{
	case DCENT_DIR:
		/* if it's a directory, we've got our man */
		goto gotcha;

	case DCENT_FILE:
		/* if it's a file, fail */
#ifdef DEBUG
		printf("<== _ftp_dircache_find_dir(): "
		       "FAILED: ENOTDIR in step 3\n");
#endif
		errno = ENOTDIR;
		return -1;

	default:
		/*
		** can't happen
		*/
#ifdef DEBUG
		printf("<== _ftp_dircache_find_dir(): "
		       "FAILED: ENOSYS in step 3\n");
#endif
		errno = ENOSYS;
		return -1;
	}

  gotcha:
	*dir_hp = dcp->dc_data.dc_files_h;
#ifdef DEBUG
	printf("<== _ftp_dircache_find_dir(): success\n");
#endif
	return 0;
}




syntax highlighted by Code2HTML, v. 0.9.1