Implement the "m", "o", "g" include modifiers to tweak the permissions, wip/filter-attrs
authorMatt McCutchen <matt@mattmccutchen.net>
Tue, 5 May 2009 03:02:43 +0000 (23:02 -0400)
committerMatt McCutchen <matt@mattmccutchen.net>
Tue, 5 May 2009 03:02:43 +0000 (23:02 -0400)
owner, or group of matching files.

exclude.c
flist.c
rsync.h
rsync.yo
util.c

index 9a7e0da..19de82d 100644 (file)
--- a/exclude.c
+++ b/exclude.c
@@ -44,12 +44,11 @@ struct filter_list_struct filter_list = { .debug_type = "" };
 struct filter_list_struct cvs_filter_list = { .debug_type = " [global CVS]" };
 struct filter_list_struct daemon_filter_list = { .debug_type = " [daemon]" };
 
-/* Need room enough for ":MODS " prefix plus some room to grow. */
-#define MAX_RULE_PREFIX (16)
+struct filter_struct *last_hit_filter;
 
-#define MODIFIERS_MERGE_FILE "-+Cenw"
-#define MODIFIERS_INCL_EXCL "/!Crsp"
-#define MODIFIERS_HIDE_PROTECT "/!p"
+/* Need room enough for ":MODS " prefix, which can now include
+ * chmod/user/group values. */
+#define MAX_RULE_PREFIX (256)
 
 #define SLASH_WILD3_SUFFIX "/***"
 
@@ -122,8 +121,27 @@ static void teardown_mergelist(struct filter_struct *ex)
        mergelist_cnt--;
 }
 
+static struct filter_chmod_struct *ref_filter_chmod(struct filter_chmod_struct *chmod)
+{
+       chmod->ref_cnt++;
+       assert(chmod->ref_cnt != 0); /* Catch overflow. */
+       return chmod;
+}
+
+static void unref_filter_chmod(struct filter_chmod_struct *chmod)
+{
+       chmod->ref_cnt--;
+       if (chmod->ref_cnt == 0) {
+               free(chmod->modestr);
+               free_chmod_mode(chmod->modes);
+               free(chmod);
+       }
+}
+
 static void free_filter(struct filter_struct *ex)
 {
+       if (ex->flags & MATCHFLG_CHMOD)
+               unref_filter_chmod(ex->chmod);
        free(ex->pattern);
        free(ex);
 }
@@ -732,6 +750,7 @@ static void report_filter_result(enum logcode code, char const *name,
 /*
  * Return -1 if file "name" is defined to be excluded by the specified
  * exclude list, 1 if it is included, and 0 if it was not matched.
+ * Sets last_hit_filter to the filter that was hit, or NULL if none.
  */
 int check_filter(struct filter_list_struct *listp, enum logcode code,
                 const char *name, int name_is_dir)
@@ -758,10 +777,12 @@ int check_filter(struct filter_list_struct *listp, enum logcode code,
                if (rule_matches(name, ent, name_is_dir)) {
                        report_filter_result(code, name, ent, name_is_dir,
                                             listp->debug_type);
+                       last_hit_filter = ent;
                        return ent->flags & MATCHFLG_INCLUDE ? 1 : -1;
                }
        }
 
+       last_hit_filter = NULL;
        return 0;
 }
 
@@ -778,9 +799,46 @@ static const uchar *rule_strcmp(const uchar *str, const char *rule, int rule_len
        return NULL;
 }
 
+static char *grab_paren_value(const uchar **s_ptr)
+{
+       const uchar *start, *end;
+       int val_sz;
+       char *val;
+
+       if ((*s_ptr)[1] != '(')
+               return NULL;
+       start = (*s_ptr) + 2;
+
+       for (end = start; *end != ')'; end++)
+               if (!*end || *end == ' ' || *end == '_')
+                       return NULL;
+
+       val_sz = end - start + 1;
+       val = new_array(char, val_sz);
+       strlcpy(val, (const char *)start, val_sz);
+       *s_ptr = end; /* remember ++s in parse_rule_tok */
+       return val;
+}
+
+static struct filter_chmod_struct *make_chmod_struct(char *modestr)
+{
+       struct filter_chmod_struct *chmod;
+       struct chmod_mode_struct *modes = NULL;
+
+       if (!parse_chmod(modestr, &modes))
+               return NULL;
+
+       if (!(chmod = new(struct filter_chmod_struct)))
+               out_of_memory("make_chmod_struct");
+       chmod->ref_cnt = 1;
+       chmod->modestr = modestr;
+       chmod->modes = modes;
+       return chmod;
+}
+
 #define MATCHFLGS_FROM_CONTAINER (MATCHFLG_ABS_PATH | MATCHFLG_INCLUDE \
                                | MATCHFLG_DIRECTORY | MATCHFLG_NEGATE \
-                               | MATCHFLG_PERISHABLE)
+                               | MATCHFLG_PERISHABLE | MATCHFLGS_ATTRS)
 
 /* Gets the next include/exclude rule from *rulestr_ptr and advances
  * *rulestr_ptr to point beyond it.  Stores the pattern's start (within
@@ -795,6 +853,7 @@ static struct filter_struct *parse_rule_tok(const char **rulestr_ptr,
                                            const char **pat_ptr, unsigned int *pat_len_ptr)
 {
        const uchar *s = (const uchar *)*rulestr_ptr;
+       char *val;
        struct filter_struct *filter;
        unsigned int len;
 
@@ -814,6 +873,12 @@ static struct filter_struct *parse_rule_tok(const char **rulestr_ptr,
        /* Inherit from the template.  Don't inherit MATCHFLGS_SIDES; we check
         * that later. */
        filter->flags = template->flags & MATCHFLGS_FROM_CONTAINER;
+       if (template->flags & MATCHFLG_CHMOD)
+               filter->chmod = ref_filter_chmod(template->chmod);
+       if (template->flags & MATCHFLG_FORCE_OWNER)
+               filter->force_uid = template->force_uid;
+       if (template->flags & MATCHFLG_FORCE_GROUP)
+               filter->force_gid = template->force_gid;
 
        /* Figure out what kind of a filter rule "s" is pointing at.  Note
         * that if MATCHFLG_NO_PREFIXES is set, the rule is either an include
@@ -959,11 +1024,63 @@ static struct filter_struct *parse_rule_tok(const char **rulestr_ptr,
                                        goto invalid;
                                filter->flags |= MATCHFLG_EXCLUDE_SELF;
                                break;
+                       case 'g': {
+                               gid_t gid;
+
+                               if (!(val = grab_paren_value(&s)))
+                                       goto invalid;
+                               if (name_to_gid(val, &gid, True)) {
+                                       filter->flags |= MATCHFLG_FORCE_GROUP;
+                                       filter->force_gid = gid;
+                               } else {
+                                       rprintf(FERROR,
+                                               "unknown group '%s' in filter rule: %s\n",
+                                               val, *rulestr_ptr);
+                                       exit_cleanup(RERR_SYNTAX);
+                               }
+                               free(val);
+                               break;
+                       }
+                       case 'm': {
+                               struct filter_chmod_struct *chmod;
+
+                               if (!(val = grab_paren_value(&s)))
+                                       goto invalid;
+                               if ((chmod = make_chmod_struct(val))) {
+                                       if (filter->flags & MATCHFLG_CHMOD)
+                                               unref_filter_chmod(filter->chmod);
+                                       filter->flags |= MATCHFLG_CHMOD;
+                                       filter->chmod = chmod;
+                               } else {
+                                       rprintf(FERROR,
+                                               "unparseable chmod string '%s' in filter rule: %s\n",
+                                               val, *rulestr_ptr);
+                                       exit_cleanup(RERR_SYNTAX);
+                               }
+                               break;
+                       }
                        case 'n':
                                if (!(filter->flags & MATCHFLG_MERGE_FILE))
                                        goto invalid;
                                filter->flags |= MATCHFLG_NO_INHERIT;
                                break;
+                       case 'o': {
+                               uid_t uid;
+
+                               if (!(val = grab_paren_value(&s)))
+                                       goto invalid;
+                               if (name_to_uid(val, &uid, True)) {
+                                       filter->flags |= MATCHFLG_FORCE_OWNER;
+                                       filter->force_uid = uid;
+                               } else {
+                                       rprintf(FERROR,
+                                               "unknown user '%s' in filter rule: %s\n",
+                                               val, *rulestr_ptr);
+                                       exit_cleanup(RERR_SYNTAX);
+                               }
+                               free(val);
+                               break;
+                       }
                        case 'p':
                                filter->flags |= MATCHFLG_PERISHABLE;
                                break;
@@ -1296,6 +1413,23 @@ char *get_rule_prefix(struct filter_struct *filter, const char *pat, int for_xfe
                else if (am_sender)
                        return NULL;
        }
+       if (filter->flags & MATCHFLGS_ATTRS) {
+               if (!for_xfer || protocol_version >= 31) {
+                       if (filter->flags & MATCHFLG_CHMOD)
+                               if (!snappendf(&op, (buf + sizeof buf) - op,
+                                       "m(%s)", filter->chmod->modestr))
+                                       return NULL;
+                       if (filter->flags & MATCHFLG_FORCE_OWNER)
+                               if (!snappendf(&op, (buf + sizeof buf) - op,
+                                       "o(%u)", (unsigned)filter->force_uid))
+                                       return NULL;
+                       if (filter->flags & MATCHFLG_FORCE_GROUP)
+                               if (!snappendf(&op, (buf + sizeof buf) - op,
+                                       "g(%u)", (unsigned)filter->force_gid))
+                                       return NULL;
+               } else if (!am_sender)
+                       return NULL;
+       }
        if (op - buf > legal_len)
                return NULL;
        if (legal_len)
diff --git a/flist.c b/flist.c
index 2af7e88..0b3ebab 100644 (file)
--- a/flist.c
+++ b/flist.c
@@ -81,6 +81,7 @@ extern struct chmod_mode_struct *chmod_modes;
 
 extern struct filter_list_struct filter_list;
 extern struct filter_list_struct daemon_filter_list;
+extern struct filter_struct *last_hit_filter;
 
 #ifdef ICONV_OPTION
 extern int filesfrom_convert;
@@ -274,7 +275,8 @@ static inline int path_is_daemon_excluded(char *path, int ignore_filename)
 
 /* This function is used to check if a file should be included/excluded
  * from the list of files based on its name and type etc.  The value of
- * filter_level is set to either SERVER_FILTERS or ALL_FILTERS. */
+ * filter_level is set to either SERVER_FILTERS or ALL_FILTERS.
+ * "last_hit_filter" will be set to the operative filter, or NULL if none. */
 static int is_excluded(const char *fname, int is_dir, int filter_level)
 {
 #if 0 /* This currently never happens, so avoid a useless compare. */
@@ -283,6 +285,8 @@ static int is_excluded(const char *fname, int is_dir, int filter_level)
 #endif
        if (is_daemon_excluded(fname, is_dir))
                return 1;
+       /* Don't leave a daemon include in last_hit_filter. */
+       last_hit_filter = NULL;
        if (filter_level != ALL_FILTERS)
                return 0;
        if (filter_list.head
@@ -1142,7 +1146,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
        } else if (readlink_stat(thisname, &st, linkname) != 0) {
                int save_errno = errno;
                /* See if file is excluded before reporting an error. */
-               if (filter_level != NO_FILTERS
+               if (filter_level != NO_FILTERS && filter_level != ALL_FILTERS_NO_EXCLUDE
                 && (is_excluded(thisname, 0, filter_level)
                  || is_excluded(thisname, 1, filter_level))) {
                        if (ignore_perishable && save_errno != ENOENT)
@@ -1187,6 +1191,12 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
 
        if (filter_level == NO_FILTERS)
                goto skip_filters;
+       if (filter_level == ALL_FILTERS_NO_EXCLUDE) {
+               /* Call only for the side effect of setting last_hit_filter to
+                * any operative include filter, which might affect attributes. */
+               is_excluded(thisname, S_ISDIR(st.st_mode) != 0, ALL_FILTERS);
+               goto skip_filters;
+       }
 
        if (S_ISDIR(st.st_mode)) {
                if (!xfer_dirs) {
@@ -1377,12 +1387,23 @@ static struct file_struct *send_file_name(int f, struct file_list *flist,
                                          int flags, int filter_level)
 {
        struct file_struct *file;
+       BOOL can_tweak_mode;
 
        file = make_file(fname, flist, stp, flags, filter_level);
        if (!file)
                return NULL;
 
-       if (chmod_modes && !S_ISLNK(file->mode) && file->mode)
+       can_tweak_mode = !S_ISLNK(file->mode) && file->mode;
+       if ((filter_level == ALL_FILTERS || filter_level == ALL_FILTERS_NO_EXCLUDE)
+               && last_hit_filter) {
+               if ((last_hit_filter->flags & MATCHFLG_CHMOD) && can_tweak_mode)
+                       file->mode = tweak_mode(file->mode, last_hit_filter->chmod->modes);
+               if ((last_hit_filter->flags & MATCHFLG_FORCE_OWNER) && uid_ndx)
+                       F_OWNER(file) = last_hit_filter->force_uid;
+               if ((last_hit_filter->flags & MATCHFLG_FORCE_GROUP) && gid_ndx)
+                       F_GROUP(file) = last_hit_filter->force_gid;
+       }
+       if (chmod_modes && can_tweak_mode)
                file->mode = tweak_mode(file->mode, chmod_modes);
 
        if (f >= 0) {
@@ -2239,7 +2260,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[])
                        struct file_struct *file;
                        file = send_file_name(f, flist, fbuf, &st,
                                              FLAG_TOP_DIR | FLAG_CONTENT_DIR | flags,
-                                             NO_FILTERS);
+                                             ALL_FILTERS_NO_EXCLUDE);
                        if (!file)
                                continue;
                        if (inc_recurse) {
@@ -2253,7 +2274,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[])
                        } else
                                send_if_directory(f, flist, file, fbuf, len, flags);
                } else
-                       send_file_name(f, flist, fbuf, &st, flags, NO_FILTERS);
+                       send_file_name(f, flist, fbuf, &st, flags, ALL_FILTERS_NO_EXCLUDE);
        }
 
        gettimeofday(&end_tv, NULL);
diff --git a/rsync.h b/rsync.h
index b85c0bb..d89aea5 100644 (file)
--- a/rsync.h
+++ b/rsync.h
 #define NO_FILTERS     0
 #define SERVER_FILTERS 1
 #define ALL_FILTERS    2
+/* Don't let the file be excluded, but check for a filter that might affect
+ * its attributes via MATCHFLGS_ATTRS. */
+#define ALL_FILTERS_NO_EXCLUDE 3
 
 #define XFLG_FATAL_ERRORS      (1<<0)
 #define XFLG_OLD_PREFIXES      (1<<1)
@@ -798,6 +801,8 @@ struct map_struct {
        int status;             /* first errno from read errors         */
 };
 
+struct chmod_mode_struct;
+
 #define MATCHFLG_WILD          (1<<0) /* pattern has '*', '[', and/or '?' */
 #define MATCHFLG_WILD2         (1<<1) /* pattern has '**' */
 #define MATCHFLG_WILD2_PREFIX  (1<<2) /* pattern starts with "**" */
@@ -818,8 +823,18 @@ struct map_struct {
 #define MATCHFLG_RECEIVER_SIDE (1<<17)/* rule applies to the receiving side */
 #define MATCHFLG_CLEAR_LIST    (1<<18)/* this item is the "!" token */
 #define MATCHFLG_PERISHABLE    (1<<19)/* perishable if parent dir goes away */
+#define MATCHFLG_CHMOD         (1<<20)/* chmod-tweak matching files */
+#define MATCHFLG_FORCE_OWNER   (1<<21)/* force owner of matching files */
+#define MATCHFLG_FORCE_GROUP   (1<<22)/* force group of matching files */
 
 #define MATCHFLGS_SIDES (MATCHFLG_SENDER_SIDE | MATCHFLG_RECEIVER_SIDE)
+#define MATCHFLGS_ATTRS (MATCHFLG_CHMOD | MATCHFLG_FORCE_OWNER | MATCHFLG_FORCE_GROUP)
+
+struct filter_chmod_struct {
+       unsigned int ref_cnt;
+       char *modestr;
+       struct chmod_mode_struct *modes;
+};
 
 struct filter_struct {
        struct filter_struct *next;
@@ -829,6 +844,11 @@ struct filter_struct {
                int slash_cnt;
                struct filter_list_struct *mergelist;
        } u;
+       /* TODO: Use an "extras" mechanism to avoid
+        * allocating this memory when we don't need it. */
+       struct filter_chmod_struct *chmod;
+       uid_t force_uid;
+       gid_t force_gid;
 };
 
 struct filter_list_struct {
index 60c6ea8..5fbe868 100644 (file)
--- a/rsync.yo
+++ b/rsync.yo
@@ -1028,6 +1028,8 @@ quote(--chmod=Dg+s,ug+w,Fo-w,+X)
 
 It is also legal to specify multiple bf(--chmod) options, as each
 additional option is just appended to the list of changes to make.
+To change permissions of files matching a pattern, use an include filter with
+the bf(m) modifier, which takes effect before any bf(--chmod) options.
 
 See the bf(--perms) and bf(--executability) options for how the resulting
 permission value can be applied to the files in the transfer.
@@ -1808,6 +1810,10 @@ be omitted, but if USER is empty, a leading colon must be supplied.
 If you specify "--chown=foo:bar, this is exactly the same as specifying
 "--usermap=*:foo --groupmap=*:bar", only easier.
 
+To change ownership of files matching a pattern, use an include filter with
+the bf(o) and bf(g) modifiers, which take effect before uid/gid mapping and
+therefore em(can) be mixed with bf(--usermap) and bf(--groupmap).
+
 dit(bf(--timeout=TIMEOUT)) This option allows you to set a maximum I/O
 timeout in seconds. If no data is transferred for the specified time
 then rsync will exit. The default is 0, which means no timeout.
@@ -2620,6 +2626,15 @@ itemization(
   option's default rules that exclude things like "CVS" and "*.o" are
   marked as perishable, and will not prevent a directory that was removed
   on the source from being deleted on the destination.
+  it() An bf(m+nop()(CHMOD)) on an include rule tweaks the permissions of matching
+  source files in the same way as bf(--chmod).  This happens before any
+  tweaks requested via bf(--chmod) options.
+  it() An bf(o+nop()(USER)) on an include rule pretends that matching source files
+  are owned by bf(USER) (a name or numeric uid).  This happens before any uid
+  mapping by name or bf(--usermap).
+  it() A bf(g+nop()(GROUP)) on an include rule pretends that matching source files
+  are owned by bf(GROUP) (a name or numeric gid).  This happens before any gid
+  mapping by name or bf(--groupmap).
 )
 
 manpagesection(MERGE-FILE FILTER RULES)
@@ -2681,6 +2696,12 @@ itemization(
   a rule prefix such as bf(hide)).
 )
 
+The attribute-affecting modifiers bf(m), bf(o), and bf(g) work only in client
+filters (not in daemon filters), and only the modifiers of the first matching
+rule are applied.  As an example, assuming bf(--super) is enabled, the
+rule "+o+nop()(root)g+nop()(root)m+nop()(go=) *~" would ensure that all "backup" files belong to
+root and are not accessible to anyone else.
+
 Per-directory rules are inherited in all subdirectories of the directory
 where the merge-file was found unless the 'n' modifier was used.  Each
 subdirectory's rules are prefixed to the inherited per-directory rules
diff --git a/util.c b/util.c
index bc01afd..27fd44f 100644 (file)
--- a/util.c
+++ b/util.c
@@ -840,6 +840,25 @@ size_t stringjoin(char *dest, size_t destsize, ...)
        return ret;
 }
 
+/* Append formatted text at *dest_ptr up to a maximum of sz (like snprintf).
+ * On success, advance *dest_ptr and return True; on overflow, return False. */
+BOOL snappendf(char **dest_ptr, size_t sz, const char *format, ...)
+{
+       va_list ap;
+       size_t len;
+       
+       va_start(ap, format);
+       len = vsnprintf(*dest_ptr, sz, format, ap);
+       va_end(ap);
+
+       if (len >= sz)
+               return False;
+       else {
+               *dest_ptr += len;
+               return True;
+       }
+}
+
 int count_dir_elements(const char *p)
 {
        int cnt = 0, new_component = 1;