feat(p6-iconv): --iconv charset conversion + PROTOCOL 2.16.0
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,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 ||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user