#!mkcmd
# $Compile(*): mkcmd -n %F %f && %b -mCompile -d%s %F.c && rm %F.[ch]
# $Compile: mkcmd -n %F %f && %b -mCompile %F.c && rm %F.[ch]
comment "%c%kCompile(*): $%{cc-cc%} -o %%F %%f"
from "<stdio.h>"
from "<sys/types.h>"
from "<sys/stat.h>"
from "<time.h>"
from "<sys/mman.h>"
from "<sysexits.h>"
from "<fcntl.h>"
from "<unistd.h>"
from "<fts.h>"
from "<stdlib.h>"

from '"machine.h"'
from '"sparse.h"'

%i
static char rcsid[] =
	"$Id: quot.m,v 1.7 2007/01/02 15:45:52 ksb Exp $";
static int iErrs = EX_OK;	/* our exit code */
static void **ppvHistogram;	/* -c's sparse space 			*/
%%
require "util_errno.m"
require "std_help.m" "util_fts.m" "std_version.m"
require "util_errno.m"
basename "quot" ""

before 6 "SetTimes();"
boolean 'a' {
	named "fAllMounted"
	abort "exit(EX_UNAVAILABLE);"
	help "visit all mounted filesystems (not implemented)"
}
boolean 'c' {
	named "fHistogram"
	help "display a histogram of file sizes"
}
boolean 'f' {
	named "fCount"
	help "display number of files, in addition to total space"
}
boolean 'g' {
	named "fByGroup"
	help "display totals per group, rather than login"
}
# tends to under report on an ext2, by a lot.
boolean 'H' {
	hidden named "fHeuristic"
	help "use the size of the file, rather than the count of physical blocks"
}
boolean 'k' {
	named "fInKB"
	help "report sizes in kb (1024byte blocks rather than 512byte blocks)"
}
boolean 'n' {
	named "fFilter"
	abort "exit(EX_UNAVAILABLE);"
	help "filter ncheck output for inode number to owner mapping (not implemented)"
}
boolean 'U' {
	named "fNoUser"
	init "1"
	help "do not include the nouser (unmapped ids) output line"
}
boolean 'v' {
	named "fIdle"
	help "output the idle space per login"
}
augment action 'V' {
	user "Version();"
}
boolean 'N' {
	named "fHeader"
	init "1"
	help "supress the header line"
}
boolean 'X' {
	named "fXdev"
	help "honor cross a filesystem boundries"
}
after 6 "MapPasswd();"

key "fts_visit" 2 {
	"Summate" "%a->fts_statp" "%a->fts_info"
}

list {
	update "ReMap(%#, %@);"
	user '%ZK/ef;'
	param "files"
	help "file hierarchies or devices to quot"
	key 2 initialize {
		"u_fts_walk" "%a" "FTS_LOGICAL|FTS_PHYSICAL|(fXdev?FTS_XDEV:0)"
	}
}

exit {
	named "Output"
	abort "exit(iErrs);"
}

%c
static void
Version()
{
#if USE_MNTENT
	printf("%s: mounted filesystems read from \"%s\"\n", progname, acMounted);
#endif
#if USE_GETMNTINFO
	printf("%s: mounted filesystems getmntinfo(3)\n", progname);
#endif
}

static time_t t30, t60, t90;		/* times to compare against */

static void
SetTimes()
{
	auto time_t tNow;

	(void) time(& tNow);
	t30 = tNow - 30 * 24 * 60 * 60;
	t60 = tNow - 60 * 24 * 60 * 60;
	t90 = tNow - 90 * 24 * 60 * 60;
}

typedef struct QMnode {
	int muid;
	char *pclogin;
	unsigned long wcount;
	unsigned long w30, w60, w90, wtotal;
} QUOT_MAP;
static QUOT_MAP *pQMAll;
static unsigned int *puiOrder;
static unsigned int iLogins;
static unsigned int iMaxLoginLen;	/* for output */

/* set the order in the uid map to "by uid"				(ksb)
 */
static int
ByUid(const void *pvLeft, const void *pvRight)
{
	return pQMAll[*((unsigned int *)pvLeft)].muid - pQMAll[*((unsigned int *)pvRight)].muid;

}
/* set the order in the uid map to "by idle disk"			(ksb)
 */
static int
BySpace(const void *pvLeft, const void *pvRight)
{
	return pQMAll[*((unsigned int *)pvRight)].wtotal - pQMAll[*((unsigned int *)pvLeft)].wtotal;
}

/* map the password file in and build a sum node for each login		(ksb)
 */
static void
MapPasswd()
{
	register char *pcPasswd, *pcEol, *pcLast;
	register int fdPasswd;
	static char acPasswd[] = "/etc/passwd";
	static char acGroup[] = "/etc/group";
	auto struct stat stSize;
	register unsigned int iLines, i, iCurLoginLen;

	if (fHistogram) {
		ppvHistogram = sp_init(SP_MAX_KEY);
	}
	if (-1 == (fdPasswd = open(fByGroup ? acGroup : acPasswd, O_RDONLY, 0))) {
		fprintf(stderr, "%s: open: %s: %s\n", progname, fByGroup ? acGroup : acPasswd, strerror(errno));
		exit(EX_OSFILE);
	}
	if (0 != fstat(fdPasswd, & stSize)) {
		fprintf(stderr, "%s: fstat: %s\n", progname, strerror(errno));
		exit(EX_OSFILE);
	}
	pcPasswd = (char *)mmap((void  *)0, stSize.st_size, PROT_READ, MAP_SHARED, fdPasswd, (off_t)0);

	/* if the password file doesn't end in a '\n' we loose
	 */
	pcEol = (char *)0; /* stupid gcc */
	for (pcLast = pcPasswd, iLines = 0; stSize.st_size > 1; ++iLines, pcLast = pcEol) {
		pcEol = strchr(pcLast, '\n');
		pcEol++;
		stSize.st_size -= (pcEol - pcLast);
		pcLast = pcEol;
	}
	iLogins = iLines;
	pQMAll = (QUOT_MAP *)calloc(iLines+1, sizeof(QUOT_MAP));
	puiOrder = (unsigned int *)calloc(iLines+1, sizeof(unsigned int));
	if ((QUOT_MAP *)0 == pQMAll || (unsigned int *)0 == puiOrder) {
		fprintf(stderr, "%s: calloc: %d,%d: %s\n", progname, iLines, sizeof(QUOT_MAP), strerror(errno));
		exit(EX_OSERR);
	}
	pcLast = pcPasswd;
	/* bin:x:1:1:bin:/bin:/sbin/nologin\n
	 */
	iCurLoginLen = 0;
	for (i = 0; i < iLines; ++i, pcLast = pcEol) {
		register char *pcUid;

		pcEol = strchr(pcLast, '\n');
		pcEol++;
		pcUid = strchr(pcLast, ':')+1;
		if (pcUid - pcLast > iCurLoginLen) {
			iCurLoginLen = (pcUid-pcLast);
		}
		pcUid = strchr(pcUid, ':')+1;
		pQMAll[i].pclogin = pcLast;
		pQMAll[i].muid = atol(pcUid);
		pQMAll[i].wcount = 0UL;
		pQMAll[i].w30 = 0;
		pQMAll[i].w60 = 0;
		pQMAll[i].w90 = 0;
		pQMAll[i].wtotal = 0;
	}
	pQMAll[i].pclogin = fByGroup ? "-nogroup:\n" : "-nouser:\n";
	pQMAll[i].muid = 0;
	pQMAll[i].wcount = 0UL;
	pQMAll[i].w30 = 0;
	pQMAll[i].w60 = 0;
	pQMAll[i].w90 = 0;
	pQMAll[i].wtotal = 0;
	for (i = 0; i < iLines; ++i) {
		puiOrder[i] = i;
	}
	puiOrder[i] = i;
	qsort(puiOrder, iLines, sizeof(unsigned int), ByUid);
	iMaxLoginLen = iCurLoginLen-1;
}

/* find a uid in the billing table					(ksb)
 */
static QUOT_MAP *
FindUid(int iUid)
{
	register int uiHi, uiLow, uiCheck;
	register QUOT_MAP *pQM;

	uiHi = iLogins;	/* 1 past the end */
	uiLow = 0;	/* we might look here */
	while (uiHi != uiLow) {
		uiCheck = (uiLow+uiHi)/2;
		pQM = & pQMAll[puiOrder[uiCheck]];
		if (iUid == pQM->muid)
			return pQM;
		if (iUid < pQM->muid)
			uiHi = uiCheck;
		else if (uiLow == uiCheck)
			break;
		else
			uiLow = uiCheck;

	}

	/* make the "unknown uid" be us, so we hit the cache
	 */
	pQM = & pQMAll[iLogins];
	pQM->muid = iUid;
	return pQM;
}

/* Assuming we are in uid order (via puiOrder[]), find the given	(ksb)
 * uid (st_uid) in the array then bill the owner for the space.
 */
static int
Summate(struct stat *pst, int ftsInfo)
{
	static QUOT_MAP *pQMCache = (QUOT_MAP *)0;
	register QUOT_MAP *pQM;
	register int iUid;
	register off_t wBill, wBlock;
	register time_t tNode;

	wBlock = 0;
	switch (ftsInfo) {
	case FTS_DOT:	/* . (don't  count me twice */
	case FTS_DP:	/* A directory being visited in post-order  */
		return 0;

	case FTS_D: /* A directory being visited in pre-order. */
	case FTS_F: /* A regular file. */
	case FTS_SLNONE: /* symlink points with lstat info, sum it */
	case FTS_DEFAULT:
		wBlock = pst->st_blocks;
		iUid = fByGroup ? pst->st_gid : pst->st_uid;
		wBill = fHeuristic ? pst->st_size / 512 : pst->st_blocks;
		tNode = pst->st_atime;
		break;

	case FTS_SL:	/* symbolic link might count as a file? */
		iUid = fByGroup ? pst->st_gid : pst->st_uid;
		wBill = 0;
		tNode = t30+60;
		break;

	case FTS_DC:
	case FTS_DNR:	/* dir not +r */
		iErrs = EX_NOPERM;
		return 0;

	case FTS_NS:	/* no stat info  for this file, sigh */
	case FTS_NSOK:	/* no stat, but that's OK? Not here... */
	case FTS_ERR:	/* any other error */
	default:
		iErrs = EX_OSERR;
		return 0;
	}

	if (fHistogram) {
		register void **ppvFound;
		if ((void **)0 == (ppvFound = sp_index(ppvHistogram, wBlock))) {
			fprintf(stderr, "%s: no memory?\n", progname);
			exit(EX_OSERR);
		}
		if ((QUOT_MAP *)0 == (pQM = (QUOT_MAP *)*ppvFound)) {
			pQM = calloc(1, sizeof(QUOT_MAP));
			if ((QUOT_MAP *)0 == pQM) {
				fprintf(stderr, "%s: no memory?\n", progname);
				exit(EX_OSERR);
			}
			pQM->wtotal = 0;
			*ppvFound = (void *)pQM;
		}
		wBill = 1;
	} else if ((QUOT_MAP *)0 == (pQM = pQMCache) || iUid != pQM->muid) {
		pQMCache = pQM = FindUid(iUid);
	}
	pQM->wtotal += wBill;
	++pQM->wcount;
	/* -v (time features) */
	if (t30 < tNode) {
		/* nada, was accessed w/o 30 days */
	} else if (t60 < tNode) {
		pQM->w30 += wBill;
	} else if (t90 < tNode) {
		pQM->w60 += wBill;
	} else {
		pQM->w90 += wBill;
	}
	return 0;
}

/* output the histogram for -c from the collected size data		(ksb)
 */
static int
OutHist(sp_key wBlocks, void *pvQM)
{
	static sp_key wAgg = 0;
	register QUOT_MAP *pQM;

	pQM = (QUOT_MAP *)pvQM;
	wAgg += wBlocks * pQM->wcount;
	printf("%-10lu %-10lu ", wBlocks, wAgg);
	if (fIdle) {
		printf("%-10lu %-10lu %-10lu ", pQM->w30, pQM->w60, pQM->w90);
	}
	if (fCount) {
		printf("  %lu", pQM->wcount);
	}
	printf("\n");
	return 0;
}

/* Output the table we were asked to build				(ksb)
 */
static void
Output()
{
	register unsigned int i, iNumWidth;
	register int iLogWidth;
	register QUOT_MAP *pQM;
	auto char acSizeWidth[64];	/* longest base 10 rep of a size */

	/* include unmapped space (for output) if we were asked to
	 */
	if (fNoUser) {
		puiOrder[iLogins] = iLogins;
		++iLogins;
	}
	(void)qsort(puiOrder, iLogins, sizeof(unsigned int), BySpace);
	sprintf(acSizeWidth, "%ld", (long)pQMAll[puiOrder[0]].wtotal);
	iNumWidth = strlen(acSizeWidth);
	if (iNumWidth < 6) {
		iNumWidth = 6;
	}
	if (fHeader) {
		iNumWidth = 10;
		printf("%*s %-*s ", iNumWidth,  "total", iMaxLoginLen, fHistogram ? "blocks" : fByGroup ? "group" : "login");
		if (fIdle) {
			printf("%*s %*s %*s ", iNumWidth, "30days", iNumWidth, "60days", iNumWidth, "90days");
		}
		if (fCount) {
			printf("  Count");
		}
		printf("\n");
	}
	if (fHistogram) {
		sp_walk(ppvHistogram, 0, SP_MAX_KEY, OutHist);
	} else for (i = 0; i < iLogins; ++i) {
		pQM = & pQMAll[puiOrder[i]];
		if (fInKB) {
			pQM->w30 = (pQM->w30+1)/2;
			pQM->w60 = (pQM->w60+1)/2;
			pQM->w90 = (pQM->w90+1)/2;
			pQM->wtotal = (pQM->wtotal+1)/2;
		}
		if (0 == pQM->wtotal)
			break;
		for (iLogWidth = 0; ':' != pQM->pclogin[iLogWidth]; ++iLogWidth) {
			/* search for epass colon after login */
		}
		printf("%*ld %-*.*s ", iNumWidth,  (long)pQM->wtotal, iMaxLoginLen, iLogWidth, pQM->pclogin);
		if (fIdle) {
			printf("%*ld %*ld %*ld ", iNumWidth, (long)pQM->w30, iNumWidth, (long)pQM->w60, iNumWidth, (long) pQM->w90);
		}
		if (fCount) {
			printf("  %ld", (long)pQM->wcount);
		}
		printf("\n");
	}
	if (fNoUser) {
		--iLogins;
	}
}

%%
%i
#if USE_MNTENT
#include <mntent.h>
static char acMounted[] = "/etc/mtab";
#endif
#if USE_GETMNTINFO
#include <sys/param.h>
#include <sys/ucred.h>
#include <sys/mount.h>
#endif
%%

%c
/* remap any device nodes to the present mount point			(ksb)
 */
static void
ReMap(int argc, char **argv)
{
	register char *pcThis;
	auto struct stat stDev;
#if USE_MNTENT
	register FILE *fpMtab;
	register struct mntent *psm;

	fpMtab = (FILE *)0;
	for (/* argv */; (char *)0 != (pcThis = *argv); ++argv) {
		if (-1 == stat(pcThis, & stDev)) {
			fprintf(stderr, "%s: stat: %s: %s\n", progname, pcThis, strerror(errno));
			exit(EX_OSERR);
		}
		if (S_ISDIR(stDev.st_mode)) {
			continue;
		}
		if ((FILE *)0 != fpMtab) {
			rewind(fpMtab);
		} else if ((FILE *)0 == (fpMtab = setmntent(acMounted, "r"))) {
			fprintf(stderr, "%s: setmentent: %s: %s\n", progname, acMounted, strerror(errno));
			exit(EX_OSERR);
		}
		while ((struct mntent *)0 != (psm = getmntent(fpMtab))) {
			if (0 == strcmp(pcThis, psm->mnt_fsname))
				break;
		}
		if ((struct mntent *)0 == psm) {
			fprintf(stderr, "%s: %s: not a directory or presently mounted device\n", progname, pcThis);
			exit(EX_NOINPUT);
		}
		*argv = strdup(psm->mnt_dir);
	}
	if ((FILE *)0 != fpMtab) {
		endmntent(fpMtab);
	}
#endif
#if USE_GETMNTINFO
	auto struct statfs *pSFMounted;
	register int iMounted, i;

	iMounted = 0;
	for (/* argv */; (char *)0 != (pcThis = *argv); ++argv) {
		if (-1 == stat(pcThis, & stDev)) {
			fprintf(stderr, "%s: stat: %s: %s\n", progname, pcThis, strerror(errno));
			exit(EX_OSERR);
		}
		if (S_ISDIR(stDev.st_mode)) {
			continue;
		}
		if (0 != iMounted) {
			/* already cached */
		} else if (-1 == (iMounted = getmntinfo(& pSFMounted, MNT_LOCAL))) {
			fprintf(stderr, "%s: getmntinfo: %s\n", progname, strerror(errno));
			exit(EX_NOINPUT);
		}
		for(i = iMounted; 0 <= --i; /* nada */) {
			if (0 == strcmp(pcThis, pSFMounted[i].f_mntfromname))
				break;
		}
		if (-1 == i) {
			fprintf(stderr, "%s: %s: not a directory or presently mounted device\n", progname, pcThis);
			exit(EX_NOINPUT);
		}
		*argv = strdup(pSFMounted[i].f_mntonname);
	}
#endif /* US_GETMNTINFO */
}
