Updated for the latest source.
[rsync.git/patches.git] / filter-attribute-mods.diff
1 From: Matt McCutchen <matt@mattmccutchen.net>
2
3 Implement the "m", "o", "g" include modifiers to tweak the permissions,
4 owner, or group of matching files.
5
6 To use this patch, run these commands for a successful build:
7
8     patch -p1 <patches/filter-attribute-mods.diff
9     ./configure                         (optional if already run)
10     make
11
12 based-on: 70c6b408dc299f7aa00dd3452ae82b56d6c17f80
13 diff --git a/exclude.c b/exclude.c
14 --- a/exclude.c
15 +++ b/exclude.c
16 @@ -44,10 +44,13 @@ filter_rule_list filter_list = { .debug_type = "" };
17  filter_rule_list cvs_filter_list = { .debug_type = " [global CVS]" };
18  filter_rule_list daemon_filter_list = { .debug_type = " [daemon]" };
19  
20 +filter_rule *last_hit_filter_rule;
21 +
22  int saw_xattr_filter = 0;
23  
24 -/* Need room enough for ":MODS " prefix plus some room to grow. */
25 -#define MAX_RULE_PREFIX (16)
26 +/* Need room enough for ":MODS " prefix, which can now include
27 + * chmod/user/group values. */
28 +#define MAX_RULE_PREFIX (256)
29  
30  #define SLASH_WILD3_SUFFIX "/***"
31  
32 @@ -126,8 +129,27 @@ static void teardown_mergelist(filter_rule *ex)
33                 mergelist_cnt--;
34  }
35  
36 +static struct filter_chmod_struct *ref_filter_chmod(struct filter_chmod_struct *chmod)
37 +{
38 +       chmod->ref_cnt++;
39 +       assert(chmod->ref_cnt != 0); /* Catch overflow. */
40 +       return chmod;
41 +}
42 +
43 +static void unref_filter_chmod(struct filter_chmod_struct *chmod)
44 +{
45 +       chmod->ref_cnt--;
46 +       if (chmod->ref_cnt == 0) {
47 +               free(chmod->modestr);
48 +               free_chmod_mode(chmod->modes);
49 +               free(chmod);
50 +       }
51 +}
52 +
53  static void free_filter(filter_rule *ex)
54  {
55 +       if (ex->rflags & FILTRULE_CHMOD)
56 +               unref_filter_chmod(ex->chmod);
57         if (ex->rflags & FILTRULE_PERDIR_MERGE)
58                 teardown_mergelist(ex);
59         free(ex->pattern);
60 @@ -729,7 +751,9 @@ static void report_filter_result(enum logcode code, char const *name,
61  
62  /* This function is used to check if a file should be included/excluded
63   * from the list of files based on its name and type etc.  The value of
64 - * filter_level is set to either SERVER_FILTERS or ALL_FILTERS. */
65 + * filter_level is set to either SERVER_FILTERS or ALL_FILTERS.
66 + * "last_hit_filter_rule" will be set to the operative filter, or NULL if none. */
67 +
68  int name_is_excluded(const char *fname, int name_flags, int filter_level)
69  {
70         if (daemon_filter_list.head && check_filter(&daemon_filter_list, FLOG, fname, name_flags) < 0) {
71 @@ -738,6 +762,9 @@ int name_is_excluded(const char *fname, int name_flags, int filter_level)
72                 return 1;
73         }
74  
75 +       /* Don't leave a daemon include in last_hit_filter_rule. */
76 +       last_hit_filter_rule = NULL;
77 +
78         if (filter_level != ALL_FILTERS)
79                 return 0;
80  
81 @@ -748,7 +775,8 @@ int name_is_excluded(const char *fname, int name_flags, int filter_level)
82  }
83  
84  /* Return -1 if file "name" is defined to be excluded by the specified
85 - * exclude list, 1 if it is included, and 0 if it was not matched. */
86 + * exclude list, 1 if it is included, and 0 if it was not matched.
87 + * Sets last_hit_filter_rule to the filter that was hit, or NULL if none. */
88  int check_filter(filter_rule_list *listp, enum logcode code,
89                  const char *name, int name_flags)
90  {
91 @@ -771,10 +799,12 @@ int check_filter(filter_rule_list *listp, enum logcode code,
92                 }
93                 if (rule_matches(name, ent, name_flags)) {
94                         report_filter_result(code, name, ent, name_flags, listp->debug_type);
95 +                       last_hit_filter_rule = ent;
96                         return ent->rflags & FILTRULE_INCLUDE ? 1 : -1;
97                 }
98         }
99  
100 +       last_hit_filter_rule = NULL;
101         return 0;
102  }
103  
104 @@ -791,9 +821,46 @@ static const uchar *rule_strcmp(const uchar *str, const char *rule, int rule_len
105         return NULL;
106  }
107  
108 +static char *grab_paren_value(const uchar **s_ptr)
109 +{
110 +       const uchar *start, *end;
111 +       int val_sz;
112 +       char *val;
113 +
114 +       if ((*s_ptr)[1] != '(')
115 +               return NULL;
116 +       start = (*s_ptr) + 2;
117 +
118 +       for (end = start; *end != ')'; end++)
119 +               if (!*end || *end == ' ' || *end == '_')
120 +                       return NULL;
121 +
122 +       val_sz = end - start + 1;
123 +       val = new_array(char, val_sz);
124 +       strlcpy(val, (const char *)start, val_sz);
125 +       *s_ptr = end; /* remember ++s in parse_rule_tok */
126 +       return val;
127 +}
128 +
129 +static struct filter_chmod_struct *make_chmod_struct(char *modestr)
130 +{
131 +       struct filter_chmod_struct *chmod;
132 +       struct chmod_mode_struct *modes = NULL;
133 +
134 +       if (!parse_chmod(modestr, &modes))
135 +               return NULL;
136 +
137 +       if (!(chmod = new(struct filter_chmod_struct)))
138 +               out_of_memory("make_chmod_struct");
139 +       chmod->ref_cnt = 1;
140 +       chmod->modestr = modestr;
141 +       chmod->modes = modes;
142 +       return chmod;
143 +}
144 +
145  #define FILTRULES_FROM_CONTAINER (FILTRULE_ABS_PATH | FILTRULE_INCLUDE \
146                                 | FILTRULE_DIRECTORY | FILTRULE_NEGATE \
147 -                               | FILTRULE_PERISHABLE)
148 +                               | FILTRULE_PERISHABLE | FILTRULES_ATTRS)
149  
150  /* Gets the next include/exclude rule from *rulestr_ptr and advances
151   * *rulestr_ptr to point beyond it.  Stores the pattern's start (within
152 @@ -808,6 +875,7 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
153                                    const char **pat_ptr, unsigned int *pat_len_ptr)
154  {
155         const uchar *s = (const uchar *)*rulestr_ptr;
156 +       char *val;
157         filter_rule *rule;
158         unsigned int len;
159  
160 @@ -827,6 +895,12 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
161         /* Inherit from the template.  Don't inherit FILTRULES_SIDES; we check
162          * that later. */
163         rule->rflags = template->rflags & FILTRULES_FROM_CONTAINER;
164 +       if (template->rflags & FILTRULE_CHMOD)
165 +               rule->chmod = ref_filter_chmod(template->chmod);
166 +       if (template->rflags & FILTRULE_FORCE_OWNER)
167 +               rule->force_uid = template->force_uid;
168 +       if (template->rflags & FILTRULE_FORCE_GROUP)
169 +               rule->force_gid = template->force_gid;
170  
171         /* Figure out what kind of a filter rule "s" is pointing at.  Note
172          * that if FILTRULE_NO_PREFIXES is set, the rule is either an include
173 @@ -972,11 +1046,63 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
174                                         goto invalid;
175                                 rule->rflags |= FILTRULE_EXCLUDE_SELF;
176                                 break;
177 +                       case 'g': {
178 +                               gid_t gid;
179 +
180 +                               if (!(val = grab_paren_value(&s)))
181 +                                       goto invalid;
182 +                               if (group_to_gid(val, &gid, True)) {
183 +                                       rule->rflags |= FILTRULE_FORCE_GROUP;
184 +                                       rule->force_gid = gid;
185 +                               } else {
186 +                                       rprintf(FERROR,
187 +                                               "unknown group '%s' in filter rule: %s\n",
188 +                                               val, *rulestr_ptr);
189 +                                       exit_cleanup(RERR_SYNTAX);
190 +                               }
191 +                               free(val);
192 +                               break;
193 +                       }
194 +                       case 'm': {
195 +                               struct filter_chmod_struct *chmod;
196 +
197 +                               if (!(val = grab_paren_value(&s)))
198 +                                       goto invalid;
199 +                               if ((chmod = make_chmod_struct(val))) {
200 +                                       if (rule->rflags & FILTRULE_CHMOD)
201 +                                               unref_filter_chmod(rule->chmod);
202 +                                       rule->rflags |= FILTRULE_CHMOD;
203 +                                       rule->chmod = chmod;
204 +                               } else {
205 +                                       rprintf(FERROR,
206 +                                               "unparseable chmod string '%s' in filter rule: %s\n",
207 +                                               val, *rulestr_ptr);
208 +                                       exit_cleanup(RERR_SYNTAX);
209 +                               }
210 +                               break;
211 +                       }
212                         case 'n':
213                                 if (!(rule->rflags & FILTRULE_MERGE_FILE))
214                                         goto invalid;
215                                 rule->rflags |= FILTRULE_NO_INHERIT;
216                                 break;
217 +                       case 'o': {
218 +                               uid_t uid;
219 +
220 +                               if (!(val = grab_paren_value(&s)))
221 +                                       goto invalid;
222 +                               if (user_to_uid(val, &uid, True)) {
223 +                                       rule->rflags |= FILTRULE_FORCE_OWNER;
224 +                                       rule->force_uid = uid;
225 +                               } else {
226 +                                       rprintf(FERROR,
227 +                                               "unknown user '%s' in filter rule: %s\n",
228 +                                               val, *rulestr_ptr);
229 +                                       exit_cleanup(RERR_SYNTAX);
230 +                               }
231 +                               free(val);
232 +                               break;
233 +                       }
234                         case 'p':
235                                 rule->rflags |= FILTRULE_PERISHABLE;
236                                 break;
237 @@ -1301,6 +1427,23 @@ char *get_rule_prefix(filter_rule *rule, const char *pat, int for_xfer,
238                 else if (am_sender)
239                         return NULL;
240         }
241 +       if (rule->rflags & FILTRULES_ATTRS) {
242 +               if (!for_xfer || protocol_version >= 31) {
243 +                       if (rule->rflags & FILTRULE_CHMOD)
244 +                               if (!snappendf(&op, (buf + sizeof buf) - op,
245 +                                       "m(%s)", rule->chmod->modestr))
246 +                                       return NULL;
247 +                       if (rule->rflags & FILTRULE_FORCE_OWNER)
248 +                               if (!snappendf(&op, (buf + sizeof buf) - op,
249 +                                       "o(%u)", (unsigned)rule->force_uid))
250 +                                       return NULL;
251 +                       if (rule->rflags & FILTRULE_FORCE_GROUP)
252 +                               if (!snappendf(&op, (buf + sizeof buf) - op,
253 +                                       "g(%u)", (unsigned)rule->force_gid))
254 +                                       return NULL;
255 +               } else if (!am_sender)
256 +                       return NULL;
257 +       }
258         if (op - buf > legal_len)
259                 return NULL;
260         if (legal_len)
261 diff --git a/flist.c b/flist.c
262 --- a/flist.c
263 +++ b/flist.c
264 @@ -83,6 +83,7 @@ extern struct chmod_mode_struct *chmod_modes;
265  
266  extern filter_rule_list filter_list;
267  extern filter_rule_list daemon_filter_list;
268 +extern filter_rule *last_hit_filter_rule;
269  
270  #ifdef ICONV_OPTION
271  extern int filesfrom_convert;
272 @@ -1183,7 +1184,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
273         } else if (readlink_stat(thisname, &st, linkname) != 0) {
274                 int save_errno = errno;
275                 /* See if file is excluded before reporting an error. */
276 -               if (filter_level != NO_FILTERS
277 +               if (filter_level != NO_FILTERS && filter_level != ALL_FILTERS_NO_EXCLUDE
278                  && (is_excluded(thisname, 0, filter_level)
279                   || is_excluded(thisname, 1, filter_level))) {
280                         if (ignore_perishable && save_errno != ENOENT)
281 @@ -1228,6 +1229,12 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
282  
283         if (filter_level == NO_FILTERS)
284                 goto skip_filters;
285 +       if (filter_level == ALL_FILTERS_NO_EXCLUDE) {
286 +               /* Call only for the side effect of setting last_hit_filter_rule to
287 +                * any operative include filter, which might affect attributes. */
288 +               is_excluded(thisname, S_ISDIR(st.st_mode) != 0, ALL_FILTERS);
289 +               goto skip_filters;
290 +       }
291  
292         if (S_ISDIR(st.st_mode)) {
293                 if (!xfer_dirs) {
294 @@ -1432,12 +1439,23 @@ static struct file_struct *send_file_name(int f, struct file_list *flist,
295                                           int flags, int filter_level)
296  {
297         struct file_struct *file;
298 +       BOOL can_tweak_mode;
299  
300         file = make_file(fname, flist, stp, flags, filter_level);
301         if (!file)
302                 return NULL;
303  
304 -       if (chmod_modes && !S_ISLNK(file->mode) && file->mode)
305 +       can_tweak_mode = !S_ISLNK(file->mode) && file->mode;
306 +       if ((filter_level == ALL_FILTERS || filter_level == ALL_FILTERS_NO_EXCLUDE)
307 +               && last_hit_filter_rule) {
308 +               if ((last_hit_filter_rule->rflags & FILTRULE_CHMOD) && can_tweak_mode)
309 +                       file->mode = tweak_mode(file->mode, last_hit_filter_rule->chmod->modes);
310 +               if ((last_hit_filter_rule->rflags & FILTRULE_FORCE_OWNER) && uid_ndx)
311 +                       F_OWNER(file) = last_hit_filter_rule->force_uid;
312 +               if ((last_hit_filter_rule->rflags & FILTRULE_FORCE_GROUP) && gid_ndx)
313 +                       F_GROUP(file) = last_hit_filter_rule->force_gid;
314 +       }
315 +       if (chmod_modes && can_tweak_mode)
316                 file->mode = tweak_mode(file->mode, chmod_modes);
317  
318         if (f >= 0) {
319 @@ -2342,7 +2360,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[])
320                         struct file_struct *file;
321                         file = send_file_name(f, flist, fbuf, &st,
322                                               FLAG_TOP_DIR | FLAG_CONTENT_DIR | flags,
323 -                                             NO_FILTERS);
324 +                                             ALL_FILTERS_NO_EXCLUDE);
325                         if (!file)
326                                 continue;
327                         if (inc_recurse) {
328 @@ -2356,7 +2374,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[])
329                         } else
330                                 send_if_directory(f, flist, file, fbuf, len, flags);
331                 } else
332 -                       send_file_name(f, flist, fbuf, &st, flags, NO_FILTERS);
333 +                       send_file_name(f, flist, fbuf, &st, flags, ALL_FILTERS_NO_EXCLUDE);
334         }
335  
336         if (reenable_multiplex >= 0)
337 diff --git a/rsync.h b/rsync.h
338 --- a/rsync.h
339 +++ b/rsync.h
340 @@ -166,6 +166,9 @@
341  #define NO_FILTERS     0
342  #define SERVER_FILTERS 1
343  #define ALL_FILTERS    2
344 +/* Don't let the file be excluded, but check for a filter that might affect
345 + * its attributes via FILTRULES_ATTRS. */
346 +#define ALL_FILTERS_NO_EXCLUDE 3
347  
348  #define XFLG_FATAL_ERRORS      (1<<0)
349  #define XFLG_OLD_PREFIXES      (1<<1)
350 @@ -910,6 +913,8 @@ struct map_struct {
351         int status;             /* first errno from read errors         */
352  };
353  
354 +struct chmod_mode_struct;
355 +
356  #define NAME_IS_FILE           (0)    /* filter name as a file */
357  #define NAME_IS_DIR            (1<<0) /* filter name as a dir */
358  #define NAME_IS_XATTR          (1<<2) /* filter name as an xattr */
359 @@ -935,8 +940,18 @@ struct map_struct {
360  #define FILTRULE_CLEAR_LIST    (1<<18)/* this item is the "!" token */
361  #define FILTRULE_PERISHABLE    (1<<19)/* perishable if parent dir goes away */
362  #define FILTRULE_XATTR         (1<<20)/* rule only applies to xattr names */
363 +#define FILTRULE_CHMOD         (1<<21)/* chmod-tweak matching files */
364 +#define FILTRULE_FORCE_OWNER   (1<<22)/* force owner of matching files */
365 +#define FILTRULE_FORCE_GROUP   (1<<23)/* force group of matching files */
366  
367  #define FILTRULES_SIDES (FILTRULE_SENDER_SIDE | FILTRULE_RECEIVER_SIDE)
368 +#define FILTRULES_ATTRS (FILTRULE_CHMOD | FILTRULE_FORCE_OWNER | FILTRULE_FORCE_GROUP)
369 +
370 +struct filter_chmod_struct {
371 +       unsigned int ref_cnt;
372 +       char *modestr;
373 +       struct chmod_mode_struct *modes;
374 +};
375  
376  typedef struct filter_struct {
377         struct filter_struct *next;
378 @@ -946,6 +961,11 @@ typedef struct filter_struct {
379                 int slash_cnt;
380                 struct filter_list_struct *mergelist;
381         } u;
382 +       /* TODO: Use an "extras" mechanism to avoid
383 +        * allocating this memory when we don't need it. */
384 +       struct filter_chmod_struct *chmod;
385 +       uid_t force_uid;
386 +       gid_t force_gid;
387  } filter_rule;
388  
389  typedef struct filter_list_struct {
390 diff --git a/rsync.yo b/rsync.yo
391 --- a/rsync.yo
392 +++ b/rsync.yo
393 @@ -1187,6 +1187,8 @@ quote(--chmod=D2775,F664)
394  
395  It is also legal to specify multiple bf(--chmod) options, as each
396  additional option is just appended to the list of changes to make.
397 +To change permissions of files matching a pattern, use an include filter with
398 +the bf(m) modifier, which takes effect before any bf(--chmod) options.
399  
400  See the bf(--perms) and bf(--executability) options for how the resulting
401  permission value can be applied to the files in the transfer.
402 @@ -2182,6 +2184,10 @@ be omitted, but if USER is empty, a leading colon must be supplied.
403  If you specify "--chown=foo:bar, this is exactly the same as specifying
404  "--usermap=*:foo --groupmap=*:bar", only easier.
405  
406 +To change ownership of files matching a pattern, use an include filter with
407 +the bf(o) and bf(g) modifiers, which take effect before uid/gid mapping and
408 +therefore em(can) be mixed with bf(--usermap) and bf(--groupmap).
409 +
410  dit(bf(--timeout=TIMEOUT)) This option allows you to set a maximum I/O
411  timeout in seconds. If no data is transferred for the specified time
412  then rsync will exit. The default is 0, which means no timeout.
413 @@ -3076,6 +3082,15 @@ itemization(
414    option's default rules that exclude things like "CVS" and "*.o" are
415    marked as perishable, and will not prevent a directory that was removed
416    on the source from being deleted on the destination.
417 +  it() An bf(m+nop()(CHMOD)) on an include rule tweaks the permissions of matching
418 +  source files in the same way as bf(--chmod).  This happens before any
419 +  tweaks requested via bf(--chmod) options.
420 +  it() An bf(o+nop()(USER)) on an include rule pretends that matching source files
421 +  are owned by bf(USER) (a name or numeric uid).  This happens before any uid
422 +  mapping by name or bf(--usermap).
423 +  it() A bf(g+nop()(GROUP)) on an include rule pretends that matching source files
424 +  are owned by bf(GROUP) (a name or numeric gid).  This happens before any gid
425 +  mapping by name or bf(--groupmap).
426    it() An bf(x) indicates that a rule affects xattr names in xattr copy/delete
427    operations (and is thus ignored when matching file/dir names). If no
428    xattr-matching rules are specified, a default xattr filtering rule is
429 @@ -3141,6 +3156,12 @@ itemization(
430    a rule prefix such as bf(hide)).
431  )
432  
433 +The attribute-affecting modifiers bf(m), bf(o), and bf(g) work only in client
434 +filters (not in daemon filters), and only the modifiers of the first matching
435 +rule are applied.  As an example, assuming bf(--super) is enabled, the
436 +rule "+o+nop()(root)g+nop()(root)m+nop()(go=) *~" would ensure that all "backup" files belong to
437 +root and are not accessible to anyone else.
438 +
439  Per-directory rules are inherited in all subdirectories of the directory
440  where the merge-file was found unless the 'n' modifier was used.  Each
441  subdirectory's rules are prefixed to the inherited per-directory rules
442 diff --git a/util.c b/util.c
443 --- a/util.c
444 +++ b/util.c
445 @@ -891,6 +891,25 @@ size_t stringjoin(char *dest, size_t destsize, ...)
446         return ret;
447  }
448  
449 +/* Append formatted text at *dest_ptr up to a maximum of sz (like snprintf).
450 + * On success, advance *dest_ptr and return True; on overflow, return False. */
451 +BOOL snappendf(char **dest_ptr, size_t sz, const char *format, ...)
452 +{
453 +       va_list ap;
454 +       size_t len;
455 +
456 +       va_start(ap, format);
457 +       len = vsnprintf(*dest_ptr, sz, format, ap);
458 +       va_end(ap);
459 +
460 +       if (len >= sz)
461 +               return False;
462 +       else {
463 +               *dest_ptr += len;
464 +               return True;
465 +       }
466 +}
467 +
468  int count_dir_elements(const char *p)
469  {
470         int cnt = 0, new_component = 1;