Merge branch 'feat/parity-codecs' into feat/parity-completion

# Conflicts:
#	src/shared/checksum.h
#	src/shared/config.h
#	tests/integration/test_fault_injection.py
#	tests/integration/test_preflight.py
#	tests/test_client_cli.c
#	tests/test_config.c
#	tests/test_fuzz_smoke.c
This commit is contained in:
2026-09-16 23:49:41 +02:00
21 changed files with 1500 additions and 195 deletions
+15 -5
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.22) cmake_minimum_required(VERSION 3.22)
project(FastFileTransfer VERSION 2.23.0) project(FastFileTransfer VERSION 2.26.0)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD 11)
@@ -68,6 +68,16 @@ if(NOT ZSTD_LIBRARY)
message(FATAL_ERROR "zstd library not found. Ensure it is in your nix-shell!") message(FATAL_ERROR "zstd library not found. Ensure it is in your nix-shell!")
endif() endif()
find_library(ZLIB_LIBRARY z)
if(NOT ZLIB_LIBRARY)
message(FATAL_ERROR "zlib library not found. Ensure zlib1g-dev / nix zlib is available!")
endif()
find_library(LZ4_LIBRARY lz4)
if(NOT LZ4_LIBRARY)
message(FATAL_ERROR "lz4 library not found. Ensure liblz4-dev / nix lz4 is available!")
endif()
find_package(OpenSSL REQUIRED) find_package(OpenSSL REQUIRED)
# --- Explicit source lists --- # --- Explicit source lists ---
@@ -136,8 +146,8 @@ set(CLIENT_MAIN_SRCS src/client/client_cli.c)
# --- Library targets --- # --- Library targets ---
add_library(fastsync_shared STATIC ${SHARED_SRCS}) add_library(fastsync_shared STATIC ${SHARED_SRCS})
target_include_directories(fastsync_shared PUBLIC src/shared) target_include_directories(fastsync_shared PUBLIC src/shared)
target_link_libraries(fastsync_shared PUBLIC Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL target_link_libraries(fastsync_shared PUBLIC Threads::Threads ${ZSTD_LIBRARY} ${ZLIB_LIBRARY}
OpenSSL::Crypto xxhash) ${LZ4_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
add_library(fastsync_client_core STATIC ${CLIENT_CORE_SRCS}) add_library(fastsync_client_core STATIC ${CLIENT_CORE_SRCS})
target_include_directories(fastsync_client_core PUBLIC src/client) target_include_directories(fastsync_client_core PUBLIC src/client)
@@ -276,7 +286,7 @@ if(ENABLE_FUZZ)
target_include_directories(${FUZZ_NAME} PRIVATE tests src/shared src/server) target_include_directories(${FUZZ_NAME} PRIVATE tests src/shared src/server)
target_compile_options(${FUZZ_NAME} PRIVATE -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer) target_compile_options(${FUZZ_NAME} PRIVATE -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer)
target_link_options(${FUZZ_NAME} PRIVATE -fsanitize=fuzzer,address,undefined) target_link_options(${FUZZ_NAME} PRIVATE -fsanitize=fuzzer,address,undefined)
target_link_libraries(${FUZZ_NAME} PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL target_link_libraries(${FUZZ_NAME} PRIVATE Threads::Threads ${ZSTD_LIBRARY} ${ZLIB_LIBRARY}
OpenSSL::Crypto xxhash) ${LZ4_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
endforeach() endforeach()
endif() endif()
+2
View File
@@ -38,6 +38,8 @@ pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
zstd zstd
zlib
lz4
openssl openssl
]; ];
+102 -33
View File
@@ -150,47 +150,96 @@ static int set_positive_int_option(int* dest, const char* value, const char* opt
} }
/* Set and validate the compression algorithm selected by the client. rsync /* Set and validate the compression algorithm selected by the client. rsync
* 3.4.1 can be built with zstd, none, lz4, zlibx, zlib and auto; FastSync only * 3.4.1 can be built with zstd, none, lz4, zlibx, zlib and auto; all of those
* implements zstd (and no compression). "auto" is accepted as the default * names are accepted and mapped to a real codec here. "auto" resolves through
* zstd choice; any other rsync choice is rejected by name instead of being * FastSync's compiled-in preference order (rsync 3.4.1's list). An unknown
* silently accepted and ignored. */ * name is a hard error with rsync's exit code 4, never a silent no-op. */
static int set_compression_choice(Config* config, const char* value) { static int set_compression_choice(Config* config, const char* value) {
/* rsync's "auto" is normalized to the canonical "zstd" at parse time (like if (!value) {
--checksum-choice=auto), so the value that crosses the wire is always one config->cli_exit_code = 4;
the receiver accepts. */
const char* canonical = strcmp(value, "auto") == 0 ? "zstd" : value;
if (strcmp(canonical, "zstd") != 0 && strcmp(canonical, "none") != 0) {
log_message(LOG_LEVEL_ERROR,
"--compress-choice '%s' is not implemented; FastSync supports zstd, none or auto "
"(rsync's lz4/zlib/zlibx are rejected, never silently ignored)",
value);
return -1; return -1;
} }
int algo;
if (strcasecmp(value, "auto") == 0)
algo = (int)compression_negotiate_default();
else
algo = compression_algo_from_name(value);
if (algo < 0) {
log_message(LOG_LEVEL_ERROR,
"--compress-choice '%s' is not a supported algorithm; FastSync supports zstd, "
"lz4, zlib, zlibx, none or auto",
value);
config->cli_exit_code = 4;
return -1;
}
const char* canonical = compression_algo_name((CompressionAlgo)algo);
if (set_string_option(&config->compress_choice, canonical, "--compress-choice") != 0) if (set_string_option(&config->compress_choice, canonical, "--compress-choice") != 0)
return -1; return -1;
config->use_compression = strcmp(canonical, "none") != 0; config->compression_algo = algo;
config->use_compression = (algo != (int)COMPRESSION_ALGO_NONE);
return 0; return 0;
} }
/* Validate and store the --checksum-choice/--cc algorithm. Only the algorithms /* Store one algorithm name into *out. Returns 0 for a valid name, 1 for
* the engine genuinely supports are accepted (xxh64/xxhash, xxh3, xxh128, md5); * "auto" (caller resolves it), -1 for an unknown/too-long name. */
* rsync's compiled-in choices that FastSync does not implement (md4, sha1, static int resolve_checksum_name(const char* name, size_t len, int* out) {
* none) and the two-name transfer/pre-transfer syntax are a clear error, never char buf[64];
* a silent no-op. "auto" (rsync's default automatic choice) selects FastSync's if (len == 0 || len >= sizeof(buf))
* default algorithm. */ return -1;
memcpy(buf, name, len);
buf[len] = '\0';
if (strcasecmp(buf, "auto") == 0)
return 1;
int algo = checksum_algo_from_name(buf);
if (algo < 0)
return -1;
*out = algo;
return 0;
}
/* Validate and store the --checksum-choice/--cc algorithm. rsync 3.4.1 accepts
* a single name (used for both the transfer and pre-transfer checksums) or the
* two-name "TRANSFER,PRE-TRANSFER" form (only one comma is significant). The
* pre-transfer half is FastSync's whole-file digest; the transfer half is
* validated for parity and, when "none", forces --whole-file like rsync. An
* unknown name (including an empty half or a second comma) is exit 4. "auto"
* resolves to FastSync's negotiated default (xxh128). */
static int set_checksum_choice(Config* config, const char* value) { static int set_checksum_choice(Config* config, const char* value) {
if (strcasecmp(value, "auto") == 0) if (!value) {
return 0; config->cli_exit_code = 4;
int algo = checksum_algo_from_name(value);
if (algo < 0) {
log_message(LOG_LEVEL_ERROR,
"--checksum-choice '%s' is not implemented; FastSync supports xxh64 (or xxhash), "
"xxh3, xxh128, md5 or auto (rsync's md4/sha1/none and the two-name "
"transfer,pre-transfer form are rejected, never silently ignored)",
value);
return -1; return -1;
} }
config->checksum_algo = algo; const char* comma = strchr(value, ',');
const char* name1 = value;
size_t len1 = comma ? (size_t)(comma - value) : strlen(value);
const char* name2 = comma ? comma + 1 : NULL;
size_t len2 = name2 ? strlen(name2) : 0;
int transfer = -1;
int pre = -1;
int rc1 = resolve_checksum_name(name1, len1, &transfer);
int rc2 = name2 ? resolve_checksum_name(name2, len2, &pre) : 1;
if (rc1 < 0 || rc2 < 0) {
log_message(LOG_LEVEL_ERROR,
"--checksum-choice '%s' is invalid; FastSync supports xxh64 (or xxhash), xxh128, "
"xxh3, md5, md4, sha1, none or auto, optionally as 'transfer,pre-transfer'",
value);
config->cli_exit_code = 4;
return -1;
}
ChecksumAlgo negotiated = checksum_negotiate_default();
if (rc1 == 1)
transfer = (int)negotiated;
if (!name2)
pre = transfer;
else if (rc2 == 1)
pre = (int)negotiated;
config->checksum_algo = pre;
config->checksum_transfer_algo = transfer;
/* rsync: "none" for the transfer checksum forces --whole-file. */
if (transfer == (int)CHECKSUM_ALGO_NONE)
config->whole_file = true;
return 0; return 0;
} }
@@ -2326,8 +2375,23 @@ static bool cli_handle_outbuf_option(CliParseCtx* ctx) {
* -1 on error. */ * -1 on error. */
static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool no_incremental) { static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool no_incremental) {
set_log_level(config->quiet ? LOG_LEVEL_ERROR : (verbose ? LOG_LEVEL_DEBUG : LOG_LEVEL_WARNING)); set_log_level(config->quiet ? LOG_LEVEL_ERROR : (verbose ? LOG_LEVEL_DEBUG : LOG_LEVEL_WARNING));
if (config->compress_choice) if (config->compress_choice) {
config->use_compression = strcmp(config->compress_choice, "none") != 0; int algo = compression_algo_from_name(config->compress_choice);
if (algo >= 0) {
config->compression_algo = algo;
config->use_compression = (algo != (int)COMPRESSION_ALGO_NONE);
}
}
if (config->use_compression && config->compression_algo == (int)COMPRESSION_ALGO_NONE)
config->compression_algo = (int)compression_negotiate_default();
/* rsync parity: "none" as the pre-transfer checksum cannot be combined with
* --checksum (exit 4). The check runs here because --checksum may appear on
* either side of --checksum-choice. */
if (config->checksum && config->checksum_algo == (int)CHECKSUM_ALGO_NONE) {
log_message(LOG_LEVEL_ERROR, "Invalid checksum-choice for --checksum: none");
config->cli_exit_code = 4;
return -1;
}
/* rsync randomizes the checksum seed for every transfer when the user did not /* rsync randomizes the checksum seed for every transfer when the user did not
* supply one (a seed of 0, including an explicit --checksum-seed=0), using * supply one (a seed of 0, including an explicit --checksum-seed=0), using
@@ -2786,7 +2850,7 @@ int main(int argc, char* argv[]) {
int parse_ret = parse_args(config, argc, argv, positional_args, &positional_count); int parse_ret = parse_args(config, argc, argv, positional_args, &positional_count);
if (parse_ret != 0) { if (parse_ret != 0) {
if (parse_ret < 0) if (parse_ret < 0)
exit_code = 1; exit_code = config->cli_exit_code ? config->cli_exit_code : 1;
goto cleanup; goto cleanup;
} }
@@ -2876,6 +2940,11 @@ int main(int argc, char* argv[]) {
goto cleanup; goto cleanup;
} }
/* Install the negotiated codec for this process before any transfer thread
* is spawned; the compressed frames are self-describing, so the receiver's
* decompressor does not need this, but the sender compressor does. */
compression_set_algo((CompressionAlgo)config->compression_algo);
/* --iconv: install the sender-side local->wire conversion before any path is /* --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 scanned or serialized (the scanner and the chunk/data path read windows are
all driven from this process, so one global initialization covers every all driven from this process, so one global initialization covers every
+6 -5
View File
@@ -138,10 +138,10 @@ void print_usage(void) {
printf(" --link-dest <dir> Like --copy-dest, but hard-links the unchanged file from DIR\n"); printf(" --link-dest <dir> Like --copy-dest, but hard-links the unchanged file from DIR\n");
printf(" into the destination (repeatable; earlier DIRs win)\n"); printf(" into the destination (repeatable; earlier DIRs win)\n");
printf(" --checksum-choice, --cc <alg> Whole-file checksum algorithm for --incremental/\n"); printf(" --checksum-choice, --cc <alg> Whole-file checksum algorithm for --incremental/\n");
printf(" --checksum compares. Accepted: xxh64 (aka xxhash), xxh3,\n"); printf(" --checksum compares. Accepted: xxh128 (default), xxh3, xxh64\n");
printf(" xxh128, md5, or auto (default xxh64). rsync choices FastSync\n"); printf(" (aka xxhash), md5, md4, sha1, or none. A two-name\n");
printf(" does not implement (md4, sha1, none) and the two-name\n"); printf(" 'transfer,pre-transfer' form is accepted like rsync; 'none' as\n");
printf(" transfer,pre-transfer form are rejected by name\n"); printf(" the pre-transfer algorithm is rejected with --checksum\n");
printf(" --checksum-seed <num> Seed for the whole-file xxHash digest (and the delta\n"); printf(" --checksum-seed <num> Seed for the whole-file xxHash digest (and the delta\n");
printf(" block strong hash, low 32 bits); md5 ignores the seed. A seed\n"); printf(" block strong hash, low 32 bits); md5 ignores the seed. A seed\n");
printf(" of 0 (the default) is randomized per transfer, exactly like\n"); printf(" of 0 (the default) is randomized per transfer, exactly like\n");
@@ -168,7 +168,8 @@ void print_usage(void) {
printf(" SSH argv is already built injection-safe)\n"); printf(" SSH argv is already built injection-safe)\n");
printf(" --sendfile Enable sendfile zero-copy (TCP only; long form only;\n"); printf(" --sendfile Enable sendfile zero-copy (TCP only; long form only;\n");
printf(" -f is bound to --filter, not --sendfile)\n"); printf(" -f is bound to --filter, not --sendfile)\n");
printf(" --compress-choice <alg> Compression algorithm (default: zstd)\n"); printf(" --compress-choice <alg> Compression algorithm: zstd (default), lz4, zlib,\n");
printf(" zlibx, none, or auto\n");
printf(" --zc <alg> Alias for --compress-choice\n"); printf(" --zc <alg> Alias for --compress-choice\n");
printf(" -v, --verbose Enable debug logging\n"); printf(" -v, --verbose Enable debug logging\n");
printf(" -q, --quiet Suppress non-error output\n"); printf(" -q, --quiet Suppress non-error output\n");
+4
View File
@@ -741,6 +741,10 @@ void handler(int file_descriptor) {
* received config. */ * received config. */
if (gate_ctx.super_mode_override != -1) if (gate_ctx.super_mode_override != -1)
config->super_mode = (SuperMode)gate_ctx.super_mode_override; config->super_mode = (SuperMode)gate_ctx.super_mode_override;
/* Install the codec this connection negotiated before the receiver/writer
* threads start (the server forks per connection, so the process-global
* codec is private to this session). */
compression_set_algo((CompressionAlgo)config->compression_algo);
/* If the client requested ownership but the effective super mode forbids it /* If the client requested ownership but the effective super mode forbids it
* (operator --no-super, a privileged standalone receiver's secure default, or * (operator --no-super, a privileged standalone receiver's secure default, or
* a daemon module without `client owner = yes`), say so ONCE per connection so * a daemon module without `client owner = yes`), say so ONCE per connection so
+209 -20
View File
@@ -11,6 +11,160 @@
#define XXH_STATIC_LINKING_ONLY #define XXH_STATIC_LINKING_ONLY
#include <xxhash.h> #include <xxhash.h>
/* ---------------------------------------------------------------------------
* Self-contained MD4 (RFC 1320). OpenSSL's MD4 lives in the legacy provider
* and is not guaranteed present, so FastSync carries its own implementation to
* keep --checksum-choice=md4 working on every build.
* ------------------------------------------------------------------------- */
typedef struct {
uint32_t state[4];
uint64_t bit_count;
uint8_t buffer[64];
size_t buffer_len;
} Md4Ctx;
static uint32_t md4_rotl(uint32_t x, int n) {
return (x << n) | (x >> (32 - n));
}
static void md4_transform(uint32_t state[4], const uint8_t block[64]) {
uint32_t x[16];
for (int i = 0; i < 16; i++)
x[i] = (uint32_t)block[i * 4] | ((uint32_t)block[i * 4 + 1] << 8) |
((uint32_t)block[i * 4 + 2] << 16) | ((uint32_t)block[i * 4 + 3] << 24);
uint32_t a = state[0], b = state[1], c = state[2], d = state[3];
#define F(x, y, z) (((x) & (y)) | (~(x) & (z)))
#define G(x, y, z) (((x) & (y)) | ((x) & (z)) | ((y) & (z)))
#define H(x, y, z) ((x) ^ (y) ^ (z))
#define ROUND1(a, b, c, d, k, s) a = md4_rotl(a + F(b, c, d) + x[k], s)
#define ROUND2(a, b, c, d, k, s) a = md4_rotl(a + G(b, c, d) + x[k] + 0x5a827999u, s)
#define ROUND3(a, b, c, d, k, s) a = md4_rotl(a + H(b, c, d) + x[k] + 0x6ed9eba1u, s)
ROUND1(a, b, c, d, 0, 3);
ROUND1(d, a, b, c, 1, 7);
ROUND1(c, d, a, b, 2, 11);
ROUND1(b, c, d, a, 3, 19);
ROUND1(a, b, c, d, 4, 3);
ROUND1(d, a, b, c, 5, 7);
ROUND1(c, d, a, b, 6, 11);
ROUND1(b, c, d, a, 7, 19);
ROUND1(a, b, c, d, 8, 3);
ROUND1(d, a, b, c, 9, 7);
ROUND1(c, d, a, b, 10, 11);
ROUND1(b, c, d, a, 11, 19);
ROUND1(a, b, c, d, 12, 3);
ROUND1(d, a, b, c, 13, 7);
ROUND1(c, d, a, b, 14, 11);
ROUND1(b, c, d, a, 15, 19);
ROUND2(a, b, c, d, 0, 3);
ROUND2(d, a, b, c, 4, 5);
ROUND2(c, d, a, b, 8, 9);
ROUND2(b, c, d, a, 12, 13);
ROUND2(a, b, c, d, 1, 3);
ROUND2(d, a, b, c, 5, 5);
ROUND2(c, d, a, b, 9, 9);
ROUND2(b, c, d, a, 13, 13);
ROUND2(a, b, c, d, 2, 3);
ROUND2(d, a, b, c, 6, 5);
ROUND2(c, d, a, b, 10, 9);
ROUND2(b, c, d, a, 14, 13);
ROUND2(a, b, c, d, 3, 3);
ROUND2(d, a, b, c, 7, 5);
ROUND2(c, d, a, b, 11, 9);
ROUND2(b, c, d, a, 15, 13);
ROUND3(a, b, c, d, 0, 3);
ROUND3(d, a, b, c, 8, 9);
ROUND3(c, d, a, b, 4, 11);
ROUND3(b, c, d, a, 12, 15);
ROUND3(a, b, c, d, 2, 3);
ROUND3(d, a, b, c, 10, 9);
ROUND3(c, d, a, b, 6, 11);
ROUND3(b, c, d, a, 14, 15);
ROUND3(a, b, c, d, 1, 3);
ROUND3(d, a, b, c, 9, 9);
ROUND3(c, d, a, b, 5, 11);
ROUND3(b, c, d, a, 13, 15);
ROUND3(a, b, c, d, 3, 3);
ROUND3(d, a, b, c, 11, 9);
ROUND3(c, d, a, b, 7, 11);
ROUND3(b, c, d, a, 15, 15);
#undef F
#undef G
#undef H
#undef ROUND1
#undef ROUND2
#undef ROUND3
state[0] += a;
state[1] += b;
state[2] += c;
state[3] += d;
}
static void md4_init(Md4Ctx* ctx) {
ctx->state[0] = 0x67452301u;
ctx->state[1] = 0xefcdab89u;
ctx->state[2] = 0x98badcfeu;
ctx->state[3] = 0x10325476u;
ctx->bit_count = 0;
ctx->buffer_len = 0;
}
static void md4_update(Md4Ctx* ctx, const uint8_t* data, size_t len) {
ctx->bit_count += (uint64_t)len * 8;
while (len > 0) {
size_t space = sizeof(ctx->buffer) - ctx->buffer_len;
size_t take = len < space ? len : space;
memcpy(ctx->buffer + ctx->buffer_len, data, take);
ctx->buffer_len += take;
data += take;
len -= take;
if (ctx->buffer_len == sizeof(ctx->buffer)) {
md4_transform(ctx->state, ctx->buffer);
ctx->buffer_len = 0;
}
}
}
static void md4_final(Md4Ctx* ctx, uint8_t out[16]) {
uint64_t bit_count = ctx->bit_count;
uint8_t pad = 0x80;
md4_update(ctx, &pad, 1);
uint8_t zero = 0;
while (ctx->buffer_len != 56)
md4_update(ctx, &zero, 1);
uint8_t length_le[8];
for (int i = 0; i < 8; i++)
length_le[i] = (uint8_t)((bit_count >> (8 * i)) & 0xff);
md4_update(ctx, length_le, sizeof(length_le));
for (int i = 0; i < 4; i++) {
out[i * 4] = (uint8_t)(ctx->state[i] & 0xff);
out[i * 4 + 1] = (uint8_t)((ctx->state[i] >> 8) & 0xff);
out[i * 4 + 2] = (uint8_t)((ctx->state[i] >> 16) & 0xff);
out[i * 4 + 3] = (uint8_t)((ctx->state[i] >> 24) & 0xff);
}
}
/* One-shot EVP digest (md5/sha1). Returns false when OpenSSL refuses. */
static bool evp_digest(const EVP_MD* md, const void* data, size_t size, uint8_t* out,
size_t out_capacity, size_t* out_len) {
static const uint8_t empty = 0;
const void* input = data ? data : &empty;
unsigned int digest_len = 0;
if (EVP_Digest(input, size, out, &digest_len, md, NULL) != 1)
return false;
if (digest_len > out_capacity)
return false;
*out_len = digest_len;
return true;
}
bool checksum_digest(ChecksumAlgo algo, uint64_t seed, const void* data, size_t size, uint8_t* out, bool checksum_digest(ChecksumAlgo algo, uint64_t seed, const void* data, size_t size, uint8_t* out,
size_t out_capacity, size_t* out_len) { size_t out_capacity, size_t* out_len) {
if (!out || !out_len || out_capacity < CHECKSUM_MAX_DIGEST_LEN) if (!out || !out_len || out_capacity < CHECKSUM_MAX_DIGEST_LEN)
@@ -18,43 +172,45 @@ bool checksum_digest(ChecksumAlgo algo, uint64_t seed, const void* data, size_t
if (data == NULL && size != 0) if (data == NULL && size != 0)
return false; return false;
if (algo == CHECKSUM_ALGO_XXH64) { switch (algo) {
case CHECKSUM_ALGO_XXH64: {
uint64_t digest = XXH64(data, size, seed); uint64_t digest = XXH64(data, size, seed);
memcpy(out, &digest, sizeof(digest)); memcpy(out, &digest, sizeof(digest));
*out_len = sizeof(digest); *out_len = sizeof(digest);
return true; return true;
} }
case CHECKSUM_ALGO_XXH3: {
if (algo == CHECKSUM_ALGO_XXH3) {
uint64_t digest = XXH3_64bits_withSeed(data, size, seed); uint64_t digest = XXH3_64bits_withSeed(data, size, seed);
memcpy(out, &digest, sizeof(digest)); memcpy(out, &digest, sizeof(digest));
*out_len = sizeof(digest); *out_len = sizeof(digest);
return true; return true;
} }
case CHECKSUM_ALGO_XXH128: {
if (algo == CHECKSUM_ALGO_XXH128) {
XXH128_hash_t digest = XXH3_128bits_withSeed(data, size, seed); XXH128_hash_t digest = XXH3_128bits_withSeed(data, size, seed);
memcpy(out, &digest, sizeof(digest)); memcpy(out, &digest, sizeof(digest));
*out_len = sizeof(digest); *out_len = sizeof(digest);
return true; return true;
} }
case CHECKSUM_ALGO_MD5:
if (algo == CHECKSUM_ALGO_MD5) {
/* md5 takes no seed; the caller's seed is deliberately ignored (documented /* md5 takes no seed; the caller's seed is deliberately ignored (documented
* in RSYNC_COMPAT.md). OpenSSL's one-shot EVP_Digest needs a non-NULL * in RSYNC_COMPAT.md). */
* buffer even for an empty input, so map a NULL data + size==0 to an empty return evp_digest(EVP_md5(), data, size, out, out_capacity, out_len);
* buffer. */ case CHECKSUM_ALGO_MD4: {
static const uint8_t empty = 0; Md4Ctx ctx;
const void* input = data ? data : &empty; md4_init(&ctx);
unsigned int digest_len = 0; md4_update(&ctx, (const uint8_t*)data, size);
if (EVP_Digest(input, size, out, &digest_len, EVP_md5(), NULL) != 1) md4_final(&ctx, out);
return false; *out_len = 16;
if (digest_len > out_capacity) return true;
return false; }
*out_len = digest_len; case CHECKSUM_ALGO_SHA1:
/* sha1 takes no seed; the caller's seed is deliberately ignored. */
return evp_digest(EVP_sha1(), data, size, out, out_capacity, out_len);
case CHECKSUM_ALGO_NONE:
/* No checksum requested: an empty digest is the successful result. */
*out_len = 0;
return true; return true;
} }
return false; return false;
} }
@@ -162,6 +318,12 @@ int checksum_algo_from_name(const char* name) {
return (int)CHECKSUM_ALGO_XXH128; return (int)CHECKSUM_ALGO_XXH128;
if (strcasecmp(name, "md5") == 0) if (strcasecmp(name, "md5") == 0)
return (int)CHECKSUM_ALGO_MD5; return (int)CHECKSUM_ALGO_MD5;
if (strcasecmp(name, "md4") == 0)
return (int)CHECKSUM_ALGO_MD4;
if (strcasecmp(name, "sha1") == 0)
return (int)CHECKSUM_ALGO_SHA1;
if (strcasecmp(name, "none") == 0)
return (int)CHECKSUM_ALGO_NONE;
return -1; return -1;
} }
@@ -175,13 +337,21 @@ const char* checksum_algo_name(ChecksumAlgo algo) {
return "xxh128"; return "xxh128";
case CHECKSUM_ALGO_MD5: case CHECKSUM_ALGO_MD5:
return "md5"; return "md5";
case CHECKSUM_ALGO_MD4:
return "md4";
case CHECKSUM_ALGO_SHA1:
return "sha1";
case CHECKSUM_ALGO_NONE:
return "none";
} }
return "<unknown>"; return "<unknown>";
} }
bool checksum_algo_valid(int algo) { bool checksum_algo_valid(int algo) {
return algo == (int)CHECKSUM_ALGO_XXH64 || algo == (int)CHECKSUM_ALGO_MD5 || return algo == (int)CHECKSUM_ALGO_XXH64 || algo == (int)CHECKSUM_ALGO_MD5 ||
algo == (int)CHECKSUM_ALGO_XXH3 || algo == (int)CHECKSUM_ALGO_XXH128; algo == (int)CHECKSUM_ALGO_XXH3 || algo == (int)CHECKSUM_ALGO_XXH128 ||
algo == (int)CHECKSUM_ALGO_MD4 || algo == (int)CHECKSUM_ALGO_SHA1 ||
algo == (int)CHECKSUM_ALGO_NONE;
} }
uint8_t checksum_digest_len(ChecksumAlgo algo) { uint8_t checksum_digest_len(ChecksumAlgo algo) {
@@ -191,7 +361,26 @@ uint8_t checksum_digest_len(ChecksumAlgo algo) {
return 8; return 8;
case CHECKSUM_ALGO_XXH128: case CHECKSUM_ALGO_XXH128:
case CHECKSUM_ALGO_MD5: case CHECKSUM_ALGO_MD5:
case CHECKSUM_ALGO_MD4:
return 16; return 16;
case CHECKSUM_ALGO_SHA1:
return 20;
case CHECKSUM_ALGO_NONE:
return 0;
} }
return 0; return 0;
} }
ChecksumAlgo checksum_negotiate_default(void) {
/* rsync 3.4.1 default preference order; every entry is compiled in, so this
* resolves to xxh128. */
static const ChecksumAlgo preference[] = {
CHECKSUM_ALGO_XXH128, CHECKSUM_ALGO_XXH3, CHECKSUM_ALGO_XXH64, CHECKSUM_ALGO_MD5,
CHECKSUM_ALGO_MD4, CHECKSUM_ALGO_SHA1, CHECKSUM_ALGO_NONE,
};
for (size_t i = 0; i < sizeof(preference) / sizeof(preference[0]); i++) {
if (checksum_algo_valid((int)preference[i]))
return preference[i];
}
return CHECKSUM_ALGO_XXH64;
}
+30 -14
View File
@@ -8,25 +8,35 @@
/* Whole-file content-digest algorithms selectable with --checksum-choice and /* Whole-file content-digest algorithms selectable with --checksum-choice and
* seeded with --checksum-seed. The ids are the values actually placed on the * seeded with --checksum-seed. The ids are the values actually placed on the
* wire (config frame), so they must be kept stable and validated on receive. * wire (config frame), so they must be kept stable and validated on receive.
* CHECKSUM_ALGO_XXH64 == 0 is the default and is byte-for-byte what FastSync * CHECKSUM_ALGO_XXH64 == 0 is the historical FastSync default and its numeric
* computed before these options existed (xxHash64 with seed 0). The set mirrors * value is preserved. The full set mirrors the algorithms rsync 3.4.1 can be
* the algorithms rsync 3.4.1 can be built with; the ones FastSync does not * built with; every one of them is implemented here. */
* implement (md4, sha1, none) are rejected by name at parse time. */
typedef enum { typedef enum {
CHECKSUM_ALGO_XXH64 = 0, CHECKSUM_ALGO_XXH64 = 0,
CHECKSUM_ALGO_MD5 = 1, CHECKSUM_ALGO_MD5 = 1,
CHECKSUM_ALGO_XXH3 = 2, CHECKSUM_ALGO_XXH3 = 2,
CHECKSUM_ALGO_XXH128 = 3 CHECKSUM_ALGO_XXH128 = 3,
CHECKSUM_ALGO_MD4 = 4,
CHECKSUM_ALGO_SHA1 = 5,
CHECKSUM_ALGO_NONE = 6
} ChecksumAlgo; } ChecksumAlgo;
/* xxh128 digest is 16 bytes, the longest supported. */ /* FastSync's negotiated default (rsync 3.4.1 auto-negotiates xxh128 first).
#define CHECKSUM_MAX_DIGEST_LEN 16 * The wire default for Config->checksum_algo is this value. */
#define CHECKSUM_ALGO_DEFAULT CHECKSUM_ALGO_XXH128
/* sha1 digest is 20 bytes, the longest supported. */
#define CHECKSUM_MAX_DIGEST_LEN 20
/* Compute the whole-file digest of the first `size` bytes of `data`. /* Compute the whole-file digest of the first `size` bytes of `data`.
* *
* - CHECKSUM_ALGO_XXH64: xxHash64(data, size, seed) (full 64-bit seed). * - CHECKSUM_ALGO_XXH64: xxHash64(data, size, seed) (full 64-bit seed).
* - CHECKSUM_ALGO_MD5: md5(data, size) via OpenSSL EVP. * - CHECKSUM_ALGO_XXH3: XXH3_64bits_withSeed(data, size, seed).
* md5 has no seed, so `seed` is ignored (documented). * - CHECKSUM_ALGO_XXH128: XXH3_128bits_withSeed(data, size, seed).
* - CHECKSUM_ALGO_MD5: md5(data, size) via OpenSSL EVP (seed ignored).
* - CHECKSUM_ALGO_MD4: md4(data, size), self-contained RFC 1320 (seed ignored).
* - CHECKSUM_ALGO_SHA1: sha1(data, size) via OpenSSL EVP (seed ignored).
* - CHECKSUM_ALGO_NONE: no digest; *out_len is 0 and nothing is written.
* - `size == 0` hashes the empty input (plus its seed), not a NULL input. * - `size == 0` hashes the empty input (plus its seed), not a NULL input.
* *
* Writes up to `out_capacity` bytes into `out`, storing the digest length in * Writes up to `out_capacity` bytes into `out`, storing the digest length in
@@ -41,10 +51,10 @@ bool checksum_digest(ChecksumAlgo algo, uint64_t seed, const void* data, size_t
bool checksum_digest_file(ChecksumAlgo algo, uint64_t seed, const char* path, uint8_t* out, bool checksum_digest_file(ChecksumAlgo algo, uint64_t seed, const char* path, uint8_t* out,
size_t out_capacity, size_t* out_len); size_t out_capacity, size_t* out_len);
/* Resolve a --checksum-choice string (case-insensitive) to an algorithm id. * Accepts /* Resolve a --checksum-choice string (case-insensitive) to an algorithm id.
* "xxh64"/"xxhash", "xxh3", "xxh128" and "md5". "auto", rsync's default automatic choice, is * Accepts "xxh64"/"xxhash", "xxh3", "xxh128", "md5", "md4", "sha1", "none".
* resolved to the default by the caller (it is not a distinct algorithm here). Returns -1 for any * "auto" is not an algorithm here; the caller resolves it to the negotiated
* name FastSync does not implement (md4/sha1/none included). */ * default. Returns -1 for any unrecognized name. */
int checksum_algo_from_name(const char* name); int checksum_algo_from_name(const char* name);
/* Canonical name of an algorithm (used in CLI error messages). */ /* Canonical name of an algorithm (used in CLI error messages). */
@@ -53,7 +63,13 @@ const char* checksum_algo_name(ChecksumAlgo algo);
/* True when `algo` is a supported id (used by config receive validation). */ /* True when `algo` is a supported id (used by config receive validation). */
bool checksum_algo_valid(int algo); bool checksum_algo_valid(int algo);
/* Digest length in bytes for an algorithm (xxh64/xxh3 = 8, md5/xxh128 = 16). */ /* Digest length in bytes for an algorithm (xxh64/xxh3 = 8,
* md5/md4/xxh128 = 16, sha1 = 20, none = 0). */
uint8_t checksum_digest_len(ChecksumAlgo algo); uint8_t checksum_digest_len(ChecksumAlgo algo);
/* Pick the first algorithm from FastSync's compiled-in preference list that is
* supported on this build (rsync 3.4.1's `--version` order:
* xxh128 xxh3 xxh64 md5 md4 sha1 none). Used to resolve "auto". */
ChecksumAlgo checksum_negotiate_default(void);
#endif /* CHECKSUM_H */ #endif /* CHECKSUM_H */
+298 -24
View File
@@ -3,12 +3,15 @@
#include "log.h" #include "log.h"
#include "protocol.h" #include "protocol.h"
#include <limits.h> #include <limits.h>
#include <lz4.h>
#include <stdatomic.h>
#include <stdint.h> #include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <strings.h> #include <strings.h>
#include <threads.h> #include <threads.h>
#include <unistd.h> #include <unistd.h>
#include <zlib.h>
#include <zstd.h> #include <zstd.h>
#define INITIAL_DECOMPRESS_BUF_SIZE (1024 * 1024) #define INITIAL_DECOMPRESS_BUF_SIZE (1024 * 1024)
@@ -29,6 +32,13 @@
"z " \ "z " \
"zip zst" "zip zst"
/* Self-describing compressed frames: the first byte is the CompressionAlgo id.
* zlib/lz4 store the uncompressed size as a little-endian uint32 after the
* codec byte so decompression can be exactly pre-sized and bounded. */
#define LZ4_SIZE_PREFIX_LEN 4
static _Atomic int g_compression_algo = COMPRESSION_ALGO_ZSTD;
/* Case-insensitive match of a bare suffix (no leading dot) against a /* Case-insensitive match of a bare suffix (no leading dot) against a
* space-separated suffix list. */ * space-separated suffix list. */
static bool suffix_in_list(const char* name, const char* list) { static bool suffix_in_list(const char* name, const char* list) {
@@ -67,6 +77,75 @@ bool compression_should_skip_with_suffixes(const char* path, char* const* suffix
return false; return false;
} }
CompressionAlgo compression_default_algo(void) {
return COMPRESSION_ALGO_ZSTD;
}
int compression_algo_from_name(const char* name) {
if (!name)
return -1;
if (strcasecmp(name, "zstd") == 0)
return (int)COMPRESSION_ALGO_ZSTD;
if (strcasecmp(name, "lz4") == 0)
return (int)COMPRESSION_ALGO_LZ4;
if (strcasecmp(name, "zlib") == 0)
return (int)COMPRESSION_ALGO_ZLIB;
if (strcasecmp(name, "zlibx") == 0)
return (int)COMPRESSION_ALGO_ZLIBX;
if (strcasecmp(name, "none") == 0)
return (int)COMPRESSION_ALGO_NONE;
return -1;
}
const char* compression_algo_name(CompressionAlgo algo) {
switch (algo) {
case COMPRESSION_ALGO_NONE:
return "none";
case COMPRESSION_ALGO_ZSTD:
return "zstd";
case COMPRESSION_ALGO_LZ4:
return "lz4";
case COMPRESSION_ALGO_ZLIB:
return "zlib";
case COMPRESSION_ALGO_ZLIBX:
return "zlibx";
}
return "<unknown>";
}
bool compression_algo_valid(int algo) {
return algo == (int)COMPRESSION_ALGO_NONE || algo == (int)COMPRESSION_ALGO_ZSTD ||
algo == (int)COMPRESSION_ALGO_LZ4 || algo == (int)COMPRESSION_ALGO_ZLIB ||
algo == (int)COMPRESSION_ALGO_ZLIBX;
}
bool compression_algo_enabled(CompressionAlgo algo) {
return algo != COMPRESSION_ALGO_NONE;
}
CompressionAlgo compression_negotiate_default(void) {
/* rsync 3.4.1 default preference order; every entry is compiled in, so this
* resolves to zstd. */
static const CompressionAlgo preference[] = {
COMPRESSION_ALGO_ZSTD, COMPRESSION_ALGO_LZ4, COMPRESSION_ALGO_ZLIBX,
COMPRESSION_ALGO_ZLIB, COMPRESSION_ALGO_NONE,
};
for (size_t i = 0; i < sizeof(preference) / sizeof(preference[0]); i++) {
if (compression_algo_valid((int)preference[i]))
return preference[i];
}
return COMPRESSION_ALGO_ZSTD;
}
void compression_set_algo(CompressionAlgo algo) {
if (compression_algo_valid((int)algo))
atomic_store(&g_compression_algo, (int)algo);
}
CompressionAlgo compression_get_algo(void) {
return (CompressionAlgo)atomic_load(&g_compression_algo);
}
/* Per-thread cache of zstd contexts plus the grow-only compression scratch /* Per-thread cache of zstd contexts plus the grow-only compression scratch
* buffer. zstd contexts are stateful and not safe to share between threads, * buffer. zstd contexts are stateful and not safe to share between threads,
* so each thread keeps its own (see compression_get_thread_ctx). The cache is * so each thread keeps its own (see compression_get_thread_ctx). The cache is
@@ -157,17 +236,25 @@ static void compression_ctx_put(CompressionThreadCtx* ctx) {
compression_ctx_free(ctx); compression_ctx_free(ctx);
} }
Data* data_compress(Data* data_to_compress, int compression_level) { /* Build a frame consisting of a copy of `src` prefixed by `codec`. */
return data_compress_with_threads(data_to_compress, compression_level, 0); static Data* frame_with_codec(const void* src, size_t size, CompressionAlgo codec) {
if (size > SIZE_MAX - 1)
return NULL;
Data* out = data_create_empty(size + 1);
if (!out)
return NULL;
((uint8_t*)out->data)[0] = (uint8_t)codec;
if (size > 0)
memcpy((uint8_t*)out->data + 1, src, size);
out->size = size + 1;
return out;
} }
Data* data_compress_with_threads(Data* data_to_compress, int compression_level, static Data* zstd_compress(Data* in, int compression_level, int compression_threads) {
int compression_threads) { size_t dst_size = ZSTD_compressBound(in->size);
if (!data_to_compress || (!data_to_compress->data && data_to_compress->size != 0) || if (dst_size > SIZE_MAX - 1)
compression_threads < 0 || compression_threads > COMPRESSION_MAX_THREADS)
return NULL; return NULL;
log_message(LOG_LEVEL_DEBUG, "Starting to compress data"); dst_size += 1; /* codec prefix */
size_t dst_size = ZSTD_compressBound(data_to_compress->size);
CompressionThreadCtx* ctx = compression_get_thread_ctx(); CompressionThreadCtx* ctx = compression_get_thread_ctx();
if (ctx == NULL) { if (ctx == NULL) {
@@ -218,7 +305,7 @@ Data* data_compress_with_threads(Data* data_to_compress, int compression_level,
if (available_threads > 0) { if (available_threads > 0) {
/* Streaming compression needs the source size before threaded mode can end a frame. */ /* Streaming compression needs the source size before threaded mode can end a frame. */
size_t zret = ZSTD_CCtx_setPledgedSrcSize(ctx->cctx, data_to_compress->size); size_t zret = ZSTD_CCtx_setPledgedSrcSize(ctx->cctx, in->size);
if (ZSTD_isError(zret)) { if (ZSTD_isError(zret)) {
log_message(LOG_LEVEL_ERROR, "Failed to set compression source size: %s", log_message(LOG_LEVEL_ERROR, "Failed to set compression source size: %s",
ZSTD_getErrorName(zret)); ZSTD_getErrorName(zret));
@@ -236,8 +323,8 @@ Data* data_compress_with_threads(Data* data_to_compress, int compression_level,
ctx->out_cap = dst_size; ctx->out_cap = dst_size;
} }
ZSTD_inBuffer input = {data_to_compress->data, data_to_compress->size, 0}; ZSTD_inBuffer input = {in->data, in->size, 0};
ZSTD_outBuffer output = {ctx->out_buf, dst_size, 0}; ZSTD_outBuffer output = {(uint8_t*)ctx->out_buf + 1, dst_size - 1, 0};
size_t ret; size_t ret;
do { do {
@@ -250,30 +337,192 @@ Data* data_compress_with_threads(Data* data_to_compress, int compression_level,
/* Hand off an exactly-sized copy; the scratch buffer stays cached so the next /* Hand off an exactly-sized copy; the scratch buffer stays cached so the next
* call does not reallocate a ZSTD_compressBound-sized block. */ * call does not reallocate a ZSTD_compressBound-sized block. */
compressed_data = data_create_empty(output.pos); compressed_data = data_create_empty(output.pos + 1);
if (compressed_data == NULL) { if (compressed_data == NULL) {
log_message(LOG_LEVEL_ERROR, "Failed to allocate compressed data"); log_message(LOG_LEVEL_ERROR, "Failed to allocate compressed data");
goto cleanup; goto cleanup;
} }
((uint8_t*)compressed_data->data)[0] = (uint8_t)COMPRESSION_ALGO_ZSTD;
if (output.pos > 0) if (output.pos > 0)
memcpy(compressed_data->data, ctx->out_buf, output.pos); memcpy((uint8_t*)compressed_data->data + 1, (uint8_t*)ctx->out_buf + 1, output.pos);
compressed_data->size = output.pos; compressed_data->size = output.pos + 1;
log_debug_message(LOG_DEBUG_UTIL, "Data succesfully compressed from %zu to %zu", log_debug_message(LOG_DEBUG_UTIL, "Data succesfully compressed from %zu to %zu", in->size,
data_to_compress->size, compressed_data->size); compressed_data->size);
cleanup: cleanup:
compression_ctx_put(ctx); compression_ctx_put(ctx);
return compressed_data; return compressed_data;
} }
Data* data_decompress_limited(Data* compressed_data, size_t maximum_size) { static Data* lz4_compress(Data* in) {
if (!compressed_data || (!compressed_data->data && compressed_data->size != 0) || int bound = LZ4_compressBound((int)in->size);
maximum_size == 0) if (bound < 0 || in->size > (size_t)INT_MAX)
return NULL; return NULL;
Data* out = data_create_empty((size_t)bound + 1 + LZ4_SIZE_PREFIX_LEN);
if (!out)
return NULL;
uint32_t raw_size = (uint32_t)in->size;
uint8_t* p = (uint8_t*)out->data;
p[0] = (uint8_t)COMPRESSION_ALGO_LZ4;
for (int i = 0; i < LZ4_SIZE_PREFIX_LEN; i++)
p[1 + i] = (uint8_t)((raw_size >> (8 * i)) & 0xff);
int written = 0;
if (in->size > 0) {
written = LZ4_compress_default((const char*)in->data, (char*)p + 1 + LZ4_SIZE_PREFIX_LEN,
(int)in->size, bound);
if (written <= 0) {
data_destroy(out);
return NULL;
}
}
out->size = (size_t)written + 1 + LZ4_SIZE_PREFIX_LEN;
return out;
}
static Data* zlib_compress(Data* in, CompressionAlgo algo, int compression_level) {
int level = compression_level;
if (level < 1)
level = Z_DEFAULT_COMPRESSION;
if (level > 9)
level = 9;
uLong bound = compressBound((uLong)in->size);
if (in->size > (size_t)ULONG_MAX)
return NULL;
Data* out = data_create_empty((size_t)bound + 1 + LZ4_SIZE_PREFIX_LEN);
if (!out)
return NULL;
uint32_t raw_size = (uint32_t)in->size;
uint8_t* p = (uint8_t*)out->data;
p[0] = (uint8_t)algo;
for (int i = 0; i < LZ4_SIZE_PREFIX_LEN; i++)
p[1 + i] = (uint8_t)((raw_size >> (8 * i)) & 0xff);
uLongf dest_len = bound;
int rc = compress2(p + 1 + LZ4_SIZE_PREFIX_LEN, &dest_len, (const Bytef*)in->data,
(uLong)in->size, level);
if (rc != Z_OK) {
data_destroy(out);
return NULL;
}
out->size = (size_t)dest_len + 1 + LZ4_SIZE_PREFIX_LEN;
return out;
}
Data* data_compress_codec(Data* data_to_compress, CompressionAlgo algo, int compression_level,
int compression_threads) {
if (!data_to_compress || (!data_to_compress->data && data_to_compress->size != 0) ||
compression_threads < 0 || compression_threads > COMPRESSION_MAX_THREADS)
return NULL;
if (!compression_algo_valid((int)algo))
return NULL;
log_message(LOG_LEVEL_DEBUG, "Starting to compress data");
switch (algo) {
case COMPRESSION_ALGO_NONE:
return frame_with_codec(data_to_compress->data, data_to_compress->size, COMPRESSION_ALGO_NONE);
case COMPRESSION_ALGO_ZSTD:
return zstd_compress(data_to_compress, compression_level, compression_threads);
case COMPRESSION_ALGO_LZ4:
return lz4_compress(data_to_compress);
case COMPRESSION_ALGO_ZLIB:
case COMPRESSION_ALGO_ZLIBX:
return zlib_compress(data_to_compress, algo, compression_level);
}
return NULL;
}
Data* data_compress_with_threads(Data* data_to_compress, int compression_level,
int compression_threads) {
return data_compress_codec(data_to_compress, compression_get_algo(), compression_level,
compression_threads);
}
Data* data_compress(Data* data_to_compress, int compression_level) {
return data_compress_codec(data_to_compress, compression_get_algo(), compression_level, 0);
}
static Data* decompress_none(const Data* compressed_data, size_t maximum_size) {
size_t size = compressed_data->size - 1;
if (size > maximum_size)
return NULL;
Data* out = data_create_empty(size);
if (!out)
return NULL;
if (size > 0)
memcpy(out->data, (const uint8_t*)compressed_data->data + 1, size);
out->size = size;
return out;
}
/* Read the 4-byte little-endian raw size stored after the codec byte. */
static bool read_raw_size(const Data* in, uint32_t* raw_size) {
if (in->size < 1 + LZ4_SIZE_PREFIX_LEN)
return false;
const uint8_t* p = (const uint8_t*)in->data;
uint32_t v = 0;
for (int i = 0; i < LZ4_SIZE_PREFIX_LEN; i++)
v |= (uint32_t)p[1 + i] << (8 * i);
*raw_size = v;
return true;
}
static Data* lz4_decompress(Data* compressed_data, size_t maximum_size, size_t hard_limit) {
uint32_t raw_size = 0;
if (!read_raw_size(compressed_data, &raw_size))
return NULL;
if (raw_size > hard_limit || raw_size > maximum_size)
return NULL;
size_t comp_size = compressed_data->size - 1 - LZ4_SIZE_PREFIX_LEN;
Data* out = data_create_empty(raw_size);
if (!out)
return NULL;
if (raw_size == 0) {
out->size = 0;
return out;
}
int rc = LZ4_decompress_safe((const char*)compressed_data->data + 1 + LZ4_SIZE_PREFIX_LEN,
(char*)out->data, (int)comp_size, (int)raw_size);
if (rc < 0 || (uint32_t)rc != raw_size) {
log_message(LOG_LEVEL_ERROR, "LZ4 decompression failed");
data_destroy(out);
return NULL;
}
out->size = raw_size;
return out;
}
static Data* zlib_decompress(Data* compressed_data, size_t maximum_size, size_t hard_limit) {
uint32_t raw_size = 0;
if (!read_raw_size(compressed_data, &raw_size))
return NULL;
if (raw_size > hard_limit || raw_size > maximum_size)
return NULL;
size_t comp_size = compressed_data->size - 1 - LZ4_SIZE_PREFIX_LEN;
Data* out = data_create_empty(raw_size);
if (!out)
return NULL;
if (raw_size == 0) {
out->size = 0;
return out;
}
uLongf dest_len = raw_size;
int rc =
uncompress((Bytef*)out->data, &dest_len,
(const Bytef*)compressed_data->data + 1 + LZ4_SIZE_PREFIX_LEN, (uLong)comp_size);
if (rc != Z_OK || dest_len != raw_size) {
log_message(LOG_LEVEL_ERROR, "zlib decompression failed");
data_destroy(out);
return NULL;
}
out->size = raw_size;
return out;
}
static Data* zstd_decompress(Data* compressed_data, size_t maximum_size) {
/* The zstd frame starts after the codec byte. */
const void* frame = (const uint8_t*)compressed_data->data + 1;
size_t frame_size = compressed_data->size - 1;
log_debug_message(LOG_DEBUG_UTIL, "Start to decompress data"); log_debug_message(LOG_DEBUG_UTIL, "Start to decompress data");
unsigned long long dst_size = unsigned long long dst_size = ZSTD_getFrameContentSize(frame, frame_size);
ZSTD_getFrameContentSize(compressed_data->data, compressed_data->size);
/* ZSTD_isError() is also true for ZSTD_CONTENTSIZE_ERROR and /* ZSTD_isError() is also true for ZSTD_CONTENTSIZE_ERROR and
* ZSTD_CONTENTSIZE_UNKNOWN (both are encoded near (size_t)-1), so test the * ZSTD_CONTENTSIZE_UNKNOWN (both are encoded near (size_t)-1), so test the
* sentinels explicitly instead of blanket-rejecting every error-ish value: * sentinels explicitly instead of blanket-rejecting every error-ish value:
@@ -287,9 +536,9 @@ Data* data_decompress_limited(Data* compressed_data, size_t maximum_size) {
// ZSTD_CONTENTSIZE_UNKNOWN (~2^64) can cause massive allocation; // ZSTD_CONTENTSIZE_UNKNOWN (~2^64) can cause massive allocation;
// fall back to a conservative estimate (3x compressed size) when unknown. // fall back to a conservative estimate (3x compressed size) when unknown.
if (dst_size == ZSTD_CONTENTSIZE_UNKNOWN) { if (dst_size == ZSTD_CONTENTSIZE_UNKNOWN) {
if (compressed_data->size > ULLONG_MAX / 3) if (frame_size > ULLONG_MAX / 3)
return NULL; return NULL;
dst_size = compressed_data->size * 3; dst_size = frame_size * 3;
if (dst_size < INITIAL_DECOMPRESS_BUF_SIZE) if (dst_size < INITIAL_DECOMPRESS_BUF_SIZE)
dst_size = INITIAL_DECOMPRESS_BUF_SIZE; dst_size = INITIAL_DECOMPRESS_BUF_SIZE;
} }
@@ -326,7 +575,7 @@ Data* data_decompress_limited(Data* compressed_data, size_t maximum_size) {
goto cleanup; goto cleanup;
} }
ZSTD_inBuffer input = {compressed_data->data, compressed_data->size, 0}; ZSTD_inBuffer input = {frame, frame_size, 0};
ZSTD_outBuffer output = {uncompressed_data->data, buf_size, 0}; ZSTD_outBuffer output = {uncompressed_data->data, buf_size, 0};
size_t ret; size_t ret;
@@ -385,6 +634,31 @@ cleanup:
return uncompressed_data; return uncompressed_data;
} }
Data* data_decompress_limited(Data* compressed_data, size_t maximum_size) {
if (!compressed_data || (!compressed_data->data && compressed_data->size != 0) ||
maximum_size == 0)
return NULL;
if (compressed_data->size < 1)
return NULL;
unsigned long long hard_limit =
maximum_size < MAX_DECOMPRESSED_SIZE ? maximum_size : MAX_DECOMPRESSED_SIZE;
uint8_t codec = ((const uint8_t*)compressed_data->data)[0];
if (!compression_algo_valid(codec))
return NULL;
switch ((CompressionAlgo)codec) {
case COMPRESSION_ALGO_NONE:
return decompress_none(compressed_data, (size_t)hard_limit);
case COMPRESSION_ALGO_ZSTD:
return zstd_decompress(compressed_data, (size_t)hard_limit);
case COMPRESSION_ALGO_LZ4:
return lz4_decompress(compressed_data, maximum_size, (size_t)hard_limit);
case COMPRESSION_ALGO_ZLIB:
case COMPRESSION_ALGO_ZLIBX:
return zlib_decompress(compressed_data, maximum_size, (size_t)hard_limit);
}
return NULL;
}
Data* data_decompress(Data* compressed_data) { Data* data_decompress(Data* compressed_data) {
return data_decompress_limited(compressed_data, MAX_DECOMPRESSED_SIZE); return data_decompress_limited(compressed_data, MAX_DECOMPRESSED_SIZE);
} }
+51 -1
View File
@@ -6,11 +6,61 @@
#define COMPRESSION_MAX_THREADS 64 #define COMPRESSION_MAX_THREADS 64
/* Compression algorithms selectable with --compress-choice / -z. The ids are
* the values placed on the wire (Config->compression_algo), so they must be
* kept stable. NONE is "no compression"; ZSTD is the historical FastSync
* default and the negotiated "auto" choice. ZLIBX is rsync's zlib-without-
* matched-data variant: FastSync compresses only the delta/token bytes (it does
* not put matched file data in the compression stream), so its zlib codec is
* already the "x" form and zlib/zlibx share the same implementation, recorded
* under distinct ids. */
typedef enum {
COMPRESSION_ALGO_NONE = 0,
COMPRESSION_ALGO_ZSTD = 1,
COMPRESSION_ALGO_LZ4 = 2,
COMPRESSION_ALGO_ZLIB = 3,
COMPRESSION_ALGO_ZLIBX = 4
} CompressionAlgo;
CompressionAlgo compression_default_algo(void);
/* Resolve a --compress-choice string (case-insensitive) to an algorithm id.
* Accepts "zstd", "lz4", "zlib", "zlibx", "none". "auto" is not an algorithm
* here; the caller resolves it to the negotiated default. Returns -1 for any
* unrecognized name. */
int compression_algo_from_name(const char* name);
const char* compression_algo_name(CompressionAlgo algo);
bool compression_algo_valid(int algo);
/* Pick the first algorithm from FastSync's compiled-in preference list
* (rsync 3.4.1's `--version` order: zstd lz4 zlibx zlib none). Resolves
* "auto". */
CompressionAlgo compression_negotiate_default(void);
/* True when the algorithm actually compresses (i.e. is not NONE). */
bool compression_algo_enabled(CompressionAlgo algo);
/* Select the process-wide codec used by the legacy wrappers below. Each
* process serves exactly one transfer config (the server forks per connection,
* the client configures itself before spawning transfer threads), so a
* process-global default is sufficient and constant for the lifetime of a
* transfer. Defaults to ZSTD when never set. Thread-safe. */
void compression_set_algo(CompressionAlgo algo);
CompressionAlgo compression_get_algo(void);
/* Codec-aware primitives. The compressed buffer is self-describing: its first
* byte is the CompressionAlgo id, so decompression never needs the codec passed
* separately (this keeps every existing Decompress call site source-compatible).
* `data_compress_codec` returns NULL on invalid input or an unsupported codec. */
Data* data_compress_codec(Data* data_to_compress, CompressionAlgo algo, int compression_level,
int compression_threads);
Data* data_decompress_limited(Data* compressed_data, size_t maximum_size);
/* Legacy zstd-default wrappers retained for existing callers/tests. */
Data* data_compress(Data* data_to_compress, int compression_level); Data* data_compress(Data* data_to_compress, int compression_level);
Data* data_compress_with_threads(Data* data_to_compress, int compression_level, Data* data_compress_with_threads(Data* data_to_compress, int compression_level,
int compression_threads); int compression_threads);
Data* data_decompress(Data* compressed_data); Data* data_decompress(Data* compressed_data);
Data* data_decompress_limited(Data* compressed_data, size_t maximum_size);
bool compression_should_skip_with_suffixes(const char* path, char* const* suffixes, int count); bool compression_should_skip_with_suffixes(const char* path, char* const* suffixes, int count);
/* Release the calling thread's cached zstd contexts (compressor, decompressor /* Release the calling thread's cached zstd contexts (compressor, decompressor
+75 -27
View File
@@ -14,6 +14,7 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <strings.h>
#include <limits.h> #include <limits.h>
#include <errno.h> #include <errno.h>
@@ -67,6 +68,8 @@ static void config_set_defaults(Config* config) {
config->human_readable = false; config->human_readable = false;
config->ignore_errors = false; config->ignore_errors = false;
config->ignore_missing_args = false; config->ignore_missing_args = false;
config->checksum_transfer_algo = CHECKSUM_ALGO_DEFAULT;
config->cli_exit_code = 0;
config->filters = NULL; config->filters = NULL;
config->files_from = NULL; config->files_from = NULL;
config->files_from_set = NULL; config->files_from_set = NULL;
@@ -196,12 +199,13 @@ static bool validate_received_config(const Config* config) {
valid_wire_bool(config->partial) && valid_wire_bool(config->delete_before) && valid_wire_bool(config->partial) && valid_wire_bool(config->delete_before) &&
valid_wire_bool(config->checksum) && valid_wire_bool(config->eight_bit_output) && valid_wire_bool(config->checksum) && valid_wire_bool(config->eight_bit_output) &&
valid_wire_bool(config->dry_run) && checksum_algo_valid(config->checksum_algo) && valid_wire_bool(config->dry_run) && checksum_algo_valid(config->checksum_algo) &&
identity_wire_valid(config) && valid_wire_bool(config->preserve_atimes) && compression_algo_valid(config->compression_algo) && identity_wire_valid(config) &&
valid_wire_bool(config->preserve_crtimes) && valid_wire_bool(config->omit_dir_times) && valid_wire_bool(config->preserve_atimes) && valid_wire_bool(config->preserve_crtimes) &&
valid_wire_bool(config->omit_link_times) && valid_wire_bool(config->preserve_perms) && valid_wire_bool(config->omit_dir_times) && valid_wire_bool(config->omit_link_times) &&
valid_wire_bool(config->preserve_times) && valid_wire_bool(config->preserve_owner) && valid_wire_bool(config->preserve_perms) && valid_wire_bool(config->preserve_times) &&
valid_wire_bool(config->preserve_group) && valid_wire_bool(config->munge_links) && valid_wire_bool(config->preserve_owner) && valid_wire_bool(config->preserve_group) &&
valid_wire_bool(config->keep_dirlinks) && valid_wire_bool(config->fake_super) && valid_wire_bool(config->munge_links) && valid_wire_bool(config->keep_dirlinks) &&
valid_wire_bool(config->fake_super) &&
(!config->copy_as_set || (config->copy_as_uid >= 0 && config->copy_as_gid >= 0)) && (!config->copy_as_set || (config->copy_as_uid >= 0 && config->copy_as_gid >= 0)) &&
(!config->use_compression || (!config->use_compression ||
(config->compression_level >= 1 && config->compression_level <= 22)) && (config->compression_level >= 1 && config->compression_level <= 22)) &&
@@ -894,6 +898,14 @@ static bool config_receive_checksum_algo(int fd, int* value) {
return true; return true;
} }
static bool config_receive_compression_algo(int fd, int* value) {
int algo;
if (!receive_int(fd, &algo) || !compression_algo_valid(algo))
return false;
*value = algo;
return true;
}
static bool config_receive_super_mode(int fd, SuperMode* value) { static bool config_receive_super_mode(int fd, SuperMode* value) {
int mode; int mode;
if (!receive_int(fd, &mode) || mode < SUPER_MODE_AUTO || mode > SUPER_MODE_OFF) if (!receive_int(fd, &mode) || mode < SUPER_MODE_AUTO || mode > SUPER_MODE_OFF)
@@ -1095,6 +1107,9 @@ fail:
#define CONFIG_SEND_INT_CHECKSUM_ALGO(name) send_int(fd, c->name) #define CONFIG_SEND_INT_CHECKSUM_ALGO(name) send_int(fd, c->name)
#define CONFIG_RECV_INT_CHECKSUM_ALGO(name) config_receive_checksum_algo(fd, &c->name) #define CONFIG_RECV_INT_CHECKSUM_ALGO(name) config_receive_checksum_algo(fd, &c->name)
#define CONFIG_SEND_INT_COMPRESSION_ALGO(name) send_int(fd, c->name)
#define CONFIG_RECV_INT_COMPRESSION_ALGO(name) config_receive_compression_algo(fd, &c->name)
#define CONFIG_SEND_SUPERMODE(name) send_int(fd, (int)c->name) #define CONFIG_SEND_SUPERMODE(name) send_int(fd, (int)c->name)
#define CONFIG_RECV_SUPERMODE(name) config_receive_super_mode(fd, &c->name) #define CONFIG_RECV_SUPERMODE(name) config_receive_super_mode(fd, &c->name)
@@ -1170,6 +1185,7 @@ CONFIG_DEFINE_SEND(send_iconv_spec, CONFIG_WIRE_ICONV_FIELDS)
CONFIG_DEFINE_SEND(send_privilege_options, CONFIG_WIRE_PRIVILEGE_FIELDS) CONFIG_DEFINE_SEND(send_privilege_options, CONFIG_WIRE_PRIVILEGE_FIELDS)
CONFIG_DEFINE_SEND(send_copy_as_options, CONFIG_WIRE_COPY_AS_FIELDS) CONFIG_DEFINE_SEND(send_copy_as_options, CONFIG_WIRE_COPY_AS_FIELDS)
CONFIG_DEFINE_SEND(send_output_options, CONFIG_WIRE_OUTPUT_FIELDS) CONFIG_DEFINE_SEND(send_output_options, CONFIG_WIRE_OUTPUT_FIELDS)
CONFIG_DEFINE_SEND(send_codec_options, CONFIG_WIRE_CODEC_FIELDS)
CONFIG_DEFINE_RECV(receive_core_fields, CONFIG_WIRE_CORE_FIELDS) CONFIG_DEFINE_RECV(receive_core_fields, CONFIG_WIRE_CORE_FIELDS)
CONFIG_DEFINE_RECV(receive_delta_fields, CONFIG_WIRE_DELTA_FIELDS) CONFIG_DEFINE_RECV(receive_delta_fields, CONFIG_WIRE_DELTA_FIELDS)
@@ -1189,6 +1205,7 @@ CONFIG_DEFINE_RECV(receive_iconv_spec, CONFIG_WIRE_ICONV_FIELDS)
CONFIG_DEFINE_RECV(receive_privilege_options, CONFIG_WIRE_PRIVILEGE_FIELDS) CONFIG_DEFINE_RECV(receive_privilege_options, CONFIG_WIRE_PRIVILEGE_FIELDS)
CONFIG_DEFINE_RECV(receive_copy_as_options, CONFIG_WIRE_COPY_AS_FIELDS) CONFIG_DEFINE_RECV(receive_copy_as_options, CONFIG_WIRE_COPY_AS_FIELDS)
CONFIG_DEFINE_RECV(receive_output_options, CONFIG_WIRE_OUTPUT_FIELDS) CONFIG_DEFINE_RECV(receive_output_options, CONFIG_WIRE_OUTPUT_FIELDS)
CONFIG_DEFINE_RECV(receive_codec_options, CONFIG_WIRE_CODEC_FIELDS)
#undef XSEND #undef XSEND
#undef XRECV #undef XRECV
@@ -1306,7 +1323,8 @@ bool config_send_wire_block(int file_descriptor, const Config* config) {
send_iconv_spec(file_descriptor, config) && send_iconv_spec(file_descriptor, config) &&
send_privilege_options(file_descriptor, config) && send_privilege_options(file_descriptor, config) &&
send_copy_as_options(file_descriptor, config) && send_copy_as_options(file_descriptor, config) &&
send_output_options(file_descriptor, config); send_output_options(file_descriptor, config) &&
send_codec_options(file_descriptor, config);
} }
bool config_send(int file_descriptor, const Config* config) { bool config_send(int file_descriptor, const Config* config) {
@@ -1377,29 +1395,59 @@ Config* config_receive_with_validate(int file_descriptor, ConfigValidateFunc val
!receive_iconv_spec(file_descriptor, config, &budget) || !receive_iconv_spec(file_descriptor, config, &budget) ||
!receive_privilege_options(file_descriptor, config, &budget) || !receive_privilege_options(file_descriptor, config, &budget) ||
!receive_copy_as_options(file_descriptor, config, &budget) || !receive_copy_as_options(file_descriptor, config, &budget) ||
!receive_output_options(file_descriptor, config, &budget)) !receive_output_options(file_descriptor, config, &budget) ||
!receive_codec_options(file_descriptor, config, &budget))
goto error; goto error;
if (config->compress_choice[0] != '\0' && strcmp(config->compress_choice, "zstd") != 0 && /* Validate/normalize the negotiated codec. compress_choice is the human
strcmp(config->compress_choice, "none") != 0 && * spelling (NULL or "" when -z was not given); compression_algo is the
strcmp(config->compress_choice, "auto") != 0) { * concrete codec id the sender used. They must agree, and "auto" is
char* escaped_choice = output_escape(config->compress_choice, config->eight_bit_output); * canonicalized to FastSync's negotiated default so the stored spelling is
log_message(LOG_LEVEL_ERROR, "Unsupported compression choice: %s", * always concrete (a hostile/older client may still send "auto"). */
escaped_choice ? escaped_choice : "<allocation failed>"); if (config->compress_choice && config->compress_choice[0] != '\0') {
char detail[128]; int choice_algo = compression_algo_from_name(config->compress_choice);
snprintf(detail, sizeof(detail), "unsupported compression choice: %s", if (choice_algo < 0 && strcasecmp(config->compress_choice, "auto") != 0) {
escaped_choice ? escaped_choice : "<allocation failed>"); char* escaped_choice = output_escape(config->compress_choice, config->eight_bit_output);
send_error_detail(file_descriptor, detail); log_message(LOG_LEVEL_ERROR, "Unsupported compression choice: %s",
free(escaped_choice); escaped_choice ? escaped_choice : "<allocation failed>");
char detail[160];
snprintf(detail, sizeof(detail), "unsupported compression choice: %s",
escaped_choice ? escaped_choice : "<allocation failed>");
send_error_detail(file_descriptor, detail);
free(escaped_choice);
goto error;
}
if (choice_algo < 0)
choice_algo = (int)compression_negotiate_default();
if (strcasecmp(config->compress_choice, "auto") == 0 ||
choice_algo == (int)COMPRESSION_ALGO_NONE) {
const char* canonical = compression_algo_name((CompressionAlgo)choice_algo);
char* dup = str_dup(canonical);
if (!dup)
goto error;
free(config->compress_choice);
config->compress_choice = dup;
}
if (config->compression_algo != choice_algo) {
log_message(LOG_LEVEL_ERROR, "Compression choice '%s' does not match codec id %d",
config->compress_choice, config->compression_algo);
send_error_detail(file_descriptor, "compression choice/codec mismatch");
goto error;
}
}
/* The concrete codec must exist only when compression is on. A client that
* left -z off has no codec in effect, but the field keeps whatever id it
* carried (the receiver never dispatches on it without use_compression), so
* the wire value round-trips untouched. */
if (config->use_compression && config->compression_algo == (int)COMPRESSION_ALGO_NONE) {
log_message(LOG_LEVEL_ERROR, "Compression requested with the 'none' codec");
send_error_detail(file_descriptor, "compression requested with the none codec");
goto error; goto error;
} }
/* Defensive: an older/hostile client may still send "auto"; canonicalize it /* rsync: "none" as the pre-transfer checksum is invalid with --checksum. */
to zstd (its effective choice) so the stored value is always concrete. */ if (config->checksum && config->checksum_algo == (int)CHECKSUM_ALGO_NONE) {
if (strcmp(config->compress_choice, "auto") == 0) { log_message(LOG_LEVEL_ERROR, "Invalid checksum-choice for --checksum: none");
char* canonical = str_dup("zstd"); send_error_detail(file_descriptor, "checksum-choice 'none' cannot be used with --checksum");
if (!canonical) goto error;
goto error;
free(config->compress_choice);
config->compress_choice = canonical;
} }
if (!validate_received_config(config)) { if (!validate_received_config(config)) {
log_message(LOG_LEVEL_ERROR, "Invalid configuration received from client"); log_message(LOG_LEVEL_ERROR, "Invalid configuration received from client");
+51 -6
View File
@@ -3,6 +3,7 @@
#include "array_list.h" #include "array_list.h"
#include "checksum.h" #include "checksum.h"
#include "compression.h"
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
@@ -81,7 +82,7 @@ typedef struct {
typedef enum SuperMode { SUPER_MODE_AUTO = 0, SUPER_MODE_ON = 1, SUPER_MODE_OFF = 2 } SuperMode; typedef enum SuperMode { SUPER_MODE_AUTO = 0, SUPER_MODE_ON = 1, SUPER_MODE_OFF = 2 } SuperMode;
/* =========================================================================== /* ===========================================================================
* Config wire-field table (single source of truth for protocol 2.23.0). * Config wire-field table (single source of truth for protocol 2.26.0).
* *
* Every field below crosses the wire. The table is the ONLY place a * Every field below crosses the wire. The table is the ONLY place a
* serialized field is named: config.h expands CONFIG_WIRE_FIELDS() to declare * serialized field is named: config.h expands CONFIG_WIRE_FIELDS() to declare
@@ -203,7 +204,7 @@ typedef enum SuperMode { SUPER_MODE_AUTO = 0, SUPER_MODE_ON = 1, SUPER_MODE_OFF
#define CONFIG_WIRE_FUZZY_FIELDS(X) X(fuzzy, bool, false, BOOL) #define CONFIG_WIRE_FUZZY_FIELDS(X) X(fuzzy, bool, false, BOOL)
#define CONFIG_WIRE_CHECKSUM_FIELDS(X) \ #define CONFIG_WIRE_CHECKSUM_FIELDS(X) \
X(checksum_algo, int, CHECKSUM_ALGO_XXH64, INT_CHECKSUM_ALGO) \ X(checksum_algo, int, CHECKSUM_ALGO_DEFAULT, INT_CHECKSUM_ALGO) \
X(checksum_seed, uint64_t, 0, RAW) X(checksum_seed, uint64_t, 0, RAW)
#define CONFIG_WIRE_IDENTITY_FIELDS(X) \ #define CONFIG_WIRE_IDENTITY_FIELDS(X) \
@@ -262,6 +263,27 @@ typedef enum SuperMode { SUPER_MODE_AUTO = 0, SUPER_MODE_ON = 1, SUPER_MODE_OFF
#define CONFIG_WIRE_OUTPUT_FIELDS(X) \ #define CONFIG_WIRE_OUTPUT_FIELDS(X) \
X(report_dest_info, bool, false, BOOL) X(report_stats, bool, false, BOOL) X(report_dest_info, bool, false, BOOL) X(report_stats, bool, false, BOOL)
/* Codec-negotiation wave (protocol 2.26.0). compression_algo is the concrete
* codec the client selected for this transfer (a CompressionAlgo id) and is the
* value the receiver validates and installs. It is the resolved result of
* --compress-choice / the "auto" negotiation so both peers agree exactly.
*
* Negotiation model: FastSync enforces a strict same-version handshake, so both
* peers carry the identical compiled-in codec set. The client resolves the
* effective algorithm deterministically and serializes it here; "auto" picks
* the first entry of the rsync 3.4.1 preference order
* (compression: zstd lz4 zlibx zlib none; checksum: xxh128 xxh3 xxh64 md5 md4
* sha1 none), and an explicit request wins. The receiver rejects (before
* STATUS_OK) any algorithm outside its own supported set, which is rsync's
* "no common choice is an error" behavior. The same resolver runs on both
* sides (compression_negotiate_default / checksum_negotiate_default), so the
* fallback is consistent.
*
* The field is appended after the output block so every pre-2.26 field keeps
* its wire position. */
#define CONFIG_WIRE_CODEC_FIELDS(X) \
X(compression_algo, int, COMPRESSION_ALGO_ZSTD, INT_COMPRESSION_ALGO)
/* All serialized fields, in exact wire order. Concatenating the per-segment /* All serialized fields, in exact wire order. Concatenating the per-segment
* lists here is what keeps the declaration order = the wire order. */ * lists here is what keeps the declaration order = the wire order. */
#define CONFIG_WIRE_FIELDS(X) \ #define CONFIG_WIRE_FIELDS(X) \
@@ -283,7 +305,8 @@ typedef enum SuperMode { SUPER_MODE_AUTO = 0, SUPER_MODE_ON = 1, SUPER_MODE_OFF
CONFIG_WIRE_ICONV_FIELDS(X) \ CONFIG_WIRE_ICONV_FIELDS(X) \
CONFIG_WIRE_PRIVILEGE_FIELDS(X) \ CONFIG_WIRE_PRIVILEGE_FIELDS(X) \
CONFIG_WIRE_COPY_AS_FIELDS(X) \ CONFIG_WIRE_COPY_AS_FIELDS(X) \
CONFIG_WIRE_OUTPUT_FIELDS(X) CONFIG_WIRE_OUTPUT_FIELDS(X) \
CONFIG_WIRE_CODEC_FIELDS(X)
typedef struct Config { typedef struct Config {
/* -j/--threads=N: number of parallel scanner worker threads for the -m /* -j/--threads=N: number of parallel scanner worker threads for the -m
@@ -378,6 +401,16 @@ typedef struct Config {
* enters the keep-set. Implied by --delete-missing-args. */ * enters the keep-set. Implied by --delete-missing-args. */
bool ignore_missing_args; bool ignore_missing_args;
/* Codec-negotiation CLI state (all client-only, never serialized). The
* effective pre-transfer checksum is Config->checksum_algo (serialized);
* checksum_transfer_algo is the rsync "transfer" half of a two-name
* --checksum-choice form (validated and used only to mirror rsync's
* whole-file forcing, since FastSync's per-block strong hash is fixed).
* cli_exit_code carries a parser-requested process exit status (rsync uses 4
* for an unsupported checksum/compress algorithm) so main() can mirror it. */
int checksum_transfer_algo;
int cli_exit_code;
// Issue #129: Advanced file selection. These fields are CLIENT-ONLY: they are // Issue #129: Advanced file selection. These fields are CLIENT-ONLY: they are
// never serialized to the wire (the receiver must not learn them). // never serialized to the wire (the receiver must not learn them).
ArrayList* filters; /* --filter=RULE rule strings, in order */ ArrayList* filters; /* --filter=RULE rule strings, in order */
@@ -941,9 +974,21 @@ typedef struct Config {
* would have removed without deleting anything. The config frame gains one * would have removed without deleting anything. The config frame gains one
* trailing report_stats bool and the receiver emits a new STATUS_STATS frame * trailing report_stats bool and the receiver emits a new STATUS_STATS frame
* (carrying matched data, the deleted-file count and the would-delete path * (carrying matched data, the deleted-file count and the would-delete path
* list) immediately before its terminal success status. Both a config-frame * list) immediately before its terminal success status.
* layout change and a frame-sequence change, hence the bump. */ *
#define PROTOCOL_VERSION "2.25.0" * (6) Codec breadth + negotiation (protocol 2.26.0): the config frame gains one
* trailing int, compression_algo (a CompressionAlgo id), appended after the
* output block. It is the negotiated/effective compression codec and is what
* the receiver's self-describing decompressor validates against its own
* supported set. The checksum_algo wire value now also accepts md4/sha1/none,
* and its default changes to the rsync 3.4.1 auto-negotiated xxh128.
*
* 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) keeps mixed deployments from
* ever reaching that state. */
#define PROTOCOL_VERSION "2.26.0"
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024) #define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */ /* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
#define MAX_BASIS_DIRS 64 #define MAX_BASIS_DIRS 64
+218
View File
@@ -0,0 +1,218 @@
"""Differential tests for --checksum-choice / --compress-choice against rsync 3.4.1.
These pin the accepted/rejected algorithm matrix and exit codes to real rsync,
and verify that every codec FastSync now offers still transfers byte-exactly.
The rsync-based tests skip cleanly when rsync is not installed.
The FastSync server confines transfers to its authorized root (the project
directory when the shared test server is launched), so every scratch tree lives
under ``TEST_DATA_DIR`` rather than pytest's ``tmp_path``.
"""
import os
import shutil
import subprocess
import sys
import pytest
sys.path.insert(0, os.path.dirname(__file__))
from common import (
TEST_DATA_DIR,
run_client,
clean_dir,
get_dest_received_dir,
)
RSYNC = shutil.which("rsync")
requires_rsync = pytest.mark.skipif(RSYNC is None, reason="rsync 3.4.1 not installed")
CHECKSUM_NAMES = ["xxh128", "xxh3", "xxh64", "md5", "md4", "sha1"]
COMPRESS_NAMES = ["zstd", "lz4", "zlib", "zlibx"]
CODEC_ROOT = os.path.join(TEST_DATA_DIR, "codec_differential")
def _rsync(args):
env = dict(os.environ, LC_ALL="C")
return subprocess.run([RSYNC] + args, capture_output=True, text=True, env=env, timeout=120)
def _scratch(tag):
"""A confined, uniquely named scratch directory under the project tree."""
path = os.path.join(CODEC_ROOT, tag)
clean_dir(path)
os.makedirs(path, exist_ok=True)
return path
def _make_corpus(root):
clean_dir(root)
os.makedirs(os.path.join(root, "sub"), exist_ok=True)
# Highly compressible payload so each codec is actually exercised.
with open(os.path.join(root, "big.bin"), "wb") as fh:
fh.write(b"FastSync codec payload " * 4096)
with open(os.path.join(root, "sub", "text.txt"), "wb") as fh:
fh.write(b"hello codec world\n" * 128)
with open(os.path.join(root, "empty"), "wb"):
pass
return root
def _tree_bytes(root):
out = {}
for dirpath, _dirs, files in os.walk(root):
for name in files:
path = os.path.join(dirpath, name)
with open(path, "rb") as fh:
out[os.path.relpath(path, root)] = fh.read()
return out
class TestCodecChoiceMatrix:
"""The CLI accept/reject set and exit codes must match rsync 3.4.1."""
@requires_rsync
@pytest.mark.ci
@pytest.mark.parametrize("name", CHECKSUM_NAMES)
def test_checksum_names_accepted_by_both(self, name, shared_server):
src = _make_corpus(_scratch(f"cc_src_{name}"))
rdst = _scratch(f"cc_rsync_{name}")
rsync_result = _rsync(["-a", f"--cc={name}", src + "/", rdst + "/"])
assert rsync_result.returncode == 0, rsync_result.stderr
fdst = _scratch(f"cc_fs_{name}")
result, _ = run_client(src, fdst, flags=[f"--cc={name}"], port=shared_server.port)
assert result.returncode == 0, (result.stderr or result.stdout)[:300]
@requires_rsync
@pytest.mark.ci
@pytest.mark.parametrize("name", COMPRESS_NAMES)
def test_compress_names_accepted_by_both(self, name, shared_server):
src = _make_corpus(_scratch(f"zc_src_{name}"))
rdst = _scratch(f"zc_rsync_{name}")
rsync_result = _rsync(["-az", f"--zc={name}", src + "/", rdst + "/"])
assert rsync_result.returncode == 0, rsync_result.stderr
fdst = _scratch(f"zc_fs_{name}")
result, _ = run_client(src, fdst, flags=["-z", f"--zc={name}"], port=shared_server.port)
assert result.returncode == 0, (result.stderr or result.stdout)[:300]
@requires_rsync
@pytest.mark.ci
@pytest.mark.parametrize("choice", ["md4,sha1", "sha1,md4", "auto,md5", "none,md5"])
def test_checksum_two_name_accepted_by_both(self, choice, shared_server):
tag = choice.replace(",", "_")
src = _make_corpus(_scratch(f"two_src_{tag}"))
rdst = _scratch(f"two_rsync_{tag}")
rsync_result = _rsync(["-a", "--checksum", f"--cc={choice}", src + "/", rdst + "/"])
assert rsync_result.returncode == 0, rsync_result.stderr
fdst = _scratch(f"two_fs_{tag}")
result, _ = run_client(src, fdst, flags=["--checksum", f"--cc={choice}"],
port=shared_server.port)
assert result.returncode == 0, (result.stderr or result.stdout)[:200]
@requires_rsync
@pytest.mark.ci
@pytest.mark.parametrize("name", ["sha256", "crc32", "md5,", "md4,md5,sha1"])
def test_unknown_checksum_rejected_exit_4_both(self, name, shared_server):
src = _make_corpus(_scratch(f"badcc_src_{name.replace(',', '_').replace(':', '_')}"))
rdst = _scratch(f"badcc_rsync_{name.replace(',', '_').replace(':', '_')}")
rsync_result = _rsync(["-a", f"--cc={name}", src + "/", rdst + "/"])
assert rsync_result.returncode == 4, rsync_result.stderr
fdst = _scratch(f"badcc_fs_{name.replace(',', '_').replace(':', '_')}")
result, _ = run_client(src, fdst, flags=[f"--cc={name}"], port=shared_server.port)
assert result.returncode == 4, (result.stderr or result.stdout)[:200]
@requires_rsync
@pytest.mark.ci
@pytest.mark.parametrize("choice", ["none", "md5,none"])
def test_checksum_none_with_checksum_rejected_exit_4_both(self, choice, shared_server):
tag = choice.replace(",", "_")
src = _make_corpus(_scratch(f"nonecc_src_{tag}"))
rdst = _scratch(f"nonecc_rsync_{tag}")
rsync_result = _rsync(["-a", "--checksum", f"--cc={choice}", src + "/", rdst + "/"])
assert rsync_result.returncode == 4, rsync_result.stderr
fdst = _scratch(f"nonecc_fs_{tag}")
result, _ = run_client(src, fdst, flags=["--checksum", f"--cc={choice}"],
port=shared_server.port)
assert result.returncode == 4, (result.stderr or result.stdout)[:200]
@requires_rsync
@pytest.mark.ci
@pytest.mark.parametrize("name", ["bogus", "zstd,lz4"])
def test_unknown_compress_rejected_exit_4_both(self, name, shared_server):
tag = name.replace(",", "_")
src = _make_corpus(_scratch(f"badzc_src_{tag}"))
rdst = _scratch(f"badzc_rsync_{tag}")
rsync_result = _rsync(["-az", f"--zc={name}", src + "/", rdst + "/"])
assert rsync_result.returncode == 4, rsync_result.stderr
fdst = _scratch(f"badzc_fs_{tag}")
result, _ = run_client(src, fdst, flags=["-z", f"--zc={name}"], port=shared_server.port)
assert result.returncode == 4, (result.stderr or result.stdout)[:200]
class TestCodecTransferDifferential:
"""Each codec lands the same bytes rsync lands."""
@requires_rsync
@pytest.mark.ci
@pytest.mark.parametrize("name", COMPRESS_NAMES + ["none"])
def test_compress_codec_matches_rsync_bytes(self, name, shared_server):
src = _make_corpus(_scratch(f"byteszc_src_{name}"))
rsync_dst = _scratch(f"byteszc_rsync_{name}")
rsync_result = _rsync(["-a", "-z", f"--zc={name}", src + "/", rsync_dst + "/"])
assert rsync_result.returncode == 0, rsync_result.stderr
fs_dst = _scratch(f"byteszc_fs_{name}")
result, _ = run_client(src, fs_dst, flags=["-a", "-z", f"--zc={name}"],
port=shared_server.port)
assert result.returncode == 0, (result.stderr or result.stdout)[:300]
received = get_dest_received_dir(fs_dst, src)
assert _tree_bytes(received) == _tree_bytes(rsync_dst)
@requires_rsync
@pytest.mark.ci
@pytest.mark.parametrize("name", CHECKSUM_NAMES)
def test_checksum_codec_matches_rsync_bytes(self, name, shared_server):
src = _make_corpus(_scratch(f"bytescc_src_{name}"))
rsync_dst = _scratch(f"bytescc_rsync_{name}")
rsync_result = _rsync(["-a", "--checksum", f"--cc={name}", src + "/", rsync_dst + "/"])
assert rsync_result.returncode == 0, rsync_result.stderr
fs_dst = _scratch(f"bytescc_fs_{name}")
result, _ = run_client(src, fs_dst, flags=["-a", "--checksum", f"--cc={name}"],
port=shared_server.port)
assert result.returncode == 0, (result.stderr or result.stdout)[:300]
received = get_dest_received_dir(fs_dst, src)
assert _tree_bytes(received) == _tree_bytes(rsync_dst)
class TestCodecNegotiationFallback:
"""FastSync's auto negotiation and deterministic fallback order."""
@pytest.mark.ci
def test_default_checksum_and_compression_agree(self, shared_server):
"""A default transfer (auto on both peers) succeeds; the negotiated
default is xxh128 + zstd."""
src = _make_corpus(_scratch("auto_src"))
fdst = _scratch("auto_fs")
result, _ = run_client(src, fdst, flags=["-a", "-z"], port=shared_server.port)
assert result.returncode == 0, (result.stderr or result.stdout)[:300]
received = get_dest_received_dir(fdst, src)
assert _tree_bytes(received) == _tree_bytes(src)
@pytest.mark.ci
def test_explicit_choice_overrides_auto(self, shared_server):
"""An explicit --zc/--cc wins over the negotiated default on both ends,
so the receiver decodes with the sender's codec."""
src = _make_corpus(_scratch("explicit_src"))
fdst = _scratch("explicit_fs")
result, _ = run_client(src, fdst, flags=["-a", "-z", "--zc=lz4", "--cc=sha1"],
port=shared_server.port)
assert result.returncode == 0, (result.stderr or result.stdout)[:300]
received = get_dest_received_dir(fdst, src)
assert _tree_bytes(received) == _tree_bytes(src)
+1 -1
View File
@@ -36,7 +36,7 @@ from common import ( # noqa: E402
verify_transfer, verify_transfer,
) )
PROTOCOL_VERSION = b"2.25.0" PROTOCOL_VERSION = b"2.26.0"
STATUS_MANIFEST = 5 STATUS_MANIFEST = 5
STATUS_OK = 0 STATUS_OK = 0
+87 -6
View File
@@ -1491,20 +1491,101 @@ class TestChecksumChoice:
assert fh.read() == b"same content\n" assert fh.read() == b"same content\n"
@pytest.mark.ci @pytest.mark.ci
def test_checksum_choice_md4_single_name_rejected(self, shared_server): @pytest.mark.parametrize("algo", ["xxh128", "xxh3", "xxh64", "md5", "md4", "sha1"])
for bad in ("md4", "sha1", "none", "xxh64,md5"): def test_checksum_choice_all_algorithms_transfer(self, shared_server, algo):
"""Every rsync 3.4.1 checksum algorithm is accepted and transfers
byte-exactly. 'none' is covered separately (it needs no digest)."""
clean_dir(DEST_DIR)
flags = ["--preserve", "--incremental", "--checksum", f"--checksum-choice={algo}"]
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=flags, port=shared_server.port)
assert result.returncode == 0, \
f"checksum-choice={algo} failed: {(result.stderr or result.stdout)[:300]}"
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
mismatches, missing = verify_transfer(SOURCE_DIR, received)
assert not missing, f"Missing: {missing}"
assert not mismatches, f"Mismatch: {mismatches}"
@pytest.mark.ci
def test_checksum_choice_two_name_form(self, shared_server):
"""The rsync 'TRANSFER,PRE-TRANSFER' form is accepted; FastSync uses the
second (pre-transfer) algorithm for its whole-file digest."""
clean_dir(DEST_DIR)
flags = ["--preserve", "--incremental", "--checksum", "--cc=md4,sha1"]
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=flags, port=shared_server.port)
assert result.returncode == 0, \
f"two-name --cc=md4,sha1 failed: {(result.stderr or result.stdout)[:300]}"
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
mismatches, missing = verify_transfer(SOURCE_DIR, received)
assert not missing and not mismatches, f"missing={missing} mismatches={mismatches}"
@pytest.mark.ci
def test_checksum_choice_none_accepted_without_checksum(self, shared_server):
clean_dir(DEST_DIR)
result, _ = run_client(SOURCE_DIR, DEST_DIR,
flags=["--preserve", "--incremental", "--cc=none"],
port=shared_server.port)
assert result.returncode == 0, \
f"--cc=none failed: {(result.stderr or result.stdout)[:300]}"
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
mismatches, missing = verify_transfer(SOURCE_DIR, received)
assert not missing and not mismatches, f"missing={missing} mismatches={mismatches}"
@pytest.mark.ci
def test_checksum_choice_none_rejected_with_checksum(self, shared_server):
"""rsync rejects 'none' as the pre-transfer checksum with --checksum and
exits 4; mirror both the rejection and the exit code."""
for choice in ("none", "md5,none"):
result, _ = run_client(SOURCE_DIR, DEST_DIR,
flags=["--checksum", f"--cc={choice}"],
port=shared_server.port)
assert result.returncode == 4, \
f"--cc={choice} --checksum must exit 4, got {result.returncode}: " \
f"{(result.stderr or result.stdout)[:200]}"
@pytest.mark.ci
def test_checksum_choice_unknown_rejected_exit_4(self, shared_server):
for bad in ("sha256", "bogus", "md5,", "md4,md5,sha1"):
result, _ = run_client(SOURCE_DIR, DEST_DIR, result, _ = run_client(SOURCE_DIR, DEST_DIR,
flags=[f"--checksum-choice={bad}"], flags=[f"--checksum-choice={bad}"],
port=shared_server.port) port=shared_server.port)
assert result.returncode != 0, f"{bad} must be rejected" assert result.returncode == 4, \
f"--checksum-choice={bad} must exit 4, got {result.returncode}"
@pytest.mark.ci @pytest.mark.ci
def test_compress_choice_unsupported_rejected(self, shared_server): @pytest.mark.parametrize("algo", ["zstd", "lz4", "zlib", "zlibx"])
for bad in ("lz4", "zlib", "zlibx"): def test_compress_choice_all_algorithms_transfer(self, shared_server, algo):
"""Every rsync 3.4.1 compression codec is accepted and transfers
byte-exactly through its own codec."""
clean_dir(DEST_DIR)
flags = ["-z", f"--compress-choice={algo}"]
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=flags, port=shared_server.port)
assert result.returncode == 0, \
f"--compress-choice={algo} failed: {(result.stderr or result.stdout)[:300]}"
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
mismatches, missing = verify_transfer(SOURCE_DIR, received)
assert not missing, f"Missing: {missing}"
assert not mismatches, f"Mismatch: {mismatches}"
@pytest.mark.ci
def test_compress_choice_unknown_rejected_exit_4(self, shared_server):
for bad in ("bogus", "zstd,lz4", ""):
result, _ = run_client(SOURCE_DIR, DEST_DIR, result, _ = run_client(SOURCE_DIR, DEST_DIR,
flags=[f"--compress-choice={bad}"], flags=[f"--compress-choice={bad}"],
port=shared_server.port) port=shared_server.port)
assert result.returncode != 0, f"{bad} must be rejected" assert result.returncode == 4, \
f"--compress-choice={bad} must exit 4, got {result.returncode}"
@pytest.mark.ci
def test_compress_choice_none_disables_compression(self, shared_server):
clean_dir(DEST_DIR)
result, _ = run_client(SOURCE_DIR, DEST_DIR,
flags=["-z", "--compress-choice=none"],
port=shared_server.port)
assert result.returncode == 0, \
f"--compress-choice=none failed: {(result.stderr or result.stdout)[:300]}"
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
mismatches, missing = verify_transfer(SOURCE_DIR, received)
assert not missing and not mismatches, f"missing={missing} mismatches={mismatches}"
@pytest.mark.ci @pytest.mark.ci
def test_compress_choice_auto_transfers(self, shared_server): def test_compress_choice_auto_transfers(self, shared_server):
+2 -2
View File
@@ -94,14 +94,14 @@ def _seed_protocol_source(source):
class TestProtocol: class TestProtocol:
@pytest.mark.ci @pytest.mark.ci
def test_protocol_current_version_accepted(self, shared_server): def test_protocol_current_version_accepted(self, shared_server):
"""--protocol=2.25.0 (the current PROTOCOL_VERSION) is accepted and the """--protocol=2.26.0 (the current PROTOCOL_VERSION) is accepted and the
transfer completes normally.""" transfer completes normally."""
source = os.path.join(TEST_DATA_DIR, "proto_ok_src") source = os.path.join(TEST_DATA_DIR, "proto_ok_src")
dest = os.path.join(TEST_DATA_DIR, "proto_ok_dst") dest = os.path.join(TEST_DATA_DIR, "proto_ok_dst")
shutil.rmtree(dest, ignore_errors=True) shutil.rmtree(dest, ignore_errors=True)
os.makedirs(dest) os.makedirs(dest)
_seed_protocol_source(source) _seed_protocol_source(source)
result, _ = run_client(source, dest, flags=["--protocol=2.25.0"], result, _ = run_client(source, dest, flags=["--protocol=2.26.0"],
port=shared_server.port) port=shared_server.port)
assert result.returncode == 0, \ assert result.returncode == 0, \
f"--protocol current run failed: {(result.stderr or result.stdout)[:400]}" f"--protocol current run failed: {(result.stderr or result.stdout)[:400]}"
+75 -4
View File
@@ -91,6 +91,63 @@ static void test_checksum_md5_seed_ignored() {
EXPECT_TRUE(memcmp(a, b, alen) == 0); EXPECT_TRUE(memcmp(a, b, alen) == 0);
} }
static void test_checksum_md4_vectors() {
uint8_t out[CHECKSUM_MAX_DIGEST_LEN];
size_t len = 0;
/* RFC 1320 / RFC 1321 test vectors. */
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_MD4, 0, "", 0, out, sizeof(out), &len));
EXPECT_TRUE(len == (size_t)16);
const uint8_t expect_empty[16] = {0x31, 0xd6, 0xcf, 0xe0, 0xd1, 0x6a, 0xe9, 0x31,
0xb7, 0x3c, 0x59, 0xd7, 0xe0, 0xc0, 0x89, 0xc0};
EXPECT_TRUE(memcmp(out, expect_empty, 16) == 0);
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_MD4, 0, "abc", 3, out, sizeof(out), &len));
const uint8_t expect_abc[16] = {0xa4, 0x48, 0x01, 0x7a, 0xaf, 0x21, 0xd8, 0x52,
0x5f, 0xc1, 0x0a, 0xe8, 0x7a, 0xa6, 0x72, 0x9d};
EXPECT_TRUE(memcmp(out, expect_abc, 16) == 0);
/* A longer input exercises the block loop and the padding boundary. */
const char* msg =
"12345678901234567890123456789012345678901234567890123456789012345678901234567890";
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_MD4, 0, msg, strlen(msg), out, sizeof(out), &len));
const uint8_t expect_long[16] = {0xe3, 0x3b, 0x4d, 0xdc, 0x9c, 0x38, 0xf2, 0x19,
0x9c, 0x3e, 0x7b, 0x16, 0x4f, 0xcc, 0x05, 0x36};
EXPECT_TRUE(memcmp(out, expect_long, 16) == 0);
}
static void test_checksum_sha1_vectors() {
uint8_t out[CHECKSUM_MAX_DIGEST_LEN];
size_t len = 0;
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_SHA1, 0, "abc", 3, out, sizeof(out), &len));
EXPECT_TRUE(len == (size_t)20);
const uint8_t expect_abc[20] = {0xa9, 0x99, 0x3e, 0x36, 0x47, 0x06, 0x81, 0x6a, 0xba, 0x3e,
0x25, 0x71, 0x78, 0x50, 0xc2, 0x6c, 0x9c, 0xd0, 0xd8, 0x9d};
EXPECT_TRUE(memcmp(out, expect_abc, 20) == 0);
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_SHA1, 0, "", 0, out, sizeof(out), &len));
EXPECT_TRUE(len == (size_t)20);
const uint8_t expect_empty[20] = {0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, 0x32, 0x55,
0xbf, 0xef, 0x95, 0x60, 0x18, 0x90, 0xaf, 0xd8, 0x07, 0x09};
EXPECT_TRUE(memcmp(out, expect_empty, 20) == 0);
/* sha1 has no seed: the digest is seed-independent (documented). */
uint8_t seeded[CHECKSUM_MAX_DIGEST_LEN];
size_t seeded_len = 0;
EXPECT_TRUE(
checksum_digest(CHECKSUM_ALGO_SHA1, 12345, "abc", 3, seeded, sizeof(seeded), &seeded_len));
EXPECT_TRUE(seeded_len == (size_t)20);
EXPECT_TRUE(memcmp(expect_abc, seeded, 20) == 0);
}
/* "none" is a successful no-digest: length 0, nothing written. */
static void test_checksum_none_digest() {
uint8_t out[CHECKSUM_MAX_DIGEST_LEN];
size_t len = 99;
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_NONE, 0, "data", 4, out, sizeof(out), &len));
EXPECT_EQ_INT((int)len, 0);
EXPECT_EQ_INT((int)checksum_digest_len(CHECKSUM_ALGO_NONE), 0);
}
static void test_checksum_algo_name_mapping() { static void test_checksum_algo_name_mapping() {
EXPECT_EQ_INT(checksum_algo_from_name("xxh64"), (int)CHECKSUM_ALGO_XXH64); EXPECT_EQ_INT(checksum_algo_from_name("xxh64"), (int)CHECKSUM_ALGO_XXH64);
EXPECT_EQ_INT(checksum_algo_from_name("XXH64"), (int)CHECKSUM_ALGO_XXH64); EXPECT_EQ_INT(checksum_algo_from_name("XXH64"), (int)CHECKSUM_ALGO_XXH64);
@@ -102,12 +159,14 @@ static void test_checksum_algo_name_mapping() {
EXPECT_EQ_INT(checksum_algo_from_name("XXH3"), (int)CHECKSUM_ALGO_XXH3); EXPECT_EQ_INT(checksum_algo_from_name("XXH3"), (int)CHECKSUM_ALGO_XXH3);
EXPECT_EQ_INT(checksum_algo_from_name("xxh128"), (int)CHECKSUM_ALGO_XXH128); EXPECT_EQ_INT(checksum_algo_from_name("xxh128"), (int)CHECKSUM_ALGO_XXH128);
EXPECT_EQ_INT(checksum_algo_from_name("XXH128"), (int)CHECKSUM_ALGO_XXH128); EXPECT_EQ_INT(checksum_algo_from_name("XXH128"), (int)CHECKSUM_ALGO_XXH128);
/* rsync choices FastSync does not implement are rejected by name. */ EXPECT_EQ_INT(checksum_algo_from_name("md4"), (int)CHECKSUM_ALGO_MD4);
EXPECT_TRUE(checksum_algo_from_name("md4") < 0); EXPECT_EQ_INT(checksum_algo_from_name("MD4"), (int)CHECKSUM_ALGO_MD4);
EXPECT_TRUE(checksum_algo_from_name("sha1") < 0); EXPECT_EQ_INT(checksum_algo_from_name("sha1"), (int)CHECKSUM_ALGO_SHA1);
EXPECT_EQ_INT(checksum_algo_from_name("SHA1"), (int)CHECKSUM_ALGO_SHA1);
EXPECT_EQ_INT(checksum_algo_from_name("none"), (int)CHECKSUM_ALGO_NONE);
/* Names rsync does not offer (or FastSync cannot compute) are rejected. */
EXPECT_TRUE(checksum_algo_from_name("sha256") < 0); EXPECT_TRUE(checksum_algo_from_name("sha256") < 0);
EXPECT_TRUE(checksum_algo_from_name("crc32") < 0); EXPECT_TRUE(checksum_algo_from_name("crc32") < 0);
EXPECT_TRUE(checksum_algo_from_name("none") < 0);
EXPECT_TRUE(checksum_algo_from_name("") < 0); EXPECT_TRUE(checksum_algo_from_name("") < 0);
EXPECT_TRUE(checksum_algo_from_name(NULL) < 0); EXPECT_TRUE(checksum_algo_from_name(NULL) < 0);
@@ -115,11 +174,20 @@ static void test_checksum_algo_name_mapping() {
EXPECT_TRUE(checksum_algo_valid((int)CHECKSUM_ALGO_MD5)); EXPECT_TRUE(checksum_algo_valid((int)CHECKSUM_ALGO_MD5));
EXPECT_TRUE(checksum_algo_valid((int)CHECKSUM_ALGO_XXH3)); EXPECT_TRUE(checksum_algo_valid((int)CHECKSUM_ALGO_XXH3));
EXPECT_TRUE(checksum_algo_valid((int)CHECKSUM_ALGO_XXH128)); EXPECT_TRUE(checksum_algo_valid((int)CHECKSUM_ALGO_XXH128));
EXPECT_TRUE(checksum_algo_valid((int)CHECKSUM_ALGO_MD4));
EXPECT_TRUE(checksum_algo_valid((int)CHECKSUM_ALGO_SHA1));
EXPECT_TRUE(checksum_algo_valid((int)CHECKSUM_ALGO_NONE));
EXPECT_FALSE(checksum_algo_valid(99)); EXPECT_FALSE(checksum_algo_valid(99));
EXPECT_EQ_STR(checksum_algo_name(CHECKSUM_ALGO_XXH64), "xxh64"); EXPECT_EQ_STR(checksum_algo_name(CHECKSUM_ALGO_XXH64), "xxh64");
EXPECT_EQ_STR(checksum_algo_name(CHECKSUM_ALGO_MD5), "md5"); EXPECT_EQ_STR(checksum_algo_name(CHECKSUM_ALGO_MD5), "md5");
EXPECT_EQ_STR(checksum_algo_name(CHECKSUM_ALGO_XXH3), "xxh3"); EXPECT_EQ_STR(checksum_algo_name(CHECKSUM_ALGO_XXH3), "xxh3");
EXPECT_EQ_STR(checksum_algo_name(CHECKSUM_ALGO_XXH128), "xxh128"); EXPECT_EQ_STR(checksum_algo_name(CHECKSUM_ALGO_XXH128), "xxh128");
EXPECT_EQ_STR(checksum_algo_name(CHECKSUM_ALGO_MD4), "md4");
EXPECT_EQ_STR(checksum_algo_name(CHECKSUM_ALGO_SHA1), "sha1");
EXPECT_EQ_STR(checksum_algo_name(CHECKSUM_ALGO_NONE), "none");
/* rsync 3.4.1 auto-negotiates xxh128 first. */
EXPECT_EQ_INT((int)checksum_negotiate_default(), (int)CHECKSUM_ALGO_XXH128);
} }
/* xxh3 is 8 bytes and seed-aware; xxh128 is 16 bytes and differs from both /* xxh3 is 8 bytes and seed-aware; xxh128 is 16 bytes and differs from both
@@ -168,6 +236,9 @@ void test_checksum(void) {
test_checksum_xxh64_seed_changes_digest(); test_checksum_xxh64_seed_changes_digest();
test_checksum_xxh64_seed_deterministic(); test_checksum_xxh64_seed_deterministic();
test_checksum_md5_vectors(); test_checksum_md5_vectors();
test_checksum_md4_vectors();
test_checksum_sha1_vectors();
test_checksum_none_digest();
test_checksum_algo_lengths_distinct(); test_checksum_algo_lengths_distinct();
test_checksum_md5_seed_ignored(); test_checksum_md5_seed_ignored();
test_checksum_algo_name_mapping(); test_checksum_algo_name_mapping();
+93 -15
View File
@@ -317,7 +317,7 @@ static void test_parse_args_protocol_accept_current() {
Config* cfg = valid_client_config(); Config* cfg = valid_client_config();
EXPECT_NOT_NULL(cfg); EXPECT_NOT_NULL(cfg);
char* argv_equals[] = {"fastsync", "--source-dir", "/src", char* argv_equals[] = {"fastsync", "--source-dir", "/src",
"--dest-dir", "/dst", "--protocol=2.25.0"}; "--dest-dir", "/dst", "--protocol=2.26.0"};
int positional_args[2]; int positional_args[2];
int positional_count = 0; int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 6, argv_equals, positional_args, &positional_count), 0); EXPECT_EQ_INT(parse_args(cfg, 6, argv_equals, positional_args, &positional_count), 0);
@@ -327,7 +327,7 @@ static void test_parse_args_protocol_accept_current() {
cfg = valid_client_config(); cfg = valid_client_config();
EXPECT_NOT_NULL(cfg); EXPECT_NOT_NULL(cfg);
char* argv_space[] = {"fastsync", "--source-dir", "/src", "--dest-dir", char* argv_space[] = {"fastsync", "--source-dir", "/src", "--dest-dir",
"/dst", "--protocol", "2.25.0"}; "/dst", "--protocol", "2.26.0"};
positional_count = 0; positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 7, argv_space, positional_args, &positional_count), 0); EXPECT_EQ_INT(parse_args(cfg, 7, argv_space, positional_args, &positional_count), 0);
EXPECT_EQ_STR(cfg->version, PROTOCOL_VERSION); EXPECT_EQ_STR(cfg->version, PROTOCOL_VERSION);
@@ -1509,22 +1509,25 @@ static void test_parse_args_checksum_choice_equals_forms() {
config_delete(cfg); config_delete(cfg);
} }
/* An algorithm FastSync does not support must be rejected, never a silent /* An algorithm FastSync does not support, an empty half, a lone/extra comma or
no-op. */ a malformed separator must be rejected, never a silent no-op. A single
md4/sha1/none name and the two-name transfer,pre-transfer form are valid. */
static void test_parse_args_checksum_choice_rejects_unsupported() { static void test_parse_args_checksum_choice_rejects_unsupported() {
static const char* const bad[] = {"md4", "sha1", "sha256", "crc32", static const char* const bad[] = {"sha256", "crc32", "bogus", "xxhash:md5",
"none", "bogus", "xxh64,md5", "xxhash:md5"}; "md5,", ",md5", "md5,md4,sha1"};
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) { for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
Config* cfg = config_create(); Config* cfg = config_create();
char* argv[] = {"fastsync", "--checksum-choice", (char*)bad[i], "/src", "/dst"}; char* argv[] = {"fastsync", "--checksum-choice", (char*)bad[i], "/src", "/dst"};
int positional_args[2]; int positional_args[2];
int positional_count = 0; int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1); EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1);
EXPECT_EQ_INT(cfg->cli_exit_code, 4);
config_delete(cfg); config_delete(cfg);
} }
} }
/* xxh3/xxh128 are accepted; "auto" keeps the default algorithm. */ /* xxh3/xxh128/md4/sha1/none and the two-name form are accepted; "auto"
resolves to FastSync's negotiated default xxh128. */
static void test_parse_args_checksum_choice_new_algos() { static void test_parse_args_checksum_choice_new_algos() {
Config* cfg = config_create(); Config* cfg = config_create();
char* argv[] = {"fastsync", "--checksum-choice=xxh3", "/src", "/dst"}; char* argv[] = {"fastsync", "--checksum-choice=xxh3", "/src", "/dst"};
@@ -1545,7 +1548,69 @@ static void test_parse_args_checksum_choice_new_algos() {
char* argv3[] = {"fastsync", "--checksum-choice=auto", "/src", "/dst"}; char* argv3[] = {"fastsync", "--checksum-choice=auto", "/src", "/dst"};
positional_count = 0; positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv3, positional_args, &positional_count), 0); EXPECT_EQ_INT(parse_args(cfg, 4, argv3, positional_args, &positional_count), 0);
EXPECT_EQ_INT(cfg->checksum_algo, (int)CHECKSUM_ALGO_XXH64); EXPECT_EQ_INT(cfg->checksum_algo, (int)CHECKSUM_ALGO_XXH128);
config_delete(cfg);
/* A single md4/sha1 name selects it for both transfer and pre-transfer. */
static const int single[] = {(int)CHECKSUM_ALGO_MD4, (int)CHECKSUM_ALGO_SHA1};
static const char* const single_names[] = {"md4", "sha1"};
for (size_t i = 0; i < 2; i++) {
cfg = config_create();
char* arg = (char*)single_names[i];
char* argv4[] = {"fastsync", "--cc", arg, "/src", "/dst"};
positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 5, argv4, positional_args, &positional_count), 0);
EXPECT_EQ_INT(cfg->checksum_algo, single[i]);
EXPECT_EQ_INT(cfg->checksum_transfer_algo, single[i]);
config_delete(cfg);
}
/* Two-name form: first is the transfer checksum, second the pre-transfer one
that FastSync actually uses. */
cfg = config_create();
char* argv5[] = {"fastsync", "--cc=sha1,md4", "/checksum/src", "/dst"};
positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv5, positional_args, &positional_count), 0);
EXPECT_EQ_INT(cfg->checksum_transfer_algo, (int)CHECKSUM_ALGO_SHA1);
EXPECT_EQ_INT(cfg->checksum_algo, (int)CHECKSUM_ALGO_MD4);
config_delete(cfg);
/* "none" is accepted without --checksum but forces --whole-file like rsync. */
cfg = config_create();
char* argv6[] = {"fastsync", "--cc=none", "/src", "/dst"};
positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv6, positional_args, &positional_count), 0);
EXPECT_EQ_INT(cfg->checksum_algo, (int)CHECKSUM_ALGO_NONE);
EXPECT_TRUE(cfg->whole_file);
config_delete(cfg);
}
/* rsync rejects "none" as the pre-transfer checksum with --checksum (exit 4),
regardless of option order. */
static void test_parse_args_checksum_none_with_checksum_rejected() {
Config* cfg = config_create();
char* argv[] = {"fastsync", "--checksum", "--cc=none", "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1);
EXPECT_EQ_INT(cfg->cli_exit_code, 4);
config_delete(cfg);
cfg = config_create();
char* argv2[] = {"fastsync", "--checksum", "--cc=md5,none", "/checksum/src", "/dst"};
positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 5, argv2, positional_args, &positional_count), -1);
EXPECT_EQ_INT(cfg->cli_exit_code, 4);
config_delete(cfg);
/* "none" as the TRANSFER checksum with a real pre-transfer checksum is
accepted (rsync allows none,md5 with -c). */
cfg = config_create();
char* argv3[] = {"fastsync", "--checksum", "--cc=none,md5", "/checksum/src", "/dst"};
positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 5, argv3, positional_args, &positional_count), 0);
EXPECT_EQ_INT(cfg->checksum_algo, (int)CHECKSUM_ALGO_MD5);
EXPECT_TRUE(cfg->whole_file);
config_delete(cfg); config_delete(cfg);
} }
@@ -1604,28 +1669,40 @@ static void test_parse_args_timeout_zero_and_no_forms() {
config_delete(cfg); config_delete(cfg);
} }
/* rsync's --compress-choice choices FastSync does not implement are rejected by /* Every rsync 3.4.1 --compress-choice name is accepted and mapped to a real
* name; zstd/none/auto are accepted. */ * codec; "auto" resolves to the negotiated default (zstd). An unknown name is
* rejected with rsync's exit code 4. */
static void test_parse_args_compress_choice_parity() { static void test_parse_args_compress_choice_parity() {
static const char* const good[] = {"zstd", "none", "auto"}; struct {
const char* name;
CompressionAlgo algo;
bool enabled;
} good[] = {
{"zstd", COMPRESSION_ALGO_ZSTD, true}, {"lz4", COMPRESSION_ALGO_LZ4, true},
{"zlib", COMPRESSION_ALGO_ZLIB, true}, {"zlibx", COMPRESSION_ALGO_ZLIBX, true},
{"none", COMPRESSION_ALGO_NONE, false}, {"auto", COMPRESSION_ALGO_ZSTD, true},
{"ZSTD", COMPRESSION_ALGO_ZSTD, true},
};
for (size_t i = 0; i < sizeof(good) / sizeof(good[0]); i++) { for (size_t i = 0; i < sizeof(good) / sizeof(good[0]); i++) {
Config* cfg = config_create(); Config* cfg = config_create();
char* argv[] = {"fastsync", "--compress-choice", (char*)good[i], "/src", "/dst"}; char* argv[] = {"fastsync", "--compress-choice", (char*)good[i].name, "/src", "/dst"};
int positional_args[2]; int positional_args[2];
int positional_count = 0; int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0); EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
/* "auto" is normalized to the canonical "zstd" the receiver accepts. */ /* "auto" is normalized to the canonical "zstd" the receiver accepts. */
EXPECT_EQ_STR(cfg->compress_choice, strcmp(good[i], "auto") == 0 ? "zstd" : good[i]); EXPECT_EQ_STR(cfg->compress_choice, compression_algo_name(good[i].algo));
EXPECT_EQ_INT(cfg->use_compression, strcmp(good[i], "none") != 0 ? 1 : 0); EXPECT_EQ_INT(cfg->compression_algo, (int)good[i].algo);
EXPECT_EQ_INT(cfg->use_compression, good[i].enabled ? 1 : 0);
config_delete(cfg); config_delete(cfg);
} }
static const char* const bad[] = {"lz4", "zlib", "zlibx", "bogus"}; static const char* const bad[] = {"bogus", "", "zstd,lz4"};
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) { for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
Config* cfg = config_create(); Config* cfg = config_create();
char* argv[] = {"fastsync", "--compress-choice", (char*)bad[i], "/src", "/dst"}; char* argv[] = {"fastsync", "--compress-choice", (char*)bad[i], "/src", "/dst"};
int positional_args[2]; int positional_args[2];
int positional_count = 0; int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1); EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1);
EXPECT_EQ_INT(cfg->cli_exit_code, 4);
config_delete(cfg); config_delete(cfg);
} }
} }
@@ -4450,6 +4527,7 @@ void test_client_cli() {
test_parse_args_checksum_choice_equals_forms(); test_parse_args_checksum_choice_equals_forms();
test_parse_args_checksum_choice_rejects_unsupported(); test_parse_args_checksum_choice_rejects_unsupported();
test_parse_args_checksum_choice_new_algos(); test_parse_args_checksum_choice_new_algos();
test_parse_args_checksum_none_with_checksum_rejected();
test_parse_args_checksum_implies_incremental_only(); test_parse_args_checksum_implies_incremental_only();
test_parse_args_no_whole_file(); test_parse_args_no_whole_file();
test_parse_args_timeout_zero_and_no_forms(); test_parse_args_timeout_zero_and_no_forms();
+95 -6
View File
@@ -157,20 +157,22 @@ static void test_chunk_compress_decompress_roundtrip() {
/* Build a zstd frame whose header omits the content size (the content size /* Build a zstd frame whose header omits the content size (the content size
* flag is cleared), which ZSTD_getFrameContentSize reports as * flag is cleared), which ZSTD_getFrameContentSize reports as
* ZSTD_CONTENTSIZE_UNKNOWN. */ * ZSTD_CONTENTSIZE_UNKNOWN. The frame carries the codec-id prefix the
* decompressor dispatches on. */
static Data* make_unknown_size_frame(const void* src, size_t len) { static Data* make_unknown_size_frame(const void* src, size_t len) {
ZSTD_CCtx* cctx = ZSTD_createCCtx(); ZSTD_CCtx* cctx = ZSTD_createCCtx();
if (!cctx) if (!cctx)
return NULL; return NULL;
ZSTD_CCtx_setParameter(cctx, ZSTD_c_contentSizeFlag, 0); ZSTD_CCtx_setParameter(cctx, ZSTD_c_contentSizeFlag, 0);
size_t cap = ZSTD_compressBound(len); size_t cap = ZSTD_compressBound(len);
Data* out = data_create_empty(cap); Data* out = data_create_empty(cap + 1);
if (!out) { if (!out) {
ZSTD_freeCCtx(cctx); ZSTD_freeCCtx(cctx);
return NULL; return NULL;
} }
((uint8_t*)out->data)[0] = (uint8_t)COMPRESSION_ALGO_ZSTD;
ZSTD_inBuffer in = {src, len, 0}; ZSTD_inBuffer in = {src, len, 0};
ZSTD_outBuffer ob = {out->data, cap, 0}; ZSTD_outBuffer ob = {(uint8_t*)out->data + 1, cap, 0};
size_t ret; size_t ret;
do { do {
ret = ZSTD_compressStream2(cctx, &ob, &in, ZSTD_e_end); ret = ZSTD_compressStream2(cctx, &ob, &in, ZSTD_e_end);
@@ -180,7 +182,7 @@ static Data* make_unknown_size_frame(const void* src, size_t len) {
return NULL; return NULL;
} }
} while (ret > 0); } while (ret > 0);
out->size = ob.pos; out->size = ob.pos + 1;
ZSTD_freeCCtx(cctx); ZSTD_freeCCtx(cctx);
return out; return out;
} }
@@ -199,8 +201,9 @@ static void test_data_decompress_unknown_size_frame() {
Data* frame = make_unknown_size_frame(buf, len); Data* frame = make_unknown_size_frame(buf, len);
free(buf); free(buf);
EXPECT_NOT_NULL(frame); EXPECT_NOT_NULL(frame);
/* Guard the premise of the test: the frame really has no stored size. */ /* Guard the premise of the test: the frame (after the codec byte) really has
EXPECT_EQ_INT((int)ZSTD_getFrameContentSize(frame->data, frame->size), * no stored size. */
EXPECT_EQ_INT((int)ZSTD_getFrameContentSize((uint8_t*)frame->data + 1, frame->size - 1),
(int)ZSTD_CONTENTSIZE_UNKNOWN); (int)ZSTD_CONTENTSIZE_UNKNOWN);
Data* decompressed = data_decompress(frame); Data* decompressed = data_decompress(frame);
@@ -326,6 +329,89 @@ static void test_data_decompress_truncated_frame_fails() {
data_destroy(input); data_destroy(input);
} }
/* Every codec must round-trip byte-exactly through the self-describing frame,
* including the empty and a highly compressible large payload. */
static void codec_roundtrip(CompressionAlgo algo) {
const char* samples[] = {
"",
"Hello, World! This is test data for compression round-trip!",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
};
for (size_t s = 0; s < sizeof(samples) / sizeof(samples[0]); s++) {
size_t len = strlen(samples[s]);
Data* original = data_create_empty(len);
EXPECT_NOT_NULL(original);
if (len > 0)
memcpy(original->data, samples[s], len);
original->size = len;
Data* compressed = data_compress_codec(original, algo, 3, 0);
EXPECT_NOT_NULL(compressed);
EXPECT_EQ_INT((int)((uint8_t*)compressed->data)[0], (int)algo);
Data* decompressed = data_decompress(compressed);
EXPECT_NOT_NULL(decompressed);
EXPECT_EQ_INT((int)decompressed->size, (int)len);
EXPECT_EQ_INT(memcmp(decompressed->data, original->data, len), 0);
data_destroy(decompressed);
data_destroy(compressed);
data_destroy(original);
}
}
static void test_codec_roundtrips() {
codec_roundtrip(COMPRESSION_ALGO_NONE);
codec_roundtrip(COMPRESSION_ALGO_ZSTD);
codec_roundtrip(COMPRESSION_ALGO_LZ4);
codec_roundtrip(COMPRESSION_ALGO_ZLIB);
codec_roundtrip(COMPRESSION_ALGO_ZLIBX);
}
static void test_codec_name_mapping() {
EXPECT_EQ_INT(compression_algo_from_name("zstd"), (int)COMPRESSION_ALGO_ZSTD);
EXPECT_EQ_INT(compression_algo_from_name("ZSTD"), (int)COMPRESSION_ALGO_ZSTD);
EXPECT_EQ_INT(compression_algo_from_name("lz4"), (int)COMPRESSION_ALGO_LZ4);
EXPECT_EQ_INT(compression_algo_from_name("zlib"), (int)COMPRESSION_ALGO_ZLIB);
EXPECT_EQ_INT(compression_algo_from_name("zlibx"), (int)COMPRESSION_ALGO_ZLIBX);
EXPECT_EQ_INT(compression_algo_from_name("none"), (int)COMPRESSION_ALGO_NONE);
EXPECT_TRUE(compression_algo_from_name("bogus") < 0);
EXPECT_TRUE(compression_algo_from_name(NULL) < 0);
EXPECT_TRUE(compression_algo_valid((int)COMPRESSION_ALGO_LZ4));
EXPECT_TRUE(compression_algo_valid((int)COMPRESSION_ALGO_ZLIB));
EXPECT_TRUE(compression_algo_valid((int)COMPRESSION_ALGO_ZLIBX));
EXPECT_FALSE(compression_algo_valid(99));
EXPECT_EQ_STR(compression_algo_name(COMPRESSION_ALGO_ZSTD), "zstd");
EXPECT_EQ_STR(compression_algo_name(COMPRESSION_ALGO_LZ4), "lz4");
EXPECT_EQ_STR(compression_algo_name(COMPRESSION_ALGO_ZLIB), "zlib");
EXPECT_EQ_STR(compression_algo_name(COMPRESSION_ALGO_ZLIBX), "zlibx");
EXPECT_EQ_STR(compression_algo_name(COMPRESSION_ALGO_NONE), "none");
/* rsync 3.4.1 auto-negotiates zstd first. */
EXPECT_EQ_INT((int)compression_negotiate_default(), (int)COMPRESSION_ALGO_ZSTD);
EXPECT_FALSE(compression_algo_enabled(COMPRESSION_ALGO_NONE));
EXPECT_TRUE(compression_algo_enabled(COMPRESSION_ALGO_ZSTD));
}
/* The process-global codec selects what the legacy wrappers produce. */
static void test_codec_global_selection() {
Data* original = data_create_empty(64);
EXPECT_NOT_NULL(original);
memset(original->data, 'q', 64);
original->size = 64;
compression_set_algo(COMPRESSION_ALGO_LZ4);
Data* compressed = data_compress(original, 3);
EXPECT_NOT_NULL(compressed);
EXPECT_EQ_INT((int)((uint8_t*)compressed->data)[0], (int)COMPRESSION_ALGO_LZ4);
Data* decompressed = data_decompress(compressed);
EXPECT_NOT_NULL(decompressed);
EXPECT_TRUE(memcmp(decompressed->data, original->data, 64) == 0);
data_destroy(decompressed);
data_destroy(compressed);
/* Restore the default so later tests are unaffected. */
compression_set_algo(COMPRESSION_ALGO_ZSTD);
data_destroy(original);
}
void test_compression() { void test_compression() {
test_data_compress_decompress_roundtrip(); test_data_compress_decompress_roundtrip();
test_data_compress_decompress_large(); test_data_compress_decompress_large();
@@ -335,4 +421,7 @@ void test_compression() {
test_data_compress_with_threads_roundtrip(); test_data_compress_with_threads_roundtrip();
test_data_compress_reused_contexts_multithreaded(); test_data_compress_reused_contexts_multithreaded();
test_chunk_compress_decompress_roundtrip(); test_chunk_compress_decompress_roundtrip();
test_codec_roundtrips();
test_codec_name_mapping();
test_codec_global_selection();
} }
+65 -5
View File
@@ -1368,6 +1368,61 @@ static void test_config_receive_rejects_invalid_checksum_algo() {
EXPECT_FALSE(roundtrip_config_ok(c)); EXPECT_FALSE(roundtrip_config_ok(c));
config_delete(c); config_delete(c);
} }
/* The negotiated codec id and the human --compress-choice spelling must agree,
* and the id itself must be a known codec. */
static void test_config_receive_rejects_invalid_compression_algo() {
if (is_running_under_valgrind())
return;
Config* c = config_create();
EXPECT_NOT_NULL(c);
c->send_directory = str_dup("/src");
c->receive_root_directory = str_dup("/dst");
c->compression_algo = 99;
EXPECT_FALSE(roundtrip_config_ok(c));
config_delete(c);
}
static void test_config_receive_rejects_codec_mismatch() {
if (is_running_under_valgrind())
return;
Config* c = config_create();
EXPECT_NOT_NULL(c);
c->send_directory = str_dup("/src");
c->receive_root_directory = str_dup("/dst");
free(c->compress_choice);
c->compress_choice = str_dup("lz4");
c->use_compression = true;
c->compression_algo = (int)COMPRESSION_ALGO_ZSTD; /* does not match lz4 */
EXPECT_FALSE(roundtrip_config_ok(c));
config_delete(c);
}
static void test_config_receive_rejects_none_codec_with_compression() {
if (is_running_under_valgrind())
return;
Config* c = config_create();
EXPECT_NOT_NULL(c);
c->send_directory = str_dup("/src");
c->receive_root_directory = str_dup("/dst");
c->use_compression = true;
c->compression_algo = (int)COMPRESSION_ALGO_NONE;
EXPECT_FALSE(roundtrip_config_ok(c));
config_delete(c);
}
static void test_config_receive_rejects_checksum_none_with_checksum() {
if (is_running_under_valgrind())
return;
Config* c = config_create();
EXPECT_NOT_NULL(c);
c->send_directory = str_dup("/src");
c->receive_root_directory = str_dup("/dst");
c->checksum = true;
c->checksum_algo = (int)CHECKSUM_ALGO_NONE;
EXPECT_FALSE(roundtrip_config_ok(c));
config_delete(c);
}
/* The identity-mapping fields (--numeric-ids / --usermap / --groupmap / /* The identity-mapping fields (--numeric-ids / --usermap / --groupmap /
--chown) cross the config wire unchanged: the receiver needs them to apply --chown) cross the config wire unchanged: the receiver needs them to apply
ownership with the same policy the client requested. */ ownership with the same policy the client requested. */
@@ -2549,6 +2604,7 @@ static bool basis_equal(const Config* a, const Config* b) {
#define CONFIG_CMP_STR_MODULE(a, b, name) str_opt_equal((a)->name, (b)->name) #define CONFIG_CMP_STR_MODULE(a, b, name) str_opt_equal((a)->name, (b)->name)
#define CONFIG_CMP_STR_REDACTED_AUTH(a, b, name) str_opt_equal((a)->name, (b)->name) #define CONFIG_CMP_STR_REDACTED_AUTH(a, b, name) str_opt_equal((a)->name, (b)->name)
#define CONFIG_CMP_INT_CHECKSUM_ALGO(a, b, name) ((a)->name == (b)->name) #define CONFIG_CMP_INT_CHECKSUM_ALGO(a, b, name) ((a)->name == (b)->name)
#define CONFIG_CMP_INT_COMPRESSION_ALGO(a, b, name) ((a)->name == (b)->name)
#define CONFIG_CMP_SUPERMODE(a, b, name) ((a)->name == (b)->name) #define CONFIG_CMP_SUPERMODE(a, b, name) ((a)->name == (b)->name)
#define CONFIG_CMP_INT_IDENTITY(a, b, name) ((a)->name == (b)->name) #define CONFIG_CMP_INT_IDENTITY(a, b, name) ((a)->name == (b)->name)
#define CONFIG_CMP_INT_SKIPCOUNT(a, b, name) ((a)->name == (b)->name) #define CONFIG_CMP_INT_SKIPCOUNT(a, b, name) ((a)->name == (b)->name)
@@ -2775,14 +2831,14 @@ static void golden_config_populate(Config* c) {
c->copy_as_gid = 222; c->copy_as_gid = 222;
} }
/* The pinned golden frame (protocol 2.25.0). The values below are the only /* The pinned golden frame (protocol 2.26.0). The values below are the only
* thing that ties the generated table to the historical wire format; update * thing that ties the generated table to the historical wire format; update
* them ONLY with a PROTOCOL_VERSION bump and a documented reason. The 2.24.0 * them ONLY with a PROTOCOL_VERSION bump and a documented reason. The 2.24.0
* per-directory delete-plan wave changed only the version string in the config * delete-plan wave changed only the version string; 2.25.0 appended the
* frame; the 2.25.0 wire-stats wave appends one report_stats bool. The * report_stats bool and 2.26.0 appended the compression_algo int. The
* byte-exact values are recomputed for the merged layout. */ * byte-exact values are recomputed for the merged layout. */
#define GOLDEN_WIRE_LEN 701 #define GOLDEN_WIRE_LEN 705
#define GOLDEN_WIRE_HASH 16170466870400670271ULL #define GOLDEN_WIRE_HASH 4673424031554175633ULL
static unsigned long long fnv1a_64(const unsigned char* buf, size_t len) { static unsigned long long fnv1a_64(const unsigned char* buf, size_t len) {
unsigned long long h = 1469598103934665603ULL; unsigned long long h = 1469598103934665603ULL;
@@ -3159,6 +3215,10 @@ void test_config() {
test_config_basis_normalization(); test_config_basis_normalization();
test_config_checksum_options_wire_roundtrip(); test_config_checksum_options_wire_roundtrip();
test_config_receive_rejects_invalid_checksum_algo(); test_config_receive_rejects_invalid_checksum_algo();
test_config_receive_rejects_invalid_compression_algo();
test_config_receive_rejects_codec_mismatch();
test_config_receive_rejects_none_codec_with_compression();
test_config_receive_rejects_checksum_none_with_checksum();
test_config_identity_wire_roundtrip(); test_config_identity_wire_roundtrip();
test_config_receive_rejects_invalid_identity(); test_config_receive_rejects_invalid_identity();
test_config_metadata_times_wire_roundtrip(); test_config_metadata_times_wire_roundtrip();
+16 -16
View File
@@ -18,10 +18,10 @@
/* P8 config-frame tail: super_mode (4) + copy-as presence (4) + uid (4) + gid (4). */ /* P8 config-frame tail: super_mode (4) + copy-as presence (4) + uid (4) + gid (4). */
#define P8_TAIL_BYTES 16 #define P8_TAIL_BYTES 16
/* Protocol 2.25.0 appends two trailing bools (report_dest_info, report_stats) /* Bytes after the P8 tail: report_dest_info (4), report_stats (4, wire-stats
* AFTER the P8 tail, so the P8 fields sit this many bytes before the end of the * wave) and compression_algo (4, codec wave). The P8 fields sit this many
* frame. */ * bytes before the end of the frame. */
#define OUTPUT_TAIL_BYTES 8 #define POST_P8_TAIL_BYTES 12
/* Smoke test for chunk_deserialize fuzz target */ /* Smoke test for chunk_deserialize fuzz target */
static void test_fuzz_chunk_deserialize() { static void test_fuzz_chunk_deserialize() {
@@ -324,7 +324,7 @@ static void test_fuzz_config_receive_p8_tail() {
size_t len = 0; size_t len = 0;
bool captured = capture_config_frame(c, &frame, &len); bool captured = capture_config_frame(c, &frame, &len);
config_delete(c); config_delete(c);
if (!captured || len <= P8_TAIL_BYTES) { if (!captured || len <= P8_TAIL_BYTES + POST_P8_TAIL_BYTES) {
free(frame); free(frame);
EXPECT_TRUE(false); EXPECT_TRUE(false);
return; return;
@@ -338,31 +338,31 @@ static void test_fuzz_config_receive_p8_tail() {
/* super_mode outside the 0..2 tri-state is refused. */ /* super_mode outside the 0..2 tri-state is refused. */
memcpy(mut, frame, len); memcpy(mut, frame, len);
put_i32(mut, len - OUTPUT_TAIL_BYTES - P8_TAIL_BYTES, 99); put_i32(mut, len - POST_P8_TAIL_BYTES - P8_TAIL_BYTES, 99);
EXPECT_FALSE(receive_config_frame(mut, len)); EXPECT_FALSE(receive_config_frame(mut, len));
put_i32(mut, len - OUTPUT_TAIL_BYTES - P8_TAIL_BYTES, -1); put_i32(mut, len - POST_P8_TAIL_BYTES - P8_TAIL_BYTES, -1);
EXPECT_FALSE(receive_config_frame(mut, len)); EXPECT_FALSE(receive_config_frame(mut, len));
/* A negative (sentinel) and an extreme copy-as uid/gid are refused. */ /* A negative (sentinel) and an extreme copy-as uid/gid are refused. */
memcpy(mut, frame, len); memcpy(mut, frame, len);
put_i32(mut, len - OUTPUT_TAIL_BYTES - P8_TAIL_BYTES, SUPER_MODE_AUTO); put_i32(mut, len - POST_P8_TAIL_BYTES - P8_TAIL_BYTES, SUPER_MODE_AUTO);
put_i32(mut, len - OUTPUT_TAIL_BYTES - P8_TAIL_BYTES + 4, 1); put_i32(mut, len - POST_P8_TAIL_BYTES - P8_TAIL_BYTES + 4, 1);
put_i32(mut, len - OUTPUT_TAIL_BYTES - P8_TAIL_BYTES + 8, -1); put_i32(mut, len - POST_P8_TAIL_BYTES - P8_TAIL_BYTES + 8, -1);
put_i32(mut, len - OUTPUT_TAIL_BYTES - P8_TAIL_BYTES + 12, 0); put_i32(mut, len - POST_P8_TAIL_BYTES - P8_TAIL_BYTES + 12, 0);
EXPECT_FALSE(receive_config_frame(mut, len)); EXPECT_FALSE(receive_config_frame(mut, len));
put_i32(mut, len - OUTPUT_TAIL_BYTES - P8_TAIL_BYTES + 8, 0); put_i32(mut, len - POST_P8_TAIL_BYTES - P8_TAIL_BYTES + 8, 0);
put_i32(mut, len - OUTPUT_TAIL_BYTES - P8_TAIL_BYTES + 12, INT32_MIN); put_i32(mut, len - POST_P8_TAIL_BYTES - P8_TAIL_BYTES + 12, INT32_MIN);
EXPECT_FALSE(receive_config_frame(mut, len)); EXPECT_FALSE(receive_config_frame(mut, len));
/* A presence int that is not a wire bool is refused. */ /* A presence int that is not a wire bool is refused. */
memcpy(mut, frame, len); memcpy(mut, frame, len);
put_i32(mut, len - OUTPUT_TAIL_BYTES - P8_TAIL_BYTES, SUPER_MODE_AUTO); put_i32(mut, len - POST_P8_TAIL_BYTES - P8_TAIL_BYTES, SUPER_MODE_AUTO);
put_i32(mut, len - OUTPUT_TAIL_BYTES - P8_TAIL_BYTES + 4, 2); put_i32(mut, len - POST_P8_TAIL_BYTES - P8_TAIL_BYTES + 4, 2);
EXPECT_FALSE(receive_config_frame(mut, len)); EXPECT_FALSE(receive_config_frame(mut, len));
/* Truncating anywhere inside the P8 tail is refused. */ /* Truncating anywhere inside the P8 tail is refused. */
EXPECT_FALSE(receive_config_frame(frame, len - 2)); EXPECT_FALSE(receive_config_frame(frame, len - 2));
EXPECT_FALSE(receive_config_frame(frame, len - OUTPUT_TAIL_BYTES - P8_TAIL_BYTES)); EXPECT_FALSE(receive_config_frame(frame, len - POST_P8_TAIL_BYTES - P8_TAIL_BYTES));
free(mut); free(mut);
free(frame); free(frame);
+3 -3
View File
@@ -1072,10 +1072,10 @@ static void test_incremental_check_basis_fifo_does_not_hang() {
EXPECT_TRUE(send_n_data(p[1], &mtime, sizeof(mtime))); EXPECT_TRUE(send_n_data(p[1], &mtime, sizeof(mtime)));
EXPECT_TRUE(send_n_data(p[1], &mtime_nsec, sizeof(mtime_nsec))); EXPECT_TRUE(send_n_data(p[1], &mtime_nsec, sizeof(mtime_nsec)));
/* config_has_basis() makes the request carry the source digest. */ /* config_has_basis() makes the request carry the source digest. */
uint8_t wire_len = 8; uint8_t wire_len = checksum_digest_len((ChecksumAlgo)cfg->checksum_algo);
uint8_t digest[8] = {0}; uint8_t digest[CHECKSUM_MAX_DIGEST_LEN] = {0};
EXPECT_TRUE(send_n_data(p[1], &wire_len, sizeof(wire_len))); EXPECT_TRUE(send_n_data(p[1], &wire_len, sizeof(wire_len)));
EXPECT_TRUE(send_n_data(p[1], digest, sizeof(digest))); EXPECT_TRUE(send_n_data(p[1], digest, wire_len));
Status s; Status s;
EXPECT_TRUE(receive_status(p[1], &s)); EXPECT_TRUE(receive_status(p[1], &s));
EXPECT_EQ_INT(s, STATUS_NEXT); EXPECT_EQ_INT(s, STATUS_NEXT);