/* @(#)walk.c	1.38 09/07/11 Copyright 2004-2009 J. Schilling */
#include <schily/mconfig.h>
#ifndef lint
static	UConst char sccsid[] =
	"@(#)walk.c	1.38 09/07/11 Copyright 2004-2009 J. Schilling";
#endif
/*
 *	Walk a directory tree
 *
 *	Copyright (c) 2004-2009 J. Schilling
 *
 *	In order to make treewalk() thread safe, we need to make it to not use
 *	chdir(2)/fchdir(2) which is process global.
 *
 *	chdir(newdir)	->	old = dfd;
 *				dfd = openat(old, newdir, O_RDONLY);
 *				close(old)
 *	fchdir(dd)	->	close(dfd); dfd = dd;
 *	stat(name)	->	fstatat(dfd, name, statb, 0)
 *	lstat(name)	-> 	fstatat(dfd, name, statb, AT_SYMLINK_NOFOLLOW)
 *	opendir(dname)	->	dd = openat(dfd, dname, O_RDONLY);
 *				dir = fdopendir(dd);
 *
 *	Similar changes need to be introduced in fetchdir().
 */
/*
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * See the file CDDL.Schily.txt in this distribution for details.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file CDDL.Schily.txt from this distribution.
 */

#include <schily/stdio.h>
#include <schily/unistd.h>
#include <schily/stdlib.h>
#ifdef	HAVE_FCHDIR
#include <schily/fcntl.h>
#else
#include <schily/maxpath.h>
#endif
#include <schily/param.h>	/* NODEV */
#include <schily/stat.h>
#include <schily/errno.h>
#include <schily/string.h>
#include <schily/standard.h>
#include <schily/getcwd.h>
#include <schily/schily.h>
#include <schily/nlsdefs.h>
#include <schily/walk.h>
#include <schily/fetchdir.h>

#if	defined(IS_MACOS_X) && defined(HAVE_CRT_EXTERNS_H)
/*
 * The MAC OS X linker does not grok "common" varaibles.
 * We need to fetch the address of "environ" using a hack.
 */
#include <crt_externs.h>
#define	environ	*_NSGetEnviron()
#else
extern	char **environ;
#endif

#ifndef	HAVE_LSTAT
#define	lstat	stat
#endif
#ifndef	HAVE_DECL_STAT
extern int stat	__PR((const char *, struct stat *));
#endif
#ifndef	HAVE_DECL_LSTAT
extern int lstat __PR((const char *, struct stat *));
#endif

#define	IS_UFS(p)	((p)[0] == 'u' && \
			(p)[1] == 'f' && \
			(p)[2] == 's' && \
			(p)[3] == '\0')
#define	IS_ZFS(p)	((p)[0] == 'z' && \
			(p)[1] == 'f' && \
			(p)[2] == 's' && \
			(p)[3] == '\0')

#define	DIR_INCR	1024		/* How to increment Curdir size */
struct twvars {
	char		*Curdir;	/* The current path name	*/
	int		Curdtail;	/* Where to append to Curdir	*/
	int		Curdlen;	/* Current size of 'Curdir'	*/
	struct stat	Sb;		/* stat(2) buffer for start dir	*/
#ifdef	HAVE_FCHDIR
	int		Home;		/* open fd to start CWD		*/
#else
	char		Home[MAXPATHNAME+1];	/* Abspath to start CWD	*/
#endif
};

struct pdirs {
	struct pdirs	*p_last; /* Previous node in list	*/
	dev_t		p_dev;	/* st_dev for this dir		*/
	ino_t		p_ino;	/* st_ino for this dir		*/
	BOOL		p_stat;	/* Need to call stat always	*/
	nlink_t		p_nlink; /* Number of subdirs to stat	*/
};


typedef	int	(*statfun)	__PR((const char *_nm, struct stat *_fs));

EXPORT	int	treewalk	__PR((char *nm, walkfun fn, struct WALK *_state));
LOCAL	int	walk		__PR((char *nm, statfun sf, walkfun fn, struct WALK *state, struct pdirs *last));
LOCAL	int	incr_dspace	__PR((struct twvars *varp, int amt));
EXPORT	void	walkinitstate	__PR((struct WALK *_state));
EXPORT	void	*walkopen	__PR((struct WALK *_state));
EXPORT	int	walkgethome	__PR((struct WALK *_state));
EXPORT	int	walkhome	__PR((struct WALK *_state));
EXPORT	int	walkclose	__PR((struct WALK *_state));
LOCAL	int	xchdotdot	__PR((struct pdirs *last, int tail, struct WALK *_state));
LOCAL	int	xchdir		__PR((char *p));

EXPORT int
treewalk(nm, fn, state)
	char		*nm;	/* The name to start walking		*/
	walkfun		fn;	/* The function to call for each node	*/
	struct WALK	*state; /* Walk state				*/
{
	struct twvars	vars;
	statfun		statf = stat;
	int		nlen;

	if ((state->walkflags & WALK_CHDIR) == 0) {
		seterrno(EINVAL);
		return (-1);
	}

	vars.Curdir  = NULL;
	vars.Curdlen = 0;
	vars.Curdtail = 0;
#ifdef	HAVE_FCHDIR
	vars.Home = -1;
#endif
	state->twprivate = &vars;
	if (walkgethome(state) < 0) {
		state->twprivate = NULL;
		return (-1);
	}

	if (nm == NULL || nm[0] == '\0')
		nm = ".";

	vars.Curdir = ___malloc(DIR_INCR, "path buffer");
	vars.Curdir[0] = 0;
	vars.Curdlen = DIR_INCR;
	/*
	 * If initial Curdir space is not sufficient, expand it.
	 */
	nlen = strlen(nm);
	if ((vars.Curdlen - 2) < nlen)
		incr_dspace(&vars, nlen + 2);

	while (lstat(nm, &vars.Sb) < 0 && geterrno() == EINTR)
		;

	state->flags = 0;
	state->base = 0;
	state->level = 0;

	if (state->walkflags & WALK_PHYS)
		statf = lstat;

	if (state->walkflags & (WALK_ARGFOLLOW|WALK_ALLFOLLOW))
		statf = stat;

	nlen = walk(nm, statf, fn, state, (struct pdirs *)0);
	walkhome(state);
	walkclose(state);

	free(vars.Curdir);
	state->twprivate = NULL;
	return (nlen);
}

LOCAL int
walk(nm, sf, fn, state, last)
	char		*nm;	/* The current name for the walk	*/
	statfun		sf;	/* stat() or lstat()			*/
	walkfun		fn;	/* The function to call for each node	*/
	struct WALK	*state;	/* For communication with (*fn)()	*/
	struct pdirs	*last;	/* This helps to avoid loops		*/
{
	struct pdirs	thisd;
	struct stat	fs;
	int		type;
	int		ret;
	int		otail;
	char		*p;
	struct twvars	*varp = state->twprivate;

	otail = varp->Curdtail;
	state->base = otail;
	state->flags = 0;
	if (varp->Curdtail == 0 || varp->Curdir[varp->Curdtail-1] == '/') {
		p = strcatl(&varp->Curdir[varp->Curdtail], nm, 0);
		varp->Curdtail = p - varp->Curdir;
	} else {
		p = strcatl(&varp->Curdir[varp->Curdtail], "/", nm, 0);
		varp->Curdtail = p - varp->Curdir;
		state->base++;
	}

	if ((state->walkflags & WALK_NOSTAT) &&
	    last != NULL && !last->p_stat && last->p_nlink <= 0) {
		/*
		 * Set important fields to useful values to make sure that
		 * no wrong information is evaluated in the no stat(2) case.
		 */
		fs.st_mode = 0;
		fs.st_ino = 0;
		fs.st_dev = NODEV;
		fs.st_nlink = 0;
		fs.st_size = 0;

		type = WALK_F;
		goto type_known;
	} else {
		while ((ret = (*sf)(nm, &fs)) < 0 && geterrno() == EINTR)
			;
	}
	if (ret >= 0) {
#ifdef	HAVE_ST_FSTYPE
		/*
		 * Check for autofs mount points...
		 */
		if (fs.st_fstype[0] == 'a' &&
		    strcmp(fs.st_fstype, "autofs") == 0) {
			int	f = open(nm, O_RDONLY|O_NDELAY);
			if (f < 0) {
				type = WALK_DNR;
			} else {
				if (fstat(f, &fs) < 0)
					type = WALK_NS;
				close(f);
			}
		}
#endif
		if (S_ISDIR(fs.st_mode)) {
			type = WALK_D;
			if (last != NULL && !last->p_stat && last->p_nlink > 0)
				last->p_nlink--;
		} else if (S_ISLNK(fs.st_mode))
			type = WALK_SL;
		else
			type = WALK_F;
	} else {
		int	err = geterrno();
		while ((ret = lstat(nm, &fs)) < 0 && geterrno() == EINTR)
			;
		if (ret >= 0 &&
		    S_ISLNK(fs.st_mode)) {
			seterrno(err);
			/*
			 * Found symbolic link that points to nonexistent file.
			 */
			ret = (*fn)(varp->Curdir, &fs, WALK_SLN, state);
			goto out;
		} else {
			/*
			 * Found unknown file type because lstat() failed.
			 */
			ret = (*fn)(varp->Curdir, &fs, WALK_NS, state);
			goto out;
		}
	}
	if ((state->walkflags & WALK_MOUNT) != 0 &&
	    varp->Sb.st_dev != fs.st_dev) {
		ret = 0;
		goto out;
	}

type_known:
	if (type == WALK_D) {
		BOOL		isdot = (nm[0] == '.' && nm[1] == '\0');
		struct pdirs	*pd = last;

		ret = 0;
		if ((state->walkflags & (WALK_PHYS|WALK_ALLFOLLOW)) == WALK_PHYS)
			sf = lstat;

		/*
		 * Search parent dir structure for possible loops.
		 * If a loop is found, do not descend.
		 */
		thisd.p_last = last;
		thisd.p_dev  = fs.st_dev;
		thisd.p_ino  = fs.st_ino;
		if (state->walkflags & WALK_NOSTAT && fs.st_nlink >= 2) {
			thisd.p_stat  = FALSE;
			thisd.p_nlink  = fs.st_nlink - 2;
#ifdef	HAVE_ST_FSTYPE
			if (!IS_UFS(fs.st_fstype) &&
			    !IS_ZFS(fs.st_fstype))
				thisd.p_stat  = TRUE;
#else
			thisd.p_stat  = TRUE;	/* Safe fallback */
#endif
		} else {
			thisd.p_stat  = TRUE;
			thisd.p_nlink  = 1;
		}

		while (pd) {
			if (pd->p_dev == fs.st_dev &&
			    pd->p_ino == fs.st_ino) {
				/*
				 * Found a directory that has been previously
				 * visited already. This may happen with hard
				 * linked directories. We found a loop, so do
				 * not descend this directory.
				 */
				ret = (*fn)(varp->Curdir, &fs, WALK_DP, state);
				goto out;
			}
			pd = pd->p_last;
		}

		if ((state->walkflags & WALK_DEPTH) == 0) {
			/*
			 * Found a directory, check the content past this dir.
			 */
			ret = (*fn)(varp->Curdir, &fs, type, state);

			if (state->flags & WALK_WF_PRUNE)
				goto out;
		}

		if (!isdot && chdir(nm) < 0) {
			state->flags |= WALK_WF_NOCHDIR;
			/*
			 * Found a directory that does not allow chdir() into.
			 */
			ret = (*fn)(varp->Curdir, &fs, WALK_DNR, state);
			state->flags &= ~WALK_WF_NOCHDIR;
			goto out;
		} else {
			char	*dp;
			char	*odp;
			int	nents;
			int	Dspace;

			/*
			 * Add space for '/' & '\0'
			 */
			Dspace = varp->Curdlen - varp->Curdtail - 2;

			if ((dp = fetchdir(".", &nents, NULL, NULL)) == NULL) {
				/*
				 * Found a directory that cannot be read.
				 */
				ret = (*fn)(varp->Curdir, &fs, WALK_DNR, state);
				goto skip;
			}

			odp = dp;
			while (nents > 0 && ret == 0) {
				register char	*name;
				register int	nlen;

				name = &dp[1];
				nlen = strlen(name);

				if (Dspace < nlen)
					Dspace += incr_dspace(varp, nlen + 2);

				state->level++;
				ret = walk(name, sf, fn, state, &thisd);
				state->level--;

				if (state->flags & WALK_WF_QUIT)
					break;
				nents--;
				dp += nlen +2;
			}
			free(odp);
		skip:
			if (!isdot && state->level > 0 && xchdotdot(last,
							otail, state) < 0) {
				ret = geterrno();
				state->flags |= WALK_WF_NOHOME;
				if ((state->walkflags & WALK_NOMSG) == 0) {
					ferrmsg(state->std[2],
					gettext(
					"Cannot chdir to '..' from '%s/'.\n"),
						varp->Curdir);
				}
				if ((state->walkflags & WALK_NOEXIT) == 0)
					comexit(ret);
				ret = -1;
				goto out;
			}
			if (ret < 0)		/* Do not call callback	    */
				goto out;	/* func past fatal errors   */
		}
		if ((state->walkflags & WALK_DEPTH) != 0)
			ret = (*fn)(varp->Curdir, &fs, type, state);
	} else {
		/*
		 * Any other non-directory and non-symlink file type.
		 */
		ret = (*fn)(varp->Curdir, &fs, type, state);
	}
out:
	varp->Curdir[otail] = '\0';
	varp->Curdtail = otail;
	return (ret);
}

LOCAL int
incr_dspace(varp, amt)
	int		amt;
	struct twvars	*varp;
{
	int	incr = DIR_INCR;

	if (amt < 0)
		amt = 0;
	while (incr < amt)
		incr += DIR_INCR;
	varp->Curdir = ___realloc(varp->Curdir, varp->Curdlen + incr,
								"path buffer");
	varp->Curdlen += incr;
	return (incr);
}

EXPORT void
walkinitstate(state)
	struct WALK	*state;
{
	state->flags = state->base = state->level = state->walkflags = 0;
	state->twprivate = NULL;
	state->std[0] = stdin;
	state->std[1] = stdout;
	state->std[2] = stderr;
	state->env = environ;
	state->quitfun = NULL;
	state->qfarg = NULL;
	state->maxdepth = state->mindepth = 0;
	state->lname = state->tree = state->patstate = NULL;
	state->err = state->pflags = state->auxi = 0;
	state->auxp = NULL;
}

EXPORT void *
walkopen(state)
	struct WALK	*state;
{
	struct twvars	*varp = ___malloc(sizeof (struct twvars), "walk vars");

	if (varp == NULL)
		return (NULL);
	varp->Curdir  = NULL;
	varp->Curdlen = 0;
	varp->Curdtail = 0;
#ifdef	HAVE_FCHDIR
	varp->Home = -1;
#else
	varp->Home[0] = '\0';
#endif
	state->twprivate = varp;

	return ((void *)varp);
}

EXPORT int
walkgethome(state)
	struct WALK	*state;
{
	struct twvars	*varp = state->twprivate;
	int		err = EX_BAD;

	if (varp == NULL) {
		if ((state->walkflags & WALK_NOMSG) == 0)
			ferrmsg(state->std[2],
				gettext("walkgethome: NULL twprivate\n"));
		if ((state->walkflags & WALK_NOEXIT) == 0)
			comexit(err);
		return (-1);
	}
#ifdef	HAVE_FCHDIR
	if (varp->Home >= 0)
		close(varp->Home);
	if ((varp->Home = open(".", O_RDONLY|O_NDELAY)) < 0) {
		err = geterrno();
		state->flags |= WALK_WF_NOCWD;
		if ((state->walkflags & WALK_NOMSG) == 0)
			ferrmsg(state->std[2],
				gettext("Cannot get working directory.\n"));
		if ((state->walkflags & WALK_NOEXIT) == 0)
			comexit(err);
		return (-1);
	}
#ifdef	F_SETFD
	fcntl(varp->Home, F_SETFD, 1);
#endif
#else
	if (getcwd(varp->Home, sizeof (varp->Home)) == NULL) {
		err = geterrno();
		state->flags |= WALK_WF_NOCWD;
		if ((state->walkflags & WALK_NOMSG) == 0)
			ferrmsg(state->std[2],
				gettext("Cannot get working directory.\n"));
		if ((state->walkflags & WALK_NOEXIT) == 0)
			comexit(err);
		return (-1);
	}
#endif
	return (0);
}

EXPORT int
walkhome(state)
	struct WALK	*state;
{
	struct twvars	*varp = state->twprivate;

	if (varp == NULL)
		return (0);
#ifdef	HAVE_FCHDIR
	if (varp->Home >= 0)
		return (fchdir(varp->Home));
#else
	if (varp->Home[0] != '\0')
		return (chdir(varp->Home));
#endif
	return (0);
}

EXPORT int
walkclose(state)
	struct WALK	*state;
{
	int		ret = 0;
	struct twvars	*varp = state->twprivate;

	if (varp == NULL)
		return (0);
#ifdef	HAVE_FCHDIR
	if (varp->Home >= 0)
		ret = close(varp->Home);
	varp->Home = -1;
#else
	varp->Home[0] = '\0';
#endif
	return (ret);
}

LOCAL int
xchdotdot(last, tail, state)
	struct pdirs	*last;
	int		tail;
	struct WALK	*state;
{
	struct twvars	*varp = state->twprivate;
	char	c;
	struct stat sb;

	if (chdir("..") >= 0) {
		seterrno(0);
		if (stat(".", &sb) >= 0) {
			if (sb.st_dev == last->p_dev &&
			    sb.st_ino == last->p_ino)
				return (0);
		}
	}

	if (walkhome(state) < 0)
		return (-1);

	c = varp->Curdir[tail];
	varp->Curdir[tail] = '\0';
	if (chdir(varp->Curdir) < 0) {
		if (geterrno() != ENAMETOOLONG) {
			varp->Curdir[tail] = c;
			return (-1);
		}
		if (xchdir(varp->Curdir) < 0) {
			varp->Curdir[tail] = c;
			return (-1);
		}
	}
	varp->Curdir[tail] = c;
	seterrno(0);
	if (stat(".", &sb) >= 0) {
		if (sb.st_dev == last->p_dev &&
		    sb.st_ino == last->p_ino)
			return (0);
	}
	return (-1);
}

LOCAL int
xchdir(p)
	char	*p;
{
	char	*p2;

	while (*p) {
		if ((p2 = strchr(p, '/')) != NULL)
			*p2 = '\0';
		if (chdir(p) < 0)
			return (-1);
		if (p2 == NULL)
			break;
		*p2 = '/';
		p = &p2[1];
	}
	return (0);
}
