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