/* %Z%%M%	%I% %E% Copyright 2004-2006 J. Schilling */
#ifndef lint
static	char sccsid[] =
	"%Z%%M%	%I% %E% Copyright 2004-2006 J. Schilling";
#endif
/*
 *	Walk a directory tree
 *
 *	Copyright (c) 2004-2006 J. Schilling
 */
/*
 * 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 <mconfig.h>
#include <stdio.h>
#include <unixstd.h>
#include <stdxlib.h>
#ifdef	HAVE_FCHDIR
#include <fctldefs.h>
#else
#include <maxpath.h>
#endif
#include <statdefs.h>
#include <errno.h>
#include <strdefs.h>
#include <standard.h>
#include <getcwd.h>
#include <schily.h>
#include <nlsdefs.h>
#include "walk.h"
#include "fetchdir.h"
#include "mem.h"

#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')

/*
 * XXX The following variables need to be removed if we like to make
 * XXX treewalk() thread safe.
 */
#ifdef	HAVE_FCHDIR
LOCAL	int	WHome = -1;
#else
LOCAL	char	WHome[MAXPATHNAME+1];
#endif

#define	DIR_INCR	1024
LOCAL	char	*Curdir;
LOCAL	int	Curdtail = 0;
LOCAL	int	Curdlen = 0;
LOCAL	struct stat Ts;

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((int amt));
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				*/
{
	statfun		statf = &stat;
	int		nlen;

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

	if (Curdir == NULL) {
		Curdir = __malloc(DIR_INCR, "path buffer");
		Curdlen = DIR_INCR;
	}
	Curdir[0] = 0;
	Curdtail = 0;
#ifdef	HAVE_FCHDIR
	if ((WHome = open(".", O_RDONLY|O_NDELAY)) < 0) {
		nlen = geterrno();
		state->flags |= WALK_WF_NOCWD;
		errmsg(gettext("Cannot get working directory.\n"));
		if ((state->walkflags & WALK_NOEXIT) == 0)
			comexit(nlen);
		return (-1);
	}
#ifdef	F_SETFD
	fcntl(WHome, F_SETFD, 1);
#endif
#else
	if (getcwd(WHome, sizeof (WHome)) == NULL) {
		nlen = geterrno();
		state->flags |= WALK_WF_NOCWD;
		errmsg(gettext("Cannot get working directory.\n"));
		if ((state->walkflags & WALK_NOEXIT) == 0)
			comexit(nlen);
		return (-1);
	}
#endif

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

	/*
	 * If initial Curdir space is not sufficient, expand it.
	 */
	nlen = strlen(nm);
	if ((Curdlen - 2) < nlen)
		incr_dspace(nlen + 2);

	while (lstat(nm, &Ts) < 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);
#ifdef	HAVE_FCHDIR
	if (WHome >= 0) {
		close(WHome);
		WHome = -1;
	}
#endif
	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;

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

	if ((state->walkflags & WALK_NOSTAT) &&
	    last != NULL && !last->p_stat && last->p_nlink <= 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);
			ret = (*fn)(Curdir, &fs, WALK_SLN, state);
			goto out;
		} else {
			ret = (*fn)(Curdir, &fs, WALK_NS, state);
			goto out;
		}
	}
	if ((state->walkflags & WALK_MOUNT) != 0 && Ts.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) {
				ret = (*fn)(Curdir, &fs, WALK_DP, state);
				goto out;
			}
			pd = pd->p_last;
		}

		if ((state->walkflags & WALK_DEPTH) == 0)
			ret = (*fn)(Curdir, &fs, type, state);

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

		if (!isdot && chdir(nm) < 0) {
			state->flags |= WALK_WF_NOCHDIR;
			ret = (*fn)(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 = Curdlen - Curdtail - 2;

			if ((dp = fetchdir(".", &nents, NULL, NULL)) == NULL) {
				ret = (*fn)(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(nlen + 2);

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

				nents--;
				dp += nlen +2;
			}
			free(odp);
		skip:
			if (!isdot && state->level > 0 && xchdotdot(last,
							otail, state) < 0) {
				ret = geterrno();
				state->flags |= WALK_WF_NOHOME;
				errmsg(
				gettext("Cannot chdir to '..' from '%s/'.\n"),
						Curdir);
				if ((state->walkflags & WALK_NOEXIT) == 0)
					comexit(ret);
				ret = -1;
				goto out;
			}
		}
		if ((state->walkflags & WALK_DEPTH) != 0)
			ret = (*fn)(Curdir, &fs, type, state);
	} else {
		ret = (*fn)(Curdir, &fs, type, state);
	}
out:
	Curdir[otail] = '\0';
	Curdtail = otail;
	return (ret);
}

LOCAL int
incr_dspace(amt)
	int	amt;
{
	int	incr = DIR_INCR;

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

EXPORT int
walkhome(state)
	struct WALK	*state;
{
#ifdef	HAVE_FCHDIR
	if (WHome >= 0)
		return (fchdir(WHome));
#else
	if (WHome[0] != '\0')
		return (chdir(WHome));
#endif
	return (0);
}

EXPORT int
walkclose(state)
	struct WALK	*state;
{
#ifdef	HAVE_FCHDIR
	int	ret = 0;

	if (WHome >= 0)
		ret = close(WHome);
	WHome = -1;
	return (ret);
#else
	WHome[0] = '\0';
	return (0);
#endif
}

LOCAL int
xchdotdot(last, tail, state)
	struct pdirs	*last;
	int		tail;
	struct WALK	*state;
{
	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 = Curdir[tail];
	Curdir[tail] = '\0';
	if (chdir(Curdir) < 0) {
		if (geterrno() != ENAMETOOLONG) {
			Curdir[tail] = c;
			return (-1);
		}
		if (xchdir(Curdir) < 0) {
			Curdir[tail] = c;
			return (-1);
		}
	}
	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);
}
