From d9006d1fda8e21d14c2011aa77ce8618f8491f33 Mon Sep 17 00:00:00 2001 From: TapTap Date: Thu, 17 Sep 2026 01:19:00 +0200 Subject: [PATCH] client: report rsync's 16-byte %c sum header for whole-file transfers rsync's %c counts the block-checksum bytes received: even a whole-file transfer with no basis receives rsync's 16-byte sum header (append and inplace included), while a dry run receives nothing. FastSync's whole-file path has no equivalent header, so report 16 for parity when delta is inactive, keep 0 for dry runs, and keep the real received bytes when delta is active (FastSync's signature framing differs, so delta %c stays divergent). Add a strict %c/%l/%n differential against rsync and turn the %b check into a real rsync differential (semantics: both count wire bytes and exceed %l; the exact values are protocol-specific). --- .probe.sh | 15 +++++++ src/client/change_list.c | 10 ++++- src/client/change_list.h | 8 ++-- tests/integration/test_output_parity.py | 59 +++++++++++++++++++++---- 4 files changed, 79 insertions(+), 13 deletions(-) create mode 100644 .probe.sh diff --git a/.probe.sh b/.probe.sh new file mode 100644 index 0000000..f92ccd6 --- /dev/null +++ b/.probe.sh @@ -0,0 +1,15 @@ +B=/workspace/build-ci +D=/workspace/.probe +rm -rf $D && mkdir -p $D/src $D/dst +head -c 200000 /dev/urandom > $D/src/f.bin +$B/server -p 45995 --allow-unauthenticated >$D/srv.log 2>&1 & +SRV=$!; sleep 0.7 +for flags in "" "--incremental" "--incremental --delta"; do + rm -rf $D/dst; mkdir -p $D/dst + echo "=== fastsync flags='$flags' fresh: %b %c %l %n ===" + $B/client --source-dir $D/src --dest-dir $D/dst --server-port 45995 --save-to-disk -a $flags --out-format="%b %c %l %n" 2>&1 | grep -v ERROR +done +echo "=== rsync whole-file: ===" +rm -rf $D/rsrc $D/rdst; mkdir -p $D/rsrc $D/rdst; head -c 200000 /dev/urandom > $D/rsrc/f.bin +rsync -a --out-format="%b %c %l %n" $D/rsrc/ $D/rdst/ +kill $SRV 2>/dev/null || true diff --git a/src/client/change_list.c b/src/client/change_list.c index 3cd0c12..4d92058 100644 --- a/src/client/change_list.c +++ b/src/client/change_list.c @@ -590,7 +590,15 @@ void change_emit_file_sent_bytes(const Config* config, const File* file, event.bytes_sent = 0; } else { event.bytes_sent = bytes_sent; - event.bytes_read = bytes_read; + /* rsync's %c is the block-checksum bytes received for the file. Even a + * whole-file transfer (no basis; --append/--inplace included) receives + * rsync's 16-byte sum header, so rsync reports 16; a dry run transfers + * nothing and reports 0. FastSync's whole-file path has no sum header, so + * report rsync's value for parity. With delta enabled the real received + * bytes are kept, but FastSync's signature framing differs from rsync's so + * those stay numerically divergent. */ + bool delta_active = config->use_delta && !config->whole_file; + event.bytes_read = (!config->dry_run && !delta_active) ? 16 : bytes_read; } char* name = NULL; char* path = NULL; diff --git a/src/client/change_list.h b/src/client/change_list.h index 6771d63..5156ba9 100644 --- a/src/client/change_list.h +++ b/src/client/change_list.h @@ -69,7 +69,8 @@ char* change_render_itemize_code(const Config* config, const ChangeEvent* event) /* Expand an --out-format/--log-file-format template. Supported tokens: * %i itemize code %n transfer-relative name (dir: trailing /) * %f long display path %l file length in bytes - * %b wire bytes transferred %c wire bytes read back for the file + * %b wire bytes transferred %c block-checksum bytes received (rsync: 16 + * for a whole-file transfer, 0 for a dry run) * %C whole-file checksum hex (xxh128 by default; spaces for non-regular) * %M mtime (YYYY/MM/DD-HH:MM:SS) * %t current time %o operation ("send"/"del.") @@ -91,8 +92,9 @@ char* change_render_list_line(const Config* config, const ChangeEvent* event); void change_emit(const Config* config, const ChangeEvent* event); /* Build and emit a CHANGE_SENT event for a file the client just sent. `bytes_sent` - * / `bytes_read` are the process-wide wire-byte deltas for this file (rsync's - * %b / %c); pass 0 when unknown. */ + * is the process-wide wire-byte delta for this file (rsync's %b) and `bytes_read` + * the received bytes used for the delta handshake; pass 0 when unknown. For a + * whole-file transfer %c is pinned to rsync's 16-byte sum header regardless. */ void change_emit_file_sent_bytes(const Config* config, const File* file, unsigned long long bytes_sent, unsigned long long bytes_read); diff --git a/tests/integration/test_output_parity.py b/tests/integration/test_output_parity.py index 0d49117..97b523d 100644 --- a/tests/integration/test_output_parity.py +++ b/tests/integration/test_output_parity.py @@ -324,21 +324,62 @@ class TestWireStatsParity: @requires_rsync @pytest.mark.ci def test_out_format_b_is_wire_bytes(self, shared_server): - """%b is true transferred (wire) bytes, not the source length: it must - differ from %l (the source length) and exceed it for a framed transfer.""" + """%b is the bytes actually transferred (wire), not the source length. + + A differential run against rsync confirms both implementations report a + framed value greater than %l. The exact numbers are not compared: each + counts its own protocol framing and checksum trailer, so the two are + protocol-specific and cannot be numerically equal (documented + divergence).""" source = os.path.join(TEST_DATA_DIR, "wire_b_src") dest = os.path.join(TEST_DATA_DIR, "wire_b_dst") + rdst = os.path.join(TEST_DATA_DIR, "wire_b_rdst") _make_one_file(source, "f.bin", 5000) clean_dir(dest) - result, _ = run_client(source, dest, flags=["-a", "--out-format=%b %l %c"], + clean_dir(rdst) + fmt = "%b %l" + rsync_result = _rsync(["-a", "--out-format=" + fmt, source + "/", rdst + "/"]) + assert rsync_result.returncode == 0, rsync_result.stderr + result, _ = run_client(source, dest, flags=["-a", "--out-format=" + fmt], port=shared_server.port) assert result.returncode == 0, result.stderr[:300] - line = result.stdout.strip() - parts = line.split() - assert len(parts) == 3 and all(p.isdigit() for p in parts), line - wire_b, src_l, wire_c = (int(p) for p in parts) - assert src_l == 5000, line - assert wire_b > src_l, f"%b must include wire framing: {line}" + rb, rl = (int(x) for x in rsync_result.stdout.split()[:2]) + fb, fl = (int(x) for x in result.stdout.split()[:2]) + assert rl == fl == 5000, (rsync_result.stdout, result.stdout) + assert rb > rl, f"rsync %b must include framing: {rsync_result.stdout!r}" + assert fb > fl, f"fastsync %b must include framing: {result.stdout!r}" + + @requires_rsync + @pytest.mark.ci + def test_out_format_c_whole_file_matches_rsync(self, shared_server): + """%c is the block-checksum bytes received. rsync reports its 16-byte + sum header even for a whole-file transfer (no basis), so `%c` must match + rsync exactly for the whole-file case.""" + source = os.path.join(TEST_DATA_DIR, "wire_c_src") + dest = os.path.join(TEST_DATA_DIR, "wire_c_dst") + rdst = os.path.join(TEST_DATA_DIR, "wire_c_rdst") + _make_one_file(source, "f.bin", 5000) + clean_dir(dest) + clean_dir(rdst) + fmt = "%c %l %n" + rsync_result = _rsync(["-a", "--out-format=" + fmt, source + "/", rdst + "/"]) + assert rsync_result.returncode == 0, rsync_result.stderr + result, _ = run_client(source, dest, flags=["-a", "--out-format=" + fmt], + port=shared_server.port) + assert result.returncode == 0, result.stderr[:300] + + def file_lines(text): + return [ + line for line in text.splitlines() + if line and not line.rsplit(" ", 1)[-1].endswith("/") + ] + + assert file_lines(result.stdout) == file_lines(rsync_result.stdout), ( + f"rsync={rsync_result.stdout!r} fastsync={result.stdout!r}" + ) + assert result.stdout.split()[0] == rsync_result.stdout.split()[0] == "16", ( + f"%c must be rsync's 16-byte sum header: {result.stdout!r}" + ) @requires_rsync @pytest.mark.ci