Commit Graph
621 Commits
Author SHA1 Message Date
TapTap 79d965ac11 ci: parallelize integration suite with pytest-xdist; fast PR gate + full coverage on merge
CI / lint (pull_request) Failing after 0s
CI / build-and-test (pull_request) Skipped
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
- Add pytest-xdist to the CI Dockerfile (image -> v10).
- Worker-isolate TEST_DATA_DIR (PYTEST_XDIST_WORKER) so concurrent xdist
  workers never collide on shared-filesystem fixtures.
- Register a 'ci' marker and tag a fast representative subset of integration
  tests (basic TCP, incremental, compression, delete, basis, append).
- ci.yaml: lint + build + unit + the marked subset (-n4) on every PR; the
  full integration suite plus sanitizer/fuzz/coverage/valgrind run only on
  push to dev/main.
- Integration step runtime drops from ~860s (serial) to ~230s (-n4); the PR
  gate lands well under ~3 minutes.
2026-09-08 16:53:54 +02:00
TapTap 829e760086 docs: recount RSYNC_COMPAT summary after Phase-3 wave C
CI / lint (push) Successful in 47s
CI / sanitizers (undefined) (push) Successful in 52s
CI / sanitizers (address) (push) Successful in 53s
CI / fuzz-build (push) Successful in 20s
CI / coverage (push) Successful in 43s
CI / valgrind (push) Successful in 37s
CI / build-and-test (push) Successful in 15m4s
CI / build-and-test (pull_request) Successful in 15m0s
CI / lint (pull_request) Successful in 47s
CI / sanitizers (address) (pull_request) Successful in 47s
CI / sanitizers (undefined) (pull_request) Successful in 45s
CI / fuzz-build (pull_request) Successful in 17s
CI / coverage (pull_request) Successful in 42s
CI / valgrind (pull_request) Successful in 36s
2026-09-07 20:09:36 +02:00
TapTap 4c98744014 Merge feat/p3-checksum-choice: --checksum-choice/--cc and --checksum-seed 2026-09-07 19:53:28 +02:00
TapTap f99e6e1edf Merge feat/p3-append: --append / --append-verify tail resume 2026-09-07 19:52:33 +02:00
TapTap 681d7a8532 Merge feat/p3-missing-args: --ignore-missing-args / --delete-missing-args 2026-09-07 19:51:02 +02:00
TapTap c6758531f6 fix: reject xxh3 alias, pin handshake digest length to algorithm, simplify xxh64 copy, note FIPS md5
CI / lint (pull_request) Successful in 35s
CI / sanitizers (undefined) (pull_request) Successful in 52s
CI / sanitizers (address) (pull_request) Successful in 52s
CI / fuzz-build (pull_request) Successful in 18s
CI / coverage (pull_request) Successful in 42s
CI / valgrind (pull_request) Successful in 37s
CI / build-and-test (pull_request) Successful in 12m58s
2026-09-07 19:50:52 +02:00
TapTap e754f0d4eb test: cover absent-parent no-op and --dirs scanner skip
CI / lint (pull_request) Successful in 38s
CI / sanitizers (undefined) (pull_request) Successful in 51s
CI / sanitizers (address) (pull_request) Successful in 51s
CI / fuzz-build (pull_request) Successful in 19s
CI / coverage (pull_request) Successful in 43s
CI / valgrind (pull_request) Successful in 37s
CI / build-and-test (pull_request) Successful in 14m55s
Unit: manifest_delete_missing_args treats a deeper missing entry whose
destination parent directory does not exist as a no-op (run continues, nothing
created). Integration (TestMissingArgs): a deeper missing entry with an absent
parent -R bare and full-mirror layouts, single-threaded and -m, no longer
aborts --delete (the extras walk still removes an unrelated extra); --dirs +
--files-from with --ignore-missing-args skips a listed-but-missing entry and
transfers the rest.
2026-09-07 18:33:34 +02:00
TapTap 4b74f6a41d fix: treat an absent missing-mirror parent as a no-op, only claim real deletions
BLOCKER: an absent mirror whose PARENT directory does not exist on the
destination (a deeper --files-from missing entry, -R or full-mirror layout) was
treated as a hard failure because file_open_secure_parent returned -1 when the
parent was missing. That aborted the whole run, skipped the --delete extras
walk, and tore down an early --delete-before/during connection, contradicting
'missing mirror = no-op / all-missing succeeds'. A parent-open failure is now a
no-op when errno is ENOENT/ENOTDIR (matching file_remove_tree_secure); only a
genuine I/O error fails the run.

Also: an already-absent mirror reached via unlinkat-ENOENT no longer prints
'Deleted: <path>' (a no-op dressed as a deletion); the per-path 'Deleted:' line
is printed only when an entry was actually removed.
2026-09-07 18:32:47 +02:00
TapTap 56981a0d9d fix: make digest-vector arrays const to satisfy cppcheck constVariable
CI / lint (pull_request) Successful in 35s
CI / sanitizers (undefined) (pull_request) Successful in 50s
CI / sanitizers (address) (pull_request) Successful in 52s
CI / fuzz-build (pull_request) Successful in 19s
CI / coverage (pull_request) Successful in 42s
CI / valgrind (pull_request) Successful in 36s
CI / build-and-test (pull_request) Successful in 12m58s
2026-09-07 17:46:52 +02:00
TapTap a2ce0a8e41 docs: mark --checksum-choice and --checksum-seed implemented
CI / lint (pull_request) Failing after 35s
CI / build-and-test (pull_request) Skipped
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
Precise notes for both rows: supported algorithms (xxh64/xxhash, md5 via EVP),
rejection of unsupported names, seed semantics (full 64-bit for whole-file,
low 32 bits for the delta block hash, ignored by md5), the --cc alias, the
length-prefixed bounded handshake digest, the 2.9.0 -> 2.10.0 protocol bump,
default byte-for-byte preservation, and the divergence from rsync's randomized
seed (FastSync defaults to seed 0). Summary count line is intentionally untouched.
2026-09-07 17:43:00 +02:00
TapTap df250ec9c9 test: coverage for --checksum-choice/--cc and --checksum-seed
Unit: digest selection with known vectors (xxh64 seed 0/empty, md5 RFC vectors,
seed changes xxh64 but not md5, name mapping, bounded buffer rejection), CLI
parse (all spellings, = forms, --cc alias, unsupported rejected, seed strict
decimal), config wire round-trip and out-of-range algo rejection, and seeded
delta signature/compute symmetry (matching seed rebuilds, mismatched seed yields
no block matches). Integration: unchanged skip + same-size/mtime redetect for
xxh64 and md5 with and without -m, near-zero-data skip run, deterministic seed,
and a byte-exact seeded delta run.
2026-09-07 17:42:53 +02:00
TapTap 0afda6b094 feat(checksum): --checksum-choice/--cc and --checksum-seed for whole-file digest
Adds real algorithm selection (xxh64 default, plus md5 via OpenSSL EVP) and a
64-bit seed for the per-file whole-file digest used by the --incremental/
--checksum handshake and basis-dir content verification. The seed also feeds
the delta path's per-block xxHash32 strong checksum (low 32 bits) so an
explicit seed deterministically changes those digests too. Sender and receiver
hash identically: the algorithm id and seed cross the config wire frame and the
STATUS_CHECK handshake now carries a length-prefixed, bounded digest instead of
a fixed 64-bit value. Unsupported algorithm names are rejected at parse time
(never a silent no-op). PROTOCOL_VERSION bumped 2.9.0 -> 2.10.0; defaults
(xxh64, seed 0) preserve prior byte-for-byte behavior.
2026-09-07 17:42:47 +02:00
TapTap bfb3a531e9 docs: mark --append / --append-verify implemented in RSYNC_COMPAT
CI / lint (pull_request) Successful in 49s
CI / sanitizers (undefined) (pull_request) Successful in 50s
CI / sanitizers (address) (pull_request) Successful in 51s
CI / fuzz-build (pull_request) Successful in 17s
CI / coverage (pull_request) Successful in 42s
CI / valgrind (pull_request) Successful in 36s
CI / build-and-test (pull_request) Successful in 12m52s
Flip both rows to Implemented with precise Notes covering the tail-resume
model, the prefix-verify semantics (and the plain-append rsync-parity safety
statement), the new wire frames, and the PROTOCOL_VERSION 2.9.0 -> 2.10.0 bump.
Add a Phase-3 append-wave implementation-notes block. The Summary count line
is deliberately left untouched.
2026-09-07 15:43:52 +02:00
TapTap ffdbb6568f test: append/append-verify resume coverage
- CLI: --append/--append-verify acceptance (parse + imply --incremental,
  validate) and incompatibility rejection with -s and --whole-file; both
  removed from the unimplemented reject list.
- Config: on-the-wire append/append_verify round-trip.
- Unit: append_resume_eligible / append_tail_length pure resume math.
- Integration (test_append.py): matching-prefix resume is byte-identical and
  tail-only (wire bytes << source size); --append with a wrong prefix keeps
  prefix+tail (rsync parity) while --append-verify detects the mismatch and
  falls back to a byte-exact full transfer; --append with --inplace and -m.
2026-09-07 15:43:33 +02:00
TapTap 6ad3887aa1 feat: --append / --append-verify tail-only resume
Implement rsync's append modes: when an existing destination file is SHORTER
than the source, the receiver negotiates a resume offset and only the tail is
transferred; the full file (retained prefix + tail) is rebuilt and installed
through the normal atomic store path, so the result is byte-identical to the
source whenever the prefix matches.

- --append: sends the tail without content-verifying the retained prefix
  (rsync parity; the documented prefix-trust risk).
- --append-verify: verifies the retained prefix against the source's prefix
  xxHash64 before appending and, on a mismatch, falls back to a clean full
  transfer (never a corrupt prefix+tail blend).

New wire frames STATUS_APPEND / STATUS_APPEND_SIG / STATUS_APPEND_OK /
STATUS_APPEND_DATA; PROTOCOL_VERSION bumped 2.9.0 -> 2.10.0 (peers must match).
Both flags imply --incremental and are incompatible with -s (chunk
serialization) and --whole-file (rejected up front). Respects --inplace,
--partial/--partial-dir and --delay-updates via the shared store engine.
2026-09-07 15:43:29 +02:00
TapTap 0feb545008 docs: mark --ignore-missing-args / --delete-missing-args implemented
CI / lint (pull_request) Successful in 37s
CI / sanitizers (undefined) (pull_request) Successful in 52s
CI / sanitizers (address) (pull_request) Successful in 53s
CI / fuzz-build (pull_request) Successful in 18s
CI / coverage (pull_request) Successful in 42s
CI / valgrind (pull_request) Successful in 37s
CI / build-and-test (pull_request) Successful in 14m17s
Precise Notes on the arg model (files-from entries), implies-relationships
(delete-missing implies ignore, independent of --delete), delete/timing/force/
exclude-protection interplay, the third manifest section and the 2.9.0 -> 2.10.0
wire bump, and the divergences (empty-list hard error, no negation, max-delete
not applied to explicit deletions). The files-from row now references the flags.
The Summary count line is intentionally left for a recount commit.
2026-09-07 14:34:35 +02:00
TapTap be752b9bfd test: --ignore-missing-args / --delete-missing-args coverage
Unit: CLI parse + imply relationships (delete-missing implies ignore, not
delete; valid with --delete and delete timing); delete_missing_args config wire
round-trip (ignore_missing_args confirmed client-only); three-section manifest
round-trip and third-section traversal rejection; manifest_delete_missing_args
exact-path semantics; commit-time parking. Existing two-section manifest frames
updated to the three-section format. Integration: default missing entry is a
hard pre-transfer error; --ignore-missing-args transfers the rest and succeeds
(all-missing transfers nothing; empty list still errors); --delete-missing-args
removes exactly the missing mirror and leaves unrelated extras unless --delete is
also present; filter-exclusion protection never blocks the explicit deletion;
--delete-before early timing composes; all parametrized single-threaded vs -m.
2026-09-07 14:34:30 +02:00
TapTap 97f2dc468b feat(sender): --ignore-missing-args / --delete-missing-args CLI and files-from preflight
New flags parse onto the config; --delete-missing-args implies
--ignore-missing-args (order-independent) and does NOT imply --delete (rsync:
independent of other delete processing). The files-from preflight now classifies
listed-but-missing entries instead of hard-failing: under the flags each is
skipped (logged + counted, never silent) and the run succeeds for the rest,
including the all-missing case; an empty list stays a hard error. Under
--delete-missing-args the missing entries' destination mirrors (bare relative
path with -R, full source mirror otherwise) ride the manifest's third section in
both the single-threaded and -m senders; --dirs listed-but-missing entries are
skipped in the scanner.
2026-09-07 14:34:25 +02:00
TapTap c9bd76e633 feat(shared): --delete-missing-args config/wire, manifest third section, receiver exact-path deletions
PROTOCOL_VERSION 2.9.0 -> 2.10.0. The STATUS_MANIFEST frame gains a third
section carrying destination-relative exact-delete paths (the missing
--files-from entries' mirrors); the config frame gains a delete_missing_args
bool (ignore_missing_args stays client-only). The receiver validates the third
section like the keep-set and commits it with manifest_delete_all():
manifest_delete_missing_args runs first (explicit user requests, never blocked
by protected-prefix exclusion protection; staging/basis protected; a non-empty
directory mirror removed only under --force/--delete, rsync parity) and then the
ordinary extras walk. Server --allow-delete gates it like --delete.
2026-09-07 14:34:19 +02:00
TapTap 6701c103cb docs: recount RSYNC_COMPAT summary after Phase-3 wave B
CI / lint (push) Successful in 38s
CI / sanitizers (address) (push) Successful in 51s
CI / sanitizers (undefined) (push) Successful in 49s
CI / fuzz-build (push) Successful in 19s
CI / coverage (push) Successful in 43s
CI / valgrind (push) Successful in 37s
CI / build-and-test (push) Successful in 12m53s
2026-09-07 00:06:41 +02:00
TapTap ebab335488 Merge feat/p3-fuzzy: --fuzzy/-y/--no-fuzzy similar-file delta basis 2026-09-06 23:52:24 +02:00
TapTap ef13a70a29 Merge feat/p3-delete-policy: delete policy (--delete-excluded/--max-delete/--ignore-errors/--force/--prune-empty-dirs) 2026-09-06 23:52:00 +02:00
TapTap 4f19f5bfe7 fix: satisfy cppcheck on the hard-bound walker test
CI / lint (pull_request) Successful in 31s
CI / sanitizers (address) (pull_request) Successful in 52s
CI / sanitizers (undefined) (pull_request) Successful in 51s
CI / fuzz-build (pull_request) Successful in 17s
CI / coverage (pull_request) Successful in 42s
CI / valgrind (pull_request) Successful in 36s
CI / build-and-test (pull_request) Successful in 12m49s
Drop the derived 'created = rootfd >= 0' that cppcheck flagged as always true
after the EXPECT_TRUE guard.
2026-09-06 23:33:21 +02:00
TapTap 87b58975de style(receiver): rework fuzzy DP suffix trim, silence cppcheck FP
CI / lint (pull_request) Successful in 37s
CI / sanitizers (undefined) (pull_request) Successful in 42s
CI / sanitizers (address) (pull_request) Successful in 44s
CI / fuzz-build (pull_request) Successful in 17s
CI / coverage (pull_request) Successful in 34s
CI / valgrind (pull_request) Successful in 36s
CI / build-and-test (pull_request) Successful in 7m19s
The suffix-trim loop using computed end offsets tripped cppcheck's
knownConditionTrueFalse value-range analysis (it unsoundly concluded the trims
always consume the whole middle).  Rewrite it with explicit moving end indices
and add an inline suppression with a rationale for the residual false
positive; cppcheck --error-exitcode=1 is clean again.  The trimming logic is
unchanged and was verified against a full DP reference over 200k random name
pairs.
2026-09-06 23:11:01 +02:00
TapTap bb54113254 docs: all-or-nothing TOCTOU caveat and two-section manifest budget
CI / lint (pull_request) Failing after 32s
CI / build-and-test (pull_request) Skipped
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
The walker's all-or-nothing guarantee holds only while the destination is not
concurrently modified (rehearsal and delete are separate walks).  The
protected-prefix list shares the 16 MB MAX_MANIFEST_BYTES budget with the
keep-set and each section is capped at MAX_MANIFEST_ENTRIES; an over-budget
frame is rejected on the receiver with STATUS_ERROR rather than truncated.
2026-09-06 23:10:42 +02:00
TapTap 5fc49a8503 test: pin the review findings
- ignore-errors scan-error test now runs single-threaded and under -m (the
  exact scan_directory_multithreaded path that had the use-after-free);
- new integration test: --delete/--delete-before --ignore-errors with an
  unreadable SOURCE ROOT (sequential and -m) must fail and delete NOTHING;
- new integration test: a destination-only file that merely matches an exclude
  rule is deleted under plain --delete (protection is sender-derived), while a
  source-excluded mirror is protected;
- delete-protects/delete-excluded coverage extended to --delete-after and
  --delete-delay;
- new unit test: the 100000-entry server hard bound is all-or-nothing (more
  extras than the bound -> nothing removed);
- new integration test: --force is inert under --delay-updates (documented);
- fixed the ignore-errors assertion message that stated the opposite of what it
  asserted.
2026-09-06 23:10:37 +02:00
TapTap 8007aed5f6 fix: read scanner io_error before destroy (-m UAF); refuse an empty keep-set from an errored scan
scan_directory_multithreaded read directory_scanner_had_io_error() /
parallel_scanner_had_io_error() AFTER destroying the scanner object (a
heap-use-after-free on every successful -m run that recorded an io_error); the
flag is now captured before the destroy.  As a second line of defense against
an empty-keep-set wipe, all four manifest send sites (sequential and -m, early
pre-scan and commit data pass) now refuse to transmit a keep-set manifest when
the scan that built it recorded an io_error and produced no keep entries: a
source that merely LOOKS empty because part of it was unreadable must never
delete the whole destination.  A genuinely empty source (no io_error) still
sends its empty keep-set and prunes extras.
2026-09-06 23:10:32 +02:00
TapTap b031e73ab0 fix: treat an unreadable source root as fatal even under --ignore-errors
A sequential scanner records an opendir failure of its seed/root directory as a
skippable io_error and would complete an EMPTY scan, whose keep-set manifest
would then delete every destination entry.  The seed directory that maps to the
transfer root (relative path "") is now fatal regardless of --ignore-errors;
only subdirectories discovered during an otherwise-successful root scan are
skippable.  The -m path never had this hole (its root open failure aborts
scanner creation), so sequential and -m now agree.
2026-09-06 23:10:27 +02:00
TapTap 741d1f3b93 test: review follow-up coverage for --fuzzy
CI / lint (pull_request) Failing after 37s
CI / build-and-test (pull_request) Skipped
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
Unit (CLI): --fuzzy --no-incremental (either order) stays a valid plain-mode
config -- no forced handshake, no delta implication; --fuzzy --no-delta stays
covered.

Integration (TestFuzzy):
- worthless basis: a sibling that passes the name+size gates but shares no
  blocks makes the sender reply STATUS_NEXT; the whole file is consumed inside
  the delta handshake with byte-exact output (no protocol desync).
- non-displacement: an existing exact-path destination file INSIDE the delta
  size bounds (same size, different content, older mtime) is used as the delta
  basis instead of a byte-identical similar sibling (whole-file wire cost).
- asymmetric bases: basis larger than source (prefix reuse) and basis smaller
  than source (appended tail as literals) both reconstruct byte-exactly with a
  small delta.
- --no-fuzzy end-to-end equals the no-flag whole-file behavior.
CountingProxy closes its listener socket (fd hygiene).
2026-09-06 23:05:43 +02:00
TapTap c379e2dbdd docs: fuzzy oracle note and --no-incremental asymmetry
Review follow-ups on the --fuzzy row: note that the receiver's block
signature is derived from a sibling file it may not otherwise send, exposing
destination sibling files to the sender at block granularity (the same
known-plaintext information class as the ordinary delta path); and document
that --fuzzy honors an explicit --no-incremental (unlike the basis-dir
options, which force it), with the delta implication suppressed accordingly.
2026-09-06 23:05:39 +02:00
TapTap c1aabc68de feat(cli): --fuzzy honors an explicit --no-incremental
The --fuzzy implication previously forced use_incremental back on even when
the user passed --no-incremental, while --no-delta and -W were honored.
Track a --no-incremental latch (like the no_delta latch): with it set, do not
force the handshake on, and because delta needs the handshake, also suppress
the delta implication so no invalid '--delta requires --incremental' config
results.  A --fuzzy --no-incremental run is therefore a plain default-mode
transfer (fuzzy inert), consistent with -W/--no-delta.  Documented in the
usage text (this deliberately differs from the basis-dir options, which still
force incremental unconditionally).
2026-09-06 23:05:35 +02:00
TapTap be37a509a5 perf(receiver): bound the fuzzy edit-distance cost, harden the open
Review follow-ups on the --fuzzy candidate scan:
- Allocate the two DP rows once per directory scan instead of once per
  candidate (4096-entry directories no longer do thousands of malloc pairs).
- Pre-prune before the DP with two cheap lower bounds on the edit distance:
  the name length gap and the count of characters of one basename absent from
  the other; a candidate whose gate (distance*2 <= longer) already fails on
  the max of those bounds is skipped without running the DP.
- Trim the common prefix and non-overlapping common suffix before the DP so
  it only runs over the differing middles.
- Note that the 4096 readdir cap bounds iterations, not per-entry DP cost,
  and that the seen set is filesystem-order dependent (winner stays
  deterministic via the total comparator).
- Open the chosen candidate with O_NONBLOCK so a name raced to a FIFO cannot
  block the receive thread forever in open(2); the existing fstat S_ISREG gate
  still rejects non-regular files.  (The pre-existing basis_open_regular has
  the same latent FIFO pattern and is intentionally left unchanged.)
- Free old_data in receive_delta_file's defensive NULL guard.
2026-09-06 23:05:32 +02:00
TapTap 5f4c7ce638 fix: free walker-test root path after cleanup (ASan leak)
CI / lint (pull_request) Successful in 31s
CI / sanitizers (address) (pull_request) Successful in 40s
CI / sanitizers (undefined) (pull_request) Successful in 39s
CI / fuzz-build (pull_request) Successful in 18s
CI / coverage (pull_request) Successful in 36s
CI / valgrind (pull_request) Successful in 36s
CI / build-and-test (pull_request) Successful in 10m49s
make_walk_root() roots were removed from disk but never freed, leaking under
the address/valgrind sanitizer jobs.
2026-09-06 22:09:25 +02:00
TapTap 356b5592e0 feat: add confined symlink-safe directory-tree removal helper
CI / lint (pull_request) Successful in 32s
CI / sanitizers (address) (pull_request) Failing after 42s
CI / sanitizers (undefined) (pull_request) Successful in 43s
CI / fuzz-build (pull_request) Successful in 18s
CI / coverage (pull_request) Successful in 35s
CI / valgrind (pull_request) Failing after 36s
CI / build-and-test (pull_request) Successful in 10m49s
file_remove_tree_secure() opens the final component O_NOFOLLOW below the
authorized root and recursively wipes it with an fd-relative walk (symlinks are
removed by name, never followed); --force uses it to clear a destination
directory that blocks an incoming regular file.
2026-09-06 21:50:25 +02:00
TapTap 6e835fd9f7 docs: mark the delete-policy family implemented in RSYNC_COMPAT
--delete-excluded/--max-delete/--ignore-errors/--force/--prune-empty-dirs now
✅ with precise notes: the rsync-parity default (plain --delete protects
filter-excluded destination mirrors), the -m divergence (FastSync -m stays
multithreading, so --prune-empty-dirs is long-only), the all-or-nothing
max-delete/hard-bound error model, the 2.8.0 -> 2.9.0 protocol bump, and the
new two-section STATUS_MANIFEST frame.  Summary counts left for the orchestrator
to recount after the wave.
2026-09-06 21:50:16 +02:00
TapTap 1de2677bb1 test: delete-policy unit and integration coverage
Unit: walker all-or-nothing bounds (exceeded -> nothing removed + distinct
result; exact bound -> deletes), protected-prefix skipping, config wire
round-trip for force_delete/delete_excluded/prune_empty_dirs/max_delete, CLI
parse/validation for the new flags.  Integration (TestDeletePolicy): default
delete-excluded protection and --delete-excluded opt-out (single-thread, -m,
early --delete-before, excluded-dir subtrees), --max-delete all-or-nothing over
and at the limit, --force file-over-nonempty-dir replacement, --prune-empty-dirs
(--dirs mode + recursion-mode parity), and --ignore-errors keeping deletion
active across a genuine scan I/O error (run as an unprivileged user).
2026-09-06 21:50:12 +02:00
TapTap 0b25bacb18 feat: protect filter-excluded destination mirrors by default (--delete-excluded opt-in), --ignore-errors scan continuation, --prune-empty-dirs
The scanners now record every entry pruned by user-selection rules
(--filter/-C/per-dir, --exclude/--include, --max-size/--min-size) as a
destination-relative protected path on a caller-supplied sink (thread-safe in
the parallel scanner); --files-from subset pruning and -R relative wire paths
are never recorded.  The sender transmits these as manifest protected prefixes,
giving rsync's default --delete behavior (excluded mirrors survive) with
--delete-excluded opting back into deleting them.  --ignore-errors makes an
unreadable source directory a recorded, non-fatal scan error: the run continues,
the deletion still runs, and the exit code reports the ignored error.
--prune-empty-dirs omits an empty source directory's explicit --dirs entry.
Empty directories were never transferred by recursive scans (rsync -m parity).
2026-09-06 21:50:07 +02:00
TapTap c4e0de8f08 feat: split delete-manifest frame into keep-set + protected prefixes; enforce --max-delete and --force on the receiver
The STATUS_MANIFEST frame now carries two count-delimited sections: the kept
paths and a protected-prefix list (excluded-on-source paths the walker must not
delete unless --delete-excluded opted out).  The receiver's DeleteManifest is
passed through the commit/early paths unchanged.  manifest_delete_extras
honors a client --max-delete (all-or-nothing) and produces a distinct error for
it versus the 100000-entry server bound.  --force clears a non-empty directory
that blocks an incoming regular file (confined, symlink-safe) via a new
file_remove_tree_secure helper.
2026-09-06 21:50:02 +02:00
TapTap 1c9b660ac0 feat: all-or-nothing bounded delete walker
delete_extras_limited now rehearses a finite-capped deletion before unlinking
anything (an identical fd-relative walk that counts files and directories) and
returns DELETE_WALK_LIMIT_EXCEEDED with nothing removed when the run would
exceed the cap, so --max-delete is enforced per run instead of truncating the
deletion.  A directory that still holds entries the walker leaves in place
(protected excluded prefix, manifest-kept file, symlink) is left behind rather
than failing the whole deletion, matching rsync's leave-non-empty-dirs
behavior.  Rehearsal/delete each open an independent file description so a
prior pass cannot drain the directory stream.
2026-09-06 21:49:57 +02:00
TapTap 3631df0a3e feat: add delete-policy config fields and CLI flags
Adds ignore_errors (client-only) and force_delete (wire) booleans, makes
max_delete default -1 (no client limit), and parses --delete-excluded,
--max-delete=NUM, --ignore-errors, --force, --prune-empty-dirs.  Bumps
PROTOCOL_VERSION 2.8.0 -> 2.9.0 for the new on-the-wire force_delete field.
2026-09-06 21:49:53 +02:00
TapTap 2bc43d6084 docs: mark -y/--fuzzy/--no-fuzzy implemented in RSYNC_COMPAT
CI / lint (pull_request) Successful in 36s
CI / sanitizers (address) (pull_request) Successful in 42s
CI / sanitizers (undefined) (pull_request) Successful in 42s
CI / fuzz-build (pull_request) Successful in 17s
CI / coverage (pull_request) Successful in 34s
CI / valgrind (pull_request) Successful in 36s
CI / build-and-test (pull_request) Successful in 7m15s
Document the receiver-side decision location, the exact deterministic
similarity heuristic, the byte-exactness argument, when fuzzy applies (and
when it deliberately does not), the 2.8.0 -> 2.9.0 protocol bump, and the
divergences from rsync.  Update the basis-dir rows' protocol references to
the now-current 2.9.0.
2026-09-06 21:04:26 +02:00
TapTap 9c1ec6758c test: --fuzzy coverage (CLI, config wire, integration)
Unit: parse -y/--fuzzy and --no-fuzzy negation (order-independent), -W and
--no-delta leave fuzzy inert, --fuzzy implies --incremental/--delta, and
validate_config rejects fuzzy with -s (chunk serialization) and -f
(sendfile).  Config: fuzzy survives the socketpair wire round-trip.

Integration (TestFuzzy, byte counts via a new CountingProxy helper): the
rename case transfers a 2 MiB file in a few percent of its size with byte-
exact output under --fuzzy, while the no-fuzzy, no-candidate, dissimilar-
sibling and --whole-file runs send the whole file; -y alias; -m parity;
dest-holds-unsuitable-file falls back to the sibling; --delay-updates and
--remove-source-files keep their semantics on fuzzy transfers.
2026-09-06 21:04:23 +02:00
TapTap cebd23a239 feat(receiver): --fuzzy similar-file delta basis
When a file must be transferred and the destination holds no usable content
at the exact path (file absent, or outside the delta engine's size bounds),
the receiver now searches the target's own destination directory for an
existing regular file with a similar basename and uses it as the delta basis
via the existing receiver-driven STATUS_DELTA_SIGNATURE handshake.  The
sender never learns the basis was another file, so no wire change beyond the
new config flag was required.

Heuristic (deterministic, simpler than rsync's, documented): candidates are
sibling entries confined below the root and opened O_NOFOLLOW (symlinks are
never followed, nothing outside the destination root is read); dotfiles,
directories, the target's own name and stage/temp names are excluded; size
gate is delta_should_attempt; name gate is a Levenshtein distance <= half
the longer basename; the closest candidate (size tie-break, then lexical) is
loaded; the scan is capped at 4096 entries.

Byte-exactness is independent of the basis: block matches are verified by
Adler-32 + xxHash32, delta_apply validates every reference, and a basis that
shares nothing makes the sender reply with a whole-file transfer.  When no
candidate qualifies the normal whole-file transfer runs unchanged.
2026-09-06 21:04:19 +02:00
TapTap b753efcecc feat(cli): parse -y/--fuzzy and --no-fuzzy
Register --fuzzy (alias -y) as a boolean option and fuzzy as negatable so
--no-fuzzy works through the generic negation machinery.  FastSync's delta
machinery is off by default (unlike rsync, where --fuzzy implies nothing
because delta is the default), so --fuzzy implies --incremental and --delta
unless --whole-file or an explicit --no-delta switched delta off (leaving
fuzzy inert, matching rsync where -W makes fuzzy irrelevant).  Add help text.
2026-09-06 21:04:14 +02:00
TapTap f099a6e20f feat(config): add fuzzy flag and bump protocol to 2.9.0
-y/--fuzzy is receiver-side similar-file basis selection, so the receiver
must learn the flag: add a bool to Config, serialize it as a trailing field
on the config frame, and validate the received value.  Because the config
frame layout changed, PROTOCOL_VERSION moves to 2.9.0 (peers must match).
2026-09-06 21:04:11 +02:00
TapTap 3815b82306 docs: recount RSYNC_COMPAT summary after Phase-3 wave A
CI / lint (push) Successful in 32s
CI / sanitizers (undefined) (push) Successful in 41s
CI / sanitizers (address) (push) Successful in 42s
CI / fuzz-build (push) Successful in 16s
CI / coverage (push) Successful in 34s
CI / valgrind (push) Successful in 37s
CI / build-and-test (push) Successful in 7m13s
2026-09-06 20:11:39 +02:00
TapTap 01a5a93089 Merge feat/p3-basis-dest: alternate basis dirs (--compare-dest/--copy-dest/--link-dest) 2026-09-06 20:10:40 +02:00
TapTap 265b1e7669 Merge feat/p3-delete-timing: rsync delete timing (--delete-before/--delete-during/--del/--delete-after/--delete-delay) 2026-09-06 19:58:40 +02:00
TapTap 329fc60b14 test: make remove-source incremental-skip test deterministic
CI / lint (pull_request) Successful in 32s
CI / sanitizers (address) (pull_request) Successful in 42s
CI / sanitizers (undefined) (pull_request) Successful in 40s
CI / fuzz-build (pull_request) Successful in 17s
CI / coverage (pull_request) Successful in 35s
CI / valgrind (pull_request) Successful in 36s
CI / build-and-test (pull_request) Successful in 3m29s
The seed run did not preserve timestamps, so the destination copy's mtime was
the write time; the incremental --remove-source-files rerun only skipped the
file when both writes happened to land in the same whole second, making the
test flaky (observed intermittently in local full-suite runs and on CI).  Seed
with -M so the destination stores the source's exact mtime.
2026-09-06 19:53:22 +02:00
TapTap d6d502fbb4 docs: precise basis-dir caveats (shared-inode --inplace, attribute provenance, 256MiB limit)
CI / lint (pull_request) Successful in 32s
CI / sanitizers (address) (pull_request) Successful in 42s
CI / sanitizers (undefined) (pull_request) Successful in 40s
CI / fuzz-build (pull_request) Successful in 18s
CI / coverage (pull_request) Successful in 35s
CI / valgrind (pull_request) Successful in 36s
CI / build-and-test (pull_request) Failing after 3m31s
- --link-dest destination entries share the basis inode: a later --inplace run
  against such a path mutates the basis snapshot through the shared inode
  (recommend --copy-dest when the destination must stay independently writable).
- basis-hit files take mode/uid/gid and mtime from the basis file, not the
  sender's metadata (with --size-only the mtime can differ from the source).
- --remove-source-files sources satisfied by a basis dir are retained.
- basis runs refuse files above the 256 MiB whole-file limit up front (FastSync
  caps every whole-file payload path at 256 MiB; rsync supports arbitrary sizes);
  the config frame always carries a basis-count field (protocol 2.8.0).
2026-09-06 19:43:25 +02:00