diff --git a/tests/integration/test_codecs.py b/tests/integration/test_codecs.py new file mode 100644 index 0000000..0e0dfbe --- /dev/null +++ b/tests/integration/test_codecs.py @@ -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) diff --git a/tests/integration/test_fault_injection.py b/tests/integration/test_fault_injection.py index 7c7127f..80d6a8d 100644 --- a/tests/integration/test_fault_injection.py +++ b/tests/integration/test_fault_injection.py @@ -36,7 +36,7 @@ from common import ( # noqa: E402 verify_transfer, ) -PROTOCOL_VERSION = b"2.23.0" +PROTOCOL_VERSION = b"2.26.0" STATUS_MANIFEST = 5 STATUS_OK = 0 diff --git a/tests/integration/test_features.py b/tests/integration/test_features.py index d9270e1..1358fb4 100644 --- a/tests/integration/test_features.py +++ b/tests/integration/test_features.py @@ -1491,20 +1491,101 @@ class TestChecksumChoice: assert fh.read() == b"same content\n" @pytest.mark.ci - def test_checksum_choice_md4_single_name_rejected(self, shared_server): - for bad in ("md4", "sha1", "none", "xxh64,md5"): + @pytest.mark.parametrize("algo", ["xxh128", "xxh3", "xxh64", "md5", "md4", "sha1"]) + 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, flags=[f"--checksum-choice={bad}"], 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 - def test_compress_choice_unsupported_rejected(self, shared_server): - for bad in ("lz4", "zlib", "zlibx"): + @pytest.mark.parametrize("algo", ["zstd", "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, flags=[f"--compress-choice={bad}"], 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 def test_compress_choice_auto_transfers(self, shared_server): diff --git a/tests/integration/test_preflight.py b/tests/integration/test_preflight.py index d6b601a..b78c808 100644 --- a/tests/integration/test_preflight.py +++ b/tests/integration/test_preflight.py @@ -94,14 +94,14 @@ def _seed_protocol_source(source): class TestProtocol: @pytest.mark.ci def test_protocol_current_version_accepted(self, shared_server): - """--protocol=2.23.0 (the current PROTOCOL_VERSION) is accepted and the + """--protocol=2.26.0 (the current PROTOCOL_VERSION) is accepted and the transfer completes normally.""" source = os.path.join(TEST_DATA_DIR, "proto_ok_src") dest = os.path.join(TEST_DATA_DIR, "proto_ok_dst") shutil.rmtree(dest, ignore_errors=True) os.makedirs(dest) _seed_protocol_source(source) - result, _ = run_client(source, dest, flags=["--protocol=2.23.0"], + result, _ = run_client(source, dest, flags=["--protocol=2.26.0"], port=shared_server.port) assert result.returncode == 0, \ f"--protocol current run failed: {(result.stderr or result.stdout)[:400]}" diff --git a/tests/test_checksum.c b/tests/test_checksum.c index 3c37e1b..bf781ae 100644 --- a/tests/test_checksum.c +++ b/tests/test_checksum.c @@ -91,6 +91,63 @@ static void test_checksum_md5_seed_ignored() { 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() { 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("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_TRUE(checksum_algo_from_name("md4") < 0); - EXPECT_TRUE(checksum_algo_from_name("sha1") < 0); + EXPECT_EQ_INT(checksum_algo_from_name("md4"), (int)CHECKSUM_ALGO_MD4); + EXPECT_EQ_INT(checksum_algo_from_name("MD4"), (int)CHECKSUM_ALGO_MD4); + 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("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); @@ -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_XXH3)); 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_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_XXH3), "xxh3"); 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 @@ -168,6 +236,9 @@ void test_checksum(void) { test_checksum_xxh64_seed_changes_digest(); test_checksum_xxh64_seed_deterministic(); test_checksum_md5_vectors(); + test_checksum_md4_vectors(); + test_checksum_sha1_vectors(); + test_checksum_none_digest(); test_checksum_algo_lengths_distinct(); test_checksum_md5_seed_ignored(); test_checksum_algo_name_mapping(); diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 66c6f3d..52ed569 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -317,7 +317,7 @@ static void test_parse_args_protocol_accept_current() { Config* cfg = valid_client_config(); EXPECT_NOT_NULL(cfg); char* argv_equals[] = {"fastsync", "--source-dir", "/src", - "--dest-dir", "/dst", "--protocol=2.23.0"}; + "--dest-dir", "/dst", "--protocol=2.26.0"}; int positional_args[2]; int 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(); EXPECT_NOT_NULL(cfg); char* argv_space[] = {"fastsync", "--source-dir", "/src", "--dest-dir", - "/dst", "--protocol", "2.23.0"}; + "/dst", "--protocol", "2.26.0"}; positional_count = 0; EXPECT_EQ_INT(parse_args(cfg, 7, argv_space, positional_args, &positional_count), 0); EXPECT_EQ_STR(cfg->version, PROTOCOL_VERSION); @@ -1499,22 +1499,25 @@ static void test_parse_args_checksum_choice_equals_forms() { config_delete(cfg); } -/* An algorithm FastSync does not support must be rejected, never a silent - no-op. */ +/* An algorithm FastSync does not support, an empty half, a lone/extra comma or + 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 const char* const bad[] = {"md4", "sha1", "sha256", "crc32", - "none", "bogus", "xxh64,md5", "xxhash:md5"}; + static const char* const bad[] = {"sha256", "crc32", "bogus", "xxhash:md5", + "md5,", ",md5", "md5,md4,sha1"}; 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); + EXPECT_EQ_INT(cfg->cli_exit_code, 4); 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() { Config* cfg = config_create(); char* argv[] = {"fastsync", "--checksum-choice=xxh3", "/src", "/dst"}; @@ -1535,7 +1538,69 @@ static void test_parse_args_checksum_choice_new_algos() { char* argv3[] = {"fastsync", "--checksum-choice=auto", "/src", "/dst"}; 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); } @@ -1594,28 +1659,40 @@ static void test_parse_args_timeout_zero_and_no_forms() { config_delete(cfg); } -/* rsync's --compress-choice choices FastSync does not implement are rejected by - * name; zstd/none/auto are accepted. */ +/* Every rsync 3.4.1 --compress-choice name is accepted and mapped to a real + * 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 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++) { 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_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. */ - EXPECT_EQ_STR(cfg->compress_choice, strcmp(good[i], "auto") == 0 ? "zstd" : good[i]); - EXPECT_EQ_INT(cfg->use_compression, strcmp(good[i], "none") != 0 ? 1 : 0); + EXPECT_EQ_STR(cfg->compress_choice, compression_algo_name(good[i].algo)); + EXPECT_EQ_INT(cfg->compression_algo, (int)good[i].algo); + EXPECT_EQ_INT(cfg->use_compression, good[i].enabled ? 1 : 0); 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++) { Config* cfg = config_create(); char* argv[] = {"fastsync", "--compress-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); + EXPECT_EQ_INT(cfg->cli_exit_code, 4); config_delete(cfg); } } @@ -4400,6 +4477,7 @@ void test_client_cli() { test_parse_args_checksum_choice_equals_forms(); test_parse_args_checksum_choice_rejects_unsupported(); 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_no_whole_file(); test_parse_args_timeout_zero_and_no_forms(); diff --git a/tests/test_compression.c b/tests/test_compression.c index ebd762e..833835e 100644 --- a/tests/test_compression.c +++ b/tests/test_compression.c @@ -157,20 +157,22 @@ static void test_chunk_compress_decompress_roundtrip() { /* Build a zstd frame whose header omits the content size (the content size * 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) { ZSTD_CCtx* cctx = ZSTD_createCCtx(); if (!cctx) return NULL; ZSTD_CCtx_setParameter(cctx, ZSTD_c_contentSizeFlag, 0); size_t cap = ZSTD_compressBound(len); - Data* out = data_create_empty(cap); + Data* out = data_create_empty(cap + 1); if (!out) { ZSTD_freeCCtx(cctx); return NULL; } + ((uint8_t*)out->data)[0] = (uint8_t)COMPRESSION_ALGO_ZSTD; 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; do { 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; } } while (ret > 0); - out->size = ob.pos; + out->size = ob.pos + 1; ZSTD_freeCCtx(cctx); return out; } @@ -199,8 +201,9 @@ static void test_data_decompress_unknown_size_frame() { Data* frame = make_unknown_size_frame(buf, len); free(buf); EXPECT_NOT_NULL(frame); - /* Guard the premise of the test: the frame really has no stored size. */ - EXPECT_EQ_INT((int)ZSTD_getFrameContentSize(frame->data, frame->size), + /* Guard the premise of the test: the frame (after the codec byte) really has + * no stored size. */ + EXPECT_EQ_INT((int)ZSTD_getFrameContentSize((uint8_t*)frame->data + 1, frame->size - 1), (int)ZSTD_CONTENTSIZE_UNKNOWN); Data* decompressed = data_decompress(frame); @@ -326,6 +329,89 @@ static void test_data_decompress_truncated_frame_fails() { 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() { test_data_compress_decompress_roundtrip(); test_data_compress_decompress_large(); @@ -335,4 +421,7 @@ void test_compression() { test_data_compress_with_threads_roundtrip(); test_data_compress_reused_contexts_multithreaded(); test_chunk_compress_decompress_roundtrip(); + test_codec_roundtrips(); + test_codec_name_mapping(); + test_codec_global_selection(); } diff --git a/tests/test_config.c b/tests/test_config.c index e8ed56b..93d01a9 100644 --- a/tests/test_config.c +++ b/tests/test_config.c @@ -1356,6 +1356,61 @@ static void test_config_receive_rejects_invalid_checksum_algo() { EXPECT_FALSE(roundtrip_config_ok(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 / --chown) cross the config wire unchanged: the receiver needs them to apply ownership with the same policy the client requested. */ @@ -2537,6 +2592,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_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_COMPRESSION_ALGO(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_SKIPCOUNT(a, b, name) ((a)->name == (b)->name) @@ -2763,14 +2819,14 @@ static void golden_config_populate(Config* c) { c->copy_as_gid = 222; } -/* The pinned golden frame (protocol 2.23.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 - * them ONLY with a PROTOCOL_VERSION bump and a documented reason. The 2.23.0 - * rsync-parity wave changes the config-frame layout (map-entry range + TO name, - * one report_dest_info bool, and other wire changes landing in this version); - * the byte-exact values are recomputed for the merged layout. */ -#define GOLDEN_WIRE_LEN 697 -#define GOLDEN_WIRE_HASH 7835017034643051109ULL + * them ONLY with a PROTOCOL_VERSION bump and a documented reason. The 2.26.0 + * codec-breadth wave appends one compression_algo int after the output block + * (checksum_algo's default is now the negotiated xxh128); the byte-exact values + * are recomputed for the merged layout. */ +#define GOLDEN_WIRE_LEN 701 +#define GOLDEN_WIRE_HASH 9130650678781158033ULL static unsigned long long fnv1a_64(const unsigned char* buf, size_t len) { unsigned long long h = 1469598103934665603ULL; @@ -3147,6 +3203,10 @@ void test_config() { test_config_basis_normalization(); test_config_checksum_options_wire_roundtrip(); 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_receive_rejects_invalid_identity(); test_config_metadata_times_wire_roundtrip(); diff --git a/tests/test_fuzz_smoke.c b/tests/test_fuzz_smoke.c index d1965ac..66670a2 100644 --- a/tests/test_fuzz_smoke.c +++ b/tests/test_fuzz_smoke.c @@ -18,9 +18,9 @@ /* P8 config-frame tail: super_mode (4) + copy-as presence (4) + uid (4) + gid (4). */ #define P8_TAIL_BYTES 16 -/* Protocol 2.23.0 appends one trailing bool (report_dest_info) AFTER the P8 - * tail, so the P8 fields sit this many bytes before the end of the frame. */ -#define OUTPUT_TAIL_BYTES 4 +/* Bytes after the P8 tail: report_dest_info (4) and, since protocol 2.26.0, + * compression_algo (4). The P8 fields sit this many bytes before the end. */ +#define POST_P8_TAIL_BYTES 8 /* Smoke test for chunk_deserialize fuzz target */ static void test_fuzz_chunk_deserialize() { @@ -323,7 +323,7 @@ static void test_fuzz_config_receive_p8_tail() { size_t len = 0; bool captured = capture_config_frame(c, &frame, &len); config_delete(c); - if (!captured || len <= P8_TAIL_BYTES) { + if (!captured || len <= P8_TAIL_BYTES + POST_P8_TAIL_BYTES) { free(frame); EXPECT_TRUE(false); return; @@ -337,31 +337,31 @@ static void test_fuzz_config_receive_p8_tail() { /* super_mode outside the 0..2 tri-state is refused. */ 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)); - 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)); /* A negative (sentinel) and an extreme copy-as uid/gid are refused. */ memcpy(mut, frame, len); - put_i32(mut, len - OUTPUT_TAIL_BYTES - P8_TAIL_BYTES, SUPER_MODE_AUTO); - put_i32(mut, len - OUTPUT_TAIL_BYTES - P8_TAIL_BYTES + 4, 1); - put_i32(mut, len - OUTPUT_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, SUPER_MODE_AUTO); + put_i32(mut, len - POST_P8_TAIL_BYTES - P8_TAIL_BYTES + 4, 1); + put_i32(mut, len - POST_P8_TAIL_BYTES - P8_TAIL_BYTES + 8, -1); + put_i32(mut, len - POST_P8_TAIL_BYTES - P8_TAIL_BYTES + 12, 0); EXPECT_FALSE(receive_config_frame(mut, len)); - put_i32(mut, len - OUTPUT_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 + 8, 0); + put_i32(mut, len - POST_P8_TAIL_BYTES - P8_TAIL_BYTES + 12, INT32_MIN); EXPECT_FALSE(receive_config_frame(mut, len)); /* A presence int that is not a wire bool is refused. */ memcpy(mut, frame, len); - put_i32(mut, len - OUTPUT_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, SUPER_MODE_AUTO); + put_i32(mut, len - POST_P8_TAIL_BYTES - P8_TAIL_BYTES + 4, 2); EXPECT_FALSE(receive_config_frame(mut, len)); /* Truncating anywhere inside the P8 tail is refused. */ 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(frame); diff --git a/tests/test_server.c b/tests/test_server.c index aa680a4..4a8d44c 100644 --- a/tests/test_server.c +++ b/tests/test_server.c @@ -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_nsec, sizeof(mtime_nsec))); /* config_has_basis() makes the request carry the source digest. */ - uint8_t wire_len = 8; - uint8_t digest[8] = {0}; + uint8_t wire_len = checksum_digest_len((ChecksumAlgo)cfg->checksum_algo); + 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], digest, sizeof(digest))); + EXPECT_TRUE(send_n_data(p[1], digest, wire_len)); Status s; EXPECT_TRUE(receive_status(p[1], &s)); EXPECT_EQ_INT(s, STATUS_NEXT);