test: coverage for --checksum-choice/--cc and --checksum-seed
Unit: digest selection with known vectors (xxh64 seed 0/empty, md5 RFC vectors, seed changes xxh64 but not md5, name mapping, bounded buffer rejection), CLI parse (all spellings, = forms, --cc alias, unsupported rejected, seed strict decimal), config wire round-trip and out-of-range algo rejection, and seeded delta signature/compute symmetry (matching seed rebuilds, mismatched seed yields no block matches). Integration: unchanged skip + same-size/mtime redetect for xxh64 and md5 with and without -m, near-zero-data skip run, deterministic seed, and a byte-exact seeded delta run.
This commit is contained in:
@@ -550,6 +550,147 @@ class TestIncremental:
|
||||
assert not mismatches, f"Mismatch: {mismatches}"
|
||||
|
||||
|
||||
class TestChecksumChoice:
|
||||
"""--checksum-choice/--cc and --checksum-seed: the whole-file digest used by
|
||||
the --incremental/--checksum handshake is selectable and seedable. The
|
||||
receiver hashes the on-disk old file with the SAME algorithm+seed, so an
|
||||
unchanged file is skipped and a changed file (even with identical size and
|
||||
mtime) is transferred -- and the transfer always lands byte-exact.
|
||||
FastSync accepts xxh64 (default, seed-aware) and md5; names it does not
|
||||
implement are rejected, never silently ignored."""
|
||||
|
||||
def test_unsupported_algorithm_is_rejected(self, shared_server):
|
||||
result, _ = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["--checksum", "--checksum-choice=sha256"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
assert result.returncode != 0, "sha256 must be rejected, not silently ignored"
|
||||
|
||||
@pytest.mark.parametrize("algo", ["xxh64", "md5"])
|
||||
@pytest.mark.parametrize("mt", [False, True])
|
||||
def test_unchanged_skipped_and_bytes_preserved(self, shared_server, algo, mt):
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-M"], port=shared_server.port)
|
||||
assert result.returncode == 0, f"seed sync failed: {result.stderr[:200]}"
|
||||
|
||||
flags = (["-M", "--incremental", "--checksum", f"--checksum-choice={algo}"] +
|
||||
(["-m"] if mt else []))
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"checksum {algo} run failed: {result.stderr[:200]}"
|
||||
|
||||
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}"
|
||||
|
||||
# A changed source file with the SAME size and mtime must still be
|
||||
# detected (and re-transferred byte-exactly) because the whole-file digest
|
||||
# differs -- the explicit reason --checksum exists. This exercises the
|
||||
# sender/receiver digest agreement for a non-default algorithm.
|
||||
@pytest.mark.parametrize("algo", ["xxh64", "md5"])
|
||||
@pytest.mark.parametrize("mt", [False, True])
|
||||
def test_changed_same_size_mtime_redetected(self, shared_server, algo, mt):
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-M"], port=shared_server.port)
|
||||
assert result.returncode == 0
|
||||
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
source_file = os.path.join(SOURCE_DIR, "small.txt") # "hello world\n" (12 bytes)
|
||||
received_file = os.path.join(received, "small.txt")
|
||||
source_stat = os.stat(source_file)
|
||||
with open(received_file, "wb") as f:
|
||||
f.write(b"DDDDDDDDDDDD") # same size, different content
|
||||
os.utime(received_file, (source_stat.st_atime, source_stat.st_mtime))
|
||||
|
||||
flags = (["-M", "--incremental", "--checksum", f"--checksum-choice={algo}"] +
|
||||
(["-m"] if mt else []))
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"checksum {algo} redetect failed: {result.stderr[:200]}"
|
||||
with open(received_file, "rb") as f:
|
||||
assert f.read() == b"hello world\n"
|
||||
|
||||
@pytest.mark.parametrize("algo", ["xxh64", "md5"])
|
||||
def test_unchanged_run_transfers_almost_no_data(self, shared_server, algo):
|
||||
# A fully-unchanged --checksum run skips every file: only the config + a
|
||||
# small handshake travels, not the payloads. Proxy byte counts are not
|
||||
# available for -m (multithreaded connections), so single-thread only.
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-M"], port=shared_server.port)
|
||||
assert result.returncode == 0
|
||||
|
||||
flags = ["-M", "--incremental", "--checksum", f"--checksum-choice={algo}"]
|
||||
proxy = CountingProxy(shared_server.port)
|
||||
cmd = (CLIENT_CMD + ["--source-dir", SOURCE_DIR, "--dest-dir", DEST_DIR,
|
||||
"--save-to-disk", "--server-port", str(proxy.port)] + flags)
|
||||
result = proxy.run(cmd)
|
||||
assert result.returncode == 0, f"checksum {algo} skip run failed: {result.stderr[:200]}"
|
||||
assert proxy.client_to_server < 100000, \
|
||||
f"unchanged --checksum run sent {proxy.client_to_server} bytes; expected a skip"
|
||||
|
||||
@pytest.mark.parametrize("mt", [False, True])
|
||||
def test_seed_is_deterministic_and_preserves_content(self, shared_server, mt):
|
||||
clean_dir(DEST_DIR)
|
||||
flags = ["-M", "--incremental", "--checksum",
|
||||
"--checksum-choice=xxh64", "--checksum-seed=987654"] + (["-m"] if mt else [])
|
||||
first, _ = run_client(SOURCE_DIR, DEST_DIR, flags=flags, port=shared_server.port)
|
||||
assert first.returncode == 0, f"seeded run failed: {first.stderr[:200]}"
|
||||
|
||||
# A second run with the SAME seed and unchanged content skips everything
|
||||
# deterministically (same digests both sides).
|
||||
second, _ = run_client(SOURCE_DIR, DEST_DIR, flags=flags, port=shared_server.port)
|
||||
assert second.returncode == 0, f"deterministic rerun failed: {second.stderr[:200]}"
|
||||
|
||||
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}"
|
||||
|
||||
# A changed file with the same size and mtime is still caught and fixed
|
||||
# (a non-zero seed does not weaken the comparison).
|
||||
source_file = os.path.join(SOURCE_DIR, "medium.txt")
|
||||
received_file = os.path.join(received, "medium.txt")
|
||||
source_stat = os.stat(source_file)
|
||||
with open(received_file, "wb") as f:
|
||||
f.write(b"z" * os.path.getsize(source_file))
|
||||
os.utime(received_file, (source_stat.st_atime, source_stat.st_mtime))
|
||||
third, _ = run_client(SOURCE_DIR, DEST_DIR, flags=flags, port=shared_server.port)
|
||||
assert third.returncode == 0, f"seeded redetect failed: {third.stderr[:200]}"
|
||||
with open(received_file, "rb") as f:
|
||||
assert f.read() == open(source_file, "rb").read()
|
||||
|
||||
# --checksum-seed also feeds the delta path's per-block strong checksum on
|
||||
# both ends (receiver signature and sender window hash use the same seed),
|
||||
# so a seeded delta transfer still lands byte-exact.
|
||||
@pytest.mark.parametrize("mt", [False, True])
|
||||
def test_seed_delta_block_hash_transfers_byte_exact(self, shared_server, mt):
|
||||
source = os.path.join(TEST_DATA_DIR, f"ccseed_{'m' if mt else 's'}_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, f"ccseed_{'m' if mt else 's'}_dst")
|
||||
clean_dir(source)
|
||||
clean_dir(dest)
|
||||
big = os.path.join(source, "big.bin")
|
||||
with open(big, "wb") as f:
|
||||
f.write(bytes(range(256)) * 200) # 51200 bytes > delta 16K floor
|
||||
result, _ = run_client(source, dest, flags=["-M"], port=shared_server.port)
|
||||
assert result.returncode == 0, f"seed delta seed failed: {result.stderr[:200]}"
|
||||
|
||||
# Edit a region so the receiver must match a changed block with the seed.
|
||||
with open(big, "r+b") as f:
|
||||
f.seek(1000)
|
||||
f.write(b"\x00" * 64)
|
||||
# Force an mtime mismatch: the incremental quick-check skips files whose
|
||||
# stored mtime second equals the source's, which can collide when the
|
||||
# edit and the prior sync share a second. Setting an old dest mtime
|
||||
# guarantees the delta path is exercised deterministically.
|
||||
os.utime(os.path.join(get_dest_received_dir(dest, source), "big.bin"), (0, 0))
|
||||
flags = (["-M", "--incremental", "--delta", "--checksum-seed=314159"] +
|
||||
(["-m"] if mt else []))
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"seed delta run failed: {result.stderr[:200]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
assert _read_file(os.path.join(received, "big.bin")) == _read_file(big), \
|
||||
"seeded delta transfer is not byte-exact"
|
||||
|
||||
|
||||
class TestUpdate:
|
||||
def test_update_skips_older_destination_and_allows_equal_or_newer_source(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "test_array_list.h"
|
||||
#include "test_chunk.h"
|
||||
#include "test_change_list.h"
|
||||
#include "test_checksum.h"
|
||||
#include "test_client_cli.h"
|
||||
#include "test_compression.h"
|
||||
#include "test_config.h"
|
||||
@@ -46,6 +47,7 @@ int main() {
|
||||
RUN_TEST(test_config);
|
||||
RUN_TEST(test_compression);
|
||||
RUN_TEST(test_scanner);
|
||||
RUN_TEST(test_checksum);
|
||||
RUN_TEST(test_delta);
|
||||
RUN_TEST(test_data);
|
||||
RUN_TEST(test_protocol);
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
#include "test_checksum.h"
|
||||
#include "checksum.h"
|
||||
#include "test_utils.h"
|
||||
#include <string.h>
|
||||
|
||||
/* Known xxHash64 vector (seed 0) for the empty string and a literal.
|
||||
* The md5 vectors are the standard NIST/RFC1321 test strings. These pin the
|
||||
* digest selection to genuinely distinct algorithm outputs so a --checksum-
|
||||
* choice change is observable, not a silent no-op. */
|
||||
|
||||
static void test_checksum_xxh64_seed0() {
|
||||
uint8_t out[CHECKSUM_MAX_DIGEST_LEN];
|
||||
size_t len = 0;
|
||||
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_XXH64, 0, "hello", 5, out, sizeof(out), &len));
|
||||
EXPECT_TRUE(len == (size_t)8);
|
||||
/* Hard-coded: XXH64("hello", 5, 0). */
|
||||
uint8_t expect[8] = {0xa3, 0x6d, 0x9f, 0x88, 0x7d, 0x82, 0xc7, 0x26};
|
||||
for (int i = 0; i < 8; i++)
|
||||
EXPECT_EQ_INT(out[i], expect[i]);
|
||||
}
|
||||
|
||||
static void test_checksum_xxh64_empty() {
|
||||
uint8_t out[CHECKSUM_MAX_DIGEST_LEN];
|
||||
size_t len = 0;
|
||||
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_XXH64, 0, "", 0, out, sizeof(out), &len));
|
||||
EXPECT_TRUE(len == (size_t)8);
|
||||
/* XXH64("", 0, 0). */
|
||||
uint8_t expect[8] = {0x99, 0xe9, 0xd8, 0x51, 0x37, 0xdb, 0x46, 0xef};
|
||||
for (int i = 0; i < 8; i++)
|
||||
EXPECT_EQ_INT(out[i], expect[i]);
|
||||
}
|
||||
|
||||
/* A nonzero seed must change the xxh64 digest: the algorithm is genuinely
|
||||
* seed-aware, deterministic, and distinct from seed 0. */
|
||||
static void test_checksum_xxh64_seed_changes_digest() {
|
||||
uint8_t a[CHECKSUM_MAX_DIGEST_LEN], b[CHECKSUM_MAX_DIGEST_LEN];
|
||||
size_t alen = 0, blen = 0;
|
||||
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_XXH64, 7, "payload", 7, a, sizeof(a), &alen));
|
||||
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_XXH64, 0, "payload", 7, b, sizeof(b), &blen));
|
||||
EXPECT_TRUE(alen == blen);
|
||||
EXPECT_TRUE(memcmp(a, b, alen) != 0);
|
||||
}
|
||||
|
||||
static void test_checksum_xxh64_seed_deterministic() {
|
||||
uint8_t a[CHECKSUM_MAX_DIGEST_LEN], b[CHECKSUM_MAX_DIGEST_LEN];
|
||||
size_t alen = 0, blen = 0;
|
||||
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_XXH64, 12345, "same", 4, a, sizeof(a), &alen));
|
||||
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_XXH64, 12345, "same", 4, b, sizeof(b), &blen));
|
||||
EXPECT_TRUE(alen == blen);
|
||||
EXPECT_TRUE(memcmp(a, b, alen) == 0);
|
||||
}
|
||||
|
||||
static void test_checksum_md5_vectors() {
|
||||
uint8_t out[CHECKSUM_MAX_DIGEST_LEN];
|
||||
size_t len = 0;
|
||||
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_MD5, 0, "", 0, out, sizeof(out), &len));
|
||||
EXPECT_TRUE(len == (size_t)16);
|
||||
uint8_t expect_empty[16] = {0xd4, 0x1d, 0x8c, 0xd9, 0x8f, 0x00, 0xb2, 0x04,
|
||||
0xe9, 0x80, 0x09, 0x98, 0xec, 0xf8, 0x42, 0x7e};
|
||||
EXPECT_TRUE(memcmp(out, expect_empty, 16) == 0);
|
||||
|
||||
/* MD5("abc") */
|
||||
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_MD5, 0, "abc", 3, out, sizeof(out), &len));
|
||||
uint8_t expect_abc[16] = {0x90, 0x01, 0x50, 0x98, 0x3c, 0xd2, 0x4f, 0xb0,
|
||||
0xd6, 0x96, 0x3f, 0x7d, 0x28, 0xe1, 0x7f, 0x72};
|
||||
EXPECT_TRUE(memcmp(out, expect_abc, 16) == 0);
|
||||
}
|
||||
|
||||
/* md5 is 16 bytes and differs from the 8-byte xxh64 for the same input, so the
|
||||
* choice is observably different both in length and in content. */
|
||||
static void test_checksum_algo_lengths_distinct() {
|
||||
EXPECT_EQ_INT((int)checksum_digest_len(CHECKSUM_ALGO_XXH64), 8);
|
||||
EXPECT_EQ_INT((int)checksum_digest_len(CHECKSUM_ALGO_MD5), 16);
|
||||
|
||||
uint8_t x[CHECKSUM_MAX_DIGEST_LEN], m[CHECKSUM_MAX_DIGEST_LEN];
|
||||
size_t xl = 0, ml = 0;
|
||||
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_XXH64, 0, "same content", 12, x, sizeof(x), &xl));
|
||||
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_MD5, 0, "same content", 12, m, sizeof(m), &ml));
|
||||
EXPECT_TRUE(xl == (size_t)8);
|
||||
EXPECT_TRUE(ml == (size_t)16);
|
||||
EXPECT_TRUE(memcmp(x, m, 8) != 0);
|
||||
}
|
||||
|
||||
/* md5 has no seed: two distinct seeds give the same md5 digest (documented);
|
||||
* the seed is only honored by xxh64 and the delta block hash (low 32 bits). */
|
||||
static void test_checksum_md5_seed_ignored() {
|
||||
uint8_t a[CHECKSUM_MAX_DIGEST_LEN], b[CHECKSUM_MAX_DIGEST_LEN];
|
||||
size_t alen = 0, blen = 0;
|
||||
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_MD5, 0, "data", 4, a, sizeof(a), &alen));
|
||||
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_MD5, 99, "data", 4, b, sizeof(b), &blen));
|
||||
EXPECT_TRUE(memcmp(a, b, alen) == 0);
|
||||
}
|
||||
|
||||
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("xxhash"), (int)CHECKSUM_ALGO_XXH64);
|
||||
EXPECT_EQ_INT(checksum_algo_from_name("XXHASH"), (int)CHECKSUM_ALGO_XXH64);
|
||||
EXPECT_EQ_INT(checksum_algo_from_name("md5"), (int)CHECKSUM_ALGO_MD5);
|
||||
EXPECT_EQ_INT(checksum_algo_from_name("MD5"), (int)CHECKSUM_ALGO_MD5);
|
||||
EXPECT_TRUE(checksum_algo_from_name("sha256") < 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(NULL) < 0);
|
||||
|
||||
EXPECT_TRUE(checksum_algo_valid((int)CHECKSUM_ALGO_XXH64));
|
||||
EXPECT_TRUE(checksum_algo_valid((int)CHECKSUM_ALGO_MD5));
|
||||
EXPECT_FALSE(checksum_algo_valid(99));
|
||||
EXPECT_EQ_STR(checksum_algo_name(CHECKSUM_ALGO_XXH64), "xxh64");
|
||||
EXPECT_EQ_STR(checksum_algo_name(CHECKSUM_ALGO_MD5), "md5");
|
||||
}
|
||||
|
||||
static void test_checksum_truncated_buffer_rejected() {
|
||||
uint8_t small[4];
|
||||
size_t len = 0;
|
||||
/* The digest cannot fit in a 4-byte buffer. */
|
||||
EXPECT_FALSE(checksum_digest(CHECKSUM_ALGO_XXH64, 0, "x", 1, small, sizeof(small), &len));
|
||||
EXPECT_FALSE(checksum_digest(CHECKSUM_ALGO_MD5, 0, "x", 1, small, sizeof(small), &len));
|
||||
EXPECT_FALSE(checksum_digest(CHECKSUM_ALGO_XXH64, 0, NULL, 5, small, sizeof(small), &len));
|
||||
EXPECT_FALSE(checksum_digest(CHECKSUM_ALGO_XXH64, 0, "x", 1, NULL, 0, &len));
|
||||
EXPECT_FALSE(checksum_digest(CHECKSUM_ALGO_XXH64, 0, "x", 1, small, sizeof(small), NULL));
|
||||
}
|
||||
|
||||
/* A NULL data pointer with size 0 is the empty input, not an error. */
|
||||
static void test_checksum_null_empty_digest() {
|
||||
uint8_t a[CHECKSUM_MAX_DIGEST_LEN], b[CHECKSUM_MAX_DIGEST_LEN];
|
||||
size_t alen = 0, blen = 0;
|
||||
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_XXH64, 0, NULL, 0, a, sizeof(a), &alen));
|
||||
EXPECT_TRUE(checksum_digest(CHECKSUM_ALGO_XXH64, 0, "", 0, b, sizeof(b), &blen));
|
||||
EXPECT_TRUE(alen == blen);
|
||||
EXPECT_TRUE(memcmp(a, b, alen) == 0);
|
||||
}
|
||||
|
||||
void test_checksum(void) {
|
||||
test_checksum_xxh64_seed0();
|
||||
test_checksum_xxh64_empty();
|
||||
test_checksum_xxh64_seed_changes_digest();
|
||||
test_checksum_xxh64_seed_deterministic();
|
||||
test_checksum_md5_vectors();
|
||||
test_checksum_algo_lengths_distinct();
|
||||
test_checksum_md5_seed_ignored();
|
||||
test_checksum_algo_name_mapping();
|
||||
test_checksum_truncated_buffer_rejected();
|
||||
test_checksum_null_empty_digest();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef TEST_CHECKSUM_H
|
||||
#define TEST_CHECKSUM_H
|
||||
|
||||
void test_checksum(void);
|
||||
|
||||
#endif
|
||||
+83
-3
@@ -1,4 +1,5 @@
|
||||
#include "test_client_cli.h"
|
||||
#include "checksum.h"
|
||||
#include "client_validation.h"
|
||||
#include "chmod.h"
|
||||
#include "config.h"
|
||||
@@ -787,8 +788,7 @@ static void test_parse_args_rejects_unimplemented_options() {
|
||||
"--ipv4",
|
||||
"--daemon",
|
||||
"--config",
|
||||
"--server",
|
||||
"--checksum-choice"};
|
||||
"--server"};
|
||||
|
||||
for (size_t i = 0; i < sizeof(options) / sizeof(options[0]); i++) {
|
||||
Config* cfg = config_create();
|
||||
@@ -953,7 +953,8 @@ static void test_parse_args_no_preserve_blocks_implicit_metadata() {
|
||||
}
|
||||
}
|
||||
|
||||
/* Checksum-choice spellings are recognized and rejected until algorithms are implemented. */
|
||||
/* --checksum-choice and its --cc alias select the whole-file digest algorithm
|
||||
(default xxh64; both "xxh64" and the rsync "xxhash" spelling accepted). */
|
||||
static void test_parse_args_checksum_choice_aliases() {
|
||||
static const char* const options[] = {"--checksum-choice", "--cc"};
|
||||
|
||||
@@ -962,12 +963,88 @@ static void test_parse_args_checksum_choice_aliases() {
|
||||
char* argv[] = {"fastsync", (char*)options[i], "xxh64", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_INT(cfg->checksum_algo, (int)CHECKSUM_ALGO_XXH64);
|
||||
config_delete(cfg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Both the "--checksum-choice=ALG" and "--cc=ALG" inline forms parse. */
|
||||
static void test_parse_args_checksum_choice_equals_forms() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "--checksum-choice=md5", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_INT(cfg->checksum_algo, (int)CHECKSUM_ALGO_MD5);
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = config_create();
|
||||
char* argv2[] = {"fastsync", "--cc=xxhash", "/src", "/dst"};
|
||||
positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv2, positional_args, &positional_count), 0);
|
||||
EXPECT_EQ_INT(cfg->checksum_algo, (int)CHECKSUM_ALGO_XXH64);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* An algorithm FastSync does not support must be rejected, never a silent
|
||||
no-op. */
|
||||
static void test_parse_args_checksum_choice_rejects_unsupported() {
|
||||
static const char* const bad[] = {"md4", "sha256", "crc32", "none", "bogus"};
|
||||
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "--checksum-choice", (char*)bad[i], "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
}
|
||||
}
|
||||
|
||||
/* --checksum-seed parses as a 64-bit non-negative integer (space and = forms);
|
||||
invalid values are rejected. */
|
||||
static void test_parse_args_checksum_seed() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "--checksum-seed=42", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_TRUE(cfg->checksum_seed == 42ULL);
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = config_create();
|
||||
char* argv2[] = {"fastsync", "--checksum-seed", "12345", "/src", "/dst"};
|
||||
positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv2, positional_args, &positional_count), 0);
|
||||
EXPECT_TRUE(cfg->checksum_seed == 12345ULL);
|
||||
config_delete(cfg);
|
||||
|
||||
/* 0 is a valid (and default) seed. */
|
||||
cfg = config_create();
|
||||
char* argv3[] = {"fastsync", "--checksum-seed=0", "/src", "/dst"};
|
||||
positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv3, positional_args, &positional_count), 0);
|
||||
EXPECT_TRUE(cfg->checksum_seed == 0ULL);
|
||||
config_delete(cfg);
|
||||
|
||||
/* Non-numeric and negative seeds are rejected. */
|
||||
static const char* const bad[] = {"abc", "-5", "1.5", ""};
|
||||
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
|
||||
cfg = config_create();
|
||||
char* argv4[] = {"fastsync", "--checksum-seed", (char*)bad[i], "/src", "/dst"};
|
||||
positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv4, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* Missing value is rejected. */
|
||||
cfg = config_create();
|
||||
char* argv5[] = {"fastsync", "--checksum-seed"};
|
||||
positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 2, argv5, positional_args, &positional_count), -1);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
static void test_parse_args_rejects_unsafe_negation() {
|
||||
static const char* const options[] = {"--no-archive", "--no-timeout", "--no-unknown"};
|
||||
for (size_t i = 0; i < sizeof(options) / sizeof(options[0]); i++) {
|
||||
@@ -1943,6 +2020,9 @@ void test_client_cli() {
|
||||
test_parse_args_log_file_format();
|
||||
test_parse_args_checksum_choice_aliases();
|
||||
test_parse_args_checksum_choice_requires_value();
|
||||
test_parse_args_checksum_choice_equals_forms();
|
||||
test_parse_args_checksum_choice_rejects_unsupported();
|
||||
test_parse_args_checksum_seed();
|
||||
test_parse_args_temp_dir();
|
||||
test_parse_args_delay_updates();
|
||||
test_validate_config_delay_updates_rejects_inplace();
|
||||
|
||||
@@ -754,6 +754,68 @@ static void test_config_is_remote_dest() {
|
||||
EXPECT_TRUE(config_is_remote_dest("user@host:"));
|
||||
}
|
||||
|
||||
/* --checksum-choice/--cc and --checksum-seed cross the wire intact so the
|
||||
receiver hashes the on-disk old file with the same algorithm and seed. */
|
||||
static void test_config_checksum_options_wire_roundtrip() {
|
||||
if (is_running_under_valgrind())
|
||||
return;
|
||||
|
||||
struct {
|
||||
int algo;
|
||||
unsigned long long seed;
|
||||
} cases[] = {
|
||||
{CHECKSUM_ALGO_XXH64, 0},
|
||||
{CHECKSUM_ALGO_XXH64, 42},
|
||||
{CHECKSUM_ALGO_MD5, 7},
|
||||
{CHECKSUM_ALGO_MD5, 0},
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
close(p[1]);
|
||||
io_set_fds(p[0], p[0]);
|
||||
Config* recv = config_receive(p[0]);
|
||||
bool ok = recv != NULL && recv->checksum_algo == cases[i].algo &&
|
||||
recv->checksum_seed == cases[i].seed;
|
||||
config_delete(recv);
|
||||
close(p[0]);
|
||||
_exit(ok ? 0 : 1);
|
||||
} else {
|
||||
close(p[0]);
|
||||
io_set_fds(p[1], p[1]);
|
||||
Config* send_cfg = config_create();
|
||||
EXPECT_NOT_NULL(send_cfg);
|
||||
send_cfg->send_directory = str_dup("/src");
|
||||
send_cfg->receive_root_directory = str_dup("/dst");
|
||||
send_cfg->checksum_algo = cases[i].algo;
|
||||
send_cfg->checksum_seed = cases[i].seed;
|
||||
bool sent = config_send(p[1], send_cfg);
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
close(p[1]);
|
||||
config_delete(send_cfg);
|
||||
EXPECT_TRUE(sent);
|
||||
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* An out-of-range algorithm id on the wire must be rejected on receive, never
|
||||
accepted as-is (prevents mixing unsupported digests on a path). */
|
||||
static void test_config_receive_rejects_invalid_checksum_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->checksum_algo = 99;
|
||||
EXPECT_FALSE(roundtrip_config_ok(c));
|
||||
config_delete(c);
|
||||
}
|
||||
|
||||
void test_config() {
|
||||
test_config_lifecycle();
|
||||
test_config_ssh_dest();
|
||||
@@ -774,6 +836,8 @@ void test_config() {
|
||||
test_config_basis_roundtrip();
|
||||
test_config_basis_wire_rejects_escaping();
|
||||
test_config_basis_normalization();
|
||||
test_config_checksum_options_wire_roundtrip();
|
||||
test_config_receive_rejects_invalid_checksum_algo();
|
||||
}
|
||||
test_config_delete_timing_early_helper();
|
||||
test_config_is_remote_dest();
|
||||
|
||||
@@ -634,12 +634,65 @@ static void test_delta_hash_index_large_mostly_matching() {
|
||||
free(new_data);
|
||||
}
|
||||
|
||||
/* --checksum-seed: the delta strong (block) hash is genuinely seed-aware. A
|
||||
* nonzero seed changes the per-block xxHash32, and a signature + delta computed
|
||||
* with the same seed still reconstruct the file exactly (symmetric), while a
|
||||
* mismatched seed produces a delta that does not match the signature blocks. */
|
||||
static void test_delta_xxhash32_seeded() {
|
||||
const char* data = "seedme";
|
||||
uint32_t a = delta_xxhash32(data, 6);
|
||||
uint32_t b = delta_xxhash32_seeded(data, 6, 42);
|
||||
uint32_t c = delta_xxhash32_seeded(data, 6, 42);
|
||||
EXPECT_TRUE(a != b);
|
||||
EXPECT_EQ_INT((int)b, (int)c);
|
||||
/* Unseeded == seeded with 0 (default reproduces today's behavior). */
|
||||
EXPECT_EQ_INT((int)delta_xxhash32(data, 6), (int)delta_xxhash32_seeded(data, 6, 0));
|
||||
}
|
||||
|
||||
static void test_delta_seeded_signature_compute_matches() {
|
||||
uint32_t block_size = 1024;
|
||||
/* Identical old/new data with a non-zero seed: the receiver builds a seeded
|
||||
signature and the sender computes a seeded delta over the same bytes, so
|
||||
every block matches and applying the delta rebuilds the file exactly. */
|
||||
char data[4096];
|
||||
for (int i = 0; i < 4096; i++)
|
||||
data[i] = (char)(i % 256);
|
||||
|
||||
DeltaSignature* sig = delta_signature_create_seeded(data, 4096, block_size, 99);
|
||||
EXPECT_NOT_NULL(sig);
|
||||
Delta* delta = delta_compute_seeded(data, 4096, sig, block_size, 99);
|
||||
EXPECT_NOT_NULL(delta);
|
||||
void* rebuilt = delta_apply(data, 4096, delta, block_size);
|
||||
EXPECT_NOT_NULL(rebuilt);
|
||||
EXPECT_TRUE(memcmp(rebuilt, data, 4096) == 0);
|
||||
free(rebuilt);
|
||||
delta_destroy(delta);
|
||||
delta_signature_destroy(sig);
|
||||
|
||||
/* A MISMATCHED seed means the sender's window xxHash32 never equals the
|
||||
receiver's signature-block xxHash32: no block can match, so the delta is
|
||||
not worthwhile / has no block matches. This proves the seed really gates
|
||||
the block comparison rather than being an inert parameter. */
|
||||
sig = delta_signature_create_seeded(data, 4096, block_size, 99);
|
||||
EXPECT_NOT_NULL(sig);
|
||||
delta = delta_compute_seeded(data, 4096, sig, block_size, 7);
|
||||
EXPECT_NOT_NULL(delta);
|
||||
bool any_match = false;
|
||||
for (uint32_t i = 0; i < delta->instruction_count; i++)
|
||||
if (delta->instructions[i].type == DELTA_INSTR_BLOCK_MATCH)
|
||||
any_match = true;
|
||||
EXPECT_FALSE(any_match);
|
||||
delta_destroy(delta);
|
||||
delta_signature_destroy(sig);
|
||||
}
|
||||
|
||||
void test_delta() {
|
||||
test_adler32_basic();
|
||||
test_adler32_different_data();
|
||||
test_xxhash32_basic();
|
||||
test_xxhash32_different_data();
|
||||
test_xxhash64_different_data();
|
||||
test_delta_xxhash32_seeded();
|
||||
test_signature_roundtrip();
|
||||
test_delta_identical_files();
|
||||
test_delta_small_edit();
|
||||
@@ -653,4 +706,5 @@ void test_delta() {
|
||||
test_delta_apply_rejects_output_overflow();
|
||||
test_delta_hash_index_matches_linear_reference();
|
||||
test_delta_hash_index_large_mostly_matching();
|
||||
test_delta_seeded_signature_compute_matches();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user