#ifndef lint
static char SCCSID[] = "@(#) duentry.c 1.18 93/08/18 00:09:03";
#endif

/*
 * "du" enhanced disk usage summary - version 2.
 *
 * Copyright 1990-1993, Unicom Systems Development.  All rights reserved.
 * See accompanying README file for terms of distribution and use.
 *
 * Edit at tabstops=4.
 */

#include "config.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#ifdef USE_UNISTD
# include <unistd.h>
#endif
#include <sys/types.h>
#include <sys/stat.h>
#include "du.h"

#ifdef USE_DIR_DIRENT
#	include <dirent.h>
#endif
#ifdef USE_DIR_SYSNDIR
#	include <sys/ndir.h>
#	define dirent direct
#endif
#ifdef USE_DIR_SYSDIR
#	include <sys/dir.h>
#	define dirent direct
#endif

#ifndef USE_SYMLINKS
#	define lstat stat
#endif


/*
 * Determine maximum pathlength of an entry on the filesystem containing "file".
 */
int max_path_len(fname)
register char *fname;
{
#ifdef USE_PATHCONF
	register int len;
	if ((len = pathconf(fname, _PC_PATH_MAX)) > 0)
		return len;
#ifdef notdef
	errmssg(ERR_WARN, errno, "could not get max pathname for \"%s\"", fname);
#endif
#endif

/*
 * If pathconf() is not available or fails, we need to take a stab.  If
 * the POSIX PATH_MAX parameter is available we will use that.  If not,
 * we will try to use the MAXNAMLEN that should be in whatever header
 * file USE_DIR_XXXX pulled in.  If all that fails, we'll just make a
 * guess of 1024.  Note that we do NOT want to use _POSIX_PATH_MAX.  That
 * specifies the minimum value that a conformant system must support.
 */
#ifndef PATH_MAX
# ifdef MAXNAMLEN
#  define PATH_MAX MAXNAMLEN
# else
#  define PATH_MAX 1024
# endif
#endif
	return PATH_MAX;
}


/*
 * du_entry - Initiate a disk usage report for a specified filesystem entry.
 *
 * This routine is called once per command line argument to setup a disk
 * usage scan.  This routine prepares to call "du_dir()" recursively to
 * scan through a directory to accumulate its usage.
 */
void du_entry(entry, ent_usage)
char *entry;					/* pathname of the entry to check		*/
struct dusage *ent_usage;		/* space in which to return disk usage	*/
{
	struct stat sbuf;
	struct fsinfo *fsinfo;
	static char *curr_dir = NULL;

#ifdef TIMEOUT
	alarm(TIMEOUT);
#endif

	/*
	 * Get the information on this entry.
	 */
	if (lstat(entry, &sbuf) != 0) {
		errmssg(ERR_WARN, errno, "could not stat \"%s\"", entry);
		return;
	}
	if ((fsinfo = fs_getinfo(&sbuf)) == NULL) {
		errmssg(ERR_WARN, errno,
			"could not get filesystem info for \"%s\"", entry);
		return;
	}

	/*
	 * If this isn't a directory then simply get its size.  Note that we
	 * won't say what size is unless "-s" or "-a" was specified.  Kind of
	 * silly, but that's documented behaviour.
	 */
	if (!S_ISDIR(sbuf.st_mode)) {
		set_usage(ent_usage, sbuf.st_mtime, fs_numblocks(fsinfo, &sbuf));
		if (Handle_output != PR_DIRS_ONLY)
			print_usage(entry, ent_usage);
		return;
	}

#ifdef OBSOLETEOPT
	/*
	 * If we aren't supposed to descend into directories then just get
	 * it's size.
	 */
	if (!Do_descend_dirs) {
		set_usage(ent_usage, sbuf.st_mtime, fs_numblocks(fsinfo, &sbuf));
		print_usage(entry, ent_usage);
		return;
	}
#endif

	/*
	 * We are about to descend down this directory entry.  We need to
	 * know where we started out so that we can return when done.
	 */
	if (curr_dir == NULL) {
		int len;
		extern char *getcwd();
		len = max_path_len(".") + 2; /* getcwd() wants two extra bytes */
		curr_dir = (char *) xmalloc((unsigned)len);
		if (errno = 0, getcwd(curr_dir, len) == NULL)
			errmssg(ERR_ABORT, errno,
				"could not get current working directory");
	}

	/*
	 * We need to initiate a recursive search through "du_dir()".  Normally
	 * "du_dir()" displays the disk usage of the directory scanned, except
	 * when "PR_TOTALS_ONLY" (-s) mode is set.  In that case it won't
	 * print anything at all, so we need to do it here.
	 */
	zero_usage(ent_usage);
	if (chdir(entry) != 0) {
		errmssg(ERR_WARN, errno, "could not chdir to \"%s\"", entry);
		return;
	}
	if (du_dir(entry, &sbuf, fsinfo, ent_usage) == 0) {
		if (Handle_output == PR_TOTALS_ONLY)
			print_usage(entry, ent_usage);
	}
	if (chdir(curr_dir) != 0)
		errmssg(ERR_ABORT, errno, "could not chdir back to \"%s\"", curr_dir);

}


/*
 * du_dir() - Scan a specified directory and accumulate its disk usage.
 *
 * This routine recursively descends a directory tree accumulating and
 * printing the disk usage.  The current working directory must be set
 * to the directory to be scanned before this routine is called.  If the
 * scan is successful, 0 will be returned and the accumlated usage
 * will be stored in "dir_usage".  If an error occurs which prevents
 * further processing, a -1 result will be returned and the usage
 * results should be ignored as bogus.
 */
int du_dir(dir_name, dir_statp, dir_fsinfo, dir_usage)
char *dir_name;					/* pathname to the directory to scan	*/
struct stat	*dir_statp;			/* inode info for this directory		*/
struct fsinfo *dir_fsinfo;		/* filesys info for this directory		*/
register struct dusage *dir_usage; /* space in which to accumulate usage*/
{
	register struct dirent *dp;	/* current dir entry being checked		*/
	struct stat ent_stat;		/* inode info for this entry			*/
	struct fsinfo *ent_fsinfo;	/* filesys info for this entry			*/
	long numblocks;				/* num disk blocks used by this entry	*/
	struct dusage ent_usage;	/* disk usage by this entry				*/
	char *ent_pathname;			/* full pathname of this entry			*/
	register char *ent_basename;/* pointer into "ent_pathname[]"		*/
	DIR *dirp;					/* stream for directory being scanned	*/

	/*
	 * Allocate space to hold the pathname.  If you've got a good malloc
	 * this isn't too painful.  I profiled malloc() at 0.4% overhead.
	 */
	ent_pathname = (char *) xmalloc((unsigned)dir_fsinfo->path_max+1);

	/*
	 * Initialize the block count with the usage of the directory itself.
	 */
	numblocks = fs_numblocks(dir_fsinfo, dir_statp);
	set_usage(dir_usage, dir_statp->st_mtime, numblocks);

	/*
	 * Setup a buffer to hold the full pathname of the entry being examined.
	 * Unless this directory is the root directory "/", append a slash
	 * to the name.  When we need the full pathname, we just need to place
	 * the filename at the location pointed to by "ent_basename".
	 */
	ent_basename = strcpy(ent_pathname, dir_name) + strlen(dir_name);
	if (ent_pathname[0] != '/' || ent_pathname[1] != '\0')
		*ent_basename++ = '/';

	/*
	 * Open up the directory so we can scan it.
	 */
	if ((dirp = opendir(".")) == NULL) {
		errmssg(ERR_WARN, errno, "could not open dir \"%s\"", dir_name);
		(void) free((PTRTYPE *)ent_pathname);
		return -1;
	}

	/*
	 * Go through each entry in the directory.
	 */
	while ((dp = readdir(dirp)) != NULL) {

#ifdef TIMEOUT
		alarm(TIMEOUT);
#endif

		/*
		 * Skip the "." and ".." entries.
		 */
		if (
			dp->d_name[0] == '.' && (
				dp->d_name[1] == '\0' ||
				(dp->d_name[1] == '.' && dp->d_name[2] == '\0')
			)
		) {
			continue;
		}

		/*
		 * Get the information on this entry.
		 */
		if (lstat(dp->d_name, &ent_stat) != 0) {
			errmssg(ERR_WARN, errno,
				"could not stat \"%s/%s\"", dir_name, dp->d_name);
			continue;
		}

		/*
		 * If it's not a directory then just accumulate its disk usage.
		 */
		if (!S_ISDIR(ent_stat.st_mode)) {

			/*
			 * See what to do with entries with multiple links.
			 */
			if (ent_stat.st_nlink > 1) {
				switch (Handle_links) {
				case LK_COUNT_FIRST:		/* count only first link */
					if (fs_linkdone(dir_fsinfo, &ent_stat))
						continue;
					break;
				case LK_COUNT_ALL:			/* count all hard links */
					break;
				case LK_COUNT_NONE:			/* skip all hard links */
					continue;
				case LK_COUNT_AVERAGE:		/* average usage over links */
					if (S_ISREG(ent_stat.st_mode)) {
						long nb = fs_numblocks(dir_fsinfo, &ent_stat);
						numblocks = nb / ent_stat.st_nlink;
						if (!fs_linkdone(dir_fsinfo, &ent_stat))
							numblocks += (nb % ent_stat.st_nlink);
						goto got_numblocks;
					}
					break;
				default:
					errmssg(ERR_ABORT, 0,
						"internal error - bad \"Handle_links\" value \"%d\"",
						Handle_links);
				}
			}

			/*
			 * Get the usage of this entry and accumulate into the dir usage.
			 */
			numblocks = fs_numblocks(dir_fsinfo, &ent_stat);
got_numblocks:
			set_usage(&ent_usage, ent_stat.st_mtime, numblocks);
			add_usage(dir_usage, &ent_usage);
			if (Handle_output == PR_EVERYTHING) {
				(void) strcpy(ent_basename, dp->d_name);
				print_usage(ent_pathname, &ent_usage);
			}

		} else {

			/*
			 * We are probably going to need the pathname, so fill it out now.
			 */
			(void) strcpy(ent_basename, dp->d_name);

			/*
			 * See if we are crossing a mount point -- and if so check if we
			 * want to process it.  Furthermore, if we cross a mount point then
			 * we need to get the filesystem information on the subdirectory.
			 * If we aren't crossing a mount point then the current filesystem
			 * info applies.
			 */
			if (dir_statp->st_dev != ent_stat.st_dev) {
				if ((ent_fsinfo = fs_getinfo(&ent_stat)) == NULL) {
					errmssg(ERR_WARN, errno,
						"could not get filesystem info for \"%s\"",
						ent_pathname);
					continue;
				}
				switch (Handle_filesys) {
				case FS_ALWAYS_CROSS:		/* cross mount points */
					break;
				case FS_NEVER_CROSS:		/* do not cross mount points */
					continue;
				case FS_LOCAL_ONLY:			/* do not cross rmt mount points */
					if (ent_fsinfo->remote)
						continue;
					break;
				default:
					errmssg(ERR_ABORT, 0,
						"internal error - bad \"Handle_filesys\" value \"%d\"",
						Handle_filesys);
				}
			} else {
				ent_fsinfo = dir_fsinfo;
			}

			/*
			 * Recursively get the usage on this directory.
			 */
			if (chdir(ent_basename) != 0) {
				errmssg(ERR_WARN, errno,
					"could not chdir to \"%s\"", ent_pathname);
				continue;
			}
			if (du_dir(ent_pathname, &ent_stat, ent_fsinfo, &ent_usage) == 0) {
				if (Do_accum_subdirs)
					add_usage(dir_usage, &ent_usage);
			}
			if (chdir("..") != 0) {
				errmssg(ERR_ABORT, errno,
					"could not chdir back to \"%s\"", dir_name);
			}

		}

	}

	/*
	 * The current directory is complete.
	 */
	(void) closedir(dirp);
	if (Handle_output != PR_TOTALS_ONLY)
		print_usage(dir_name, dir_usage);
	(void) free((PTRTYPE *)ent_pathname);
	return 0;
}

