root/usr/src/cmd/filesync/action.c
/*
 * 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 (c) 1995 Sun Microsystems, Inc.  All Rights Reserved
 * Copyright (c) 2016 by Delphix. All rights reserved.
 *
 * module:
 *      action.c
 *
 * purpose:
 *      routines to carryout reconciliation actions and make the
 *      appropriate updates to the database file structure.
 *
 * contents:
 *      do_like ... change ownership and protection
 *      do_copy ... copy a file from one side to the other
 *      do_remove . remove a file from one side
 *      do_rename . rename a file on one side
 *      copy ...... (static) do the actual copy
 *      checksparse (static) figure out if a file is sparse
 *
 * ASSERTIONS:
 *      any of these action routines is responsible for all baseline
 *      and statistics updates associated with the reconciliation
 *      actions.  If notouch is specified, they should fake the
 *      updates well enough so that link tests will still work.
 *
 *      success:
 *              bump bp->b_{src,dst}_{copies,deletes,misc}
 *              update fp->f_info[srcdst]
 *              update fp->f_info[OPT_BASE] from fp->f_info[srcdst]
 *              if there might be multiple links, call link_update
 *              return ERR_RESOLVABLE
 *
 *      failure:
 *              set fp->f_flags |= F_CONFLICT
 *              set fp->f_problem
 *              bump bp->b_unresolved
 *              return ERR_UNRESOLVED
 *
 *      pretend this never happened:
 *              return 0, and baseline will be unchanged
 *
 * notes:
 *      Action routines can be called in virtually any order
 *      or combination, and it is certainly possible for an
 *      earlier action to succeed while a later action fails.
 *      If each successful action results in a completed baseline
 *      update, a subsequent failure will force the baseline to
 *      roll back to the last success ... which is appropriate.
 */
#ident  "%W%    %E% SMI"

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <utime.h>
#include <errno.h>
#include <sys/mkdev.h>
#include <sys/statvfs.h>

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

/*
 * globals and importeds
 */
bool_t need_super;      /* warn user that we can't fix ownership        */
extern char *srcname;   /* file we are emulating                        */
extern char *dstname;   /* file we are updating                         */

/*
 * locals
 */
static errmask_t copy(char *, char *, int);
static int checksparse(int);
static char *copy_err_str;              /* what went wrong w/copy       */

/*
 * routine:
 *      do_like
 *
 * purpose:
 *      to propagate ownership and protection changes between
 *      one existing file and another.
 *
 * parameters:
 *      file pointer
 *      src/dst indication for who needs to change
 *      whether or not to update statistics (there may be a copy and a like)
 *
 * returns:
 *      error mask
 *
 * notes:
 *      if we are called from reconcile, we should update
 *      the statistics, but if we were called from do_copy
 *      that routine will do the honors.
 */
errmask_t
do_like(struct file *fp, side_t srcdst, bool_t do_stats)
{       char *dst;
        int rc = 0;
        int do_chown, do_chmod, do_chgrp, do_acls;
        errmask_t errs = 0;
        char *errstr = 0;
        struct base *bp;
        struct fileinfo *sp;
        struct fileinfo *dp;
        struct fileinfo *ip;
        extern int errno;

        bp = fp->f_base;

        /* see if this is a forbidden propagation */
        if (srcdst == opt_oneway) {
                fp->f_flags |= F_CONFLICT;
                fp->f_problem = gettext(PROB_prohibited);
                bp->b_unresolved++;
                return (ERR_UNRESOLVED);
        }


        /* get info about source and target files               */
        if (srcdst == OPT_SRC) {
                sp = &fp->f_info[ OPT_DST ];
                dp = &fp->f_info[ OPT_SRC ];
                dst = srcname;
        } else {
                sp = &fp->f_info[ OPT_SRC ];
                dp = &fp->f_info[ OPT_DST ];
                dst = dstname;
        }
        ip = &fp->f_info[ OPT_BASE ];

        /* figure out what needs fixing                         */
        do_chmod = (sp->f_mode != dp->f_mode);
        do_chown = (sp->f_uid != dp->f_uid);
        do_chgrp = (sp->f_gid != dp->f_gid);
        do_acls  = ((fp->f_srcdiffs|fp->f_dstdiffs) & D_FACLS);

        /*
         * try to anticipate things that we might not be able to
         * do, and return appropriate errorst if the calling user
         * cannot safely perform the requiested updates.
         */
        if (my_uid != 0) {
                if (do_chown)
                        errstr = gettext(PROB_chown);
                else if (my_uid != dp->f_uid) {
                        if (do_chmod)
                                errstr = gettext(PROB_chmod);
                        else if (do_acls)
                                errstr = gettext(PROB_chacl);
                        else if (do_chgrp)
                                errstr = gettext(PROB_chgrp);
                }
#ifdef  ACL_UID_BUG
                else if (do_acls && my_gid != dp->f_gid)
                        errstr = gettext(PROB_botch);
#endif

                if (errstr) {
                        need_super = TRUE;

                        /* if the user doesn't care, shine it on        */
                        if (opt_everything == 0)
                                return (0);

                        /* if the user does care, return the error      */
                        rc = -1;
                        goto nogood;
                }
        }

        if (opt_debug & DBG_RECON) {
                fprintf(stderr, "RECO: do_like %s (", dst);
                if (do_chmod)
                        fprintf(stderr, "chmod ");
                if (do_acls)
                        fprintf(stderr, "acls ");
                if (do_chown)
                        fprintf(stderr, "chown ");
                if (do_chgrp)
                        fprintf(stderr, "chgrp ");
                fprintf(stderr, ")\n");
        }

        if (do_chmod) {
                if (!opt_quiet)
                        fprintf(stdout, "chmod %o %s\n", sp->f_mode,
                                                noblanks(dst));

#ifdef  DBG_ERRORS
                /* should we simulate a chmod failure   */
                if (errno = dbg_chk_error(dst, 'p'))
                        rc = -1;
                else
#endif
                rc = opt_notouch ? 0 : chmod(dst, sp->f_mode);

                if (opt_debug & DBG_RECON)
                        fprintf(stderr, "RECO: do_chmod %o -> %d(%d)\n",
                                sp->f_mode, rc, errno);

                /* update dest and baseline to reflect the change */
                if (rc == 0) {
                        dp->f_mode = sp->f_mode;
                        ip->f_mode = sp->f_mode;
                } else
                        errstr = gettext(PROB_chmod);
        }

        /*
         * see if we need to fix the acls
         */
        if (rc == 0 && do_acls) {
                if (!opt_quiet)
                        fprintf(stdout, "setfacl %s %s\n",
                                show_acls(sp->f_numacls, sp->f_acls),
                                noblanks(dst));

#ifdef  DBG_ERRORS
                /* should we simulate a set acl failure */
                if (errno = dbg_chk_error(dst, 'a'))
                        rc = -1;
                else
#endif
                rc = opt_notouch ? 0 : set_acls(dst, sp);

                if (opt_debug & DBG_RECON)
                        fprintf(stderr, "RECO: do_acls %d -> %d(%d)\n",
                                sp->f_numacls, rc, errno);

                /* update dest and baseline to reflect the change */
                if (rc == 0) {
                        dp->f_numacls = sp->f_numacls;
                        dp->f_acls = sp->f_acls;
                        ip->f_numacls = sp->f_numacls;
                        ip->f_acls = sp->f_acls;
#ifdef  ACL_UID_BUG
                        /* SETFACL changes a file's UID/GID     */
                        if (my_uid != dp->f_uid) {
                                do_chown = 1;
                                dp->f_uid = my_uid;
                        }
                        if (my_gid != dp->f_gid) {
                                do_chgrp = 1;
                                dp->f_gid = my_gid;
                        }
#endif
                } else if (errno == ENOSYS) {
                        /*
                         * if the file system doesn't support ACLs
                         * we should just pretend we never saw them
                         */
                        fprintf(stderr, gettext(WARN_noacls), dst);
                        ip->f_numacls = 0;
                        sp->f_numacls = 0;
                        dp->f_numacls = 0;
                        rc = 0;
                } else
                        errstr = gettext(PROB_chacl);
        }

        /*
         * see if we need to fix the ownership
         */
        if (rc == 0 && (do_chown || do_chgrp)) {
                if (do_chown)
                        fprintf(stdout, "chown %ld %s; ",
                                sp->f_uid, noblanks(dst));
                if (do_chgrp)
                        fprintf(stdout, "chgrp %ld %s",
                                sp->f_gid, noblanks(dst));

                fprintf(stdout, "\n");

#ifdef  DBG_ERRORS
                /* should we simulate a chown failure   */
                if (errno = dbg_chk_error(dst, 'O'))
                        rc = -1;
                else
#endif
                rc = opt_notouch ? 0 : lchown(dst, sp->f_uid, sp->f_gid);

                if (opt_debug & DBG_RECON)
                        fprintf(stderr, "RECO: do_chown %ld %ld -> %d(%d)\n",
                                        sp->f_uid, sp->f_gid, rc, errno);

                /* update the destination to reflect changes */
                if (rc == 0) {
                        dp->f_uid = sp->f_uid;
                        dp->f_gid = sp->f_gid;
                        ip->f_uid = sp->f_uid;
                        ip->f_gid = sp->f_gid;
                } else {
                        if (errno == EPERM) {
                                need_super = TRUE;
                                if (opt_everything == 0)
                                        return (0);
                        }

                        if (rc != 0)
                                errstr = gettext(do_chown ?
                                                PROB_chown : PROB_chgrp);
                }
        }

        /*
         * if we were successful, we should make sure the other links
         * see the changes.  If we were called from do_copy, we don't
         * want to do the link_updates either because do_copy will
         * handle them too.
         */
        if (rc == 0 && do_stats)
                link_update(fp, srcdst);

nogood:
        if (!do_stats)
                return (errs);

        if (rc != 0) {
                fprintf(stderr, gettext(ERR_cannot), errstr, dst);
                fp->f_problem = errstr;
                fp->f_flags |= F_CONFLICT;
                bp->b_unresolved++;
                errs |= ERR_PERM | ERR_UNRESOLVED;
        } else {
                /*
                 * it worked, so update the baseline and statistics
                 */
                if (srcdst == OPT_SRC)
                        bp->b_src_misc++;
                else
                        bp->b_dst_misc++;

                fp->f_problem = 0;
                errs |= ERR_RESOLVABLE;
        }

        return (errs);
}

/*
 * routine:
 *      do_copy
 *
 * purpose:
 *      to propagate a creation or change
 *
 * parameters:
 *      file pointer
 *      src/dst indication for who gets the copy
 *
 * returns:
 *      error mask
 *
 * note:
 *      after any successful operation we update the stat/info
 *      structure for the updated file.  This is somewhat redundant
 *      because we will restat at the end of the routine, but these
 *      anticipatory updates help to ensure that the link finding
 *      code will still behave properly in notouch mode (when restats
 *      cannot be done).
 */
errmask_t
do_copy(struct file *fp, side_t srcdst)
{       char *src, *dst;
        char cmdbuf[ MAX_PATH + MAX_NAME ];
        int mode, maj, min, type;
        uid_t uid;
        gid_t gid;
        int rc;
        long mtime;
        int do_chmod = 0;
        int do_chown = 0;
        int do_chgrp = 0;
        int do_unlink = 0;
        int do_acls = 0;
        int do_create = 0;
        char *errstr = "???";
        errmask_t errs = 0;
        struct base *bp;
        struct file *lp;
        struct fileinfo *sp, *dp;
        struct utimbuf newtimes;
        struct stat statb;

        bp = fp->f_base;

        /* see if this is a forbidden propagation */
        if (srcdst == opt_oneway) {
                fp->f_problem = gettext(PROB_prohibited);
                fp->f_flags |= F_CONFLICT;
                bp->b_unresolved++;
                return (ERR_UNRESOLVED);
        }

        /* figure out who is the source and who is the destination      */
        if (srcdst == OPT_SRC) {
                sp = &fp->f_info[ OPT_DST ];
                dp = &fp->f_info[ OPT_SRC ];
                src = dstname;
                dst = srcname;
        } else {
                sp = &fp->f_info[ OPT_SRC ];
                dp = &fp->f_info[ OPT_DST ];
                src = srcname;
                dst = dstname;
        }

        /* note information about the file to be created                */
        type  = sp->f_type;             /* type of the new file         */
        uid   = sp->f_uid;              /* owner of the new file        */
        gid   = sp->f_gid;              /* group of the new file        */
        mode  = sp->f_mode;             /* modes for the new file       */
        mtime = sp->f_modtime;          /* modtime (if preserving)      */
        maj   = sp->f_rd_maj;           /* major (if it is a device)    */
        min   = sp->f_rd_min;           /* minor (if it is a device)    */

        /*
         * creating a file does not guarantee it will get the desired
         * modes, uid and gid.  If the file already exists, it will
         * retain its old ownership and protection.  If my UID/GID
         * are not the desired ones, the new file will also require
         * manual correction.  If the file has the wrong type, we will
         * need to delete it and recreate it.  If the file is not writable,
         * it is easier to delete it than to chmod it to permit overwrite
         */
        if ((dp->f_type == S_IFREG && sp->f_type == S_IFREG) &&
            (dp->f_mode & 0200)) {
                /* if the file already exists           */
                if (dp->f_uid != uid)
                        do_chown = 1;

                if (dp->f_gid != gid)
                        do_chgrp = 1;

                if (dp->f_mode != mode)
                        do_chmod = 1;
        } else {
                /* if we will be creating a new file    */
                do_create = 1;
                if (dp->f_type)
                        do_unlink = 1;
                if (uid != my_uid)
                        do_chown = 1;
                if (gid != my_gid)
                        do_chgrp = 1;
        }

        /*
         * if the source has acls, we will surely have to set them for dest
         */
        if (sp->f_numacls)
                do_acls = 1;

        /*
         * for any case other than replacing a normal file with a normal
         * file, we need to delete the existing file before creating
         * the new one.
         */
        if (do_unlink) {
                if (dp->f_type == S_IFDIR) {
                        if (!opt_quiet)
                                fprintf(stdout, "rmdir %s\n", noblanks(dst));

                        errstr = gettext(PROB_rmdir);
#ifdef  DBG_ERRORS
                        /* should we simulate a rmdir failure   */
                        if (errno = dbg_chk_error(dst, 'D'))
                                rc = -1;
                        else
#endif
                        rc = opt_notouch ? 0 : rmdir(dst);
                } else {
                        if (!opt_quiet)
                                fprintf(stdout, "rm %s\n", noblanks(dst));

                        errstr = gettext(PROB_unlink);
#ifdef  DBG_ERRORS
                        /* should we simulate a unlink failure  */
                        if (errno = dbg_chk_error(dst, 'u'))
                                rc = -1;
                        else
#endif
                        rc = opt_notouch ? 0 : unlink(dst);
                }

                if (rc != 0)
                        goto cant;

                /* note that this file no longer exists         */
                dp->f_type = 0;
                dp->f_mode = 0;
        }

        if (opt_debug & DBG_RECON) {
                fprintf(stderr, "RECO: do_copy %s %s (", src, dst);
                if (do_unlink)
                        fprintf(stderr, "unlink ");
                if (do_chmod)
                        fprintf(stderr, "chmod ");
                if (do_acls)
                        fprintf(stderr, "acls ");
                if (do_chown)
                        fprintf(stderr, "chown ");
                if (do_chgrp)
                        fprintf(stderr, "chgrp ");
                fprintf(stderr, ")\n");
        }

        /*
         * how we go about copying a file depends on what type of file
         * it is that we are supposed to copy
         */
        switch (type) {
            case S_IFDIR:
                if (!opt_quiet) {
                        fprintf(stdout, "mkdir %s;", noblanks(dst));
                        fprintf(stdout, " chmod %o %s;\n", mode, noblanks(dst));
                }

                errstr = gettext(PROB_mkdir);

#ifdef  DBG_ERRORS
                /* should we simulate a mkdir failure   */
                if (errno = dbg_chk_error(dst, 'd'))
                        rc = -1;
                else
#endif
                rc = opt_notouch ? 0 : mkdir(dst, mode);

                /* update stat with what we have just created   */
                if (rc == 0) {
                        dp->f_type = S_IFDIR;
                        dp->f_uid = my_uid;
                        dp->f_gid = my_gid;
                        dp->f_mode = mode;
                }

                break;

            case S_IFLNK:
                errstr = gettext(PROB_readlink);
#ifdef  DBG_ERRORS
                /* should we simulate a symlink read failure    */
                if (errno = dbg_chk_error(dst, 'r'))
                        rc = -1;
                else
#endif
                rc = readlink(src, cmdbuf, sizeof (cmdbuf));
                if (rc > 0) {
                        cmdbuf[rc] = 0;
                        if (!opt_quiet) {
                                fprintf(stdout, "ln -s %s", noblanks(cmdbuf));
                                fprintf(stdout, " %s;\n", noblanks(dst));
                        }
                        errstr = gettext(PROB_symlink);
#ifdef  DBG_ERRORS
                        /* should we simulate a symlink failure */
                        if (errno = dbg_chk_error(dst, 'l'))
                                rc = -1;
                        else
#endif
                        rc = opt_notouch ? 0 : symlink(cmdbuf, dst);

                        if (rc == 0)
                                dp->f_type = S_IFLNK;
                }
                break;

            case S_IFBLK:
            case S_IFCHR:
                if (!opt_quiet)
                        fprintf(stdout, "mknod %s %s %d %d\n", noblanks(dst),
                                (type == S_IFBLK) ? "b" : "c", maj, min);

                errstr = gettext(PROB_mknod);
#ifdef  DBG_ERRORS
                /* should we simulate a mknod failure   */
                if (errno = dbg_chk_error(dst, 'd'))
                        rc = -1;
                else
#endif
                rc = opt_notouch ? 0
                                : mknod(dst, mode|type, makedev(maj, min));

                /* update stat with what we have just created   */
                if (rc == 0) {
                        dp->f_type = type;
                        dp->f_uid = my_uid;
                        dp->f_gid = my_gid;
                        dp->f_mode = 0666;

                        if (dp->f_mode != mode)
                                do_chmod = 1;
                }
                break;

            case S_IFREG:
                /*
                 * The first thing to do is ascertain whether or not
                 * the alleged new copy might in fact be a new link.
                 * We trust find_link to weigh all the various factors,
                 * so if it says make a link, we'll do it.
                 */
                lp = find_link(fp, srcdst);
                if (lp) {
                        /* figure out name of existing file     */
                        src = full_name(lp, srcdst, OPT_BASE);

                        /*
                         * if file already exists, it must be deleted
                         */
                        if (dp->f_type) {
                                if (!opt_quiet)
                                        fprintf(stdout, "rm %s\n",
                                                noblanks(dst));

                                errstr = gettext(PROB_unlink);
#ifdef  DBG_ERRORS
                                /* should we simulate a unlink failure  */
                                if (errno = dbg_chk_error(dst, 'u'))
                                        rc = -1;
                                else
#endif
                                rc = opt_notouch ? 0 : unlink(dst);

                                /*
                                 * if we couldn't do the unlink, we must
                                 * mark the linkee in conflict as well
                                 * so its reference count remains the same
                                 * in the baseline and it continues to show
                                 * up on the change list.
                                 */
                                if (rc != 0) {
                                        lp->f_flags |= F_CONFLICT;
                                        lp->f_problem = gettext(PROB_link);
                                        goto cant;
                                }
                        }

                        if (!opt_quiet) {
                                fprintf(stdout, "ln %s", noblanks(src));
                                fprintf(stdout, " %s\n", noblanks(dst));
                        }
                        errstr = gettext(PROB_link);

#ifdef  DBG_ERRORS
                        /* should we simulate a link failure    */
                        if (errno = dbg_chk_error(dst, 'l'))
                                rc = -1;
                        else
#endif
                        rc = opt_notouch ? 0 : link(src, dst);

                        /*
                         * if this is a link, there is no reason to worry
                         * about ownership and modes, they are automatic
                         */
                        do_chown = 0; do_chgrp = 0; do_chmod = 0; do_acls = 0;
                        if (rc == 0) {
                                dp->f_type = type;
                                dp->f_uid = uid;
                                dp->f_gid = gid;
                                dp->f_mode = mode;
                                break;
                        } else {
                                /*
                                 * if we failed to make a link, we want to
                                 * mark the linkee in conflict too, so that
                                 * its reference count remains the same in
                                 * the baseline, and it shows up on the change
                                 * list again next time.
                                 */
                                lp->f_flags |= F_CONFLICT;
                                lp->f_problem = errstr;
                                break;
                        }

                        /*
                         * in some situation we haven't figured out yet
                         * we might want to fall through and try a copy
                         * if the link failed.
                         */
                }

                /* we are going to resolve this by making a copy        */
                if (!opt_quiet) {
                        fprintf(stdout, "cp %s", noblanks(src));
                        fprintf(stdout, " %s\n", noblanks(dst));
                }
                rc = opt_notouch ? 0 : copy(src, dst, mode);
                if (rc != 0) {
                        errs |= rc;
                        if (copy_err_str)
                                errstr = copy_err_str;
                        else
                                errstr = gettext(PROB_copy);

                        /*
                         * The new copy (if it exists at all) is a botch.
                         * If this was a new create or a remove and copy
                         * we should get rid of the botched copy so that
                         * it doesn't show up as two versions next time.
                         */
                        if (do_create)
                                unlink(dst);
                } else if (dp->f_mode == 0) {
                        dp->f_type = S_IFREG;
                        dp->f_uid = my_uid;
                        dp->f_gid = my_gid;
                        dp->f_mode = mode;

                        /* FIX: inode number is still wrong     */
                }

                /* for normal files we have an option to preserve mod time  */
                if (rc == 0 && opt_notouch == 0 && opt_mtime) {
                        newtimes.actime = mtime;
                        newtimes.modtime = mtime;

                        /* ignore the error return on this one  */
                        (void) utime(dst, &newtimes);
                }
                break;

            default:
                errstr = gettext(PROB_deal);
                rc = -1;
        }

        /*
         * if any of the file's attributes need attention, I should let
         * do_like take care of them, since it knows all rules for who
         * can and cannot make what types of changes.
         */
        if (rc == 0 && (do_chmod || do_chown || do_chgrp || do_acls)) {
                rc = do_like(fp, srcdst, FALSE);
                errstr = fp->f_problem;
                errs |= rc;
        }

        /*
         * finish off by re-stating the destination and using that to
         * update the baseline.  If we were completely successful in
         * our chowns/chmods, stating the destination will confirm it.
         * If we were unable to make all the necessary changes, stating
         * the destination will make the source appear to have changed,
         * so that the differences will continue to reappear as new
         * changes (inconsistancies).
         */
        if (rc == 0)
                if (!opt_notouch) {
                        errstr = gettext(PROB_restat);

#ifdef  DBG_ERRORS
                        /* should we simulate a restat failure  */
                        if (errno = dbg_chk_error(dst, 'R'))
                                rc = -1;
                        else
#endif
                        rc = lstat(dst, &statb);

                        if (rc == 0) {
                                note_info(fp, &statb, srcdst);
                                link_update(fp, srcdst);
                                if (do_acls)
                                        (void) get_acls(dst, dp);
                                update_info(fp, srcdst);
                        }
                } else {
                        /*
                         * BOGOSITY ALERT
                         *      we are in notouch mode and haven't really
                         *      done anything, but if we want link detection
                         *      to work and be properly reflected in the
                         *      what-I-would-do output for a case where
                         *      multiple links are created to a new file,
                         *      we have to make the new file appear to
                         *      have been created.  Since we didn't create
                         *      the new file we can't stat it, but if
                         *      no file exists, we can't make a link to
                         *      it, so we will pretend we created a file.
                         */
                        if (dp->f_ino == 0 || dp->f_nlink == 0) {
                                dp->f_ino = sp->f_ino;
                                dp->f_nlink = 1;
                        }
                }

cant:   if (rc != 0) {
                fprintf(stderr, gettext(ERR_cannot), errstr, dst);
                bp->b_unresolved++;
                fp->f_flags |= F_CONFLICT;
                fp->f_problem = errstr;
                if (errs == 0)
                        errs = ERR_PERM;
                errs |= ERR_UNRESOLVED;
        } else {
                /* update the statistics                        */
                if (srcdst == OPT_SRC)
                        bp->b_src_copies++;
                else
                        bp->b_dst_copies++;
                errs |= ERR_RESOLVABLE;
        }

        return (errs);
}

/*
 * routine:
 *      do_remove
 *
 * purpose:
 *      to propagate a deletion
 *
 * parameters:
 *      file pointer
 *      src/dst indication for which side gets changed
 *
 * returns:
 *      error mask
 */
errmask_t
do_remove(struct file *fp, side_t srcdst)
{       char *name;
        int rc;
        struct base *bp = fp->f_base;
        errmask_t errs = 0;
        char *errstr = "???";

        /* see if this is a forbidden propagation */
        if (srcdst == opt_oneway) {
                fp->f_problem = gettext(PROB_prohibited);
                fp->f_flags |= F_CONFLICT;
                bp->b_unresolved++;
                return (ERR_UNRESOLVED);
        }

        name = (srcdst == OPT_SRC) ? srcname : dstname;

        if (fp->f_info[0].f_type == S_IFDIR) {
                if (!opt_quiet)
                        fprintf(stdout, "rmdir %s\n", noblanks(name));

                errstr = gettext(PROB_rmdir);

#ifdef  DBG_ERRORS
                /* should we simulate a rmdir failure   */
                if (errno = dbg_chk_error(name, 'D'))
                        rc = -1;
                else
#endif
                rc = opt_notouch ? 0 : rmdir(name);
        } else {
                if (!opt_quiet)
                        fprintf(stdout, "rm %s\n", noblanks(name));

                errstr = gettext(PROB_unlink);

#ifdef  DBG_ERRORS
                /* should we simulate an unlink failure */
                if (errno = dbg_chk_error(name, 'u'))
                        rc = -1;
                else
#endif
                rc = opt_notouch ? 0 : unlink(name);
        }

        if (opt_debug & DBG_RECON)
                fprintf(stderr, "RECO: do_remove %s -> %d(%d)\n",
                        name, rc, errno);

        if (rc == 0) {
                /* tell any other hard links that one has gone away     */
                fp->f_info[srcdst].f_nlink--;
                link_update(fp, srcdst);

                fp->f_flags |= F_REMOVE;
                if (srcdst == OPT_SRC)
                        fp->f_base->b_src_deletes++;
                else
                        fp->f_base->b_dst_deletes++;
                errs |= ERR_RESOLVABLE;
        } else {
                fprintf(stderr, gettext(ERR_cannot), errstr, name);
                fp->f_problem = errstr;
                fp->f_flags |= F_CONFLICT;
                bp->b_unresolved++;
                errs |= ERR_PERM | ERR_UNRESOLVED;
        }

        return (errs);
}

/*
 * routine:
 *      do_rename
 *
 * purpose:
 *      to propagate a rename
 *
 * parameters:
 *      file pointer for the new name
 *      src/dst indication for which side gets changed
 *
 * returns:
 *      error mask
 */
errmask_t
do_rename(struct file *fp, side_t srcdst)
{       int rc;
        struct file *pp = fp->f_previous;
        struct base *bp = fp->f_base;
        errmask_t errs = 0;
        char *errstr = "???";
        char *newname;
        char *oldname;
        struct stat statb;

        /* see if this is a forbidden propagation */
        if (srcdst == opt_oneway) {
                fp->f_problem = gettext(PROB_prohibited);

                /* if we can't resolve the TO, the FROM is also unresolved */
                pp->f_problem = gettext(PROB_prohibited);
                pp->f_flags |= F_CONFLICT;
                bp->b_unresolved++;
                return (ERR_UNRESOLVED);
        }

        newname = (srcdst == OPT_SRC) ? srcname : dstname;
        oldname = full_name(pp, srcdst, OPT_BASE);

        if (!opt_quiet)
                fprintf(stdout, "%s %s %s\n",
                        (fp->f_info[0].f_type == S_IFDIR) ? "mvdir" : "mv",
                        noblanks(oldname), noblanks(newname));

#ifdef  DBG_ERRORS
        /* should we simulate a rename failure  */
        if (errno = dbg_chk_error(oldname, 'm'))
                rc = -1;
        else
#endif
        rc = opt_notouch ? 0 : rename(oldname, newname);

        if (opt_debug & DBG_RECON)
                fprintf(stderr, "RECO: do_rename %s %s -> %d(%d)\n",
                        oldname, newname, rc, errno);

        /* if we succeed, update the baseline                   */
        if (rc == 0)
                if (!opt_notouch) {
                        errstr = gettext(PROB_restat);

#ifdef  DBG_ERRORS
                        /* should we simulate a restat failure  */
                        if (errno = dbg_chk_error(newname, 'S'))
                                rc = -1;
                        else
#endif
                        rc = lstat(newname, &statb);

                        if (rc == 0) {
                                note_info(fp, &statb, srcdst);
                                link_update(fp, srcdst);
                                update_info(fp, srcdst);
                        }
                } else {
                        /*
                         * BOGOSITY ALERT
                         * in order for link tests to work in notouch
                         * mode we have to dummy up some updated status
                         */
                        fp->f_info[srcdst].f_ino = pp->f_info[srcdst].f_ino;
                        fp->f_info[srcdst].f_nlink = pp->f_info[srcdst].f_nlink;
                        fp->f_info[srcdst].f_type = pp->f_info[srcdst].f_type;
                        fp->f_info[srcdst].f_size = pp->f_info[srcdst].f_size;
                        fp->f_info[srcdst].f_mode = pp->f_info[srcdst].f_mode;
                        fp->f_info[srcdst].f_uid = pp->f_info[srcdst].f_uid;
                        fp->f_info[srcdst].f_gid = pp->f_info[srcdst].f_gid;
                        update_info(fp, srcdst);
                }
        else
                errstr = gettext(PROB_rename2);

        if (rc == 0) {
                pp->f_flags |= F_REMOVE;

                if (srcdst == OPT_SRC) {
                        bp->b_src_copies++;
                        bp->b_src_deletes++;
                } else {
                        bp->b_dst_copies++;
                        bp->b_dst_deletes++;
                }
                errs |= ERR_RESOLVABLE;
        } else {
                fprintf(stderr, gettext(ERR_cannot), errstr, oldname);

                bp->b_unresolved++;
                fp->f_flags |= F_CONFLICT;
                pp->f_flags |= F_CONFLICT;

                fp->f_problem = errstr;
                pp->f_problem = gettext(PROB_rename);

                errs |= ERR_PERM | ERR_UNRESOLVED;
        }

        return (errs);
}

/*
 * routine:
 *      copy
 *
 * purpose:
 *      to copy one file to another
 *
 * parameters:
 *      source file name
 *      destination file name
 *      desired modes
 *
 * returns:
 *      0       OK
 *      else    error mask, and a setting of copy_err_str
 *
 * notes:
 *      We try to preserve the holes in sparse files, by skipping over
 *      any holes that are at least MIN_HOLE bytes long.  There are
 *      pathological cases where the hole detection test could become
 *      expensive, but for most blocks of most files we will fall out
 *      of the zero confirming loop in the first couple of bytes.
 */
static errmask_t
copy(char *src, char *dst, int mode)
{       int ifd, ofd, count, ret;
        long *p, *e;
        long long length;               /* total size of file   */
        errmask_t errs = 0;
        int bsize;                      /* block-size for file  */
        bool_t sparse;                  /* file may be sparse   */
        bool_t was_hole = FALSE;                /* file ends with hole  */
        long inbuf[ COPY_BSIZE/4 ];     /* long to speed checks */
        struct stat statbuf;            /* info on source file  */
        struct statvfs statvsbuf;       /* info on target fs    */

        copy_err_str = 0;

        /* open the input file                  */
#ifdef  DBG_ERRORS
        if (opt_errors && dbg_chk_error(src, 'o'))
                ifd = -1;
        else
#endif
        ifd = open(src, O_RDONLY);

        if (ifd < 0) {
                copy_err_str = gettext(PROB_copyin);
                return (ERR_PERM);
        }

        /*
         * if we suspect a file may be sparse, we must process it
         * a little more carefully, looking for holes and skipping
         * over them in the output.  If a file is not sparse, we
         * can move through it at greater speed.
         */
        bsize = checksparse(ifd);
        if (bsize > 0 && bsize <= COPY_BSIZE)
                sparse = TRUE;
        else {
                sparse = FALSE;
                bsize = COPY_BSIZE;
        }

        /*
         * if the target file already exists and we overwrite it without
         * first ascertaining that there is enough room, we could wind
         * up actually losing data.  Try to determine how much space is
         * available on the target file system, and if that is not enough
         * for the source file, fail without even trying.  If, however,
         * the target file does not already exist, we have nothing to
         * lose by just doing the copy without checking the space.
         */
        ret = statvfs(dst, &statvsbuf);
        if (ret == 0 && statvsbuf.f_frsize != 0) {
#ifdef  DBG_ERRORS
                /* should we simulate an out-of-space situation */
                if ((length = dbg_chk_error(dst, 'Z')) == 0)
#endif
                length = statvsbuf.f_bavail * statvsbuf.f_frsize;

                ret = fstat(ifd, &statbuf);
                if (ret == 0) {
                        length /= 512;          /* st_blocks in 512s    */
                        if (length < statbuf.st_blocks) {
                                copy_err_str = gettext(PROB_space);
                                close(ifd);
                                return (ERR_FILES);
                        }
                } else {
                        copy_err_str = gettext(PROB_restat);
                        close(ifd);
                        return (ERR_FILES);
                }
        }

        /* create the output file               */
#ifdef  DBG_ERRORS
        if (opt_errors && dbg_chk_error(dst, 'c'))
                ofd = -1;
        else
#endif
        ofd = creat(dst, mode);

        if (ofd < 0) {
                close(ifd);
                copy_err_str = gettext(PROB_copyout);
                return (ERR_PERM);
        }

        /* copy the data from the input file to the output file */
        for (;;) {
#ifdef  DBG_ERRORS
                if (opt_errors && dbg_chk_error(dst, 'r'))
                        count = -1;
                else
#endif
                count = read(ifd, (char *) inbuf, bsize);
                if (count <= 0)
                        break;

                /*
                 * if the file might be sparse and we got an entire block,
                 * we should see if the block is all zeros
                 */
                if (sparse && count == bsize) {
                        p = inbuf; e = &inbuf[count/4];
                        while (p < e && *p == 0)
                                p++;
                        if (p == e) {
                                (void) lseek(ofd, (off_t) count, SEEK_CUR);
                                was_hole = TRUE;
                                continue;
                        }
                }
                was_hole = FALSE;

#ifdef  DBG_ERRORS
                if (opt_errors && dbg_chk_error(dst, 'w'))
                        ret = -1;
                else
#endif
                ret = write(ofd, (char *) inbuf, count);

                if (ret != count) {
                        errs = ERR_FILES;
                        copy_err_str = gettext(PROB_write);
                        break;
                }
        }

        if (count < 0) {
                copy_err_str = gettext(PROB_read);
                errs = ERR_FILES;
        } else if (was_hole) {
                /*
                 * if we skipped the last write because of a hole, we
                 * need to make sure that we write a single byte of null
                 * at the end of the file to update the file length.
                 */
                (void) lseek(ofd, (off_t)-1, SEEK_CUR);
                (void) write(ofd, "", 1);
        }

        /*
         * if the output file was botched, free up its space
         */
        if (errs)
                ftruncate(ofd, (off_t) 0);

        close(ifd);
        close(ofd);
        return (errs);
}

/*
 * routine:
 *      checksparse
 *
 * purpose:
 *      to determine whether or not a file might be sparse, and if
 *      it is sparse, what the granularity of the holes is likely
 *      to be.
 *
 * parameters:
 *      file descriptor for file in question
 *
 * returns:
 *      0       file does not appear to be sparse
 *      else    block size for this file
 */
static int
checksparse(int fd)
{
        struct stat statb;

        /*
         * unable to stat the file is very strange (since we got it
         * open) but it probably isn't worth causing a fuss over.
         * Return the conservative answer
         */
        if (fstat(fd, &statb) < 0)
                return (MIN_HOLE);

        /*
         * if the file doesn't have enough blocks to account for
         * all of its bytes, there is a reasonable chance that it
         * is sparse.  This test is not perfect, in that it will
         * fail to find holes in cases where the holes aren't
         * numerous enough to componsent for the indirect blocks
         * ... but losing those few holes is not going to be a
         * big deal.
         */
        if (statb.st_size > 512 * statb.st_blocks)
                return (statb.st_blksize);
        else
                return (0);
}