feat: add rsync old-args compatibility mode
CI / lint (pull_request) Successful in 11s
CI / sanitizers (address) (pull_request) Successful in 37s
CI / sanitizers (undefined) (pull_request) Successful in 37s
CI / fuzz-build (pull_request) Successful in 15s
CI / coverage (pull_request) Successful in 32s
CI / build-and-test (pull_request) Successful in 1m15s
CI / valgrind (pull_request) Successful in 34s

This commit is contained in:
2026-09-03 16:27:45 +02:00
parent 190fc5d300
commit 393a440c78
10 changed files with 92 additions and 10 deletions
+1 -1
View File
@@ -211,7 +211,7 @@ This document maps rsync's full feature set to FastSync's current implementation
| Max data/string/chunk sizes | Prevent OOM attacks | ✅ Implemented | Per-message limits | | Max data/string/chunk sizes | Prevent OOM attacks | ✅ Implemented | Per-message limits |
| Per-connection memory limit | 1GB per connection | ✅ Implemented | `MAX_CONNECTION_MEMORY` | | Per-connection memory limit | 1GB per connection | ✅ Implemented | `MAX_CONNECTION_MEMORY` |
| `--trust-sender` | Trust remote sender's file list | ❌ Not Implemented | | | `--trust-sender` | Trust remote sender's file list | ❌ Not Implemented | |
| `--old-args` | Disable modern arg protection | ❌ Not Implemented | | | `--old-args` | Disable modern arg protection | ✅ Implemented | SSH-only legacy mode; restores raw remote command construction and permits shell interpretation of the configured server path |
| `--ignore-missing-args` | Ignore missing source args | ❌ Not Implemented | | | `--ignore-missing-args` | Ignore missing source args | ❌ Not Implemented | |
| `--delete-missing-args` | Delete missing source args | ❌ Not Implemented | | | `--delete-missing-args` | Delete missing source args | ❌ Not Implemented | |
+1
View File
@@ -146,6 +146,7 @@ static const OptionEntry OPTION_TABLE[] = {
{"--backup", NULL, OPT_FLAG, offsetof(Config, backup)}, {"--backup", NULL, OPT_FLAG, offsetof(Config, backup)},
{"--stats", NULL, OPT_FLAG, offsetof(Config, stats)}, {"--stats", NULL, OPT_FLAG, offsetof(Config, stats)},
{"--partial", NULL, OPT_FLAG, offsetof(Config, partial)}, {"--partial", NULL, OPT_FLAG, offsetof(Config, partial)},
{"--old-args", NULL, OPT_FLAG, offsetof(Config, old_args)},
{"--links", "-l", OPT_FLAG, offsetof(Config, follow_symlinks)}, {"--links", "-l", OPT_FLAG, offsetof(Config, follow_symlinks)},
{"--copy-links", NULL, OPT_FLAG, offsetof(Config, copy_links)}, {"--copy-links", NULL, OPT_FLAG, offsetof(Config, copy_links)},
{"--safe-links", NULL, OPT_FLAG, offsetof(Config, safe_links)}, {"--safe-links", NULL, OPT_FLAG, offsetof(Config, safe_links)},
+1 -1
View File
@@ -46,7 +46,7 @@ static Client* connect_transfer_client(const Config* config) {
return NULL; return NULL;
} }
return client_connect_ssh(config->ssh_destination, config->ssh_port, return client_connect_ssh(config->ssh_destination, config->ssh_port,
config->fastsync_server_path); config->fastsync_server_path, config->old_args);
} }
Client* client = client_create(); Client* client = client_create();
+2
View File
@@ -62,6 +62,8 @@ void print_usage(void) {
printf(" --partial-dir <dir> Directory for partial files\n"); printf(" --partial-dir <dir> Directory for partial files\n");
printf(" --fastsync-server-path <path>\n"); printf(" --fastsync-server-path <path>\n");
printf(" Path to fastsync-server on remote (default: fastsync-server)\n"); printf(" Path to fastsync-server on remote (default: fastsync-server)\n");
printf(
" --old-args Disable safe SSH command argument quoting (legacy compatibility)\n");
printf(" -l, --links Copy symlinks as symlinks\n"); printf(" -l, --links Copy symlinks as symlinks\n");
printf(" --copy-links Transform symlinks into referent files\n"); printf(" --copy-links Transform symlinks into referent files\n");
printf(" --safe-links Skip symlinks that point outside transfer tree\n"); printf(" --safe-links Skip symlinks that point outside transfer tree\n");
+1
View File
@@ -82,6 +82,7 @@ static void config_set_defaults(Config* config) {
config->relative = false; config->relative = false;
config->rsh_command = NULL; config->rsh_command = NULL;
config->rsync_path = NULL; config->rsync_path = NULL;
config->old_args = false;
config->temp_dir = NULL; config->temp_dir = NULL;
config->compare_dest = NULL; config->compare_dest = NULL;
config->copy_dest = NULL; config->copy_dest = NULL;
+1
View File
@@ -96,6 +96,7 @@ typedef struct Config {
// Issue #130: Remote shell/connection options // Issue #130: Remote shell/connection options
char* rsh_command; char* rsh_command;
char* rsync_path; char* rsync_path;
bool old_args;
char* temp_dir; char* temp_dir;
char* compare_dest; char* compare_dest;
char* copy_dest; char* copy_dest;
+51 -3
View File
@@ -3,6 +3,7 @@
#include "utils.h" #include "utils.h"
#include <fcntl.h> #include <fcntl.h>
#include <stdio.h> #include <stdio.h>
#include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/socket.h> #include <sys/socket.h>
@@ -68,7 +69,52 @@ static int parse_remote_dest(const char* dest, RemoteDest* r) {
return 0; return 0;
} }
Client* client_connect_ssh(const char* destination, int port, const char* server_path) { char* ssh_build_remote_command(const char* server_path, bool old_args) {
const char* path = server_path ? server_path : "fastsync-server";
const char* suffix = " --stdio";
size_t path_len = strlen(path);
size_t suffix_len = strlen(suffix);
if (old_args) {
if (path_len > SIZE_MAX - suffix_len - 1)
return NULL;
char* command = malloc(path_len + suffix_len + 1);
if (!command)
return NULL;
memcpy(command, path, path_len);
memcpy(command + path_len, suffix, suffix_len + 1);
return command;
}
/* Quote the executable as one remote-shell word. This is the default safety boundary. */
size_t quote_count = 0;
for (const char* p = path; *p; p++)
if (*p == '\'')
quote_count++;
if (path_len > SIZE_MAX - suffix_len - 4 ||
quote_count > (SIZE_MAX - path_len - suffix_len - 4) / 4)
return NULL;
size_t command_len = path_len + quote_count * 4 + suffix_len + 4;
char* command = malloc(command_len + 1);
if (!command)
return NULL;
char* out = command;
*out++ = '\'';
for (const char* p = path; *p; p++) {
if (*p == '\'') {
memcpy(out, "'\\''", 4);
out += 4;
} else {
*out++ = *p;
}
}
*out++ = '\'';
memcpy(out, suffix, suffix_len + 1);
return command;
}
Client* client_connect_ssh(const char* destination, int port, const char* server_path,
bool old_args) {
RemoteDest r; RemoteDest r;
if (parse_remote_dest(destination, &r) != 0) { if (parse_remote_dest(destination, &r) != 0) {
fprintf(stderr, "Invalid remote destination: %s\n", destination); fprintf(stderr, "Invalid remote destination: %s\n", destination);
@@ -135,6 +181,9 @@ Client* client_connect_ssh(const char* destination, int port, const char* server
char* ssh_argv[16]; char* ssh_argv[16];
int ac = 0; int ac = 0;
char port_str[16]; char port_str[16];
char* remote_command = ssh_build_remote_command(server_path, old_args);
if (!remote_command)
_exit(1);
ssh_argv[ac++] = "ssh"; ssh_argv[ac++] = "ssh";
ssh_argv[ac++] = "-o"; ssh_argv[ac++] = "-o";
ssh_argv[ac++] = "Compression=no"; ssh_argv[ac++] = "Compression=no";
@@ -148,8 +197,7 @@ Client* client_connect_ssh(const char* destination, int port, const char* server
ssh_argv[ac++] = port_str; ssh_argv[ac++] = port_str;
} }
ssh_argv[ac++] = ssh_user; ssh_argv[ac++] = ssh_user;
ssh_argv[ac++] = (char*)(server_path ? server_path : "fastsync-server"); ssh_argv[ac++] = remote_command;
ssh_argv[ac++] = "--stdio";
ssh_argv[ac] = NULL; ssh_argv[ac] = NULL;
execvp("ssh", ssh_argv); execvp("ssh", ssh_argv);
log_perror("exec of ssh failed"); log_perror("exec of ssh failed");
+3 -1
View File
@@ -3,6 +3,8 @@
#include "transport_tcp.h" #include "transport_tcp.h"
Client* client_connect_ssh(const char* destination, int port, const char* server_path); Client* client_connect_ssh(const char* destination, int port, const char* server_path,
bool old_args);
char* ssh_build_remote_command(const char* server_path, bool old_args);
#endif #endif
+12
View File
@@ -339,6 +339,17 @@ static void test_parse_args_archive() {
config_delete(cfg); config_delete(cfg);
} }
static void test_parse_args_old_args() {
Config* cfg = config_create();
char* argv[] = {"fastsync", "--old-args", "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
EXPECT_TRUE(cfg->old_args);
config_delete(cfg);
}
void test_client_cli() { void test_client_cli() {
test_validate_config_required_paths(); test_validate_config_required_paths();
test_validate_config_incompatible_options(); test_validate_config_incompatible_options();
@@ -360,4 +371,5 @@ void test_client_cli() {
test_parse_args_unknown_option(); test_parse_args_unknown_option();
test_parse_args_rejects_unimplemented_options(); test_parse_args_rejects_unimplemented_options();
test_parse_args_archive(); test_parse_args_archive();
test_parse_args_old_args();
} }
+19 -4
View File
@@ -4,13 +4,13 @@
static void test_ssh_connect_invalid_dest_no_colon() { static void test_ssh_connect_invalid_dest_no_colon() {
/* cppcheck-suppress constVariablePointer */ /* cppcheck-suppress constVariablePointer */
Client* client = client_connect_ssh("invalid-destination-no-colon", 22, NULL); Client* client = client_connect_ssh("invalid-destination-no-colon", 22, NULL, false);
EXPECT_NULL(client); EXPECT_NULL(client);
} }
static void test_ssh_connect_invalid_dest_empty() { static void test_ssh_connect_invalid_dest_empty() {
/* cppcheck-suppress constVariablePointer */ /* cppcheck-suppress constVariablePointer */
Client* client = client_connect_ssh("", 22, NULL); Client* client = client_connect_ssh("", 22, NULL, false);
EXPECT_NULL(client); EXPECT_NULL(client);
} }
@@ -18,7 +18,7 @@ static void test_ssh_connect_invalid_dest_empty() {
* parse_remote_dest succeeds, ssh is exec'd and fails, but the function * parse_remote_dest succeeds, ssh is exec'd and fails, but the function
* creates a Client that must be cleaned up. */ * creates a Client that must be cleaned up. */
static void test_ssh_connect_malformed() { static void test_ssh_connect_malformed() {
Client* client = client_connect_ssh(":", 22, NULL); Client* client = client_connect_ssh(":", 22, NULL, false);
/* ssh binary exists, so exec succeeds; the function returns a Client. /* ssh binary exists, so exec succeeds; the function returns a Client.
* We just verify it doesn't crash and clean up properly. */ * We just verify it doesn't crash and clean up properly. */
if (client != NULL) { if (client != NULL) {
@@ -31,7 +31,7 @@ static void test_ssh_connect_malformed() {
/* Test client_connect_ssh with valid format but unreachable host. /* Test client_connect_ssh with valid format but unreachable host.
* The function launches ssh which will fail to connect, returns a Client. */ * The function launches ssh which will fail to connect, returns a Client. */
static void test_ssh_connect_unreachable() { static void test_ssh_connect_unreachable() {
Client* client = client_connect_ssh("nonexistent.invalid:/remote/path", 22, NULL); Client* client = client_connect_ssh("nonexistent.invalid:/remote/path", 22, NULL, false);
if (client != NULL) { if (client != NULL) {
client_disconnect(client); client_disconnect(client);
client_delete(client); client_delete(client);
@@ -39,9 +39,24 @@ static void test_ssh_connect_unreachable() {
EXPECT_TRUE(true); EXPECT_TRUE(true);
} }
static void test_ssh_remote_command_argument_modes() {
char* command = ssh_build_remote_command("fast sync; touch /tmp/pwned", false);
EXPECT_EQ_STR(command, "'fast sync; touch /tmp/pwned' --stdio");
free(command);
command = ssh_build_remote_command("fast'sync", false);
EXPECT_EQ_STR(command, "'fast'\\''sync' --stdio");
free(command);
command = ssh_build_remote_command("fast sync; touch /tmp/pwned", true);
EXPECT_EQ_STR(command, "fast sync; touch /tmp/pwned --stdio");
free(command);
}
void test_transport_ssh() { void test_transport_ssh() {
test_ssh_connect_invalid_dest_no_colon(); test_ssh_connect_invalid_dest_no_colon();
test_ssh_connect_invalid_dest_empty(); test_ssh_connect_invalid_dest_empty();
test_ssh_connect_malformed(); test_ssh_connect_malformed();
test_ssh_connect_unreachable(); test_ssh_connect_unreachable();
test_ssh_remote_command_argument_modes();
} }