/* Copyright (c) 1997-1998 Karl M. Hegbloom <karlheg@debian.org>
 * (note: above email address is defunct)
 *
 *  Originally Based on `tmpwatch-1.2/1.4' RHS, Erik Troan <ewt@redhat.com>
 *
 * This program may be freely redistributed under the terms of the GNU
 * Public License.  You should be able to find a copy of the GPL in
 * your "/usr/doc/copyright" directory on most GNU/Linux installations.
 *
 * Now maintained by ps - Paul Slootman <tmpreaper@packages.debian.org>
 * 2001-12-03 1.4.15
 * Patches from * mb - Marcus Brinkmann <Marcus.Brinkmann@ruhr-uni-bochum.de>
 * for using get_current_dir_name() and portability on HURD. ps hacked this
 * so that it should (still?) compile on non-glibc systems.
 * 2001-12-02 1.5.0
 * Added autoconf support. Also added getopt.c, getopt1.c, getopt.h so that
 * tmpreaper can be compiled on e.g. Solaris. It builds and runs on Solaris
 * and netBSD in addition to Linux now.
 * Ignore ext2fs immutable files.
 * 2002-02-05 1.6.0
 * ctime option added, check ctime in addition to atime, for those cases where
 * files get created with an old date in atime and mtime (e.g. via Samba from
 * a PC).
 * See changelog for later changes.
 */

#define _LARGEFILE_SOURCE 1     /* handle >2GB files where possible */
#define _FILE_OFFSET_BITS 64

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#define _GNU_SOURCE	/* For get_current_dir_name()  */

#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#if HAVE_DIRENT_H
# include <dirent.h>
# define NAMLEN(dirent) strlen((dirent)->d_name)
#else
# define dirent direct
# define NAMLEN(dirent) (dirent)->d_namlen
# if HAVE_SYS_NDIR_H
#  include <sys/ndir.h>
# endif
# if HAVE_SYS_DIR_H
#  include <sys/dir.h>
# endif
# if HAVE_NDIR_H
#  include <ndir.h>
# endif
#endif
#ifdef HAVE_ERRNO_H
# include <errno.h>
#endif
#include <getopt.h>
#include <glob.h>
#ifdef HAVE_LIMITS_H
# include <limits.h>
#endif
#include <stdarg.h>
#include <stdio.h>
#if STDC_HEADERS
# include <string.h>
#else
# ifndef HAVE_MEMCPY
#  define memmove(d, s, n) bcopy ((s), (d), (n))
# endif
#endif
#ifdef HAVE_SYS_WAIT_H
#include <sys/wait.h>
#endif
#include <time.h>
#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif
#ifdef HAVE_STDLIB_H
# include <stdlib.h>
#endif

#include <fcntl.h>

#ifdef __linux__
# include <sys/ioctl.h>
# include <ext2fs/ext2_fs.h>
# include <utime.h>
# ifdef HAVE_LIBMOUNT_LIBMOUNT_H
#  include <libmount/libmount.h>
# endif
#endif

/* do we need to work around a glibc dependency? */
#ifndef __USE_GNU
# ifdef __linux__      /* sanity check */
#  error "__USE_GNU not defined on a linux system, investigate! And report to tmpreaper@packages.debian.org"
# else
#  define NO_get_current_dir_name
   /* XXX TODO: a workaround for missing get_current_dir_name() */
# endif
#endif

#ifndef MAX_FORKS
# define MAX_FORKS	200
#endif

#ifdef GLOB_BRACE
# define TMPREAPER_GLOB_BRACE GLOB_BRACE
#else
# define TMPREAPER_GLOB_BRACE 0
#endif

/*
 * tmpreaper.c -- remove files in a directory, but do it carefully
 */

/* `--force': also rm files that are mode `-w EUID $$' */
#define FLAGS_FORCE		(1 << 0)
#define FLAGS_FORCE_P(fl)	((fl) & FLAGS_FORCE)

/* `--mtime': Use `mtime' rather than `atime' */
#define FLAGS_MTIME		(1 << 1)
#define FLAGS_MTIME_P(fl)	((fl) & FLAGS_MTIME)

/* `--mtime-dir': rm empty directories based on mtime */
#define FLAGS_MTIME_DIR		(1 << 2)
#define FLAGS_MTIME_DIR_P(fl)	((fl) & FLAGS_MTIME_DIR)

/* `--symlinks': also rm symlinks like files and directories */
#define FLAGS_SYMLINKS		(1 << 3)
#define FLAGS_SYMLINKS_P(fl)	((fl) & FLAGS_SYMLINKS)

/* `--all': also rm symlinks, fifos, devices, and sockets... */
#define FLAGS_ALLFILES		(1 << 4)
#define FLAGS_ALLFILES_P(fl)	((fl) & FLAGS_ALLFILES)

/* the `--protect' option was given */
#define FLAGS_PROTECT		(1 << 5)
#define FLAGS_PROTECT_P(fl)	((fl) & FLAGS_PROTECT)

/* the `--test' option was given */
#define FLAGS_TEST		(1 << 6)
#define FLAGS_TEST_P(fl)	((fl) & FLAGS_TEST)

/* `--ctime': Use `ctime' in addition to `atime' */
#define FLAGS_CTIME		(1 << 7)
#define FLAGS_CTIME_P(fl)	((fl) & FLAGS_CTIME)

/* The `--showdeleted' option was given */
#define FLAGS_SHOWDEL           (1 << 8)
#define FLAGS_SHOWDEL_P(fl)	((fl) & FLAGS_SHOWDEL)

#define TIME_SECONDS	1
#define TIME_MINUTES	60
#define TIME_HOURS	60 * 60
#define TIME_DAYS	24 * 60 * 60


glob_t	protect_glob; /* list of files to not rm */

typedef struct protect_entry
{
    ino_t  inode;
    char * name;
} protect_entry;

protect_entry * protect_table;	/* Global */

#define LOG_REALDEBUG	1
#define LOG_DEBUG	2
#define LOG_VERBOSE	3
#define LOG_NORMAL	4
#define LOG_ERROR	5
#define LOG_FATAL	6

int	logLevel = LOG_NORMAL;
int     delayMax = 0;
int     runtimeMax = 55;

void
message (const int    level,
	 const char * format,
	 ...) /*VARARGS*/
{
    va_list args;
    FILE  * where = stderr;
    
    if (level >= logLevel) {
	va_start (args, format);

	switch (level) {
	case LOG_DEBUG:
	    where = stdout;
            /*FALLTHRU*/
	case LOG_REALDEBUG:
	    fprintf (where, "debug: ");
	    break;

	case LOG_ERROR:
	case LOG_FATAL:
	    fprintf (where, "error: ");
	    break;

	case LOG_NORMAL:
	case LOG_VERBOSE:
	    where = stdout;
	    break;

	default:
	    break;
	}

	vfprintf (where, format, args);

	if (level == LOG_FATAL)
		exit (1);
    }
}

void
sig_alarmhandler(int sig)
{
    message(LOG_FATAL, "run time exceeded!\n"
                       "This may be indicative of an attack to use tmpreaper to remove critical files;\n"
                       "or the directories to clean up are excessive large and/or messed up.\n"
                       "Please investigate.\n");
    exit(1); /* redundant */
}

int
safe_chdir (const char * dirname)
{
    struct stat sb1, sb2;

    if (lstat (dirname, &sb1)) {
	message (LOG_ERROR,
		 "lstat() of directory `%s' failed: %s\n",
		 dirname, strerror (errno));
	return 1;
    }
    if (! S_ISDIR (sb1.st_mode)) {
	if (S_ISLNK (sb1.st_mode)) {
	    message (LOG_ERROR,
		     "safe_chdir(): Will not chdir across symlink `%s' (inode %lu).\n",
		     dirname, (u_long) sb1.st_ino);
	    return 1;
	} 
	message (LOG_ERROR,
		 "Not a directory: `%s' (inode %lu).\n",
		 dirname, (u_long) sb1.st_ino);
	return 1;
    }

    message (LOG_DEBUG,
	     "safe_chdir() : Before chdir(), `dirname' is inode `%lu'.\n",
	     (u_long) sb1.st_ino);
    message (LOG_DEBUG,
	     "safe_chdir() : Before chdir(), `dirname' on device `%lu'.\n",
	     (u_long) sb1.st_dev);

    if (chdir (dirname)) {
	message (LOG_ERROR,
		 "chdir() to directory `%s' (inode %lu) failed: %s\n",
		 dirname, (u_long) sb1.st_ino, strerror (errno));
	return 1;
    }
    if (lstat (".", &sb2)) {
	message (LOG_ERROR,
		 "lstat() of directory `%s' after chdir() failed: %s\n",
		 dirname, strerror (errno));
	return 1;
    }

    message (LOG_DEBUG,
	     "safe_chdir() : After chdir(), `dirname' is inode `%lu'.\n",
	     (u_long) sb2.st_ino);
    message (LOG_DEBUG,
	     "safe_chdir() : After chdir(), `dirname' on device `%lu'.\n",
	     (u_long) sb2.st_dev);

    if (sb1.st_ino != sb2.st_ino) {
	message (LOG_ERROR,
		 "Inode mismatch!!! : `%s' (inode %lu) != `.' (inode %lu)\n",
		 dirname,
		 (u_long) sb1.st_ino, (u_long) sb2.st_ino);
	message (LOG_FATAL,
		 "This indicates a possible subversion attempt.\n");
	/*NOTREACHED*/
    }
    else if (sb1.st_dev != sb2.st_dev) {
	message (LOG_ERROR,
		 "Device mismatch!!!\n");
	message (LOG_ERROR,
		 "`%s' (inode %lu), device `%lu' != `.' (inode %lu), device `%lu'\n",
		 dirname,
		 (u_long) sb1.st_ino, (u_long) sb1.st_dev,
		 (u_long) sb2.st_ino, (u_long) sb2.st_dev);
	message (LOG_FATAL,
		 "This indicates a possible subversion attempt.\n");
	/*NOTREACHED*/
    }
    return 0;
}


/*
 * mountpoint stuff copied from mountpoint.c, part of util-linux and modified.
 * Perhaps the mountpoint.c stuff could be used more extensively, replacing
 * hand-coded stuff in tmpreaper
 */
#ifdef HAVE_LIBMOUNT_LIBMOUNT_H

#define _PATH_PROC_MOUNTINFO	"/proc/self/mountinfo"

struct mountpoint_control {
	char *path;
	dev_t dev;
	struct stat st;
	unsigned int
		dev_devno:1,
		fs_devno:1;
};

int
dir_to_device(struct mountpoint_control *ctl)
{
	struct libmnt_table *tb = mnt_new_table_from_file(_PATH_PROC_MOUNTINFO);
	struct libmnt_fs *fs;
	struct libmnt_cache *cache;
	int rc = -1;

	if (!tb) {
		/* Not possible to detect bind mounts */

		return -1;
	}

	/* to canonicalize all necessary paths */
	cache = mnt_new_cache();
	mnt_table_set_cache(tb, cache);
	mnt_unref_cache(cache);

	fs = mnt_table_find_target(tb, ctl->path, MNT_ITER_BACKWARD);
	if (fs && mnt_fs_get_target(fs)) {
		ctl->dev = mnt_fs_get_devno(fs);
		rc = 0;
	}

	mnt_unref_table(tb);
	return rc;
}
#endif /* HAVE_LIBMOUNT_LIBMOUNT_H */

int
dir_empty_p (const char * dirname)
{
    DIR * dir;
    struct dirent * ent;
    int count = 0;

    if (! (dir = opendir(dirname))) {
	message(LOG_ERROR,
		"Cannot opendir(%s): %s\n", dirname, strerror(errno));
	return 0;
    }
    while ((ent = readdir(dir))) {
        if ((ent->d_name[0] == '.') && (
                 (ent->d_name[1] == '\0') ||
                 ((ent->d_name[1] == '.') && (ent->d_name[2] == '\0'))
            )
           )
	    continue;
	count++;
    }
    closedir(dir);
    return count != 0 ? 0 : 1;	/* #t when empty. */
}

int
cleanupDirectory (const char	   * dirname ,
		  const unsigned int killTime,
		  const int	     flags)
{
    DIR		  * dir;
    struct dirent * ent;
    int		    status, pid, skip = 0;
    int		    i;
    static int	    fork_count = 0;

    if (FLAGS_TEST_P (flags)) {
	message (LOG_VERBOSE,
		 "(PID %u) Pretending to clean up directory `%s'.\n",
		 (u_int) getpid(), dirname);
    }
    else {
	message(LOG_DEBUG,
		"Cleaning up directory `%s'.\n",
		dirname);
    }
    if (fork_count > MAX_FORKS) {
#ifdef NO_get_current_dir_name
	char	path[PATH_MAX], *p;
#else
	char	*path, *p;
#endif
	int	l;
	/* the admin will want to know where the problem lies */
#ifdef NO_get_current_dir_name
	if (getcwd(path, PATH_MAX) < 0)
	    /* too long even for PATH_MAX, show something anyway */
#else
	if ((path = get_current_dir_name()) < 0)
#endif
        {
	    p = "..../";
	}
	else {
	    l = strlen(path);
	    if (l > 65) {
		    /* remove middle part of the path */
		    p = path + l - 30;
		    strcpy(&path[30], " ... ");
		    memmove(&path[35], p, 30);
		    path[65] = 0;
	    }
	    p = path;
	}
	message(LOG_FATAL,
		"Too deep directory nesting detected while trying to clean up\n`%s/%s',\npossible attack?\n",
		p, dirname);
	exit(2);
    }

    /*
     * Do everything in a child process so we don't have to chdir(".."),
     * which would lead to a race condition. fork() on Linux is very efficient
     * so this shouldn't be a big deal (probably just a exception on one page
     * of stack, not bad). I should probably just keep a directory stack
     * and fchdir() back up it, but it's not worth changing now. - ewt
     *
     * After reading "http://www.geek-girl.com/bugtraq/1996_2/0054.html" (look
     * for "cd .."), I think it is best to use this method rather than the
     * contrasting algorithm mentioned in Eric's comment. IMHO he has made the
     * right choice by having chosen the `recursive forking' algorithm.  It is
     * much easier to implement and understand as well as being more secure
     * through avoiding `cd ..'. - karlheg
     *
     * The geek girl link is dead.  fork() is not efficient on all systems,
     * so it would still be better to use a directory stack + fchdir(). - mb
     *
     * This is another link to what geek-girl had:
     * http://www.geocrawler.com/mail/msg.php3?msg_id=184906&list=91
     * - ps
     */

    if (! (pid = fork())) {
	    struct stat	    sb, here;
	    if (safe_chdir (dirname))
		    exit(1);
	    fork_count++;
	    if (! (dir = opendir ("."))) {
		message (LOG_FATAL,
			 "Opening directory, `%s' as `.' : %s\n",
			 dirname, strerror (errno));
	    }

	    if (lstat (".", &here)) {
		message (LOG_FATAL,
			 "Statting current directory, `%s' as `.' : %s\n",
			 dirname, strerror (errno));
	    }

	    do {
		errno = 0;
		ent = readdir (dir);
		if (errno) {
		    message (LOG_ERROR,
			     "Reading directory entry. : %s\n",
			     strerror (errno));
		}
		if (!ent)
		    break;

		/*
		 * if name starts with dot, and
		 *    is 1 byte long, or
		 *    has a second dot and is 2 bytes long,
		 *   skip it.
		 */
		if ((ent->d_name[0] == '.') && (
			 (ent->d_name[1] == '\0') ||
			 ((ent->d_name[1] == '.') && (ent->d_name[2] == '\0'))
		    )
		   )
		    continue;

		message (LOG_DEBUG,
			 "(PID %u) Found directory entry `%s'.\n",
			 (u_int) getpid(), ent->d_name);

		if (lstat (ent->d_name, &sb)) {
		    message (LOG_ERROR,
			     "Failed to lstat() `%s/%s': %s\n",
			     dirname, ent->d_name, strerror (errno));
		    continue;
		}
		if (((!getuid() && !sb.st_uid) || (sb.st_uid == geteuid())) &&
		    !FLAGS_FORCE_P (flags) && !(sb.st_mode & S_IWUSR)) {
		    message (LOG_VERBOSE,
			     "Non-writeable file, owned by UID (%u) skipped: `%s'\n",
			     (u_int) sb.st_uid, ent->d_name);
		    continue;
		}
		if (sb.st_dev != here.st_dev) {
		    message (LOG_VERBOSE,
			     "File on different device skipped: `%s/%s'\n",
			     dirname, ent->d_name);
		    continue;
		}

		if (FLAGS_PROTECT_P (flags)) {
		    skip = i = 0;
		    for (i = 0; protect_table[i].name; i++) {
			if (sb.st_ino == protect_table[i].inode) {
			    message (LOG_VERBOSE,
				     "Entry matching `--protect' pattern skipped. `%s'\n",
				     protect_table[i].name);
			    skip = 1;
			    break;
			}
		    }
		    if (skip)
			continue;
		}

		if (S_ISDIR (sb.st_mode)) {
#ifdef HAVE_LIBMOUNT_LIBMOUNT_H
		    struct mountpoint_control ctl = { NULL };

		    ctl.path = ent->d_name;
		    if (stat(ctl.path, &ctl.st)) {
			message (LOG_ERROR,
				 "Failed to stat() `%s/%s/.': %s\n",
				 dirname, ent->d_name, strerror (errno));
			continue;
		    }
		    if (!dir_to_device(&ctl)) {
			message (LOG_VERBOSE,
				 "Directory on different device skipped: `%s/%s'\n",
				 dirname, ent->d_name);
			continue;
		    }
#else /* fallback for systems (non-Linux?) without libmount */
                    char *dst;
		    struct stat sb2;

                    if ((dst = malloc(strlen(ent->d_name) + 3)) == NULL)
                        message (LOG_FATAL, "malloc failed.\n");
                    strcpy(dst, ent->d_name);
                    strcat(dst, "/.");
		    if (lstat (dst, &sb2)) {
			if (errno != ENOENT) {
			    message (LOG_ERROR,
				 "Failed to lstat() `%s/%s/.': %s\n",
				 dirname, ent->d_name, strerror (errno));
			}
			free(dst);
			continue;
		    }
		    free(dst);
		    if (sb2.st_dev != sb.st_dev) {
			message (LOG_VERBOSE,
				 "Directory on different device skipped: `%s/%s'\n",
				 dirname, ent->d_name);
			continue;
		    }
#endif
		    cleanupDirectory (ent->d_name, killTime, flags);
		    message (LOG_VERBOSE,
			     "(PID %u) Back from recursing down `%s'.\n",
			     (u_int) getpid(), ent->d_name);
		}

		/* Decide whether to remove the file or not */
		/* check for mtime on directory instead of atime if requested */
		if ( FLAGS_MTIME_P(flags) ||
		    (FLAGS_MTIME_DIR_P(flags) && S_ISDIR(sb.st_mode))) {
		    if (sb.st_mtime >= killTime) {
			message (LOG_DEBUG,
				 "Entry (dir) mtime too recent (%li>=%li). `%s/%s'.\n",
				 sb.st_mtime, killTime, dirname, ent->d_name);
			continue;
		    }
		}
		else {
		    if (sb.st_atime >= killTime) {
			message (LOG_DEBUG,
				 "Entry atime too recent (%li>=%li). `%s/%s'.\n",
				 sb.st_ctime, killTime, dirname, ent->d_name);
			continue;
		    }
                    /* If ctime option is given, the ctime must also be so    */
                    /* long ago. This is e.g. for samba as DOS preserves      */
                    /* mtime when copying, so such files get removed too soon */
                    /* Don't do it for directories though, as ctime may get   */
                    /* reset below through utime()                            */
                    if (!S_ISDIR(sb.st_mode) &&
                        FLAGS_CTIME_P(flags) && sb.st_ctime >= killTime) {
			    message (LOG_DEBUG,
			             "Entry ctime too recent (%li>=%li). `%s/%s'.\n",
			             sb.st_ctime, killTime, dirname, ent->d_name);
                            continue;
		    }
		}

		if (S_ISDIR (sb.st_mode)) {
                    if (dir_empty_p (ent->d_name)) {
                        if (FLAGS_TEST_P (flags)) {
                            message (LOG_VERBOSE,
                                     "Pretending to remove empty directory `%s'.\n",
                                     ent->d_name);
                            if (FLAGS_SHOWDEL_P(flags))
                                message(LOG_NORMAL, "rmdir %s/%s\n", dirname, ent->d_name);
                        }
                        else {
			    message (LOG_VERBOSE,
				     "Removing directory `%s'.\n",
				     ent->d_name);
			    if (rmdir (ent->d_name)) {
				message (LOG_ERROR,
					 "Failed to rmdir `%s': %s\n",
					 ent->d_name, strerror (errno));
                            }
                            else {
                                if (FLAGS_SHOWDEL_P(flags))
                                    message(LOG_NORMAL, "rmdir %s/%s\n", dirname, ent->d_name);
                            }
			}
		    }
		}
		else if (FLAGS_ALLFILES_P (flags) || S_ISREG (sb.st_mode)
			 || (FLAGS_SYMLINKS_P (flags) && S_ISLNK (sb.st_mode))) {
		    if (FLAGS_TEST_P (flags)) {
			message (LOG_VERBOSE,
				 "Pretending to remove file `%s/%s'.\n",
				 dirname, ent->d_name);
                        if (FLAGS_SHOWDEL_P(flags))
                            message(LOG_NORMAL, "rm %s/%s\n", dirname, ent->d_name);
		    }
		    else {
			message (LOG_VERBOSE,
				 "Removing file `%s/%s'.\n",
				 dirname, ent->d_name);
			if (unlink (ent->d_name))
#if defined(__linux__)
                        {
                            int give_error = 1;
                            int fd;
                            if ((fd = open(ent->d_name, O_RDONLY)) >= 0) {
                                unsigned int flags;
                                if (ioctl(fd, EXT2_IOC_GETFLAGS, &flags) >= 0) {
                                    if (flags & EXT2_IMMUTABLE_FL) {
                                        give_error = 0;
                                    }
                                }
                                close(fd);
                                /* Restore access time! */
                                /* However, only bother if we give a warning. */
                                /* With no warning, it doesn't matter.        */
                                /* And it delays the next attempt :-)         */
                                if (give_error) {
                                    struct utimbuf ts;
                                    ts.actime  = sb.st_atime;
                                    ts.modtime = sb.st_mtime;
                                    utime(ent->d_name, &ts);
                                }
                            }
                            if (give_error)
#endif
                                message (LOG_ERROR,
				     "Failed to unlink `%s': %s\n",
				     ent->d_name, strerror (errno));
#if defined(__linux__)
                            else
                                message (LOG_VERBOSE,
                                    "file `%s' marked immutable -- skipping.\n",
                                    ent->d_name);
                        }
#endif
                        else {
                            if (FLAGS_SHOWDEL_P(flags))
                                message(LOG_NORMAL, "rm %s/%s\n", dirname, ent->d_name);
                        }
		    }
		}
		else {
		    if (FLAGS_SYMLINKS_P (flags)) {
			message (LOG_VERBOSE,
				 "Not a regular file, symlink, or directory `%s' -- skipping.\n",
				 ent->d_name);
		    }
		    else {		    
			message (LOG_VERBOSE,
				 "Not a regular file or directory `%s' -- skipping.\n",
				 ent->d_name);
		    }
		}
	    } while (ent);
	    
	    closedir (dir);
	    
	    exit (0);
    }

    if (pid < 0) {
        message(LOG_ERROR, "fork failed: %s\n", strerror(errno));
        return 1;
    }

    if (waitpid(pid, &status, 0) < 0) {
	message(LOG_ERROR, "waitpid failed: %s\n", strerror(errno));
	return 1;
    }

    if (WIFEXITED (status))
	return WEXITSTATUS (status);

    return 0;
}

void
printCopyright (void)
{
    fprintf (stderr,
#ifdef DEBIAN
	     "tmpreaper -- Version: " VERSION "-DEB\n"
#else
	     "tmpreaper -- Version: " VERSION "\n"
#endif
	     "(c) 1997 Karl M. Hegbloom   <tmpreaper@packages.debian.org>\n"
	     "(c) 2006-2025 Paul Slootman <tmpreaper@packages.debian.org>\n"
	     "This may be freely redistributed under the terms of the GNU Public License.\n");
}
/*	               1         2         3         4         5         6         7         8
  	      02345678901234567890123456789012345678901234567890123456789012345678901234567890
*/

void
usage(void)
{
    printCopyright ();
    fprintf (stderr, "\n"
	     "tmpreaper [-htvfmMsadVT] [--help] [--test] [--verbose] [--force]\n"
	     "          [--delay=99] [--runtime=99] [--showdeleted]\n"
	     "          [--ctime] [--mtime] [--mtime-dir] [--symlinks] [--all]\n"
	     "          [[--protect '<shell_pattern>']...] <time> <dirs>...\n"
	     "<time> is time since a file in a <dir> was last accessed.\n"
	     "It defaults to hours, or you may suffix with `s', `m', `h', or `d'.\n\n");
    exit (1);
}

int
main (int     argc,
      char ** argv)
{
    unsigned int  grace = 0;
    unsigned int  killTime = 0;
    int		  long_index = 0;
    int		  ret = 0, arg = 0, i = 0, j = 0, k = 0, adir = 0;
    int		  flags = 0;
    char	* pwd = NULL;
    char        **pp;

    struct stat	  sb;

    /*
     * Utilize GNU C dynamic array allocation and statement expressions to
     * allocate an array to hold the --protect argument strings.
     */
    char *protect_argv[ ({ int i;
			   int count = 0;
			   for (i = 1; i < argc; i++)
			       if (! strncmp (argv[i], "--protect", 9))
				   count++;
			   ++count; }) ];

    char **p = protect_argv;

    struct option const options[] = {
	{ "help",	no_argument,		NULL, 'h' },
	{ "test",	no_argument,		NULL, 't' },
	{ "verbose",	optional_argument,	NULL, 'v' },
	{ "force",	no_argument,		NULL, 'f' },
	{ "ctime",	no_argument,		NULL, 'c' },
	{ "mtime",	no_argument,		NULL, 'm' },
	{ "mtime-dirs",	no_argument,		NULL, 'M' },
	{ "symlinks",	no_argument,		NULL, 's' },
	{ "all",	no_argument,		NULL, 'a' },
	{ "protect",	required_argument,	NULL, 'p' },
	{ "delay",	optional_argument,	NULL, 'd' },
	{ "showdeleted",no_argument,	        NULL, 'V' },
	{ "runtime",	required_argument,	NULL, 'T' },
	{ 0, 0, 0, 0 }
    };

    if (argc == 1)
	usage ();

    while (1) {
	long_index = 0;

	arg = getopt_long(argc, argv, "htv::fcmMsap:d::VT:", options, &long_index);
	if (arg == -1)
	    break;

	switch (arg) {
	case '?':
	case 'h':
	    usage ();
	    break;

	case 't':
	    flags |= FLAGS_TEST;
            optarg = 0;
	    /*FALLTHRU*/

	case 'v':
	    if (!optarg)
                logLevel ? logLevel -= 1 : 0;
            else if (atoi(optarg) <= 0)
                logLevel = LOG_NORMAL;
            else if (atoi(optarg) > 3)
                logLevel = LOG_REALDEBUG;
            else
                logLevel = LOG_NORMAL - atoi(optarg);
	    break;

	case 'f':
	    flags |= FLAGS_FORCE;
	    break;

	case 'c':
            if ((flags & FLAGS_MTIME) == FLAGS_MTIME) {
                message(LOG_FATAL, "--ctime conflicts with --mtime option\n");
                /*NOTREACHED*/
            }
	    flags |= FLAGS_CTIME;
	    break;

	case 'm':
            if ((flags & FLAGS_CTIME) == FLAGS_CTIME) {
                message(LOG_FATAL, "--mtime conflicts with --ctime option\n");
                /*NOTREACHED*/
            }
	    flags |= FLAGS_MTIME;
	    break;

	case 'M':
	    flags |= FLAGS_MTIME_DIR;
	    break;

	case 's':
	    flags |= FLAGS_SYMLINKS;
	    break;

	case 'a':
	    flags |= FLAGS_ALLFILES;
	    break;

	case 'p':
	    protect_argv[k++] = optarg;
	    protect_argv[k] = NULL;
	    flags |= FLAGS_PROTECT;
	    break;

	case 'd':
	    if (optarg)
                delayMax = atoi(optarg);
            else
                delayMax = 256;
            message(LOG_DEBUG, "delay is %d seconds max\n", delayMax);
	    break;

        case 'V':
            flags |= FLAGS_SHOWDEL;
            break;

        case 'T':
            runtimeMax = atoi(optarg);
            message(LOG_DEBUG, "runtime is %d seconds max\n", runtimeMax);
            if (runtimeMax < 5 && runtimeMax != 0) {
                message(LOG_FATAL, "runtime must be at least 5 seconds\n");
                /*NOTREACHED*/
            }
	}
    }

    if (optind == argc) {
	message (LOG_FATAL,
		 "Time must be given.\n");
	/*NOTREACHED*/
    }

#define multiplier j

    i = strlen (argv[optind]) - 1;
    switch (argv[optind][i]) {
    case 's':
    case 'S':
	multiplier = TIME_SECONDS;
	argv[optind][i] = '\0';
	break;
    case 'm':
    case 'M':
	multiplier = TIME_MINUTES;
	argv[optind][i] = '\0';
	break;
    case 'd':
    case 'D':
	multiplier = TIME_DAYS;
	argv[optind][i] = '\0';
	break;
    case 'h':
    case 'H':
	argv[optind][i] = '\0';
    default:
	multiplier = TIME_HOURS;
    }

    if (sscanf (argv[optind], "%d", &grace) != 1) {
	message (LOG_FATAL,
		 "Bad time argument `%s'.\n",
		 argv[optind]);
	/*NOTREACHED*/
    }

    optind++;
    if (optind == argc) {
	message (LOG_FATAL,
		 "Directory name(s) expected.\n");
	/*NOTREACHED*/
    }

    /*
     * Use line-buffered I/O for stdout, so that even when the output is being
     * piped to a file, it gets flushed after a newline.  If we don't do this,
     * when it forks, the child inherits an unflushed buffer, so when it
     * flushes and the parent does too, we get doubled output.  Note that
     * `stderr' is unbuffered already.
     * See: `APUE', W. Richard Stevens,  pp. 189-190
     * Bug pointed out by Joey Hess <joey@kitenet.net>
     */
#ifdef SETVBUF_REVERSED
    if (setvbuf (stdout, _IOLBF, (void *)NULL, BUFSIZ) != 0)
#else
    if (setvbuf (stdout, (void *)NULL, _IOLBF, BUFSIZ) != 0)
#endif
    {
	message (LOG_FATAL,
		 "setvbuf() failed for `stdout'.\n");
	/*NOTREACHED*/
    }

    grace = grace * multiplier;
    message (LOG_DEBUG,
	     "Grace period is `%u' seconds.\n",
	     (u_int)grace);
    
    message (LOG_DEBUG,
	     "This is PID (%u) being run by UID (%u), EUID (%u).\n",
	     getpid(), getuid(), geteuid());

    killTime = time (NULL) - grace;

    /* use getcwd() to test whether current directory is valid */
    if (!(pwd = malloc(PATH_MAX))) {
	message (LOG_FATAL,
		 "malloc for getcwd failed: %s\n",
		 strerror(errno));
	/*NOTREACHED*/
    }
    if (getcwd (pwd, PATH_MAX) == NULL) {
	message (LOG_FATAL,
		 "getcwd() failed: %s\n",
		 strerror(errno));
	/*NOTREACHED*/
    }
    /* don't need pwd, but leave it allocated for possible debug things below */

    /* Set an alarm, so that we aren't vulnerable to the type of attack that  */
    /* needs to have multiple concurrent tmpreaper instances running.         */
    /* A little less than one minute should be enough...                      */
    /* First, delay a random (small) time to foil wannabee bad guys even more */
    /* if --delay was given as option.                                        */
    if (delayMax) {
        int fd;
        unsigned char x;
        unsigned int timetosleep;
        if ((fd = open("/dev/urandom", O_RDONLY)) >= 0) {
            read(fd, &x, 1);
            close(fd);
            timetosleep = ((x+0.01)/256.0)*delayMax;
        }
        else {
            /* fallback if open /dev/urandom failed */
            srand(getpid());
            timetosleep = 1+(int)((delayMax+0.01)*rand()/(RAND_MAX+1.0));
        }
	message (LOG_DEBUG, "timetosleep=%u\n", timetosleep);
        sleep(timetosleep);
    }
      
    if (runtimeMax) {
        signal(SIGALRM, sig_alarmhandler);
        alarm(runtimeMax);
    }

    /* For each directory in the cleanup list, cd there, glob, then delete */
    for (adir = optind; adir < argc; adir++) {
	if ((ret = chdir (argv[adir])) != 0) {
	    message (LOG_ERROR,
		     "Cannot chdir() to `%s' for --protect glob: %s\n",
		     argv[adir], strerror(errno));
	    continue;
	}
	if (LOG_DEBUG >= logLevel) {
	    /* this test is to prevent non-necessary calls to getcwd() */
	    message (LOG_DEBUG,
		 "getcwd() is: %s\n",
		 getcwd (pwd, PATH_MAX));
	}
	/*  ... for each protect_argv, glob, using GLOB_APPEND */
	if (FLAGS_PROTECT_P (flags)) {
	    pp = p;
	    while (*pp != NULL) {
		if (p == pp) {
		    ret = glob (*pp, (GLOB_NOSORT | TMPREAPER_GLOB_BRACE), NULL, &protect_glob);
		} else {
		    ret = glob (*pp, (GLOB_APPEND | GLOB_NOSORT | TMPREAPER_GLOB_BRACE), NULL, &protect_glob);
		}
		if (ret == GLOB_NOSPACE) {
		    message (LOG_FATAL,
			     "glob() returned GLOB_NOSPACE : Insufficient memory to hold pattern expansion.\n");
		    /*NOTREACHED*/
		}
		if (ret == GLOB_NOMATCH) {
		    if (LOG_VERBOSE >= logLevel) {
			/* test to prevent non-necessary calls to getcwd() */
			message (LOG_VERBOSE,
			     "Nothing in `%s' matches `%s'.\n",
			     getcwd (pwd, PATH_MAX), *pp);
		    }
		}
		pp++;
	    }
	}

	protect_table = malloc ((1 + protect_glob.gl_pathc) * sizeof (protect_entry));
	if (! protect_table) {
	    message (LOG_FATAL, "malloc() failed: %s\n", strerror (errno));
	    /*NOTREACHED*/
	}
	j = 0;
	for (i = 0; i < protect_glob.gl_pathc; i++) {
	    if (lstat (protect_glob.gl_pathv[i], &sb)) {
		message (LOG_ERROR,
			 "lstat() of `%s' failed (`--protect'): %s\n",
			 protect_glob.gl_pathv[i], strerror (errno));
		continue;
	    }
	    protect_table[j].inode = sb.st_ino;
	    protect_table[j].name = protect_glob.gl_pathv[i];
	    j++;
	}
	protect_table[j].name = NULL;

	/* Now go to work */
	if (lstat (argv[adir], &sb)) {
	    message (LOG_ERROR,
		     "lstat() of directory `%s' failed (argv): %s\n",
		     argv[adir], strerror (errno));
	}
	if (S_ISLNK (sb.st_mode)) {
	    message (LOG_DEBUG,
		     "Initial directory `%s' is a symlink -- skipping\n",
		     argv[adir]);
	}
	else {
	    cleanupDirectory (argv[adir], killTime, flags);
	}
	free(protect_table);
	globfree(&protect_glob);
    }
    return 0;
}
