OpenSolaris_b135/cmd/filesync/eval.c

Compare this file to the similar file:
Show the results in this format:

/*
 * CDDL HEADER START
 *
 * 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.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 1995-2003 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*
 * module:
 *	eval.c
 *
 * purpose:
 *	routines to ascertain the current status of all of the files
 *	described by a set of rules.  Some of the routines that update
 *	file status information are also called later (during reconcilation)
 *	to reflect the changes that have been made to files.
 *
 * contents:
 *	evaluate	top level - evaluate one side of one base
 *	add_file_arg	(static) add a file to the list of files to evaluate
 *	eval_file	(static) stat a specific file, recurse on directories
 *	walker		(static) node visitor for recursive descent
 *	note_info	update a file_info structure from a stat structure
 *	do_update	(static) update one file_info structure from another
 *	update_info	update the baseline file_info from the prevailng side
 *	fakedata	(static) make it look like one side hasn't changed
 *	check_inum	(static) sanity check to detect wrong-dir errors
 *	add_glob	(static) expand a wildcard in an include rule
 *	add_run		(static) run a program to generate an include list
 *
 * notes:
 *	pay careful attention to the use of the LISTED and EVALUATE
 *	flags in each file description structure.
 */

#pragma ident	"%Z%%M%	%I%	%E% SMI"

#include <stdio.h>
#include <stdlib.h>
#include <libgen.h>
#include <unistd.h>
#include <string.h>
#include <glob.h>
#include <ftw.h>
#include <sys/mkdev.h>
#include <errno.h>

#include "filesync.h"
#include "database.h"
#include "messages.h"
#include "debug.h"

/*
 * routines:
 */
static errmask_t eval_file(struct base *, struct file *);
static errmask_t add_file_arg(struct base *, char *);
static int walker(const char *, const struct stat *, int, struct FTW *);
static errmask_t add_glob(struct base *, char *);
static errmask_t add_run(struct base *, char *);
static void check_inum(struct file *, int);
static void fakedata(struct file *, int);

/*
 * globals
 */
static bool_t usingsrc;	/* this pass is on the source side		*/
static int walk_errs;	/* errors found in tree walk			*/
static struct file *cur_dir;	/* base directory for this pass		*/
static struct base *cur_base;	/* base pointer for this pass		*/

/*
 * routine:
 *	evaluate
 *
 * purpose:
 *	to build up a baseline description for all of the files
 *	under one side of one base pair (as specified by the rules
 *	for that base pair).
 *
 * parameters:
 *	pointer to the base to be evaluated
 *	source/destination indication
 *	are we restricted to new rules
 *
 * returns:
 *	error mask
 *
 * notes:
 *	we evaluate source and destination separately, and
 *	reinterpret the include rules on each side (since there
 *	may be wild cards and programs that must be evaluated
 *	in a specific directory context).  Similarly the ignore
 *	rules must be interpreted anew for each base.
 */
errmask_t
evaluate(struct base *bp, side_t srcdst, bool_t newrules)
{	errmask_t errs = 0;
	char *dir;
	struct rule *rp;
	struct file *fp;

	/* see if this base is still relevant		*/
	if ((bp->b_flags & F_LISTED) == 0)
		return (0);

	/* figure out what this pass is all about	*/
	usingsrc = (srcdst == OPT_SRC);

	/*
	 * the ignore engine maintains considerable per-base-directory
	 * state, and so must be reset at the start of a new tree.
	 */
	ignore_reset();

	/* all evaluation must happen from the appropriate directory */
	dir = usingsrc ? bp->b_src_name : bp->b_dst_name;
	if (chdir(dir) < 0) {
		fprintf(stderr, gettext(ERR_chdir), dir);

		/*
		 * if we have -n -o we are actually willing to
		 * pretend that nothing has changed on the missing
		 * side.  This is actually useful on a disconnected
		 * notebook to ask what has been changed so far.
		 */
		if (opt_onesided == (usingsrc ? OPT_DST : OPT_SRC)) {
			for (fp = bp->b_files; fp; fp = fp->f_next)
				fakedata(fp, srcdst);

			if (opt_debug & DBG_EVAL)
				fprintf(stderr, "EVAL: FAKE DATA %s dir=%s\n",
					usingsrc ? "SRC" : "DST", dir);
			return (0);
		} else
			return (ERR_NOBASE);
	}

	if (opt_debug & DBG_EVAL)
		fprintf(stderr, "EVAL: base=%d, %s dir=%s\n",
			bp->b_ident, usingsrc ? "SRC" : "DST", dir);

	/* assemble the include list			*/
	for (rp = bp->b_includes; rp; rp = rp->r_next) {

		/* see if we are skipping old rules	*/
		if (newrules && ((rp->r_flags & R_NEW) == 0))
			continue;

		if (rp->r_flags & R_PROGRAM)
			errs |= add_run(bp, rp->r_file);
		else if (rp->r_flags & R_WILD)
			errs |= add_glob(bp, rp->r_file);
		else
			errs |= add_file_arg(bp, rp->r_file);
	}

	/* assemble the base-specific exclude list		*/
	for (rp = bp->b_excludes; rp; rp = rp->r_next)
		if (rp->r_flags & R_PROGRAM)
			ignore_pgm(rp->r_file);
		else if (rp->r_flags & R_WILD)
			ignore_expr(rp->r_file);
		else
			ignore_file(rp->r_file);

	/* add in the global excludes				*/
	for (rp = omnibase.b_excludes; rp; rp = rp->r_next)
		if (rp->r_flags & R_WILD)
			ignore_expr(rp->r_file);
		else
			ignore_file(rp->r_file);

	/*
	 * because of restriction lists and new-rules, the baseline
	 * may contain many more files than we are actually supposed
	 * to look at during the impending evaluation/analysis phases
	 *
	 * when LIST arguments are encountered within a rule, we turn
	 * on the LISTED flag for the associated files.  We only evaluate
	 * files that have the LISTED flag.  We turn the LISTED flag off
	 * after evaluating them because just because a file was enumerated
	 * in the source doesn't mean that will necessarily be enumerated
	 * in the destination.
	 */
	for (fp = bp->b_files; fp; fp = fp->f_next)
		if (fp->f_flags & F_LISTED) {
			errs |= eval_file(bp, fp);
			fp->f_flags &= ~F_LISTED;
		}

	/* note that this base has been evaluated	*/
	bp->b_flags |= F_EVALUATE;

	return (errs);
}

/*
 * routine:
 *	add_file_arg
 *
 * purpose:
 *	to create file node(s) under a specified base for an explictly
 *	included file.
 *
 * parameters:
 *	pointer to associated base
 *	name of the file
 *
 * returns:
 *	error mask
 *
 * notes:
 *	the trick is that an include LIST argument need not be a file
 *	in the base directory, but may be a path passing through
 *	several intermediate directories.  If this is the case we
 *	need to ensure that all of those directories are added to
 *	the tree SPARSELY since it is not intended that they be
 *	expanded during the course of evaluation.
 *
 *	we ignore arguments that end in .. because they have the
 *	potential to walk out of the base tree, because it can
 *	result in different names for a single file, and because
 *	should never be necessary to specify files that way.
 */
static errmask_t
add_file_arg(struct base *bp, char *path)
{	int i;
	errmask_t errs = 0;
	struct file *dp = 0;
	struct file *fp;
	char *s, *p;
	char name[ MAX_NAME ];

	/*
	 * see if someone is trying to feed us a ..
	 */
	if (strcmp(path, "..") == 0 || prefix(path, "../") ||
	    suffix(path, "/..") || contains(path, "/../")) {
		fprintf(stderr, gettext(WARN_ignore), path);
		return (ERR_MISSING);
	}

	/*
	 * strip off any trailing "/." or "/"
	 *	since noone will miss these, it is safe to actually
	 *	take them off the name.  When we fall out of this
	 *	loop, s will point where the null belongs.  We don't
	 *	actually null the end of string yet because we want
	 *	to leave it pristine for error messages.
	 */
	for (s = path; *s; s++);
	while (s > path) {
		if (s[-1] == '/') {
			s--;
			continue;
		}
		if (s[-1] == '.' && s > &path[1] && s[-2] == '/') {
			s -= 2;
			continue;
		}
		break;
	}

	/*
	 * skip over leading "/" and "./" (but not over a lone ".")
	 */
	for (p = path; p < s; ) {
		if (*p == '/') {
			p++;
			continue;
		}
		if (*p == '.' && s > &p[1] && p[1] == '/') {
			p += 2;
			continue;
		}
		break;
	}

	/*
	 * if there is nothing left, we're miffed, but done
	 */
	if (p >= s) {
		fprintf(stderr, gettext(WARN_ignore), path);
		return (ERR_MISSING);
	} else {
		/*
		 * this is actually storing a null into the argument,
		 * but it is OK to do this because the stuff we are
		 * truncating really is garbage that noone will ever
		 * want to see.
		 */
		*s = 0;
		path = p;
	}

	/*
	 * see if there are any restrictions that would force
	 * us to ignore this argument
	 */
	if (check_restr(bp, path) == 0)
		return (0);

	while (*path) {
		/* lex off the next name component	*/
		for (i = 0; path[i] && path[i] != '/'; i++)
			name[i] = path[i];
		name[i] = 0;

		/* add it into the database		*/
		fp = (dp == 0)  ? add_file_to_base(bp, name)
				: add_file_to_dir(dp, name);

		/* see if this was an intermediate directory	*/
		if (path[i] == '/') {
			fp->f_flags |= F_LISTED | F_SPARSE;
			path += i+1;
		} else {
			fp->f_flags |= F_LISTED;
			path += i;
		}

		dp = fp;
	}

	return (errs);
}

/*
 * routine:
 *	eval_file
 *
 * purpose:
 *	to evaluate one named file under a particular directory
 *
 * parameters:
 *	pointer to base structure
 *	pointer to file structure
 *
 * returns:
 *	error mask
 *	filled in evaluations in the baseline
 *
 * note:
 *	due to new rules and other restrictions we may not be expected
 *	to evaluate the entire tree.  We should only be called on files
 *	that are LISTed, and we should only invoke ourselves recursively
 *	on such files.
 */
static errmask_t
eval_file(struct base *bp, struct file *fp)
{	errmask_t errs = 0;
	int rc;
	char *name;
	struct file *cp;
	struct stat statb;

	if (opt_debug & DBG_EVAL)
		fprintf(stderr, "EVAL: FILE, flags=%s, name=%s\n",
			showflags(fileflags, fp->f_flags), fp->f_name);

	/* stat the file and fill in the file structure information	*/
	name = get_name(fp);

#ifdef 	DBG_ERRORS
	/* see if we should simulated a stat error on this file	*/
	if (opt_errors && (errno = dbg_chk_error(name, usingsrc ? 's' : 'S')))
		rc = -1;
	else
#endif
	rc = lstat(name, &statb);

	if (rc < 0) {
		if (opt_debug & DBG_EVAL)
			fprintf(stderr, "EVAL: FAIL lstat, errno=%d\n", errno);
		switch (errno) {
		    case EACCES:
			fp->f_flags |= F_STAT_ERROR;
			return (ERR_PERM);
		    case EOVERFLOW:
			fp->f_flags |= F_STAT_ERROR;
			return (ERR_UNRESOLVED);
		    default:
			return (ERR_MISSING);
		}
	}

	/* record the information we've just gained			*/
	note_info(fp, &statb, usingsrc ? OPT_SRC : OPT_DST);

	/*
	 * checking for ACLs is expensive, so we only do it if we
	 * have been asked to, or if we have reason to believe that
	 * the file has an ACL
	 */
	if (opt_acls || fp->f_info[OPT_BASE].f_numacls)
		(void) get_acls(name,
				&fp->f_info[usingsrc ? OPT_SRC : OPT_DST]);


	/* note that this file has been evaluated			*/
	fp->f_flags |= F_EVALUATE;

	/* if it is not a directory, a simple stat will suffice	*/
	if ((statb.st_mode & S_IFMT) != S_IFDIR)
		return (0);

	/*
	 * as a sanity check, we look for changes in the I-node
	 * numbers associated with LISTed directories ... on the
	 * assumption that these are high-enough up on the tree
	 * that they aren't likely to change, and so a change
	 * might indicate trouble.
	 */
	if (fp->f_flags & F_LISTED)
		check_inum(fp, usingsrc);

	/*
	 * sparse directories are on the path between a base and
	 * a listed directory.  As such, we don't walk these
	 * directories.  Rather, we just enumerate the LISTed
	 * files.
	 */
	if (fp->f_flags & F_SPARSE) {
		push_name(fp->f_name);

		/* this directory isn't supposed to be fully walked	*/
		for (cp = fp->f_files; cp; cp = cp->f_next)
			if (cp->f_flags & F_LISTED) {
				errs |= eval_file(bp, cp);
				cp->f_flags &= ~F_LISTED;
			}
		pop_name();
	} else {
		/* fully walk the tree beneath this directory		*/
		walk_errs = 0;
		cur_base = bp;
		cur_dir = fp;
		nftw(get_name(fp), &walker, MAX_DEPTH, FTW_PHYS|FTW_MOUNT);
		errs |= walk_errs;
	}

	return (errs);
}

/*
 * routine:
 *	walker
 *
 * purpose:
 *	node visitor for recursive directory enumeration
 *
 * parameters:
 *	name of file
 *	pointer to stat buffer for file
 *	file type
 *	FTW structure (base name offset, walk-depth)
 *
 * returns:
 *	0 	continue
 *	-1	stop
 *
 * notes:
 *	Ignoring files is easy, but ignoring directories is harder.
 *	Ideally we would just decline to walk the trees beneath
 *	ignored directories, but ftw doesn't allow the walker to
 *	tell it to "don't enter this directory, but continue".
 *
 *	Instead, we have to set a global to tell us to ignore
 *	everything under that tree.  The variable ignore_level
 *	is set to a level, below which, everything should be
 *	ignored.  Once the enumeration rises above that level
 *	again, we clear it.
 */
static int
walker(const char *name, const struct stat *sp, int type,
		struct FTW *ftwx)
{	const char *path;
	struct file *fp;
	int level;
	int which;
	bool_t restr;
	static struct file *dirstack[ MAX_DEPTH + 1 ];
	static int ignore_level = 0;

	path = &name[ftwx->base];
	level = ftwx->level;
	which = usingsrc ? OPT_SRC : OPT_DST;

	/*
	 * see if we are ignoring all files in this sub-tree
	 */
	if (ignore_level > 0 && level >= ignore_level) {
		if (opt_debug & DBG_EVAL)
			fprintf(stderr, "EVAL: SKIP file=%s\n", name);
		return (0);
	} else
		ignore_level = 0;	/* we're through ignoring	*/

#ifdef 	DBG_ERRORS
	/* see if we should simulated a stat error on this file	*/
	if (opt_errors && dbg_chk_error(name, usingsrc ? 'n' : 'N'))
		type = FTW_NS;
#endif

	switch (type) {
	case FTW_F:	/* file 		*/
	case FTW_SL:	/* symbolic link	*/
		/*
		 * filter out files of inappropriate types
		 */
		switch (sp->st_mode & S_IFMT) {
			default:	/* anything else we ignore	*/
				return (0);

			case S_IFCHR:
			case S_IFBLK:
			case S_IFREG:
			case S_IFLNK:
				if (opt_debug & DBG_EVAL)
					fprintf(stderr,
						"EVAL: WALK lvl=%d, file=%s\n",
						level, path);

				/* see if we were told to ignore this one */
				if (ignore_check(path))
					return (0);

				fp = add_file_to_dir(dirstack[level-1], path);
				note_info(fp, sp, which);

				/* note that this file has been evaluated */
				fp->f_flags |= F_EVALUATE;

				/* see if we should check ACLs		*/
				if ((sp->st_mode & S_IFMT) == S_IFLNK)
					return (0);

				if (fp->f_info[OPT_BASE].f_numacls || opt_acls)
					(void) get_acls(name,
							&fp->f_info[which]);

				return (0);
		}

	case FTW_D:	/* enter directory 		*/
		if (opt_debug & DBG_EVAL)
			fprintf(stderr, "EVAL: WALK lvl=%d, dir=%s\n",
				level, name);

		/*
		 * if we have been told to ignore this directory, we should
		 * ignore all files under it.  Similarly, if we are outside
		 * of our restrictions, we should ignore the entire subtree
		 */
		restr = check_restr(cur_base, name);
		if (restr == FALSE || ignore_check(path)) {
			ignore_level = level + 1;
			return (0);
		}

		fp = (level == 0) ?  cur_dir :
		    add_file_to_dir(dirstack[level-1], path);

		note_info(fp, sp, which);

		/* see if we should be checking ACLs	*/
		if (opt_acls || fp->f_info[OPT_BASE].f_numacls)
			(void) get_acls(name, &fp->f_info[which]);

		/* note that this file has been evaluated */
		fp->f_flags |= F_EVALUATE;

		/* note the parent of the children to come	*/
		dirstack[ level ] = fp;

		/*
		 * PROBLEM: given the information that nftw provides us with,
		 *	    how do we know that we have confirmed the fact
		 *	    that a file no longer exists.  Or to rephrase
		 *	    this in filesync terms, how do we know when to
		 *	    set the EVALUATE flag for a file we didn't find.
		 *
		 * if we are going to fully scan this directory (we
		 * are completely within our restrictions) then we
		 * will be confirming the non-existance of files that
		 * used to be here.  Thus any file that was in the
		 * base line under this directory should be considered
		 * to have been evaluated (whether we found it or not).
		 *
		 * if, however, we are only willing to scan selected
		 * files (due to restrictions), or the file was not
		 * in the baseline, then we should not assume that this
		 * pass will evaluate it.
		 */
		if (restr == TRUE)
			for (fp = fp->f_files; fp; fp = fp->f_next) {
				if ((fp->f_flags & F_IN_BASELINE) == 0)
					continue;
				fp->f_flags |= F_EVALUATE;
			}

		return (0);

	case FTW_DP:	/* end of directory	*/
		dirstack[ level ] = 0;
		break;

	case FTW_DNR:	/* unreadable directory	*/
		walk_errs |= ERR_PERM;
		/* FALLTHROUGH	*/
	case FTW_NS:	/* unstatable file	*/
		if (opt_debug & DBG_EVAL)
			fprintf(stderr, "EVAL: walker can't stat/read %s\n",
				name);
		fp = (level == 0) ?  cur_dir :
			add_file_to_dir(dirstack[level-1], path);
		fp->f_flags |= F_STAT_ERROR;
		walk_errs |= ERR_UNRESOLVED;
		break;
	}

	return (0);
}

/*
 * routine:
 *	note_info
 *
 * purpose:
 * 	to record information about a file in its file node
 *
 * parameters
 *	file node pointer
 *	stat buffer
 *	which file info structure to fill in (0-2)
 *
 * returns
 *	void
 */
void
note_info(struct file *fp, const struct stat *sp, side_t which)
{	struct fileinfo *ip;
	static int flags[3] = { F_IN_BASELINE, F_IN_SOURCE, F_IN_DEST };

	ip = &fp->f_info[ which ];

	ip->f_ino	= sp->st_ino;
	ip->f_d_maj	= major(sp->st_dev);
	ip->f_d_min	= minor(sp->st_dev);
	ip->f_type	= sp->st_mode & S_IFMT;
	ip->f_size	= sp->st_size;
	ip->f_mode	= sp->st_mode & S_IAMB;
	ip->f_uid	= sp->st_uid;
	ip->f_gid	= sp->st_gid;
	ip->f_modtime	= sp->st_mtim.tv_sec;
	ip->f_modns	= sp->st_mtim.tv_nsec;
	ip->f_nlink	= sp->st_nlink;
	ip->f_rd_maj	= major(sp->st_rdev);
	ip->f_rd_min	= minor(sp->st_rdev);

	/* indicate where this file has been found	*/
	fp->f_flags |= flags[which];

	if (opt_debug & DBG_STAT)
		fprintf(stderr,
			"STAT: list=%d, file=%s, mod=%08lx.%08lx, nacl=%d\n",
			which, fp->f_name, ip->f_modtime, ip->f_modns,
			ip->f_numacls);
}

/*
 * routine:
 *	do_update
 *
 * purpose:
 * 	to copy information from one side into the baseline in order
 *	to reflect the effects of recent reconciliation actions
 *
 * parameters
 *	fileinfo structure to be updated
 *	fileinfo structure to be updated from
 *
 * returns
 *	void
 *
 * note:
 *	we play fast and loose with the copying of acl chains
 *	here, but noone is going to free or reuse any of this
 * 	memory anyway.  None the less, I do feel embarassed.
 */
static void
do_update(struct fileinfo *np, struct fileinfo *ip)
{
	/* get most of the fields from the designated "right" copy */
	np->f_type	= ip->f_type;
	np->f_size	= ip->f_size;
	np->f_mode	= ip->f_mode;
	np->f_uid	= ip->f_uid;
	np->f_gid	= ip->f_gid;
	np->f_rd_maj	= ip->f_rd_maj;
	np->f_rd_min	= ip->f_rd_min;

	/* see if facls have to be propagated	*/
	np->f_numacls = ip->f_numacls;
	np->f_acls = ip->f_acls;
}

/*
 * routine:
 *	update_info
 *
 * purpose:
 * 	to update the baseline to reflect recent reconcliations
 *
 * parameters
 *	file node pointer
 *	which file info structure to trust (1/2)
 *
 * returns
 *	void
 *
 * note:
 *	after we update this I-node we run down the entire
 *	change list looking for links and update them too.
 *	This is to ensure that when subsequent links get
 *	reconciled, they are already found to be up-to-date.
 */
void
update_info(struct file *fp, side_t which)
{
	/* first update the specified fileinfo structure	*/
	do_update(&fp->f_info[ OPT_BASE ], &fp->f_info[ which ]);

	if (opt_debug & DBG_STAT)
		fprintf(stderr,
			"STAT: UPDATE from=%d, file=%s, mod=%08lx.%08lx\n",
			which, fp->f_name, fp->f_info[ which ].f_modtime,
			fp->f_info[ which ].f_modns);
}

/*
 * routine:
 *	fakedata
 *
 * purpose:
 *	to populate a tree we cannot analyze with information from the baseline
 *
 * parameters:
 *	file to be faked
 *	which side to fake
 *
 * notes:
 *	We would never use this for real reconciliation, but it is useful
 *	if a disconnected notebook user wants to find out what has been
 *	changed so far.  We only do this if we are notouch and oneway.
 */
static void
fakedata(struct file *fp, int which)
{	struct file *lp;

	/* pretend we actually found the file			*/
	fp->f_flags |= (which == OPT_SRC) ? F_IN_SOURCE : F_IN_DEST;

	/* update the specified side from the baseline		*/
	do_update(&fp->f_info[ which ], &fp->f_info[ OPT_BASE ]);
	fp->f_info[which].f_nlink = (which == OPT_SRC) ? fp->f_s_nlink :
							fp->f_d_nlink;
	fp->f_info[which].f_modtime = (which == OPT_SRC) ? fp->f_s_modtime :
							fp->f_d_modtime;

	for (lp = fp->f_files; lp; lp = lp->f_next)
		fakedata(lp, which);
}

/*
 * routine:
 *	check_inum
 *
 * purpose:
 *	sanity check inode #s on directories that are unlikely to change
 *
 * parameters:
 *	pointer to file node
 *	are we using the source
 *
 * note:
 *	the purpose of this sanity check is to catch a case where we
 *	have somehow been pointed at a directory that is not the one
 *	we expected to be reconciling against.  It could happen if a
 *	variable wasn't properly set, or if we were in a new domain
 *	where an old path no longer worked.  This could result in
 *	bazillions of inappropriate propagations and deletions.
 */
void
check_inum(struct file *fp, int src)
{	struct fileinfo *ip;

	/*
	 * we validate the inode number and the major device numbers ... minor
	 * device numbers for NFS devices are arbitrary
	 */
	if (src) {
		ip = &fp->f_info[ OPT_SRC ];
		if (ip->f_ino == fp->f_s_inum && ip->f_d_maj == fp->f_s_maj)
			return;

		/* if file was newly created/deleted, this isn't warnable */
		if (fp->f_s_inum == 0 || ip->f_ino == 0)
			return;

		if (opt_verbose)
			fprintf(stdout, V_change, fp->f_name, TXT_src,
				fp->f_s_maj, fp->f_s_min, fp->f_s_inum,
				ip->f_d_maj, ip->f_d_min, ip->f_ino);
	} else {
		ip = &fp->f_info[ OPT_DST ];
		if (ip->f_ino == fp->f_d_inum && ip->f_d_maj == fp->f_d_maj)
			return;

		/* if file was newly created/deleted, this isn't warnable */
		if (fp->f_d_inum == 0 || ip->f_ino == 0)
			return;

		if (opt_verbose)
			fprintf(stdout, V_change, fp->f_name, TXT_dst,
				fp->f_d_maj, fp->f_d_min, fp->f_d_inum,
				ip->f_d_maj, ip->f_d_min, ip->f_ino);
	}

	/* note that something has changed	*/
	inum_changes++;
}

/*
 * routine:
 *	add_glob
 *
 * purpose:
 *	to evaluate a wild-carded expression into names, and add them
 *	to the evaluation list.
 *
 * parameters:
 *	base
 *	expression
 *
 * returns:
 * 	error mask
 *
 * notes:
 *	we don't want to allow any patterns to expand to a . because
 *	that could result in re-evaluation of a tree under a different
 *	name.  The real thing we are worried about here is ".*" which
 *	is meant to pick up . files, but shouldn't pick up . and ..
 */
static errmask_t
add_glob(struct base *bp, char *expr)
{	int i;
	errmask_t errs = 0;
#ifndef BROKEN_GLOB
	glob_t gt;
	char *s;

	/* expand the regular expression	*/
	i = glob(expr, GLOB_NOSORT, 0, &gt);
	if (i == GLOB_NOMATCH)
		return (ERR_MISSING);
	if (i) {
		/* this shouldn't happen, so it's cryptic message time	*/
		fprintf(stderr, "EVAL: add_glob globfail expr=%s, ret=%d\n",
				expr, i);
		return (ERR_OTHER);
	}

	for (i = 0; i < gt.gl_pathc; i++) {
		/* make sure we don't let anything expand to a . */
		s = basename(gt.gl_pathv[i]);
		if (strcmp(s, ".") == 0) {
			fprintf(stderr, gettext(WARN_ignore), gt.gl_pathv[i]);
			errs |= ERR_MISSING;
			continue;
		}

		errs |= add_file_arg(bp, gt.gl_pathv[i]);
	}

	globfree(&gt);
#else
	/*
	 * in 2.4 the glob function was completely broken.  The
	 * easiest way to get around this problem is to just ask
	 * the shell to do the work for us.  This is much slower
	 * but produces virtually identical results.  Given that
	 * the 2.4 version is internal use only, I probably won't
	 * worry about the performance difference (less than 2
	 * seconds for a typical filesync command, and no hit
	 * at all if they don't use regular expressions in
	 * their LIST rules).
	 */
	char cmdbuf[MAX_LINE];

	sprintf(cmdbuf, "ls -d %s 2> /dev/null", expr);
	errs |= add_run(bp, cmdbuf);
#endif

	return (errs);
}


/*
 * routine:
 *	add_run
 *
 * purpose:
 *	to run a command and capture the names it outputs in the
 *	evaluation list.
 *
 * parameters
 *	base
 *	command
 *
 * returns:
 *	error mask
 */
static errmask_t
add_run(struct base *bp, char *cmd)
{	char *s, *p;
	FILE *fp;
	char inbuf[ MAX_LINE ];
	errmask_t errs = 0;
	int added = 0;

	if (opt_debug & DBG_EVAL)
		fprintf(stderr, "EVAL: RUN %s\n", cmd);

	/* run the command and collect its ouput	*/
	fp = popen(cmd, "r");
	if (fp == NULL) {
		fprintf(stderr, gettext(ERR_badrun), cmd);
		return (ERR_OTHER);
	}

	while (fgets(inbuf, sizeof (inbuf), fp) != 0) {
		/* strip off any trailing newline	*/
		for (s = inbuf; *s && *s != '\n'; s++);
		*s = 0;

		/* skip any leading white space		*/
		for (s = inbuf; *s == ' ' || *s == '\t'; s++);

		/* make sure we don't let anything expand to a . */
		p = basename(s);
		if (strcmp(p, ".") == 0) {
			fprintf(stderr, gettext(WARN_ignore), s);
			errs |= ERR_MISSING;
			continue;
		}

		/* add this file to the list		*/
		if (*s) {
			errs |= add_file_arg(bp, s);
			added++;
		}
	}

	pclose(fp);

#ifdef	BROKEN_GLOB
	/*
	 * if we are being used to simulate libc glob, and we didn't
	 * return anything, we should probably assume that the regex
	 * was unable to match anything
	 */
	if (added == 0)
		errs |= ERR_MISSING;
#endif
	return (errs);
}