From: Matt McCutchen
Implement the "m", "o", "g" include modifiers to tweak the permissions,
owner, or group of matching files.
To use this patch, run these commands for a successful build:
patch -p1 ref_cnt++;
+ assert(chmod->ref_cnt != 0); /* Catch overflow. */
+ return chmod;
+}
+
+static void unref_filter_chmod(struct filter_chmod_struct *chmod)
+{
+ chmod->ref_cnt--;
+ if (chmod->ref_cnt == 0) {
+ free(chmod->modestr);
+ free_chmod_mode(chmod->modes);
+ free(chmod);
+ }
+}
+
static void free_filter(filter_rule *ex)
{
+ if (ex->rflags & FILTRULE_CHMOD)
+ unref_filter_chmod(ex->chmod);
if (ex->rflags & FILTRULE_PERDIR_MERGE)
teardown_mergelist(ex);
free(ex->pattern);
@@ -722,7 +744,9 @@ static void report_filter_result(enum logcode code, char const *name,
/* This function is used to check if a file should be included/excluded
* from the list of files based on its name and type etc. The value of
- * filter_level is set to either SERVER_FILTERS or ALL_FILTERS. */
+ * filter_level is set to either SERVER_FILTERS or ALL_FILTERS.
+ * "last_hit_filter_rule" will be set to the operative filter, or NULL if none. */
+
int name_is_excluded(const char *fname, int name_flags, int filter_level)
{
if (daemon_filter_list.head && check_filter(&daemon_filter_list, FLOG, fname, name_flags) < 0) {
@@ -731,6 +755,9 @@ int name_is_excluded(const char *fname, int name_flags, int filter_level)
return 1;
}
+ /* Don't leave a daemon include in last_hit_filter_rule. */
+ last_hit_filter_rule = NULL;
+
if (filter_level != ALL_FILTERS)
return 0;
@@ -741,7 +768,8 @@ int name_is_excluded(const char *fname, int name_flags, int filter_level)
}
/* Return -1 if file "name" is defined to be excluded by the specified
- * exclude list, 1 if it is included, and 0 if it was not matched. */
+ * exclude list, 1 if it is included, and 0 if it was not matched.
+ * Sets last_hit_filter_rule to the filter that was hit, or NULL if none. */
int check_filter(filter_rule_list *listp, enum logcode code,
const char *name, int name_flags)
{
@@ -764,10 +792,12 @@ int check_filter(filter_rule_list *listp, enum logcode code,
}
if (rule_matches(name, ent, name_flags)) {
report_filter_result(code, name, ent, name_flags, listp->debug_type);
+ last_hit_filter_rule = ent;
return ent->rflags & FILTRULE_INCLUDE ? 1 : -1;
}
}
+ last_hit_filter_rule = NULL;
return 0;
}
@@ -784,9 +814,45 @@ static const uchar *rule_strcmp(const uchar *str, const char *rule, int rule_len
return NULL;
}
+static char *grab_paren_value(const uchar **s_ptr)
+{
+ const uchar *start, *end;
+ int val_sz;
+ char *val;
+
+ if ((*s_ptr)[1] != '(')
+ return NULL;
+ start = (*s_ptr) + 2;
+
+ for (end = start; *end != ')'; end++)
+ if (!*end || *end == ' ' || *end == '_')
+ return NULL;
+
+ val_sz = end - start + 1;
+ val = new_array(char, val_sz);
+ strlcpy(val, (const char *)start, val_sz);
+ *s_ptr = end; /* remember ++s in parse_rule_tok */
+ return val;
+}
+
+static struct filter_chmod_struct *make_chmod_struct(char *modestr)
+{
+ struct filter_chmod_struct *chmod;
+ struct chmod_mode_struct *modes = NULL;
+
+ if (!parse_chmod(modestr, &modes))
+ return NULL;
+
+ chmod = new(struct filter_chmod_struct);
+ chmod->ref_cnt = 1;
+ chmod->modestr = modestr;
+ chmod->modes = modes;
+ return chmod;
+}
+
#define FILTRULES_FROM_CONTAINER (FILTRULE_ABS_PATH | FILTRULE_INCLUDE \
| FILTRULE_DIRECTORY | FILTRULE_NEGATE \
- | FILTRULE_PERISHABLE)
+ | FILTRULE_PERISHABLE | FILTRULES_ATTRS)
/* Gets the next include/exclude rule from *rulestr_ptr and advances
* *rulestr_ptr to point beyond it. Stores the pattern's start (within
@@ -801,6 +867,7 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
const char **pat_ptr, unsigned int *pat_len_ptr)
{
const uchar *s = (const uchar *)*rulestr_ptr;
+ char *val;
filter_rule *rule;
unsigned int len;
@@ -819,6 +886,12 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
/* Inherit from the template. Don't inherit FILTRULES_SIDES; we check
* that later. */
rule->rflags = template->rflags & FILTRULES_FROM_CONTAINER;
+ if (template->rflags & FILTRULE_CHMOD)
+ rule->chmod = ref_filter_chmod(template->chmod);
+ if (template->rflags & FILTRULE_FORCE_OWNER)
+ rule->force_uid = template->force_uid;
+ if (template->rflags & FILTRULE_FORCE_GROUP)
+ rule->force_gid = template->force_gid;
/* Figure out what kind of a filter rule "s" is pointing at. Note
* that if FILTRULE_NO_PREFIXES is set, the rule is either an include
@@ -964,11 +1037,63 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
goto invalid;
rule->rflags |= FILTRULE_EXCLUDE_SELF;
break;
+ case 'g': {
+ gid_t gid;
+
+ if (!(val = grab_paren_value(&s)))
+ goto invalid;
+ if (group_to_gid(val, &gid, True)) {
+ rule->rflags |= FILTRULE_FORCE_GROUP;
+ rule->force_gid = gid;
+ } else {
+ rprintf(FERROR,
+ "unknown group '%s' in filter rule: %s\n",
+ val, *rulestr_ptr);
+ exit_cleanup(RERR_SYNTAX);
+ }
+ free(val);
+ break;
+ }
+ case 'm': {
+ struct filter_chmod_struct *chmod;
+
+ if (!(val = grab_paren_value(&s)))
+ goto invalid;
+ if ((chmod = make_chmod_struct(val))) {
+ if (rule->rflags & FILTRULE_CHMOD)
+ unref_filter_chmod(rule->chmod);
+ rule->rflags |= FILTRULE_CHMOD;
+ rule->chmod = chmod;
+ } else {
+ rprintf(FERROR,
+ "unparseable chmod string '%s' in filter rule: %s\n",
+ val, *rulestr_ptr);
+ exit_cleanup(RERR_SYNTAX);
+ }
+ break;
+ }
case 'n':
if (!(rule->rflags & FILTRULE_MERGE_FILE))
goto invalid;
rule->rflags |= FILTRULE_NO_INHERIT;
break;
+ case 'o': {
+ uid_t uid;
+
+ if (!(val = grab_paren_value(&s)))
+ goto invalid;
+ if (user_to_uid(val, &uid, True)) {
+ rule->rflags |= FILTRULE_FORCE_OWNER;
+ rule->force_uid = uid;
+ } else {
+ rprintf(FERROR,
+ "unknown user '%s' in filter rule: %s\n",
+ val, *rulestr_ptr);
+ exit_cleanup(RERR_SYNTAX);
+ }
+ free(val);
+ break;
+ }
case 'p':
rule->rflags |= FILTRULE_PERISHABLE;
break;
@@ -1282,6 +1407,23 @@ char *get_rule_prefix(filter_rule *rule, const char *pat, int for_xfer,
else if (am_sender)
return NULL;
}
+ if (rule->rflags & FILTRULES_ATTRS) {
+ if (!for_xfer || protocol_version >= 31) {
+ if (rule->rflags & FILTRULE_CHMOD)
+ if (!snappendf(&op, (buf + sizeof buf) - op,
+ "m(%s)", rule->chmod->modestr))
+ return NULL;
+ if (rule->rflags & FILTRULE_FORCE_OWNER)
+ if (!snappendf(&op, (buf + sizeof buf) - op,
+ "o(%u)", (unsigned)rule->force_uid))
+ return NULL;
+ if (rule->rflags & FILTRULE_FORCE_GROUP)
+ if (!snappendf(&op, (buf + sizeof buf) - op,
+ "g(%u)", (unsigned)rule->force_gid))
+ return NULL;
+ } else if (!am_sender)
+ return NULL;
+ }
if (op - buf > legal_len)
return NULL;
if (legal_len)
diff --git a/flist.c b/flist.c
--- a/flist.c
+++ b/flist.c
@@ -84,6 +84,7 @@ extern struct chmod_mode_struct *chmod_modes;
extern filter_rule_list filter_list;
extern filter_rule_list daemon_filter_list;
+extern filter_rule *last_hit_filter_rule;
#ifdef ICONV_OPTION
extern int filesfrom_convert;
@@ -1229,7 +1230,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
} else if (readlink_stat(thisname, &st, linkname) != 0) {
int save_errno = errno;
/* See if file is excluded before reporting an error. */
- if (filter_level != NO_FILTERS
+ if (filter_level != NO_FILTERS && filter_level != ALL_FILTERS_NO_EXCLUDE
&& (is_excluded(thisname, 0, filter_level)
|| is_excluded(thisname, 1, filter_level))) {
if (ignore_perishable && save_errno != ENOENT)
@@ -1274,6 +1275,12 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
if (filter_level == NO_FILTERS)
goto skip_filters;
+ if (filter_level == ALL_FILTERS_NO_EXCLUDE) {
+ /* Call only for the side effect of setting last_hit_filter_rule to
+ * any operative include filter, which might affect attributes. */
+ is_excluded(thisname, S_ISDIR(st.st_mode) != 0, ALL_FILTERS);
+ goto skip_filters;
+ }
if (S_ISDIR(st.st_mode)) {
if (!xfer_dirs) {
@@ -1494,12 +1501,23 @@ static struct file_struct *send_file_name(int f, struct file_list *flist,
int flags, int filter_level)
{
struct file_struct *file;
+ BOOL can_tweak_mode;
file = make_file(fname, flist, stp, flags, filter_level);
if (!file)
return NULL;
- if (chmod_modes && !S_ISLNK(file->mode) && file->mode)
+ can_tweak_mode = !S_ISLNK(file->mode) && file->mode;
+ if ((filter_level == ALL_FILTERS || filter_level == ALL_FILTERS_NO_EXCLUDE)
+ && last_hit_filter_rule) {
+ if ((last_hit_filter_rule->rflags & FILTRULE_CHMOD) && can_tweak_mode)
+ file->mode = tweak_mode(file->mode, last_hit_filter_rule->chmod->modes);
+ if ((last_hit_filter_rule->rflags & FILTRULE_FORCE_OWNER) && uid_ndx)
+ F_OWNER(file) = last_hit_filter_rule->force_uid;
+ if ((last_hit_filter_rule->rflags & FILTRULE_FORCE_GROUP) && gid_ndx)
+ F_GROUP(file) = last_hit_filter_rule->force_gid;
+ }
+ if (chmod_modes && can_tweak_mode)
file->mode = tweak_mode(file->mode, chmod_modes);
if (f >= 0) {
@@ -2399,7 +2417,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[])
struct file_struct *file;
file = send_file_name(f, flist, fbuf, &st,
FLAG_TOP_DIR | FLAG_CONTENT_DIR | flags,
- NO_FILTERS);
+ ALL_FILTERS_NO_EXCLUDE);
if (!file)
continue;
if (inc_recurse) {
@@ -2413,7 +2431,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[])
} else
send_if_directory(f, flist, file, fbuf, len, flags);
} else
- send_file_name(f, flist, fbuf, &st, flags, NO_FILTERS);
+ send_file_name(f, flist, fbuf, &st, flags, ALL_FILTERS_NO_EXCLUDE);
}
if (reenable_multiplex >= 0)
diff --git a/rsync.1.md b/rsync.1.md
--- a/rsync.1.md
+++ b/rsync.1.md
@@ -1285,7 +1285,9 @@ your home directory (remove the '=' for that).
> --chmod=D2775,F664
It is also legal to specify multiple `--chmod` options, as each additional
- option is just appended to the list of changes to make.
+ option is just appended to the list of changes to make. To change
+ permissions of files matching a pattern, use an include filter with the `m`
+ modifier, which takes effect before any `--chmod` options.
See the `--perms` and `--executability` options for how the resulting
permission value can be applied to the files in the transfer.
@@ -2636,6 +2638,10 @@ your home directory (remove the '=' for that).
"`--usermap=*:foo --groupmap=*:bar`", only easier. If your shell complains
about the wildcards, use `--protect-args` (`-s`).
+ To change ownership of files matching a pattern, use an include filter with
+ a `o` or `g` modifier, which take effect before uid/gid mapping and
+ therefore *can* be mixed with `--usermap` and `--groupmap`.
+
0. `--timeout=SECONDS`
This option allows you to set a maximum I/O timeout in seconds. If no data
@@ -3633,6 +3639,15 @@ The following modifiers are accepted after a "`+`" or "`-`":
rules that exclude things like "CVS" and "`*.o`" are marked as perishable,
and will not prevent a directory that was removed on the source from being
deleted on the destination.
+- An `m(CHMOD)` on an include rule tweaks the permissions of matching
+ source files in the same way as `--chmod`. This happens before any tweaks
+ requested via `--chmod` options.
+- An `o(USER)` on an include rule pretends that matching source files are
+ owned by `USER` (a name or numeric uid). This happens before any uid mapping
+ by name or `--usermap`.
+- A `g(GROUP)` on an include rule pretends that matching source files are
+ owned by `GROUP` (a name or numeric gid). This happens before any gid
+ mapping by name or `--groupmap`.
- An `x` indicates that a rule affects xattr names in xattr copy/delete
operations (and is thus ignored when matching file/dir names). If no
xattr-matching rules are specified, a default xattr filtering rule is used
@@ -3690,6 +3705,12 @@ The following modifiers are accepted after a merge or dir-merge rule:
rules in the file must not specify sides (via a modifier or a rule prefix
such as `hide`).
+The attribute-affecting modifiers `m`, `o`, and `g` work only in client filters
+(not in daemon filters), and only the modifiers of the first matching rule are
+applied. As an example, assuming `--super` is enabled, the rule
+"`+o(root),g(root),m(go=) *~`" would ensure that all "backup"
+files belong to root and are not accessible to anyone else.
+
Per-directory rules are inherited in all subdirectories of the directory where
the merge-file was found unless the 'n' modifier was used. Each subdirectory's
rules are prefixed to the inherited per-directory rules from its parents, which
diff --git a/rsync.h b/rsync.h
--- a/rsync.h
+++ b/rsync.h
@@ -171,6 +171,9 @@
#define NO_FILTERS 0
#define SERVER_FILTERS 1
#define ALL_FILTERS 2
+/* Don't let the file be excluded, but check for a filter that might affect
+ * its attributes via FILTRULES_ATTRS. */
+#define ALL_FILTERS_NO_EXCLUDE 3
#define XFLG_FATAL_ERRORS (1<<0)
#define XFLG_OLD_PREFIXES (1<<1)
@@ -966,6 +969,8 @@ struct map_struct {
int status; /* first errno from read errors */
};
+struct chmod_mode_struct;
+
#define NAME_IS_FILE (0) /* filter name as a file */
#define NAME_IS_DIR (1<<0) /* filter name as a dir */
#define NAME_IS_XATTR (1<<2) /* filter name as an xattr */
@@ -991,8 +996,18 @@ struct map_struct {
#define FILTRULE_CLEAR_LIST (1<<18)/* this item is the "!" token */
#define FILTRULE_PERISHABLE (1<<19)/* perishable if parent dir goes away */
#define FILTRULE_XATTR (1<<20)/* rule only applies to xattr names */
+#define FILTRULE_CHMOD (1<<21)/* chmod-tweak matching files */
+#define FILTRULE_FORCE_OWNER (1<<22)/* force owner of matching files */
+#define FILTRULE_FORCE_GROUP (1<<23)/* force group of matching files */
#define FILTRULES_SIDES (FILTRULE_SENDER_SIDE | FILTRULE_RECEIVER_SIDE)
+#define FILTRULES_ATTRS (FILTRULE_CHMOD | FILTRULE_FORCE_OWNER | FILTRULE_FORCE_GROUP)
+
+struct filter_chmod_struct {
+ unsigned int ref_cnt;
+ char *modestr;
+ struct chmod_mode_struct *modes;
+};
typedef struct filter_struct {
struct filter_struct *next;
@@ -1002,6 +1017,11 @@ typedef struct filter_struct {
int slash_cnt;
struct filter_list_struct *mergelist;
} u;
+ /* TODO: Use an "extras" mechanism to avoid
+ * allocating this memory when we don't need it. */
+ struct filter_chmod_struct *chmod;
+ uid_t force_uid;
+ gid_t force_gid;
} filter_rule;
typedef struct filter_list_struct {
diff --git a/util.c b/util.c
--- a/util.c
+++ b/util.c
@@ -884,6 +884,25 @@ size_t stringjoin(char *dest, size_t destsize, ...)
return ret;
}
+/* Append formatted text at *dest_ptr up to a maximum of sz (like snprintf).
+ * On success, advance *dest_ptr and return True; on overflow, return False. */
+BOOL snappendf(char **dest_ptr, size_t sz, const char *format, ...)
+{
+ va_list ap;
+ size_t len;
+
+ va_start(ap, format);
+ len = vsnprintf(*dest_ptr, sz, format, ap);
+ va_end(ap);
+
+ if (len >= sz)
+ return False;
+ else {
+ *dest_ptr += len;
+ return True;
+ }
+}
+
int count_dir_elements(const char *p)
{
int cnt = 0, new_component = 1;
diff -Nurp a/rsync.1 b/rsync.1
--- a/rsync.1
+++ b/rsync.1
@@ -1378,7 +1378,9 @@ Using octal mode numbers is also allowed
.RE
.IP
It is also legal to specify multiple \fB\-\-chmod\fP options, as each additional
-option is just appended to the list of changes to make.
+option is just appended to the list of changes to make. To change
+permissions of files matching a pattern, use an include filter with the \fBm\fP
+modifier, which takes effect before any \fB\-\-chmod\fP options.
.IP
See the \fB\-\-perms\fP and \fB\-\-executability\fP options for how the resulting
permission value can be applied to the files in the transfer.
@@ -2686,6 +2688,10 @@ USER is empty, a leading colon must be s
If you specify "\fB\-\-chown=foo:bar\fP", this is exactly the same as specifying
"\fB\-\-usermap=*:foo\ \-\-groupmap=*:bar\fP", only easier. If your shell complains
about the wildcards, use \fB\-\-protect-args\fP (\fB\-s\fP).
+.IP
+To change ownership of files matching a pattern, use an include filter with
+a \fBo\fP or \fBg\fP modifier, which take effect before uid/gid mapping and
+therefore \fIcan\fP be mixed with \fB\-\-usermap\fP and \fB\-\-groupmap\fP.
.IP "\fB\-\-timeout=SECONDS\fP"
This option allows you to set a maximum I/O timeout in seconds. If no data
is transferred for the specified time then rsync will exit. The default is
@@ -3704,6 +3710,18 @@ rules that exclude things like "CVS" and
and will not prevent a directory that was removed on the source from being
deleted on the destination.
.IP o
+An \fBm(CHMOD)\fP on an include rule tweaks the permissions of matching
+source files in the same way as \fB\-\-chmod\fP. This happens before any tweaks
+requested via \fB\-\-chmod\fP options.
+.IP o
+An \fBo(USER)\fP on an include rule pretends that matching source files are
+owned by \fBUSER\fP (a name or numeric uid). This happens before any uid mapping
+by name or \fB\-\-usermap\fP.
+.IP o
+A \fBg(GROUP)\fP on an include rule pretends that matching source files are
+owned by \fBGROUP\fP (a name or numeric gid). This happens before any gid
+mapping by name or \fB\-\-groupmap\fP.
+.IP o
An \fBx\fP indicates that a rule affects xattr names in xattr copy/delete
operations (and is thus ignored when matching file/dir names). If no
xattr-matching rules are specified, a default xattr filtering rule is used
@@ -3772,6 +3790,12 @@ specifies sides to affect (via the \fBs\
rules in the file must not specify sides (via a modifier or a rule prefix
such as \fBhide\fP).
.P
+The attribute-affecting modifiers \fBm\fP, \fBo\fP, and \fBg\fP work only in client filters
+(not in daemon filters), and only the modifiers of the first matching rule are
+applied. As an example, assuming \fB\-\-super\fP is enabled, the rule
+"\fB+o(root),g(root),m(go=)\ *~\fP" would ensure that all "backup"
+files belong to root and are not accessible to anyone else.
+.P
Per-directory rules are inherited in all subdirectories of the directory where
the merge-file was found unless the 'n' modifier was used. Each subdirectory's
rules are prefixed to the inherited per-directory rules from its parents, which
diff -Nurp a/rsync.1.html b/rsync.1.html
--- a/rsync.1.html
+++ b/rsync.1.html
@@ -1242,7 +1242,9 @@ consistent executability across all bits
It is also legal to specify multiple --chmod
options, as each additional
-option is just appended to the list of changes to make.
+option is just appended to the list of changes to make. To change
+permissions of files matching a pattern, use an include filter with the m
+modifier, which takes effect before any --chmod
options.
See the --perms
and --executability
options for how the resulting
permission value can be applied to the files in the transfer.
@@ -2490,6 +2492,9 @@ USER is empty, a leading colon must be s
If you specify "--chown=foo:bar
", this is exactly the same as specifying
"--usermap=*:foo --groupmap=*:bar
", only easier. If your shell complains
about the wildcards, use --protect-args
(-s
).
+To change ownership of files matching a pattern, use an include filter with
+a o
or g
modifier, which take effect before uid/gid mapping and
+therefore can be mixed with --usermap
and --groupmap
.
--timeout=SECONDS
@@ -3431,6 +3436,15 @@ directories that are being deleted. For
rules that exclude things like "CVS" and "*.o
" are marked as perishable,
and will not prevent a directory that was removed on the source from being
deleted on the destination.
+An m(CHMOD)
on an include rule tweaks the permissions of matching
+source files in the same way as --chmod
. This happens before any tweaks
+requested via --chmod
options.
+An o(USER)
on an include rule pretends that matching source files are
+owned by USER
(a name or numeric uid). This happens before any uid mapping
+by name or --usermap
.
+A g(GROUP)
on an include rule pretends that matching source files are
+owned by GROUP
(a name or numeric gid). This happens before any gid
+mapping by name or --groupmap
.
An x
indicates that a rule affects xattr names in xattr copy/delete
operations (and is thus ignored when matching file/dir names). If no
xattr-matching rules are specified, a default xattr filtering rule is used
@@ -3486,6 +3500,11 @@ specifies sides to affect (via the hide
).
+The attribute-affecting modifiers m
, o
, and g
work only in client filters
+(not in daemon filters), and only the modifiers of the first matching rule are
+applied. As an example, assuming --super
is enabled, the rule
+"+o(root),g(root),m(go=) *~
" would ensure that all "backup"
+files belong to root and are not accessible to anyone else.
Per-directory rules are inherited in all subdirectories of the directory where
the merge-file was found unless the 'n' modifier was used. Each subdirectory's
rules are prefixed to the inherited per-directory rules from its parents, which