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