52d2b3e2eb948dbb9f18c28c7b32a56b6985f31c
[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: 0b2d5fe4940211ba25a89f18a9889b9ab55d38ef
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 @@ -1182,7 +1183,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 @@ -1227,6 +1228,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 @@ -1445,12 +1452,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 @@ -2355,7 +2373,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 @@ -2369,7 +2387,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.1.md b/rsync.1.md
338 --- a/rsync.1.md
339 +++ b/rsync.1.md
340 @@ -1244,7 +1244,9 @@ your home directory (remove the '=' for that).
341      >     --chmod=D2775,F664
342  
343      It is also legal to specify multiple `--chmod` options, as each additional
344 -    option is just appended to the list of changes to make.
345 +    option is just appended to the list of changes to make.  To change
346 +    permissions of files matching a pattern, use an include filter with the `m`
347 +    modifier, which takes effect before any `--chmod` options.
348  
349      See the `--perms` and `--executability` options for how the resulting
350      permission value can be applied to the files in the transfer.
351 @@ -2444,6 +2446,10 @@ your home directory (remove the '=' for that).
352      If you specify "`--chown=foo:bar`", this is exactly the same as specifying
353      "`--usermap=*:foo --groupmap=*:bar`", only easier.
354  
355 +    To change ownership of files matching a pattern, use an include filter with
356 +    the `o` and `g` modifiers, which take effect before uid/gid mapping and
357 +    therefore em(can) be mixed with `--usermap` and `--groupmap`.
358 +
359  0.  `--timeout=TIMEOUT`
360  
361      This option allows you to set a maximum I/O timeout in seconds.  If no data
362 @@ -3382,6 +3388,15 @@ The following modifiers are accepted after a "`+`" or "`-`":
363    rules that exclude things like "CVS" and "`*.o`" are marked as perishable,
364    and will not prevent a directory that was removed on the source from being
365    deleted on the destination.
366 +- An `m+nop()(CHMOD)` on an include rule tweaks the permissions of matching
367 +  source files in the same way as `--chmod`.  This happens before any tweaks
368 +  requested via `--chmod` options.
369 +- An `o+nop()(USER)` on an include rule pretends that matching source files are
370 +  owned by `USER` (a name or numeric uid).  This happens before any uid mapping
371 +  by name or `--usermap`.
372 +- A `g+nop()(GROUP)` on an include rule pretends that matching source files are
373 +  owned by `GROUP` (a name or numeric gid).  This happens before any gid
374 +  mapping by name or `--groupmap`.
375  - An `x` indicates that a rule affects xattr names in xattr copy/delete
376    operations (and is thus ignored when matching file/dir names).  If no
377    xattr-matching rules are specified, a default xattr filtering rule is used
378 @@ -3439,6 +3454,12 @@ The following modifiers are accepted after a merge or dir-merge rule:
379    rules in the file must not specify sides (via a modifier or a rule prefix
380    such as `hide`).
381  
382 +The attribute-affecting modifiers `m`, `o`, and `g` work only in client filters
383 +(not in daemon filters), and only the modifiers of the first matching rule are
384 +applied.  As an example, assuming `--super` is enabled, the rule
385 +"`+o+nop()(root)g+nop()(root)m+nop()(go=) *~`" would ensure that all "backup"
386 +files belong to root and are not accessible to anyone else.
387 +
388  Per-directory rules are inherited in all subdirectories of the directory where
389  the merge-file was found unless the 'n' modifier was used.  Each subdirectory's
390  rules are prefixed to the inherited per-directory rules from its parents, which
391 diff --git a/rsync.h b/rsync.h
392 --- a/rsync.h
393 +++ b/rsync.h
394 @@ -170,6 +170,9 @@
395  #define NO_FILTERS     0
396  #define SERVER_FILTERS 1
397  #define ALL_FILTERS    2
398 +/* Don't let the file be excluded, but check for a filter that might affect
399 + * its attributes via FILTRULES_ATTRS. */
400 +#define ALL_FILTERS_NO_EXCLUDE 3
401  
402  #define XFLG_FATAL_ERRORS      (1<<0)
403  #define XFLG_OLD_PREFIXES      (1<<1)
404 @@ -918,6 +921,8 @@ struct map_struct {
405         int status;             /* first errno from read errors         */
406  };
407  
408 +struct chmod_mode_struct;
409 +
410  #define NAME_IS_FILE           (0)    /* filter name as a file */
411  #define NAME_IS_DIR            (1<<0) /* filter name as a dir */
412  #define NAME_IS_XATTR          (1<<2) /* filter name as an xattr */
413 @@ -943,8 +948,18 @@ struct map_struct {
414  #define FILTRULE_CLEAR_LIST    (1<<18)/* this item is the "!" token */
415  #define FILTRULE_PERISHABLE    (1<<19)/* perishable if parent dir goes away */
416  #define FILTRULE_XATTR         (1<<20)/* rule only applies to xattr names */
417 +#define FILTRULE_CHMOD         (1<<21)/* chmod-tweak matching files */
418 +#define FILTRULE_FORCE_OWNER   (1<<22)/* force owner of matching files */
419 +#define FILTRULE_FORCE_GROUP   (1<<23)/* force group of matching files */
420  
421  #define FILTRULES_SIDES (FILTRULE_SENDER_SIDE | FILTRULE_RECEIVER_SIDE)
422 +#define FILTRULES_ATTRS (FILTRULE_CHMOD | FILTRULE_FORCE_OWNER | FILTRULE_FORCE_GROUP)
423 +
424 +struct filter_chmod_struct {
425 +       unsigned int ref_cnt;
426 +       char *modestr;
427 +       struct chmod_mode_struct *modes;
428 +};
429  
430  typedef struct filter_struct {
431         struct filter_struct *next;
432 @@ -954,6 +969,11 @@ typedef struct filter_struct {
433                 int slash_cnt;
434                 struct filter_list_struct *mergelist;
435         } u;
436 +       /* TODO: Use an "extras" mechanism to avoid
437 +        * allocating this memory when we don't need it. */
438 +       struct filter_chmod_struct *chmod;
439 +       uid_t force_uid;
440 +       gid_t force_gid;
441  } filter_rule;
442  
443  typedef struct filter_list_struct {
444 diff --git a/util.c b/util.c
445 --- a/util.c
446 +++ b/util.c
447 @@ -889,6 +889,25 @@ size_t stringjoin(char *dest, size_t destsize, ...)
448         return ret;
449  }
450  
451 +/* Append formatted text at *dest_ptr up to a maximum of sz (like snprintf).
452 + * On success, advance *dest_ptr and return True; on overflow, return False. */
453 +BOOL snappendf(char **dest_ptr, size_t sz, const char *format, ...)
454 +{
455 +       va_list ap;
456 +       size_t len;
457 +
458 +       va_start(ap, format);
459 +       len = vsnprintf(*dest_ptr, sz, format, ap);
460 +       va_end(ap);
461 +
462 +       if (len >= sz)
463 +               return False;
464 +       else {
465 +               *dest_ptr += len;
466 +               return True;
467 +       }
468 +}
469 +
470  int count_dir_elements(const char *p)
471  {
472         int cnt = 0, new_component = 1;