test(codec): differential coverage vs rsync and wire-golden updates
Add tests/integration/test_codecs.py (accept/reject matrix and byte differential against rsync 3.4.1 for every algorithm), extend the checksum/compression unit tests with MD4/SHA1/none vectors and per-codec round-trips, pin the new config golden (2.26.0), and update the version strings and codec acceptance expectations.
This commit is contained in:
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]}"
|
||||
|
||||
+75
-4
@@ -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();
|
||||
|
||||
+93
-15
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
+67
-7
@@ -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();
|
||||
|
||||
+15
-15
@@ -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);
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user