Updated patches to work with the current trunk.
[rsync/rsync-patches.git] / filter-attribute-mods.diff
CommitLineData
7170ca8d
WD
1From: Matt McCutchen <matt@mattmccutchen.net>
2
3Implement the "m", "o", "g" include modifiers to tweak the permissions,
4owner, or group of matching files.
5
6To 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
c1ff70aa 12based-on: a01e3b490eb36ccf9e704840e1b6683dab867550
7170ca8d
WD
13diff --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)
238diff --git a/flist.c b/flist.c
239--- a/flist.c
240+++ b/flist.c
c1ff70aa 241@@ -82,6 +82,7 @@ extern struct chmod_mode_struct *chmod_modes;
7170ca8d
WD
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;
c1ff70aa 249@@ -284,7 +285,8 @@ static inline int path_is_daemon_excluded(char *path, int ignore_filename)
7170ca8d
WD
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. */
c1ff70aa 259@@ -293,6 +295,8 @@ static int is_excluded(const char *fname, int is_dir, int filter_level)
7170ca8d
WD
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
c1ff70aa 268@@ -1171,7 +1175,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
7170ca8d
WD
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)
c1ff70aa 277@@ -1216,6 +1220,12 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
7170ca8d
WD
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) {
c1ff70aa 290@@ -1416,12 +1426,23 @@ static struct file_struct *send_file_name(int f, struct file_list *flist,
7170ca8d
WD
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) {
c1ff70aa 315@@ -2300,7 +2321,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[])
7170ca8d
WD
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) {
c1ff70aa 324@@ -2314,7 +2335,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[])
7170ca8d
WD
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
5214a41b 332 if (reenable_multiplex >= 0)
7170ca8d
WD
333diff --git a/rsync.h b/rsync.h
334--- a/rsync.h
335+++ b/rsync.h
5214a41b 336@@ -151,6 +151,9 @@
7170ca8d
WD
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)
5214a41b 346@@ -809,6 +812,8 @@ struct map_struct {
7170ca8d
WD
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 "**" */
5214a41b 355@@ -829,8 +834,18 @@ struct map_struct {
7170ca8d
WD
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;
5214a41b 374@@ -840,6 +855,11 @@ typedef struct filter_struct {
7170ca8d
WD
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 {
386diff --git a/rsync.yo b/rsync.yo
387--- a/rsync.yo
388+++ b/rsync.yo
389@@ -1028,6 +1028,8 @@ quote(--chmod=Dg+s,ug+w,Fo-w,+X)
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.
c1ff70aa 398@@ -1846,6 +1848,10 @@ be omitted, but if USER is empty, a leading colon must be supplied.
7170ca8d
WD
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.
c1ff70aa 409@@ -2671,6 +2677,15 @@ itemization(
7170ca8d
WD
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)
c1ff70aa 425@@ -2732,6 +2747,12 @@ itemization(
7170ca8d
WD
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
438diff --git a/util.c b/util.c
439--- a/util.c
440+++ b/util.c
72e5645e 441@@ -816,6 +816,25 @@ size_t stringjoin(char *dest, size_t destsize, ...)
7170ca8d
WD
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;