#ifndef lint
static char SCCSID[] = "@(#) fsinfo.c 1.25 93/08/18 00:09:06";
#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.
 */

/*
 * All of the machine-specific filesystem support should be encapsulated
 * within this one file.  We maintain the necessary filesystem information
 * as a dynamically allocated list of (struct fsinfo) records, one record
 * per mounted filesystem.
 *
 * There are four routines for working with filesystem information.
 *
 * fs_initinfo() - Initialize the "Fsinfo_list" filesystem information.
 * fs_getinfo() - Gets filesystem information on a entry.
 * fs_linkdone() - Determines whether a file has been visited already.
 * fs_numblocks() - Calculates disk usage of an entry.
 *
 * One of the trickiest tasks is discovering what filesystems are mounted.
 * This is *highly* system specific.  We also provide a set of three
 * routines -- open_mnttab(), read_mnttab(), and close_mnttab() -- for
 * fs_initinfo() to use to get this information.
 */

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

/*
 * Within each filesystem information record, we maintain a dynamically
 * allocated bit vector that indicates which inodes have been already
 * visited on the filesystem.  If the system has a working statfs() call
 * then we can preallocate the bit vector to precisely the right size.
 * If statfs() is missing (e.g. old SysV) or broken (e.g. the Neurotic
 * File System) we will instead make a guess and grow the bit vector as
 * needed.   In this case, the allocation increment will be in chunks of
 * ITABINCR/8 bytes (since one byte will hold the status of 8 inodes).
 * Selection of this value is a simple speed vs memory tradeoff.
 */
#define ITABINCR 8192

/*
 * Length of bit vector required for a particular number of inodes.
 */
#define NUMINO_TO_VECLEN(NUMINO) ((NUMINO)/8 + 1)

/*
 * Can you believe this shit?
 */
#ifdef USE_MOUNT_MNTTAB
#	include <mnttab.h>
#	define mount_struct mnttab
#	define mount_device mt_dev
#	define mount_point  mt_filsys
#endif
#ifdef USE_MOUNT_R4MNTTAB
#	include <sys/mnttab.h>
#	define mount_struct mnttab
#	define mount_device mnt_special
#	define mount_point  mnt_mountp
#endif
#ifdef USE_MOUNT_MNTENT
#	include <mntent.h>
#	define mount_struct mntent
#	define mount_device mnt_fsname
#	define mount_point  mnt_dir
#endif
#ifdef USE_MOUNT_FSTAB
#	include <fstab.h>
#	define mount_struct fstab
#	define mount_device fs_spec
#	define mount_point  fs_file
#endif
#ifdef USE_MOUNT_MNTCTL
#	include <sys/mntctl.h>
#	include <sys/vmount.h>
	struct mount_struct {
		char *mount_device;
		char *mount_point;
	};
#endif

/*
 * sigh...
 */
#ifdef USE_STATFS_SYSV
#	include <sys/statfs.h>
#	define STATFS(MNT, PTR, PSIZE, FSTYPE) \
		statfs(MNT, PTR, PSIZE, FSTYPE)
#endif
#ifdef USE_STATFS_BSD
#	include <sys/mount.h>
#	define STATFS(MNT, PTR, PSIZE, FSTYPE) \
		statfs(MNT, PTR)
#endif
#ifdef USE_STATFS_SUN
#	include <sys/vfs.h>
#	define STATFS(MNT, PTR, PSIZE, FSTYPE) \
		statfs(MNT, PTR, PSIZE, FSTYPE)
#endif
#ifdef USE_STATFS_HPUX
#	include <sys/vfs.h>
#	define STATFS(MNT, PTR, PSIZE, FSTYPE) \
		statfs(MNT, PTR)
#endif
#ifdef USE_STATFS_NONE
#	include <sys/param.h>
	struct statfs {
		long f_bsize, f_files;
	};
#	define STATFS(MNT, PTR, PSIZE, FSTYPE) \
		((PTR)->f_bsize = BSIZE, (PTR)->f_files = 0, 0)
#endif


/*
 * Dynamically allocated list of filesystem information records.
 */
struct fsinfo **Fsinfo_list;
int Fsinfo_size;

/*
 * Mount table handling routines.
 */
int open_mnttab __ARGS((void));
struct mount_struct *read_mnttab __ARGS((void));
int close_mnttab __ARGS((void));


/*
 * fs_initinfo() - Initialize the "Fsinfo_list" filesystem information.
 */
void fs_initinfo()
{
	struct fsinfo *fsp;
	struct mount_struct *mnt;
	struct stat sbuf;
	struct statfs fsbuf;
	int n;

	Fsinfo_list = (struct fsinfo **) xmalloc(sizeof(struct fsinfo *));
	Fsinfo_list[0] = NULL;
	Fsinfo_size = 0;

	/*
	 * Open up the mount table.
	 */
	if (open_mnttab() != 0)
		errmssg(ERR_ABORT, errno, "could not open mount table");

	/*
	 * Initialize a filesystem information record for each mounted filesystem.
	 */
	while ((mnt = read_mnttab()) != NULL) {

		/*
		 * Get the information on this filesystem.
		 */
		if (stat(mnt->mount_point, &sbuf) != 0) {
			errmssg(ERR_WARN, errno, "could not stat \"%s\"", mnt->mount_point);
			continue;
		}
		if (STATFS(mnt->mount_point, &fsbuf, sizeof(struct statfs), 0) != 0) {
			errmssg(ERR_WARN, errno,
				"could not statfs \"%s\"", mnt->mount_point);
			continue;
		}

		/*
		 * Allocate and initialize the filesystem information structure.
		 * If "f_files" is zero then we take an initial guess at the
		 * number of inodes.  This will happen if this system does not
		 * have a statfs() call or this is an NFS filesystem.
		 */
		fsp = (struct fsinfo *) xmalloc(sizeof(struct fsinfo));
		fsp->dev = sbuf.st_dev;
		fsp->nino = (fsbuf.f_files > 0 ? fsbuf.f_files : ITABINCR);
		fsp->bsize = fsbuf.f_bsize;
		fsp->nindir = fsp->bsize / sizeof(daddr_t);
		fsp->remote = (sbuf.st_dev < 0) ||
			(strchr(mnt->mount_device, ':') != NULL);

#ifdef BROKE_STBLOCKS
		/* very cool algorithm by Lars Henrik Mathiesen <thorinn@diku.dk> */
		if (sbuf.st_blocks == 0) {
			errmssg(ERR_WARN, 0,
				"cannot determine \"%s\" stat blksize - assuming 512",
				mnt->mount_device);
			fsp->stbsize = 512;
		} else {
			int round, ratio;
			round = (sbuf.st_size + fsbuf.f_bsize - 1) & ~(fsbuf.f_bsize - 1);
			ratio = round / sbuf.st_blocks;
			for (fsp->stbsize = 512 ; fsp->stbsize < ratio ; fsp->stbsize <<= 1)
				;
		}
#endif

		/*
		 * Determine how long pathname buffers should be for
		 * entries on this filesystem.
		 */
		fsp->path_max = max_path_len(mnt->mount_point);

		/*
		 * Create the bit vector that is used by fs_linkdone() to track
		 * which inodes have already been encountered.
		 */
		n = NUMINO_TO_VECLEN(fsp->nino);
		fsp->idone = (unsigned char *) xmalloc((unsigned)n);
		(void) memset((PTRTYPE *)fsp->idone, 0, n);

		/*
		 * Attach the filesystem information to the end of the list.
		 */
		Fsinfo_list = (struct fsinfo **) xrealloc((PTRTYPE *)Fsinfo_list,
			(Fsinfo_size+2)*sizeof(struct fsinfo *));
		Fsinfo_list[Fsinfo_size++] = fsp;
		Fsinfo_list[Fsinfo_size] = NULL;

#ifdef DEBUG
		Dprintf(stderr, "*** mount_device=\"%s\" mount_point=\"%s\"\n",
			mnt->mount_device, mnt->mount_point);
		Dprintf(stderr, "  remote=%s dev=%d nino=%d bsize=%d nindir=%d\n",
			(fsp->remote ? "TRUE" : "FALSE"),
			fsp->dev, fsp->nino, fsp->bsize, fsp->nindir);
#ifdef BROKE_STBLOCKS
		Dprintf(stderr, "   stbsize=%d", fsp->stbsize);
#endif
		Dprintf(stderr, "   path_max=%d\n", fsp->path_max);
#endif

	}

	(void) close_mnttab();

}


/*
 * fs_getinfo() - Gets filesystem information on a entry.
 *
 * Given the inode info for an entry, locate the corresponding filesystem
 * info record and return a pointer to it.  The most recent result is cached
 * to reduce the number of list searches.
 */
struct fsinfo *fs_getinfo(sbufp)
register struct stat *sbufp;
{
	static struct fsinfo *fsp_save = NULL;
	register struct fsinfo **fsp;

	if (fsp_save != NULL && fsp_save->dev == sbufp->st_dev)
		return fsp_save;
	for (fsp = Fsinfo_list ; *fsp != NULL ; ++fsp) {
		if ((*fsp)->dev == sbufp->st_dev) {
			fsp_save = *fsp;
			return fsp_save;
		}
	}
	return (struct fsinfo *) NULL;
}


/*
 * fs_linkdone() - Determines whether a file has been visited already.
 *
 * This procedure implements the logic to avoid recounting of multiply
 * linked files.  Each (struct fsinfo) contains a bit vector to track
 * which inodes have already been visited.  The first time this procedure
 * is called for a particular inode, we return FALSE and mark it in the bit
 * vector.  TRUE is returned subsequent times this procedure is called for
 * the same inode.
 */
int fs_linkdone(fsp, sbufp)
register struct fsinfo *fsp;
register struct stat *sbufp;
{
	register unsigned char *rowp;
	int mask;
	unsigned old_bytes, new_bytes;

	/*
	 * Verify we haven't gone off the edge of the bit vector.  This
	 * could happen if we had to take an initial guess at the number
	 * of inodes.
	 */
	if (sbufp->st_ino > fsp->nino) {

		old_bytes = NUMINO_TO_VECLEN(fsp->nino);
#ifdef DEBUG
		Dprintf(stderr, "*** growing bit vector for device %d at inode %d\n",
			fsp->dev, sbufp->st_ino);
		Dprintf(stderr, "  old vector size %d bytes (%d inodes)\n",
			old_bytes, fsp->nino);
#endif
		while (sbufp->st_ino > fsp->nino)
			fsp->nino += ITABINCR;
		new_bytes = NUMINO_TO_VECLEN(fsp->nino);
#ifdef DEBUG
		Dprintf(stderr, "  new vector size %d bytes (%d inodes)\n",
			new_bytes, fsp->nino);
#endif

		fsp->idone =
			(unsigned char *)xrealloc((PTRTYPE *)fsp->idone, new_bytes);
		(void) memset((PTRTYPE *)(fsp->idone+old_bytes),
			0, new_bytes-old_bytes);
	}

	/*
	 * Locate the bit within the vector for this inode.
	 */
	rowp = fsp->idone + (sbufp->st_ino >> 3);
	mask = 1 << (sbufp->st_ino & 07);

	/*
	 * If the bit is set then this link was already done.
	 */
	if (*rowp & mask)
		return TRUE;

	/*
	 * Set the bit and indicate the link hasn't been done yet.
	 */
	*rowp |= mask;
	return FALSE;
}


/*
 * A classic UNIX filesys contains ten data block addresses in the inode.
 */
#define DIRBLKS		10

/*
 * Macro to calculate ceiling(A/B) pretty efficiently.
 */
#define CEIL_DIV(A, B)  (((A)+(B)-1) / (B))

/*
 * fs_numblocks() - Calculates disk usage of an entry.
 *
 * This routine is a profiling "hot spot".  It is called for every entry,
 * and about 15% of the time appears to be spent here.  Unfortunately, the
 * calculation this routine has to implement is apparently easy to botch given
 * the number of vendor's du's that get it wrong.  Therefore, I traded a
 * bit of speed for readability and clarity.  Even still, my benchmarking
 * tests show that it stands up well to other implementations.
 */
long fs_numblocks(fsp, sbufp)
register struct fsinfo *fsp;
struct stat *sbufp;
{

	register long	n_used;		/* num blocks used, incl overhead	*/

#ifdef USE_STBLOCKS /*{*/

	/*
	 * Ignore this entry if we are counting usage for a specific
	 * user and that user doesn't own this entry.
	 */
	if (Selected_user >= 0 && sbufp->st_uid != Selected_user)
		return 0L;

#ifdef BROKE_STBLOCKS
# define STBSIZE(fsp)	((fsp)->stbsize)
#else
# define STBSIZE(fsp)	(512)
#endif

	/*
	 * This is a piece of cake on systems with "st_blocks".
	 */
	if (Report_blksize == STBSIZE(fsp))
		n_used = sbufp->st_blocks;
	else if (Report_blksize == 0)
		n_used = CEIL_DIV(sbufp->st_blocks*STBSIZE(fsp), fsp->bsize);
	else
		n_used = CEIL_DIV(sbufp->st_blocks*STBSIZE(fsp), Report_blksize);

#else /*}!USE_STBLOCKS{*/

	register long	n_to_place;	/* num data blocks to be placed		*/
	long	n_single_ind;	/* scratch single indirect block cntr	*/
	long	n_double_ind;	/* scratch double indirect block cntr	*/

	/*
	 * Ignore this entry if we are counting usage for a specific
	 * user and that user doesn't own this entry.
	 */
	if (Selected_user >= 0 && sbufp->st_uid != Selected_user)
		return 0L;

	/*
	 * Determine the number of data blocks required to store this file.
	 */
	n_used = CEIL_DIV(sbufp->st_size, fsp->bsize);

	/*
	 * The first DIRBLKS addresses are stored directly in the inode
	 * and thus require no additional overhead.  Figure out how many
	 * data blocks remain to be placed through indirect addresses.
	 */
	n_to_place = n_used - DIRBLKS;

	/*
	 * If the file has DIRBLKS or less data blocks then the entire
	 * file can be stored in direct data blocks, there is no indirect
	 * block overhead, and thus we are done.
	 */
	if (n_to_place <= 0)
		goto done;

	/*
	 * With the single indirect block, we can get another "nindir" blocks.
	 */
	++n_used;
	n_to_place -= fsp->nindir;
	if (n_to_place <= 0)
		goto done;

	/*
	 * With the double indirect block, we can get another "nindir" single
	 * indirect blocks, for a total of another "nindir**2" data blocks.
	 */
	n_single_ind = CEIL_DIV(n_to_place, fsp->nindir);
	if (n_single_ind > fsp->nindir)
		n_single_ind = fsp->nindir;
	n_used += 1 + n_single_ind;
	n_to_place -= n_single_ind * fsp->nindir ;
	if (n_to_place <= 0)
		goto done;

	/*
	 * With the triple indirect block, we can get another "nindir" double
	 * indirect blocks, for another "nindir**2" single indirect blocks, for
	 * a total of another "nindir**3" data blocks.
	 */
	n_single_ind = CEIL_DIV(n_to_place, fsp->nindir);
	n_double_ind = CEIL_DIV(n_single_ind, fsp->nindir);
	n_used += 1 + n_double_ind + n_single_ind;

done:

	/*
	 * If required, convert from native blocksize to reporting blocksize.
	 */
	if (Report_blksize != 0 && Report_blksize != fsp->bsize)
		n_used = CEIL_DIV(n_used*fsp->bsize, Report_blksize);

#endif /*}!USE_STBLOCKS*/

	return n_used;

}


/*****************************************************************************
 *
 * Mount Table Access Routines
 *
 * int open_mnttab();
 *		Initialize for mount table scan.  Return zero on success.
 *
 * struct mount_struct *read_mnttab();
 *		Return information on next mount entry; NULL at end of table.
 *
 * int close_mnttab();
 *		Close the mount table.  Return zero on success, nonzero on error.
 *
 ****************************************************************************/


#ifdef USE_MOUNT_MNTTAB /*{*/

#ifndef MNTTAB
#	ifdef PNMNTTAB
#		define MNTTAB PNMNTTAB
#	else
#		define MNTTAB "/etc/mnttab"
#	endif
#endif

#ifndef ISMNTFREE
#	define ISMNTFREE(mp)	((mp)->mt_dev[0] == '\0')
#endif

static FILE *fpmnt;

int open_mnttab()
{
	return ((fpmnt = fopen(MNTTAB, "r")) != NULL ? 0 : -1);
}

struct mnttab *read_mnttab()
{
	static struct mnttab mbuf;
	while (fread((PTRTYPE *)&mbuf, sizeof(mbuf), 1, fpmnt) == 1) {
		if (!ISMNTFREE(&mbuf))
			return &mbuf;
	}
	return (struct mnttab *)NULL;
}

int close_mnttab()
{
	return fclose(fpmnt);
}

#endif /*} USE_MOUNT_MNTTAB*/


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


#ifdef USE_MOUNT_R4MNTTAB /*{*/

#ifndef MNTTAB
#	define MNTTAB "/etc/mnttab"
#endif

static FILE *fpmnt;

int open_mnttab()
{
	return ((fpmnt = fopen(MNTTAB, "r")) != NULL ? 0 : -1);
}

struct mnttab *read_mnttab()
{
	static struct mnttab mbuf;
	mntnull(&mbuf);
	return (getmntent(fpmnt, &mbuf) == 0 ? &mbuf : (struct mnttab *)NULL);
}

int close_mnttab()
{
	return fclose(fpmnt);
}

#endif /*} USE_MOUNT_R4MNTTAB*/


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


#ifdef USE_MOUNT_MNTENT /*{*/

#ifndef MNT_MNTTAB
#	define MNT_MNTTAB "/etc/mtab"
#endif

static FILE *fpmnt;

int open_mnttab()
{
	return ((fpmnt = setmntent(MNT_MNTTAB, "r")) != NULL ? 0 : -1);
}

struct mntent *read_mnttab()
{
	return getmntent(fpmnt);
}

int close_mnttab()
{
	return endmntent(fpmnt);
}

#endif /*}USE_MOUNT_MNTENT*/


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


#ifdef USE_MOUNT_FSTAB /*{*/

int open_mnttab()
{
	return (setfsent() ? 0 : -1);
}

struct fstab *read_mnttab()
{
	return getfsent();
}

int close_mnttab()
{
	endfsent();
	return 0;
}

#endif /*}USE_MOUNT_FSTAB*/


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


#ifdef USE_MOUNT_MNTCTL /*{*/

/*
 * Here is a fine example of what happens when you have ivory tower
 * doctorates instead of experienced system programmers designing stuff.
 */

static char *mntctl_buf;
static char *mntctl_entry;
static int mntctl_fscount;

int open_mnttab()
{
	unsigned n;

	if (mntctl(MCTL_QUERY, sizeof(n), (char *)&n) != 0)
		return -1;
	n += 1024;
	mntctl_buf = xmalloc(n);
	if ((mntctl_fscount = mntctl(MCTL_QUERY, n, mntctl_buf)) == -1)
		return -1;
	mntctl_entry = mntctl_buf;
	return 0;
}

struct mount_struct *read_mnttab()
{
	struct vmount *v;
	unsigned n, m;
	static struct mount_struct mbuf = { NULL, NULL };

	if (mntctl_fscount <= 0)
		return (struct mount_struct *) NULL;

	v = (struct vmount *)mntctl_entry;
	mntctl_entry += v->vmt_length;
	--mntctl_fscount;

	if (mbuf.mount_device != NULL)
		free(mbuf.mount_device);
	if (mbuf.mount_point != NULL)
		free(mbuf.mount_point);

	n = vmt2datasize(v, VMT_STUB);
	mbuf.mount_point = xmalloc(n+1);
	(void) strncpy(mbuf.mount_point, vmt2dataptr(v, VMT_STUB), n);
	mbuf.mount_point[n] = '\0';

	n = vmt2datasize(v, VMT_HOST);
	m = vmt2datasize(v, VMT_OBJECT);
	mbuf.mount_device = xmalloc(n+sizeof((char)':')+m+1);
	if (v->vmt_flags & MNT_REMOTE) {
		(void) strncpy(mbuf.mount_device, vmt2dataptr(v, VMT_HOST), n);
		mbuf.mount_device[n] = '\0';
		n = strlen(mbuf.mount_device);
		mbuf.mount_device[n++] = ':';
	} else {
		n = 0;
	}
	(void) strncpy(mbuf.mount_device+n, vmt2dataptr(v, VMT_OBJECT), m);
	mbuf.mount_device[n+m] = '\0';

	return &mbuf;
}

int close_mnttab()
{
	free((PTRTYPE *)mntctl_buf);
	return 0;
}

#endif /*} USE_MOUNT_MNTCTL*/

