Some doc fixes.
[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: 5eda68f11bf6efe782cca60a2415191f4532c3b5
13 diff --git a/exclude.c b/exclude.c
14 --- a/exclude.c
15 +++ b/exclude.c
16 @@ -46,10 +46,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 @@ -128,8 +131,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 @@ -723,7 +745,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 @@ -732,6 +756,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 @@ -742,7 +769,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 @@ -765,10 +793,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 @@ -785,9 +815,45 @@ 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 +       chmod = new(struct filter_chmod_struct);
138 +       chmod->ref_cnt = 1;
139 +       chmod->modestr = modestr;
140 +       chmod->modes = modes;
141 +       return chmod;
142 +}
143 +
144  #define FILTRULES_FROM_CONTAINER (FILTRULE_ABS_PATH | FILTRULE_INCLUDE \
145                                 | FILTRULE_DIRECTORY | FILTRULE_NEGATE \
146 -                               | FILTRULE_PERISHABLE)
147 +                               | FILTRULE_PERISHABLE | FILTRULES_ATTRS)
148  
149  /* Gets the next include/exclude rule from *rulestr_ptr and advances
150   * *rulestr_ptr to point beyond it.  Stores the pattern's start (within
151 @@ -802,6 +868,7 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
152                                    const char **pat_ptr, unsigned int *pat_len_ptr)
153  {
154         const uchar *s = (const uchar *)*rulestr_ptr;
155 +       char *val;
156         filter_rule *rule;
157         unsigned int len;
158  
159 @@ -820,6 +887,12 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
160         /* Inherit from the template.  Don't inherit FILTRULES_SIDES; we check
161          * that later. */
162         rule->rflags = template->rflags & FILTRULES_FROM_CONTAINER;
163 +       if (template->rflags & FILTRULE_CHMOD)
164 +               rule->chmod = ref_filter_chmod(template->chmod);
165 +       if (template->rflags & FILTRULE_FORCE_OWNER)
166 +               rule->force_uid = template->force_uid;
167 +       if (template->rflags & FILTRULE_FORCE_GROUP)
168 +               rule->force_gid = template->force_gid;
169  
170         /* Figure out what kind of a filter rule "s" is pointing at.  Note
171          * that if FILTRULE_NO_PREFIXES is set, the rule is either an include
172 @@ -965,11 +1038,63 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
173                                         goto invalid;
174                                 rule->rflags |= FILTRULE_EXCLUDE_SELF;
175                                 break;
176 +                       case 'g': {
177 +                               gid_t gid;
178 +
179 +                               if (!(val = grab_paren_value(&s)))
180 +                                       goto invalid;
181 +                               if (group_to_gid(val, &gid, True)) {
182 +                                       rule->rflags |= FILTRULE_FORCE_GROUP;
183 +                                       rule->force_gid = gid;
184 +                               } else {
185 +                                       rprintf(FERROR,
186 +                                               "unknown group '%s' in filter rule: %s\n",
187 +                                               val, *rulestr_ptr);
188 +                                       exit_cleanup(RERR_SYNTAX);
189 +                               }
190 +                               free(val);
191 +                               break;
192 +                       }
193 +                       case 'm': {
194 +                               struct filter_chmod_struct *chmod;
195 +
196 +                               if (!(val = grab_paren_value(&s)))
197 +                                       goto invalid;
198 +                               if ((chmod = make_chmod_struct(val))) {
199 +                                       if (rule->rflags & FILTRULE_CHMOD)
200 +                                               unref_filter_chmod(rule->chmod);
201 +                                       rule->rflags |= FILTRULE_CHMOD;
202 +                                       rule->chmod = chmod;
203 +                               } else {
204 +                                       rprintf(FERROR,
205 +                                               "unparseable chmod string '%s' in filter rule: %s\n",
206 +                                               val, *rulestr_ptr);
207 +                                       exit_cleanup(RERR_SYNTAX);
208 +                               }
209 +                               break;
210 +                       }
211                         case 'n':
212                                 if (!(rule->rflags & FILTRULE_MERGE_FILE))
213                                         goto invalid;
214                                 rule->rflags |= FILTRULE_NO_INHERIT;
215                                 break;
216 +                       case 'o': {
217 +                               uid_t uid;
218 +
219 +                               if (!(val = grab_paren_value(&s)))
220 +                                       goto invalid;
221 +                               if (user_to_uid(val, &uid, True)) {
222 +                                       rule->rflags |= FILTRULE_FORCE_OWNER;
223 +                                       rule->force_uid = uid;
224 +                               } else {
225 +                                       rprintf(FERROR,
226 +                                               "unknown user '%s' in filter rule: %s\n",
227 +                                               val, *rulestr_ptr);
228 +                                       exit_cleanup(RERR_SYNTAX);
229 +                               }
230 +                               free(val);
231 +                               break;
232 +                       }
233                         case 'p':
234                                 rule->rflags |= FILTRULE_PERISHABLE;
235                                 break;
236 @@ -1283,6 +1408,23 @@ char *get_rule_prefix(filter_rule *rule, const char *pat, int for_xfer,
237                 else if (am_sender)
238                         return NULL;
239         }
240 +       if (rule->rflags & FILTRULES_ATTRS) {
241 +               if (!for_xfer || protocol_version >= 31) {
242 +                       if (rule->rflags & FILTRULE_CHMOD)
243 +                               if (!snappendf(&op, (buf + sizeof buf) - op,
244 +                                       "m(%s)", rule->chmod->modestr))
245 +                                       return NULL;
246 +                       if (rule->rflags & FILTRULE_FORCE_OWNER)
247 +                               if (!snappendf(&op, (buf + sizeof buf) - op,
248 +                                       "o(%u)", (unsigned)rule->force_uid))
249 +                                       return NULL;
250 +                       if (rule->rflags & FILTRULE_FORCE_GROUP)
251 +                               if (!snappendf(&op, (buf + sizeof buf) - op,
252 +                                       "g(%u)", (unsigned)rule->force_gid))
253 +                                       return NULL;
254 +               } else if (!am_sender)
255 +                       return NULL;
256 +       }
257         if (op - buf > legal_len)
258                 return NULL;
259         if (legal_len)
260 diff --git a/flist.c b/flist.c
261 --- a/flist.c
262 +++ b/flist.c
263 @@ -83,6 +83,7 @@ extern struct chmod_mode_struct *chmod_modes;
264  
265  extern filter_rule_list filter_list;
266  extern filter_rule_list daemon_filter_list;
267 +extern filter_rule *last_hit_filter_rule;
268  
269  #ifdef ICONV_OPTION
270  extern int filesfrom_convert;
271 @@ -1187,7 +1188,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
272         } else if (readlink_stat(thisname, &st, linkname) != 0) {
273                 int save_errno = errno;
274                 /* See if file is excluded before reporting an error. */
275 -               if (filter_level != NO_FILTERS
276 +               if (filter_level != NO_FILTERS && filter_level != ALL_FILTERS_NO_EXCLUDE
277                  && (is_excluded(thisname, 0, filter_level)
278                   || is_excluded(thisname, 1, filter_level))) {
279                         if (ignore_perishable && save_errno != ENOENT)
280 @@ -1232,6 +1233,12 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
281  
282         if (filter_level == NO_FILTERS)
283                 goto skip_filters;
284 +       if (filter_level == ALL_FILTERS_NO_EXCLUDE) {
285 +               /* Call only for the side effect of setting last_hit_filter_rule to
286 +                * any operative include filter, which might affect attributes. */
287 +               is_excluded(thisname, S_ISDIR(st.st_mode) != 0, ALL_FILTERS);
288 +               goto skip_filters;
289 +       }
290  
291         if (S_ISDIR(st.st_mode)) {
292                 if (!xfer_dirs) {
293 @@ -1448,12 +1455,23 @@ static struct file_struct *send_file_name(int f, struct file_list *flist,
294                                           int flags, int filter_level)
295  {
296         struct file_struct *file;
297 +       BOOL can_tweak_mode;
298  
299         file = make_file(fname, flist, stp, flags, filter_level);
300         if (!file)
301                 return NULL;
302  
303 -       if (chmod_modes && !S_ISLNK(file->mode) && file->mode)
304 +       can_tweak_mode = !S_ISLNK(file->mode) && file->mode;
305 +       if ((filter_level == ALL_FILTERS || filter_level == ALL_FILTERS_NO_EXCLUDE)
306 +               && last_hit_filter_rule) {
307 +               if ((last_hit_filter_rule->rflags & FILTRULE_CHMOD) && can_tweak_mode)
308 +                       file->mode = tweak_mode(file->mode, last_hit_filter_rule->chmod->modes);
309 +               if ((last_hit_filter_rule->rflags & FILTRULE_FORCE_OWNER) && uid_ndx)
310 +                       F_OWNER(file) = last_hit_filter_rule->force_uid;
311 +               if ((last_hit_filter_rule->rflags & FILTRULE_FORCE_GROUP) && gid_ndx)
312 +                       F_GROUP(file) = last_hit_filter_rule->force_gid;
313 +       }
314 +       if (chmod_modes && can_tweak_mode)
315                 file->mode = tweak_mode(file->mode, chmod_modes);
316  
317         if (f >= 0) {
318 @@ -2353,7 +2371,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[])
319                         struct file_struct *file;
320                         file = send_file_name(f, flist, fbuf, &st,
321                                               FLAG_TOP_DIR | FLAG_CONTENT_DIR | flags,
322 -                                             NO_FILTERS);
323 +                                             ALL_FILTERS_NO_EXCLUDE);
324                         if (!file)
325                                 continue;
326                         if (inc_recurse) {
327 @@ -2367,7 +2385,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[])
328                         } else
329                                 send_if_directory(f, flist, file, fbuf, len, flags);
330                 } else
331 -                       send_file_name(f, flist, fbuf, &st, flags, NO_FILTERS);
332 +                       send_file_name(f, flist, fbuf, &st, flags, ALL_FILTERS_NO_EXCLUDE);
333         }
334  
335         if (reenable_multiplex >= 0)
336 diff --git a/rsync.1.md b/rsync.1.md
337 --- a/rsync.1.md
338 +++ b/rsync.1.md
339 @@ -1249,7 +1249,9 @@ your home directory (remove the '=' for that).
340      >     --chmod=D2775,F664
341  
342      It is also legal to specify multiple `--chmod` options, as each additional
343 -    option is just appended to the list of changes to make.
344 +    option is just appended to the list of changes to make.  To change
345 +    permissions of files matching a pattern, use an include filter with the `m`
346 +    modifier, which takes effect before any `--chmod` options.
347  
348      See the `--perms` and `--executability` options for how the resulting
349      permission value can be applied to the files in the transfer.
350 @@ -2556,6 +2558,10 @@ your home directory (remove the '=' for that).
351      If you specify "`--chown=foo:bar`", this is exactly the same as specifying
352      "`--usermap=*:foo --groupmap=*:bar`", only easier.
353  
354 +    To change ownership of files matching a pattern, use an include filter with
355 +    a `o` or `g` modifier, which take effect before uid/gid mapping and
356 +    therefore *can* be mixed with `--usermap` and `--groupmap`.
357 +
358  0.  `--timeout=SECONDS`
359  
360      This option allows you to set a maximum I/O timeout in seconds.  If no data
361 @@ -3547,6 +3553,15 @@ The following modifiers are accepted after a "`+`" or "`-`":
362    rules that exclude things like "CVS" and "`*.o`" are marked as perishable,
363    and will not prevent a directory that was removed on the source from being
364    deleted on the destination.
365 +- An `m(CHMOD)` on an include rule tweaks the permissions of matching
366 +  source files in the same way as `--chmod`.  This happens before any tweaks
367 +  requested via `--chmod` options.
368 +- An `o(USER)` on an include rule pretends that matching source files are
369 +  owned by `USER` (a name or numeric uid).  This happens before any uid mapping
370 +  by name or `--usermap`.
371 +- A `g(GROUP)` on an include rule pretends that matching source files are
372 +  owned by `GROUP` (a name or numeric gid).  This happens before any gid
373 +  mapping by name or `--groupmap`.
374  - An `x` indicates that a rule affects xattr names in xattr copy/delete
375    operations (and is thus ignored when matching file/dir names).  If no
376    xattr-matching rules are specified, a default xattr filtering rule is used
377 @@ -3604,6 +3619,12 @@ The following modifiers are accepted after a merge or dir-merge rule:
378    rules in the file must not specify sides (via a modifier or a rule prefix
379    such as `hide`).
380  
381 +The attribute-affecting modifiers `m`, `o`, and `g` work only in client filters
382 +(not in daemon filters), and only the modifiers of the first matching rule are
383 +applied.  As an example, assuming `--super` is enabled, the rule
384 +"`+o(root),g(root),m(go=) *~`" would ensure that all "backup"
385 +files belong to root and are not accessible to anyone else.
386 +
387  Per-directory rules are inherited in all subdirectories of the directory where
388  the merge-file was found unless the 'n' modifier was used.  Each subdirectory's
389  rules are prefixed to the inherited per-directory rules from its parents, which
390 diff --git a/rsync.h b/rsync.h
391 --- a/rsync.h
392 +++ b/rsync.h
393 @@ -171,6 +171,9 @@
394  #define NO_FILTERS     0
395  #define SERVER_FILTERS 1
396  #define ALL_FILTERS    2
397 +/* Don't let the file be excluded, but check for a filter that might affect
398 + * its attributes via FILTRULES_ATTRS. */
399 +#define ALL_FILTERS_NO_EXCLUDE 3
400  
401  #define XFLG_FATAL_ERRORS      (1<<0)
402  #define XFLG_OLD_PREFIXES      (1<<1)
403 @@ -954,6 +957,8 @@ struct map_struct {
404         int status;             /* first errno from read errors         */
405  };
406  
407 +struct chmod_mode_struct;
408 +
409  #define NAME_IS_FILE           (0)    /* filter name as a file */
410  #define NAME_IS_DIR            (1<<0) /* filter name as a dir */
411  #define NAME_IS_XATTR          (1<<2) /* filter name as an xattr */
412 @@ -979,8 +984,18 @@ struct map_struct {
413  #define FILTRULE_CLEAR_LIST    (1<<18)/* this item is the "!" token */
414  #define FILTRULE_PERISHABLE    (1<<19)/* perishable if parent dir goes away */
415  #define FILTRULE_XATTR         (1<<20)/* rule only applies to xattr names */
416 +#define FILTRULE_CHMOD         (1<<21)/* chmod-tweak matching files */
417 +#define FILTRULE_FORCE_OWNER   (1<<22)/* force owner of matching files */
418 +#define FILTRULE_FORCE_GROUP   (1<<23)/* force group of matching files */
419  
420  #define FILTRULES_SIDES (FILTRULE_SENDER_SIDE | FILTRULE_RECEIVER_SIDE)
421 +#define FILTRULES_ATTRS (FILTRULE_CHMOD | FILTRULE_FORCE_OWNER | FILTRULE_FORCE_GROUP)
422 +
423 +struct filter_chmod_struct {
424 +       unsigned int ref_cnt;
425 +       char *modestr;
426 +       struct chmod_mode_struct *modes;
427 +};
428  
429  typedef struct filter_struct {
430         struct filter_struct *next;
431 @@ -990,6 +1005,11 @@ typedef struct filter_struct {
432                 int slash_cnt;
433                 struct filter_list_struct *mergelist;
434         } u;
435 +       /* TODO: Use an "extras" mechanism to avoid
436 +        * allocating this memory when we don't need it. */
437 +       struct filter_chmod_struct *chmod;
438 +       uid_t force_uid;
439 +       gid_t force_gid;
440  } filter_rule;
441  
442  typedef struct filter_list_struct {
443 diff --git a/util.c b/util.c
444 --- a/util.c
445 +++ b/util.c
446 @@ -884,6 +884,25 @@ size_t stringjoin(char *dest, size_t destsize, ...)
447         return ret;
448  }
449  
450 +/* Append formatted text at *dest_ptr up to a maximum of sz (like snprintf).
451 + * On success, advance *dest_ptr and return True; on overflow, return False. */
452 +BOOL snappendf(char **dest_ptr, size_t sz, const char *format, ...)
453 +{
454 +       va_list ap;
455 +       size_t len;
456 +
457 +       va_start(ap, format);
458 +       len = vsnprintf(*dest_ptr, sz, format, ap);
459 +       va_end(ap);
460 +
461 +       if (len >= sz)
462 +               return False;
463 +       else {
464 +               *dest_ptr += len;
465 +               return True;
466 +       }
467 +}
468 +
469  int count_dir_elements(const char *p)
470  {
471         int cnt = 0, new_component = 1;