From 2b5aaef409a152bc8baad0f21b1c1b68e3b58623 Mon Sep 17 00:00:00 2001 From: opencode Date: Wed, 16 Sep 2026 22:26:58 +0200 Subject: [PATCH] fix(parity): --preallocate wins over --sparse, prefer fallocate(2) --- src/shared/file.c | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/shared/file.c b/src/shared/file.c index 27084d6..b8c7028 100644 --- a/src/shared/file.c +++ b/src/shared/file.c @@ -40,19 +40,27 @@ static bool write_all(int fd, const void* data, unsigned long long size) { } /* Preallocate `size` bytes on `fd` before any data is written (--preallocate). - * posix_fallocate reserves real disk blocks, so an out-of-space condition + * fallocate(2) reserves real disk blocks, so an out-of-space condition * (ENOSPC/EDQUOT) surfaces up front instead of partway through a transfer; - * unavoidable fragmentation of a streamed file is also reduced. Some - * filesystems (e.g. tmpfs, ZFS) do not support it and return EOPNOTSUPP/ENOSYS, - * where we fall back to ftruncate, which still extends the logical size so the - * fail-fast/contiguity intent degrades gracefully but never fails. Genuine - * allocation failures are propagated as the error code (caller fails the write). - * posix_fallocate leaves the fd's file offset unchanged, so the subsequent - * write_all at offset 0 is unaffected. Returns 0 on success (including the - * fallback) or a nonzero error code. */ + * unavoidable fragmentation of a streamed file is also reduced. rsync favors + * the syscall over glibc posix_fallocate (whose emulation can be subtly + * different), so try fallocate(2) first and only fall back to posix_fallocate, + * then to ftruncate on filesystems (e.g. tmpfs, ZFS) that support neither. The + * logical size is always extended, so the fail-fast/contiguity intent degrades + * gracefully but never fails on an unsupported filesystem; genuine allocation + * failures are propagated as the error code (caller fails the write). Neither + * leaves the fd's file offset guaranteed, so the caller seeks back to 0 before + * writing. Returns 0 on success (including the fallback) or a nonzero error + * code. */ static int preallocate_fd(int fd, unsigned long long size) { if (size == 0) return 0; +#ifdef __linux__ + if (fallocate(fd, 0, 0, (off_t)size) == 0) + return 0; + if (errno != EOPNOTSUPP && errno != ENOSYS && errno != EINVAL) + return errno; +#endif int rc = posix_fallocate(fd, 0, (off_t)size); if (rc == EOPNOTSUPP || rc == ENOSYS) { if (ftruncate(fd, (off_t)size) == 0) @@ -1066,11 +1074,10 @@ static bool file_to_disk_secure_impl(const char* path, const void* data, } else { /* Preallocate the expected payload size before writing so an out-of-space condition fails cleanly up front (--preallocate). - --sparse takes precedence: posix_fallocate would allocate every - block, defeating the holes the sparse writer would create, so the - two never combine here (the ftruncate presize below stays). */ + rsync lets --preallocate win over --sparse (the reserved blocks + survive the sparse writer's seeks), so both flags can be active. */ int prealloc_rc = 0; - if (preallocate && !sparse && data_size > 0) { + if (preallocate && data_size > 0) { prealloc_rc = preallocate_fd(fd, data_size); if (prealloc_rc != 0) { char* escaped_path = output_escape(path, log_get_8_bit_output()); @@ -1196,7 +1203,7 @@ static bool file_to_disk_secure_impl(const char* path, const void* data, if (fd < 0) continue; /* EEXIST (or a transient open error): try a fresh name. */ int prealloc_rc = 0; - if (preallocate && !sparse && data_size > 0) { + if (preallocate && data_size > 0) { prealloc_rc = preallocate_fd(fd, data_size); if (prealloc_rc != 0) { char* escaped_path = output_escape(path, log_get_8_bit_output());