mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-09-13 05:38:57 -04:00
The exclude-self rule that a ":e" merge synthesizes is built by hand with
new0(), so it inherited no flags. While the merge file was still being
parsed the global parse state masked that, but once parsing finished the
stored rule looked argument-origin, and report_filter_result() printed its
pattern -- a merge file's own text -- verbatim:
[sender] hiding file PAT-x9 because of pattern PAT-[x]9 [per-dir ...]
Plain -vv reaches this on a stock client; no --debug is involved. That is
the fifth site of this shape, and the first to get there by constructing a
rule rather than by printing one, so the redaction helper could not catch it.
Also fix the location a per-directory merge reports. Its fname points into
dirbuf, which is cut back to the directory before the name was saved, so the
error said "<rule from .../src/ line 1>" instead of naming .rsync-filter --
no leak, but it breaks the "redact what, keep where" bargain the rest of this
work depends on. Save the name before the truncation.
The rrsync test's claim to close "the rest of the FILTER trace family" was
too strong and is corrected: options.c maps verbosity onto the debug flags,
so -vvv still raises a restricted server to FILTER2 and its trace metadata
comes back. Rule text stays redacted at every verbosity, which is the
property that matters; -vvv is added to the unaffected-transfer cases.
1998 lines
60 KiB
C
1998 lines
60 KiB
C
/*
|
|
* The filter include/exclude routines.
|
|
*
|
|
* Copyright (C) 1996-2001 Andrew Tridgell <tridge@samba.org>
|
|
* Copyright (C) 1996 Paul Mackerras
|
|
* Copyright (C) 2002 Martin Pool
|
|
* Copyright (C) 2003-2024 Wayne Davison
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, visit the http://fsf.org website.
|
|
*/
|
|
|
|
#include "rsync.h"
|
|
#include "ifuncs.h"
|
|
|
|
extern int am_server;
|
|
extern int am_sender;
|
|
extern int am_generator;
|
|
extern int eol_nulls;
|
|
extern int io_error;
|
|
extern int xfer_dirs;
|
|
extern int recurse;
|
|
extern int local_server;
|
|
extern int prune_empty_dirs;
|
|
extern int ignore_perishable;
|
|
extern int relative_paths;
|
|
extern int delete_mode;
|
|
extern int delete_excluded;
|
|
extern int cvs_exclude;
|
|
extern int sanitize_paths;
|
|
extern int protocol_version;
|
|
extern int trust_sender_args;
|
|
extern int module_id;
|
|
extern int operator_path_resolve;
|
|
|
|
/* Set while the daemon loads its own filter parameters; see parse_filter_file(). */
|
|
int daemon_config_filter_file = 0;
|
|
|
|
/* Where the rule text now being parsed came from, when that is a file's
|
|
* CONTENTS rather than an argument. A rule that fails to parse used to be
|
|
* echoed back verbatim, and the peer chooses which file gets merged (a
|
|
* per-directory merge rule travels over the protocol, so no argument of ours
|
|
* ever names it), which made the filter parser a read-any-line oracle: any
|
|
* line that is not valid filter syntax came straight back in the error.
|
|
* Report where the bad rule is, not what it says. */
|
|
static int rule_src_in_file = 0; /* parsing a file's contents right now */
|
|
static const char *rule_src_file = NULL; /* ...and its name is safe to show */
|
|
static int rule_src_line = 0;
|
|
/* Where a file whose own name we must NOT print was named, which is a location
|
|
* we CAN print: it keeps the diagnostic useful without echoing the pathname a
|
|
* merge rule supplied. */
|
|
static const char *rule_src_named_at = NULL;
|
|
|
|
/* True while the text we are handling came out of a file's contents: either we
|
|
* are parsing that file right now, or this is a deferred per-dir merge whose
|
|
* NAME came from one and which carries the provenance on the rule. */
|
|
#define TEXT_FROM_FILE(template) \
|
|
(rule_src_in_file \
|
|
|| ((template) && (template)->rflags & FILTRULE_FROM_FILE))
|
|
|
|
/* "FILE line N", or just "FILE" when the count is not a line count. */
|
|
static const char *rule_src_where(void)
|
|
{
|
|
static char buf[MAXPATHLEN + 32];
|
|
|
|
if (!rule_src_file) {
|
|
if (!rule_src_named_at)
|
|
return "a file read earlier"; /* origin not retained */
|
|
snprintf(buf, sizeof buf, "a file named at %s", rule_src_named_at);
|
|
return buf;
|
|
}
|
|
if (rule_src_line < 0)
|
|
return rule_src_file;
|
|
snprintf(buf, sizeof buf, "%s line %d", rule_src_file, rule_src_line);
|
|
return buf;
|
|
}
|
|
|
|
/* THE chokepoint. Every diagnostic string that is, or is built from, a filter
|
|
* rule's own text -- a pattern, a merge-file name, a path composed from one --
|
|
* must be passed through rule_text() on its way to rprintf(). When the rule
|
|
* came from an argument the text is returned unchanged, because it is the
|
|
* user's own and hiding it only makes typos harder to fix. When it came from
|
|
* a FILE's contents it is replaced by a description of where it came from,
|
|
* because the peer chooses which file gets merged and any line of it that
|
|
* reaches a message is a line the peer can read back.
|
|
*
|
|
* Doing it here rather than at each site is the point: a message added later
|
|
* cannot reintroduce the leak by forgetting to check, and there is one place
|
|
* to audit. `template' is the rule the text belongs to, or NULL when the only
|
|
* thing that matters is whether we are parsing a file right now.
|
|
*
|
|
* The returned buffer is rotated, so two calls in one rprintf() are safe. */
|
|
static const char *rule_text_len(const filter_rule *template,
|
|
const char *text, int len)
|
|
{
|
|
static char buf[2][BIGPATHBUFLEN];
|
|
static int which = 0;
|
|
char *b = buf[which];
|
|
|
|
which ^= 1;
|
|
if (!TEXT_FROM_FILE(template)) {
|
|
if (len < 0)
|
|
return text;
|
|
snprintf(b, sizeof buf[0], "%.*s", len, text);
|
|
return b;
|
|
}
|
|
snprintf(b, sizeof buf[0], "<rule from %s>", rule_src_where());
|
|
return b;
|
|
}
|
|
|
|
static const char *rule_text(const filter_rule *template, const char *text)
|
|
{
|
|
return rule_text_len(template, text, -1);
|
|
}
|
|
|
|
/* For the extra detail some messages add ABOUT the text -- a character of it,
|
|
* an offset into it. Dropped along with the text it describes. */
|
|
static const char *rule_detail(const filter_rule *template, const char *detail)
|
|
{
|
|
return TEXT_FROM_FILE(template) ? "" : detail;
|
|
}
|
|
|
|
static void filter_rule_err(const char *msg, const char *rulestr)
|
|
{
|
|
rprintf(FERROR, "%s: %s\n", msg, rule_text(NULL, rulestr));
|
|
exit_cleanup(RERR_SYNTAX);
|
|
}
|
|
|
|
extern char curr_dir[MAXPATHLEN];
|
|
extern unsigned int curr_dir_len;
|
|
extern unsigned int module_dirlen;
|
|
|
|
filter_rule_list filter_list = { .debug_type = "" };
|
|
filter_rule_list cvs_filter_list = { .debug_type = " [global CVS]" };
|
|
filter_rule_list daemon_filter_list = { .debug_type = " [daemon]" };
|
|
filter_rule_list implied_filter_list = { .debug_type = " [implied]" };
|
|
|
|
int saw_xattr_filter = 0;
|
|
int trust_sender_args = 0;
|
|
int trust_sender_filter = 0;
|
|
|
|
/* Need room enough for ":MODS " prefix plus some room to grow. */
|
|
#define MAX_RULE_PREFIX (16)
|
|
|
|
#define SLASH_WILD3_SUFFIX "/***"
|
|
|
|
/* The dirbuf is set by push_local_filters() to the current subdirectory
|
|
* relative to curr_dir that is being processed. The path always has a
|
|
* trailing slash appended, and the variable dirbuf_len contains the length
|
|
* of this path prefix. The path is always absolute. */
|
|
static char dirbuf[MAXPATHLEN+1];
|
|
static unsigned int dirbuf_len = 0;
|
|
static int dirbuf_depth;
|
|
|
|
/* This is True when we're scanning parent dirs for per-dir merge-files. */
|
|
static BOOL parent_dirscan = False;
|
|
|
|
#define MAX_MERGE_DEPTH 32
|
|
static int merge_depth = 0;
|
|
|
|
/* This array contains a list of all the currently active per-dir merge
|
|
* files. This makes it easier to save the appropriate values when we
|
|
* "push" down into each subdirectory. */
|
|
static filter_rule **mergelist_parents;
|
|
static int mergelist_cnt = 0;
|
|
static int mergelist_size = 0;
|
|
|
|
#define LOCAL_RULE 1
|
|
#define REMOTE_RULE 2
|
|
static uchar cur_elide_value = REMOTE_RULE;
|
|
|
|
/* Each filter_list_struct describes a singly-linked list by keeping track
|
|
* of both the head and tail pointers. The list is slightly unusual in that
|
|
* a parent-dir's content can be appended to the end of the local list in a
|
|
* special way: the last item in the local list has its "next" pointer set
|
|
* to point to the inherited list, but the local list's tail pointer points
|
|
* at the end of the local list. Thus, if the local list is empty, the head
|
|
* will be pointing at the inherited content but the tail will be NULL. To
|
|
* help you visualize this, here are the possible list arrangements:
|
|
*
|
|
* Completely Empty Local Content Only
|
|
* ================================== ====================================
|
|
* head -> NULL head -> Local1 -> Local2 -> NULL
|
|
* tail -> NULL tail -------------^
|
|
*
|
|
* Inherited Content Only Both Local and Inherited Content
|
|
* ================================== ====================================
|
|
* head -> Parent1 -> Parent2 -> NULL head -> L1 -> L2 -> P1 -> P2 -> NULL
|
|
* tail -> NULL tail ---------^
|
|
*
|
|
* This means that anyone wanting to traverse the whole list to use it just
|
|
* needs to start at the head and use the "next" pointers until it goes
|
|
* NULL. To add new local content, we insert the item after the tail item
|
|
* and update the tail (obviously, if "tail" was NULL, we insert it at the
|
|
* head). To clear the local list, WE MUST NOT FREE THE INHERITED CONTENT
|
|
* because it is shared between the current list and our parent list(s).
|
|
* The easiest way to handle this is to simply truncate the list after the
|
|
* tail item and then free the local list from the head. When inheriting
|
|
* the list for a new local dir, we just save off the filter_list_struct
|
|
* values (so we can pop back to them later) and set the tail to NULL.
|
|
*/
|
|
|
|
static void teardown_mergelist(filter_rule *ex)
|
|
{
|
|
int j;
|
|
|
|
if (!ex->u.mergelist)
|
|
return;
|
|
|
|
if (DEBUG_GTE(FILTER, 2)) {
|
|
rprintf(FINFO, "[%s] deactivating mergelist #%d%s\n",
|
|
who_am_i(), mergelist_cnt - 1,
|
|
ex->u.mergelist->debug_type);
|
|
}
|
|
|
|
free(ex->u.mergelist->debug_type);
|
|
free(ex->u.mergelist);
|
|
|
|
for (j = 0; j < mergelist_cnt; j++) {
|
|
if (mergelist_parents[j] == ex) {
|
|
mergelist_parents[j] = NULL;
|
|
break;
|
|
}
|
|
}
|
|
while (mergelist_cnt && mergelist_parents[mergelist_cnt-1] == NULL)
|
|
mergelist_cnt--;
|
|
}
|
|
|
|
static void free_filter(filter_rule *ex)
|
|
{
|
|
if (ex->rflags & FILTRULE_PERDIR_MERGE)
|
|
teardown_mergelist(ex);
|
|
free(ex->pattern);
|
|
free(ex);
|
|
}
|
|
|
|
static void free_filters(filter_rule *ent)
|
|
{
|
|
while (ent) {
|
|
filter_rule *next = ent->next;
|
|
free_filter(ent);
|
|
ent = next;
|
|
}
|
|
}
|
|
|
|
/* Build a filter structure given a filter pattern. The value in "pat"
|
|
* is not null-terminated. "rule" is either held or freed, so the
|
|
* caller should not free it. */
|
|
static void add_rule(filter_rule_list *listp, const char *pat, unsigned int pat_len,
|
|
filter_rule *rule, int xflags)
|
|
{
|
|
const char *cp;
|
|
unsigned int pre_len, suf_len, slash_cnt = 0;
|
|
char *mention_rule_suffix;
|
|
|
|
if (DEBUG_GTE(FILTER, 1) && pat_len && (pat[pat_len-1] == ' ' || pat[pat_len-1] == '\t'))
|
|
mention_rule_suffix = " -- CAUTION: trailing whitespace!";
|
|
else
|
|
mention_rule_suffix = DEBUG_GTE(FILTER, 2) ? "" : NULL;
|
|
if (mention_rule_suffix) {
|
|
rprintf(FINFO, "[%s] add_rule(%s%s)%s%s\n",
|
|
who_am_i(), rule_detail(rule, get_rule_prefix(rule, pat, 0, NULL)),
|
|
rule_text_len(rule, pat, (int)pat_len),
|
|
listp->debug_type, rule_detail(rule, mention_rule_suffix));
|
|
}
|
|
|
|
/* These flags also indicate that we're reading a list that
|
|
* needs to be filtered now, not post-filtered later. */
|
|
if (xflags & (XFLG_ANCHORED2ABS|XFLG_ABS_IF_SLASH)
|
|
&& (rule->rflags & FILTRULES_SIDES)
|
|
== (am_sender ? FILTRULE_RECEIVER_SIDE : FILTRULE_SENDER_SIDE)) {
|
|
/* This filter applies only to the other side. Drop it. */
|
|
free_filter(rule);
|
|
return;
|
|
}
|
|
|
|
if (pat_len > 1 && pat[pat_len-1] == '/') {
|
|
pat_len--;
|
|
rule->rflags |= FILTRULE_DIRECTORY;
|
|
}
|
|
|
|
for (cp = pat; cp < pat + pat_len; cp++) {
|
|
if (*cp == '/')
|
|
slash_cnt++;
|
|
}
|
|
|
|
if (!(rule->rflags & (FILTRULE_ABS_PATH | FILTRULE_MERGE_FILE))
|
|
&& ((xflags & (XFLG_ANCHORED2ABS|XFLG_ABS_IF_SLASH) && *pat == '/')
|
|
|| (xflags & XFLG_ABS_IF_SLASH && slash_cnt))) {
|
|
rule->rflags |= FILTRULE_ABS_PATH;
|
|
if (*pat == '/')
|
|
pre_len = dirbuf_len - module_dirlen - 1;
|
|
else
|
|
pre_len = 0;
|
|
} else
|
|
pre_len = 0;
|
|
|
|
/* The daemon wants dir-exclude rules to get an appended "/" + "***". */
|
|
if (xflags & XFLG_DIR2WILD3
|
|
&& BITS_SETnUNSET(rule->rflags, FILTRULE_DIRECTORY, FILTRULE_INCLUDE)) {
|
|
rule->rflags &= ~FILTRULE_DIRECTORY;
|
|
suf_len = sizeof SLASH_WILD3_SUFFIX - 1;
|
|
} else
|
|
suf_len = 0;
|
|
|
|
rule->pattern = new_array(char, pre_len + pat_len + suf_len + 1);
|
|
if (pre_len) {
|
|
memcpy(rule->pattern, dirbuf + module_dirlen, pre_len);
|
|
for (cp = rule->pattern; cp < rule->pattern + pre_len; cp++) {
|
|
if (*cp == '/')
|
|
slash_cnt++;
|
|
}
|
|
}
|
|
rule->elide = 0;
|
|
strlcpy(rule->pattern + pre_len, pat, pat_len + 1);
|
|
pat_len += pre_len;
|
|
if (suf_len) {
|
|
memcpy(rule->pattern + pat_len, SLASH_WILD3_SUFFIX, suf_len+1);
|
|
pat_len += suf_len;
|
|
slash_cnt++;
|
|
}
|
|
|
|
if (strpbrk(rule->pattern, "*[?")) {
|
|
rule->rflags |= FILTRULE_WILD;
|
|
if ((cp = strstr(rule->pattern, "**")) != NULL) {
|
|
rule->rflags |= FILTRULE_WILD2;
|
|
/* If the pattern starts with **, note that. */
|
|
if (cp == rule->pattern)
|
|
rule->rflags |= FILTRULE_WILD2_PREFIX;
|
|
/* If the pattern ends with ***, note that. */
|
|
if (pat_len >= 3
|
|
&& rule->pattern[pat_len-3] == '*'
|
|
&& rule->pattern[pat_len-2] == '*'
|
|
&& rule->pattern[pat_len-1] == '*')
|
|
rule->rflags |= FILTRULE_WILD3_SUFFIX;
|
|
}
|
|
}
|
|
|
|
if (rule->rflags & FILTRULE_PERDIR_MERGE) {
|
|
filter_rule_list *lp;
|
|
unsigned int len;
|
|
int i;
|
|
|
|
if ((cp = strrchr(rule->pattern, '/')) != NULL)
|
|
cp++;
|
|
else
|
|
cp = rule->pattern;
|
|
|
|
/* If the local merge file was already mentioned, don't
|
|
* add it again. */
|
|
for (i = 0; i < mergelist_cnt; i++) {
|
|
filter_rule *ex = mergelist_parents[i];
|
|
const char *s;
|
|
if (!ex)
|
|
continue;
|
|
s = strrchr(ex->pattern, '/');
|
|
if (s)
|
|
s++;
|
|
else
|
|
s = ex->pattern;
|
|
len = strlen(s);
|
|
if (len == pat_len - (cp - rule->pattern) && memcmp(s, cp, len) == 0) {
|
|
free_filter(rule);
|
|
return;
|
|
}
|
|
}
|
|
|
|
lp = new_array0(filter_rule_list, 1);
|
|
if (asprintf(&lp->debug_type, " [per-dir %s]", rule_text(rule, cp)) < 0)
|
|
out_of_memory("add_rule");
|
|
rule->u.mergelist = lp;
|
|
|
|
if (mergelist_cnt == mergelist_size) {
|
|
mergelist_size += 5;
|
|
mergelist_parents = realloc_array(mergelist_parents, filter_rule *, mergelist_size);
|
|
}
|
|
if (DEBUG_GTE(FILTER, 2)) {
|
|
rprintf(FINFO, "[%s] activating mergelist #%d%s\n",
|
|
who_am_i(), mergelist_cnt, lp->debug_type);
|
|
}
|
|
mergelist_parents[mergelist_cnt++] = rule;
|
|
} else
|
|
rule->u.slash_cnt = slash_cnt;
|
|
|
|
if (!listp->tail) {
|
|
rule->next = listp->head;
|
|
listp->head = listp->tail = rule;
|
|
} else {
|
|
rule->next = listp->tail->next;
|
|
listp->tail->next = rule;
|
|
listp->tail = rule;
|
|
}
|
|
}
|
|
|
|
/* If the wildcards failed, the remote shell might give us a file matching the literal
|
|
* wildcards. Since "*" & "?" already match themselves, this just needs to deal with
|
|
* failed "[foo]" idioms.
|
|
*/
|
|
static void maybe_add_literal_brackets_rule(filter_rule const *based_on, int arg_len)
|
|
{
|
|
filter_rule *rule;
|
|
const char *arg = based_on->pattern, *cp;
|
|
char *p;
|
|
int cnt = 0;
|
|
|
|
if (arg_len < 0)
|
|
arg_len = strlen(arg);
|
|
|
|
for (cp = arg; *cp; cp++) {
|
|
if (*cp == '\\' && cp[1]) {
|
|
cp++;
|
|
} else if (*cp == '[')
|
|
cnt++;
|
|
}
|
|
if (!cnt)
|
|
return;
|
|
|
|
rule = new0(filter_rule);
|
|
rule->rflags = based_on->rflags;
|
|
rule->u.slash_cnt = based_on->u.slash_cnt;
|
|
p = rule->pattern = new_array(char, arg_len + cnt + 1);
|
|
for (cp = arg; *cp; ) {
|
|
if (*cp == '\\' && cp[1]) {
|
|
*p++ = *cp++;
|
|
} else if (*cp == '[')
|
|
*p++ = '\\';
|
|
*p++ = *cp++;
|
|
}
|
|
*p++ = '\0';
|
|
|
|
rule->next = implied_filter_list.head;
|
|
implied_filter_list.head = rule;
|
|
if (DEBUG_GTE(FILTER, 3)) {
|
|
rprintf(FINFO, "[%s] add_implied_include(%s%s)\n", who_am_i(), rule->pattern,
|
|
rule->rflags & FILTRULE_DIRECTORY ? "/" : "");
|
|
}
|
|
}
|
|
|
|
static char *partial_string_buf = NULL;
|
|
static int partial_string_len = 0;
|
|
void implied_include_partial_string(const char *s_start, const char *s_end)
|
|
{
|
|
partial_string_len = s_end - s_start;
|
|
if (partial_string_len <= 0 || partial_string_len >= MAXPATHLEN) { /* too-large should be impossible... */
|
|
partial_string_len = 0;
|
|
return;
|
|
}
|
|
if (!partial_string_buf)
|
|
partial_string_buf = new_array(char, MAXPATHLEN);
|
|
memcpy(partial_string_buf, s_start, partial_string_len);
|
|
}
|
|
|
|
void free_implied_include_partial_string()
|
|
{
|
|
if (partial_string_buf) {
|
|
if (partial_string_len)
|
|
add_implied_include("", 0);
|
|
free(partial_string_buf);
|
|
partial_string_buf = NULL;
|
|
}
|
|
partial_string_len = 0; /* paranoia */
|
|
}
|
|
|
|
/* Each arg the client sends to the remote sender turns into an implied include
|
|
* that the receiver uses to validate the file list from the sender. */
|
|
void add_implied_include(const char *arg, int skip_daemon_module)
|
|
{
|
|
int arg_len, saw_wild = 0, saw_live_open_brkt = 0, backslash_cnt = 0;
|
|
int slash_cnt = 0;
|
|
const char *cp;
|
|
char *p;
|
|
if (trust_sender_args)
|
|
return;
|
|
if (partial_string_len) {
|
|
arg_len = strlen(arg);
|
|
if (partial_string_len + arg_len >= MAXPATHLEN) {
|
|
partial_string_len = 0;
|
|
return; /* Should be impossible... */
|
|
}
|
|
memcpy(partial_string_buf + partial_string_len, arg, arg_len + 1);
|
|
partial_string_len = 0;
|
|
arg = partial_string_buf;
|
|
}
|
|
if (skip_daemon_module) {
|
|
if ((cp = strchr(arg, '/')) != NULL)
|
|
arg = cp + 1;
|
|
else
|
|
arg = "";
|
|
}
|
|
if (relative_paths) {
|
|
if ((cp = strstr(arg, "/./")) != NULL)
|
|
arg = cp + 3;
|
|
} else if ((cp = strrchr(arg, '/')) != NULL) {
|
|
arg = cp + 1;
|
|
}
|
|
if (*arg == '.' && arg[1] == '\0')
|
|
arg++;
|
|
arg_len = strlen(arg);
|
|
if (arg_len) {
|
|
char *new_pat;
|
|
if (strpbrk(arg, "*[?")) {
|
|
/* We need to add room to escape backslashes if wildcard chars are present. */
|
|
for (cp = arg; (cp = strchr(cp, '\\')) != NULL; cp++)
|
|
arg_len++;
|
|
saw_wild = 1;
|
|
}
|
|
arg_len++; /* Leave room for the prefixed slash */
|
|
p = new_pat = new_array(char, arg_len + 1);
|
|
*p++ = '/';
|
|
slash_cnt++;
|
|
for (cp = arg; *cp; ) {
|
|
switch (*cp) {
|
|
case '\\':
|
|
if (cp[1] == ']') {
|
|
if (!saw_wild)
|
|
cp++; /* A \] in a non-wild filter causes a problem, so drop the \ . */
|
|
} else if (!cp[1] || !strchr("*[?", cp[1])) {
|
|
backslash_cnt++;
|
|
if (saw_wild)
|
|
*p++ = '\\';
|
|
}
|
|
*p++ = *cp++;
|
|
break;
|
|
case '/':
|
|
if (p[-1] == '/') { /* This is safe because of the initial slash. */
|
|
if (*++cp == '\0') {
|
|
slash_cnt--;
|
|
p--;
|
|
}
|
|
} else if (cp[1] == '\0') {
|
|
cp++;
|
|
} else {
|
|
slash_cnt++;
|
|
*p++ = *cp++;
|
|
}
|
|
break;
|
|
case '.':
|
|
if (p[-1] == '/') {
|
|
if (cp[1] == '/') {
|
|
cp += 2;
|
|
if (!*cp) {
|
|
slash_cnt--;
|
|
p--;
|
|
}
|
|
} else if (cp[1] == '\0') {
|
|
cp++;
|
|
slash_cnt--;
|
|
p--;
|
|
} else
|
|
*p++ = *cp++;
|
|
} else
|
|
*p++ = *cp++;
|
|
break;
|
|
case '[':
|
|
saw_live_open_brkt = 1;
|
|
*p++ = *cp++;
|
|
break;
|
|
default:
|
|
*p++ = *cp++;
|
|
break;
|
|
}
|
|
}
|
|
*p = '\0';
|
|
arg_len = p - new_pat;
|
|
if (!arg_len)
|
|
free(new_pat);
|
|
else {
|
|
filter_rule *rule = new0(filter_rule);
|
|
rule->rflags = FILTRULE_INCLUDE + (saw_wild ? FILTRULE_WILD : 0);
|
|
rule->u.slash_cnt = slash_cnt;
|
|
arg = rule->pattern = new_pat;
|
|
if (!implied_filter_list.head)
|
|
implied_filter_list.head = implied_filter_list.tail = rule;
|
|
else {
|
|
rule->next = implied_filter_list.head;
|
|
implied_filter_list.head = rule;
|
|
}
|
|
if (DEBUG_GTE(FILTER, 3))
|
|
rprintf(FINFO, "[%s] add_implied_include(%s)\n", who_am_i(), arg);
|
|
if (saw_live_open_brkt)
|
|
maybe_add_literal_brackets_rule(rule, arg_len);
|
|
if (relative_paths && slash_cnt) {
|
|
int sub_slash_cnt = slash_cnt;
|
|
while ((p = strrchr(new_pat, '/')) != NULL && p != new_pat) {
|
|
filter_rule const *ent;
|
|
filter_rule *R_rule;
|
|
int found = 0;
|
|
*p = '\0';
|
|
for (ent = implied_filter_list.head; ent; ent = ent->next) {
|
|
if (ent != rule && strcmp(ent->pattern, new_pat) == 0) {
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (found) {
|
|
*p = '/';
|
|
break; /* We added all parent dirs already */
|
|
}
|
|
R_rule = new0(filter_rule);
|
|
R_rule->rflags = FILTRULE_INCLUDE | FILTRULE_DIRECTORY;
|
|
/* Check if our sub-path has wildcards or escaped backslashes */
|
|
if (saw_wild && strpbrk(new_pat, "*[?\\"))
|
|
R_rule->rflags |= FILTRULE_WILD;
|
|
R_rule->pattern = strdup(new_pat);
|
|
R_rule->u.slash_cnt = --sub_slash_cnt;
|
|
R_rule->next = implied_filter_list.head;
|
|
implied_filter_list.head = R_rule;
|
|
if (DEBUG_GTE(FILTER, 3)) {
|
|
rprintf(FINFO, "[%s] add_implied_include(%s/)\n",
|
|
who_am_i(), R_rule->pattern);
|
|
}
|
|
if (saw_live_open_brkt)
|
|
maybe_add_literal_brackets_rule(R_rule, -1);
|
|
}
|
|
for (p = new_pat; sub_slash_cnt < slash_cnt; sub_slash_cnt++) {
|
|
p += strlen(p);
|
|
*p = '/';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (recurse || xfer_dirs) {
|
|
/* Now create a rule with an added "/" & "**" or "*" at the end */
|
|
filter_rule *rule = new0(filter_rule);
|
|
rule->rflags = FILTRULE_INCLUDE | FILTRULE_WILD;
|
|
if (recurse)
|
|
rule->rflags |= FILTRULE_WILD2;
|
|
/* We must leave enough room for / * * \0. */
|
|
if (!saw_wild && backslash_cnt) {
|
|
/* We are appending a wildcard, so now the backslashes need to be escaped. */
|
|
p = rule->pattern = new_array(char, arg_len + backslash_cnt + 3 + 1);
|
|
for (cp = arg; *cp; ) { /* Note that arg_len != 0 because backslash_cnt > 0 */
|
|
if (*cp == '\\')
|
|
*p++ = '\\';
|
|
*p++ = *cp++;
|
|
}
|
|
} else {
|
|
p = rule->pattern = new_array(char, arg_len + 3 + 1);
|
|
if (arg_len) {
|
|
memcpy(p, arg, arg_len);
|
|
p += arg_len;
|
|
}
|
|
}
|
|
*p++ = '/';
|
|
*p++ = '*';
|
|
if (recurse)
|
|
*p++ = '*';
|
|
*p = '\0';
|
|
rule->u.slash_cnt = slash_cnt + 1;
|
|
rule->next = implied_filter_list.head;
|
|
implied_filter_list.head = rule;
|
|
if (DEBUG_GTE(FILTER, 3))
|
|
rprintf(FINFO, "[%s] add_implied_include(%s)\n", who_am_i(), rule->pattern);
|
|
if (saw_live_open_brkt)
|
|
maybe_add_literal_brackets_rule(rule, p - rule->pattern);
|
|
}
|
|
}
|
|
|
|
/* This frees any non-inherited items, leaving just inherited items on the list. */
|
|
static void pop_filter_list(filter_rule_list *listp)
|
|
{
|
|
filter_rule *inherited;
|
|
|
|
if (!listp->tail)
|
|
return;
|
|
|
|
inherited = listp->tail->next;
|
|
|
|
/* Truncate any inherited items from the local list. */
|
|
listp->tail->next = NULL;
|
|
/* Now free everything that is left. */
|
|
free_filters(listp->head);
|
|
|
|
listp->head = inherited;
|
|
listp->tail = NULL;
|
|
}
|
|
|
|
/* This returns an expanded (absolute) filename for the merge-file name if
|
|
* the name has any slashes in it OR if the parent_dirscan var is True;
|
|
* otherwise it returns the original merge_file name. If the len_ptr value
|
|
* is non-NULL the merge_file name is limited by the referenced length
|
|
* value and will be updated with the length of the resulting name. We
|
|
* always return a name that is null terminated, even if the merge_file
|
|
* name was not. */
|
|
static char *parse_merge_name(const filter_rule *template,
|
|
const char *merge_file, unsigned int *len_ptr,
|
|
unsigned int prefix_skip)
|
|
{
|
|
static char buf[MAXPATHLEN];
|
|
char *fn, tmpbuf[MAXPATHLEN];
|
|
unsigned int fn_len;
|
|
|
|
if (!parent_dirscan && *merge_file != '/') {
|
|
/* Return the name unchanged it doesn't have any slashes. */
|
|
if (len_ptr) {
|
|
const char *p = merge_file + *len_ptr;
|
|
while (--p > merge_file && *p != '/') {}
|
|
if (p == merge_file) {
|
|
strlcpy(buf, merge_file, *len_ptr + 1);
|
|
return buf;
|
|
}
|
|
} else if (strchr(merge_file, '/') == NULL)
|
|
return (char *)merge_file;
|
|
}
|
|
|
|
fn = *merge_file == '/' ? buf : tmpbuf;
|
|
if (sanitize_paths) {
|
|
const char *r = prefix_skip ? "/" : NULL;
|
|
/* null-terminate the name if it isn't already */
|
|
if (len_ptr && merge_file[*len_ptr]) {
|
|
char *to = fn == buf ? tmpbuf : buf;
|
|
strlcpy(to, merge_file, *len_ptr + 1);
|
|
merge_file = to;
|
|
}
|
|
if (!sanitize_path(fn, merge_file, r, dirbuf_depth, SP_DEFAULT)) {
|
|
rprintf(FERROR, "merge-file name overflows: %s\n",
|
|
rule_text(template, merge_file));
|
|
return NULL;
|
|
}
|
|
fn_len = strlen(fn);
|
|
} else {
|
|
strlcpy(fn, merge_file, len_ptr ? *len_ptr + 1 : MAXPATHLEN);
|
|
fn_len = clean_fname(fn, CFN_COLLAPSE_DOT_DOT_DIRS);
|
|
}
|
|
|
|
/* If the name isn't in buf yet, it wasn't absolute. */
|
|
if (fn != buf) {
|
|
int d_len = dirbuf_len - prefix_skip;
|
|
if (d_len + fn_len >= MAXPATHLEN) {
|
|
rprintf(FERROR, "merge-file name overflows: %s\n",
|
|
rule_text(template, fn));
|
|
return NULL;
|
|
}
|
|
memcpy(buf, dirbuf + prefix_skip, d_len);
|
|
memcpy(buf + d_len, fn, fn_len + 1);
|
|
fn_len = clean_fname(buf, CFN_COLLAPSE_DOT_DOT_DIRS);
|
|
}
|
|
|
|
if (len_ptr)
|
|
*len_ptr = fn_len;
|
|
return buf;
|
|
}
|
|
|
|
/* Sets the dirbuf and dirbuf_len values. */
|
|
void set_filter_dir(const char *dir, unsigned int dirlen)
|
|
{
|
|
unsigned int len;
|
|
if (*dir != '/') {
|
|
memcpy(dirbuf, curr_dir, curr_dir_len);
|
|
dirbuf[curr_dir_len] = '/';
|
|
len = curr_dir_len + 1;
|
|
if (len + dirlen >= MAXPATHLEN)
|
|
dirlen = 0;
|
|
} else
|
|
len = 0;
|
|
memcpy(dirbuf + len, dir, dirlen);
|
|
dirbuf[dirlen + len] = '\0';
|
|
dirbuf_len = clean_fname(dirbuf, CFN_COLLAPSE_DOT_DOT_DIRS);
|
|
if (dirbuf_len > 1 && dirbuf[dirbuf_len-1] == '.'
|
|
&& dirbuf[dirbuf_len-2] == '/')
|
|
dirbuf_len -= 2;
|
|
if (dirbuf_len != 1)
|
|
dirbuf[dirbuf_len++] = '/';
|
|
dirbuf[dirbuf_len] = '\0';
|
|
if (sanitize_paths)
|
|
dirbuf_depth = count_dir_elements(dirbuf + module_dirlen);
|
|
}
|
|
|
|
/* This routine takes a per-dir merge-file entry and finishes its setup.
|
|
* If the name has a path portion then we check to see if it refers to a
|
|
* parent directory of the first transfer dir. If it does, we scan all the
|
|
* dirs from that point through the parent dir of the transfer dir looking
|
|
* for the per-dir merge-file in each one. */
|
|
static BOOL setup_merge_file(int mergelist_num, filter_rule *ex,
|
|
filter_rule_list *lp)
|
|
{
|
|
char buf[MAXPATHLEN];
|
|
char *x, *y, *pat = ex->pattern;
|
|
unsigned int len;
|
|
|
|
if (!(x = parse_merge_name(ex, pat, NULL, 0)) || *x != '/')
|
|
return 0;
|
|
|
|
if (DEBUG_GTE(FILTER, 2)) {
|
|
rprintf(FINFO, "[%s] performing parent_dirscan for mergelist #%d%s\n",
|
|
who_am_i(), mergelist_num, lp->debug_type);
|
|
}
|
|
y = strrchr(x, '/');
|
|
*y = '\0';
|
|
ex->pattern = strdup(y+1);
|
|
if (!*x)
|
|
x = "/";
|
|
if (*x == '/')
|
|
strlcpy(buf, x, MAXPATHLEN);
|
|
else
|
|
pathjoin(buf, MAXPATHLEN, dirbuf, x);
|
|
|
|
len = clean_fname(buf, CFN_COLLAPSE_DOT_DOT_DIRS);
|
|
if (len != 1 && len < MAXPATHLEN-1) {
|
|
buf[len++] = '/';
|
|
buf[len] = '\0';
|
|
}
|
|
/* This ensures that the specified dir is a parent of the transfer. */
|
|
for (x = buf, y = dirbuf; *x && *x == *y; x++, y++) {}
|
|
if (*x)
|
|
y += strlen(y); /* nope -- skip the scan */
|
|
|
|
parent_dirscan = True;
|
|
while (*y) {
|
|
char save[MAXPATHLEN];
|
|
/* copylen is strlen(y) which is < MAXPATHLEN. +1 for \0 */
|
|
size_t copylen = strlcpy(save, y, MAXPATHLEN) + 1;
|
|
*y = '\0';
|
|
dirbuf_len = y - dirbuf;
|
|
strlcpy(x, ex->pattern, MAXPATHLEN - (x - buf));
|
|
parse_filter_file(lp, buf, ex, XFLG_ANCHORED2ABS);
|
|
if (ex->rflags & FILTRULE_NO_INHERIT) {
|
|
/* Free the undesired rules to clean up any per-dir
|
|
* mergelists they defined. Otherwise pop_local_filters
|
|
* may crash trying to restore nonexistent state for
|
|
* those mergelists. */
|
|
free_filters(lp->head);
|
|
lp->head = NULL;
|
|
}
|
|
lp->tail = NULL;
|
|
strlcpy(y, save, copylen);
|
|
while ((*x++ = *y++) != '/') {}
|
|
}
|
|
parent_dirscan = False;
|
|
if (DEBUG_GTE(FILTER, 2)) {
|
|
rprintf(FINFO, "[%s] completed parent_dirscan for mergelist #%d%s\n",
|
|
who_am_i(), mergelist_num, lp->debug_type);
|
|
}
|
|
free(pat);
|
|
return 1;
|
|
}
|
|
|
|
struct local_filter_state {
|
|
int mergelist_cnt;
|
|
filter_rule_list mergelists[1];
|
|
};
|
|
|
|
/* Each time rsync changes to a new directory it call this function to
|
|
* handle all the per-dir merge-files. The "dir" value is the current path
|
|
* relative to curr_dir (which might not be null-terminated). We copy it
|
|
* into dirbuf so that we can easily append a file name on the end. */
|
|
void *push_local_filters(const char *dir, unsigned int dirlen)
|
|
{
|
|
struct local_filter_state *push;
|
|
int i;
|
|
|
|
set_filter_dir(dir, dirlen);
|
|
if (DEBUG_GTE(FILTER, 2)) {
|
|
rprintf(FINFO, "[%s] pushing local filters for %s\n",
|
|
who_am_i(), dirbuf);
|
|
}
|
|
|
|
if (!mergelist_cnt) {
|
|
/* No old state to save and no new merge files to push. */
|
|
return NULL;
|
|
}
|
|
|
|
push = (struct local_filter_state *)new_array(char,
|
|
sizeof (struct local_filter_state)
|
|
+ (mergelist_cnt-1) * sizeof (filter_rule_list));
|
|
|
|
push->mergelist_cnt = mergelist_cnt;
|
|
for (i = 0; i < mergelist_cnt; i++) {
|
|
filter_rule *ex = mergelist_parents[i];
|
|
if (!ex)
|
|
continue;
|
|
memcpy(&push->mergelists[i], ex->u.mergelist, sizeof (filter_rule_list));
|
|
}
|
|
|
|
/* Note: parse_filter_file() might increase mergelist_cnt, so keep
|
|
* this loop separate from the above loop. */
|
|
for (i = 0; i < mergelist_cnt; i++) {
|
|
filter_rule *ex = mergelist_parents[i];
|
|
filter_rule_list *lp;
|
|
if (!ex)
|
|
continue;
|
|
lp = ex->u.mergelist;
|
|
|
|
if (DEBUG_GTE(FILTER, 2)) {
|
|
rprintf(FINFO, "[%s] pushing mergelist #%d%s\n",
|
|
who_am_i(), i, lp->debug_type);
|
|
}
|
|
|
|
lp->tail = NULL; /* Switch any local rules to inherited. */
|
|
if (ex->rflags & FILTRULE_NO_INHERIT)
|
|
lp->head = NULL;
|
|
|
|
if (ex->rflags & FILTRULE_FINISH_SETUP) {
|
|
ex->rflags &= ~FILTRULE_FINISH_SETUP;
|
|
if (setup_merge_file(i, ex, lp))
|
|
set_filter_dir(dir, dirlen);
|
|
}
|
|
|
|
if (strlcpy(dirbuf + dirbuf_len, ex->pattern,
|
|
MAXPATHLEN - dirbuf_len) < MAXPATHLEN - dirbuf_len) {
|
|
parse_filter_file(lp, dirbuf, ex,
|
|
XFLG_ANCHORED2ABS);
|
|
} else {
|
|
io_error |= IOERR_GENERAL;
|
|
rprintf(FERROR,
|
|
"cannot add local filter rules in long-named directory: %s\n",
|
|
rule_text(ex, full_fname(dirbuf)));
|
|
}
|
|
dirbuf[dirbuf_len] = '\0';
|
|
}
|
|
|
|
return (void*)push;
|
|
}
|
|
|
|
void pop_local_filters(void *mem)
|
|
{
|
|
struct local_filter_state *pop = (struct local_filter_state *)mem;
|
|
int i;
|
|
int old_mergelist_cnt = pop ? pop->mergelist_cnt : 0;
|
|
|
|
if (DEBUG_GTE(FILTER, 2))
|
|
rprintf(FINFO, "[%s] popping local filters\n", who_am_i());
|
|
|
|
for (i = mergelist_cnt; i-- > 0; ) {
|
|
filter_rule *ex = mergelist_parents[i];
|
|
filter_rule_list *lp;
|
|
if (!ex)
|
|
continue;
|
|
lp = ex->u.mergelist;
|
|
|
|
if (DEBUG_GTE(FILTER, 2)) {
|
|
rprintf(FINFO, "[%s] popping mergelist #%d%s\n",
|
|
who_am_i(), i, lp->debug_type);
|
|
}
|
|
|
|
pop_filter_list(lp);
|
|
if (i >= old_mergelist_cnt && lp->head) {
|
|
/* This mergelist does not exist in the state to be restored, but it
|
|
* still has inherited rules. This can sometimes happen if a per-dir
|
|
* merge file calls setup_merge_file() in push_local_filters() and that
|
|
* leaves some inherited rules that aren't in the pushed list state. */
|
|
if (DEBUG_GTE(FILTER, 2)) {
|
|
rprintf(FINFO, "[%s] freeing parent_dirscan filters of mergelist #%d%s\n",
|
|
who_am_i(), i, ex->u.mergelist->debug_type);
|
|
}
|
|
pop_filter_list(lp);
|
|
}
|
|
}
|
|
|
|
if (!pop)
|
|
return; /* No state to restore. */
|
|
|
|
for (i = 0; i < old_mergelist_cnt; i++) {
|
|
filter_rule *ex = mergelist_parents[i];
|
|
if (!ex)
|
|
continue;
|
|
memcpy(ex->u.mergelist, &pop->mergelists[i], sizeof (filter_rule_list));
|
|
}
|
|
|
|
free(pop);
|
|
}
|
|
|
|
void change_local_filter_dir(const char *dname, int dlen, int dir_depth)
|
|
{
|
|
static int cur_depth = -1;
|
|
static void *filt_array[MAXPATHLEN/2+1];
|
|
|
|
if (!dname) {
|
|
for ( ; cur_depth >= 0; cur_depth--) {
|
|
if (filt_array[cur_depth]) {
|
|
pop_local_filters(filt_array[cur_depth]);
|
|
filt_array[cur_depth] = NULL;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
assert(dir_depth < MAXPATHLEN/2+1);
|
|
|
|
for ( ; cur_depth >= dir_depth; cur_depth--) {
|
|
if (filt_array[cur_depth]) {
|
|
pop_local_filters(filt_array[cur_depth]);
|
|
filt_array[cur_depth] = NULL;
|
|
}
|
|
}
|
|
|
|
cur_depth = dir_depth;
|
|
filt_array[cur_depth] = push_local_filters(dname, dlen);
|
|
}
|
|
|
|
static int rule_matches(const char *fname, filter_rule *ex, int name_flags)
|
|
{
|
|
int slash_handling, str_cnt = 0, anchored_match = 0;
|
|
int ret_match = ex->rflags & FILTRULE_NEGATE ? 0 : 1;
|
|
const char *p, *pattern = ex->pattern;
|
|
const char *strings[16]; /* more than enough */
|
|
const char *name = fname + (*fname == '/');
|
|
|
|
if (!*name || ex->elide == cur_elide_value)
|
|
return 0;
|
|
|
|
if (!(name_flags & NAME_IS_XATTR) ^ !(ex->rflags & FILTRULE_XATTR))
|
|
return 0;
|
|
|
|
if (!ex->u.slash_cnt && !(ex->rflags & FILTRULE_WILD2)) {
|
|
/* If the pattern does not have any slashes AND it does
|
|
* not have a "**" (which could match a slash), then we
|
|
* just match the name portion of the path. */
|
|
if ((p = strrchr(name,'/')) != NULL)
|
|
name = p+1;
|
|
} else if (ex->rflags & FILTRULE_ABS_PATH && *fname != '/'
|
|
&& curr_dir_len > module_dirlen + 1) {
|
|
/* If we're matching against an absolute-path pattern,
|
|
* we need to prepend our full path info. */
|
|
strings[str_cnt++] = curr_dir + module_dirlen + 1;
|
|
strings[str_cnt++] = "/";
|
|
} else if (ex->rflags & FILTRULE_WILD2_PREFIX && *fname != '/') {
|
|
/* Allow "**"+"/" to match at the start of the string. */
|
|
strings[str_cnt++] = "/";
|
|
}
|
|
strings[str_cnt++] = name;
|
|
if (name_flags & NAME_IS_DIR) {
|
|
/* Allow a trailing "/"+"***" to match the directory. */
|
|
if (ex->rflags & FILTRULE_WILD3_SUFFIX)
|
|
strings[str_cnt++] = "/";
|
|
} else if (ex->rflags & FILTRULE_DIRECTORY)
|
|
return !ret_match;
|
|
strings[str_cnt] = NULL;
|
|
|
|
if (*pattern == '/') {
|
|
anchored_match = 1;
|
|
pattern++;
|
|
}
|
|
|
|
if (!anchored_match && ex->u.slash_cnt
|
|
&& !(ex->rflags & FILTRULE_WILD2)) {
|
|
/* A non-anchored match with an infix slash and no "**"
|
|
* needs to match the last slash_cnt+1 name elements. */
|
|
slash_handling = ex->u.slash_cnt + 1;
|
|
} else if (!anchored_match && !(ex->rflags & FILTRULE_WILD2_PREFIX)
|
|
&& ex->rflags & FILTRULE_WILD2) {
|
|
/* A non-anchored match with an infix or trailing "**" (but not
|
|
* a prefixed "**") needs to try matching after every slash. */
|
|
slash_handling = -1;
|
|
} else {
|
|
/* The pattern matches only at the start of the path or name. */
|
|
slash_handling = 0;
|
|
}
|
|
|
|
if (ex->rflags & FILTRULE_WILD) {
|
|
if (wildmatch_array(pattern, strings, slash_handling))
|
|
return ret_match;
|
|
} else if (str_cnt > 1) {
|
|
if (litmatch_array(pattern, strings, slash_handling))
|
|
return ret_match;
|
|
} else if (anchored_match) {
|
|
if (strcmp(name, pattern) == 0)
|
|
return ret_match;
|
|
} else {
|
|
int l1 = strlen(name);
|
|
int l2 = strlen(pattern);
|
|
if (l2 <= l1 &&
|
|
strcmp(name+(l1-l2),pattern) == 0 &&
|
|
(l1==l2 || name[l1-(l2+1)] == '/')) {
|
|
return ret_match;
|
|
}
|
|
}
|
|
|
|
return !ret_match;
|
|
}
|
|
|
|
static void report_filter_result(enum logcode code, char const *name,
|
|
filter_rule const *ent,
|
|
int name_flags, const char *type)
|
|
{
|
|
int log_level = am_sender || am_generator ? 1 : 3;
|
|
|
|
/* If a trailing slash is present to match only directories,
|
|
* then it is stripped out by add_rule(). So as a special
|
|
* case we add it back in the log output. */
|
|
if (DEBUG_GTE(FILTER, log_level)) {
|
|
static char *actions[2][2]
|
|
= { {"show", "hid"}, {"risk", "protect"} };
|
|
const char *w = who_am_i();
|
|
const char *t = name_flags & NAME_IS_XATTR ? "xattr"
|
|
: name_flags & NAME_IS_DIR ? "directory"
|
|
: "file";
|
|
rprintf(code, "[%s] %sing %s %s because of pattern %s%s%s\n",
|
|
w, actions[*w=='g'][!(ent->rflags & FILTRULE_INCLUDE)],
|
|
t, name, rule_text(ent, ent->pattern),
|
|
rule_detail(ent, ent->rflags & FILTRULE_DIRECTORY ? "/" : ""), type);
|
|
}
|
|
}
|
|
|
|
/* 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. */
|
|
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) {
|
|
if (!(name_flags & NAME_IS_XATTR))
|
|
errno = ENOENT;
|
|
return 1;
|
|
}
|
|
|
|
if (filter_level != ALL_FILTERS)
|
|
return 0;
|
|
|
|
if (filter_list.head && check_filter(&filter_list, FINFO, fname, name_flags) < 0)
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int check_server_filter(filter_rule_list *listp, enum logcode code, const char *name, int name_flags)
|
|
{
|
|
int ret;
|
|
cur_elide_value = LOCAL_RULE;
|
|
ret = check_filter(listp, code, name, name_flags);
|
|
cur_elide_value = REMOTE_RULE;
|
|
return ret;
|
|
}
|
|
|
|
/* Returns 1 if `name` matches an implied-parent rule (a directory component
|
|
* seeded by add_implied_include() with FILTRULE_DIRECTORY) but not a leaf
|
|
* rule -- i.e. the client asked for something under the dir, never the dir
|
|
* itself as content.
|
|
*
|
|
* The receiver uses this to refuse a malicious sender that sets XMIT_TOP_DIR
|
|
* without XMIT_NO_CONTENT_DIR on such a dir: the honest encoding is both flags
|
|
* (flist.c send path), so otherwise the receiver would set FLAG_CONTENT_DIR
|
|
* and delete_in_dir() could sweep pre-existing siblings under --delete. */
|
|
int is_implied_parent_dir(const char *name)
|
|
{
|
|
filter_rule *ent;
|
|
int parent_match = 0;
|
|
|
|
if (!implied_filter_list.head)
|
|
return 0;
|
|
|
|
/* The receiver exempts its synthetic transfer-root entry from the
|
|
* requested-name filter. Treat it as parent-only unless an empty/root
|
|
* source argument added the root-content rule. */
|
|
if ((name[0] == '.' && name[1] == '\0')
|
|
|| (name[0] == '/' && name[1] == '.' && name[2] == '\0')) {
|
|
for (ent = implied_filter_list.head; ent; ent = ent->next) {
|
|
if (!(ent->rflags & FILTRULE_INCLUDE))
|
|
continue;
|
|
if (strcmp(ent->pattern, "/**") == 0
|
|
|| strcmp(ent->pattern, "/*") == 0)
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
for (ent = implied_filter_list.head; ent; ent = ent->next) {
|
|
if (ent->rflags & (FILTRULE_PERDIR_MERGE | FILTRULE_CVS_IGNORE))
|
|
continue;
|
|
if (!rule_matches(name, ent, NAME_IS_DIR))
|
|
continue;
|
|
if (!(ent->rflags & FILTRULE_INCLUDE))
|
|
continue;
|
|
if (ent->rflags & FILTRULE_DIRECTORY) {
|
|
parent_match = 1;
|
|
continue;
|
|
}
|
|
/* A non-DIRECTORY include rule = a leaf the client asked for, so
|
|
* the dir is legitimately in the list, not parent-only. */
|
|
return 0;
|
|
}
|
|
return parent_match;
|
|
}
|
|
|
|
/* 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. */
|
|
int check_filter(filter_rule_list *listp, enum logcode code,
|
|
const char *name, int name_flags)
|
|
{
|
|
filter_rule *ent;
|
|
|
|
for (ent = listp->head; ent; ent = ent->next) {
|
|
if (ignore_perishable && ent->rflags & FILTRULE_PERISHABLE)
|
|
continue;
|
|
if (ent->rflags & FILTRULE_PERDIR_MERGE) {
|
|
int rc = check_filter(ent->u.mergelist, code, name, name_flags);
|
|
if (rc)
|
|
return rc;
|
|
continue;
|
|
}
|
|
if (ent->rflags & FILTRULE_CVS_IGNORE) {
|
|
int rc = check_filter(&cvs_filter_list, code, name, name_flags);
|
|
if (rc)
|
|
return rc;
|
|
continue;
|
|
}
|
|
if (rule_matches(name, ent, name_flags)) {
|
|
report_filter_result(code, name, ent, name_flags, listp->debug_type);
|
|
return ent->rflags & FILTRULE_INCLUDE ? 1 : -1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
#define RULE_STRCMP(s,r) rule_strcmp((s), (r), sizeof (r) - 1)
|
|
|
|
static const uchar *rule_strcmp(const uchar *str, const char *rule, int rule_len)
|
|
{
|
|
if (strncmp((char*)str, rule, rule_len) != 0)
|
|
return NULL;
|
|
if (isspace(str[rule_len]) || str[rule_len] == '_' || !str[rule_len])
|
|
return str + rule_len - 1;
|
|
if (str[rule_len] == ',')
|
|
return str + rule_len;
|
|
return NULL;
|
|
}
|
|
|
|
#define FILTRULES_FROM_CONTAINER (FILTRULE_ABS_PATH | FILTRULE_INCLUDE \
|
|
| FILTRULE_DIRECTORY | FILTRULE_NEGATE \
|
|
| FILTRULE_PERISHABLE)
|
|
|
|
/* Gets the next include/exclude rule from *rulestr_ptr and advances
|
|
* *rulestr_ptr to point beyond it. Stores the pattern's start (within
|
|
* *rulestr_ptr) and length in *pat_ptr and *pat_len_ptr, and returns a newly
|
|
* allocated filter_rule containing the rest of the information. Returns
|
|
* NULL if there are no more rules in the input.
|
|
*
|
|
* The template provides defaults for the new rule to inherit, and the
|
|
* template rflags and the xflags additionally affect parsing. */
|
|
static filter_rule *parse_rule_tok(const char **rulestr_ptr,
|
|
const filter_rule *template, int xflags,
|
|
const char **pat_ptr, unsigned int *pat_len_ptr)
|
|
{
|
|
const uchar *s = (const uchar *)*rulestr_ptr;
|
|
filter_rule *rule;
|
|
unsigned int len;
|
|
|
|
if (template->rflags & FILTRULE_WORD_SPLIT) {
|
|
/* Skip over any initial whitespace. */
|
|
while (isspace(*s))
|
|
s++;
|
|
/* Update to point to real start of rule. */
|
|
*rulestr_ptr = (const char *)s;
|
|
}
|
|
if (!*s)
|
|
return NULL;
|
|
|
|
rule = new0(filter_rule);
|
|
|
|
/* Inherit from the template. Don't inherit FILTRULES_SIDES; we check
|
|
* that later. */
|
|
rule->rflags = template->rflags & FILTRULES_FROM_CONTAINER;
|
|
if (rule_src_in_file)
|
|
rule->rflags |= FILTRULE_FROM_FILE; /* before parse_merge_name() */
|
|
|
|
/* 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
|
|
* or an exclude based on the inheritance of the FILTRULE_INCLUDE
|
|
* flag (above). XFLG_OLD_PREFIXES indicates a compatibility mode
|
|
* for old include/exclude patterns where just "+ " and "- " are
|
|
* allowed as optional prefixes. */
|
|
if (template->rflags & FILTRULE_NO_PREFIXES) {
|
|
if (*s == '!' && template->rflags & FILTRULE_CVS_IGNORE)
|
|
rule->rflags |= FILTRULE_CLEAR_LIST; /* Tentative! */
|
|
} else if (xflags & XFLG_OLD_PREFIXES) {
|
|
if (*s == '-' && s[1] == ' ') {
|
|
rule->rflags &= ~FILTRULE_INCLUDE;
|
|
s += 2;
|
|
} else if (*s == '+' && s[1] == ' ') {
|
|
rule->rflags |= FILTRULE_INCLUDE;
|
|
s += 2;
|
|
} else if (*s == '!')
|
|
rule->rflags |= FILTRULE_CLEAR_LIST; /* Tentative! */
|
|
} else {
|
|
char ch = 0;
|
|
BOOL prefix_specifies_side = False;
|
|
switch (*s) {
|
|
case 'c':
|
|
if ((s = RULE_STRCMP(s, "clear")) != NULL)
|
|
ch = '!';
|
|
break;
|
|
case 'd':
|
|
if ((s = RULE_STRCMP(s, "dir-merge")) != NULL)
|
|
ch = ':';
|
|
break;
|
|
case 'e':
|
|
if ((s = RULE_STRCMP(s, "exclude")) != NULL)
|
|
ch = '-';
|
|
break;
|
|
case 'h':
|
|
if ((s = RULE_STRCMP(s, "hide")) != NULL)
|
|
ch = 'H';
|
|
break;
|
|
case 'i':
|
|
if ((s = RULE_STRCMP(s, "include")) != NULL)
|
|
ch = '+';
|
|
break;
|
|
case 'm':
|
|
if ((s = RULE_STRCMP(s, "merge")) != NULL)
|
|
ch = '.';
|
|
break;
|
|
case 'p':
|
|
if ((s = RULE_STRCMP(s, "protect")) != NULL)
|
|
ch = 'P';
|
|
break;
|
|
case 'r':
|
|
if ((s = RULE_STRCMP(s, "risk")) != NULL)
|
|
ch = 'R';
|
|
break;
|
|
case 's':
|
|
if ((s = RULE_STRCMP(s, "show")) != NULL)
|
|
ch = 'S';
|
|
break;
|
|
default:
|
|
ch = *s;
|
|
if (s[1] == ',')
|
|
s++;
|
|
break;
|
|
}
|
|
switch (ch) {
|
|
case ':':
|
|
trust_sender_filter = 1;
|
|
rule->rflags |= FILTRULE_PERDIR_MERGE
|
|
| FILTRULE_FINISH_SETUP;
|
|
/* FALL THROUGH */
|
|
case '.':
|
|
rule->rflags |= FILTRULE_MERGE_FILE;
|
|
break;
|
|
case '+':
|
|
rule->rflags |= FILTRULE_INCLUDE;
|
|
break;
|
|
case '-':
|
|
break;
|
|
case 'S':
|
|
rule->rflags |= FILTRULE_INCLUDE;
|
|
/* FALL THROUGH */
|
|
case 'H':
|
|
rule->rflags |= FILTRULE_SENDER_SIDE;
|
|
prefix_specifies_side = True;
|
|
break;
|
|
case 'R':
|
|
rule->rflags |= FILTRULE_INCLUDE;
|
|
/* FALL THROUGH */
|
|
case 'P':
|
|
rule->rflags |= FILTRULE_RECEIVER_SIDE;
|
|
prefix_specifies_side = True;
|
|
break;
|
|
case '!':
|
|
rule->rflags |= FILTRULE_CLEAR_LIST;
|
|
break;
|
|
default:
|
|
filter_rule_err("Unknown filter rule", *rulestr_ptr);
|
|
}
|
|
while (ch != '!' && *++s && *s != ' ' && *s != '_') {
|
|
if (template->rflags & FILTRULE_WORD_SPLIT && isspace(*s)) {
|
|
s--;
|
|
break;
|
|
}
|
|
switch (*s) {
|
|
default:
|
|
invalid: {
|
|
char where[32];
|
|
snprintf(where, sizeof where, " '%c' at position %d",
|
|
*s, (int)(s - (const uchar *)*rulestr_ptr));
|
|
rprintf(FERROR, "invalid modifier%s in filter rule: %s\n",
|
|
rule_detail(NULL, where),
|
|
rule_text(NULL, *rulestr_ptr));
|
|
exit_cleanup(RERR_SYNTAX);
|
|
}
|
|
case '-':
|
|
if (!BITS_SETnUNSET(rule->rflags, FILTRULE_MERGE_FILE, FILTRULE_NO_PREFIXES))
|
|
goto invalid;
|
|
rule->rflags |= FILTRULE_NO_PREFIXES;
|
|
break;
|
|
case '+':
|
|
if (!BITS_SETnUNSET(rule->rflags, FILTRULE_MERGE_FILE, FILTRULE_NO_PREFIXES))
|
|
goto invalid;
|
|
rule->rflags |= FILTRULE_NO_PREFIXES
|
|
| FILTRULE_INCLUDE;
|
|
break;
|
|
case '/':
|
|
rule->rflags |= FILTRULE_ABS_PATH;
|
|
break;
|
|
case '!':
|
|
/* Negation really goes with the pattern, so it
|
|
* isn't useful as a merge-file default. */
|
|
if (rule->rflags & FILTRULE_MERGE_FILE)
|
|
goto invalid;
|
|
rule->rflags |= FILTRULE_NEGATE;
|
|
break;
|
|
case 'C':
|
|
if (rule->rflags & FILTRULE_NO_PREFIXES || prefix_specifies_side)
|
|
goto invalid;
|
|
rule->rflags |= FILTRULE_NO_PREFIXES
|
|
| FILTRULE_WORD_SPLIT
|
|
| FILTRULE_NO_INHERIT
|
|
| FILTRULE_CVS_IGNORE;
|
|
break;
|
|
case 'e':
|
|
if (!(rule->rflags & FILTRULE_MERGE_FILE))
|
|
goto invalid;
|
|
rule->rflags |= FILTRULE_EXCLUDE_SELF;
|
|
break;
|
|
case 'n':
|
|
if (!(rule->rflags & FILTRULE_MERGE_FILE))
|
|
goto invalid;
|
|
rule->rflags |= FILTRULE_NO_INHERIT;
|
|
break;
|
|
case 'p':
|
|
rule->rflags |= FILTRULE_PERISHABLE;
|
|
break;
|
|
case 'r':
|
|
if (prefix_specifies_side)
|
|
goto invalid;
|
|
rule->rflags |= FILTRULE_RECEIVER_SIDE;
|
|
break;
|
|
case 's':
|
|
if (prefix_specifies_side)
|
|
goto invalid;
|
|
rule->rflags |= FILTRULE_SENDER_SIDE;
|
|
break;
|
|
case 'w':
|
|
if (!(rule->rflags & FILTRULE_MERGE_FILE))
|
|
goto invalid;
|
|
rule->rflags |= FILTRULE_WORD_SPLIT;
|
|
break;
|
|
case 'x':
|
|
rule->rflags |= FILTRULE_XATTR;
|
|
saw_xattr_filter = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (*s)
|
|
s++;
|
|
}
|
|
if (template->rflags & FILTRULES_SIDES) {
|
|
if (rule->rflags & FILTRULES_SIDES) {
|
|
/* The filter and template both specify side(s). This
|
|
* is dodgy (and won't work correctly if the template is
|
|
* a one-sided per-dir merge rule), so reject it. */
|
|
filter_rule_err("specified-side merge file contains specified-side filter",
|
|
*rulestr_ptr);
|
|
}
|
|
rule->rflags |= template->rflags & FILTRULES_SIDES;
|
|
}
|
|
|
|
if (template->rflags & FILTRULE_WORD_SPLIT) {
|
|
const uchar *cp = s;
|
|
/* Token ends at whitespace or the end of the string. */
|
|
while (!isspace(*cp) && *cp != '\0')
|
|
cp++;
|
|
len = cp - s;
|
|
} else
|
|
len = strlen((char*)s);
|
|
|
|
if (rule->rflags & FILTRULE_CLEAR_LIST) {
|
|
if (!(template->rflags & FILTRULE_NO_PREFIXES)
|
|
&& !(xflags & XFLG_OLD_PREFIXES) && len) {
|
|
filter_rule_err("'!' rule has trailing characters", *rulestr_ptr);
|
|
}
|
|
if (len > 1)
|
|
rule->rflags &= ~FILTRULE_CLEAR_LIST;
|
|
} else if (!len && !(rule->rflags & FILTRULE_CVS_IGNORE)) {
|
|
filter_rule_err("unexpected end of filter rule", *rulestr_ptr);
|
|
}
|
|
|
|
/* --delete-excluded turns an un-modified include/exclude into a sender-side rule. */
|
|
if (delete_excluded
|
|
&& !(rule->rflags & (FILTRULES_SIDES|FILTRULE_MERGE_FILE|FILTRULE_PERDIR_MERGE)))
|
|
rule->rflags |= FILTRULE_SENDER_SIDE;
|
|
|
|
*pat_ptr = (const char *)s;
|
|
*pat_len_ptr = len;
|
|
*rulestr_ptr = *pat_ptr + len;
|
|
return rule;
|
|
}
|
|
|
|
static void get_cvs_excludes(uint32 rflags)
|
|
{
|
|
static int initialized = 0;
|
|
char *p, fname[MAXPATHLEN];
|
|
|
|
if (initialized)
|
|
return;
|
|
initialized = 1;
|
|
|
|
parse_filter_str(&cvs_filter_list, default_cvsignore(),
|
|
rule_template(rflags | (protocol_version >= 30 ? FILTRULE_PERISHABLE : 0)),
|
|
0);
|
|
|
|
p = module_id >= 0 && lp_use_chroot(module_id) ? "/" : getenv("HOME");
|
|
if (p && pathjoin(fname, MAXPATHLEN, p, ".cvsignore") < MAXPATHLEN)
|
|
parse_filter_file(&cvs_filter_list, fname, rule_template(rflags), 0);
|
|
|
|
parse_filter_str(&cvs_filter_list, getenv("CVSIGNORE"), rule_template(rflags), 0);
|
|
}
|
|
|
|
const filter_rule *rule_template(uint32 rflags)
|
|
{
|
|
static filter_rule template; /* zero-initialized */
|
|
template.rflags = rflags;
|
|
return &template;
|
|
}
|
|
|
|
void parse_filter_str(filter_rule_list *listp, const char *rulestr,
|
|
const filter_rule *template, int xflags)
|
|
{
|
|
filter_rule *rule;
|
|
const char *pat;
|
|
unsigned int pat_len;
|
|
|
|
if (!rulestr)
|
|
return;
|
|
|
|
while (1) {
|
|
uint32 new_rflags;
|
|
|
|
/* Remember that the returned string is NOT '\0' terminated! */
|
|
if (!(rule = parse_rule_tok(&rulestr, template, xflags, &pat, &pat_len)))
|
|
break;
|
|
|
|
if (pat_len >= MAXPATHLEN) {
|
|
rprintf(FERROR, "discarding over-long filter: %s\n",
|
|
rule_text_len(NULL, pat, (int)pat_len));
|
|
free_continue:
|
|
free_filter(rule);
|
|
continue;
|
|
}
|
|
|
|
new_rflags = rule->rflags;
|
|
if (new_rflags & FILTRULE_CLEAR_LIST) {
|
|
if (DEBUG_GTE(FILTER, 2)) {
|
|
rprintf(FINFO,
|
|
"[%s] clearing filter list%s\n",
|
|
who_am_i(), listp->debug_type);
|
|
}
|
|
pop_filter_list(listp);
|
|
listp->head = NULL;
|
|
goto free_continue;
|
|
}
|
|
|
|
if (new_rflags & FILTRULE_MERGE_FILE) {
|
|
if (!pat_len) {
|
|
pat = ".cvsignore";
|
|
pat_len = 10;
|
|
}
|
|
if (new_rflags & FILTRULE_EXCLUDE_SELF) {
|
|
const char *name;
|
|
filter_rule *excl_self;
|
|
|
|
excl_self = new0(filter_rule);
|
|
/* The pattern below is the merge rule's own text, so it
|
|
* inherits that rule's provenance. Built by hand, this
|
|
* rule looked argument-origin once parsing finished and
|
|
* the match trace echoed a merge file's contents at -vv. */
|
|
excl_self->rflags = rule->rflags & FILTRULE_FROM_FILE;
|
|
/* Find the beginning of the basename and add an exclude for it. */
|
|
for (name = pat + pat_len; name > pat && name[-1] != '/'; name--) {}
|
|
add_rule(listp, name, (pat + pat_len) - name, excl_self, 0);
|
|
rule->rflags &= ~FILTRULE_EXCLUDE_SELF;
|
|
}
|
|
if (new_rflags & FILTRULE_PERDIR_MERGE) {
|
|
if (parent_dirscan) {
|
|
const char *p;
|
|
unsigned int len = pat_len;
|
|
if ((p = parse_merge_name(rule, pat, &len, module_dirlen)))
|
|
add_rule(listp, p, len, rule, 0);
|
|
else
|
|
free_filter(rule);
|
|
continue;
|
|
}
|
|
} else {
|
|
const char *p;
|
|
unsigned int len = pat_len;
|
|
if ((p = parse_merge_name(rule, pat, &len, 0)))
|
|
parse_filter_file(listp, p, rule, XFLG_FATAL_ERRORS);
|
|
free_filter(rule);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
add_rule(listp, pat, pat_len, rule, xflags);
|
|
|
|
if (new_rflags & FILTRULE_CVS_IGNORE
|
|
&& !(new_rflags & FILTRULE_MERGE_FILE))
|
|
get_cvs_excludes(new_rflags);
|
|
}
|
|
}
|
|
|
|
void parse_filter_file(filter_rule_list *listp, const char *fname, const filter_rule *template, int xflags)
|
|
{
|
|
FILE *fp;
|
|
char line[BIGPATHBUFLEN];
|
|
char *eob = line + sizeof line - 1;
|
|
BOOL word_split = (template->rflags & FILTRULE_WORD_SPLIT) != 0;
|
|
const char *save_src_file, *save_src_named_at;
|
|
int save_src_line, save_src_in_file;
|
|
int named_by_file;
|
|
int pending = EOF;
|
|
char named_at[MAXPATHLEN + 32];
|
|
/* Our own copy: fname may point into parse_merge_name()'s static buffer,
|
|
* which a merge rule inside THIS file overwrites while we still need it. */
|
|
char src_name[MAXPATHLEN];
|
|
|
|
if (!fname || !*fname)
|
|
return;
|
|
|
|
if (merge_depth >= MAX_MERGE_DEPTH) {
|
|
rprintf(FERROR,
|
|
"[%s] merge-file include depth limit (%d) exceeded at %s\n",
|
|
who_am_i(), MAX_MERGE_DEPTH, rule_text(template, fname));
|
|
/* Match the failed-open path below: abort under a fatal
|
|
* (operator-supplied) merge, otherwise drop the rule. */
|
|
if (xflags & XFLG_FATAL_ERRORS)
|
|
exit_cleanup(RERR_FILEIO);
|
|
return;
|
|
}
|
|
merge_depth++;
|
|
|
|
if (*fname != '-' || fname[1] || am_server) {
|
|
/* This path is operator- and (via per-directory merge files like
|
|
* .cvsignore) sender-controlled: a planted symlink could leak a
|
|
* root-readable file through the filter parser, or redirect an
|
|
* --exclude-from open via a planted parent. Refuse symlinks not
|
|
* owned by uid 0 or our euid. */
|
|
const char *open_path;
|
|
int fd;
|
|
if (daemon_filter_list.head) {
|
|
char *dir;
|
|
strlcpy(line, fname, sizeof line);
|
|
/* parse_merge_name() prepends module_dir for absolute paths,
|
|
* so strip module_dirlen back off before the check or the
|
|
* anchored module-relative daemon rule won't match (as
|
|
* options.c does for --exclude-from/--include-from). The
|
|
* original absolute path is still used for the open below. */
|
|
dir = line + (*line == '/' ? module_dirlen : 0);
|
|
clean_fname(dir, CFN_COLLAPSE_DOT_DOT_DIRS);
|
|
if (check_filter(&daemon_filter_list, FLOG, dir, 0) < 0) {
|
|
/* Hidden by the daemon filter: treat the merge file as
|
|
* non-existent rather than tripping XFLG_FATAL_ERRORS
|
|
* below, so it neither errors out nor leaks a
|
|
* fatal-vs-silent oracle. */
|
|
if (DEBUG_GTE(FILTER, 2)) {
|
|
/* Same rule as everywhere else: the name is
|
|
* file content when a rule we read named it,
|
|
* and so is "the daemon filter hides it". */
|
|
rprintf(FINFO, "[%s] parse_filter_file(%s)%s\n",
|
|
who_am_i(), rule_text(template, fname),
|
|
rule_detail(template, " hidden by daemon filter"));
|
|
}
|
|
merge_depth--;
|
|
return;
|
|
}
|
|
open_path = line;
|
|
} else
|
|
open_path = fname;
|
|
|
|
/* Confine the open to the module root. The ownership walk on its own
|
|
* is not enough for a peer-driven merge file: a non-chrooted daemon
|
|
* writes --backup-dir entries as root, so a raced backup symlink is
|
|
* ROOT-owned -- exactly what open_no_attacker_symlinks() treats as
|
|
* trusted -- and naming it in a dir-merge rule would read an
|
|
* out-of-module file in as filter rules (their text comes back to the
|
|
* peer in "Unknown filter rule" errors).
|
|
*
|
|
* The daemon's own "filter"/"include from"/"exclude from" parameters
|
|
* are exempt: those are operator-configured and legitimately live
|
|
* outside the module (/etc/rsync/excludes and the like). */
|
|
int save_opr = operator_path_resolve;
|
|
if (!daemon_config_filter_file)
|
|
operator_path_resolve = 1;
|
|
fd = open_no_attacker_symlinks(open_path, O_RDONLY, 0);
|
|
operator_path_resolve = save_opr;
|
|
|
|
if (fd < 0)
|
|
fp = NULL;
|
|
else if (!(fp = fdopen(fd, "rb")))
|
|
close(fd);
|
|
} else
|
|
fp = stdin;
|
|
|
|
if (DEBUG_GTE(FILTER, 2)) {
|
|
/* The name is file CONTENT when a rule we read named it, and a
|
|
* word-split per-dir merge turns every word of a file into one
|
|
* of these -- so the trace would echo what the syntax errors no
|
|
* longer do. Say where it came from instead. */
|
|
rprintf(FINFO, "[%s] parse_filter_file(%s,%x,%x)%s\n",
|
|
who_am_i(), rule_text(template, fname), template->rflags, xflags,
|
|
rule_detail(template, fp ? "" : " [not found]"));
|
|
}
|
|
|
|
if (!fp) {
|
|
if (xflags & XFLG_FATAL_ERRORS) {
|
|
/* rule_src_file is still the PARENT's context here: when it
|
|
* is set, this name came out of a file we read, so neither
|
|
* the name nor errno (an existence oracle) may be shown. */
|
|
if (TEXT_FROM_FILE(template)) {
|
|
/* errno too: it answers "does this path exist". */
|
|
rprintf(FERROR, "failed to open %sclude file %s\n",
|
|
template->rflags & FILTRULE_INCLUDE ? "in" : "ex",
|
|
rule_text(template, fname));
|
|
} else {
|
|
rsyserr(FERROR, errno,
|
|
"failed to open %sclude file %s",
|
|
template->rflags & FILTRULE_INCLUDE ? "in" : "ex",
|
|
fname);
|
|
}
|
|
exit_cleanup(RERR_FILEIO);
|
|
}
|
|
merge_depth--;
|
|
return;
|
|
}
|
|
/* Before dirbuf is cut back: a per-directory fname points INTO dirbuf,
|
|
* so truncating first leaves only the directory and the location we
|
|
* report loses the filename. */
|
|
strlcpy(src_name, fname, sizeof src_name);
|
|
dirbuf[dirbuf_len] = '\0';
|
|
|
|
/* Rule text from here on is this file's contents, not an argument, so
|
|
* a syntax error must not echo it. Saved and restored because a merge
|
|
* rule inside this file can bring us back in for another file. */
|
|
save_src_in_file = rule_src_in_file;
|
|
save_src_file = rule_src_file;
|
|
save_src_line = rule_src_line;
|
|
/* If a rule we read named THIS file, our own path is file content too:
|
|
* track the location for provenance but do not put it in a message. */
|
|
named_by_file = TEXT_FROM_FILE(template);
|
|
save_src_named_at = rule_src_named_at;
|
|
if (named_by_file) {
|
|
/* Snapshot where we were told to merge this, before that state
|
|
* is replaced below (rule_src_where returns a static buffer).
|
|
* A DEFERRED merge has no live location to point at -- the file
|
|
* that named it was read and finished long ago -- so leave the
|
|
* generic description rather than nesting two vague ones. */
|
|
if (rule_src_in_file) {
|
|
strlcpy(named_at, rule_src_where(), sizeof named_at);
|
|
rule_src_named_at = named_at;
|
|
} else
|
|
rule_src_named_at = NULL;
|
|
}
|
|
rule_src_in_file = 1;
|
|
rule_src_file = named_by_file ? NULL : src_name;
|
|
rule_src_line = word_split ? -1 : 0; /* -1: tokens, not lines */
|
|
|
|
while (1) {
|
|
char *s = line;
|
|
int ch, overflow = 0;
|
|
if (rule_src_line >= 0)
|
|
rule_src_line++;
|
|
while (1) {
|
|
if (pending != EOF) { /* a CR lookahead we could not push back */
|
|
ch = pending;
|
|
pending = EOF;
|
|
} else if ((ch = getc(fp)) == EOF) {
|
|
if (ferror(fp) && errno == EINTR) {
|
|
clearerr(fp);
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
if (word_split && isspace(ch))
|
|
break;
|
|
if (eol_nulls? !ch : (ch == '\n' || ch == '\r')) {
|
|
if (ch == '\r') { /* CRLF is one line, not two */
|
|
int nxt;
|
|
while ((nxt = getc(fp)) == EOF
|
|
&& ferror(fp) && errno == EINTR)
|
|
clearerr(fp);
|
|
if (nxt == EOF) {
|
|
if (!ferror(fp))
|
|
ch = EOF; /* real end of file */
|
|
} else if (nxt != '\n' && ungetc(nxt, fp) == EOF) {
|
|
/* Pushback failed: hand it to the
|
|
* NEXT rule, where it belongs --
|
|
* appending it here would both
|
|
* corrupt this rule and skip the
|
|
* s < eob bound below. */
|
|
pending = nxt;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
if (s < eob)
|
|
*s++ = ch;
|
|
else
|
|
overflow = 1;
|
|
}
|
|
if (overflow) {
|
|
rprintf(FERROR, "discarding over-long filter: %s\n",
|
|
rule_text_len(NULL, line, 0));
|
|
s = line;
|
|
}
|
|
*s = '\0';
|
|
/* Skip an empty token and (when line parsing) comments. */
|
|
if (*line && (word_split || (*line != ';' && *line != '#'))) {
|
|
rule_src_file = named_by_file ? NULL : src_name;
|
|
parse_filter_str(listp, line, template, xflags);
|
|
}
|
|
if (ch == EOF)
|
|
break;
|
|
}
|
|
rule_src_in_file = save_src_in_file;
|
|
rule_src_file = save_src_file;
|
|
rule_src_line = save_src_line;
|
|
rule_src_named_at = save_src_named_at;
|
|
fclose(fp);
|
|
merge_depth--;
|
|
}
|
|
|
|
/* If the "for_xfer" flag is set, the prefix is made compatible with the
|
|
* current protocol_version (if possible) or a NULL is returned (if not
|
|
* possible). */
|
|
char *get_rule_prefix(filter_rule *rule, const char *pat, int for_xfer,
|
|
unsigned int *plen_ptr)
|
|
{
|
|
static char buf[MAX_RULE_PREFIX+1];
|
|
char *op = buf;
|
|
int legal_len = for_xfer && protocol_version < 29 ? 1 : MAX_RULE_PREFIX-1;
|
|
|
|
if (rule->rflags & FILTRULE_PERDIR_MERGE) {
|
|
if (legal_len == 1)
|
|
return NULL;
|
|
*op++ = ':';
|
|
} else if (rule->rflags & FILTRULE_INCLUDE)
|
|
*op++ = '+';
|
|
else if (legal_len != 1
|
|
|| ((*pat == '-' || *pat == '+') && pat[1] == ' '))
|
|
*op++ = '-';
|
|
else
|
|
legal_len = 0;
|
|
|
|
if (rule->rflags & FILTRULE_ABS_PATH)
|
|
*op++ = '/';
|
|
if (rule->rflags & FILTRULE_NEGATE)
|
|
*op++ = '!';
|
|
if (rule->rflags & FILTRULE_CVS_IGNORE)
|
|
*op++ = 'C';
|
|
else {
|
|
if (rule->rflags & FILTRULE_NO_INHERIT)
|
|
*op++ = 'n';
|
|
if (rule->rflags & FILTRULE_WORD_SPLIT)
|
|
*op++ = 'w';
|
|
if (rule->rflags & FILTRULE_NO_PREFIXES) {
|
|
if (rule->rflags & FILTRULE_INCLUDE)
|
|
*op++ = '+';
|
|
else
|
|
*op++ = '-';
|
|
}
|
|
}
|
|
if (rule->rflags & FILTRULE_EXCLUDE_SELF)
|
|
*op++ = 'e';
|
|
if (rule->rflags & FILTRULE_XATTR)
|
|
*op++ = 'x';
|
|
if (rule->rflags & FILTRULE_SENDER_SIDE
|
|
&& (!for_xfer || protocol_version >= 29))
|
|
*op++ = 's';
|
|
if (rule->rflags & FILTRULE_RECEIVER_SIDE
|
|
&& (!for_xfer || protocol_version >= 29
|
|
|| (delete_excluded && am_sender)))
|
|
*op++ = 'r';
|
|
if (rule->rflags & FILTRULE_PERISHABLE) {
|
|
if (!for_xfer || protocol_version >= 30)
|
|
*op++ = 'p';
|
|
else if (am_sender)
|
|
return NULL;
|
|
}
|
|
if (op - buf > legal_len)
|
|
return NULL;
|
|
if (legal_len)
|
|
*op++ = ' ';
|
|
*op = '\0';
|
|
if (plen_ptr)
|
|
*plen_ptr = op - buf;
|
|
return buf;
|
|
}
|
|
|
|
static void send_rules(int f_out, filter_rule_list *flp)
|
|
{
|
|
filter_rule *ent;
|
|
|
|
for (ent = flp->head; ent; ent = ent->next) {
|
|
unsigned int len, plen, dlen;
|
|
int elide = 0;
|
|
char *p;
|
|
|
|
/* Note we need to check delete_excluded here in addition to
|
|
* the code in parse_rule_tok() because some rules may have
|
|
* been added before we found the --delete-excluded option.
|
|
* We must also elide any CVS merge-file rules to avoid a
|
|
* backward compatibility problem, and we elide any no-prefix
|
|
* merge files as an optimization (since they can only have
|
|
* include/exclude rules). */
|
|
if (ent->rflags & FILTRULE_SENDER_SIDE)
|
|
elide = am_sender ? LOCAL_RULE : REMOTE_RULE;
|
|
if (ent->rflags & FILTRULE_RECEIVER_SIDE)
|
|
elide = elide ? 0 : am_sender ? REMOTE_RULE : LOCAL_RULE;
|
|
else if (delete_excluded && !elide
|
|
&& (!(ent->rflags & FILTRULE_PERDIR_MERGE)
|
|
|| ent->rflags & FILTRULE_NO_PREFIXES))
|
|
elide = am_sender ? LOCAL_RULE : REMOTE_RULE;
|
|
ent->elide = elide;
|
|
if (elide == LOCAL_RULE)
|
|
continue;
|
|
if (ent->rflags & FILTRULE_CVS_IGNORE
|
|
&& !(ent->rflags & FILTRULE_MERGE_FILE)) {
|
|
int f = am_sender || protocol_version < 29 ? f_out : -2;
|
|
send_rules(f, &cvs_filter_list);
|
|
if (f == f_out)
|
|
continue;
|
|
}
|
|
p = get_rule_prefix(ent, ent->pattern, 1, &plen);
|
|
if (!p) {
|
|
rprintf(FERROR,
|
|
"filter rules are too modern for remote rsync.\n");
|
|
exit_cleanup(RERR_PROTOCOL);
|
|
}
|
|
if (f_out < 0)
|
|
continue;
|
|
len = strlen(ent->pattern);
|
|
dlen = ent->rflags & FILTRULE_DIRECTORY ? 1 : 0;
|
|
if (!(plen + len + dlen))
|
|
continue;
|
|
write_int(f_out, plen + len + dlen);
|
|
if (plen)
|
|
write_buf(f_out, p, plen);
|
|
write_buf(f_out, ent->pattern, len);
|
|
if (dlen)
|
|
write_byte(f_out, '/');
|
|
}
|
|
}
|
|
|
|
/* This is only called by the client. */
|
|
void send_filter_list(int f_out)
|
|
{
|
|
int receiver_wants_list = prune_empty_dirs
|
|
|| (delete_mode && (!delete_excluded || protocol_version >= 29));
|
|
|
|
if (local_server || (am_sender && !receiver_wants_list))
|
|
f_out = -1;
|
|
if (cvs_exclude && am_sender) {
|
|
if (protocol_version >= 29)
|
|
parse_filter_str(&filter_list, ":C", rule_template(0), 0);
|
|
parse_filter_str(&filter_list, "-C", rule_template(0), 0);
|
|
}
|
|
|
|
send_rules(f_out, &filter_list);
|
|
|
|
if (f_out >= 0)
|
|
write_int(f_out, 0);
|
|
|
|
if (cvs_exclude) {
|
|
if (!am_sender || protocol_version < 29)
|
|
parse_filter_str(&filter_list, ":C", rule_template(0), 0);
|
|
if (!am_sender)
|
|
parse_filter_str(&filter_list, "-C", rule_template(0), 0);
|
|
}
|
|
}
|
|
|
|
/* This is only called by the server. */
|
|
void recv_filter_list(int f_in)
|
|
{
|
|
char line[BIGPATHBUFLEN];
|
|
int xflags = protocol_version >= 29 ? 0 : XFLG_OLD_PREFIXES;
|
|
int receiver_wants_list = prune_empty_dirs
|
|
|| (delete_mode && (!delete_excluded || protocol_version >= 29));
|
|
unsigned int len;
|
|
|
|
if (!local_server && (am_sender || receiver_wants_list)) {
|
|
while ((len = read_int(f_in)) != 0) {
|
|
if (len >= sizeof line)
|
|
overflow_exit("recv_rules");
|
|
read_sbuf(f_in, line, len);
|
|
parse_filter_str(&filter_list, line, rule_template(0), xflags);
|
|
}
|
|
}
|
|
|
|
if (cvs_exclude) {
|
|
if (local_server || am_sender || protocol_version < 29)
|
|
parse_filter_str(&filter_list, ":C", rule_template(0), 0);
|
|
if (local_server || am_sender)
|
|
parse_filter_str(&filter_list, "-C", rule_template(0), 0);
|
|
}
|
|
|
|
if (local_server) /* filter out any rules that aren't for us. */
|
|
send_rules(-1, &filter_list);
|
|
}
|