1 From: Matt McCutchen <matt@mattmccutchen.net>
3 Implement the "m", "o", "g" include modifiers to tweak the permissions,
4 owner, or group of matching files.
6 To use this patch, run these commands for a successful build:
8 patch -p1 <patches/filter-attribute-mods.diff
9 ./configure (optional if already run)
12 based-on: 5eda68f11bf6efe782cca60a2415191f4532c3b5
13 diff --git a/exclude.c 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]" };
20 +filter_rule *last_hit_filter_rule;
22 int saw_xattr_filter = 0;
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)
30 #define SLASH_WILD3_SUFFIX "/***"
32 @@ -128,8 +131,27 @@ static void teardown_mergelist(filter_rule *ex)
36 +static struct filter_chmod_struct *ref_filter_chmod(struct filter_chmod_struct *chmod)
39 + assert(chmod->ref_cnt != 0); /* Catch overflow. */
43 +static void unref_filter_chmod(struct filter_chmod_struct *chmod)
46 + if (chmod->ref_cnt == 0) {
47 + free(chmod->modestr);
48 + free_chmod_mode(chmod->modes);
53 static void free_filter(filter_rule *ex)
55 + if (ex->rflags & FILTRULE_CHMOD)
56 + unref_filter_chmod(ex->chmod);
57 if (ex->rflags & FILTRULE_PERDIR_MERGE)
58 teardown_mergelist(ex);
60 @@ -723,7 +745,9 @@ static void report_filter_result(enum logcode code, char const *name,
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. */
68 int name_is_excluded(const char *fname, int name_flags, int filter_level)
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)
75 + /* Don't leave a daemon include in last_hit_filter_rule. */
76 + last_hit_filter_rule = NULL;
78 if (filter_level != ALL_FILTERS)
81 @@ -742,7 +769,8 @@ int name_is_excluded(const char *fname, int name_flags, int filter_level)
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)
91 @@ -765,10 +793,12 @@ int check_filter(filter_rule_list *listp, enum logcode code,
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;
100 + last_hit_filter_rule = NULL;
104 @@ -785,9 +815,45 @@ static const uchar *rule_strcmp(const uchar *str, const char *rule, int rule_len
108 +static char *grab_paren_value(const uchar **s_ptr)
110 + const uchar *start, *end;
114 + if ((*s_ptr)[1] != '(')
116 + start = (*s_ptr) + 2;
118 + for (end = start; *end != ')'; end++)
119 + if (!*end || *end == ' ' || *end == '_')
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 */
129 +static struct filter_chmod_struct *make_chmod_struct(char *modestr)
131 + struct filter_chmod_struct *chmod;
132 + struct chmod_mode_struct *modes = NULL;
134 + if (!parse_chmod(modestr, &modes))
137 + chmod = new(struct filter_chmod_struct);
138 + chmod->ref_cnt = 1;
139 + chmod->modestr = modestr;
140 + chmod->modes = modes;
144 #define FILTRULES_FROM_CONTAINER (FILTRULE_ABS_PATH | FILTRULE_INCLUDE \
145 | FILTRULE_DIRECTORY | FILTRULE_NEGATE \
146 - | FILTRULE_PERISHABLE)
147 + | FILTRULE_PERISHABLE | FILTRULES_ATTRS)
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)
154 const uchar *s = (const uchar *)*rulestr_ptr;
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
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;
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,
174 rule->rflags |= FILTRULE_EXCLUDE_SELF;
179 + if (!(val = grab_paren_value(&s)))
181 + if (group_to_gid(val, &gid, True)) {
182 + rule->rflags |= FILTRULE_FORCE_GROUP;
183 + rule->force_gid = gid;
186 + "unknown group '%s' in filter rule: %s\n",
187 + val, *rulestr_ptr);
188 + exit_cleanup(RERR_SYNTAX);
194 + struct filter_chmod_struct *chmod;
196 + if (!(val = grab_paren_value(&s)))
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;
205 + "unparseable chmod string '%s' in filter rule: %s\n",
206 + val, *rulestr_ptr);
207 + exit_cleanup(RERR_SYNTAX);
212 if (!(rule->rflags & FILTRULE_MERGE_FILE))
214 rule->rflags |= FILTRULE_NO_INHERIT;
219 + if (!(val = grab_paren_value(&s)))
221 + if (user_to_uid(val, &uid, True)) {
222 + rule->rflags |= FILTRULE_FORCE_OWNER;
223 + rule->force_uid = uid;
226 + "unknown user '%s' in filter rule: %s\n",
227 + val, *rulestr_ptr);
228 + exit_cleanup(RERR_SYNTAX);
234 rule->rflags |= FILTRULE_PERISHABLE;
236 @@ -1283,6 +1408,23 @@ char *get_rule_prefix(filter_rule *rule, const char *pat, int for_xfer,
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))
246 + if (rule->rflags & FILTRULE_FORCE_OWNER)
247 + if (!snappendf(&op, (buf + sizeof buf) - op,
248 + "o(%u)", (unsigned)rule->force_uid))
250 + if (rule->rflags & FILTRULE_FORCE_GROUP)
251 + if (!snappendf(&op, (buf + sizeof buf) - op,
252 + "g(%u)", (unsigned)rule->force_gid))
254 + } else if (!am_sender)
257 if (op - buf > legal_len)
260 diff --git a/flist.c b/flist.c
263 @@ -83,6 +83,7 @@ extern struct chmod_mode_struct *chmod_modes;
265 extern filter_rule_list filter_list;
266 extern filter_rule_list daemon_filter_list;
267 +extern filter_rule *last_hit_filter_rule;
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,
282 if (filter_level == NO_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);
291 if (S_ISDIR(st.st_mode)) {
293 @@ -1448,12 +1455,23 @@ static struct file_struct *send_file_name(int f, struct file_list *flist,
294 int flags, int filter_level)
296 struct file_struct *file;
297 + BOOL can_tweak_mode;
299 file = make_file(fname, flist, stp, flags, filter_level);
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;
314 + if (chmod_modes && can_tweak_mode)
315 file->mode = tweak_mode(file->mode, chmod_modes);
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,
323 + ALL_FILTERS_NO_EXCLUDE);
327 @@ -2367,7 +2385,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[])
329 send_if_directory(f, flist, file, fbuf, len, flags);
331 - send_file_name(f, flist, fbuf, &st, flags, NO_FILTERS);
332 + send_file_name(f, flist, fbuf, &st, flags, ALL_FILTERS_NO_EXCLUDE);
335 if (reenable_multiplex >= 0)
336 diff --git a/rsync.1.md b/rsync.1.md
339 @@ -1249,7 +1249,9 @@ your home directory (remove the '=' for that).
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.
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.
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`.
358 0. `--timeout=SECONDS`
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
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.
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
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
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 */
407 +struct chmod_mode_struct;
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 */
420 #define FILTRULES_SIDES (FILTRULE_SENDER_SIDE | FILTRULE_RECEIVER_SIDE)
421 +#define FILTRULES_ATTRS (FILTRULE_CHMOD | FILTRULE_FORCE_OWNER | FILTRULE_FORCE_GROUP)
423 +struct filter_chmod_struct {
424 + unsigned int ref_cnt;
426 + struct chmod_mode_struct *modes;
429 typedef struct filter_struct {
430 struct filter_struct *next;
431 @@ -990,6 +1005,11 @@ typedef struct filter_struct {
433 struct filter_list_struct *mergelist;
435 + /* TODO: Use an "extras" mechanism to avoid
436 + * allocating this memory when we don't need it. */
437 + struct filter_chmod_struct *chmod;
442 typedef struct filter_list_struct {
443 diff --git a/util.c b/util.c
446 @@ -884,6 +884,25 @@ size_t stringjoin(char *dest, size_t destsize, ...)
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, ...)
457 + va_start(ap, format);
458 + len = vsnprintf(*dest_ptr, sz, format, ap);
469 int count_dir_elements(const char *p)
471 int cnt = 0, new_component = 1;