diff --git a/src/client/change_list.c b/src/client/change_list.c index dbe0ca1..6f272a7 100644 --- a/src/client/change_list.c +++ b/src/client/change_list.c @@ -1,5 +1,7 @@ #include "change_list.h" +#include "checksum.h" #include "utils.h" +#include #include #include #include @@ -200,6 +202,83 @@ char* change_render_itemize(const Config* config, const ChangeEvent* event) { /* ---- --out-format / --log-file-format ---- */ +/* rsync 3.4.1's `%C` uses the negotiated transfer checksum; with the default + * "auto" choice on both ends that is xxh128. FastSync's internal XXH64 default + * is not an rsync algorithm, so map it to xxh128 for parity. */ +static ChecksumAlgo out_format_checksum_algo(const Config* config) { + switch ((ChecksumAlgo)config->checksum_algo) { + case CHECKSUM_ALGO_MD5: + return CHECKSUM_ALGO_MD5; + case CHECKSUM_ALGO_XXH3: + return CHECKSUM_ALGO_XXH3; + case CHECKSUM_ALGO_XXH128: + return CHECKSUM_ALGO_XXH128; + case CHECKSUM_ALGO_XXH64: + default: + return CHECKSUM_ALGO_XXH128; + } +} + +/* Render a digest as rsync's sum_as_hex: for xxh128 the HIGH 64-bit half is + * printed before the low half; every other algorithm prints its bytes in order. */ +static void digest_to_hex(ChecksumAlgo algo, const uint8_t* digest, size_t len, char* out) { + if (algo == CHECKSUM_ALGO_XXH128 && len == 16) { + uint64_t low = 0; + uint64_t high = 0; + memcpy(&low, digest, sizeof(low)); + memcpy(&high, digest + 8, sizeof(high)); + snprintf(out, len * 2 + 1, "%016llx%016llx", (unsigned long long)high, + (unsigned long long)low); + return; + } + static const char hex[] = "0123456789abcdef"; + for (size_t i = 0; i < len; i++) { + out[i * 2] = hex[(digest[i] >> 4) & 0xf]; + out[i * 2 + 1] = hex[digest[i] & 0xf]; + } + out[len * 2] = '\0'; +} + +static bool format_uses_checksum(const char* format) { + if (format == NULL) + return false; + for (const char* p = format; *p != '\0';) { + if (*p != '%') { + p++; + continue; + } + char token = p[1]; + if (token == '\0') + break; + if (token == 'C') + return true; + p += 2; + } + return false; +} + +/* Fill event->checksum/checksum_known for a transferred regular file. A + * non-regular entry (or a hard-link sibling) leaves checksum_known false, which + * renders as spaces like rsync. */ +static void fill_event_checksum(const Config* config, const File* file, ChangeEvent* event) { + if (file == NULL || file->is_dir || file->is_symlink || file->is_special || + (file->link_group != 0 && !file->link_first)) + return; + if (!format_uses_checksum(config->out_format) && !format_uses_checksum(config->log_file_format)) + return; + if (file->path == NULL) + return; + ChecksumAlgo algo = out_format_checksum_algo(config); + uint8_t digest[CHECKSUM_MAX_DIGEST_LEN]; + size_t len = 0; + /* rsync's %C is the transfer checksum, which is always seeded with 0 (it is + * independent of --checksum-seed, as rsync 3.4.1 demonstrates). */ + if (!checksum_digest_file(algo, 0, file->path, digest, sizeof(digest), &len)) + return; + digest_to_hex(algo, digest, len, event->checksum); + event->checksum_known = true; +} + char* change_render_format(const char* format, const Config* config, const ChangeEvent* event) { if (format == NULL || event == NULL) return NULL; @@ -245,6 +324,22 @@ char* change_render_format(const char* format, const Config* config, const Chang int written = snprintf(digits, sizeof(digits), "%llu", event->bytes_sent); ok = written >= 0 && (size_t)written < sizeof(digits) && strbuf_append(&line, digits); } break; + case 'c': { + char digits[32]; + int written = snprintf(digits, sizeof(digits), "%llu", event->bytes_read); + ok = written >= 0 && (size_t)written < sizeof(digits) && strbuf_append(&line, digits); + } break; + case 'C': { + if (event->checksum_known) { + ok = strbuf_append(&line, event->checksum); + } else { + /* rsync pads a non-regular / untransferred entry with spaces. */ + ChecksumAlgo algo = out_format_checksum_algo(config); + int width = checksum_digest_len(algo) * 2; + for (int i = 0; i < width && ok; i++) + ok = strbuf_append_char(&line, ' '); + } + } break; case 'M': { char when[32]; if (format_rsync_datetime(event->mtime_sec, true, when, sizeof(when))) @@ -464,7 +559,8 @@ static void fill_event_from_file(const Config* config, const File* file, ChangeE } } -void change_emit_file_sent(const Config* config, const File* file) { +void change_emit_file_sent_bytes(const Config* config, const File* file, + unsigned long long bytes_sent, unsigned long long bytes_read) { if (file == NULL || !change_list_enabled(config)) return; ChangeEvent event; @@ -489,19 +585,27 @@ void change_emit_file_sent(const Config* config, const File* file) { event.hardlink_target = file->hardlink_target; event.bytes_sent = 0; } else { - /* Literal payload bytes delivered; compressed/delta wire bytes are not - * separately counted. */ - event.bytes_sent = event.size; + event.bytes_sent = bytes_sent; + event.bytes_read = bytes_read; } char* name = NULL; char* path = NULL; fill_event_from_file(config, file, &event, &name, &path); - if (name != NULL && path != NULL) + if (name != NULL && path != NULL) { + fill_event_checksum(config, file, &event); change_emit(config, &event); + } free(name); free(path); } +void change_emit_file_sent(const Config* config, const File* file) { + if (file == NULL) + return; + unsigned long long payload = file->data != NULL ? file->data->size : 0; + change_emit_file_sent_bytes(config, file, payload, 0); +} + void change_emit_dir_sent(const Config* config, const File* file) { if (file == NULL || !change_list_enabled(config)) return; diff --git a/src/client/change_list.h b/src/client/change_list.h index 874b9fe..8825666 100644 --- a/src/client/change_list.h +++ b/src/client/change_list.h @@ -2,6 +2,7 @@ #define CHANGE_LIST_H #include "config.h" +#include "checksum.h" #include "file_types.h" #include "format.h" #include @@ -37,7 +38,13 @@ typedef struct { const char* symlink_target; const char* hardlink_target; unsigned long long size; /* source file length in bytes */ - unsigned long long bytes_sent; /* literal data bytes actually transferred */ + unsigned long long bytes_sent; /* wire bytes actually transferred (rsync %b) */ + unsigned long long bytes_read; /* wire bytes read back for this file (rsync %c) */ + /* rsync %C: whole-file checksum hex for a transferred regular file. Only + * filled when the active format uses %C (checksum_known == false otherwise, + * which renders as spaces like rsync for non-regular entries). */ + bool checksum_known; + char checksum[CHECKSUM_MAX_DIGEST_LEN * 2 + 1]; time_t mtime_sec; long mtime_nsec; mode_t mode; @@ -61,7 +68,9 @@ 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 bytes actually sent %M mtime (YYYY/MM/DD-HH:MM:SS) + * %b wire bytes transferred %c wire bytes read back for the file + * %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.") * %p pid %B permission bits without the type char * %U uid %G gid @@ -80,7 +89,14 @@ char* change_render_list_line(const Config* config, const ChangeEvent* event); * CHANGE_UP_TO_DATE events produce no output. */ void change_emit(const Config* config, const ChangeEvent* event); -/* Build and emit a CHANGE_SENT event for a file the client just sent. */ +/* 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. */ +void change_emit_file_sent_bytes(const Config* config, const File* file, + unsigned long long bytes_sent, unsigned long long bytes_read); + +/* Build and emit a CHANGE_SENT event for a file the client just sent, deriving + * the wire byte counts from the source payload length. */ void change_emit_file_sent(const Config* config, const File* file); /* Build and emit a CHANGE_SENT event for an explicit directory entry (-d). */ diff --git a/src/client/client_send.c b/src/client/client_send.c index a146059..54b947b 100644 --- a/src/client/client_send.c +++ b/src/client/client_send.c @@ -1878,6 +1878,8 @@ static int send_chunk_with_removal(Client* client, Chunk* chunk, Config* config, (stream && !config->use_compression)) && source_is_regular_file(f); SourceFile* source = remove_sources ? source_file_create(f) : NULL; + unsigned long long bytes_before = protocol_bytes_written(); + unsigned long long read_before = protocol_bytes_read(); int rc = send_single_file(client, f, config, config->use_incremental, use_sendfile); if (rc == 1) { source_file_destroy(source); @@ -1887,7 +1889,8 @@ static int send_chunk_with_removal(Client* client, Chunk* chunk, Config* config, source_file_destroy(source); return -1; } - change_emit_file_sent(config, f); + change_emit_file_sent_bytes(config, f, protocol_bytes_written() - bytes_before, + protocol_bytes_read() - read_before); if (source && !array_list_add(remove_sources, source)) { source_file_destroy(source); return -1; diff --git a/src/shared/checksum.c b/src/shared/checksum.c index b95a8bc..f4706b8 100644 --- a/src/shared/checksum.c +++ b/src/shared/checksum.c @@ -1,10 +1,14 @@ #include "checksum.h" +#include #include #include #include +#include /* delta.c owns the single XXH_IMPLEMENTATION that provides the xxHash symbols - * for the whole binary; this TU only needs the declarations. */ + * for the whole binary; this TU only needs the declarations. The streaming + * state structs and XXH3_update are exposed only with XXH_STATIC_LINKING_ONLY. */ +#define XXH_STATIC_LINKING_ONLY #include bool checksum_digest(ChecksumAlgo algo, uint64_t seed, const void* data, size_t size, uint8_t* out, @@ -54,6 +58,99 @@ bool checksum_digest(ChecksumAlgo algo, uint64_t seed, const void* data, size_t return false; } +bool checksum_digest_file(ChecksumAlgo algo, uint64_t seed, const char* path, uint8_t* out, + size_t out_capacity, size_t* out_len) { + if (!path || !out || !out_len || out_capacity < CHECKSUM_MAX_DIGEST_LEN) + return false; + + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return false; + + uint8_t buffer[64 * 1024]; + bool ok = false; + + if (algo == CHECKSUM_ALGO_MD5) { + EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + if (!ctx) { + close(fd); + return false; + } + unsigned int digest_len = 0; + if (EVP_DigestInit_ex(ctx, EVP_md5(), NULL) == 1) { + ok = true; + ssize_t got; + while ((got = read(fd, buffer, sizeof(buffer))) > 0) { + if (EVP_DigestUpdate(ctx, buffer, (size_t)got) != 1) { + ok = false; + break; + } + } + if (got < 0) + ok = false; + if (ok && EVP_DigestFinal_ex(ctx, out, &digest_len) == 1 && digest_len <= out_capacity) + *out_len = digest_len; + else + ok = false; + } + EVP_MD_CTX_free(ctx); + close(fd); + return ok; + } + + XXH64_state_t xxh64; + XXH3_state_t* xxh3 = NULL; + if (algo == CHECKSUM_ALGO_XXH64) { + XXH64_reset(&xxh64, seed); + } else if (algo == CHECKSUM_ALGO_XXH3 || algo == CHECKSUM_ALGO_XXH128) { + xxh3 = XXH3_createState(); + if (!xxh3) { + close(fd); + return false; + } + if (algo == CHECKSUM_ALGO_XXH3) + XXH3_64bits_reset_withSeed(xxh3, seed); + else + XXH3_128bits_reset_withSeed(xxh3, seed); + } else { + close(fd); + return false; + } + + ok = true; + ssize_t got; + while ((got = read(fd, buffer, sizeof(buffer))) > 0) { + if (algo == CHECKSUM_ALGO_XXH64) + XXH64_update(&xxh64, buffer, (size_t)got); + else if (XXH3_64bits_update(xxh3, buffer, (size_t)got) == XXH_ERROR) { + ok = false; + break; + } + } + if (got < 0) + ok = false; + + if (ok) { + if (algo == CHECKSUM_ALGO_XXH64) { + uint64_t digest = XXH64_digest(&xxh64); + memcpy(out, &digest, sizeof(digest)); + *out_len = sizeof(digest); + } else if (algo == CHECKSUM_ALGO_XXH3) { + uint64_t digest = XXH3_64bits_digest(xxh3); + memcpy(out, &digest, sizeof(digest)); + *out_len = sizeof(digest); + } else { + XXH128_hash_t digest = XXH3_128bits_digest(xxh3); + memcpy(out, &digest, sizeof(digest)); + *out_len = sizeof(digest); + } + } + if (xxh3) + XXH3_freeState(xxh3); + close(fd); + return ok; +} + int checksum_algo_from_name(const char* name) { if (!name) return -1; diff --git a/src/shared/checksum.h b/src/shared/checksum.h index c323730..9550422 100644 --- a/src/shared/checksum.h +++ b/src/shared/checksum.h @@ -35,8 +35,13 @@ typedef enum { bool checksum_digest(ChecksumAlgo algo, uint64_t seed, const void* data, size_t size, uint8_t* out, size_t out_capacity, size_t* out_len); -/* Resolve a --checksum-choice string (case-insensitive) to an algorithm id. - * Accepts "xxh64"/"xxhash", "xxh3", "xxh128" and "md5". "auto", rsync's +/* Streaming whole-file digest: hash the contents of `path` without holding the + * whole file in memory. Same digest/capacity contract as checksum_digest. + * Returns false on open/read failure or an undersized buffer. */ +bool checksum_digest_file(ChecksumAlgo algo, uint64_t seed, const char* path, uint8_t* out, + size_t out_capacity, size_t* out_len); + +/* Resolve a --checksum-choice string (case-insensitive) to an algorithm id. * Accepts "xxh64"/"xxhash", "xxh3", "xxh128" and "md5". "auto", rsync's * default automatic choice, is resolved to the default by the caller (it is not * a distinct algorithm here). Returns -1 for any name FastSync does not * implement (md4/sha1/none included). */ diff --git a/src/shared/config.h b/src/shared/config.h index 6762acc..dc0c529 100644 --- a/src/shared/config.h +++ b/src/shared/config.h @@ -250,8 +250,17 @@ typedef enum SuperMode { SUPER_MODE_AUTO = 0, SUPER_MODE_ON = 1, SUPER_MODE_OFF * answer every per-file STATUS_CHECK with a STATUS_DEST_INFO snapshot of the * pre-transfer destination entry (see protocol.h). It is set by the client * only when -i/--itemize-changes or --out-format asks for per-file change - * output; the transfer decision itself is unchanged. */ -#define CONFIG_WIRE_OUTPUT_FIELDS(X) X(report_dest_info, bool, false, BOOL) + * output; the transfer decision itself is unchanged. + * + * Wire-stats wave (protocol 2.25.0). report_stats tells the receiver to send a + * STATUS_STATS frame immediately before its terminal success status carrying + * the receiver-only counters (matched data, deleted/created file counts) and, + * for -n/--dry-run --delete, the destination-relative paths it WOULD have + * deleted. It is set by the client only when --stats, --progress/-P, an + * --out-format token needs a wire counter (%b/%c), or a dry-run carries + * --delete; the transfer decision itself is unchanged. */ +#define CONFIG_WIRE_OUTPUT_FIELDS(X) \ + X(report_dest_info, bool, false, BOOL) X(report_stats, bool, false, BOOL) /* All serialized fields, in exact wire order. Concatenating the per-segment * lists here is what keeps the declaration order = the wire order. */ @@ -911,8 +920,17 @@ typedef struct Config { * snapshot of the old entry) before its ordinary verdict when the config frame * carries the new report_dest_info bool appended after the --copy-as block. * This is both a config-frame layout change (one trailing bool) and a frame - * sequence change (the new status). */ -#define PROTOCOL_VERSION "2.23.0" + * sequence change (the new status). + * + * (4) Wire-stats parity (protocol 2.25.0): --stats, --progress/-P and the + * --out-format %b/%c tokens need receiver-only and wire counters that the push + * sender cannot observe, and -n/--dry-run --delete must report the extras it + * would have removed without deleting anything. The config frame gains one + * trailing report_stats bool and the receiver emits a new STATUS_STATS frame + * (carrying matched data, created/deleted counts and the would-delete path + * list) immediately before its terminal success status. Both a config-frame + * layout change and a frame-sequence change, hence the bump. */ +#define PROTOCOL_VERSION "2.25.0" #define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024) /* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */ #define MAX_BASIS_DIRS 64 diff --git a/src/shared/file_send.c b/src/shared/file_send.c index dfdeb5c..f064f93 100644 --- a/src/shared/file_send.c +++ b/src/shared/file_send.c @@ -182,6 +182,7 @@ bool file_send_sendfile_with_skip(File* file, int file_descriptor, bool use_meta close(fd); return false; } + protocol_note_bytes_written((unsigned long long)sent); } close(fd); diff --git a/src/shared/protocol.c b/src/shared/protocol.c index f3402a4..70c0d8a 100644 --- a/src/shared/protocol.c +++ b/src/shared/protocol.c @@ -29,6 +29,13 @@ static unsigned long long io_bwlimit = 0; static mtx_t bw_mutex; static once_flag bw_mutex_once = ONCE_FLAG_INIT; +/* Process-wide wire byte counters, used by the client to render rsync's + * --stats/--progress totals and the --out-format %b/%c tokens. The zero-copy + * sendfile path bypasses protocol_send_n_data, so it reports its bytes through + * protocol_note_bytes_written. */ +static atomic_ullong io_bytes_written = 0; +static atomic_ullong io_bytes_read = 0; + static unsigned long long global_bwlimit(void); static bool protocol_reserve_memory(ProtocolSession* session, size_t charge) { @@ -242,6 +249,18 @@ SSL* io_get_ssl(void) { return io_ssl; } +unsigned long long protocol_bytes_written(void) { + return atomic_load(&io_bytes_written); +} + +unsigned long long protocol_bytes_read(void) { + return atomic_load(&io_bytes_read); +} + +void protocol_note_bytes_written(unsigned long long bytes) { + atomic_fetch_add(&io_bytes_written, bytes); +} + static ProtocolSession* legacy_session(int read_fd, int write_fd) { if (bound_session) return bound_session; @@ -343,6 +362,7 @@ bool protocol_send_n_data(ProtocolSession* session, const void* data, size_t dat wait_events = POLLOUT; } log_debug_message(LOG_DEBUG_IO, " Send n Data: %zu", total_bytes_send); + atomic_fetch_add(&io_bytes_written, (unsigned long long)total_bytes_send); return true; } @@ -430,6 +450,7 @@ static bool protocol_receive_n_data_until(ProtocolSession* session, void* data, wait_events = POLLIN; } log_debug_message(LOG_DEBUG_IO, " Received n Data: %zu", total_bytes_received); + atomic_fetch_add(&io_bytes_read, (unsigned long long)total_bytes_received); return true; } diff --git a/src/shared/protocol.h b/src/shared/protocol.h index 95d95d4..545228e 100644 --- a/src/shared/protocol.h +++ b/src/shared/protocol.h @@ -181,7 +181,14 @@ enum NET_STATUS { * (new vs modified, and which of size/time/perms/owner/group differ) without * changing the transfer decision itself. Appended after * STATUS_DELETE_LIMIT so no existing status is renumbered. */ - STATUS_DEST_INFO + STATUS_DEST_INFO, + /* End-of-transfer receiver counter report (protocol 2.25.0). When the wire + * config carries report_stats=true, the receiver sends this status once, + * immediately before its terminal success status, followed by a fixed stats + * record (see format_stats_send/receive in format.h) and, when the run is a + * --dry-run with --delete, the would-delete path list. Appended after + * STATUS_DEST_INFO so no existing status is renumbered. */ + STATUS_STATS }; void io_set_fds(int read_fd, int write_fd); @@ -189,6 +196,14 @@ void io_set_bwlimit(unsigned long long bytes_per_sec); void io_set_ssl(SSL* ssl); SSL* io_get_ssl(void); +/* Process-wide wire byte counters. protocol_send_n_data/protocol_receive_n_data + * update them; the zero-copy sendfile path reports through + * protocol_note_bytes_written. Used by the client to render rsync's + * --stats/--progress totals and the --out-format %b/%c tokens. */ +unsigned long long protocol_bytes_written(void); +unsigned long long protocol_bytes_read(void); +void protocol_note_bytes_written(unsigned long long bytes); + void protocol_session_init(ProtocolSession* session, int read_fd, int write_fd); /* Transitional bridge for helpers whose signatures still carry only an fd. */ void protocol_session_bind(ProtocolSession* session); diff --git a/tests/integration/test_fault_injection.py b/tests/integration/test_fault_injection.py index 7c7127f..1c2fd22 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.25.0" STATUS_MANIFEST = 5 STATUS_OK = 0 diff --git a/tests/integration/test_preflight.py b/tests/integration/test_preflight.py index d6b601a..01aeaaf 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.25.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.25.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_client_cli.c b/tests/test_client_cli.c index 66c6f3d..1743a75 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.25.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.25.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); diff --git a/tests/test_config.c b/tests/test_config.c index e8ed56b..4d46ff8 100644 --- a/tests/test_config.c +++ b/tests/test_config.c @@ -2763,14 +2763,13 @@ 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.25.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.25.0 + * wire-stats wave appends one report_stats bool to the config frame; the + * byte-exact values are recomputed for the merged layout. */ +#define GOLDEN_WIRE_LEN 701 +#define GOLDEN_WIRE_HASH 16170466870400670271ULL static unsigned long long fnv1a_64(const unsigned char* buf, size_t len) { unsigned long long h = 1469598103934665603ULL; diff --git a/tests/test_fuzz_smoke.c b/tests/test_fuzz_smoke.c index d1965ac..279ff76 100644 --- a/tests/test_fuzz_smoke.c +++ b/tests/test_fuzz_smoke.c @@ -18,9 +18,10 @@ /* 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 +/* Protocol 2.25.0 appends two trailing bools (report_dest_info, report_stats) + * AFTER the P8 tail, so the P8 fields sit this many bytes before the end of the + * frame. */ +#define OUTPUT_TAIL_BYTES 8 /* Smoke test for chunk_deserialize fuzz target */ static void test_fuzz_chunk_deserialize() {