# # CDDL HEADER START # # The contents of this file are subject to the terms of the # Common Development and Distribution License (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 2009 Sun Microsystems, Inc. All rights reserved. # Use is subject to license terms. # # Copyright (c) 2018, Joyent, Inc. PROG= filesync OBJS= action.o anal.o base.o debug.o files.o ignore.o main.o recon.o rename.o rules.o acls.o eval.o SRCS= $(OBJS:%.o=%.c) HDRS= filesync.h messages.h database.h include ../Makefile.cmd LDLIBS += -lgen CERRWARN += -Wno-parentheses CERRWARN += $(CNOWARN_UNINIT) # not linted SMATCH=off FILEMODE= 0555 .KEEP_STATE: .PARALLEL: $(OBJS) all: $(PROG) $(OBJS): $(HDRS) $(PROG): $(OBJS) $(LINK.c) $(OBJS) -o $@ $(LDLIBS) $(POST_PROCESS) install: all $(ROOTPROG) clean: $(RM) $(OBJS) lint: lint_SRCS # # we need our own rule for building a message catalog because our strings # are already extracted, and xgettext expects to find them in situ. # # The sed command turns each string define in messages.h into # a call to gettext, so that xgettext can extract them. Also # we capture all comments, since the only comments in messages.h # are for the benefit of the translators. # $(POFILE): messages.h $(SED) -e "s/#define/gettext(/" -e "/gettext/s/$$/)/" messages.h | \ $(XGETTEXT) -c "" -d $(TEXT_DOMAIN) - $(MV) $(TEXT_DOMAIN).po $@ include ../Makefile.targ # # 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 # #ident "%W% %E% SMI" # # design notes that are likely to be of general (rather than # merely historical) interest. Table of Contents Overview what filesync does Primary Data Structures general principles why they exist key concepts what they represent data structures major structures and their contents Overview of Passes main phases of program execution Modules list and descriptions of files Studying the Code active ingredients a reading list of high points the whole thing a suggested order for everything Gross calling structure who calls whom Helpful hints good things to know Overview The purpose of this program is to compare pairs of directory trees with a baseline snapshot, to determine which files have changed, and to propagate the changes in order to bring the trees back into congruency. The baseline snapshot describes size, ownership, ... for all files that filesync is managing WHEN THEY WERE LAST IN SYNC. The files and directory trees to be compared are determined by a relatively flexible (user editable) rules file, whose format (packingrules.4) permits files and or trees to be specified, explicitly, implicitly, or with wild cards. There are also provisions for filtering out unwanted files and for running programs to generate lists of files and directories to be included or excluded. The comparisons begin by comparing the structured name spaces. For names that appear in both trees, the files are then compared on the basis of type, size, contents, ownership and protections. For files that are already in the baseline snapshot, if the sizes and modification times have not changed, we do not bother to recheck the contents. The reconciliation process (resolving the differences) will only propagate a change if it is obvious what should be done (one side has changed relative to the snapshot, while the other has not). If there are conflicting changes, the file is flagged and the user is asked to reconcile the differences manually. There are, however a few switches that can be used to constrain the analysis or reconciliation, or to force one particular side to win in case of a conflict. Primary Data Structures general principles: we will build up an in-memory tree that represents the union of the name spaces found in the baseline and on the source and destination sides. keep in mind that the baseline recalls the state of files THE LAST TIME THEY WERE IN AGREEMENT. If files have disagreed for a long time, the baseline still remembers what they were like when they agreed. If files have never agreed, the baseline has no notions of how they "used to be". key concepts: a "base pair" is a pair of directories whose contents (or a subset of whose contents) are to be syncrhonized. The "base pairs" to be managed are specified in the packing rules file. associated with each "base pair" is a set of rules that describe which files (under those directories) are to be kept in sync. Each rule is a list of: files and or directories to be included wild cards for files or directories to be included programs to generate lists of names for inclusion file names to be ignored wild cards for file names to be ignored programs to generate lists of names for ignoring as a result of the "evaluation" process we build up (under each base pair) a tree that represents all of the files that we are supposed to keep in sync, and contains everything we need to know about each one of those files. The structure of the tree mirrors the directory hierarchy ... actually the union of the three hiearchies (baseline, source and destination). for each file, we record interesting information (type, size, owner, protection, mod time) and keep separate note of what these values were: in the baseline last time two sides agreed on the source side, as we just examined it on the destination side, as we just examined it data structures: there is an ordered list of "base" structures for each base, we maintain three lists of associated "rule" descriptions: inclusion rules exclusion rules restriction rules (from the command line) a "file" tree, representing all files below the bases a list of statistics to be printed as a summary for each "rule", we maintain some flags describing the type of rule the character string that is the rule for each "file", we maintain sibling and child pointers to give them tree structure flags to describe what we have done/should do "fileinfo" information from the src, dest, and baseline in addition there are some fields that are used to add the file to a list of files requiring reconciliation and record what happened to it. a "fileinfo" structure contains a subset of the information that we obtain from a stat call: major/minor/inum type link count ownership, protection, and acls size modification time there is also, built up during analysis, a reconciliation list. This is an ordered list of "file" structures which are believed to descibe files that have changed and require reconciliation. The ordering is important both for correctness and to preserve relative modification times. Overview of passes: pass I (evaluate) stat every file that we might be interested in (on both src/dest sides). This includes walking the trees under all directories in order to find out what files exist and stating all of them. the main trick in this pass is that there may be files we don't want to evaluate (because we are limiting our attention to specific files and trees). There is a LISTED flag kept in the database that tells me whether or not I need to stat/descend any given node. all restrictions and ignores take effect during this pass. pass II (analyze) given the baseline and all of the current stat information gained during pass I, figure out what might conceivably have changed and queue it for pass III. This pass doesn't try to figure out what happened or who should win ... it merely identifies candidates for pass III. This pass ignores any nodes that were not evaluated during pass I. the queueing process, however, determines the order in which the files will be processed in pass III, and the order is very important. pass III (reconcile) process the list of candidates, figuring out what has actually changed and which versions deserve to win. If is clear what needs doing, we actually do it in this pass. Modules filesync.h defines for limits, sizes and return codes declarations for global variables (mostly cmd-line parms) defines for default file names declarations for routines of general interest database.h data-structures for recording rules data-structures for recording information about files declarations for routines that operate on/with those structures messages.h the text of all localizable messages debug.h definitions and declarations for routines for error simulation and bit-map display. acls.c routines to get, set, compare, and display Access Control Lists action.c routines to do the real work of copying, deleting, or changing ownership in order to make one side agree with the other. anal.c routines to examine the in-core list of files and determine what has changed (and therefore what is files are candidates for reconciliation). This analysis includes figuring out which files should be links rather than copies. base.c routines to read and write the baseline file routines to search and manipulate the in-core base list debug.c data structures and routines, used to sumulate errors and produce debug output, that map between bits (as found in various flag words) character string names for their meanings. eval.c routines to build up the internal tree that describes the status of all of the files that are described by the current rules. files.c routines to manipulate file name arguments, including wild cards and embedded environment variables. ignore.c routines to maintain a list of names or patterns for files to be ignored, and to check file names against that list. main.c global variables, cmd-line parameter processing, parameter validation, error reporting, and the main loop. recon.c routines to examine a list of files that appear to have changed, and figure out what the appropriate reconciliation course of action is. rename.c routines to search the tree to determine whether or not any creates/deletes are actually renames. rules.c routines to read and write the rules file routines to add rules and enumerate in-core rules filecheck.c not really a part of filesync, but rather a utility program that is used in the test suite. It extracts information about files that is not readily available from other unix commands. Comments on studying the code if you are only interested in the "active ingredients": read the above notes on data structures and then read the structure declarations in database.h read the above notes overviewing the passes in recon.c: read reconcile this routine almost makes sense on its own, and it is unquestionably the most important routine in the entire program. Everything else just gathers data for reconcile to use, or updates the books to reflect the changes. in eval.c: read evaluate, eval_file, walker, and note_info this is the main guts of pass I in anal.c: read analyze, check_file, check_changes & queue_file this is the main guts of pass II if you want to read the whole thing: the following routines do fundamentally simple things in simple ways, and can (for the most part) be understood in vaccuuo. The things they do are probably sufficiently obvious that you can probably understand the more interesting code without having read them at all. base.c rules.c files.c debug.c ignore.c acls.c the following routines constitute the real meat of the program, and while they are broken into specialized modules, they probably need to be understood as an organic whole: main.c setup and control eval.c pass I anal.c pass II recon.c pass III action.c execution and book-keeping rename.c a special case for a common situation Gross calling structure / flow of control main.c:main findfiles read_baseline read_rules if new rules add_base add_include evaluate analyze write_baseline write_summary eval.c:evaluate add_file_to_base add_glob add_run ignore_pgm ignore_file ignore_expr eval_file eval.c:eval_file note_info nftw walker note_info anal.c:analyze check_file reconcile anal.c:check_file check_changes queue_file recon.c:reconcile samedata samestuff do_copy copy do_like update_info do_like do_remove Helpful Hints the "file" structure contains a bunch of flags. Many of them just summarize what we know about the file (e.g. where it was found). Others are more subtle and control the evaluation process or the writing out of the baseline file. You can't really understand the processing unless you understand what these flags mean. F_NEW added by a new rule F_LISTED this name was generated by a rule F_SPARSE this directory is an intermediate on the way to a name generated by a rule and should not be recursively walked. F_EVALUATE this node was found in evaluation and has up-to-date stat information F_CONFLICT there is a conflict on this node so baseline should remain unchanged F_REMOVE this node should be purged from the baseline F_STAT_ERROR it was impossible to stat this file (and anything below it) the implications of these flags on processing are F_NEW, F_LISTED, F_SPARSE affect whether or not a particular node should be included in the evaluation pass. in some situations, only new rules are interpreted. listed files and directories should be evaluated and analyzed. sparse directories should not be recursively enumerated. F_EVALUATE determines whether or not a node is included in the analysis pass. Only nodes that have been evaluated will be analyzed. F_CONFLICT, F_REMOVE, F_EVALUATE affect how a node should be written back into the baseline file. if there is a conflict or we haven't evaluated a node, we won't update the baseline. if a node is marked for removal, it will be excluded from the baseline when it is written out. F_STAT_ERROR if we could not get proper status information about a file (or the tree under it) we cannot, with any confidence, determine what its state is or do anything about it. Such files are flagged as "in conflict". it is somewhat kinky that we put error flagged files on the reconciliation list. We do this because this is the easiest way to pull them out for reporting as conflicts. /* * 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 * * module: * acls.c * * purpose: * routines to manipulate access control lists, mapping between * the data structures required by the filesystem ACL system calls * and the representation used in our fileinfo structure. * */ #ident "%W% %E% SMI" #include #include #include "filesync.h" #include "database.h" #ifdef NO_ACLS /* * Solaris 2.4 libc.so does not contain this entry point, so if we * want to build a 2.4 version of filesync, we need to provide a * dummy entry point that will fail when-ever it is called. */ #define acl bogus_acl static int acl(const char *name, int opcode, int count, aclent_t *acls) { return (-1); } #endif /* * routine: * get_acls * * purpose: * to read the ACL (if any) from a file into a fileinfo structure * * parameters: * name of file * pointer to fileinfo structure * * returns: * number of ACL entries */ int get_acls(const char *name, struct fileinfo *ip) { int count; int i; static aclent_t acls[MAX_ACL_ENTRIES]; aclent_t *list; count = acl(name, GETACL, MAX_ACL_ENTRIES, acls); if (count <= 0) return (0); /* with a count of 3 or 4 there may not be any real ones */ if (count > 4) goto gotsome; /* look for anything beyond the normal unix protection */ for (i = 0; i < count; i++) switch (acls[i].a_type) { default: /* weird types are real */ goto gotsome; case USER_OBJ: case GROUP_OBJ: case OTHER_OBJ: case CLASS_OBJ: continue; /* all file have these */ } return (0); /* nothing interesting */ gotsome: /* allocate an array to hold the acls */ list = (aclent_t *) malloc(count * sizeof (*list)); if (list == 0) nomem("Access Control List"); /* copy the acls into the new list */ for (i = 0; i < count; i++) { list[i].a_type = acls[i].a_type; list[i].a_id = acls[i].a_id; list[i].a_perm = acls[i].a_perm; } ip->f_acls = list; ip->f_numacls = count; return (ip->f_numacls); } /* * routine: * cmp_acls * * purpose: * determine whether or not two ACLs are the same * * parameters: * pointer to first fileinfo * pointer to second fileinfo * * returns: * true equal * false different */ int cmp_acls(struct fileinfo *f1, struct fileinfo *f2) { int i; if (f1->f_numacls != f2->f_numacls) return (0); if (f1->f_numacls == 0) return (1); for (i = 0; i < f1->f_numacls; i++) { if (f1->f_acls[i].a_type != f2->f_acls[i].a_type) return (0); if (f1->f_acls[i].a_id != f2->f_acls[i].a_id) return (0); if (f1->f_acls[i].a_perm != f2->f_acls[i].a_perm) return (0); } return (1); } /* * routine: * set_acls * * purpose: * to write the ACL of a file * * parameters: * name of file * fileinfo pointer (which contains an acl pointer) * * returns: * retcode and errno */ int set_acls(const char *name, struct fileinfo *fp) { int rc; int nacl; aclent_t acls[4], *list; if (fp->f_numacls == 0) { /* fabricate a standard set of bogus ACLs */ acls[0].a_type = USER_OBJ; acls[0].a_id = fp->f_uid; acls[0].a_perm = (fp->f_mode >> 6) & 7; acls[1].a_type = GROUP_OBJ; acls[1].a_id = fp->f_gid; acls[1].a_perm = (fp->f_mode >> 3) & 7; acls[2].a_type = CLASS_OBJ; acls[2].a_id = 0; acls[2].a_perm = (fp->f_mode >> 6) & 7; acls[3].a_type = OTHER_OBJ; acls[3].a_id = 0; acls[3].a_perm = fp->f_mode & 7; nacl = 4; list = acls; } else { nacl = fp->f_numacls; list = fp->f_acls; } rc = acl(name, SETACL, nacl, list); /* non-negative number mean success */ if (rc < 0) return (rc); else return (0); } /* * routine: * show_acls * * purpose: * to map an acl into arguments for a setfacl command * * paramters: * number of elements in list * pointer to list * * returns: * pointer to character buffer containing arguments */ char *show_acls(int numacl, aclent_t *list) { int i, j; int type, perm, id; char *s; static char buf[ MAX_LINE ]; s = buf; if (numacl > 0) { *s++ = '-'; *s++ = 's'; *s++ = ' '; } else { *s++ = '-'; *s++ = 'd'; } for (i = 0; i < numacl; i++) { type = list[i].a_type; id = list[i].a_id; perm = list[i].a_perm; if (i > 0) *s++ = ','; /* note whether this is per-file or default */ if (type & ACL_DEFAULT) { *s++ = 'd'; *s++ = ':'; } /* print out the entry type */ if (type & (USER_OBJ|USER)) { *s++ = 'u'; *s++ = ':'; } else if (type & (GROUP_OBJ|GROUP)) { *s++ = 'g'; *s++ = ':'; } else if (type & OTHER_OBJ) { *s++ = 'o'; *s++ = ':'; } else if (type & CLASS_OBJ) { *s++ = 'm'; *s++ = ':'; } /* print out the ID for this ACL */ if (type & (USER_OBJ|GROUP_OBJ)) *s++ = ':'; else if (type & (USER|GROUP)) { for (j = 1; id/j > 10; j *= 10); while (j > 0) { *s++ = '0' + (id/j); id %= j*10; j /= 10; } *s++ = ':'; } /* print out the permissions for this ACL */ *s++ = (perm & 04) ? 'r' : '-'; *s++ = (perm & 02) ? 'w' : '-'; *s++ = (perm & 01) ? 'x' : '-'; } *s = 0; return (buf); } /* * 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 #include #include #include #include #include #include #include #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); } /* * 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 * * module: * anal.c * * purpose: * routines to analyze the file trees and figure out what has changed * and queue files for reconciliation. It also contains tree enumeration * routines to for other purposes (pruning and link location). * * contents: * * change analysis: * analyze .... (top level) analyze all files in the tree for changes * summary .... print out change/reconciliation statistics for each base * check_file . (static) look for changes and queue file for reconciliation * check_changes (static) figure out if a particular file has changed * queue_file . (static) add a file to the reconciliation list * * other tree enumeration functions: * prune_file . (static) recursive descent and actual pruning * prune ...... (top level) initiate pruning analysis for nonexistant files * find_link .. look for other files to which a file may be a link * link_update. propagate changed stat info to all other links * same_name .. (static) figure out if two nodes describe same file * * misc: * push_name .. maintain a running full pathname as we descend * pop_name ... maintain a running full pathname as we pop back * get_name ... return full pathname for the current file * * notes: * analysis is limited to files that were evaluated in the previous * pass ... since we don't have complete information about files that * were not evaluated in the previous pass. */ #include #include #include #include "messages.h" #include "filesync.h" #include "database.h" #include "debug.h" /* * routines: */ void push_name(const char *); void pop_name(); char *get_name(struct file *); static errmask_t check_file(struct file *fp); static diffmask_t check_changes(struct file *fp, int first, int second); static int prune_file(struct file *fp); static void queue_file(struct file *fp); /* * globals */ static struct file *changes; /* list of files to be reconciled */ static long total_files; /* total number of files being considered */ static long est_deletes; /* estimated number of files to be deleted */ static long est_rmdirs; /* est rmdirs of non-empty directories */ int inum_changes; /* LISTed directories whose I#s changed */ /* * routine: * analyze * * purpose: * top level routine for the analysis/reconciliation process * * parameters: * none * * returns: * error mask * * notes: * a critical side effect of this routine is the creation of * the reconciliation list, an ordered list of files that * needed to be processed in the subsequent reconciliation pass */ errmask_t analyze() { struct base *bp; struct file *fp; int errs = 0; int err; int percentage; bool_t aborted = FALSE; char msgbuf[MAX_LINE]; /* * run through all bases and directories looking for files * that have been renamed. This must be done before the * difference analysis because a directory rename can introduce * radical restructuring into a name-based tree. */ for (bp = bases; bp; bp = bp->b_next) { for (fp = bp->b_files; fp; fp = fp->f_next) if (fp->f_flags & F_EVALUATE) errs |= find_renames(fp); } /* * run through all bases and files looking for candidates * note, however that we only descend into trees that have * the evaluate flag turned on. As a result of new rules or * restriction arguments, we may be deliberatly ignoring * large amounts of the baseline. This means we won't do * any stats to update the information in those nodes, and * they will be written back just as they were. * * note that there is code to prune out baseline nodes for * files that no longer exist, but that code is in reconcile * and will never get a chance to run on nodes that aren't * analyzed. * * we also want to run though all nodes with STAT errors * so that we can put them on the reconciliation list. */ for (bp = bases; bp; bp = bp->b_next) { for (fp = bp->b_files; fp; fp = fp->f_next) if (fp->f_flags & (F_EVALUATE|F_STAT_ERROR)) errs |= check_file(fp); } /* * my greatest fear is that someday, somehow, by messing with * variables or baselines or who-knows-what, that someone will * run a reconciliation against a large tree that doesn't correspond * to the baseline, and I will infer that a bazillion files have * been deleted and will propagate the slaughter before anyone * can say somebody stop that maniac. * * in order to prevent such a possibility, we have a few different * sanity checks. There is, of course, a tradeoff here between * danger and irritation. The current set of heuristics for whether * or not to generate a warning are (any of) * * at least CONFIRM_MIN files have been deleted AND * CONFIRM_PCT of all files have been deleted * * the inode number on a LISTed directory has changed * * a non-empty directory has been deleted. */ msgbuf[0] = 0; percentage = (est_deletes * 100) / (total_files ? total_files : 1); if (est_deletes >= CONFIRM_MIN && percentage >= CONFIRM_PCT) sprintf(msgbuf, gettext(WARN_deletes), est_deletes); else if (inum_changes > 0) sprintf(msgbuf, gettext(WARN_ichange), inum_changes); else if (est_rmdirs) sprintf(msgbuf, gettext(WARN_rmdirs), est_rmdirs); if (msgbuf[0]) confirm(msgbuf); /* * TRICK: * the change list contains both files that have changed * (and probably warrant reconciliation) and files that * we couldn't get up-to-date stat information on. The * latter files should just be flagged as being in conflict * so they can be reported in the summary. The same is * true of all subsequent files if we abort reconciliation. */ for (fp = changes; fp; fp = fp->f_rnext) if (aborted || (fp->f_flags & F_STAT_ERROR)) { fp->f_flags |= F_CONFLICT; /* if it isn't in the baseline yet, don't add it */ if ((fp->f_flags & F_IN_BASELINE) == 0) fp->f_flags |= F_REMOVE; fp->f_problem = aborted ? PROB_aborted : PROB_restat; (fp->f_base)->b_unresolved++; errs |= ERR_UNRESOLVED; if (opt_verbose) fprintf(stdout, gettext(aborted ? V_suppressed : V_nostat), fp->f_fullname); } else { err = reconcile(fp); errs |= err; if (opt_halt && (err & ERR_ABORT)) { fprintf(stderr, gettext(ERR_abort_h)); aborted = TRUE; } } return (errs); } /* * routine: * prune_file * * purpose: * to look for file entries that should be pruned from baseline * prune the current file if it needs pruning, and recursively * descend if it is a directory. * * parameters: * pointer to file node */ static int prune_file(struct file *fp) { struct file *cp; int prunes = 0; /* if node hasn't been evaluated, mark it for removal */ if ((fp->f_flags & (F_EVALUATE|F_STAT_ERROR)) == 0) { fp->f_flags |= F_REMOVE; prunes++; if (opt_debug & DBG_ANAL) fprintf(stderr, "ANAL: PRUNE %s\n", fp->f_name); } /* now check our children */ for (cp = fp->f_files; cp; cp = cp->f_next) prunes += prune_file(cp); return (prunes); } /* * routine: * prune * * purpose: * to prune the baseline of entries that no longer correspond to * existing rules. * * notes: * This routine just calls prune_file on the top of each base tree. */ int prune() { struct base *bp; struct file *fp; int prunes = 0; for (bp = bases; bp; bp = bp->b_next) { for (fp = bp->b_files; fp; fp = fp->f_next) prunes += prune_file(fp); if ((bp->b_flags & F_EVALUATE) == 0) bp->b_flags |= F_REMOVE; } return (prunes); } /* * routine: * summary * * purpose: * to print out statics and conflict lists */ void summary() { struct base *bp; struct file *fp; extern bool_t need_super; (void) fflush(stdout); for (bp = bases; bp; bp = bp->b_next) { /* see if this base was irrelevant */ if ((bp->b_flags & F_EVALUATE) == 0) continue; /* print out a summary for this base */ fprintf(stderr, gettext(SUM_hd), bp->b_src_spec, bp->b_dst_spec, bp->b_totfiles); fprintf(stderr, gettext(SUM_dst), bp->b_dst_copies, bp->b_dst_deletes, bp->b_dst_misc); fprintf(stderr, gettext(SUM_src), bp->b_src_copies, bp->b_src_deletes, bp->b_src_misc); if (bp->b_unresolved) fprintf(stderr, gettext(SUM_unresolved), bp->b_unresolved); /* print out a list of unreconciled files for this base */ for (fp = changes; fp; fp = fp->f_rnext) { if (fp->f_base != bp) continue; if ((fp->f_flags & F_CONFLICT) == 0) continue; fprintf(stderr, "\t\t%s (%s)\n", fp->f_fullname, fp->f_problem ? fp->f_problem : "???"); } fprintf(stderr, "\n"); } if (need_super) fprintf(stderr, gettext(WARN_super)); } /* * routine: * check_file * * purpose: * figure out if a file requires reconciliation and recursively * descend into all sub-files and directories * * parameters: * base pointer * file pointer * * returns: * error mask * built up changes needed list * updated statistics * * notes: * this routine builds up a path name as it descends through * the tree (see push_name, pop_name, get_name). */ static errmask_t check_file(struct file *fp) { struct file *cp; int errs = 0; if ((fp->f_flags & F_STAT_ERROR) == 0) { /* see if the source has changed */ fp->f_info[OPT_BASE].f_modtime = fp->f_s_modtime; fp->f_info[OPT_BASE].f_ino = fp->f_s_inum; fp->f_info[OPT_BASE].f_d_maj = fp->f_s_maj; fp->f_info[OPT_BASE].f_d_min = fp->f_s_min; fp->f_info[OPT_BASE].f_nlink = fp->f_s_nlink; fp->f_srcdiffs |= check_changes(fp, OPT_BASE, OPT_SRC); /* see if the destination has changed */ fp->f_info[OPT_BASE].f_modtime = fp->f_d_modtime; fp->f_info[OPT_BASE].f_ino = fp->f_d_inum; fp->f_info[OPT_BASE].f_d_maj = fp->f_d_maj; fp->f_info[OPT_BASE].f_d_min = fp->f_d_min; fp->f_info[OPT_BASE].f_nlink = fp->f_d_nlink; fp->f_dstdiffs |= check_changes(fp, OPT_BASE, OPT_DST); /* if nobody thinks the file exists, baseline needs pruning */ if ((fp->f_flags & (F_IN_SOURCE|F_IN_DEST)) == 0) { fp->f_srcdiffs |= D_DELETE; fp->f_dstdiffs |= D_DELETE; } /* keep track of possible deletions to look for trouble */ if ((fp->f_dstdiffs | fp->f_srcdiffs) & D_DELETE) { est_deletes++; /* see if file is (or has been) a non-empty directory */ if (fp->f_files) est_rmdirs++; } } /* if we found differences, queue the file for reconciliation */ if (fp->f_srcdiffs || fp->f_dstdiffs || fp->f_flags & F_STAT_ERROR) { queue_file(fp); if (opt_debug & DBG_ANAL) { fprintf(stderr, "ANAL: src=%s", showflags(diffmap, fp->f_srcdiffs)); fprintf(stderr, " dst=%s", showflags(diffmap, fp->f_dstdiffs)); fprintf(stderr, " flgs=%s", showflags(fileflags, fp->f_flags)); fprintf(stderr, " name=%s\n", fp->f_fullname); } } /* bump the total file count */ fp->f_base->b_totfiles++; total_files++; /* if this is not a directory, we're done */ if (fp->f_files == 0) return (errs); /* * If this is a directory, we need to recursively analyze * our children, but only children who have been evaluated. * If a node has not been evaluated, then we don't have * updated stat information and there is nothing to analyze. * * we also want to run though all nodes with STAT errors * so that we can put them on the reconciliation list. * If a directory is unreadable on one side, all files * under that directory (ON BOTH SIDES) must be marked as * blocked by stat errors. */ push_name(fp->f_name); for (cp = fp->f_files; cp; cp = cp->f_next) { if (fp->f_flags & F_STAT_ERROR) cp->f_flags |= F_STAT_ERROR; if (cp->f_flags & (F_EVALUATE|F_STAT_ERROR)) errs |= check_file(cp); } pop_name(); return (errs); } /* * routine: * check_changes * * purpose: * to figure out what has changed for a specific file * * parameters: * file pointer * the reference info * the info to be checked for changes * * returns: * diff mask * * notes: * this routine doesn't pretend to understand what happened. * it merely enumerates the ways in which the files differ. */ static diffmask_t check_changes(struct file *fp, int ref, int new) { struct fileinfo *rp, *np; int mask = 0; int type; rp = &fp->f_info[ref]; np = &fp->f_info[new]; if (np->f_uid != rp->f_uid) mask |= D_UID; if (np->f_gid != rp->f_gid) mask |= D_GID; if (np->f_mode != rp->f_mode) mask |= D_PROT; type = np->f_type; if (type != rp->f_type) { if (type == 0) mask |= D_DELETE; else if (rp->f_type == 0) mask |= D_CREATE; else mask |= D_TYPE; } else if (type == S_IFBLK || type == S_IFCHR) { /* * for special files, we only look at the maj/min */ if (np->f_rd_maj != rp->f_rd_maj) mask |= D_SIZE; if (np->f_rd_min != rp->f_rd_min) mask |= D_SIZE; } else if (type != S_IFDIR) { /* * for directories, we don't look directly at * the contents, so these fields don't mean * anything. If the directories have changed * in any interesting way, we'll find it by * walking the tree. */ if (np->f_modtime > rp->f_modtime) mask |= D_MTIME; if (np->f_size != rp->f_size) mask |= D_SIZE; if (np->f_nlink != rp->f_nlink) mask |= D_LINKS; } if (cmp_acls(rp, np) == 0) mask |= D_FACLS; return (mask); } /* * routine: * same_name * * purpose: * to figure out whether or not two databsae nodes actually refer to * the same file. * * parameters: * pointers to two file description nodes * which side we should check * * returns: * TRUE/FALSE * * notes: * if a single directory is specified in multiple base pairs, it * is possible to have multiple nodes in the database describing * the same file. This routine is supposed to detect those cases. * * what should be a trivial string comparison is complicated by * the possibility that the two nodes might describe the same file * from base directories at different depths. Thus, rather than * comparing two strings, we really want to compare the concatenation * of two pairs of strings. Unfortunately calling full_name would * be awkward right now, so instead we have our own comparison * routine that automatically skips from the first string to * the second. */ static bool_t same_name(struct file *f1, struct file *f2, side_t srcdst) { char *s1, *s2, *x1, *x2; if (srcdst == OPT_SRC) { s1 = (f1->f_base)->b_src_name; s2 = (f2->f_base)->b_src_name; } else { s1 = (f1->f_base)->b_dst_name; s2 = (f2->f_base)->b_dst_name; } x1 = f1->f_fullname; x2 = f2->f_fullname; /* * Compare the two names, and if they differ before they end * this is a non-match. If they both end at the same time, * this is a match. * * The trick here is that each string is actually the logical * concatenation of two strings, and we need to automatically * wrap from the first to the second string in each pair. There * is no requirement that the two (concatenated) strings be * broken at the same point, so we have a slightly baroque * comparsion loop. */ while (*s1 && *s1 == *s2) { /* * strings have been identical so far, so advance the * pointers and continue the comparison. The trick * is that when either string ends, we have to wrap * over to its extension. */ s1++; s2++; if (*s1 && *s2) continue; /* * at least one of the strings has ended. * there is an implicit slash between the string * and its extension, and this has to be matched * against the other string. */ if (*s1 != *s2) { if (*s1 == 0 && *s2 == '/') s2++; else if (*s2 == 0 && *s1 == '/') s1++; else /* the disagreement doesn't come at a slash */ break; } /* * if either string has ended, wrap to its extension */ if (*s1 == 0 && x1 != 0) { s1 = x1; x1 = 0; } if (*s2 == 0 && x2 != 0) { s2 = x2; x2 = 0; } } return (*s1 == *s2); } /* * routine: * find_link * * purpose: * to figure out if there is a file to which we should * be creating a link (rather than making a copy) * * parameters: * file node for the file to be created (that we hope is merely a link) * which side is to be changed (src/dst) * * return: * 0 no link is appropriate * else pointer to file node for link referent * * notes: * there are a few strange heuristics in this routine and I * wouldn't bet my soul that I got all of them right. The general * theory is that when a new file is created, we look to see if it * is a link to another file on the changed side, and if it is, we * find the corresponding file on the unchanged side. * * cases we want to be able to handle: * 1. one or more links are created to a prexisting file * 2. a preexisting only link is renamed * 3. a rename of one of multiple links to a preexisting file * 4. a single file is created with multiple links */ struct file * find_link(struct file *fp, side_t srcdst) { struct file *lp; side_t chgside, tgtside; struct fileinfo *chgp, *tgtp, *basp, *fcp, *ftp; /* chg = side on which the change was noticed */ /* tgt = side to which the change is to be propagated */ chgside = (srcdst == OPT_SRC) ? OPT_DST : OPT_SRC; tgtside = (srcdst == OPT_SRC) ? OPT_SRC : OPT_DST; fcp = &fp->f_info[chgside]; ftp = &fp->f_info[tgtside]; /* * cases 1 and 3 * * When a new link is created, we should be able to find * another file in the changed hierarchy that has the same * I-node number. We expect it to be on the changed list * because the link count will have gone up or because all * of the copies are new. If we find one, then the new file * on the receiving file should be a link to the corresponding * existing file. * * case 4 * * the first link will be dealt with as a copy, but all * subsequent links should find an existing file analogous * to one of the links on the changed side, and create * corresponding links on the other side. * * in each of these cases, there should be multiple links * on the changed side. If the linkcount on the changed * side is one, we needn't bother searching for other links. */ if (fcp->f_nlink > 1) for (lp = changes; lp; lp = lp->f_rnext) { /* finding the same node doesn't count */ if (fp == lp) continue; tgtp = &lp->f_info[tgtside]; chgp = &lp->f_info[chgside]; /* * if the file doesn't already exist on the target side * we cannot make a link to it */ if (tgtp->f_mode == 0) continue; /* * if this is indeed a link, then the prospective file on * the changed side will have the same dev/inum as the file * we are looking for */ if (fcp->f_d_maj != chgp->f_d_maj) continue; if (fcp->f_d_min != chgp->f_d_min) continue; if (fcp->f_ino != chgp->f_ino) continue; /* * if the target side is already a link to this file, * then there is no new link to be created * FIX: how does this interact with copies over links */ if ((ftp->f_d_maj == tgtp->f_d_maj) && (ftp->f_d_min == tgtp->f_d_min) && (ftp->f_ino == tgtp->f_ino)) continue; /* * there is a pathological situation where a single file * might appear under multiple base directories. This is * damned awkward to detect in any other way, so we must * check to see if we have just found another database * instance for the same file (on the changed side). */ if ((fp->f_base != lp->f_base) && same_name(fp, lp, chgside)) continue; if (opt_debug & DBG_ANAL) fprintf(stderr, "ANAL: FIND LINK %s and %s\n", fp->f_fullname, lp->f_fullname); return (lp); } /* * case 2: a simple rename of the only link * * In this case, there may not be any other existing file on * the changed side that has the same I-node number. There * might, however, be a record of such a file in the baseline. * If we can find an identical file with a different name that * has recently disappeared, we have a likely rename. */ for (lp = changes; lp; lp = lp->f_rnext) { /* finding the same node doesn't count */ if (fp == lp) continue; tgtp = &lp->f_info[tgtside]; chgp = &lp->f_info[chgside]; /* * if the file still exists on the changed side this is * not a simple rename, and in fact the previous pass * would have found it. */ if (chgp->f_mode != 0) continue; /* * the inode number for the new link on the changed * side must match the inode number for the old link * from the baseline. */ if (fcp->f_d_maj != ((srcdst == OPT_SRC) ? lp->f_d_maj : lp->f_s_maj)) continue; if (fcp->f_d_min != ((srcdst == OPT_SRC) ? lp->f_d_min : lp->f_s_min)) continue; if (fcp->f_ino != ((srcdst == OPT_SRC) ? lp->f_d_inum : lp->f_s_inum)) continue; /* finding a file we are already linked to doesn't help */ if ((ftp->f_d_maj == tgtp->f_d_maj) && (ftp->f_d_min == tgtp->f_d_min) && (ftp->f_ino == tgtp->f_ino)) continue; /* * there is a danger that we will confuse an * inode reallocation with a rename. We should * only consider this to be a rename if the * new file is identical to the old one */ basp = &lp->f_info[OPT_BASE]; if (fcp->f_type != basp->f_type) continue; if (fcp->f_size != basp->f_size) continue; if (fcp->f_mode != basp->f_mode) continue; if (fcp->f_uid != basp->f_uid) continue; if (fcp->f_gid != basp->f_gid) continue; if (opt_debug & DBG_ANAL) fprintf(stderr, "ANAL: FIND RENAME %s and %s\n", fp->f_fullname, lp->f_fullname); return (lp); } return (0); } /* * routine: * has_other_links * * purpose: * to determine whether or not there is more that one link to a * particular file. We are willing to delete a link to a file * that has changed if we will still have other links to it. * The trick here is that we only care about links under our * dominion. * * parameters: * file pointer to node we are interested in * which side we are looking to additional links on * * returns: * TRUE if there are multiple links * FALSE if this is the only one we know of */ bool_t has_other_links(struct file *fp, side_t srcdst) { struct file *lp; struct fileinfo *fip, *lip; fip = &fp->f_info[srcdst]; /* if the link count is one, there couldn't be others */ if (fip->f_nlink < 2) return (FALSE); /* look for any other files for the same inode */ for (lp = changes; lp; lp = lp->f_rnext) { /* finding the same node doesn't count */ if (fp == lp) continue; lip = &lp->f_info[srcdst]; /* * file must still exist on this side */ if (lip->f_mode == 0) continue; /* * if this is indeed a link, then the prospective file on * the changed side will have the same dev/inum as the file * we are looking for */ if (lip->f_d_maj != fip->f_d_maj) continue; if (lip->f_d_min != fip->f_d_min) continue; if (lip->f_ino != fip->f_ino) continue; /* * we have found at least one other link */ return (TRUE); } return (FALSE); } /* * routine: * link_update * * purpose: * to propoagate a stat change to all other file nodes that * correspond to the same I-node on the changed side * * parameters: * file pointer for the updated file * which side was changed * * returns: * void * * notes: * if we have copied onto a file, we have copied onto all * of its links, but since we do all stats before we do any * copies, the stat information recently collected for links * is no longer up-to-date, and this would result in incorrect * reconciliation (redundant copies). * * There is an assumption here that all links to a changed * file will be in the change list. This is true for almost * all cases not involving restriction. If we do fail to * update the baseline for a file that was off the change list, * the worst that is likely to happen is that we will think * it changed later (but will almost surely find that both * copies agree). */ void link_update(struct file *fp, side_t which) { struct file *lp; for (lp = changes; lp; lp = lp->f_rnext) { /* finding the current entry doesn't count */ if (lp == fp) continue; /* look for same i#, maj, min on changed side */ if (lp->f_info[which].f_ino != fp->f_info[which].f_ino) continue; if (lp->f_info[which].f_d_maj != fp->f_info[which].f_d_maj) continue; if (lp->f_info[which].f_d_min != fp->f_info[which].f_d_min) continue; /* * this appears to be another link to the same file * so the updated stat information for one must be * correct for the other. */ lp->f_info[which].f_type = fp->f_info[which].f_type; lp->f_info[which].f_size = fp->f_info[which].f_size; lp->f_info[which].f_mode = fp->f_info[which].f_mode; lp->f_info[which].f_uid = fp->f_info[which].f_uid; lp->f_info[which].f_gid = fp->f_info[which].f_gid; lp->f_info[which].f_modtime = fp->f_info[which].f_modtime; lp->f_info[which].f_modns = fp->f_info[which].f_modns; lp->f_info[which].f_nlink = fp->f_info[which].f_nlink; lp->f_info[which].f_rd_maj = fp->f_info[which].f_rd_maj; lp->f_info[which].f_rd_min = fp->f_info[which].f_rd_min; if (opt_debug & DBG_STAT) fprintf(stderr, "STAT: UPDATE LINK, file=%s, mod=%08lx.%08lx\n", lp->f_name, lp->f_info[which].f_modtime, lp->f_info[which].f_modns); } } /* * routine: * queue_file * * purpose: * append a file to the list of needed reconciliations * * parameters: * pointer to file * * notes: * when a request is appended to the reconciliation list, * we fill in the full name. We delayed this in hopes that * it wouldn't be necessary (saving cycles and memory) * * There is some funny business with modification times. * In general, we queue files in order of the latest modification * time so that propagations preserve relative ordering. There * are, however, a few important exceptions: * 1. all directory creations happen at time zero, * so that they are created before any files can * be added to them. * 2. all directory deletions happen at time infinity-depth, * so that everything else can be removed before the * directories themselves are removed. * 3. all file deletions happen at time infinity-depth * so that (in renames) the links will preceed the unlinks. */ static void queue_file(struct file *fp) { struct file **pp, *np; #define TIME_ZERO 0L /* the earliest possible time */ #define TIME_LONG 0x7FFFFFFF /* the latest possible time */ /* * figure out the modification time for sequencing purposes */ if ((fp->f_srcdiffs|fp->f_dstdiffs) & D_DELETE) { /* * deletions are performed last, and depth first */ fp->f_modtime = TIME_LONG - fp->f_depth; } else if (fp->f_info[OPT_SRC].f_type != S_IFDIR && fp->f_info[OPT_DST].f_type != S_IFDIR) { /* * for most files we use the latest mod time */ fp->f_modtime = fp->f_info[OPT_SRC].f_modtime; fp->f_modns = fp->f_info[OPT_SRC].f_modns; if (fp->f_modtime < fp->f_info[OPT_DST].f_modtime) { fp->f_modtime = fp->f_info[OPT_DST].f_modtime; fp->f_modns = fp->f_info[OPT_DST].f_modns; } } else { /* * new directory creations need to happen before anything * else and are automatically sequenced in traversal order */ fp->f_modtime = TIME_ZERO; } /* * insertion is time ordered, and for equal times, * insertions is in (pre-order) traversal order */ for (pp = &changes; (np = *pp) != 0; pp = &np->f_rnext) { if (fp->f_modtime > np->f_modtime) continue; if (fp->f_modtime < np->f_modtime) break; if (fp->f_modns < np->f_modns) break; } fp->f_fullname = strdup(get_name(fp)); fp->f_rnext = np; *pp = fp; } /* * routines: * push_name/pop_name/get_name * * purpose: * maintain a name stack so we can form name of a particular file * as the concatenation of all of the names between it and the * (know to be fully qualified) base directory. * * notes: * we go to this trouble because most files never change and * so we don't need to associate full names with every one. * This stack is maintained during analysis, and if we decide * to add a file to the reconciliation list, we can use the * stack to generate a fully qualified name at that time. * * we compress out '/./' when we return a name. Given that the * stack was built by a tree walk, the only place a /./ should * appear is at the first level after the base ... but there * are legitimate ways for them to appear there. * * these names can get deep, so we dynamically size our name buffer */ static const char *namestack[ MAX_DEPTH + 1 ]; static int namedepth = 0; static int namelen = 0; void push_name(const char *name) { namestack[ namedepth++ ] = name; namelen += 2 + strlen(name); /* make sure we don't overflow our name stack */ if (namedepth >= MAX_DEPTH) { fprintf(stderr, gettext(ERR_deep), name); exit(ERR_OTHER); } } void pop_name(void) { namelen -= 2 + strlen(namestack[--namedepth]); namestack[ namedepth ] = 0; #ifdef DBG_ERRORS /* just a little sanity check here */ if (namedepth <= 0) { if (namedepth < 0) { fprintf(stderr, "ASSERTION FAILURE: namedepth < 0\n"); exit(ERR_OTHER); } else if (namelen != 0) { fprintf(stderr, "ASSERTION FAILURE: namelen != 0\n"); exit(ERR_OTHER); } } #endif } char *get_name(struct file *fp) { int i; static char *namebuf = 0; static int buflen = 0; /* make sure we have an adequate buffer */ i = namelen + 1 + strlen(fp->f_name); if (buflen < i) { for (buflen = MAX_PATH; buflen < i; buflen += MAX_NAME); namebuf = (char *) realloc(namebuf, buflen); } /* assemble the name */ namebuf[0] = 0; for (i = 0; i < namedepth; i++) { if (strcmp(namestack[i], ".")) { strcat(namebuf, namestack[i]); strcat(namebuf, "/"); } } strcat(namebuf, fp->f_name); return (namebuf); } /* * 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 * * module: * base.c * * purpose: * routines to create, traverse, read and write the baseline database * * contents: * manipulation: * add_base, add_file_to_base, add_file_to_dir * (static) add_file_to_list * reading baseline: * read_baseline * (static) gettype * writing baseline: * write_baseline * (static) bw_header, bw_base, bw_file, showtype */ #include #include #include #include #include "filesync.h" #include "database.h" #include "messages.h" #define BASE_MAJOR 1 /* base file format major rev */ #define BASE_MINOR 2 /* base file format minor rev */ #define BASE_TAG "filesync-BaseLine" /* * globals */ struct base omnibase; /* dummy to hold global rules */ struct base *bases; /* pointer to the base list */ /* * locals */ static int num_bases; /* used to generate sequence #s */ static errmask_t bw_header(FILE *); /* write out baseline header */ static errmask_t bw_base(FILE *, struct base *); /* write out one base */ static errmask_t bw_file(FILE *, struct file *, int); static struct file *add_file_to_list(struct file **, const char *); static char showtype(int); static long gettype(int); /* * routine: * add_base * * purpose: * to find a base pair in the chain, adding it if necessary * * parameters: * spec for source directory * spec for dest directory * * returns: * pointer to the base pair * */ struct base * add_base(const char *src, const char *dst) { struct base *bp, **bpp; /* first see if we already have it */ for (bpp = &bases; (bp = *bpp) != 0; bpp = &bp->b_next) { /* base must match on both src and dst */ if (strcmp(src, bp->b_src_spec)) continue; if (strcmp(dst, bp->b_dst_spec)) continue; if (opt_debug & DBG_BASE) fprintf(stderr, "BASE: FOUND base=%d, src=%s, dst=%s\n", bp->b_ident, src, dst); return (bp); } /* no joy, so we have to allocate one */ bp = malloc(sizeof (struct base)); if (bp == 0) nomem("base structure"); /* initialize the new base */ memset((void *) bp, 0, sizeof (struct base)); bp->b_ident = ++num_bases; bp->b_src_spec = strdup(src); bp->b_dst_spec = strdup(dst); /* names are expanded at run-time, and this is run-time */ if ((bp->b_src_name = expand(bp->b_src_spec)) == 0) { fprintf(stderr, gettext(ERR_badbase), bp->b_src_spec); exit(ERR_FILES); } if ((bp->b_dst_name = expand(bp->b_dst_spec)) == 0) { fprintf(stderr, gettext(ERR_badbase), bp->b_dst_spec); exit(ERR_FILES); } /* chain it in */ *bpp = bp; if (opt_debug & DBG_BASE) fprintf(stderr, "BASE: ADDED base=%d, src=%s, dst=%s\n", bp->b_ident, src, dst); return (bp); } /* * routine: * add_file_to_list * * purpose: * to find a file on a list, or if necessary add it to the list * * this is an internal routine, used only by add_file_to_base * and add_file_to_dir. * * parameters: * pointer to the list head * * returns: * pointer to a file structure * * notes: * * list is sorted to provide some search optimization * * most files are in the baseline, and so come in in alphabetical * order. If we keep a guess pointer to the last file we added/found, * there is a better than even chance that this one should be * added immediately onto the end of it ... and in so doing we * can save ourselves the trouble of searching the lists most * of the time. * * this win would be even better if the FTW traversal was sorted, * but building the baseline is enough of a win to justify the * feature ... but even without this we run a 60%-70% hit rate. */ static struct file * add_file_to_list(struct file **pp, const char *name) { struct file *fp, *new; int rslt; static struct file **last_list; static struct file *last_file; /* * start with the guess pointer, we hope to find that * this request will be satisfied by the next file in * the list. The two cases we are trying to optimize * are: * appending to the list, with appends in alphabetical order * searches of the list, with searches in alphabetical order */ if (last_list == pp && (new = last_file) != 0) { /* we like to think we belong farther down-list */ if (strcmp(name, new->f_name) > 0) { fp = new->f_next; /* if we're at the end, we just won */ if (fp == 0) { pp = &new->f_next; goto makeit; } /* or if the next one is what we want */ if (strcmp(name, fp->f_name) == 0) { fp->f_flags &= ~F_NEW; new = fp; goto gotit; } } } /* * our guess pointer failed, so it is exhaustive search time */ last_list = pp; for (fp = *pp; fp; pp = &fp->f_next, fp = *pp) { rslt = strcmp(name, fp->f_name); /* see if we got a match */ if (rslt == 0) { fp->f_flags &= ~F_NEW; new = fp; goto gotit; } /* see if we should go no farther */ if (rslt < 0) break; } makeit: /* * we didn't find it: * pp points at where our pointer should go * fp points at the node after ours */ new = malloc(sizeof (*new)); if (new == 0) nomem("file structure"); /* initialize the new node */ memset(new, 0, sizeof (struct file)); new->f_name = strdup(name); new->f_flags = F_NEW; /* chain it into the list */ new->f_next = fp; *pp = new; gotit: /* remember this as our next guess pointer */ last_file = new; return (new); } /* * routine: * add_file_to_base * * purpose: * to add a file-node to a baseline * * parameters: * pointer to base * name of file to be added * * returns: * pointer to file structure */ struct file * add_file_to_base(struct base *bp, const char *name) { struct file *fp; fp = add_file_to_list(&bp->b_files, name); fp->f_base = bp; fp->f_depth = 0; if (opt_debug & DBG_LIST) fprintf(stderr, "LIST: base=%d, %s file=%s\n", bp->b_ident, (fp->f_flags&F_NEW) ? "NEW" : "FOUND", name); return (fp); } /* * routine: * add_file_to_dir * * purpose: * to add a file-node to a directory * * parameters: * pointer to file entry for directory * name of file to be added * * returns: * pointer to file structure */ struct file * add_file_to_dir(struct file *dp, const char *name) { struct file *fp; fp = add_file_to_list(&dp->f_files, name); fp->f_base = dp->f_base; fp->f_depth = dp->f_depth + 1; if (opt_debug & DBG_LIST) fprintf(stderr, "LIST: dir=%s, %s file=%s\n", dp->f_name, (fp->f_flags&F_NEW) ? "NEW" : "FOUND", name); return (fp); } /* * routine: * read_baseline * * purpose: * to read in the baseline file * * parameters: * name of baseline file * * returns: * error mask */ errmask_t read_baseline(char *name) { FILE *file; errmask_t errs = 0; char *s; char *s1 = 0; char type; char *field = "???"; unsigned long l; unsigned long long ll; /* intermediate for 64 bit file support */ int level; int major, minor; struct base *bp = 0; struct file *fp; struct fileinfo *ip; aclent_t *ap; struct file *dirstack[ MAX_DEPTH ]; file = fopen(name, "r"); if (file == NULL) { fprintf(stderr, gettext(ERR_open), gettext(TXT_base), name); return (ERR_FILES); } lex_linenum = 0; if (opt_debug & DBG_FILES) fprintf(stderr, "FILE: READ BASELINE %s\n", name); while (!feof(file)) { /* find the first token on the line */ s = lex(file); /* skip blank lines and comments */ if (s == 0 || *s == 0 || *s == '#' || *s == '*') continue; field = "keyword"; /* see if the first token is a known keyword */ if (strcmp(s, "VERSION") == 0 || strcmp(s, BASE_TAG) == 0) { s = lex(0); field = gettext(TXT_noargs); if (s == 0) goto bad; major = strtol(s, &s1, 10); field = gettext(TXT_badver); if (*s1 != '.') goto bad; minor = strtol(&s1[1], 0, 10); if (major != BASE_MAJOR || minor > BASE_MINOR) { fprintf(stderr, gettext(ERR_badver), major, minor, gettext(TXT_base), name); errs |= ERR_FILES; } s1 = 0; continue; } if (strcmp(s, "BASE_SRC") == 0) { s = lex(0); field = "source directory"; if (s == 0) goto bad; s1 = strdup(s); bp = 0; continue; } if (strcmp(s, "BASE_DST") == 0) { s = lex(0); field = "destination directory"; if (s == 0) goto bad; /* make sure we have a source too */ if (s1 == 0) { field = "no source directory"; goto bad; } bp = add_base(s1, s); free(s1); s1 = 0; continue; } if (strcmp(s, "FILE") == 0) { /* make sure we have a base to add to */ if (bp == 0) { field = "missing base"; goto bad; } s = lex(0); /* level */ field = "level"; if (s == 0 || *s == 0) goto bad; l = strtoul(s, 0, 0); level = l; s = lex(0); /* type */ field = "file type"; if (s == 0 || *s == 0) goto bad; type = *s; if (gettype(type) < 0) goto bad; s = lex(0); /* name */ field = "file name"; if (s == 0 || *s == 0) goto bad; /* allocate a file structure for this entry */ if (level == 0) fp = add_file_to_base(bp, s); else fp = add_file_to_dir(dirstack[level-1], s); fp->f_flags |= F_IN_BASELINE; /* maintain the directory stack */ if (level >= MAX_DEPTH) { fprintf(stderr, gettext(ERR_deep), s); exit(ERR_OTHER); } dirstack[ level ] = fp; /* get a pointer to the baseline file info structure */ ip = &fp->f_info[ OPT_BASE ]; ip->f_type = gettype(type); /* note file type */ s = lex(0); /* modes */ field = "file modes"; if (s == 0 || *s == 0) goto bad; l = strtoul(s, 0, 0); ip->f_mode = l; s = lex(0); /* uid */ field = "file UID"; if (s == 0 || *s == 0) goto bad; l = strtoul(s, 0, 0); ip->f_uid = l; s = lex(0); /* gid */ field = "file GID"; if (s == 0 || *s == 0) goto bad; l = strtoul(s, 0, 0); ip->f_gid = l; s = lex(0); /* source inode */ field = "source i#"; if (s == 0 || *s == 0) goto bad; ll = strtoull(s, 0, 0); fp->f_s_inum = (ino_t)ll; s = lex(0); /* source major */ field = "source major"; if (s == 0 || *s == 0) goto bad; l = strtoul(s, 0, 0); fp->f_s_maj = l; s = lex(0); /* source minor */ field = "source minor"; if (s == 0 || *s == 0) goto bad; l = strtoul(s, 0, 0); fp->f_s_min = l; s = lex(0); /* source nlink */ field = "source nlink"; if (s == 0 || *s == 0) goto bad; l = strtoul(s, 0, 0); fp->f_s_nlink = l; s = lex(0); /* source mod */ field = "source modtime"; if (s == 0 || *s == 0) goto bad; l = strtoul(s, 0, 0); fp->f_s_modtime = l; s = lex(0); /* dest inode */ field = "destination i#"; if (s == 0 || *s == 0) goto bad; ll = strtoull(s, 0, 0); fp->f_d_inum = (ino_t)ll; s = lex(0); /* dest major */ field = "destination major"; if (s == 0 || *s == 0) goto bad; l = strtoul(s, 0, 0); fp->f_d_maj = l; s = lex(0); /* dest minor */ field = "destination minor"; if (s == 0 || *s == 0) goto bad; l = strtoul(s, 0, 0); fp->f_d_min = l; s = lex(0); /* dest nlink */ field = "dest nlink"; if (s == 0 || *s == 0) goto bad; l = strtoul(s, 0, 0); fp->f_d_nlink = l; s = lex(0); /* dest mod */ field = "dest modtime"; if (s == 0 || *s == 0) goto bad; l = strtoul(s, 0, 0); fp->f_d_modtime = l; s = lex(0); /* major or size */ if (type == 'C' || type == 'B') { field = "rdev major"; if (s == 0 || *s == 0) goto bad; l = strtoul(s, 0, 0); ip->f_rd_maj = l; s = lex(0); /* minor */ field = "rdev minor"; if (s == 0 || *s == 0) goto bad; l = strtoul(s, 0, 0); ip->f_rd_min = l; } else { field = "file size"; if (s == 0 || *s == 0) goto bad; ll = strtoul(s, 0, 0); ip->f_size = (off_t)ll; /* size */ } /* * all fields after this point were added to the * 1.0 format and so should be considered optional */ s = lex(0); /* acl length ? */ field = "acl count"; if (s && *s) { l = strtoul(s, 0, 0); ip->f_numacls = l; ip->f_acls = malloc(ip->f_numacls * sizeof (aclent_t)); if (ip->f_acls == 0) nomem("Access Control List"); } continue; } if (strcmp(s, "ACL") == 0) { /* make sure there is a place to put the ACL */ if (ip == 0 || ip->f_acls == 0) { field = "ACL w/o FILE/LIST"; goto bad; } /* acl entry number */ s = lex(0); field = "acl index"; if (s == 0) goto bad; l = strtoul(s, 0, 0); if (l >= ip->f_numacls) goto bad; else ap = &ip->f_acls[l]; /* acl entry type */ s = lex(0); field = "acl type"; if (s == 0) goto bad; l = strtoul(s, 0, 0); ap->a_type = l; /* acl entry ID */ s = lex(0); field = "acl id"; if (s == 0) goto bad; l = strtoul(s, 0, 0); ap->a_id = l; /* acl entry perms */ s = lex(0); field = "acl perm"; if (s == 0) goto bad; l = strtoul(s, 0, 0); ap->a_perm = l; continue; } bad: /* log the error and continue processing to find others */ fprintf(stderr, gettext(ERR_badinput), lex_linenum, field, name); errs |= ERR_FILES; } (void) fclose(file); return (errs); } /* * routine: * write_baseline * * purpose: * to rewrite the baseline file * * parameters: * name of the new baseline file * * returns: * error mask */ errmask_t write_baseline(char *name) { FILE *newfile; errmask_t errs = 0; struct base *bp; char tmpname[ MAX_PATH ]; if (opt_debug & DBG_FILES) fprintf(stderr, "FILE: WRITE BASELINE %s\n", name); /* if no-touch is specified, we don't update files */ if (opt_notouch) return (0); /* create a temporary output file */ sprintf(tmpname, "%s-TMP", name); /* create our output file */ newfile = fopen(tmpname, "w+"); if (newfile == NULL) { fprintf(stderr, gettext(ERR_creat), gettext(TXT_base), tmpname); return (ERR_FILES); } errs |= bw_header(newfile); for (bp = bases; bp; bp = bp->b_next) errs |= bw_base(newfile, bp); if (ferror(newfile)) { fprintf(stderr, gettext(ERR_write), gettext(TXT_base), tmpname); errs |= ERR_FILES; } if (fclose(newfile)) { fprintf(stderr, gettext(ERR_fclose), gettext(TXT_base), tmpname); errs |= ERR_FILES; } /* now switch the new file for the old one */ if (errs == 0) if (rename(tmpname, name) != 0) { fprintf(stderr, gettext(ERR_rename), gettext(TXT_base), tmpname, name); errs |= ERR_FILES; } return (errs); } /* * routine: * bw_header * * purpose: * to write out a baseline header * * parameters: * FILE* for the output file * * returns: * error mask * * notes: */ static errmask_t bw_header(FILE *file) { time_t now; struct tm *local; /* figure out what time it is */ (void) time(&now); local = localtime(&now); fprintf(file, "%s %d.%d\n", BASE_TAG, BASE_MAJOR, BASE_MINOR); fprintf(file, "#\n"); fprintf(file, "# filesync baseline, last written by %s, %s", cuserid(NULL), asctime(local)); fprintf(file, "#\n"); return (0); } /* * routine: * bw_base * * purpose: * to write out the summary for one base-pair * * parameters: * FILE * for the output file * * returns: * error mask * * notes: */ static errmask_t bw_base(FILE *file, struct base *bp) { struct file *fp; errmask_t errs = 0; /* see if this base is to be dropped from baseline */ if (bp->b_flags & F_REMOVE) return (0); fprintf(file, "\n"); fprintf(file, "BASE_SRC %s\n", noblanks(bp->b_src_spec)); fprintf(file, "BASE_DST %s\n", noblanks(bp->b_dst_spec)); for (fp = bp->b_files; fp; fp = fp->f_next) errs |= bw_file(file, fp, 0); return (errs); } /* * routine: * bw_file * * purpose: * to write a file description out to the baseline * * parameters: * output FILE * pointer to file description * recursion depth * * returns: * error mask * * notes: * some of the information we write out is kept separately * for source and destination files because the values should * be expected to be different for different systems/copies. * * if a file has an unresolved conflict, we want to leave * the old values in place so that we continue to compare * files against the last time they agreed. */ static errmask_t bw_file(FILE *file, struct file *fp, int depth) { struct file *cp; int i; errmask_t errs = 0; long long ll; /* intermediate for 64 bit file support */ struct fileinfo *ip = &fp->f_info[OPT_BASE]; /* if this file is to be removed from baseline, skip it */ if (fp->f_flags & F_REMOVE) return (0); /* * if this node is in conflict, or if it has not been * evaluated this time around, we should just leave the * baseline file the way it was before. If there is a * conflict, let the baseline reflect the last agreement. * If the node wasn't evaluated, let the baseline reflect * our last knowledge. */ if (fp->f_flags & F_CONFLICT || (fp->f_flags&F_EVALUATE) == 0) { fp->f_info[OPT_SRC].f_ino = fp->f_s_inum; fp->f_info[OPT_SRC].f_nlink = fp->f_s_nlink; fp->f_info[OPT_SRC].f_d_maj = fp->f_s_maj; fp->f_info[OPT_SRC].f_d_min = fp->f_s_min; fp->f_info[OPT_SRC].f_modtime = fp->f_s_modtime; fp->f_info[OPT_DST].f_ino = fp->f_d_inum; fp->f_info[OPT_DST].f_nlink = fp->f_d_nlink; fp->f_info[OPT_DST].f_d_maj = fp->f_d_maj; fp->f_info[OPT_DST].f_d_min = fp->f_d_min; fp->f_info[OPT_DST].f_modtime = fp->f_d_modtime; } /* write out the entry for this file */ fprintf(file, "FILE %d %c %-20s 0%04o", depth, showtype(ip->f_type), noblanks(fp->f_name), ip->f_mode); fprintf(file, " %6ld %6ld", ip->f_uid, ip->f_gid); ll = fp->f_info[OPT_SRC].f_ino; fprintf(file, "\t%6lld %4ld %4ld %4d 0x%08lx", ll, fp->f_info[OPT_SRC].f_d_maj, fp->f_info[OPT_SRC].f_d_min, fp->f_info[OPT_SRC].f_nlink, fp->f_info[OPT_SRC].f_modtime); ll = fp->f_info[OPT_DST].f_ino; fprintf(file, "\t%6lld %4ld %4ld %4d 0x%08lx", ll, fp->f_info[OPT_DST].f_d_maj, fp->f_info[OPT_DST].f_d_min, fp->f_info[OPT_DST].f_nlink, fp->f_info[OPT_DST].f_modtime); /* last fields are file type specific */ if (S_ISBLK(ip->f_type) || S_ISCHR(ip->f_type)) fprintf(file, "\t%4ld %4ld", ip->f_rd_maj, ip->f_rd_min); else { ll = ip->f_size; fprintf(file, "\t%lld", ll); } /* ACL count goes at the end because it was added */ fprintf(file, "\t%d", ip->f_numacls); fprintf(file, "\n"); /* if this file has ACLs, we have to write them out too */ for (i = 0; i < ip->f_numacls; i++) fprintf(file, "ACL %d %d %ld %o\n", i, ip->f_acls[i].a_type, ip->f_acls[i].a_id, ip->f_acls[i].a_perm); /* then enumerate all of the children (if any) */ for (cp = fp->f_files; cp; cp = cp->f_next) errs |= bw_file(file, cp, depth + 1); return (errs); } /* * routines: * gettype/showtype * * purpose: * to convert between a file type (as found in a mode word) * and a single character representation * * parameters/return * mode word -> character * character -> mode word */ static char types[16] __nonstring = "-PC?DNB?F?S?s???"; static char showtype(int mode) { return (types[ (mode & S_IFMT) >> 12 ]); } static long gettype(int code) { int i; for (i = 0; i < 16; i++) if (types[i] == code) return (i << 12); return (-1); } /* * 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) 1996 Sun Microsystems, Inc. All Rights Reserved * * module: * database.h * * purpose: * definition of the baseline and rules data structures */ #ifndef _DATABASE_H #define _DATABASE_H #ifdef __cplusplus extern "C" { #endif #include #include #define ACL_UID_BUG 1 /* acl:SETACL sets owner to be caller */ /* * flag bits describing what we know about an individual file, or in * some cases an entire base pair. These flags are found in the * base and file stuctures. */ typedef int fflags_t; /* type for file flags */ #define F_NEW 0x01 /* newly allocated */ #define F_IN_BASELINE 0x02 /* file found in baseline */ #define F_IN_SOURCE 0x04 /* file found in source tree */ #define F_IN_DEST 0x08 /* file found in dest tree */ #define F_EVALUATE 0x10 /* include in analysis */ #define F_SPARSE 0x20 /* don't walk this directory */ #define F_REMOVE 0x40 /* remove from baseline */ #define F_CONFLICT 0x80 /* unresolvable conflict */ #define F_LISTED 0x100 /* file came from LIST */ #define F_STAT_ERROR 0x200 /* unable to stat file */ #define F_WHEREFOUND (F_IN_BASELINE|F_IN_SOURCE|F_IN_DEST) /* * a base is a pair of directories to be kept in sync * all rules and baseline data is stored beneath some base */ struct base { struct base *b_next; /* pointer to next base */ fflags_t b_flags; /* what I know about this base */ int b_ident; /* base sequence # (DBG) */ char *b_src_spec; /* spec name of source dir */ char *b_dst_spec; /* spec name of dest dir */ char *b_src_name; /* expanded name of source dir */ char *b_dst_name; /* expanded name of dest dir */ struct rule *b_includes; /* chain of include rules */ struct rule *b_excludes; /* chain of exclude rules */ struct rule *b_restrictions; /* chain of restrictions */ struct file *b_files; /* chain of files */ /* statistics for wrap-up summary */ int b_totfiles; /* total files found in tree */ int b_src_copies; /* files copied to source */ int b_src_deletes; /* files deleted from source */ int b_src_misc; /* ownership changes on source */ int b_dst_copies; /* files copied to dest */ int b_dst_deletes; /* files deleted from dest */ int b_dst_misc; /* ownership changes on source */ int b_unresolved; /* unresolved conflicts */ }; /* * flag bits describing what we know about a particular rule. * These flags are found in the rule structure */ typedef int rflags_t; /* type for rule flags */ #define R_NEW 0x01 /* newly added rule (=OPT_NEW) */ #define R_PROGRAM 0x02 /* program (vs literal names) */ #define R_IGNORE 0x04 /* IGNORE (vs INCLUDE) */ #define R_RESTRICT 0x08 /* restriction (-r argument) */ #define R_WILD 0x10 /* name involves wild cards */ #define R_BOGUS 0x20 /* fabricated rule */ /* * a rule describes files to be included or excluded * they are stored under bases */ struct rule { struct rule *r_next; /* pointer to next rule in base */ rflags_t r_flags; /* flags associated with rule */ char *r_file; /* file for this rule */ }; /* * this is the information we keep track of for a file */ struct fileinfo { ino_t f_ino; /* inode number of this file */ long f_d_maj; /* maj dev on which it lives */ long f_d_min; /* minj dev on which it lives */ int f_type; /* file/dir/special ... */ int f_mode; /* protection */ int f_nlink; /* number of links to file */ uid_t f_uid; /* owning UID */ gid_t f_gid; /* owning GID */ off_t f_size; /* length in bytes */ long f_modtime; /* last modification time */ long f_modns; /* low order bits of modtime */ long f_rd_maj; /* major dev for specials */ long f_rd_min; /* minor dev for specials */ int f_numacls; /* number of entries in acls */ aclent_t *f_acls; /* acl list (if any) */ }; /* * flag bits describing the differences we have detected between a file * and the last time it was in sync (based on the baseline). * These flags are used in the srcdiffs and dstdiffs fields of the * file structure */ typedef int diffmask_t; /* type for difference masks */ #define D_CREATE 0x01 /* file has been created */ #define D_DELETE 0x02 /* file has been deleted */ #define D_MTIME 0x04 /* file has been modified */ #define D_SIZE 0x08 /* file has changed size */ #define D_UID 0x10 /* file has changed user id */ #define D_GID 0x20 /* file has changed group id */ #define D_PROT 0x40 /* file has changed protection */ #define D_LINKS 0x80 /* file has changed link count */ #define D_TYPE 0x100 /* file has changed type */ #define D_FACLS 0x200 /* file has changed facls */ #define D_RENAME_TO 0x400 /* file came from a rename */ #define D_RENAME_FROM 0x800 /* file has been renamed */ /* * these masks are used to determine how important potential changes are. * * D_CONTENTS there may be changes to the file's contents * D_ADMIN there may be changes to the ownership and protection * D_IMPORTANT there may be changes that should block a deletion * * Note: * I am torn on whether or not to include modtime in D_IMPORTANT. * Experience suggests that deleting one of many links affects the * file modification time. */ #define D_ADMIN (D_UID|D_GID|D_PROT|D_FACLS) #define D_CONTENTS (D_SIZE|D_TYPE|D_CREATE|D_MTIME) #define D_IMPORTANT (D_SIZE|D_TYPE|D_CREATE|D_MTIME|D_ADMIN) /* * a file is an instance that follows (under a base) from a rule * (for that base). A file structure may exist because of any * combination of a file under the source, destination, in a * baseline for historical reasons, or merely because a rule * calls it out (whether it exists or not). */ struct file { struct file *f_next; /* pointer to next file in base */ struct file *f_files; /* pointer to files in subdir */ struct base *f_base; /* pointer to owning base */ fflags_t f_flags; /* flags associated with file */ int f_depth; /* directory depth for file */ char *f_name; /* name of this file */ /* * these fields capture information, gleaned from the baseline * that is side-specific, and should not be expected to be in * agreement between the two sides. As a result, this info can * not be properly captured in f_info[OPT_BASE] and needs to * be kept somewhere else. */ long f_s_modtime; /* baseline source mod time */ ino_t f_s_inum; /* baseline source inode # */ long f_s_nlink; /* baseline source link count */ long f_s_maj; /* baseline source dev maj */ long f_s_min; /* baseline source dev min */ long f_d_modtime; /* baseline target mod time */ ino_t f_d_inum; /* baseline target inode # */ long f_d_nlink; /* baseline target link count */ long f_d_maj; /* baseline target dev maj */ long f_d_min; /* baseline target dev min */ /* stat information from baseline file and evaluation */ struct fileinfo f_info[3]; /* baseline, source, dest */ /* summary of changes discovered in analysis */ diffmask_t f_srcdiffs; /* changes on source side */ diffmask_t f_dstdiffs; /* changes on dest side */ /* this field is only valid for a renamed file */ struct file * f_previous; /* node for previous filename */ /* * these fields are only valid for a file that has been added * to the reconciliation list */ struct file *f_rnext; /* reconciliation chain ptr */ char *f_fullname; /* full name for reconciling */ long f_modtime; /* modtime for ordering purpose */ long f_modns; /* low order modtime */ /* this field is only valid for a file with a hard conflict */ char *f_problem; /* description of conflict */ }; /* * globals */ extern struct base omnibase; /* base for global rules */ extern struct base *bases; /* base for the main list */ extern int inum_changes; /* LISTed dirs with i# changes */ /* routines to manage base nodes, file nodes, and file infor */ errmask_t read_baseline(char *); errmask_t write_baseline(char *); struct file *add_file_to_base(struct base *, const char *); struct file *add_file_to_dir(struct file *, const char *); struct base *add_base(const char *src, const char *dst); void note_info(struct file *, const struct stat *, side_t); void update_info(struct file *, side_t); /* routines to manage rules */ errmask_t read_rules(char *); errmask_t write_rules(char *); errmask_t add_include(struct base *, char *); errmask_t add_ignore(struct base *, char *); /* routines to manage and querry restriction lists */ errmask_t add_restr(char *); bool_t check_restr(struct base *, const char *); /* routines for dealing with ignore lists */ void ignore_reset(); void ignore_pgm(const char *); void ignore_expr(const char *); void ignore_file(const char *); bool_t ignore_check(const char *); /* database processing routines for the primary passes */ errmask_t evaluate(struct base *, side_t, bool_t); errmask_t analyze(void); errmask_t find_renames(struct file *); errmask_t reconcile(struct file *); int prune(void); void summary(void); char *full_name(struct file *, side_t, side_t); /* routines in action.c to carry out reconciliation */ errmask_t do_copy(struct file *, side_t); errmask_t do_remove(struct file *, side_t); errmask_t do_rename(struct file *, side_t); errmask_t do_like(struct file *, side_t, bool_t); /* routines to deal with links in the reconciliation list */ struct file *find_link(struct file *, side_t); void link_update(struct file *, side_t); bool_t has_other_links(struct file *, side_t); /* maintain a name stack during directory tree traversal */ void push_name(const char *); void pop_name(); char *get_name(struct file *); /* acl manipulation functions */ int get_acls(const char *, struct fileinfo *); int set_acls(const char *, struct fileinfo *); int cmp_acls(struct fileinfo *, struct fileinfo *); char *show_acls(int, aclent_t *); #ifdef __cplusplus } #endif #endif /* _DATABASE_H */ /* * 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: * debug.c * * purpose: * utility routines for debugging filesync (tracing, diagnostics, * and error simulation) * * contents: * showflags display a word of flags symbolicly * dbg_usage printout usage info for -D switch * err_usage printout usage info for -E switch * dbg_set_error enable an error simulation * dbg_check_error check for error simulation * * * note: * there are numerous flag words and bit fields in this * program, and it would be horrendous to just print them * out in hex (in debugging output). These routines use * a "flaglist" data structure to map between bits and * character string names or descriptions. * * a flaglist is merely a list of paired bits and name strings. */ #include #include #include #include #include #include "filesync.h" #include "database.h" #include "debug.h" /* bits in opt_debug for usage message */ static struct flaglist dbgflags[] = { DBG_BASE, "BASE: base include building", DBG_RULE, "RULE: rule tree building", DBG_STAT, "STAT: file stats", DBG_ANAL, "ANAL: difference analysis", DBG_RECON, "RECO: reconciliation list processing", DBG_VARS, "VARS: qualification and expansion", DBG_FILES, "FILE: rule and baseline files", DBG_LIST, "LIST: tree building", DBG_EVAL, "EVAL: tree walking", DBG_IGNORE, "IGNO: ignore list", DBG_MISC, "MISC: everything else", 0, 0 }; /* bits in opt_debug for dsiplay */ struct flaglist dbgmap[] = { DBG_BASE, "BASE", DBG_RULE, "RULE", DBG_STAT, "STAT", DBG_ANAL, "ANAL", DBG_RECON, "RECO", DBG_VARS, "VARS", DBG_FILES, "FILE", DBG_LIST, "LIST", DBG_EVAL, "EVAL", DBG_IGNORE, "IGNO", DBG_MISC, "MISC", 0, 0 }; /* bits in the rules flag field */ struct flaglist rflags[] = { R_IGNORE, "IGNORE", R_PROGRAM, "PROGRAM", R_WILD, "WILD", R_NEW, "NEW", R_BOGUS, "BOGUS", R_RESTRICT, "RESTRICT", 0, 0 }; /* bits in the files flag field */ struct flaglist fileflags[] = { F_NEW, "new", F_IN_BASELINE, "base", F_IN_SOURCE, "srce", F_IN_DEST, "dest", F_EVALUATE, "eval", F_SPARSE, "sparse", F_REMOVE, "remove", F_CONFLICT, "conflict", F_LISTED, "listed", F_STAT_ERROR, "statfail", 0, 0 }; /* bits in the file src/dst difference mask */ struct flaglist diffmap[] = { D_CREATE, "create", D_DELETE, "delete", D_MTIME, "modtime", D_SIZE, "size", D_UID, "uid", D_GID, "gid", D_PROT, "modes", D_LINKS, "links", D_TYPE, "type", D_FACLS, "facls", D_RENAME_TO, "rename2", D_RENAME_FROM, "renamed", 0, 0 }; /* bits in the exit error code mask */ struct flaglist errmap[] = { ERR_RESOLVABLE, "resolvable", ERR_UNRESOLVED, "unresolvable", ERR_MISSING, "missing files", ERR_PERM, "permissions", ERR_FILES, "rule/base errors", ERR_INVAL, "invalid arguments", ERR_NOBASE, "bad base dir", ERR_OTHER, "other", 0, 0 }; /* * routine: * showflags * * purpose: * format flags for printing * * parameters: * pointer to map * mask to be interpreted \ * * returns: * pointer to a static buffer */ char * showflags(struct flaglist *map, long mask) { int i; static char outbuf[MAX_NAME]; outbuf[0] = 0; for (i = 0; map[i].fl_mask; i++) if (mask & map[i].fl_mask) { if (outbuf[0]) strcat(outbuf, "|"); strcat(outbuf, map[i].fl_name); } return (outbuf); } /* * routines: * dbg_usage, err_usage * * purpose: * to print out usage messages for the secret debugging flags * * returns: * void */ void dbg_usage(void) { int i; fprintf(stderr, "Usage:\tfilesync -Dmask ...\n"); for (i = 0; dbgflags[i].fl_mask; i++) fprintf(stderr, "\t0x%04lx .... %s\n", dbgflags[i].fl_mask, dbgflags[i].fl_name); fprintf(stderr, "\n"); } #ifdef DBG_ERRORS /* * The -E flag is a debugging feature that enables the user to request * the simulation of difficult to trigger error conditions in order * to test out the error handling code in filesync. We maintain a * registry that specifies a file name and an operation, and an errno * to be returned if the specified operation is attempted on the * specified file. */ void err_usage(void) { fprintf(stderr, "Usage:\tfilesync -E,,\n"); fprintf(stderr, "\ts ... eval stat source\n"); fprintf(stderr, "\tS ... eval stat destination\n"); fprintf(stderr, "\tn ... eval nftw source\n"); fprintf(stderr, "\tN ... eval nftw destination\n"); fprintf(stderr, "\tc ... reconcile copy create\n"); fprintf(stderr, "\to ... reconcile copy open\n"); fprintf(stderr, "\tr ... reconcile copy read/readlink\n"); fprintf(stderr, "\tw ... reconcile copy write\n"); fprintf(stderr, "\tl ... reconcile link/symlink\n"); fprintf(stderr, "\tu ... reconcile unlink\n"); fprintf(stderr, "\td ... reconcile mkdir/mknod\n"); fprintf(stderr, "\tD ... reconcile rmdir\n"); fprintf(stderr, "\tm ... reconcile rename\n"); fprintf(stderr, "\tR ... reconcile restat\n"); fprintf(stderr, "\tp ... reconcile protection (chmod)"); fprintf(stderr, "\ta ... reconcile access control (setfacl)"); fprintf(stderr, "\tO ... reconcile ownership (chown)"); fprintf(stderr, "\tZ ... out of space on target\n"); fprintf(stderr, "\n"); } /* * this data structure us used to keep track of the error simulations * that have been requested. */ static struct errsim { int Errno; /* error number to return */ char code; /* event triggering the error */ char *file; /* file name triggering error */ } errsim[ DBG_MAX_ERR ]; static int num_errs; /* number of simulated errors */ /* * routine: * dbg_set_error * * purpose: * note that we have been requested to simulate file access errors * * parameters: * argument string ,, * * returns: * error mask */ int dbg_set_error(char *arg) { char *s; char error_type; int error_no; if (num_errs >= DBG_MAX_ERR) { fprintf(stderr, "ERROR: only %d -E specifications allowed\n", DBG_MAX_ERR); return (ERR_INVAL); } /* get the error number */ if (!isdigit(arg[0])) return (ERR_INVAL); error_no = strtol(arg, &s, 0); /* get the error condition */ if (*s++ != ',' || !isalpha(*s)) return (ERR_INVAL); error_type = *s; /* get the file name */ while (*s && *s != ',') s++; if (*s++ != ',' || *s == 0) return (ERR_INVAL); /* register the error simulation */ errsim[num_errs].Errno = error_no; errsim[num_errs].code = error_type; errsim[num_errs].file = s; if (opt_debug & DBG_MISC) fprintf(stderr, "MISC: errsim[%d] %c(%s) -> %d\n", num_errs, error_type, s, error_no); num_errs++; return (0); } /* * routine: * dbg_chk_error * * purpose: * determine whether or not we have been asked to simulate an * error for a specified file. * * parameters: * file name * * returns: * errno (or zero if no error) */ int dbg_chk_error(const char *name, char code) { int i; for (i = 0; i < num_errs; i++) { /* see if this code matches any registered condition */ if (code != errsim[i].code) continue; /* see if this also matches the file name */ if (!suffix(name, errsim[i].file)) continue; /* we have a winner */ if (opt_debug & DBG_MISC) fprintf(stderr, "MISC: trigger %d for file %c(%s)\n", errsim[i].Errno, code, name); return (errsim[i].Errno); } return (0); } #else /* ! DBG_ERRORS */ void err_usage(void) { fprintf(stderr, "ERROR: this filesync does not support -E\n"); } int dbg_set_error(char *arg) { return (ERR_INVAL); } int dbg_chk_error(const char *name, char code) { return (0); } #endif /* * 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) 1996 Sun Microsystems, Inc. All Rights Reserved * * module: * debug.h * * purpose: * definitions and declarations for special debugging features */ #ifndef _DEBUG_H #define _DEBUG_H #ifdef __cplusplus extern "C" { #endif #define DBG_ERRORS 1 /* enable error simulation code */ #define DBG_MAX_ERR 20 /* maximum # simulated errs */ /* * the flaglists are used by the showflags routine in order to * print bitmasks in a symbolic form */ struct flaglist { long fl_mask; /* the bit in question */ char *fl_name; /* the name of that bit */ }; extern struct flaglist dbgmap[], rflags[], fileflags[], diffmap[], errmap[]; char *showflags(struct flaglist *, long); /* turn bit to a name */ int dbg_set_error(char *arg); /* simulate error */ int dbg_chk_error(const char *name, char code); /* check for simul err */ void dbg_usage(); /* debug flag usage */ void err_usage(); /* error simul usage */ #ifdef __cplusplus } #endif #endif /* _DEBUG_H */ /* * 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. */ #include #include #include #include #include #include #include #include #include #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, >); 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(>); #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); } /* * 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 * * module: * files.c * * purpose: * routines to examine and manipulate file names * * contents: * qualify ... ensure that a name is fully qualified * expand ... expand env variables within a string or file name * noblanks .. ensure that a name contains no embdded unescaped blanks * lex ....... a lexer that can handle escaped/embedded blanks * wildcards . see whether or not a name contains wild cards * prefix .... does one string begin with another * suffix .... does one string end with another * contains .. does one string contain another * * cannonize (static) ... compress redundant "." and ".." out of name * * notes: * we are interested in embedded blanks because international * character sets and non-unix file systems can both contain * the byte 0x20. Thus, whenever we record a filename in * file, we must be careful to escape any embedded blanks that * cause trouble when we re-lex that file later. */ #ident "%W% %E% SMI" #include #include #include #include #include #include "filesync.h" #include "messages.h" static void cannonize(char *name); /* * routine: * qualify * * purpose: * to fully qualify a name * * parameters: * name to be qualified * * returns: * either original pointer or copy to a new (malloced) buffer * * notes: * someday I may conclude that I should always make a copy * so that the caller can know that it is safe to free the parm * * I thought about this and concluded that there is never a need * to fully qualify a string containing variables. If the string * came from the command line, the variables were already expanded * and if it came from the rules data base it is required to already * be fully qualified. */ char * qualify(char *name) { char namebuf[ MAX_PATH ]; /* in the simple case, the parameter is already there */ if (*name == '/') { cannonize(name); return (name); } /* things that begin with variables get the benefit of the doubt */ if (*name == '$') { cannonize(name); return (name); } /* start with the current working directory */ if (getcwd(namebuf, sizeof (namebuf)) == 0) { fprintf(stderr, gettext(ERR_nocwd), name); exit(ERR_OTHER); } /* make sure we have room for our file name */ if ((strlen(namebuf) + strlen(name) + 2) >= sizeof (namebuf)) { fprintf(stderr, gettext(ERR_longname), name); exit(ERR_OTHER); } /* append the specified file name to it */ strcat(namebuf, "/"); strcat(namebuf, name); /* filter out redundant dots */ cannonize(namebuf); if (opt_debug & DBG_VARS) fprintf(stderr, "VARS: QUALIFY %s to %s\n", name, namebuf); /* and return a newly malloc'd copy */ return (strdup(namebuf)); } /* * routine: * expand * * purpose: * to expand variable names within a string * * parameters: * string to be expanded. Variable references always begin * with a $ and are delimited by parens or curleys. * * returns: * either original pointer or a copy to a new (malloced) buffer * * notes: * someday I may conclude that I should always make a copy * so that the caller can know that it is safe to free the parm * * someday I may decide to support escape conventions for embedding * $(){} in file names, but I suspec that day will never come. * * I thought about this and concluded there was no reason to * fully qualify these names, because the only names that should * need qualification are src/dst lines from the command line, * and the shell should have handled those for me. Once something * makes it into the database, it is expected to be fully qualified * already. * * We are limited to producing strings of length MAX_PATH or less * and variable names of length MAX_NAME or less. In practice, * these limitations should not be a problem. */ char * expand(char *name) { const char *s; char *p, *v; char delim; char namebuf[ MAX_PATH ]; char varbuf[ MAX_NAME ]; /* first see if there are no variables to be bound */ for (s = name; *s && *s != '$'; s++); if (*s == 0) return (name); /* move through the string, copying and expanding */ for (s = name, p = namebuf; *s; s++) { /* check for overflow */ if (p >= &namebuf[ MAX_PATH ]) { fprintf(stderr, gettext(ERR_longname), name); exit(ERR_OTHER); } /* normal characters, we just copy */ if (*s != '$') { *p++ = *s; continue; } /* figure out how the variable name is delimited */ delim = *++s; if (delim == '(') { delim = ')'; s++; } else if (delim == '{') { delim = '}'; s++; } else delim = 0; /* copy the variable name up to the closing delimiter */ for (v = varbuf; *s; s++) { if (isalnum(*s) || (*s == '_') || (delim && *s != delim)) *v++ = *s; else break; /* make sure we don't overflow var name buffer */ if (v >= &varbuf[MAX_NAME - 1]) { *v = 0; fprintf(stderr, gettext(ERR_longname), varbuf); exit(ERR_OTHER); } } *v = 0; /* FIX THIS ... there must be a more elegant way */ /* we may have to back up because s will be bumped */ if (delim == 0 || *s != delim) s--; /* look up the variable */ v = getenv(varbuf); if (v == 0 || *v == 0) { fprintf(stderr, gettext(ERR_undef), varbuf); return (0); } /* copy the variable into the buffer */ while (*v) *p++ = *v++; } /* null terminate the copy */ *p = 0; /* compress out any redundant dots and dot-dots */ cannonize(namebuf); if (opt_debug & DBG_VARS) fprintf(stderr, "VARS: EXPAND %s to %s\n", name, namebuf); /* and return a newly malloc'd copy */ return (strdup(namebuf)); } /* * routine: * noblanks * * purpose: * to ensure that a name contains no unescaped embedded blanks * * parameters: * pointer to name * * returns: * pointer to name or pointer to buffer containing escaped version of name * * notes: * this routine can be called on full file names, and so can * conceivably require an arbitrarily large buffer. */ const char * noblanks(const char *name) { const char *s; char *p; static char *namebuf = 0; static int buflen = 0; int l; /* first see if there are no embedded blanks */ for (s = name; *s && *s != ' '; s++); if (*s == 0) return (name); /* make sure we have a buffer large enough for the worst case */ l = 4 + (2*strlen(name)); for (buflen = MAX_PATH; buflen < l; buflen += MAX_NAME); namebuf = (char *) realloc(namebuf, buflen); /* quote the name, and copy it, escaping quotes */ p = namebuf; *p++ = '"'; for (s = name; *s; s++) { if (*s == '"' || *s == '\\') *p++ = '\\'; *p++ = *s; } *p++ = '"'; *p = 0; return (namebuf); } /* * routine: * lex * * purpose: * my own version of strtok that handles quoting and escaping * * parameters: * FILE structure for file to read (0 for same string, same file) * * returns: * pointer to next token * * notes: * this routine makes no changes to the string it is passed, * copying tokens into a static buffer. * * this routine handles continuation lines after reading and * before the lexing even starts. This limits continued lines * to a length of MAX_LINE, but keeps everything else very simple. * We also, therefore, limit tokens to a maximum length of MAX_LINE. */ int lex_linenum; /* line number in current input file */ char * lex(FILE *file) { char c, delim; char *p; char *s; static char *savep; static char namebuf[ MAX_LINE ]; static char inbuf[ MAX_LINE ]; if (file) { /* read a new line */ p = inbuf + sizeof (inbuf); /* read the next input line, with all continuations */ for (s = inbuf; savep = fgets(s, p - s, file); ) { lex_linenum++; /* go find the last character of the input line */ while (*s && s[1]) s++; if (*s == '\n') s--; /* see whether or not we need a continuation */ if (s < inbuf || *s != '\\') break; continue; } if (savep == 0) return (0); s = inbuf; } else { /* continue with old line */ if (savep == 0) return (0); s = savep; } savep = 0; /* skip over leading white space */ while (isspace(*s)) s++; if (*s == 0) return (0); /* see if this is a quoted string */ c = *s; if (c == '\'' || c == '"') { delim = c; s++; } else delim = 0; /* copy the token into the buffer */ for (p = namebuf; (c = *s) != 0; s++) { /* literal escape */ if (c == '\\') { s++; *p++ = *s; continue; } /* closing delimiter */ if (c == delim) { s++; break; } /* delimiting white space */ if (delim == 0 && isspace(c)) break; /* ordinary characters */ *p++ = *s; } /* remember where we left off */ savep = *s ? s : 0; /* null terminate and return the buffer */ *p = 0; return (namebuf); } /* * routine: * wildcards * * purpose: * determine whether or not there are any wild cards in a name * * parameters: * name to be checked * * returns: * true/false * * notes: * we use this to take shortcuts */ bool_t wildcards(const char *name) { const char *s; int literal = 0; for (s = name; *s; s++) if (literal) switch (*s) { case '\'': /* end of literal string */ literal = 0; continue; case '\\': /* escape next character */ s++; continue; } else switch (*s) { case '\'': /* literal string */ literal = 1; continue; case '\\': /* escape next character */ s++; continue; case '*': case '[': case '{': case '?': /* any of these is a wild card */ return (TRUE); } return (FALSE); } /* * routine: * cannonize * * purpose: * to compress redundant dots out of a path * * parameters: * file name in an editable buffer * * returns: * void * * notes: * because we compress the string in place, there is no danger * of our overflowing any fixed sized buffer. */ static void cannonize(char *name) { char *s, *p; /* leading dot-slashes */ for (s = name; *s == '.' && s[1] == '/'; strcpy(s, &s[2])); for (s = name; *s; s++) { /* interesting things happen after slashes */ if (*s != '/') continue; /* embedded dot-slashes */ while (s[1] == '.' && s[2] == '/') strcpy(&s[1], &s[3]); /* embedded slash-dot-dot-slash */ if (strncmp(s, "/../", 4) == 0) { /* scan backwards to eliminate last directory */ for (p = s-1; p > name && *p != '/'; p--); if (p < name) p = name; strcpy(p, &s[3]); } continue; } } /* * routine: * prefix * * purpose: * determine whether or not one string begins with another * * parameters: * string to be tested * suspected prefix * * returns: * no 0 * yes pointer character after prefix */ const char * prefix(const char *s, const char *p) { while (*p) if (*p++ != *s++) return (0); return (s); } /* * routine: * suffix * * purpose: * determine whether or not one string ends with another * * parameters: * string to be tested * suspected suffix * * returns: * true/false */ bool_t suffix(const char *str, const char *suf) { const char *s; /* go to where the alleged suffix would start */ for (s = str; *s; s++); s -= strlen(suf); if (s < str) return (FALSE); /* see if the string ends with the suffix */ while (*suf) if (*suf++ != *s++) return (FALSE); return (TRUE); } /* * routine: * contains * * purpose: * determine whether or not one string contains another * * parameters: * string to be checked * pattern we are seeking * * returns: * true/false */ bool_t contains(const char *str, const char *pat) { const char *s, *p; while (*str) { if (*str++ == *pat) { for (s = str, p = &pat[1]; *s == *p; s++, p++) if (p[1] == 0) return (TRUE); } } return (FALSE); } /* * 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) 1996 Sun Microsystems, Inc. All Rights Reserved * Copyright (c) 2016 by Delphix. All rights reserved. * * module: * filesync.h * * purpose: * general defines for use throughout the program */ #ifndef _FILESYNC_H #define _FILESYNC_H #ifdef __cplusplus extern "C" { #endif #include /* * arbitrary limits */ #define MAX_NAME 256 /* longest path component */ #define MAX_PATH 1024 /* longest total path length */ #define MAX_RLIST 32 /* max number of -r arguments */ #define MAX_LINE 1024 /* longest input line */ #define MAX_DEPTH 20 /* how deep to recurse */ #define COPY_BSIZE 8192 /* block size for file copies */ #define MIN_HOLE 1024 /* minimum hole in sparse file */ #define HASH_SIZE 99 /* ignore list hash table */ /* * sanity check limits */ #define CONFIRM_MIN 4 /* min # deletetes to confirm */ #define CONFIRM_PCT 25 /* min pctg of files to confirm */ /* * special types used in the program */ typedef enum { FALSE = 0, TRUE = 1, MAYBE = 2 /* only partially true */ } bool_t; typedef enum { OPT_BASE = 0, /* use the baseline data */ OPT_SRC = 1, /* use the source side */ OPT_DST = 2, /* use the destination side */ OPT_OLD = 3, /* use the old one */ OPT_NEW = 4 /* use the new one */ } side_t; /* * values for debug mask */ typedef long dbgmask_t; /* type for debug masks */ #define DBG_BASE 0x0001 /* baseline changes */ #define DBG_RULE 0x0002 /* rule base changes */ #define DBG_STAT 0x0004 /* file stats */ #define DBG_ANAL 0x0008 /* analysis tracing */ #define DBG_RECON 0x0010 /* reconciliation tracing */ #define DBG_VARS 0x0020 /* variable tracing */ #define DBG_FILES 0x0040 /* file reading/writing */ #define DBG_LIST 0x0080 /* include list building */ #define DBG_EVAL 0x0100 /* evaluation tracing */ #define DBG_IGNORE 0x0200 /* ignore tracing */ #define DBG_MISC 0x0400 /* catch-all everything else */ /* * values for error codes */ typedef int errmask_t; /* type for error masks */ #define ERR_OK 0 /* everything is fine */ #define ERR_RESOLVABLE 1 /* resolvable conflicts */ #define ERR_UNRESOLVED 2 /* unresolvable conflicts */ #define ERR_MISSING 4 /* some files missing */ #define ERR_PERM 8 /* insufficient access */ #define ERR_FILES 16 /* file format or I/O errors */ #define ERR_INVAL 32 /* invalid arguments */ #define ERR_NOBASE 64 /* inaccessable base directory */ #define ERR_OTHER 128 /* anything else */ /* errors that will prevent reconciliation from taking place */ #define ERR_FATAL (ERR_FILES|ERR_INVAL|ERR_NOBASE|ERR_OTHER) /* errors that will cause reconciliation to stop with -h specified */ #define ERR_ABORT (ERR_FILES|ERR_PERM) /* * program defaults */ #define DFLT_PRFX "$HOME/" /* default location/pfx */ #define SUFX_RULES ".packingrules" /* rules v1.1 location */ #define SUFX_BASE ".filesync-base" /* baseline location */ #define SUFX_OLD ".filesync-rules" /* rules v1.0 location */ /* * global variables for command line options */ extern bool_t opt_acls; /* enable acl checking/preservation */ extern bool_t opt_mtime; /* preserve modification times */ extern bool_t opt_notouch; /* don't actually make any changes */ extern side_t opt_force; /* designated winner for conflicts */ extern side_t opt_oneway; /* one way only propagation */ extern side_t opt_onesided; /* permit one sided analysis */ extern bool_t opt_everything; /* everything must agree (modes/uid/gid) */ extern bool_t opt_quiet; /* stiffle reconciliaton descriptions */ extern bool_t opt_verbose; /* generate analysis commentary */ extern bool_t opt_errors; /* simulate errors on specified files */ extern bool_t opt_halt; /* halt on any propagation error */ extern dbgmask_t opt_debug; /* debugging options */ /* * information gained during startup that other people may need */ extern uid_t my_uid; /* User ID for files I create */ extern gid_t my_gid; /* Group ID for files I create */ /* error and warning routines */ void confirm(char *); /* ask user if they're sure */ void nomem(char *); /* die from malloc failure */ /* routines for dealing with strings and file names */ const char *prefix(const char *, const char *); /* does s1 begin with s2 */ char *qualify(char *); /* validate and fully qualify */ char *expand(char *); /* expand variables in name */ char *lex(FILE *); /* lex off one token */ extern int lex_linenum; /* current input file line number */ const char *noblanks(const char *); /* escape strings for embedded blanks */ bool_t wildcards(const char *); /* does name contain wildcards */ bool_t suffix(const char *, const char *); /* does s1 end with s2 */ bool_t contains(const char *, const char *); /* does s1 contain s2 */ #ifdef __cplusplus } #endif #endif /* _FILESYNC_H */ /* * 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 * * module: * ignore.c * * purpose: * routines to manage the ignore lists and test names against them, * * contents: * ignore_check ... is a particular file covered by an ignore rule * ignore_file .... add a specific file name to be ignored * ignore_expr .... add a regular expression for files to be ignored * ignore_pgm ..... add a rule to run a program to generate a list * ignore_reset ... flush the internal optimization data structures * * static * ign_hash ... maintain a hash table of ignored names * cheap_check. build up a table of safe suffixes * * notes: * a much simpler implementation could have been provided, but * this test (every file tested against every rule) has the * potential to be EXTREMELY expensive. This module implements * an engine that attempts to optimize the process of determining * that a file has not been ignored. * * the usage scenario is * per base * call ignore_{file,expr,pgm} for each ignore rule * call ignore_check for every file under the base * call ignore_reset when you are done */ #ident "%W% %E% SMI" #include #include #include #include #include "filesync.h" #include "messages.h" /* * routines: */ static struct list *ign_hash(const char *, int); static void cheap_check(const char *); /* * globals */ struct list { char *l_value; /* the actual string */ struct list *l_next; /* pointer to next element */ }; static struct list *expr_list; /* list of regular expressions */ static struct list *file_list[ HASH_SIZE ]; /* hash table of literal names */ static char cheap_last[256]; /* cheap test: last char */ static char cheap_penu[256]; /* cheap test: penultimate char */ /* * routine: * ignore_check * * purpose: * determine whether or not a particular name matches an ignore pattern. * * parameters: * file name * * returns: * true/false * * note: * becuse this routine is called on every single file in * every single sub-directory, it is critical that we make * it fail quickly for most files. The purpose of the cheap_last * and cheap_penu arrays is to quickly determine there is no chance * that a name will match any expression. Most expressions have * wildcards near the front and constant suffixes, so our cheap * test is to look at the last two bytes. */ bool_t ignore_check(const char *name) { struct list *lp; const char *s; /* * start with the cheap test */ for (s = name; *s; s++); if (cheap_last[ (unsigned char) s[-1] ] == 0 || cheap_penu[ (unsigned char) s[-2] ] == 0) return (FALSE); /* check the literal names in the hash table */ if (ign_hash(name, 0)) { if (opt_debug & DBG_IGNORE) fprintf(stderr, "IGNO: match %s\n", name); return (TRUE); } /* check all the regular expressions */ for (lp = expr_list; lp; lp = lp->l_next) { if (gmatch(name, lp->l_value) == 0) continue; if (opt_debug & DBG_IGNORE) fprintf(stderr, "IGNO: regex %s : %s\n", lp->l_value, name); return (TRUE); } return (FALSE); } /* * routine: * ignore_file * * purpose: * to add a specific file to an ignore list * * parameters: * command to run */ void ignore_file(const char *name) { cheap_check(name); (void) ign_hash(name, 1); if (opt_debug & DBG_IGNORE) fprintf(stderr, "IGNO: add file %s\n", name); } /* * routine: * ignore_expr * * purpose: * to add a regular expression to an ignore list * * parameters: * command to run */ void ignore_expr(const char *expr) { struct list *lp; cheap_check(expr); /* allocate a new node and stick it on the front of the list */ lp = malloc(sizeof (*lp)); if (lp == 0) nomem("ignore list"); lp->l_value = strdup(expr); lp->l_next = expr_list; expr_list = lp; if (opt_debug & DBG_IGNORE) fprintf(stderr, "IGNO: add expr %s\n", expr); } /* * routine: * ignore_pgm * * purpose: * to run a program and gather up the ignore list it produces * * parameters: * command to run */ void ignore_pgm(const char *cmd) { char *s; FILE *fp; char inbuf[ MAX_LINE ]; if (opt_debug & DBG_IGNORE) fprintf(stderr, "IGNO: add pgm %s\n", cmd); /* run the command and collect its ouput */ fp = popen(cmd, "r"); if (fp == NULL) { fprintf(stderr, gettext(ERR_badrun), cmd); return; } /* * read each line, strip off the newline and add it to the list */ 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++); /* add this file to the list */ if (*s) { cheap_check(s); (void) ign_hash(s, 1); if (opt_debug & DBG_IGNORE) fprintf(stderr, "IGNO: ... %s\n", s); } } pclose(fp); } /* * routine: * ign_hash * * purpose: * to find an entry in the hash list * * parameters: * name * allocate flag * * returns: * pointer to new list entry or 0 */ static struct list * ign_hash(const char *name, int alloc) { const unsigned char *s; int i; struct list *lp; struct list **pp; /* perform the hash and find the chain */ for (s = (const unsigned char *) name, i = 0; *s; s++) i += *s; pp = &file_list[i % HASH_SIZE ]; /* search for the specified entry */ for (lp = *pp; lp; lp = *pp) { if (strcmp(name, lp->l_value) == 0) return (lp); pp = &(lp->l_next); } /* if caller said alloc, buy a new node and chain it in */ if (alloc) { lp = malloc(sizeof (*lp)); if (lp == 0) nomem("ignore list"); lp->l_value = strdup(name); lp->l_next = 0; *pp = lp; } return (lp); } /* * routine: * cheap_check * * purpose: * to update the cheap-check arrays for an ignore expression * * parameters: * name/expression */ static void cheap_check(const char *name) { const char *s; unsigned char c; int i; for (s = name; *s; s++); s--; /* if expr ends in a wild card, we are undone */ c = *s; if (c == '*' || c == '?' || c == ']' || c == '}') { for (i = 0; i < 256; i++) { cheap_last[i] = 1; cheap_penu[i] = 1; } return; } else cheap_last[c] = 1; if (s <= name) return; /* check the next to last character too */ c = s[-1]; if (c == '*' || c == '?' || c == ']' || c == '}') { for (i = 0; i < 256; i++) cheap_penu[i] = 1; } else cheap_penu[c] = 1; } /* * routine: * ignore_reset * * purpose: * to free up all the ignore entries so we can start anew */ void ignore_reset(void) { int i; struct list *np = 0; /* for LINT */ struct list *lp; /* clear the cheap check arrays */ for (i = 0; i < 255; i++) { cheap_last[i] = 0; cheap_penu[i] = 0; } /* free all of the literal hash chains */ for (i = 0; i < HASH_SIZE; i++) { for (lp = file_list[i]; lp; lp = np) { np = lp->l_next; free(lp->l_value); free(lp); } file_list[i] = 0; } /* free all of the expressions on the chain */ for (lp = expr_list; lp; lp = np) { np = lp->l_next; free(lp->l_value); free(lp); } expr_list = 0; } /* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License (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 2007 Sun Microsystems, Inc. All rights reserved. * Use is subject to license terms. */ /* * module: * main.c * * purpose: * argument handling and top level dispatch * * contents: * main argument handling and main loop * usage (static) print out usage message * confirm prompt the user for a confirmation and get it * nomem fatal error handler for malloc failures * findfiles (static) locate our baseline and rules files * cleanup (static) unlock baseline and delete temp file * check_access (static) do we have adequate access to a file/directory * whoami (static) get uid/gid/umask */ #include #include #include #include #include #include #include #include #include "filesync.h" #include "database.h" #include "messages.h" #include "debug.h" /* * local routines in this module: */ static errmask_t findfiles(); /* find rule and baseline files */ static void cleanup(int); /* cleanup locks and temps */ static errmask_t check_access(char *, int *); /* check access to file */ static void whoami(); /* gather information about me */ static void usage(void); /* general usage */ /* * globals exported to the rest of the program */ bool_t opt_mtime; /* preserve modification times on propagations */ bool_t opt_notouch; /* don't actually make any changes */ bool_t opt_quiet; /* disable reconciliation command output */ bool_t opt_verbose; /* enable analysis descriptions */ side_t opt_force; /* designated winner for conflicts */ side_t opt_oneway; /* one way only propagation */ side_t opt_onesided; /* permit one-sided evaluation */ bool_t opt_everything; /* everything must agree (modes/uid/gid) */ bool_t opt_yes; /* pre-confirm massive deletions are OK */ bool_t opt_acls; /* always scan for acls on all files */ bool_t opt_errors; /* simulate errors on specified files */ bool_t opt_halt; /* halt on propagation errors */ dbgmask_t opt_debug; /* debug mask */ uid_t my_uid; /* default UID for files I create */ gid_t my_gid; /* default GID for files I create */ static char *file_rules; /* name of rules file */ static char *file_base; /* name of baseline file */ static int new_baseline; /* are we creating a new baseline */ static int new_rules; /* are we creating a new rules file */ static int my_umask; /* default UMASK for files I create */ static int lockfd; /* file descriptor for locking baseline */ static char *rlist[MAX_RLIST]; static int num_restrs = 0; /* * routine: * main * * purpose: * argument processing and primary dispatch * * returns: * error codes per filesync.1 (ERR_* in filesync.h) * * notes: * read filesync.1 in order to understand the argument processing * * most of the command line options just set some opt_ global * variable that is later looked at by the code that actually * implements the features. Only file names are really processed * in this routine. */ int main(int argc, char **argv) { int i; int c; errmask_t errs = ERR_OK; int do_prune = 0; char *srcname = 0; char *dstname = 0; struct base *bp; /* keep the error messages simple */ argv[0] = "filesync"; /* gather together all of the options */ while ((c = getopt(argc, argv, "AaehmnqvyD:E:r:s:d:f:o:")) != EOF) switch (c) { case 'a': /* always scan for acls */ opt_acls = TRUE; break; case 'e': /* everything agrees */ opt_everything = TRUE; break; case 'h': /* halt on error */ opt_halt = TRUE; break; case 'm': /* preserve modtimes */ opt_mtime = TRUE; break; case 'n': /* notouch */ opt_notouch = TRUE; break; case 'q': /* quiet */ opt_quiet = TRUE; break; case 'v': /* verbose */ opt_verbose = TRUE; break; case 'y': /* yes */ opt_yes = TRUE; break; case 'D': /* debug options */ if (!isdigit(optarg[0])) { dbg_usage(); exit(ERR_INVAL); } opt_debug |= strtol(optarg, (char **)NULL, 0); break; case 'E': /* error simulation */ if (dbg_set_error(optarg)) { err_usage(); exit(ERR_INVAL); } opt_errors = TRUE; break; case 'f': /* force conflict resolution */ switch (optarg[0]) { case 's': opt_force = OPT_SRC; break; case 'd': opt_force = OPT_DST; break; case 'o': opt_force = OPT_OLD; break; case 'n': opt_force = OPT_NEW; break; default: fprintf(stderr, gettext(ERR_badopt), c, optarg); errs |= ERR_INVAL; break; } break; case 'o': /* one way propagation */ switch (optarg[0]) { case 's': opt_oneway = OPT_SRC; break; case 'd': opt_oneway = OPT_DST; break; default: fprintf(stderr, gettext(ERR_badopt), c, optarg); errs |= ERR_INVAL; break; } break; case 'r': /* restricted reconciliation */ if (num_restrs < MAX_RLIST) rlist[ num_restrs++ ] = optarg; else { fprintf(stderr, gettext(ERR_tomany), MAX_RLIST); errs |= ERR_INVAL; } break; case 's': if ((srcname = qualify(optarg)) == 0) errs |= ERR_MISSING; break; case 'd': if ((dstname = qualify(optarg)) == 0) errs |= ERR_MISSING; break; default: case '?': errs |= ERR_INVAL; break; } if (opt_debug & DBG_MISC) fprintf(stderr, "MISC: DBG=%s\n", showflags(dbgmap, opt_debug)); /* if we have file names, we need a source and destination */ if (optind < argc) { if (srcname == 0) { fprintf(stderr, gettext(ERR_nosrc)); errs |= ERR_INVAL; } if (dstname == 0) { fprintf(stderr, gettext(ERR_nodst)); errs |= ERR_INVAL; } } /* check for simple usage errors */ if (errs & ERR_INVAL) { usage(); exit(errs); } /* locate our baseline and rules files */ if (c = findfiles()) exit(c); /* figure out file creation defaults */ whoami(); /* read in our initial baseline */ if (!new_baseline && (c = read_baseline(file_base))) errs |= c; /* read in the rules file if we need or have rules */ if (optind >= argc && new_rules) { fprintf(stderr, ERR_nonames); errs |= ERR_INVAL; } else if (!new_rules) errs |= read_rules(file_rules); /* if anything has failed with our setup, go no further */ if (errs) { cleanup(errs); exit(errs); } /* * figure out whether or not we are willing to do a one-sided * analysis (where we don't even look at the other side. This * is an "I'm just curious what has changed" query, and we are * only willing to do it if: * we aren't actually going to do anything * we have a baseline we can compare against * otherwise, we are going to insist on being able to access * both the source and destination. */ if (opt_notouch && !new_baseline) opt_onesided = opt_oneway; /* * there are two interested usage scenarios: * file names specified * create new rules for the specified files * evaulate and reconcile only the specified files * no file names specified * use already existing rules * consider restricting them to specified subdirs/files */ if (optind < argc) { /* figure out what base pair we're working on */ bp = add_base(srcname, dstname); /* perverse default rules to avoid trouble */ if (new_rules) { errs |= add_ignore(0, SUFX_RULES); errs |= add_ignore(0, SUFX_BASE); } /* create include rules for each file/dir arg */ while (optind < argc) errs |= add_include(bp, argv[ optind++ ]); /* * evaluate the specified base on each side, * being careful to limit evaulation to new rules */ errs |= evaluate(bp, OPT_SRC, TRUE); errs |= evaluate(bp, OPT_DST, TRUE); } else { /* note any possible evaluation restrictions */ for (i = 0; i < num_restrs; i++) errs |= add_restr(rlist[i]); /* * we can only prune the baseline file if we have done * a complete (unrestricted) analysis. */ if (i == 0) do_prune = 1; /* evaulate each base on each side */ for (bp = bases; bp; bp = bp->b_next) { errs |= evaluate(bp, OPT_SRC, FALSE); errs |= evaluate(bp, OPT_DST, FALSE); } } /* if anything serious happened, skip reconciliation */ if (errs & ERR_FATAL) { cleanup(errs); exit(errs); } /* analyze and deal with the differenecs */ errs |= analyze(); /* see if there is any dead-wood in the baseline */ if (do_prune) { c = prune(); if (c > 0 && opt_verbose) fprintf(stdout, V_prunes, c); } /* print out a final summary */ summary(); /* update the rules and baseline files (if needed) */ (void) umask(my_umask); errs |= write_baseline(file_base); errs |= write_rules(file_rules); if (opt_debug & DBG_MISC) fprintf(stderr, "MISC: EXIT=%s\n", showflags(errmap, errs)); /* just returning ERR_RESOLVABLE upsets some people */ if (errs == ERR_RESOLVABLE && !opt_notouch) errs = 0; /* all done */ cleanup(0); return (errs); } /* * routine: * usage * * purpose: * print out a usage message * * parameters: * none * * returns: * none * * note: * the -D and -E switches are for development/test/support * use only and do not show up in the general usage message. */ static void usage(void) { fprintf(stderr, "%s\t%s %s\n", gettext(ERR_usage), "filesync", gettext(USE_simple)); fprintf(stderr, "\t%s %s\n", "filesync", gettext(USE_all)); fprintf(stderr, "\t-a .......... %s\n", gettext(USE_a)); fprintf(stderr, "\t-e .......... %s\n", gettext(USE_e)); fprintf(stderr, "\t-h .......... %s\n", gettext(USE_h)); fprintf(stderr, "\t-m .......... %s\n", gettext(USE_m)); fprintf(stderr, "\t-n .......... %s\n", gettext(USE_n)); fprintf(stderr, "\t-q .......... %s\n", gettext(USE_q)); fprintf(stderr, "\t-v .......... %s\n", gettext(USE_v)); fprintf(stderr, "\t-y .......... %s\n", gettext(USE_y)); fprintf(stderr, "\t-s dir ...... %s\n", gettext(USE_s)); fprintf(stderr, "\t-d dir ...... %s\n", gettext(USE_d)); fprintf(stderr, "\t-r dir ...... %s\n", gettext(USE_r)); fprintf(stderr, "\t-f [sdon].... %s\n", gettext(USE_f)); fprintf(stderr, "\t-o src/dst... %s\n", gettext(USE_o)); } /* * routine: * confirm * * purpose: * to confirm that the user is willing to do something dangerous * * parameters: * warning message to be printed * * returns: * void * * notes: * if this is a "notouch" or if the user has pre-confirmed, * we should not obtain the confirmation and just return that * the user has confirmed. */ void confirm(char *message) { FILE *ttyi, *ttyo; char ansbuf[ MAX_LINE ]; /* if user pre-confirmed, we don't have to ask */ if (opt_yes || opt_notouch) return; ttyo = fopen("/dev/tty", "w"); ttyi = fopen("/dev/tty", "r"); if (ttyi == NULL || ttyo == NULL) exit(ERR_OTHER); /* explain the problem and prompt for confirmation */ fprintf(ttyo, message); fprintf(ttyo, gettext(WARN_proceed)); /* if the user doesn't kill us, we can continue */ (void) fgets(ansbuf, sizeof (ansbuf), ttyi); /* close the files and return */ (void) fclose(ttyi); (void) fclose(ttyo); } void nomem(char *reason) { fprintf(stderr, gettext(ERR_nomem), reason); exit(ERR_OTHER); } /* * routine: * findfiles * * purpose: * to locate our baseline and rules files * * parameters: * none * * returns: * error mask * settings of file_base and file_rules * * side-effects: * in order to keep multiple filesyncs from running in parallel * we put an advisory lock on the baseline file. If the baseline * file does not exist we create one. The unlocking (and deletion * of extraneous baselines) is handled in cleanup. */ static errmask_t findfiles(void) /* find rule and baseline files */ { char *s, *where; char namebuf[MAX_PATH]; int ret; errmask_t errs = 0; /* figure out where the files should be located */ s = getenv("FILESYNC"); where = (s && *s) ? expand(s) : expand(DFLT_PRFX); /* see if we got a viable name */ if (where == 0) { fprintf(stderr, gettext(ERR_nofsync)); return (ERR_FILES); } /* try to form the name of the rules file */ strcpy(namebuf, where); strcat(namebuf, SUFX_RULES); s = strdup(namebuf); errs = check_access(namebuf, &new_rules); /* if we cannot find a proper rules file, look in the old place */ if (new_rules && errs == 0) { strcpy(namebuf, where); strcat(namebuf, SUFX_OLD); file_rules = strdup(namebuf); errs = check_access(namebuf, &new_rules); /* if we couldn't find that either, go with new name */ if (new_rules && errs == 0) file_rules = s; } else file_rules = s; /* try to form the name of the baseline file */ strcpy(namebuf, where); strcat(namebuf, SUFX_BASE); file_base = strdup(namebuf); errs |= check_access(namebuf, &new_baseline); if (opt_debug & DBG_FILES) { fprintf(stderr, "FILE: %s rules file: %s\n", new_rules ? "new" : "existing", file_rules); fprintf(stderr, "FILE: %s base file: %s\n", new_baseline ? "new" : "existing", file_base); } /* * in order to lock out other filesync programs we need some * file we can lock. We do an advisory lock on the baseline * file. If no baseline file exists, we create an empty one. */ if (new_baseline) lockfd = creat(file_base, 0666); else lockfd = open(file_base, O_RDWR); if (lockfd < 0) { fprintf(stderr, new_baseline ? ERR_creat : ERR_open, TXT_base, file_base); errs |= ERR_FILES; } else { ret = lockf(lockfd, F_TLOCK, 0L); if (ret < 0) { fprintf(stderr, ERR_lock, TXT_base, file_base); errs |= ERR_FILES; } else if (opt_debug & DBG_FILES) fprintf(stderr, "FILE: locking baseline file %s\n", file_base); } return (errs); } /* * routine: * cleanup * * purpose: * to clean up temporary files and locking prior to exit * * paremeters: * error mask * * returns: * void * * notes: * if there are no errors, the baseline file is assumed to be good. * Otherwise, if we created a temporary baseline file (just for * locking) we will delete it. */ static void cleanup(errmask_t errmask) { /* unlock the baseline file */ if (opt_debug & DBG_FILES) fprintf(stderr, "FILE: unlock baseline file %s\n", file_base); (void) lockf(lockfd, F_ULOCK, 0); /* see if we need to delete a temporary copy */ if (errmask && new_baseline) { if (opt_debug & DBG_FILES) fprintf(stderr, "FILE: unlink temp baseline file %s\n", file_base); (void) unlink(file_base); } } /* * routine: * check_access * * purpose: * to determine whether or not we can access an existing file * or create a new one * * parameters: * name of file (in a clobberable buffer) * pointer to new file flag * * returns: * error mask * setting of the new file flag * * note: * it is kind of a kluge that this routine clobbers the name, * but it is only called from one place, it needs a modified * copy of the name, and the one caller doesn't mind. */ static errmask_t check_access(char *name, int *newflag) { char *s; /* start out by asking for what we want */ if (access(name, R_OK|W_OK) == 0) { *newflag = 0; return (0); } /* if the problem is isn't non-existence, lose */ if (errno != ENOENT) { *newflag = 0; fprintf(stderr, gettext(ERR_rdwri), name); return (ERR_FILES); } /* * the file doesn't exist, so there is still hope if we can * write in the directory that should contain the file */ *newflag = 1; /* truncate the file name to its containing directory */ for (s = name; s[1]; s++); while (s > name && *s != '/') s--; if (s > name) *s = 0; else if (*s == '/') s[1] = 0; else name = "."; /* then see if we have write access to the directory */ if (access(name, W_OK) == 0) return (0); fprintf(stderr, gettext(ERR_dirwac), name); return (ERR_FILES); } /* * routine: * whoami * * purpose: * to figure out who I am and what the default modes/ownership * is on files that I create. */ static void whoami() { my_uid = geteuid(); my_gid = getegid(); my_umask = umask(0); if (opt_debug & DBG_MISC) fprintf(stderr, "MISC: my_uid=%u, my_gid=%u, my_umask=%03o\n", my_uid, my_gid, my_umask); } /* * 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) 1996 Sun Microsystems, Inc. All Rights Reserved * * module: * messages.h * * purpose: * contins defines for all localizable messages * * notes: * unless otherwise specified, all %s arguments can be assumed * to be file names. Non-obvious arguments are explained in * comments. */ #ifndef _MESSAGES_H #define _MESSAGES_H #ifdef __cplusplus extern "C" { #endif #include /* * summary output messages */ #define SUM_hd "RECONCILE %s and %s (%d files)\n" #define SUM_dst "\t-> %4d copies, %4d deletes, %4d ownership\n" #define SUM_src "\t<- %4d copies, %4d deletes, %4d ownership\n" #define SUM_unresolved "\tUNRESOLVED CONFLICTS: %d\n" /* * verbose mode analysis commentary */ #define V_nomore "# file %s no longer exists\n" #define V_deleted "# file %s deleted from %s\n" /* src/dst */ #define V_created "# file %s created on %s\n" /* src/dst */ #define V_delconf "# file %s has been deleted and changed\n" #define V_trunconf "# file %s has been truncated and changed\n" #define V_unchanged "# file %s has two identical versions\n" #define V_different "# file %s has two different versions\n" #define V_modes "# file %s has changed modes/ownership\n" #define V_changed "# file %s has been modified\n" #define V_renamed "# file %s has been renamed to %s\n" #define V_prunes "# %d stale entries pruned from baseline\n" #define V_nostat "# WARNING: unable to stat file %s\n" #define V_change "# WARNING: file %s on %s, was <%ld,%ld>#%ld, now <%ld,%ld>#%ld\n" #define V_suppressed "# file %s not reconciled due to halt-on-error\n" /* * usage messages */ #define ERR_usage "Usage:" #define USE_a "always check for Access Control Lists" #define USE_e "everything must agree (modes, owner, group)" #define USE_h "halt immediately after a file propagation error" #define USE_m "modification times should be preserved" #define USE_n "no touch (do not change any files)" #define USE_q "quiet (do not list reconciliation commands)" #define USE_v "verbose (commentary on each changed file)" #define USE_y "yes (do not prompt for confirmations)" #define USE_s "source directory for new rules" #define USE_d "destination directory for new rules" #define USE_r "restrict reconciliation to specified directories" #define USE_f "force conflicts to resolve in favor of src/dst/old/new" #define USE_o "one-way: only propagate changes from src/dst" /* * These are the basic usage scenario line, and in most cases should not * be translated. */ #define USE_simple "[-mnqv] -s dir -d dir file ..." #define USE_all "[-aehmnqvy] [-r dir] [-f src/dst/old/new] [-o src/dst]" /* * error messages */ #define ERR_open "ERROR: cannot open %s file %s\n" #define ERR_creat "ERROR: unable to create %s file %s\n" #define ERR_write "ERROR: write error in %s file %s\n" #define ERR_fclose "ERROR: error in flushing and closing %s file %s\n" #define ERR_chdir "ERROR: unable to chdir to %s\n" #define ERR_rename "ERROR: unable to rename %s file %s to %s\n" #define ERR_lock "ERROR: unable to lock %s file %s\n" /* * first %s argument is "rules" or "baseline" */ #define ERR_badinput "ERROR: invalid input at line %d, %s in %s\n" /* * first %s argument is a the name of the offending * field (e.g. "mode" or "major dev"). The last * %s argument is the name of the file being * processed. */ #define ERR_badver "ERROR: bad version (%d.%d) found in %s file %s\n" /* * second %s is "rules" or "baseline" * last %s is file name */ #define ERR_nocwd "ERROR: unable to get working directory for %s\n" #define ERR_longname "ERROR: excessively long name %s\n" #define ERR_undef "ERROR: undefined variable %s\n" #define ERR_deep "ERROR: directory tree is too deep at directory %s\n" #define ERR_badopt "ERROR: unrecognized option -%c %s\n" /* * the %c argument is the offending flag * (e.g. -f or -o) and the %s is the argument * that followed it. */ #define ERR_nofsync "ERROR: unable to find rule and baseline files\n" #define ERR_badbase "ERROR: invalid BASE directory %s\n" #define ERR_nosrc "ERROR: no source directory specified\n" #define ERR_nodst "ERROR: no destination directory specified\n" #define ERR_nonames "ERROR: no file/directory names specified and no rules file found\n" #define ERR_tomany "ERROR: only %d -r arguments allowed\n" #define ERR_rdwri "ERROR: cannot read/write file %s\n" #define ERR_dirwac "ERROR: cannot create files in directory %s\n" #define ERR_nomem "ERROR: unable to allocate memory for %s\n" /* * the %s argument is the name of a data structure * that could not be allocated. It is only useful * for telling the support person over the phone. */ #define ERR_badrun "ERROR: bad exit code from %s\n" /* * argument is a command from the rules file */ #define ERR_cannot "ERROR: %s %s\n" /* * The first %s argument will be a PROB_ string. * The second %s argument is the file we were * trying to do it to. */ #define ERR_abort_h "ERROR: aborting because of propagation failure\n" #define WARN_ignore "WARNING: ignoring LIST rule for %s (illegal '.', '..', or '/')\n" #define WARN_noacls "WARNING: ACLs are not supported for file %s\n" #define WARN_deletes "WARNING: this operation might delete %d files\n" #define WARN_rmdirs "WARNING: operation might delete %d non-empty directories\n" #define WARN_ichange "WARNING: %d listed directories have changed Inode #s\n" #define WARN_proceed "Press Enter to confirm, or interrupt to abort\n" #define WARN_super "NOTE: there are ownership and protection conflicts that can only be\n resolved by the super user\n" /* * descriptions of problems in unreconcilable files */ #define PROB_del_change "deleted and changed" #define PROB_different "two different versions" #define PROB_ownership "different owners" #define PROB_protection "different protections" #define PROB_prohibited "blocked by -o switch" #define PROB_aborted "aborted by -h switch" #define PROB_chown "unable to chown" #define PROB_chgrp "unable to chgrp" #define PROB_chmod "unable to chmod" #define PROB_chacl "unable to setfacl" #define PROB_link "unable to link" #define PROB_unlink "unable to unlink" #define PROB_rmdir "unable to rmdir" #define PROB_copy "unable to copy" #define PROB_mknod "unable to mknod" #define PROB_mkdir "unable to mkdir" #define PROB_readlink "unable to read symlink" #define PROB_symlink "unable to create symlink" #define PROB_restat "unable to stat/restat" #define PROB_deal "unable to deal with" #define PROB_copyin "unable to open changed file" #define PROB_copyout "unable to create new file" #define PROB_botch "unable to safely setfacl" #define PROB_rename "unable to rename" #define PROB_rename2 "unable to rename/create" #define PROB_read "read error" #define PROB_write "write error" #define PROB_space "insufficient space to copy" /* * text snippets */ #define TXT_src "source" /* for WARN_change */ #define TXT_dst "destination" /* for WARN_change */ #define TXT_srcdst "missing source/destination" /* for ERR_bad_input */ #define TXT_noargs "missing arguments" /* for ERR_bad_input */ #define TXT_badver "invalid version number" /* for ERR_bad_input */ #define TXT_nobase "LIST without a BASE" /* for ERR_bad_input */ #define TXT_rules "rules" /* for ERR_bad_ver */ #define TXT_base "baseline" /* for ERR_bad_ver */ #ifdef __cplusplus } #endif #endif /* _MESSAGES_H */ /* * 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 * * module: * recon.c * * purpose: * process the reconciliation list, figure out exactly what the * changes were, and what we should do about them. * * contents: * reconcile ... (top level) process the reconciliation list * samedata .... (static) do two files have the same contents * samestuff ... (static) do two files have the same ownership/protection * samecompare . (static) actually read and compare the contents * samelink .... (static) do two symlinks have the same contents * truncated ... (static) was one of the two copies truncted * older ....... (static) which copy is older * newer ....... (static) which copy is newer * full_name ... generate a full path name for a file * * notes: * If you only study one routine in this whole program, reconcile * is that routine. Everything else is just book keeping. * * things were put onto the reconciliation list because analyze * thought that they might have changed ... but up until now * nobody has figured out what the changes really were, or even * if there really were any changes. * * queue_file has ordered the reconciliation list with directory * creations first (depth ordered) and deletions last (inversely * depth ordered). all other changes have been ordered by mod time. */ #ident "%W% %E% SMI" #include #include #include #include #include #include "filesync.h" #include "database.h" #include "messages.h" #include "debug.h" /* * local routines to figure out how the files really differ */ static bool_t samedata(struct file *); static bool_t samestuff(struct file *); static bool_t samecompare(struct file *); static bool_t truncated(struct file *); static bool_t samelink(); static side_t newer(struct file *); static side_t older(struct file *); /* * globals */ char *srcname; /* file we are emulating */ char *dstname; /* file we are updating */ /* * routine: * reconcile * * purpose: * to perform the reconciliation action associated with a file * * parameters: * file pointer * * returns: * built up error mask * updated statistics * * notes: * The switch statement handles the obvious stuff. * The TRUE side of the samedata test handles minor differences. * The interesting stuff is in the FALSE side of the samedata test. * * The desparation heuristics (in the diffmask&CONTENTS test) are * not rigorously correct ... but they always try do the right thing * with data, and only lose mode/ownership changes in relatively * pathological cases. But I claim that the benefits outweigh the * risks, and most users will be pleased with the resulting decisions. * * Another trick is in the deletion cases of the switch. We * normally won't allow an unlink that conflicts with data * changes. If there are multiple links to the file, however, * we can make the changes and do the deletion. * * The action routines do_{remove,rename,like,copy} handle all * of their own statistics and status updating. This routine * only has to handle its own reconciliation failures (when we * can't decide what to do). */ errmask_t reconcile(struct file *fp) { errmask_t errs = 0; diffmask_t diffmask; if (opt_debug & DBG_RECON) fprintf(stderr, "RECO: %s flgs=%s, mtime=%08lx.%08lx\n", fp->f_fullname, showflags(fileflags, fp->f_flags), fp->f_modtime, fp->f_modns); /* * form the fully qualified names for both files */ srcname = full_name(fp, OPT_SRC, OPT_SRC); dstname = full_name(fp, OPT_DST, OPT_DST); /* * because they are so expensive to read and so troublesome * to set, we try to put off reading ACLs as long as possible. * If we haven't read them yet, we must read them now (so that * samestuff can compare them). */ if (opt_acls == 0 && fp->f_info[ OPT_BASE ].f_numacls == 0) { if (get_acls(srcname, &fp->f_info[ OPT_SRC ])) fp->f_srcdiffs |= D_FACLS; if (get_acls(dstname, &fp->f_info[ OPT_DST ])) fp->f_dstdiffs |= D_FACLS; } /* * If a rename has been detected, we don't have to figure * it out, since both the rename-to and rename-from files * have already been designated. When we encounter a rename-to * we should carry it out. When we encounter a rename-from * we can ignore it, since it should be dealt with as a side * effect of processing the rename-to. */ if ((fp->f_srcdiffs|fp->f_dstdiffs) & D_RENAME_FROM) return (0); if ((fp->f_srcdiffs|fp->f_dstdiffs) & D_RENAME_TO) { if (opt_verbose) fprintf(stdout, gettext(V_renamed), fp->f_previous->f_fullname, fp->f_name); if (fp->f_srcdiffs & D_RENAME_TO) { errs = do_rename(fp, OPT_DST); fp->f_srcdiffs &= D_MTIME | D_SIZE; } else if (fp->f_dstdiffs & D_RENAME_TO) { errs = do_rename(fp, OPT_SRC); fp->f_dstdiffs &= D_MTIME | D_SIZE; } if (errs != ERR_RESOLVABLE) goto done; /* * if any differences remain, then we may be dealing * with contents changes in addition to a rename */ if ((fp->f_srcdiffs | fp->f_dstdiffs) == 0) goto done; /* * fall through to reconcile the data changes */ } /* * pull of the easy cases (non-conflict creations & deletions) */ switch (fp->f_flags & (F_WHEREFOUND)) { case F_IN_BASELINE: /* only exists in baseline */ case 0: /* only exists in rules */ if (opt_verbose) fprintf(stdout, gettext(V_nomore), fp->f_fullname); fp->f_flags |= F_REMOVE; /* fix baseline */ return (0); case F_IN_BASELINE|F_IN_SOURCE: /* deleted from dest */ /* * the basic principle here is that we are willing * to do the deletion if: * no changes were made on the other side * OR * we have been told to force in this direction * * we do, however, make an exception for files that * will still have other links. In this case, the * (changed) data will still be accessable through * another link and so we are willing to do the unlink * inspite of conflicting changes (which may well * have been introduced through another link. * * The jury is still out on this one */ if (((fp->f_srcdiffs&D_IMPORTANT) == 0) || (opt_force == OPT_DST) || has_other_links(fp, OPT_SRC)) { if (opt_verbose) fprintf(stdout, gettext(V_deleted), fp->f_fullname, "dst"); errs = do_remove(fp, OPT_SRC); goto done; } /* a deletion combined with changes */ if (opt_verbose) fprintf(stdout, gettext(V_delconf), fp->f_fullname); /* if we are to resolve in favor of source */ if (opt_force == OPT_SRC) { errs = do_copy(fp, OPT_DST); goto done; } fp->f_problem = gettext(PROB_del_change); goto cant; case F_IN_BASELINE|F_IN_DEST: /* deleted from src */ /* just like previous case, w/sides reversed */ if (((fp->f_dstdiffs&D_IMPORTANT) == 0) || (opt_force == OPT_SRC) || has_other_links(fp, OPT_DST)) { if (opt_verbose) fprintf(stdout, gettext(V_deleted), fp->f_fullname, "src"); errs = do_remove(fp, OPT_DST); goto done; } /* a deletion combined with changes */ if (opt_verbose) fprintf(stdout, gettext(V_delconf), fp->f_fullname); /* if we are to resolve in favor of destination */ if (opt_force == OPT_DST) { errs = do_copy(fp, OPT_SRC); goto done; } fp->f_problem = gettext(PROB_del_change); goto cant; /* * if something new shows up, and for some reason we cannot * propagate it to the other side, we should suppress the * file from the baseline, so it will show up as a new * creation next time too. */ case F_IN_SOURCE: /* created in src */ if (opt_verbose) fprintf(stdout, gettext(V_created), fp->f_fullname, "src"); errs = do_copy(fp, OPT_DST); goto done; case F_IN_DEST: /* created in dest */ if (opt_verbose) fprintf(stdout, gettext(V_created), fp->f_fullname, "dst"); errs = do_copy(fp, OPT_SRC); goto done; case F_IN_SOURCE|F_IN_DEST: /* not in baseline */ /* * since we don't have a baseline, we cannot * know which of the two copies should prevail */ break; case F_IN_BASELINE|F_IN_SOURCE|F_IN_DEST: /* * we have a baseline where the two copies agreed, * so maybe we can determine that only one of the * two copies have changed ... but before we decide * who should be the winner we should determine * that the two copies are actually different. */ break; } /* * if we have fallen out of the case statement, it is because * we have discovered a non-obvious situation where potentially * changed versions of the file exist on both sides. * * if the two copies turn out to be identical, this is simple */ if (samedata(fp)) { if (samestuff(fp)) { /* files are identical, just update baseline */ if (opt_verbose) fprintf(stdout, gettext(V_unchanged), fp->f_fullname); update_info(fp, OPT_SRC); goto done; } else { /* * contents agree but ownership/protection does * not agree, so we have to bring these into * agreement. We can pick a winner if one * side hasn't changed, or if the user has * specified a force flag. */ if (opt_verbose) fprintf(stdout, gettext(V_modes), fp->f_fullname); if (((fp->f_srcdiffs & D_ADMIN) == 0) || (opt_force == OPT_DST)) { errs = do_like(fp, OPT_SRC, TRUE); goto done; } if (((fp->f_dstdiffs & D_ADMIN) == 0) || (opt_force == OPT_SRC)) { errs = do_like(fp, OPT_DST, TRUE); goto done; } } /* falls down to cant */ } else { /* * The two files have different contents, so we have * a potential conflict here. If we know that only one * side has changed, we go with that side. */ if (fp->f_dstdiffs == 0 || fp->f_srcdiffs == 0) { if (opt_verbose) fprintf(stdout, gettext(V_changed), fp->f_fullname); errs = do_copy(fp, fp->f_srcdiffs ? OPT_DST : OPT_SRC); goto done; } /* * Both sides have changed, so we have a real conflict. */ if (opt_verbose) fprintf(stdout, gettext(truncated(fp) ? V_trunconf : V_different), fp->f_fullname); /* * See if the user has given us explicit instructions * on how to resolve conflicts. We may have been told * to favor the older, the newer, the source, or the * destination ... but the default is to leave the * conflict unresolved. */ if (opt_force == OPT_OLD) { errs = do_copy(fp, newer(fp)); goto done; } if (opt_force == OPT_NEW) { errs = do_copy(fp, older(fp)); goto done; } if (opt_force != 0) { errs = do_copy(fp, (opt_force == OPT_SRC) ? OPT_DST : OPT_SRC); goto done; } /* * This is our last chance before giving up. * * We know that the files have different contents and * that there were changes on both sides. The only way * we can safely handle this is if there were pure contents * changes on one side and pure ownership changes on the * other side. In this case we can propagate the ownership * one way and the contents the other way. * * We decide whether or not this is possible by ANDing * together the changes on the two sides, and seeing * if the changes were all orthogonal (none of the same * things changed on both sides). */ diffmask = fp->f_srcdiffs & fp->f_dstdiffs; if ((diffmask & D_CONTENTS) == 0) { /* * if ownership changes were only made on one side * (presumably the side that didn't have data changes) * we can handle them separately. In this case, * ownership changes must be fixed first, because * the subsequent do_copy will overwrite them. */ if ((diffmask & D_ADMIN) == 0) errs |= do_like(fp, (fp->f_srcdiffs&D_ADMIN) ? OPT_DST : OPT_SRC, TRUE); /* * Now we can deal with the propagation of the data * changes. Note that any ownership/protection * changes (from the other side) that have not been * propagated yet are about to be lost. The cases * in which this might happen are all pathological * and the consequences of losing the protection * changes are (IMHO) minor when compared to the * obviously correct data propagation. */ errs |= do_copy(fp, (fp->f_srcdiffs&D_CONTENTS) ? OPT_DST : OPT_SRC); goto done; } /* * there are conflicting changes, nobody has told us how to * resolve conflicts, and we cannot figure out how to merge * the differences. */ fp->f_problem = gettext(PROB_different); } cant: /* * I'm not smart enough to resolve this conflict automatically, * so I have no choice but to bounce it back to the user. */ fp->f_flags |= F_CONFLICT; fp->f_base->b_unresolved++; errs |= ERR_UNRESOLVED; done: /* * if we have a conflict and the file is not in the baseline, * then there was never any point at which the two copies were * in agreement, and we want to preserve the conflict for future * resolution. */ if ((errs&ERR_UNRESOLVED) && (fp->f_flags & F_IN_BASELINE) == 0) if (fp->f_files == 0) /* * in most cases, this is most easily done by just * excluding the file in question from the baseline */ fp->f_flags |= F_REMOVE; else /* * but ... if the file in question is a directory * with children, excluding it from the baseline * would keep all of its children (even those with * no conflicts) out of the baseline as well. In * This case, it is better to tell a lie and to * manufacture a point of imaginary agreement * in the baseline ... but one that is absurd enough * that we will still see conflicts each time we run. * * recording a type of directory, and everything * else as zero should be absurd enough. */ fp->f_info[ OPT_BASE ].f_type = S_IFDIR; if (opt_debug & DBG_MISC) fprintf(stderr, "MISC: %s ERRS=%s\n", fp->f_fullname, showflags(errmap, errs)); return (errs); } /* * routine: * newer * * purpose: * determine which of two files is newer * * parameters: * struct file * * returns: * side_t (src/dest) */ static side_t newer(struct file *fp) { struct fileinfo *sp, *dp; sp = &fp->f_info[OPT_SRC]; dp = &fp->f_info[OPT_DST]; if (sp->f_modtime > dp->f_modtime) return (OPT_SRC); if (sp->f_modtime < dp->f_modtime) return (OPT_DST); if (sp->f_modns >= dp->f_modns) return (OPT_SRC); return (OPT_DST); } /* * routine: * older * * purpose: * determine which of two files is older * * parameters: * struct file * * returns: * side_t (src/dest) */ static side_t older(struct file *fp) { struct fileinfo *sp, *dp; sp = &fp->f_info[OPT_SRC]; dp = &fp->f_info[OPT_DST]; if (sp->f_modtime < dp->f_modtime) return (OPT_SRC); if (sp->f_modtime > dp->f_modtime) return (OPT_DST); if (sp->f_modns <= dp->f_modns) return (OPT_SRC); return (OPT_DST); } /* * routine: * samedata * * purpose: * determine whether or not two files contain the same data * * parameters: * struct file * * returns: * bool_t (true/false) */ static bool_t samedata(struct file *fp) { struct fileinfo *sp, *dp; sp = &fp->f_info[OPT_SRC]; dp = &fp->f_info[OPT_DST]; /* cheap test: types are different */ if (sp->f_type != dp->f_type) return (FALSE); /* cheap test: directories have same contents */ if (sp->f_type == S_IFDIR) return (TRUE); /* special files are compared via their maj/min */ if ((sp->f_type == S_IFBLK) || (sp->f_type == S_IFCHR)) { if (sp->f_rd_maj != dp->f_rd_maj) return (FALSE); if (sp->f_rd_min != dp->f_rd_min) return (FALSE); return (TRUE); } /* symlinks are the same if their contents are the same */ if (sp->f_type == S_IFLNK) return (samelink()); /* cheap test: sizes are different */ if (fp->f_info[OPT_SRC].f_size != fp->f_info[OPT_DST].f_size) return (FALSE); /* expensive test: byte for byte comparison */ if (samecompare(fp) == 0) return (FALSE); return (TRUE); } /* * routine: * samestuff * * purpose: * determine whether or not two files have same owner/protection * * parameters: * struct file * * returns: * bool_t (true/false) */ static bool_t samestuff(struct file *fp) { int same_mode, same_uid, same_gid, same_acl; struct fileinfo *sp, *dp; sp = &fp->f_info[OPT_SRC]; dp = &fp->f_info[OPT_DST]; same_mode = (sp->f_mode == dp->f_mode); same_uid = (sp->f_uid == dp->f_uid); same_gid = (sp->f_gid == dp->f_gid); same_acl = cmp_acls(sp, dp); /* if the are all the same, it is easy to tell the truth */ if (same_uid && same_gid && same_mode && same_acl) return (TRUE); /* note the nature of the conflict */ if (!same_uid || !same_gid || !same_acl) fp->f_problem = gettext(PROB_ownership); else fp->f_problem = gettext(PROB_protection); return (FALSE); } /* * routine: * samecompare * * purpose: * do a byte-for-byte comparison of two files * * parameters: * struct file * * returns: * bool_t (true/false) */ static bool_t samecompare(struct file *fp) { int sfd, dfd; int i, count; char srcbuf[ COPY_BSIZE ], dstbuf[ COPY_BSIZE ]; bool_t same = TRUE; sfd = open(srcname, 0); if (sfd < 0) return (FALSE); dfd = open(dstname, 0); if (dfd < 0) { close(sfd); return (FALSE); } for ( count = read(sfd, srcbuf, COPY_BSIZE); count > 0; count = read(sfd, srcbuf, COPY_BSIZE)) { /* do a matching read */ if (read(dfd, dstbuf, COPY_BSIZE) != count) { same = FALSE; goto done; } /* do the comparison for this block */ for (i = 0; i < count; i++) { if (srcbuf[i] != dstbuf[i]) { same = FALSE; goto done; } } } done: if (opt_debug & DBG_ANAL) fprintf(stderr, "ANAL: SAME=%d %s\n", same, fp->f_fullname); close(sfd); close(dfd); return (same); } /* * routine: * truncated * * purpose: * to determine whether or not a file has been truncated * * parameters: * pointer to file structure * * returns: * true/false */ static bool_t truncated(struct file *fp) { /* either source or destination must now be zero length */ if (fp->f_info[OPT_SRC].f_size && fp->f_info[OPT_DST].f_size) return (FALSE); /* file must have originally had a non-zero length */ if (fp->f_info[OPT_BASE].f_size == 0) return (FALSE); /* file type must "normal" all around */ if (fp->f_info[OPT_BASE].f_type != S_IFREG) return (FALSE); if (fp->f_info[OPT_SRC].f_type != S_IFREG) return (FALSE); if (fp->f_info[OPT_DST].f_type != S_IFREG) return (FALSE); return (TRUE); } /* * routine: * samelink * * purpose: * to determine whether or not two symbolic links agree * * parameters: * pointer to file structure * * returns: * true/false */ static bool_t samelink() { int i, srclen, dstlen; char srcbuf[ MAX_PATH ], dstbuf[ MAX_PATH ]; /* read both copies of the link */ srclen = readlink(srcname, srcbuf, sizeof (srcbuf)); dstlen = readlink(dstname, dstbuf, sizeof (dstbuf)); /* if they aren't the same length, they disagree */ if (srclen < 0 || dstlen < 0 || srclen != dstlen) return (FALSE); /* look for differences in contents */ for (i = 0; i < srclen; i++) if (srcbuf[i] != dstbuf[i]) return (FALSE); return (TRUE); } /* * routine: * full_name * * purpose: * to figure out the fully qualified path name to a file on the * reconciliation list. * * parameters: * pointer to the file structure * side indication for which base to use * side indication for which buffer to use * * returns: * pointer to a clobberable buffer * * notes: * the zero'th buffer is used for renames and links, where * we need the name of another file on the same side. */ char * full_name(struct file *fp, side_t srcdst, side_t whichbuf) { static char *buffers[3]; static int buflen = 0; char *p, *b; int l; /* see if the existing buffer is long enough */ b = (srcdst == OPT_SRC) ? fp->f_base->b_src_name : fp->f_base->b_dst_name; /* see if the allocated buffer is long enough */ l = strlen(b) + strlen(fp->f_fullname) + 2; if (l > buflen) { /* figure out the next "nice" size to use */ for (buflen = MAX_PATH; buflen < l; buflen += MAX_NAME); /* reallocate all buffers to this size */ for (l = 0; l < 3; l++) { buffers[l] = (char *) realloc(buffers[l], buflen); if (buffers[l] == 0) nomem("full name"); } } /* assemble the name in the buffer and reurn it */ p = buffers[whichbuf]; strcpy(p, b); strcat(p, "/"); strcat(p, fp->f_fullname); return (p); } /* * 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 * * module: * rename.c * * purpose: * routines to determine whether or not any renames have taken place * and note them (for reconciliation) if we find any * * contents: * find_renames . look for files that have been renamed * find_oldname . (static) find the file we were renamed from * note_rename .. (static) note the rename for subsequent reconciliation * * notes: * the reason renames warrant special attention is because the tree * we have constructed is name based, and a directory rename can * appear as zillions of changes. We attempt to find and deal with * renames prior to doing the difference analysis. * * The only case we deal with here is simple renames. If new links * have been created beneath other directories (i.e. a file has been * moved from one directory to another), the generalized link finding * stuff will deal with it. * * This is still under construction, and to completely deal with * directory renames may require some non-trivial tree restructuring. * There is a whole design note on this subject. In the mean time, * we still detect file renames, so that the user will see them * reported as "mv"s rather than as "ln"s and "rm"s. Until directory * renames are fully implemented, they will instead be handled as * mkdirs, massive links and unlinks, and rmdirs. */ #ident "%W% %E% SMI" #include #include "filesync.h" #include "database.h" /* local routines */ static struct file *find_oldname(struct file *, struct file *, side_t); static errmask_t note_rename(struct file *, struct file *, struct file *, side_t); /* * routine: * find_renames * * purpose: * recursively perform rename analysis on a directory * * parameters: * file node for the suspected directory * * returns: * error mask * * note: * the basic algorithm here is to search every directory * for files that have been newly created on one side, * and then look to see if they correspond to an identical * file that has been newly deleted on the same side. */ errmask_t find_renames(struct file *fp) { struct file *np, *rp; errmask_t errs = 0; int stype, dtype, btype, side; /* if this isn't a directory, there is nothing to analyze */ if (fp->f_files == 0) return (0); /* look for any files under this directory that may have been renamed */ for (np = fp->f_files; np; np = np->f_next) { btype = np->f_info[OPT_BASE].f_type; stype = np->f_info[OPT_SRC].f_type; dtype = np->f_info[OPT_DST].f_type; /* a rename must be a file that is new on only one side */ if (btype == 0 && stype != dtype && (!stype || !dtype)) { side = stype ? OPT_SRC : OPT_DST; rp = find_oldname(fp, np, side); if (rp) errs |= note_rename(fp, np, rp, side); } } /* recursively examine all my children */ for (np = fp->f_files; np; np = np->f_next) { errs |= find_renames(np); } return (errs); } /* * routine: * find_oldname * * purpose: * to search for an old name for a newly discovered file * * parameters: * file node for the containing directory * file node for the new file * which side the rename is believed to have happened on * * returns: * pointer to likely previous file * 0 no candidate found * * note: * this routine only deals with simple renames within a single * directory. */ static struct file *find_oldname(struct file *dirp, struct file *new, side_t side) { struct file *fp; long maj, min; ino_t inum; off_t size; side_t otherside = (side == OPT_SRC) ? OPT_DST : OPT_SRC; /* figure out what we're looking for */ inum = new->f_info[side].f_ino; maj = new->f_info[side].f_d_maj; min = new->f_info[side].f_d_min; size = new->f_info[side].f_size; /* * search the same directory for any entry that might describe * the previous name of the new file. */ for (fp = dirp->f_files; fp; fp = fp->f_next) { /* previous name on changed side must no longer exist */ if (fp->f_info[side].f_type != 0) continue; /* previous name on the other side must still exist */ if (fp->f_info[otherside].f_type == 0) continue; /* it must describe the same inode as the new file */ if (fp->f_info[OPT_BASE].f_type != new->f_info[side].f_type) continue; /* must be same type */ if (((side == OPT_SRC) ? fp->f_s_inum : fp->f_d_inum) != inum) continue; /* must be same inode # */ if (((side == OPT_SRC) ? fp->f_s_maj : fp->f_d_maj) != maj) continue; /* must be same major # */ if (((side == OPT_SRC) ? fp->f_s_min : fp->f_d_min) != min) continue; /* must be same minor # */ /* * occasionally a prompt delete and create can reuse the * same i-node in the same directory. What we really * want is generation, but that isn't available just * yet, so our poor-man's approximation is the size. * There is little point in checking ownership and * modes, since the fact that it is in the same * directory strongly suggests that it is the same * user who is doing the deleting and creating. */ if (fp->f_info[OPT_BASE].f_size != size) continue; /* looks like we found a match */ return (fp); } /* no joy */ return (0); } /* * routine: * note_rename * * purpose: * to record a discovered rename, so that the reconciliation * phase will deal with it as a rename rather than as link * followed by an unlink. * * parameters: * file node for the containing directory * file node for the new file * file node for the old file * which side the rename is believed to have happened on * * returns: * error mask */ static errmask_t note_rename(struct file *dirp, struct file *new, struct file *old, side_t side) { int dir; errmask_t errs = 0; static char *sidenames[] = {"base", "source", "dest"}; dir = new->f_info[side].f_type == S_IFDIR; if (opt_debug & DBG_ANAL) fprintf(stderr, "ANAL: NOTE RENAME %s %s/%s -> %s/%s on %s\n", dir ? "directory" : "file", dirp->f_name, old->f_name, dirp->f_name, new->f_name, sidenames[side]); /* FIX: we don't deal with directory renames yet */ if (dir) return (0); /* note that a rename has taken place */ if (side == OPT_SRC) { new->f_srcdiffs |= D_RENAME_TO; old->f_srcdiffs |= D_RENAME_FROM; } else { new->f_dstdiffs |= D_RENAME_TO; old->f_dstdiffs |= D_RENAME_FROM; } /* put a link to the old name in the new name */ new->f_previous = old; /* for most files, there is nothing else we have to do */ if (!dir) return (errs); /* * FIX ... someday we are going to have to merge the old and * new children into a single tree, but there are * horrendous backout problems if we are unable to * do the mvdir, so I have postponed this feature. */ return (errs); } /* * 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 * * module: * rules.c * * purpose: * to read and write the rules file and manage rules lists * * contents: * reading rules file * read_rules * (static) read_command * writing rules file * write_rules * (static) rw_header, rw_base * adding rules * add_ignore, add_include * (static) add_rule * adding/checking restrictions * add_restr, check_restr */ #include #include #include #include #include #include "filesync.h" #include "database.h" #include "messages.h" #include "debug.h" /* * routines: */ static errmask_t rw_base(FILE *file, struct base *bp); static errmask_t rw_header(FILE *file); static errmask_t add_rule(struct base *, int, const char *); static char *read_cmd(char *); /* * globals */ static int rules_added; static int restr_added; /* * locals */ #define RULE_MAJOR 1 /* rules file format major rev */ #define RULE_MINOR 1 /* rules file format minor rev */ #define RULE_TAG "PACKINGRULES" /* magic string for rules files */ /* * routine: * read_rules * * purpose: * to read in the rules file * * parameters: * name of rules file * * returns: * error mask * * notes: * later when I implement a proper (comment preserving) update * function I'm going to wish I had figured out how to build the * input functions for this function in a way that would make * the more usable for that too. */ errmask_t read_rules(char *name) { FILE *file; errmask_t errs = 0; int flags; int major, minor; char *s, *s1, *s2; struct base *bp; char *errstr = "???"; file = fopen(name, "r"); if (file == NULL) { fprintf(stderr, gettext(ERR_open), gettext(TXT_rules), name); return (ERR_FILES); } lex_linenum = 0; if (opt_debug & DBG_FILES) fprintf(stderr, "FILE: READ RULES %s\n", name); bp = &omnibase; /* default base before any others */ while (!feof(file)) { /* find the first token on the line */ s = lex(file); /* skip blank lines and comments */ if (s == 0 || *s == 0 || *s == '#' || *s == '*') continue; /* see if the first token is a known keyword */ if (strcmp(s, "BASE") == 0) { /* get the source & destination tokens */ errstr = gettext(TXT_srcdst); s1 = lex(0); if (s1 == 0) goto bad; s1 = strdup(s1); s2 = lex(0); if (s2 == 0) goto bad; s2 = strdup(s2); /* creat the new base pair */ bp = add_base(s1, s2); bp->b_flags |= F_LISTED; free(s1); free(s2); continue; } if (strcmp(s, "LIST") == 0) { /* make sure we are associated with a real base */ if (bp == &omnibase) { errstr = gettext(TXT_nobase); goto bad; } /* skip to the next token */ s = lex(0); errstr = gettext(TXT_noargs); if (s == 0) goto bad; /* see if it is a program or a name */ if (*s == '!') { errs |= add_rule(bp, R_PROGRAM, read_cmd(&s[1])); } else { do { flags = wildcards(s) ? R_WILD : 0; errs |= add_rule(bp, flags, s); s = lex(0); } while (s != 0); } continue; } if (strcmp(s, "IGNORE") == 0) { /* skip to the next token */ s = lex(0); errstr = gettext(TXT_noargs); if (s == 0) goto bad; flags = R_IGNORE; /* see if it is a program or a name */ if (*s == '!') { errs |= add_rule(bp, R_PROGRAM|flags, read_cmd(&s[1])); } else { do { if (wildcards(s)) flags |= R_WILD; errs |= add_rule(bp, flags, s); s = lex(0); } while (s != 0); } continue; } if (strcmp(s, "VERSION") == 0 || strcmp(s, RULE_TAG) == 0) { s = lex(0); errstr = gettext(TXT_noargs); if (s == 0) goto bad; major = strtol(s, &s1, 10); errstr = gettext(TXT_badver); if (*s1 != '.') goto bad; minor = strtol(&s1[1], 0, 10); if (major != RULE_MAJOR || minor > RULE_MINOR) { fprintf(stderr, gettext(ERR_badver), major, minor, gettext(TXT_rules), name); errs |= ERR_FILES; } continue; } bad: /* log the error and continue processing to find others */ fprintf(stderr, gettext(ERR_badinput), lex_linenum, errstr, name); errs |= ERR_FILES; } (void) fclose(file); return (errs); } /* * routine: * read_cmd * * purpose: * to lex a runnable command (! lines) into a buffer * * parameters: * first token * * returns: * pointer to a command line in a static buffer * (it is assumed the caller will copy it promptly) * * notes: * this is necessary because lex has already choped off * the first token for us */ static char *read_cmd(char * s) { static char cmdbuf[ MAX_LINE ]; cmdbuf[0] = 0; do { if (*s) { strcat(cmdbuf, s); strcat(cmdbuf, " "); } } while ((s = lex(0)) != 0); return (cmdbuf); } /* * routine: * write_rules * * purpose: * to rewrite the rules file, appending the new rules * * parameters: * name of output file * * returns: * error mask * */ errmask_t write_rules(char *name) { FILE *newfile; errmask_t errs = 0; struct base *bp; char tmpname[ MAX_PATH ]; /* if no-touch is specified, we don't update files */ if (opt_notouch || rules_added == 0) return (0); /* create a temporary output file */ sprintf(tmpname, "%s-TMP", name); /* create our output file */ newfile = fopen(tmpname, "w+"); if (newfile == NULL) { fprintf(stderr, gettext(ERR_creat), gettext(TXT_rules), name); return (ERR_FILES); } if (opt_debug & DBG_FILES) fprintf(stderr, "FILE: UPDATE RULES %s\n", name); errs |= rw_header(newfile); errs |= rw_base(newfile, &omnibase); for (bp = bases; bp; bp = bp->b_next) errs |= rw_base(newfile, bp); if (ferror(newfile)) { fprintf(stderr, gettext(ERR_write), gettext(TXT_rules), tmpname); errs |= ERR_FILES; } if (fclose(newfile)) { fprintf(stderr, gettext(ERR_fclose), gettext(TXT_rules), tmpname); errs |= ERR_FILES; } /* now switch the new file for the old one */ if (errs == 0) if (rename(tmpname, name) != 0) { fprintf(stderr, gettext(ERR_rename), gettext(TXT_rules), tmpname, name); errs |= ERR_FILES; } return (errs); } /* * routine: * rw_header * * purpose: * to write out a rules header * * parameters: * FILE* for the output file * * returns: * error mask * * notes: */ static errmask_t rw_header(FILE *file) { time_t now; struct tm *local; /* figure out what time it is */ (void) time(&now); local = localtime(&now); fprintf(file, "%s %d.%d\n", RULE_TAG, RULE_MAJOR, RULE_MINOR); fprintf(file, "#\n"); fprintf(file, "# filesync rules, last written by %s, %s", cuserid((char *) 0), asctime(local)); fprintf(file, "#\n"); return (0); } /* * routine: * rw_base * * purpose: * to write out the summary for one base-pair * * parameters: * FILE * for the output file * * returns: * error mask * * notes: */ static errmask_t rw_base(FILE *file, struct base *bp) { struct rule *rp; fprintf(file, "\n"); /* global rules don't appear within a base */ if (bp->b_ident) fprintf(file, "BASE %s %s\n", noblanks(bp->b_src_spec), noblanks(bp->b_dst_spec)); for (rp = bp->b_includes; rp; rp = rp->r_next) if (rp->r_flags & R_PROGRAM) fprintf(file, "LIST !%s\n", rp->r_file); else fprintf(file, "LIST %s\n", noblanks(rp->r_file)); for (rp = bp->b_excludes; rp; rp = rp->r_next) if (rp->r_flags & R_PROGRAM) fprintf(file, "IGNORE !%s\n", rp->r_file); else fprintf(file, "IGNORE %s\n", noblanks(rp->r_file)); return (0); } /* * routine: * add_rule * * purpose: * to add a new rule * * parameters: * pointer to list base * rule flags * associated name/arguments * * returns: * error flags * * notes: * we always copy the argument string because most of them * were read from a file and are just in a transient buffer */ static errmask_t add_rule(struct base *bp, int flags, const char *args) { struct rule *rp; struct rule **list; rp = malloc(sizeof (struct rule)); if (rp == 0) nomem("rule struture"); /* initialize the new base */ memset((void *) rp, 0, sizeof (struct rule)); rp->r_flags = flags; rp->r_file = strdup(args); /* figure out which list to put it on */ if (flags&R_IGNORE) list = &bp->b_excludes; else if (flags&R_RESTRICT) list = &bp->b_restrictions; else list = &bp->b_includes; while (*list) list = &((*list)->r_next); *list = rp; if (flags & R_NEW) rules_added++; if (opt_debug & DBG_RULE) { fprintf(stderr, "RULE: base=%d, ", bp->b_ident); fprintf(stderr, "flags=%s, ", showflags(rflags, rp->r_flags)); fprintf(stderr, "arg=%s\n", rp->r_file); } return (0); } /* * routine: * add_ignore, add_include * * purpose: * wrappers for add_rule that permit outsiders (like main.c) * not to know what is inside of a base, file, or list entry * * parameters: * base under which rules should be added * argument associated with rule * * returns: * error flags * * notes: * basically these routines figure out what the right * flags are for a rule, and what list to put it on, * and then call a common handler. */ errmask_t add_ignore(struct base *bp, char *name) { int flags = R_IGNORE | R_NEW; if (bp == 0) bp = &omnibase; if (wildcards(name)) flags |= R_WILD; return (add_rule(bp, flags, name)); } errmask_t add_include(struct base *bp, char *name) { int flags = R_NEW; if (bp == 0) bp = &omnibase; if (wildcards(name)) flags |= R_WILD; bp->b_flags |= F_LISTED; return (add_rule(bp, flags, name)); } /* * routine: * add_restr * * purpose: * to add a restriction to a base * * parameters: * address of base * restriction string * * returns: * error mask * * notes: * a restriction is specified on the command line and * tells us to limit our analysis/reconcilation to * specified files and/or directories. We deal with * these by adding a restriction rule to any base that * looks like it might fit the restriction. We need to * treat this as a rule because the restriction string * may extend beyond the base directory and part-way into * its tree ... meaning that individual file names under * the base will have to be checked against the restriction. */ errmask_t add_restr(char *restr) { const char *s; errmask_t errs = 0; struct base *bp; for (bp = bases; bp; bp = bp->b_next) { /* * see if this restriction could apply to this base. * It could match either the source or destination * directory name for this base. If it matches neither * then the restriction does not apply to this base. */ s = prefix(restr, bp->b_src_name); if (s == 0) s = prefix(restr, bp->b_dst_name); if (s == 0) continue; /* * if there is more restriction string after the * base, we will need to note the remainder of the * string so that we can match individual files * against it. */ if (*s == '/') s++; errs |= add_rule(bp, R_RESTRICT, s); restr_added++; } return (errs); } /* * routine: * check_restr * * purpose: * to see if an argument falls within restrictions * * parameters: * pointer to relevant base * file name * * returns: * TRUE name is within restrictions * FALSE name is outside of restrictions * MAYBE name is on the path to a restriction * * notes: * if no restrictions have been specified, we evaluate * everything. If any restrictions have been specified, * we process only files that match one of the restrictions. * * add_restr has ensured that if the restriction includes * a portion that must be matched by individual files under * the base, that the restriction rule will contain that * portion of the restriction which must be matched against * individual file names. */ bool_t check_restr(struct base *bp, const char *name) { struct rule *rp; /* if there are no restrictions, everything is OK */ if (restr_added == 0) return (TRUE); /* now we have to run through the list */ for (rp = bp->b_restrictions; rp; rp = rp->r_next) { /* see if current path is under the restriction */ if (prefix(name, rp->r_file)) return (TRUE); /* see if current path is on the way to restr */ if (prefix(rp->r_file, name)) /* * this is kinky, but walker really needs * to know the difference between a directory * that we are unreservedly scanning, and one * that we are scanning only to find something * beneath it. */ return (MAYBE); } /* * there are restrictions in effect and this file doesn't seem * to meet any of them */ if (opt_debug & DBG_RULE) fprintf(stderr, "RULE: FAIL RESTRICTION base=%d, file=%s\n", bp->b_ident, name); return (FALSE); }