feat(p6-iconv): --iconv charset conversion + PROTOCOL 2.16.0

This commit is contained in:
2026-09-10 17:13:01 +02:00
parent 6bb63c6f21
commit 6e02a24232
15 changed files with 585 additions and 32 deletions
+18
View File
@@ -1,5 +1,6 @@
#include "client_send.h"
#include "client_validation.h"
#include "charset.h"
#include "chmod.h"
#include "compression.h"
#include "config.h"
@@ -590,6 +591,11 @@ static const OptionEntry OPTION_TABLE[] = {
* path; main() reads it (after the destination form is known) and derives
* the wire credentials. Never crosses the wire. */
{"--password-file", NULL, OPT_STRING, offsetof(Config, password_file)},
/* --iconv (protocol 2.16.0): convert file-NAME charsets at the wire
* boundary. The CONVERT_SPEC (LOCAL[,REMOTE]) is validated for real iconv
* charsets at startup (client_validation.c) and the full spec rides the
* config frame so the receiver derives the wire charset symmetrically. */
{"--iconv", NULL, OPT_STRING, offsetof(Config, iconv_spec)},
{"--delete-before", NULL, OPT_FLAG, offsetof(Config, delete_before)},
{"--delete-during", "--del", OPT_FLAG, offsetof(Config, delete_during)},
{"--delete-delay", NULL, OPT_FLAG, offsetof(Config, delete_delay)},
@@ -1593,6 +1599,17 @@ int main(int argc, char* argv[]) {
goto cleanup;
}
/* --iconv: install the sender-side local->wire conversion before any path is
scanned or serialized (the scanner and the chunk/data path read windows are
all driven from this process, so one global initialization covers every
send site). */
if (!charset_wire_init_sender(config->iconv_spec)) {
log_message(LOG_LEVEL_ERROR,
"--iconv has an invalid CONVERT_SPEC or an unsupported charset name");
exit_code = 1;
goto cleanup;
}
/* Apply the requested --outbuf style now that the mode is parsed. */
apply_output_buffering(config);
@@ -1614,6 +1631,7 @@ int main(int argc, char* argv[]) {
}
cleanup:
charset_wire_free();
if (config) {
config_delete(config);
}
+10 -9
View File
@@ -1,6 +1,7 @@
#include "client_send.h"
#include "array_list.h"
#include "change_list.h"
#include "charset.h"
#include "chunk.h"
#include "compression.h"
#include "config.h"
@@ -810,21 +811,21 @@ static int send_delete_manifest(int fd, ArrayList* manifest, ArrayList* protecte
if (!send_int(fd, keep_count))
return -1;
for (int i = 0; i < keep_count; i++) {
if (!send_str(fd, (char*)manifest->items[i]))
if (!send_wire_str(fd, (char*)manifest->items[i]))
return -1;
}
int protected_count = protected_prefixes ? protected_prefixes->size : 0;
if (!send_int(fd, protected_count))
return -1;
for (int i = 0; i < protected_count; i++) {
if (!send_str(fd, (char*)protected_prefixes->items[i]))
if (!send_wire_str(fd, (char*)protected_prefixes->items[i]))
return -1;
}
int missing_count = missing_args ? missing_args->size : 0;
if (!send_int(fd, missing_count))
return -1;
for (int i = 0; i < missing_count; i++) {
if (!send_str(fd, (char*)missing_args->items[i]))
if (!send_wire_str(fd, (char*)missing_args->items[i]))
return -1;
}
return 0;
@@ -899,7 +900,7 @@ static int incremental_check(Client* client, File* file, const Config* config,
*resume_offset = 0;
if (!send_status(client->file_descriptor, STATUS_CHECK))
return -1;
if (!send_str(client->file_descriptor, file_wire_path(file)))
if (!send_wire_str(client->file_descriptor, file_wire_path(file)))
return -1;
unsigned long long fsize = file->data->size;
long long mtime = file->metadata ? file->metadata->mtime_sec : 0;
@@ -1119,7 +1120,7 @@ static bool send_directory_entry(Client* client, File* file) {
return false;
if (!send_status(client->file_descriptor, STATUS_MKDIR))
return false;
return send_str(client->file_descriptor, file_wire_path(file));
return send_wire_str(client->file_descriptor, file_wire_path(file));
}
/* Transmit one symlink entry: a STATUS_SYMLINK frame carrying the destination
@@ -1130,8 +1131,8 @@ static bool send_symlink_entry(const Client* client, File* file, const Config* c
if (!file || !file_wire_path(file) || !file->symlink_target)
return false;
int fd = client->file_descriptor;
if (!send_status(fd, STATUS_SYMLINK) || !send_str(fd, file_wire_path(file)) ||
!send_str(fd, file->symlink_target))
if (!send_status(fd, STATUS_SYMLINK) || !send_wire_str(fd, file_wire_path(file)) ||
!send_wire_str(fd, file->symlink_target))
return false;
return !config->use_metadata || metadata_send(fd, file->metadata);
}
@@ -1311,9 +1312,9 @@ static int send_chunk_with_removal(Client* client, Chunk* chunk, Config* config,
wire path so the receiver links this entry to that installed file. */
if (f->link_group != 0 && !f->link_first && f->hardlink_target != NULL) {
if (!send_status(client->file_descriptor, STATUS_HARDLINK) ||
!send_str(client->file_descriptor, file_wire_path(f)) ||
!send_wire_str(client->file_descriptor, file_wire_path(f)) ||
!send_int(client->file_descriptor, f->link_group) ||
!send_str(client->file_descriptor, f->hardlink_target))
!send_wire_str(client->file_descriptor, f->hardlink_target))
return -1;
change_emit_file_sent(config, f);
continue;
+9
View File
@@ -1,4 +1,5 @@
#include "client_validation.h"
#include "charset.h"
#include "delay_updates.h"
#include "log.h"
#include "usage.h"
@@ -123,5 +124,13 @@ bool validate_config(const Config* config) {
"timing; at most one may be given and each implies --delete");
return false;
}
/* --iconv: reject a malformed CONVERT_SPEC or an unsupported charset name at
startup (a probe iconv_open is attempted), so a typo'd charset never fails
the run mid-transfer with per-file errors. */
if (!charset_spec_valid(config->iconv_spec)) {
log_message(LOG_LEVEL_ERROR,
"--iconv requires LOCAL[,REMOTE] charset names supported by iconv");
return false;
}
return true;
}
+6
View File
@@ -35,6 +35,12 @@ void print_usage(void) {
printf(" --progress Show transfer progress\n");
printf(" -P Partial mode with progress (retention incomplete)\n");
printf(" -8, --8-bit-output Leave high-bit characters unescaped in output\n");
printf(" --iconv=LOCAL[,REMOTE] Convert file-NAME charsets at the wire boundary:\n");
printf(" LOCAL is the charset of our file names, REMOTE is the\n");
printf(" remote side's charset (defaults to LOCAL). Names are\n");
printf(" converted before transmission and back on receipt; a\n");
printf(" name that cannot be represented in the target charset\n");
printf(" fails that transfer cleanly (rsync-compatible)\n");
printf(" --delete Delete files on receiver not in source\n");
printf(" (default timing: delete only after the whole\n");
printf(" transfer has succeeded)\n");
+2 -1
View File
@@ -1,5 +1,6 @@
#include "receiver.h"
#include "charset.h"
#include "chunk.h"
#include "config.h"
#include "delay_updates.h"
@@ -79,7 +80,7 @@ static bool receiver_process_batch(Config* config, int file_descriptor) {
count > MAX_MANIFEST_ENTRIES)
return false;
for (int i = 0; i < count; i++) {
char* check_path = receive_str(file_descriptor);
char* check_path = receive_wire_str(file_descriptor);
if (!check_path)
return false;
unsigned long long check_size;
+26
View File
@@ -1,4 +1,5 @@
#include "config.h"
#include "charset.h"
#include "credentials.h"
#include "daemon_conf.h"
#include "delay_updates.h"
@@ -31,6 +32,10 @@ static bool allow_delete;
static bool trust_sender;
static bool allow_unauthenticated;
static const char* required_client_cn;
/* --iconv CONVERT_SPEC the server was itself started with (borrowed argv
* pointer). Its LOCAL half may override the local charset the client assumed;
* see charset_wire_init_receiver. */
static const char* server_iconv_spec;
/* Non-NULL exactly when the listener runs in --daemon mode. Loaded once in
* main before any accept-loop fork, then shared read-only by every forked
@@ -318,6 +323,20 @@ void handler(int file_descriptor) {
return;
}
config->use_delete = config->use_delete && allow_delete;
/* --iconv (protocol 2.16.0): install the receiver-side wire->local conversion
now that the client's full CONVERT_SPEC has been received and validated,
before any received file name is decoded. The server's own --iconv (if
any) may override the local charset; a spec the client is known to have
validated cannot fail here unless the server's override names an
unsupported charset. */
if (config->iconv_spec && !charset_wire_init_receiver(config->iconv_spec, server_iconv_spec)) {
log_message(LOG_LEVEL_ERROR,
"--iconv: unsupported charset conversion requested (LOCAL[,REMOTE])");
config_delete(config);
close(file_descriptor);
protocol_session_unbind();
return;
}
/* --delete-missing-args deletes destination mirrors receiver-side, so it is
deletion and stays gated by the same --allow-delete server policy. When
the server policy is off the flag is inert (the missing entries are still
@@ -485,6 +504,7 @@ void handler(int file_descriptor) {
}
protocol_session_unbind();
identity_clear_active();
charset_wire_free();
close(file_descriptor);
}
@@ -535,6 +555,11 @@ static void print_server_usage(void) {
printf(" -6, --ipv6 Bind an IPv6 socket\n");
printf(" --allow-delete Permit manifest deletion\n");
printf(" --trust-sender Trust the remote sender's file list\n");
printf(" --iconv=LOCAL[,REMOTE] Declare this server's LOCAL charset for file-name\n");
printf(" conversion: received names are translated to this\n");
printf(" charset (the wire charset still comes from the\n");
printf(" client's CONVERT_SPEC). A name that cannot be\n");
printf(" represented fails the run cleanly\n");
printf(" --allow-unauthenticated Allow plaintext/anonymous network clients\n");
printf(" -v, --verbose Enable debug logging\n");
printf(" --help Show this help\n");
@@ -621,6 +646,7 @@ int main(int argc, char* argv[]) {
allow_delete = opts.allow_delete;
trust_sender = opts.trust_sender;
allow_unauthenticated = opts.allow_unauthenticated;
server_iconv_spec = opts.iconv_spec;
signal(SIGINT, cleanup);
signal(SIGTERM, cleanup);
+22
View File
@@ -1,4 +1,5 @@
#include "server_cli.h"
#include "charset.h"
#include "utils.h"
#include <limits.h>
#include <stdarg.h>
@@ -143,6 +144,12 @@ int server_cli_parse(int argc, char* argv[], ServerCliOptions* opts, char* err,
opts->trust_sender = true;
} else if (arg_is(argv[i], "--allow-unauthenticated")) {
opts->allow_unauthenticated = true;
} else if (arg_is(argv[i], "--iconv")) {
if (i + 1 >= argc) {
set_error(err, err_size, "missing argument for --iconv");
return -1;
}
opts->iconv_spec = argv[++i];
} else if (arg_is(argv[i], "-p")) {
if (i + 1 >= argc) {
set_error(err, err_size, "missing argument for -p");
@@ -180,6 +187,15 @@ int server_cli_parse(int argc, char* argv[], ServerCliOptions* opts, char* err,
inline_value = argv[++i];
}
opts->early_input_file = inline_value;
} else if (arg_has_value(argv[i], "--iconv", &inline_value)) {
if (!inline_value) {
if (i + 1 >= argc) {
set_error(err, err_size, "missing argument for --iconv");
return -1;
}
inline_value = argv[++i];
}
opts->iconv_spec = inline_value;
} else if (arg_has_value(argv[i], "--dparam", &inline_value)) {
if (!inline_value) {
if (i + 1 >= argc) {
@@ -227,6 +243,12 @@ int server_cli_parse(int argc, char* argv[], ServerCliOptions* opts, char* err,
"--daemon");
return -1;
}
/* --iconv: reject a malformed CONVERT_SPEC or an unsupported charset name at
startup (a probe iconv_open is attempted). */
if (opts->iconv_spec != NULL && !charset_spec_valid(opts->iconv_spec)) {
set_error(err, err_size, "--iconv requires LOCAL[,REMOTE] charset names supported by iconv");
return -1;
}
return 0;
}
+6
View File
@@ -33,6 +33,12 @@ typedef struct ServerCliOptions {
bool allow_delete; /* --allow-delete */
bool trust_sender; /* --trust-sender */
bool allow_unauthenticated; /* --allow-unauthenticated */
/* --iconv=CONVERT_SPEC: the server's own LOCAL charset declaration. The
* client's full spec rides the wire config frame anyway; when the server is
* started with its own --iconv, its LOCAL half overrides the local charset
* the client assumed so the server converts received names to ITS charset.
* Borrowed pointer into argv (never owns heap). */
const char* iconv_spec; /* --iconv value, or NULL */
} ServerCliOptions;
/* Parse argc/argv into *opts. Zero-initialize *opts before calling (or use
+275
View File
@@ -0,0 +1,275 @@
#include "charset.h"
#include "log.h"
#include "protocol.h"
#include "utils.h"
#include <errno.h>
#include <iconv.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
iconv_t cd;
} CharsetConversion;
static CharsetConversion* g_wire_conv;
/* Grow *buf to double capacity, freeing it on failure. realloc preserves the
* already-written prefix, so the caller only tracks its write offset. */
static bool grow_charset_buffer(char** buf, size_t* cap) {
size_t new_cap = *cap * 2;
if (new_cap <= *cap) {
free(*buf);
*buf = NULL;
return false;
}
char* grown = realloc(*buf, new_cap);
if (!grown) {
free(*buf);
*buf = NULL;
return false;
}
*buf = grown;
*cap = new_cap;
return true;
}
int charset_spec_parse(const char* spec, char** local_out, char** remote_out) {
if (local_out)
*local_out = NULL;
if (remote_out)
*remote_out = NULL;
if (!spec || spec[0] == '\0')
return -1;
char* dup = str_dup(spec);
if (!dup)
return -1;
char* comma = strchr(dup, ',');
if (comma) {
if (comma == dup || comma[1] == '\0') {
free(dup);
return -1;
}
*comma = '\0';
*local_out = str_dup(dup);
*remote_out = str_dup(comma + 1);
free(dup);
} else {
*local_out = str_dup(dup);
*remote_out = str_dup(dup);
free(dup);
}
if (!*local_out || !*remote_out) {
free(*local_out);
free(*remote_out);
*local_out = NULL;
*remote_out = NULL;
return -1;
}
return 0;
}
void* charset_conversion_open(const char* from_charset, const char* to_charset) {
if (!from_charset || !to_charset)
return NULL;
iconv_t cd = iconv_open(to_charset, from_charset);
if (cd == (iconv_t)-1)
return NULL;
CharsetConversion* conv = malloc(sizeof(CharsetConversion));
if (!conv) {
iconv_close(cd);
return NULL;
}
conv->cd = cd;
return conv;
}
void charset_conversion_close(void* conversion) {
if (!conversion)
return;
CharsetConversion* conv = (CharsetConversion*)conversion;
iconv_close(conv->cd);
free(conv);
}
bool charset_pair_valid(const char* local, const char* remote) {
if (!local || !remote)
return false;
void* conv = charset_conversion_open(local, remote);
if (!conv)
return false;
charset_conversion_close(conv);
return true;
}
bool charset_spec_valid(const char* spec) {
if (!spec)
return true;
char* local;
char* remote;
if (charset_spec_parse(spec, &local, &remote) != 0)
return false;
bool ok = charset_pair_valid(local, remote);
free(local);
free(remote);
return ok;
}
char* charset_convert(const void* conversion, const char* in, int* err_out) {
if (!conversion || !in)
return NULL;
const CharsetConversion* conv = (const CharsetConversion*)conversion;
size_t in_len = strlen(in);
size_t cap = in_len + 16;
char* out = malloc(cap);
if (!out)
return NULL;
size_t in_left = in_len;
char* in_ptr = (char*)in;
size_t out_used = 0;
while (in_left > 0) {
char* out_ptr = out + out_used;
size_t out_left = cap - out_used;
if (iconv(conv->cd, &in_ptr, &in_left, &out_ptr, &out_left) == (size_t)-1) {
if (errno != E2BIG) {
if (err_out)
*err_out = errno;
free(out);
return NULL;
}
if (!grow_charset_buffer(&out, &cap))
return NULL;
continue;
}
out_used = (size_t)(out_ptr - out);
}
/* Flush any pending shift state (a no-op for the stateless single-byte and
UTF charsets this feature targets, but keeps the descriptor clean). */
for (;;) {
char* out_ptr = out + out_used;
size_t out_left = cap - out_used;
if (iconv(conv->cd, NULL, NULL, &out_ptr, &out_left) == (size_t)-1) {
if (errno != E2BIG) {
if (err_out)
*err_out = errno;
free(out);
return NULL;
}
if (!grow_charset_buffer(&out, &cap))
return NULL;
continue;
}
out_used = (size_t)(out_ptr - out);
break;
}
out[out_used] = '\0';
return out;
}
bool charset_wire_init_sender(const char* spec) {
charset_wire_free();
if (!spec)
return true;
char* local;
char* remote;
if (charset_spec_parse(spec, &local, &remote) != 0)
return false;
void* conv = charset_conversion_open(local, remote);
free(local);
free(remote);
if (!conv)
return false;
g_wire_conv = (CharsetConversion*)conv;
return true;
}
bool charset_wire_init_receiver(const char* spec, const char* server_spec) {
charset_wire_free();
if (!spec)
return true;
char* local;
char* remote;
if (charset_spec_parse(spec, &local, &remote) != 0)
return false;
/* The wire charset is the client spec's REMOTE half; the local charset is
* the client spec's LOCAL half unless the server was itself started with
* --iconv naming a different local charset (the server halves above never
* travel, so the server's own flag is the only way its local charset can
* differ from what the client assumed). */
const char* wire = remote;
const char* target_local = local;
char* server_local = NULL;
char* server_remote = NULL;
if (server_spec) {
if (charset_spec_parse(server_spec, &server_local, &server_remote) != 0) {
free(local);
free(remote);
return false;
}
target_local = server_local;
}
void* conv = charset_conversion_open(wire, target_local);
free(server_local);
free(server_remote);
free(local);
free(remote);
if (!conv)
return false;
g_wire_conv = (CharsetConversion*)conv;
return true;
}
void charset_wire_free(void) {
if (g_wire_conv) {
charset_conversion_close(g_wire_conv);
g_wire_conv = NULL;
}
}
bool charset_wire_active(void) {
return g_wire_conv != NULL;
}
char* charset_wire_apply(const char* path) {
if (!g_wire_conv)
return str_dup(path);
return charset_convert(g_wire_conv, path, NULL);
}
static void charset_convert_failure_log(const char* path) {
char* escaped = output_escape(path, false);
log_message(LOG_LEVEL_ERROR, "--iconv: cannot convert file name '%s' to the target charset",
escaped ? escaped : "<unprintable>");
free(escaped);
}
bool send_wire_str(int file_descriptor, const char* local_path) {
if (!g_wire_conv)
return send_str(file_descriptor, local_path);
char* wire = charset_wire_apply(local_path);
if (!wire) {
charset_convert_failure_log(local_path);
return false;
}
bool ok = send_str(file_descriptor, wire);
free(wire);
return ok;
}
char* receive_wire_str(int file_descriptor) {
char* raw = receive_str(file_descriptor);
if (!raw)
return NULL;
if (!g_wire_conv)
return raw;
char* local = charset_convert(g_wire_conv, raw, NULL);
if (!local) {
charset_convert_failure_log(raw);
free(raw);
return NULL;
}
free(raw);
return local;
}
+70
View File
@@ -0,0 +1,70 @@
#ifndef CHARSET_H
#define CHARSET_H
#include <stdbool.h>
#include <stddef.h>
/* --iconv=CONVERT_SPEC file-name charset conversion (rsync compatibility).
*
* CONVERT_SPEC is "LOCAL[,REMOTE]": LOCAL is the charset of our own file
* names, REMOTE is the charset of the remote side's file names and defaults
* to LOCAL when the comma half is omitted. The sender converts every local
* path from LOCAL to REMOTE before it goes on the wire; the receiver converts
* every received path back from REMOTE to LOCAL. A NULL/disabled spec means
* identity with zero overhead (the common path never consults iconv).
*
* All helpers are friendly to the strict cold path: the wire conversion state
* is process-global (one direction per process -- a client only sends, a
* server only receives) and is initialized once, before any path is
* serialized, so conversion compiles to a single non-NULL check when disabled.
*/
/* Parse CONVERT_SPEC into malloc'd LOCAL and REMOTE charset names (caller
* frees both). REMOTE is a separate copy of LOCAL when no comma is present.
* Returns 0 on success, -1 on a malformed spec (empty halves / missing value /
* allocation failure); nothing is allocated on the -1 path. */
int charset_spec_parse(const char* spec, char** local_out, char** remote_out);
/* True when a CONVERT_SPEC is well-formed AND every charset name opens in a
* probe iconv_open (so a typo'd name is rejected at startup, not mid-run).
* NULL (iconv disabled) is always valid. */
bool charset_spec_valid(const char* spec);
/* Probe a local->remote conversion pair without keeping the descriptor. */
bool charset_pair_valid(const char* local, const char* remote);
/* One-shot conversion of a NUL-terminated input to a malloc'd NUL-terminated
* result, or NULL on failure. On failure *err_out (when non-NULL) receives
* the iconv errno (EILSEQ/EINVAL = the input is not representable in the
* target charset). The caller must free the result. */
char* charset_convert(const void* conversion, const char* in, int* err_out);
/* Open a conversion descriptor for direction from_charset -> to_charset.
* Returns NULL (errno = EINVAL) when a charset name is unsupported. Freed
* with charset_conversion_close. */
void* charset_conversion_open(const char* from_charset, const char* to_charset);
void charset_conversion_close(void* conversion);
/* Process-wide wire conversion. charset_wire_init_sender (client side) opens
* LOCAL->REMOTE; charset_wire_init_receiver (server side) opens
* wire(REMOTE)->server-local. server_spec is the server's own --iconv, whose
* LOCAL half may override the local charset the client assumed; NULL reuses
* the client spec's LOCAL half. Both return false on an unsupported spec.
* The state is freed with charset_wire_free. */
bool charset_wire_init_sender(const char* spec);
bool charset_wire_init_receiver(const char* spec, const char* server_spec);
void charset_wire_free(void);
bool charset_wire_active(void);
/* Convert a path across the wire in the process direction. Returns a malloc'd
* string, or NULL when the name cannot be represented in the target charset. */
char* charset_wire_apply(const char* path);
/* Convenience wire string I/O: encode+send_str / receive_str+decode. Both
* return false/NULL (logging a clear --iconv error) on conversion failure, so
* an unconvertible path FAILS the transfer cleanly instead of silently sending
* a mangled name. */
bool send_wire_str(int file_descriptor, const char* local_path);
char* receive_wire_str(int file_descriptor);
#endif
+65 -5
View File
@@ -6,6 +6,7 @@
#include <string.h>
#include "array_list.h"
#include "charset.h"
#include "chunk.h"
#include "compression.h"
#include "data.h"
@@ -63,9 +64,22 @@ void chunk_destroy(void* item) {
free(chunk);
}
/* --iconv: a chunk blob carries wire-charset path/target bytes. Encode the
* sender-side path (a no-op copy when iconv is disabled) so the blob is in the
* same charset as every other wire string. */
static char* chunk_encode_wire(const char* path) {
if (!charset_wire_active())
return str_dup(path);
return charset_wire_apply(path);
}
static unsigned long long per_file_serialize_size(File* file, bool use_metadata) {
unsigned long long size = sizeof(size_t);
size_t path_len = strlen(file_wire_path(file));
char* wire_path = chunk_encode_wire(file_wire_path(file));
if (!wire_path)
return 0;
size_t path_len = strlen(wire_path);
free(wire_path);
unsigned long long metadata_size =
use_metadata ? sizeof(int) + (file->metadata ? FILE_METADATA_WIRE_SIZE : 0) : 0;
if ((unsigned long long)path_len > ULLONG_MAX - size)
@@ -94,7 +108,11 @@ static unsigned long long per_file_serialize_size(File* file, bool use_metadata)
size += file->data->size;
/* Symlink entries append the target string (length-prefixed). */
if (file->is_symlink) {
size_t target_len = file->symlink_target ? strlen(file->symlink_target) : 0;
char* wire_target = chunk_encode_wire(file->symlink_target ? file->symlink_target : "");
if (!wire_target)
return 0;
size_t target_len = strlen(wire_target);
free(wire_target);
if (sizeof(size_t) > ULLONG_MAX - size)
return 0;
size += sizeof(size_t);
@@ -128,12 +146,17 @@ Data* chunk_serialize(Chunk* chunk, bool use_metadata) {
char* data_pointer = data->data;
for (int i = 0; i < chunk->element_count; i++) {
File* file = chunk->items[i];
const char* wire_path = file_wire_path(file);
char* wire_path = chunk_encode_wire(file_wire_path(file));
if (wire_path == NULL) {
data_destroy(data);
return NULL;
}
size_t path_len = strlen(wire_path);
memcpy(data_pointer, &path_len, sizeof(size_t));
data_pointer += sizeof(size_t);
memcpy(data_pointer, wire_path, path_len);
data_pointer += path_len;
free(wire_path);
int entry_type = file->is_dir ? 1 : (file->is_symlink ? 2 : (file->is_special ? 3 : 0));
memcpy(data_pointer, &entry_type, sizeof(int));
@@ -159,12 +182,18 @@ Data* chunk_serialize(Chunk* chunk, bool use_metadata) {
data_pointer += file_data_size;
if (file->is_symlink) {
size_t target_len = file->symlink_target ? strlen(file->symlink_target) : 0;
char* wire_target = chunk_encode_wire(file->symlink_target ? file->symlink_target : "");
if (wire_target == NULL) {
data_destroy(data);
return NULL;
}
size_t target_len = strlen(wire_target);
memcpy(data_pointer, &target_len, sizeof(size_t));
data_pointer += sizeof(size_t);
if (target_len > 0)
memcpy(data_pointer, file->symlink_target, target_len);
memcpy(data_pointer, wire_target, target_len);
data_pointer += target_len;
free(wire_target);
}
}
return data;
@@ -222,6 +251,22 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
data_pointer += path_len;
remaining_size -= path_len;
/* --iconv: the blob holds the wire charset; translate it to the receiver's
local charset before validation and creation so the destination gets the
local name. A name that cannot be decoded fails the file cleanly. */
if (charset_wire_active()) {
char* local_path = charset_wire_apply(path);
free(path);
if (local_path == NULL) {
log_message(LOG_LEVEL_ERROR,
"--iconv: received chunk file name cannot be converted to the local charset");
array_list_delete(files);
return NULL;
}
path = local_path;
path_len = strlen(path);
}
if (path_len == 0 || has_path_traversal(path)) {
free(path);
array_list_delete(files);
@@ -392,6 +437,21 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
array_list_delete(files);
return NULL;
}
/* The symlink target also rides the wire charset; decode it to the local
charset like the path (a target is a path). */
if (charset_wire_active()) {
char* local_target = charset_wire_apply(target);
free(target);
if (local_target == NULL) {
log_message(LOG_LEVEL_ERROR,
"--iconv: received chunk symlink target cannot be converted to the local "
"charset");
file_destroy(file);
array_list_delete(files);
return NULL;
}
target = local_target;
}
file->symlink_target = target;
data_pointer += target_len;
remaining_size -= target_len;
+36 -3
View File
@@ -1,4 +1,5 @@
#include "config.h"
#include "charset.h"
#include "chmod.h"
#include "credentials.h"
#include "daemon_conf.h"
@@ -42,6 +43,7 @@ static void config_set_defaults(Config* config) {
config->auth_user = NULL;
config->auth_password_hash = NULL;
config->password_file = NULL;
config->iconv_spec = NULL;
config->fastsync_server_path = NULL;
config->exclude_patterns = NULL;
config->exclude_count = 0;
@@ -242,7 +244,13 @@ static bool validate_received_config(const Config* config) {
config->max_delete >= -1 && config->skip_compress_count >= 0 &&
config->skip_compress_count <= 10000 && config->max_alloc > 0 &&
(!config->chmod_spec || !*config->chmod_spec ||
chmod_apply(0, config->chmod_spec, &(mode_t){0}));
chmod_apply(0, config->chmod_spec, &(mode_t){0})) &&
/* The received --iconv CONVERT_SPEC is untrusted input that drives
the receiver's path decoding: reject a malformed spec or an
unsupported charset name so the run is refused up front instead of
every received file name failing mid-transfer. A NULL spec (iconv
disabled) is always accepted. */
(!config->iconv_spec || charset_spec_valid(config->iconv_spec));
}
Config* config_create(void) {
@@ -619,6 +627,7 @@ void config_delete(Config* config) {
free(config->auth_user);
free(config->auth_password_hash);
free(config->password_file);
free(config->iconv_spec);
free(config->fastsync_server_path);
for (int i = 0; i < config->exclude_count; i++)
free(config->exclude_patterns[i]);
@@ -1144,6 +1153,29 @@ static bool receive_daemon_auth(int fd, Config* c) {
return true;
}
/* --iconv CONVERT_SPEC (protocol 2.16.0). Trailing string on the config frame,
* sent after the Wave A/B daemon-auth block and before the ack, so the
* receiver knows the wire charset before the first file name arrives. The full
* spec travels (LOCAL,REMOTE) and each end derives its own LOCAL and the wire
* (REMOTE) charset symmetrically; an unset spec is serialized as "" and
* canonicalized back to NULL on receive. */
static bool send_iconv_spec(int fd, const Config* c) {
return send_str(fd, c->iconv_spec ? c->iconv_spec : "");
}
static bool receive_iconv_spec(int fd, Config* c) {
char* spec = receive_str(fd);
if (!spec)
return false;
if (*spec == '\0') {
free(spec);
c->iconv_spec = NULL;
return true;
}
c->iconv_spec = spec;
return true;
}
bool config_send(int file_descriptor, const Config* config) {
protocol_session_set_max_alloc(NULL, config->max_alloc);
if (!send_core_fields(file_descriptor, config) || !send_delta_fields(file_descriptor, config) ||
@@ -1156,7 +1188,8 @@ bool config_send(int file_descriptor, const Config* config) {
!send_metadata_times_options(file_descriptor, config) ||
!send_symlink_trust_options(file_descriptor, config) ||
!send_phase4_xattr_options(file_descriptor, config) ||
!send_daemon_module(file_descriptor, config) || !send_daemon_auth(file_descriptor, config))
!send_daemon_module(file_descriptor, config) || !send_daemon_auth(file_descriptor, config) ||
!send_iconv_spec(file_descriptor, config))
return false;
Status status;
if (!receive_status(file_descriptor, &status))
@@ -1198,7 +1231,7 @@ Config* config_receive_with_validate(int file_descriptor, ConfigValidateFunc val
!receive_symlink_trust_options(file_descriptor, config) ||
!receive_phase4_xattr_options(file_descriptor, config) ||
!receive_daemon_module(file_descriptor, config) ||
!receive_daemon_auth(file_descriptor, config))
!receive_daemon_auth(file_descriptor, config) || !receive_iconv_spec(file_descriptor, config))
goto error;
if (config->compress_choice[0] != '\0' && strcmp(config->compress_choice, "zstd") != 0 &&
strcmp(config->compress_choice, "none") != 0) {
+26 -2
View File
@@ -110,6 +110,17 @@ typedef struct Config {
* populate auth_user/auth_password_hash before connecting). */
char* password_file;
char* fastsync_server_path;
/* --iconv=CONVERT_SPEC (protocol 2.16.0, rsync compatibility): convert the
* charset of FILE NAMES at the wire boundary. CONVERT_SPEC is
* "LOCAL[,REMOTE]": LOCAL is the charset of our own file names, REMOTE is
* the remote side's charset and defaults to LOCAL. The sender converts
* every path LOCAL->REMOTE before transmitting it; the receiver converts
* every received path back REMOTE->LOCAL before creating/writing it. The
* FULL SPEC crosses the wire as a trailing config-frame string so each end
* derives its own LOCAL and the wire (REMOTE) charset symmetrically. NULL
* (or "") means no conversion: identity with zero overhead. See charset.c
* and the PROTOCOL_VERSION note below. */
char* iconv_spec;
char** exclude_patterns;
int exclude_count;
char** include_patterns;
@@ -493,8 +504,21 @@ typedef struct Config {
* reads that frame right after the ack (client_send.c) -- symmetric
* server->client in every build, so the strict same-version handshake keeps the
* two peers in lockstep and nothing can desynchronize. The --stdio SSH path
* sends/reads no MOTD at all. */
#define PROTOCOL_VERSION "2.15.0"
* sends/reads no MOTD at all.
*
* --iconv Wave (P6): 2.15.0 -> 2.16.0.
*
* WHY the bump, grounded in the wire: the --iconv feature adds a serialized
* field to the binary config frame. The client sends the full CONVERT_SPEC
* (Config->iconv_spec) as a new trailing string AFTER the Wave A/B daemon-auth
* block (in config_send/config_receive), so the receiver knows the wire charset
* (the REMOTE half) before the first file name arrives. Any config-frame
* layout change must bump the protocol version: a peer that does not parse the
* new trailing bytes would desynchronize on the frame boundary, and the strict
* same-version handshake (config_receive rejects a mismatched version before
* parsing anything else) is what keeps a 2.16 client and a 2.15 server from
* ever reaching that state. */
#define PROTOCOL_VERSION "2.16.0"
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
#define MAX_BASIS_DIRS 64
+10 -9
View File
@@ -10,6 +10,7 @@
#include <unistd.h>
#include "array_list.h"
#include "charset.h"
#include "chmod.h"
#include "compression.h"
#include "config.h"
@@ -1555,7 +1556,7 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
return NULL;
}
*skipped = false;
char* check_path = receive_str(fd);
char* check_path = receive_wire_str(fd);
if (check_path == NULL) {
return NULL;
}
@@ -2082,7 +2083,7 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
}
File* file_receive(const Config* config, int file_descriptor) {
char* path = receive_str(file_descriptor);
char* path = receive_wire_str(file_descriptor);
if (path == NULL)
return NULL;
if (path[0] == '\0' || (!file_get_trust_sender() && has_path_traversal(path))) {
@@ -2142,7 +2143,7 @@ File* file_receive(const Config* config, int file_descriptor) {
traversal), and the created File is routed through the regular store_file
sink so single-threaded and -m receivers handle directories identically. */
File* file_receive_directory(int file_descriptor) {
char* path = receive_str(file_descriptor);
char* path = receive_wire_str(file_descriptor);
if (path == NULL)
return NULL;
if (path[0] == '\0' || (!file_get_trust_sender() && has_path_traversal(path))) {
@@ -2169,7 +2170,7 @@ File* file_receive_directory(int file_descriptor) {
member. All paths are validated like every other received path (non-empty,
relative, no traversal). */
File* file_receive_hardlink(int file_descriptor) {
char* path = receive_str(file_descriptor);
char* path = receive_wire_str(file_descriptor);
if (path == NULL)
return NULL;
if (path[0] == '\0' || (!file_get_trust_sender() && has_path_traversal(path))) {
@@ -2186,7 +2187,7 @@ File* file_receive_hardlink(int file_descriptor) {
free(path);
return NULL;
}
char* target = receive_str(file_descriptor);
char* target = receive_wire_str(file_descriptor);
if (!target) {
free(path);
return NULL;
@@ -2219,7 +2220,7 @@ File* file_receive_hardlink(int file_descriptor) {
routed through the regular store_file sink, which creates the link beneath
the receive root (unmungeing the target first). */
File* file_receive_symlink(int file_descriptor, const Config* config) {
char* path = receive_str(file_descriptor);
char* path = receive_wire_str(file_descriptor);
if (path == NULL)
return NULL;
if (path[0] == '\0' || (!file_get_trust_sender() && has_path_traversal(path))) {
@@ -2231,7 +2232,7 @@ File* file_receive_symlink(int file_descriptor, const Config* config) {
send_status(file_descriptor, STATUS_ERROR);
return NULL;
}
char* target = receive_str(file_descriptor);
char* target = receive_wire_str(file_descriptor);
if (!target) {
free(path);
return NULL;
@@ -2274,7 +2275,7 @@ File* file_receive_symlink(int file_descriptor, const Config* config) {
* confined). rdev is validated here (non-negative, range-checked) so a bogus
* value cannot drive a dangerous node on the receiver. */
File* file_receive_special(int file_descriptor) {
char* path = receive_str(file_descriptor);
char* path = receive_wire_str(file_descriptor);
if (path == NULL)
return NULL;
if (path[0] == '\0' || (!file_get_trust_sender() && has_path_traversal(path))) {
@@ -2354,7 +2355,7 @@ static bool receive_manifest_section(int fd, ArrayList* list, size_t* manifest_b
return false;
}
for (int i = 0; i < count; i++) {
char* s = receive_str(fd);
char* s = receive_wire_str(fd);
size_t entry_size = s ? strlen(s) : 0;
if (!s || s[0] == '\0' || s[0] == '/' || has_path_traversal(s) ||
entry_size > MAX_MANIFEST_BYTES - *manifest_bytes ||
+4 -3
View File
@@ -10,6 +10,7 @@
#include <time.h>
#include <unistd.h>
#include "charset.h"
#include "compression.h"
#include "data.h"
#include "file.h"
@@ -27,7 +28,7 @@ bool file_send_special(const File* file, int file_descriptor, bool use_metadata)
return false;
if (!send_status(file_descriptor, STATUS_SPECIAL))
return false;
if (!send_str(file_descriptor, file_wire_path(file)))
if (!send_wire_str(file_descriptor, file_wire_path(file)))
return false;
if (use_metadata && !metadata_send(file_descriptor, file->metadata))
return false;
@@ -61,7 +62,7 @@ bool file_send_single_calls_with_skip(File* file, int file_descriptor, bool use_
}
data_to_send = compressed_data;
}
if (send_path && !send_str(file_descriptor, file_wire_path(file))) {
if (send_path && !send_wire_str(file_descriptor, file_wire_path(file))) {
data_destroy(compressed_data);
return false;
}
@@ -97,7 +98,7 @@ bool file_send_sendfile_with_skip(File* file, int file_descriptor, bool use_meta
send_path, skip_suffixes, skip_count,
compression_threads, send_xattrs);
if (send_path && !send_str(file_descriptor, file_wire_path(file)))
if (send_path && !send_wire_str(file_descriptor, file_wire_path(file)))
return false;
if (use_metadata && !metadata_send(file_descriptor, file->metadata))
return false;