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: 6b5ae825db985e9d1c98576651b50c8e490ddb97
13 diff --git a/exclude.c 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]" };
20 +filter_rule *last_hit_filter_rule;
22 int saw_xattr_filter = 0;
23 int trust_sender_args = 0;
24 int trust_sender_filter = 0;
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)
32 #define SLASH_WILD3_SUFFIX "/***"
34 @@ -139,8 +142,27 @@ static void teardown_mergelist(filter_rule *ex)
38 +static struct filter_chmod_struct *ref_filter_chmod(struct filter_chmod_struct *chmod)
41 + assert(chmod->ref_cnt != 0); /* Catch overflow. */
45 +static void unref_filter_chmod(struct filter_chmod_struct *chmod)
48 + if (chmod->ref_cnt == 0) {
49 + free(chmod->modestr);
50 + free_chmod_mode(chmod->modes);
55 static void free_filter(filter_rule *ex)
57 + if (ex->rflags & FILTRULE_CHMOD)
58 + unref_filter_chmod(ex->chmod);
59 if (ex->rflags & FILTRULE_PERDIR_MERGE)
60 teardown_mergelist(ex);
62 @@ -1005,7 +1027,9 @@ static void report_filter_result(enum logcode code, char const *name,
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. */
70 int name_is_excluded(const char *fname, int name_flags, int filter_level)
72 if (daemon_filter_list.head && check_filter(&daemon_filter_list, FLOG, fname, name_flags) < 0) {
73 @@ -1014,6 +1038,9 @@ int name_is_excluded(const char *fname, int name_flags, int filter_level)
77 + /* Don't leave a daemon include in last_hit_filter_rule. */
78 + last_hit_filter_rule = NULL;
80 if (filter_level != ALL_FILTERS)
83 @@ -1033,7 +1060,8 @@ int check_server_filter(filter_rule_list *listp, enum logcode code, const char *
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)
93 @@ -1056,10 +1084,12 @@ int check_filter(filter_rule_list *listp, enum logcode code,
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;
102 + last_hit_filter_rule = NULL;
106 @@ -1076,9 +1106,45 @@ static const uchar *rule_strcmp(const uchar *str, const char *rule, int rule_len
110 +static char *grab_paren_value(const uchar **s_ptr)
112 + const uchar *start, *end;
116 + if ((*s_ptr)[1] != '(')
118 + start = (*s_ptr) + 2;
120 + for (end = start; *end != ')'; end++)
121 + if (!*end || *end == ' ' || *end == '_')
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 */
131 +static struct filter_chmod_struct *make_chmod_struct(char *modestr)
133 + struct filter_chmod_struct *chmod;
134 + struct chmod_mode_struct *modes = NULL;
136 + if (!parse_chmod(modestr, &modes))
139 + chmod = new(struct filter_chmod_struct);
140 + chmod->ref_cnt = 1;
141 + chmod->modestr = modestr;
142 + chmod->modes = modes;
146 #define FILTRULES_FROM_CONTAINER (FILTRULE_ABS_PATH | FILTRULE_INCLUDE \
147 | FILTRULE_DIRECTORY | FILTRULE_NEGATE \
148 - | FILTRULE_PERISHABLE)
149 + | FILTRULE_PERISHABLE | FILTRULES_ATTRS)
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 @@ -1093,6 +1159,7 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
154 const char **pat_ptr, unsigned int *pat_len_ptr)
156 const uchar *s = (const uchar *)*rulestr_ptr;
161 @@ -1111,6 +1178,12 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
162 /* Inherit from the template. Don't inherit FILTRULES_SIDES; we check
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;
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 @@ -1257,11 +1330,63 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
176 rule->rflags |= FILTRULE_EXCLUDE_SELF;
181 + if (!(val = grab_paren_value(&s)))
183 + if (group_to_gid(val, &gid, True)) {
184 + rule->rflags |= FILTRULE_FORCE_GROUP;
185 + rule->force_gid = gid;
188 + "unknown group '%s' in filter rule: %s\n",
189 + val, *rulestr_ptr);
190 + exit_cleanup(RERR_SYNTAX);
196 + struct filter_chmod_struct *chmod;
198 + if (!(val = grab_paren_value(&s)))
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;
207 + "unparseable chmod string '%s' in filter rule: %s\n",
208 + val, *rulestr_ptr);
209 + exit_cleanup(RERR_SYNTAX);
214 if (!(rule->rflags & FILTRULE_MERGE_FILE))
216 rule->rflags |= FILTRULE_NO_INHERIT;
221 + if (!(val = grab_paren_value(&s)))
223 + if (user_to_uid(val, &uid, True)) {
224 + rule->rflags |= FILTRULE_FORCE_OWNER;
225 + rule->force_uid = uid;
228 + "unknown user '%s' in filter rule: %s\n",
229 + val, *rulestr_ptr);
230 + exit_cleanup(RERR_SYNTAX);
236 rule->rflags |= FILTRULE_PERISHABLE;
238 @@ -1575,6 +1700,23 @@ char *get_rule_prefix(filter_rule *rule, const char *pat, int for_xfer,
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))
248 + if (rule->rflags & FILTRULE_FORCE_OWNER)
249 + if (!snappendf(&op, (buf + sizeof buf) - op,
250 + "o(%u)", (unsigned)rule->force_uid))
252 + if (rule->rflags & FILTRULE_FORCE_GROUP)
253 + if (!snappendf(&op, (buf + sizeof buf) - op,
254 + "g(%u)", (unsigned)rule->force_gid))
256 + } else if (!am_sender)
259 if (op - buf > legal_len)
262 diff --git a/flist.c b/flist.c
265 @@ -86,6 +86,7 @@ extern char curr_dir[MAXPATHLEN];
266 extern struct chmod_mode_struct *chmod_modes;
268 extern filter_rule_list filter_list, implied_filter_list, daemon_filter_list;
269 +extern filter_rule *last_hit_filter_rule;
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,
284 if (filter_level == NO_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);
293 if (S_ISDIR(st.st_mode)) {
295 @@ -1536,12 +1543,23 @@ static struct file_struct *send_file_name(int f, struct file_list *flist,
296 int flags, int filter_level)
298 struct file_struct *file;
299 + BOOL can_tweak_mode;
301 file = make_file(fname, flist, stp, flags, filter_level);
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;
316 + if (chmod_modes && can_tweak_mode)
317 file->mode = tweak_mode(file->mode, chmod_modes);
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,
325 + ALL_FILTERS_NO_EXCLUDE);
329 @@ -2457,7 +2475,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[])
331 send_if_directory(f, flist, file, fbuf, len, flags);
333 - send_file_name(f, flist, fbuf, &st, flags, NO_FILTERS);
334 + send_file_name(f, flist, fbuf, &st, flags, ALL_FILTERS_NO_EXCLUDE);
337 if (reenable_multiplex >= 0)
338 diff --git a/rsync.1.md b/rsync.1.md
341 @@ -1513,7 +1513,9 @@ expand it.
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.
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 @@ -3030,6 +3032,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.
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).
360 0. `--timeout=SECONDS`
362 This option allows you to set a maximum I/O timeout in seconds. If no data
363 @@ -4184,6 +4190,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
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 @@ -4241,6 +4256,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
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.
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
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
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 */
409 +struct chmod_mode_struct;
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 */
422 #define FILTRULES_SIDES (FILTRULE_SENDER_SIDE | FILTRULE_RECEIVER_SIDE)
423 +#define FILTRULES_ATTRS (FILTRULE_CHMOD | FILTRULE_FORCE_OWNER | FILTRULE_FORCE_GROUP)
425 +struct filter_chmod_struct {
426 + unsigned int ref_cnt;
428 + struct chmod_mode_struct *modes;
431 typedef struct filter_struct {
432 struct filter_struct *next;
433 @@ -1018,6 +1033,11 @@ typedef struct filter_struct {
435 struct filter_list_struct *mergelist;
437 + /* TODO: Use an "extras" mechanism to avoid
438 + * allocating this memory when we don't need it. */
439 + struct filter_chmod_struct *chmod;
445 diff --git a/util1.c b/util1.c
448 @@ -918,6 +918,25 @@ size_t stringjoin(char *dest, size_t destsize, ...)
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, ...)
459 + va_start(ap, format);
460 + len = vsnprintf(*dest_ptr, sz, format, ap);
471 int count_dir_elements(const char *p)
473 int cnt = 0, new_component = 1;