## Changes in this version:
-### OUTPUT CHANGES:
+### BEHAVIOR CHANGES:
+
+ - A new form of arg protection was added that works similarly to the older
+ `--protect-args` (`-s`) option but in a way that avoids breaking things like
+ rrsync (the restricted rsync script): rsync now uses backslash escaping for
+ sending "shell-active" characters to the remote shell. This includes spaces,
+ so fetching a remote file via a simple quoted filename value now works by
+ default without any extra quoting:
+
+ ```shell
+ rsync -aiv host:'a simple file.pdf' .
+ ```
+
+ Wildcards are not escaped in filename args, but they are escaped in options
+ like the `--suffix` and `--usermap` values. If your rsync script depends on
+ the old arg-splitting behavior, either run it with the `--old-args` option
+ or `export RSYNC_OLD_ARGS=1` in the script's environment.
- A long-standing bug was preventing rsync from figuring out the current
locale's decimal point character, which made rsync always output numbers
rprintf(FERROR, "internal: args[] overflowed in do_cmd()\n");
exit_cleanup(RERR_SYNTAX);
}
- if (**remote_argv == '-') {
- if (asprintf(args + argc++, "./%s", *remote_argv++) < 0)
- out_of_memory("do_cmd");
- } else
- args[argc++] = *remote_argv++;
+ args[argc++] = safe_arg(NULL, *remote_argv++);
remote_argc--;
}
}
char *filesfrom_host = NULL;
int eol_nulls = 0;
int protect_args = -1;
+int old_style_args = -1;
int human_readable = 1;
int recurse = 0;
int mkpath_dest_arg = 0;
{"files-from", 0, POPT_ARG_STRING, &files_from, 0, 0, 0 },
{"from0", '0', POPT_ARG_VAL, &eol_nulls, 1, 0, 0},
{"no-from0", 0, POPT_ARG_VAL, &eol_nulls, 0, 0, 0},
+ {"old-args", 0, POPT_ARG_VAL, &old_style_args, 1, 0, 0},
+ {"no-old-args", 0, POPT_ARG_VAL, &old_style_args, 0, 0, 0},
{"protect-args", 's', POPT_ARG_VAL, &protect_args, 1, 0, 0},
{"no-protect-args", 0, POPT_ARG_VAL, &protect_args, 0, 0, 0},
{"no-s", 0, POPT_ARG_VAL, &protect_args, 0, 0, 0},
max_alloc = size;
}
+ if (old_style_args < 0) {
+ if (!am_server && (arg = getenv("RSYNC_OLD_ARGS")) != NULL && *arg)
+ old_style_args = atoi(arg) ? 1 : 0;
+ else
+ old_style_args = 0;
+ }
+
if (protect_args < 0) {
if (am_server)
protect_args = 0;
}
+static char SPLIT_ARG_WHEN_OLD[1];
+
+/**
+ * Do backslash quoting of any weird chars in "arg", append the resulting
+ * string to the end of the "opt" (which gets a "=" appended if it is not
+ * an empty or NULL string), and return the (perhaps malloced) result.
+ * If opt is NULL, arg is considered a filename arg that allows wildcards.
+ * If it is "" or any other value, it is considered an option.
+ **/
+char *safe_arg(const char *opt, const char *arg)
+{
+#define SHELL_CHARS "!#$&;|<>(){}\"' \t\\"
+#define WILD_CHARS "*?[]" /* We don't allow remote brace expansion */
+ BOOL is_filename_arg = !opt;
+ char *escapes = is_filename_arg ? SHELL_CHARS : WILD_CHARS SHELL_CHARS;
+ BOOL escape_leading_dash = is_filename_arg && *arg == '-';
+ int len1 = opt && *opt ? strlen(opt) + 1 : 0;
+ int len2 = strlen(arg);
+ int extras = escape_leading_dash ? 2 : 0;
+ char *ret;
+ if (!protect_args && (!old_style_args || (!is_filename_arg && opt != SPLIT_ARG_WHEN_OLD))) {
+ const char *f;
+ for (f = arg; *f; f++) {
+ if (strchr(escapes, *f))
+ extras++;
+ }
+ }
+ if (!len1 && !extras)
+ return (char*)arg;
+ ret = new_array(char, len1 + len2 + extras + 1);
+ if (len1) {
+ memcpy(ret, opt, len1-1);
+ ret[len1-1] = '=';
+ }
+ if (escape_leading_dash) {
+ ret[len1++] = '.';
+ ret[len1++] = '/';
+ extras -= 2;
+ }
+ if (!extras)
+ memcpy(ret + len1, arg, len2);
+ else {
+ const char *f = arg;
+ char *t = ret + len1;
+ while (*f) {
+ if (strchr(escapes, *f))
+ *t++ = '\\';
+ *t++ = *f++;
+ }
+ }
+ ret[len1+len2+extras] = '\0';
+ return ret;
+}
+
+
/**
* Construct a filtered list of options to pass through from the
* client to the server.
set++;
else
set = iconv_opt;
- if (asprintf(&arg, "--iconv=%s", set) < 0)
- goto oom;
- args[ac++] = arg;
+ args[ac++] = safe_arg("--iconv", set);
}
#endif
}
if (backup_dir) {
+ /* This split idiom allows for ~/path expansion via the shell. */
args[ac++] = "--backup-dir";
- args[ac++] = backup_dir;
+ args[ac++] = safe_arg("", backup_dir);
}
/* Only send --suffix if it specifies a non-default value. */
- if (strcmp(backup_suffix, backup_dir ? "" : BACKUP_SUFFIX) != 0) {
- /* We use the following syntax to avoid weirdness with '~'. */
- if (asprintf(&arg, "--suffix=%s", backup_suffix) < 0)
- goto oom;
- args[ac++] = arg;
- }
+ if (strcmp(backup_suffix, backup_dir ? "" : BACKUP_SUFFIX) != 0)
+ args[ac++] = safe_arg("--suffix", backup_suffix);
- if (checksum_choice) {
- if (asprintf(&arg, "--checksum-choice=%s", checksum_choice) < 0)
- goto oom;
- args[ac++] = arg;
- }
+ if (checksum_choice)
+ args[ac++] = safe_arg("--checksum-choice", checksum_choice);
if (do_compression == CPRES_ZLIBX)
args[ac++] = "--new-compress";
else if (compress_choice && do_compression == CPRES_ZLIB)
args[ac++] = "--old-compress";
- else if (compress_choice) {
- if (asprintf(&arg, "--compress-choice=%s", compress_choice) < 0)
- goto oom;
- args[ac++] = arg;
- }
+ else if (compress_choice)
+ args[ac++] = safe_arg("--compress-choice", compress_choice);
if (am_sender) {
if (max_delete > 0) {
args[ac++] = arg;
} else if (max_delete == 0)
args[ac++] = "--max-delete=-1";
- if (min_size >= 0) {
- args[ac++] = "--min-size";
- args[ac++] = min_size_arg;
- }
- if (max_size >= 0) {
- args[ac++] = "--max-size";
- args[ac++] = max_size_arg;
- }
+ if (min_size >= 0)
+ args[ac++] = safe_arg("--min-size", min_size_arg);
+ if (max_size >= 0)
+ args[ac++] = safe_arg("--max-size", max_size_arg);
if (delete_before)
args[ac++] = "--delete-before";
else if (delete_during == 2)
if (do_stats)
args[ac++] = "--stats";
} else {
- if (skip_compress) {
- if (asprintf(&arg, "--skip-compress=%s", skip_compress) < 0)
- goto oom;
- args[ac++] = arg;
- }
+ if (skip_compress)
+ args[ac++] = safe_arg("--skip-compress", skip_compress);
}
- if (max_alloc_arg && max_alloc != DEFAULT_MAX_ALLOC) {
- args[ac++] = "--max-alloc";
- args[ac++] = max_alloc_arg;
- }
+ if (max_alloc_arg && max_alloc != DEFAULT_MAX_ALLOC)
+ args[ac++] = safe_arg("--max-alloc", max_alloc_arg);
/* --delete-missing-args needs the cooperation of both sides, but
* the sender can handle --ignore-missing-args by itself. */
if (partial_dir && am_sender) {
if (partial_dir != tmp_partialdir) {
args[ac++] = "--partial-dir";
- args[ac++] = partial_dir;
+ args[ac++] = safe_arg("", partial_dir);
}
if (delay_updates)
args[ac++] = "--delay-updates";
args[ac++] = "--use-qsort";
if (am_sender) {
- if (usermap) {
- if (asprintf(&arg, "--usermap=%s", usermap) < 0)
- goto oom;
- args[ac++] = arg;
- }
+ if (usermap)
+ args[ac++] = safe_arg("--usermap", usermap);
- if (groupmap) {
- if (asprintf(&arg, "--groupmap=%s", groupmap) < 0)
- goto oom;
- args[ac++] = arg;
- }
+ if (groupmap)
+ args[ac++] = safe_arg("--groupmap", groupmap);
if (ignore_existing)
args[ac++] = "--ignore-existing";
if (tmpdir) {
args[ac++] = "--temp-dir";
- args[ac++] = tmpdir;
+ args[ac++] = safe_arg("", tmpdir);
}
if (do_fsync)
*/
for (i = 0; i < basis_dir_cnt; i++) {
args[ac++] = alt_dest_opt(0);
- args[ac++] = basis_dir[i];
+ args[ac++] = safe_arg("", basis_dir[i]);
}
}
}
if (files_from && (!am_sender || filesfrom_host)) {
if (filesfrom_host) {
args[ac++] = "--files-from";
- args[ac++] = files_from;
+ args[ac++] = safe_arg("", files_from);
if (eol_nulls)
args[ac++] = "--from0";
} else {
exit_cleanup(RERR_SYNTAX);
}
for (j = 1; j <= remote_option_cnt; j++)
- args[ac++] = (char*)remote_options[j];
+ args[ac++] = safe_arg(SPLIT_ARG_WHEN_OLD, remote_options[j]);
}
*argc_p = ac;
import re, argparse
short_no_arg = { }
-short_with_num = { '@': 1 };
+short_with_num = { '@': 1 }
long_opts = { # These include some extra long-args that BackupPC uses:
'block-size': 1,
'daemon': -1,
continue
if last_long_opt:
- m = re.search(r'args\[ac\+\+\] = ([^["\s]+);', line)
+ m = re.search(r'args\[ac\+\+\] = safe_arg\("", ([^[("\s]+)\);', line)
if m:
long_opts[last_long_opt] = 2
last_long_opt = None
continue
+ if 'args[ac++] = ' in line:
+ last_long_opt = None
m = re.search(r'return "--([^"]+-dest)";', line)
if m:
if not m:
m = re.search(r'args\[ac\+\+\] = "--([^"=]+)=', line)
if not m:
- m = re.search(r'fmt = .*: "--([^"=]+)=', line)
+ m = re.search(r'args\[ac\+\+\] = safe_arg\("--([^"=]+)"', line)
+ if not m:
+ m = re.search(r'fmt = .*: "--([^"=]+)=', line)
if m:
long_opts[m.group(1)] = 1
last_long_opt = None
long_opts['files-from'] = 3
txt = """\
-### START of options data produced by the cull_options script. ###
+### START of options data produced by the cull-options script. ###
# To disable a short-named option, add its letter to this string:
"""
print("}")
else:
print(");")
- print("\n### END of options data produced by the cull_options script. ###")
+ print("\n### END of options data produced by the cull-options script. ###")
def str_assign(name, val, comment=None):
> rsync -av host:file1 :file2 host:file{3,4} /dest/
> rsync -av host::modname/file{1,2} host::modname/file3 /dest/
-> rsync -av host::modname/file1 ::modname/file{3,4}
+> rsync -av host::modname/file1 ::modname/file{3,4} /dest/
-Older versions of rsync required using quoted spaces in the SRC, like these
+**Older versions of rsync** required using quoted spaces in the SRC, like these
examples:
> rsync -av host:'dir1/file1 dir2/file2' /dest
> rsync host::'modname/dir1/file1 modname/dir2/file2' /dest
-This word-splitting still works (by default) in the latest rsync, but is not as
-easy to use as the first method.
-
-If you need to transfer a filename that contains whitespace, you can either
-specify the `--protect-args` (`-s`) option, or you'll need to escape the
-whitespace in a way that the remote shell will understand. For instance:
-
-> rsync -av host:'file\ name\ with\ spaces' /dest
+This word-splitting only works in a modern rsync by using `--old-args` (or its
+environment variable) and making sure that `--protect-args` is not enabled.
# CONNECTING TO AN RSYNC DAEMON
--append append data onto shorter files
--append-verify --append w/old data in file checksum
--dirs, -d transfer directories without recursing
+--old-dirs, --old-d works like --dirs when talking to old rsync
--mkpath create the destination's path component
--links, -l copy symlinks as symlinks
--copy-links, -L transform symlink into referent file/dir
--include-from=FILE read include patterns from FILE
--files-from=FILE read list of source-file names from FILE
--from0, -0 all *-from/filter files are delimited by 0s
+--old-args disable the modern arg-protection idiom
--protect-args, -s no space-splitting; wildcard chars only
--copy-as=USER[:GROUP] specify user & optional group for the copy
--address=ADDRESS bind address for outgoing socket to daemon
cause rsync to have a different idea about what data to expect next over
the socket, and that will make it fail in a cryptic fashion.
- Note that it is best to use a separate `--remote-option` for each option
- you want to pass. This makes your usage compatible with the
- `--protect-args` option. If that option is off, any spaces in your remote
- options will be split by the remote shell unless you take steps to protect
- them.
+ Note that you should use a separate `-M` option for each remote option you
+ want to pass. On older rsync versions, the presence of any spaces in the
+ remote-option arg could cause it to be split into separate remote args, but
+ this requires the use of `--old-args` in a modern rsync.
When performing a local transfer, the "local" side is the sender and the
"remote" side is the receiver.
files specified in a `--filter` rule. It does not affect `--cvs-exclude`
(since all names read from a .cvsignore file are split on whitespace).
+0. `--old-args`
+
+ This option tells rsync to stop trying to protect the arg values from
+ unintended word-splitting or other misinterpretation by using its new
+ backslash-escape idiom. The newest default is for remote filenames to only
+ allow wildcards characters to be interpretated by the shell while
+ protecting other shell-interpreted characters (and the args of options get
+ even wildcards escaped). The only active wildcard characters on the remote
+ side are: `*`, `?`, `[`, & `]`.
+
+ If you have a script that wants to use old-style arg splitting, this option
+ should get it going.
+
+ You may also control this setting via the RSYNC_OLD_ARGS environment
+ variable. If this variable has a non-zero value, this setting will be
+ enabled by default, otherwise it will be disabled by default. Either state
+ is overridden by a manually specified positive or negative version of this
+ option (the negative is `--no-old-args`).
+
0. `--protect-args`, `-s`
This option sends all filenames and most options to the remote rsync
- without allowing the remote shell to interpret them. This means that
- spaces are not split in names, and any non-wildcard special characters are
- not translated (such as `~`, `$`, `;`, `&`, etc.). Wildcards are expanded
- on the remote host by rsync (instead of the shell doing it).
+ without allowing the remote shell to interpret them. Wildcards are
+ expanded on the remote host by rsync instead of the shell doing it.
+
+ This is similar to the new-style backslash-escaping of args that was added
+ in 3.2.4, but supports some extra features and doesn't rely on backslash
+ escaping in the remote shell.
If you use this option with `--iconv`, the args related to the remote side
will also be translated from the local to the remote character-set. The
translation happens before wild-cards are expanded. See also the
`--files-from` option.
- You may also control this option via the RSYNC_PROTECT_ARGS environment
- variable. If this variable has a non-zero value, this option will be
+ You may also control this setting via the RSYNC_PROTECT_ARGS environment
+ variable. If this variable has a non-zero value, this setting will be
enabled by default, otherwise it will be disabled by default. Either state
is overridden by a manually specified positive or negative version of this
option (note that `--no-s` and `--no-protect-args` are the negative
- versions). Since this option was first introduced in 3.0.0, you'll need to
- make sure it's disabled if you ever need to interact with a remote rsync
- that is older than that.
+ versions).
- Rsync can also be configured (at build time) to have this option enabled by
- default (with is overridden by both the environment and the command-line).
- Run `rsync --version` to check if this is the case, as it will display
- "default protect-args" or "optional protect-args" depending on how it was
- compiled.
+ You may need to disable this option when interacting with an older rsync
+ (one prior to 3.0.0).
- This option will eventually become a new default setting at some
- as-yet-undetermined point in the future.
+ Note that this option is incompatible with the use of the restricted rsync
+ script (`rrsync`) since it hides options from the script's inspection.
0. `--copy-as=USER[:GROUP]`
The `--usermap` option implies the `--owner` option while the `--groupmap`
option implies the `--group` option.
- If your shell complains about the wildcards, use `--protect-args` (`-s`).
+ An older rsync client may need to use `--protect-args` (`-s`) to avoid a
+ complaint about wildcard characters, but a modern rsync handles this
+ automatically.
0. `--chown=USER:GROUP`
"`--usermap=*:foo --groupmap=*:bar`", only easier (with the same implied
`--owner` and/or `--group` option).
- If your shell complains about the wildcards, use `--protect-args` (`-s`).
+ An older rsync client may need to use `--protect-args` (`-s`) to avoid a
+ complaint about wildcard characters, but a modern rsync handles this
+ automatically.
0. `--timeout=SECONDS`
Specify a default `--iconv` setting using this environment variable. (First
supported in 3.0.0.)
+0. `RSYNC_OLD_ARGS`
+
+ Specify a non-zero numeric value if you want the `--old-args` option to be
+ enabled by default, or a zero value to make sure that it is disabled by
+ default. (First supported in 3.2.4.)
+
0. `RSYNC_PROTECT_ARGS`
Specify a non-zero numeric value if you want the `--protect-args` option to
# NOTE when disabling: check for both a short & long version of the option!
-### START of options data produced by the cull_options script. ###
+### START of options data produced by the cull-options script. ###
# To disable a short-named option, add its letter to this string:
short_disabled = 's'
'files-from': 3,
'force': 0,
'from0': 0,
- 'fsync': 2,
+ 'fsync': 0,
'fuzzy': 0,
'group': 0,
'groupmap': 1,
'write-devices': -1,
}
-### END of options data produced by the cull_options script. ###
+### END of options data produced by the cull-options script. ###
import os, sys, re, argparse, glob, socket, time, subprocess
from argparse import RawTextHelpFormatter