From b9dbfd85c1a34acecf979482266335f8f16bee23 Mon Sep 17 00:00:00 2001 From: "Yury V. Zaytsev" Date: Tue, 29 Sep 2026 09:01:14 +0000 Subject: [PATCH 1/7] (fix) copy_file_file: don't preallocate space in append and reget modes posix_fallocate() extends the file to offset + len; in append and reget modes the destination was extended to the source size before copying, and the data was then appended after the preallocated area: the result was too long and had a zero-filled gap (reget), or preallocation failed with EINVAL when the source was smaller than the destination (append). Assisted-By: Claude Opus 5.5 Signed-off-by: Yury V. Zaytsev --- src/filemanager/file.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/filemanager/file.c b/src/filemanager/file.c index 256f792a6..e31374bf8 100644 --- a/src/filemanager/file.c +++ b/src/filemanager/file.c @@ -2696,9 +2696,11 @@ copy_file_file (file_op_context_t *ctx, const char *src_path, const char *dst_pa goto ret; } - // try preallocate space; if fail, try copy anyway - while (mc_global.vfs.preallocate_space - && vfs_preallocate (dest_desc, file_size, appending ? dst_stat.st_size : 0) != 0) + // try preallocate space; if fail, try copy anyway. + // Not in append and reget modes: posix_fallocate() extends the file, so the data would be + // appended after the preallocated area. + while (mc_global.vfs.preallocate_space && !appending + && vfs_preallocate (dest_desc, file_size, 0) != 0) { if (ctx->ignore_all) { From 7303d0093f147dc5afee7e409eadf61a9c6e5b7b Mon Sep 17 00:00:00 2001 From: Phil Krylov Date: Sun, 4 Oct 2026 01:28:16 +0200 Subject: [PATCH 2/7] (fix) file cloning: Require COPY_FILE_RANGE_CLONE support on FreeBSD (15.0+) Otherwise copy_file_range() with flags=0 performs a kernel copy offload without block cloning guarantee. Signed-off-by: Phil Krylov --- configure.ac | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index 755e4a75e..2dcde39aa 100644 --- a/configure.ac +++ b/configure.ac @@ -383,8 +383,26 @@ esac dnl Check for file cloning support case $host_os in darwin*) AC_CHECK_HEADERS([sys/clonefile.h]) ;; # clonefile(2) (macOS 10.12+) -freebsd*) AC_CHECK_FUNCS(copy_file_range) ;; # copy_file_range(2) (FreeBSD 13+) solaris*) AC_CHECK_FUNCS(reflink) ;; # reflink(3C) (Solaris 11.3+) +freebsd*) # copy_file_range(2) (FreeBSD 13+) + AC_CHECK_FUNCS(copy_file_range) + AC_MSG_CHECKING([for COPY_FILE_RANGE_CLONE]) + AC_EGREP_CPP([COPY_FILE_RANGE_CLONE_IS_DEFINED], + [ + #include + #ifdef COPY_FILE_RANGE_CLONE + COPY_FILE_RANGE_CLONE_IS_DEFINED + #endif + ], + [ + have_copy_file_range_clone=yes + AC_DEFINE(HAVE_COPY_FILE_RANGE_CLONE, 1, [Define if COPY_FILE_RANGE_CLONE is supported on FreeBSD]) + AC_MSG_RESULT(yes) + ], + [ + AC_MSG_RESULT(no) + ]) + ;; linux*) # FICLONERANGE (Linux 4.5+) AC_CHECK_HEADERS([linux/fs.h]) AC_MSG_CHECKING([for FICLONERANGE]) @@ -407,7 +425,7 @@ linux*) # FICLONERANGE (Linux 4.5+) esac if test "x$have_ficlonerange" = xyes || \ - test "x$ac_cv_func_copy_file_range" = xyes; then + test "x$have_copy_file_range_clone" = xyes; then AC_DEFINE([HAVE_FILE_CLONING_BY_RANGE], [1], [Define if system can clone files by range]) fi From 9365e47bda7ae5e93f05313bdcef8c8e3377d276 Mon Sep 17 00:00:00 2001 From: Phil Krylov Date: Sat, 26 Sep 2026 18:43:28 +0200 Subject: [PATCH 3/7] Add support for cross-filesystem file cloning on Linux 5.19+ This allows to make cross-dataset file clones on ZFS on Linux. If ioctl(FICLONERANGE) sets EXDEV, then try copy_file_range(), otherwise skip it as it could result in a non-cloning kernel copy offload. Before Linux 5.19, copy_file_range() had bugs and inconsistent behaviour, so guard it with a runtime version check. On Linux, use copy_file_range() only with 64-bit off_t. Signed-off-by: Phil Krylov Co-authored-by: Yury V. Zaytsev Assisted-By: Claude Opus 5.5 --- configure.ac | 2 ++ lib/vfs/vfs.c | 59 +++++++++++++++++++++++++++++++--- tests/lib/vfs/vfs_clone_file.c | 6 +++- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/configure.ac b/configure.ac index 2dcde39aa..ddcadcf9e 100644 --- a/configure.ac +++ b/configure.ac @@ -421,6 +421,8 @@ linux*) # FICLONERANGE (Linux 4.5+) [ AC_MSG_RESULT(no) ]) + # copy_file_range(2) (Linux): glibc declares it with off64_t *, so require a 64-bit off_t + AS_IF([test "x$ac_cv_sizeof_off_t" = x8], [AC_CHECK_FUNCS(copy_file_range)]) ;; esac diff --git a/lib/vfs/vfs.c b/lib/vfs/vfs.c index ed537deaf..163de9a5e 100644 --- a/lib/vfs/vfs.c +++ b/lib/vfs/vfs.c @@ -49,8 +49,16 @@ #ifdef HAVE_FICLONERANGE #include // FICLONERANGE #include // ioctl() -#elif defined(HAVE_COPY_FILE_RANGE) -#include // COPY_FILE_RANGE_CLONE +#endif + +#if defined(HAVE_COPY_FILE_RANGE) + +#include // copy_file_range(), COPY_FILE_RANGE_CLONE + +#ifdef __linux__ +#include // uname() +#endif + #elif defined(HAVE_SYS_CLONEFILE_H) #include // CLONE_NOOWNERCOPY #elif defined(HAVE_REFLINK) @@ -210,6 +218,31 @@ vfs_test_current_dir (const vfs_path_t *vpath) && my_stat.st_ino == my_stat2.st_ino && my_stat.st_dev == my_stat2.st_dev); } +/* --------------------------------------------------------------------------------------------- */ + +#ifdef HAVE_COPY_FILE_RANGE +/* Return true if copy_file_range(2) works well. To stay on the safe side, this is false on Linux + * kernels before 5.19 and on FreeBSD builds before 15.0 which has introduced + * COPY_FILE_RANGE_CLONE. */ +static gboolean +vfs_copy_file_range_works (void) +{ +#if defined(__FreeBSD__) && defined(COPY_FILE_RANGE_CLONE) + return TRUE; +#elif defined(__linux__) + static int copy_file_range_works_cache = -1; + struct utsname name; + + if (copy_file_range_works_cache < 0) + copy_file_range_works_cache = + uname (&name) == 0 && 0 <= str_verscmp (name.release, "5.19.0"); + return (gboolean) copy_file_range_works_cache; +#else + return FALSE; +#endif +} +#endif + /* --------------------------------------------------------------------------------------------- */ /*** public functions ****************************************************************************/ /* --------------------------------------------------------------------------------------------- */ @@ -762,6 +795,7 @@ vfs_clone_file (int dest_vfs_fd, int src_vfs_fd) #if defined(FICLONERANGE) { + int rc; struct file_clone_range fcr = { .src_fd = *(int *) src_fd, .src_offset = in_offset, @@ -769,16 +803,33 @@ vfs_clone_file (int dest_vfs_fd, int src_vfs_fd) .dest_offset = out_offset, }; - return ioctl (*(int *) dest_fd, FICLONERANGE, &fcr); + rc = ioctl (*(int *) dest_fd, FICLONERANGE, &fcr); +#if defined(HAVE_COPY_FILE_RANGE) + if (rc != -1 || errno != EXDEV) +#endif + return rc; + /* Proceed with copy_file_range() */ } -#elif defined(COPY_FILE_RANGE_CLONE) + +#if defined(HAVE_COPY_FILE_RANGE) { ssize_t result; + if (!vfs_copy_file_range_works ()) + { + errno = ENOTSUP; + return -1; + } + do { +#if defined(COPY_FILE_RANGE_CLONE) result = copy_file_range (*(int *) src_fd, &in_offset, *(int *) dest_fd, &out_offset, SSIZE_MAX, COPY_FILE_RANGE_CLONE); +#else + result = copy_file_range (*(int *) src_fd, &in_offset, *(int *) dest_fd, &out_offset, + SSIZE_MAX, 0); +#endif } while (result > 0); return result; diff --git a/tests/lib/vfs/vfs_clone_file.c b/tests/lib/vfs/vfs_clone_file.c index 98eed3aea..132c709a1 100644 --- a/tests/lib/vfs/vfs_clone_file.c +++ b/tests/lib/vfs/vfs_clone_file.c @@ -33,8 +33,12 @@ #ifdef HAVE_FICLONERANGE #include // FICLONERANGE #include // ioctl() -#elif defined(HAVE_COPY_FILE_RANGE) +#endif +#if defined(HAVE_COPY_FILE_RANGE) #include // copy_file_range() +#ifndef COPY_FILE_RANGE_CLONE +#define COPY_FILE_RANGE_CLONE 0 // Linux shim +#endif #elif defined(HAVE_REFLINK) #include // reflink() #elif defined(HAVE_SYS_CLONEFILE_H) From 6e793fe00b3860ab63f6d9d4bfb0bce4e7570d2e Mon Sep 17 00:00:00 2001 From: Phil Krylov Date: Sun, 27 Sep 2026 21:36:44 +0200 Subject: [PATCH 4/7] ux: Report progress during slow file cloning, e.g. NFS < 4.2 There are cases where cloning the entire file in a single syscall still takes considerable time, for example, the FICLONERANGE Linux ioctl on a pre-4.2 NFS filesystem. Refactor range cloning path to work in chunks (of size increasing until it hurts progress reporting) and report progress. The new workflow is: 1. determine if we should try cloning for the given paths and append mode; 2. if cloning in append mode, open with O_WRONLY+lseek instead of O_APPEND; 3. if cloning, determine the copy method by trying to clone the first chunk. On Linux, if FICLONERANGE fails with EXDEV, retry with copy_file_range(); 4. if both copy methods have failed, switch to plain copy mode and only now perform preallocation; 5. in the main copy loop, if cloning, increase copy chunk size until it hurts progress bar smoothness. Signed-off-by: Phil Krylov Co-authored-by: Yury V. Zaytsev Assisted-By: Claude Opus 5.5 --- lib/vfs/vfs.c | 120 ++++++------------ lib/vfs/vfs.h | 11 +- src/filemanager/file.c | 220 +++++++++++++++++++++++---------- tests/lib/vfs/vfs_clone_file.c | 82 +----------- 4 files changed, 209 insertions(+), 224 deletions(-) diff --git a/lib/vfs/vfs.c b/lib/vfs/vfs.c index 163de9a5e..c4af75fc8 100644 --- a/lib/vfs/vfs.c +++ b/lib/vfs/vfs.c @@ -752,96 +752,58 @@ vfs_preallocate (int dest_vfs_fd, off_t src_fsize, off_t dest_fsize) /* --------------------------------------------------------------------------------------------- */ -int -vfs_clone_file (int dest_vfs_fd, int src_vfs_fd) -{ -#ifdef HAVE_FILE_CLONING_BY_RANGE - void *dest_fd = NULL; - void *src_fd = NULL; - struct vfs_class *dest_class; - struct vfs_class *src_class; - off_t in_offset, out_offset; - - dest_class = vfs_class_find_by_handle (dest_vfs_fd, &dest_fd); - if ((dest_class->flags & VFSF_LOCAL) == 0) - { - errno = ENOTSUP; - return (-1); - } - if (dest_fd == NULL) - { - errno = EBADF; - return (-1); - } - - src_class = vfs_class_find_by_handle (src_vfs_fd, &src_fd); - if ((src_class->flags & VFSF_LOCAL) == 0) - { - errno = ENOTSUP; - return (-1); - } - if (src_fd == NULL) - { - errno = EBADF; - return (-1); - } - - in_offset = mc_lseek (src_vfs_fd, 0, SEEK_CUR); - if (in_offset < 0) - return (-1); - out_offset = mc_lseek (dest_vfs_fd, 0, SEEK_CUR); - if (out_offset < 0) - return (-1); - #if defined(FICLONERANGE) - { - int rc; - struct file_clone_range fcr = { - .src_fd = *(int *) src_fd, - .src_offset = in_offset, - .src_length = 0, - .dest_offset = out_offset, - }; - - rc = ioctl (*(int *) dest_fd, FICLONERANGE, &fcr); -#if defined(HAVE_COPY_FILE_RANGE) - if (rc != -1 || errno != EXDEV) +ssize_t +mc_copy_file_range_ficlonerange (int src_fd, off_t *src_offset, int dest_fd, off_t *dest_offset, + size_t len) +{ + int rc; + struct file_clone_range fcr = { + .src_fd = src_fd, + .src_offset = *src_offset, + .src_length = (len == SSIZE_MAX) ? 0 : len, + .dest_offset = *dest_offset, + }; + off_t n_copied; + + rc = ioctl (dest_fd, FICLONERANGE, &fcr); + if (rc == -1) + return rc; + + n_copied = (len == SSIZE_MAX) ? lseek (src_fd, 0, SEEK_END) - *src_offset : (off_t) len; + *src_offset += n_copied; + *dest_offset += n_copied; + return n_copied; +} #endif - return rc; - /* Proceed with copy_file_range() */ - } + +/* --------------------------------------------------------------------------------------------- */ #if defined(HAVE_COPY_FILE_RANGE) +ssize_t +mc_copy_file_range_native (int src_fd, off_t *src_offset, int dest_fd, off_t *dest_offset, + size_t len) +{ + if (!vfs_copy_file_range_works ()) { - ssize_t result; - - if (!vfs_copy_file_range_works ()) - { - errno = ENOTSUP; - return -1; - } + errno = ENOTSUP; + return -1; + } - do - { #if defined(COPY_FILE_RANGE_CLONE) - result = copy_file_range (*(int *) src_fd, &in_offset, *(int *) dest_fd, &out_offset, - SSIZE_MAX, COPY_FILE_RANGE_CLONE); + return copy_file_range (src_fd, src_offset, dest_fd, dest_offset, len, COPY_FILE_RANGE_CLONE); #else - result = copy_file_range (*(int *) src_fd, &in_offset, *(int *) dest_fd, &out_offset, - SSIZE_MAX, 0); + return copy_file_range (src_fd, src_offset, dest_fd, dest_offset, len, 0); #endif - } - while (result > 0); - return result; - } +} #endif -#else - (void) dest_vfs_fd; - (void) src_vfs_fd; - errno = ENOTSUP; - return (-1); -#endif +/* --------------------------------------------------------------------------------------------- */ + +gboolean +vfs_cloning_supported (const vfs_path_t *src_vpath, const vfs_path_t *dst_vpath) +{ + return vfs_file_is_local (src_vpath) && vfs_file_is_local (dst_vpath); } /* --------------------------------------------------------------------------------------------- */ diff --git a/lib/vfs/vfs.h b/lib/vfs/vfs.h index 6ce271e38..efebee5e1 100644 --- a/lib/vfs/vfs.h +++ b/lib/vfs/vfs.h @@ -302,9 +302,18 @@ char *vfs_get_cwd (void); int vfs_preallocate (int dest_desc, off_t src_fsize, off_t dest_fsize); -int vfs_clone_file (int dest_vfs_fd, int src_vfs_fd); int vfs_clone_file_by_path (const vfs_path_t *dest_vpath, const vfs_path_t *src_vpath, gboolean preserve_uidgid); +gboolean vfs_cloning_supported (const vfs_path_t *src_vpath, const vfs_path_t *dst_vpath); + +#if defined(HAVE_FICLONERANGE) +ssize_t mc_copy_file_range_ficlonerange (int src_fd, off_t *src_offset, int dest_fd, + off_t *dest_offset, size_t len); +#endif +#if defined(HAVE_COPY_FILE_RANGE) +ssize_t mc_copy_file_range_native (int src_fd, off_t *src_offset, int dest_fd, off_t *dest_offset, + size_t len); +#endif /** * Interface functions described in interface.c diff --git a/src/filemanager/file.c b/src/filemanager/file.c index e31374bf8..b297e0b01 100644 --- a/src/filemanager/file.c +++ b/src/filemanager/file.c @@ -2294,6 +2294,9 @@ copy_file_file (file_op_context_t *ctx, const char *src_path, const char *dst_pa int open_flags; vfs_path_t *src_vpath = NULL, *dst_vpath = NULL; char *buf = NULL; +#ifdef HAVE_FILE_CLONING_BY_RANGE + gboolean try_cloning = FALSE; +#endif /* Keep the non-default value applied in chain of calls: move_file_file() -> file_progress_real_query_replace() @@ -2613,24 +2616,25 @@ copy_file_file (file_op_context_t *ctx, const char *src_path, const char *dst_pa } #endif +#ifdef HAVE_FILE_CLONING_BY_RANGE + try_cloning = mc_global.vfs.file_cloning && vfs_cloning_supported (src_vpath, dst_vpath); +#endif + open_flags = O_WRONLY; if (!dst_exists) open_flags |= O_CREAT | O_EXCL; else if (ctx->do_append) #ifdef HAVE_FILE_CLONING_BY_RANGE - // FICLONERANGE on Linux and copy_file_range(2) on FreeBSD support block-aligned ranges for - // cloning, but not in O_APPEND mode. Use O_WRONLY + mc_lseek instead as we don't care - // about atomicity in our use cases. - open_flags |= mc_global.vfs.file_cloning ? 0 : O_APPEND; + // FICLONERANGE on Linux and copy_file_range(2) support block-aligned ranges for cloning, + // but not in O_APPEND mode. Use O_WRONLY + mc_lseek instead as we don't care about + // atomicity in our use cases. Non-local VFSes (ftpfs, shell) need O_APPEND to append. + open_flags |= try_cloning ? 0 : O_APPEND; #else open_flags |= O_APPEND; #endif else open_flags |= O_CREAT | O_TRUNC; -#ifdef HAVE_FILE_CLONING_BY_RANGE -open_dest: -#endif while ((dest_desc = mc_open (dst_vpath, open_flags, src_mode)) < 0) { if (errno != EEXIST) @@ -2658,25 +2662,24 @@ copy_file_file (file_op_context_t *ctx, const char *src_path, const char *dst_pa ctx->do_append = FALSE; #ifdef HAVE_FILE_CLONING_BY_RANGE - // Try clone the file first, but not if the file is in O_APPEND mode - if (mc_global.vfs.file_cloning && (open_flags & O_APPEND) == 0) + if (try_cloning) { - if ((appending ? mc_lseek (dest_desc, 0, SEEK_END) >= 0 : TRUE) - && vfs_clone_file (dest_desc, src_desc) == 0) + // If we're appending but not in O_APPEND mode, seek first + while (appending && mc_lseek (dest_desc, 0, SEEK_END) < 0) { - dst_status = DEST_FULL; - return_status = FILE_CONT; + if (ctx->ignore_all) + return_status = FILE_IGNORE_ALL; + else + { + return_status = + file_error (ctx, TRUE, _ ("Cannot seek in target file\n%s"), dst_path); + if (return_status == FILE_RETRY) + continue; + if (return_status == FILE_IGNORE_ALL) + ctx->ignore_all = TRUE; + } goto ret; } - if (appending && (open_flags & O_APPEND) == 0) - { - // Cloning append has failed, resort to normal append - ctx->do_append = TRUE; - mc_close (dest_desc); - dst_status = DEST_NONE; - open_flags |= O_APPEND; - goto open_dest; - } } #endif @@ -2696,44 +2699,6 @@ copy_file_file (file_op_context_t *ctx, const char *src_path, const char *dst_pa goto ret; } - // try preallocate space; if fail, try copy anyway. - // Not in append and reget modes: posix_fallocate() extends the file, so the data would be - // appended after the preallocated area. - while (mc_global.vfs.preallocate_space && !appending - && vfs_preallocate (dest_desc, file_size, 0) != 0) - { - if (ctx->ignore_all) - { - // cannot allocate, start the file copying anyway - return_status = FILE_CONT; - break; - } - - return_status = - file_error (ctx, TRUE, _ ("Cannot preallocate space for target file\n%s"), dst_path); - - if (return_status == FILE_IGNORE_ALL) - ctx->ignore_all = TRUE; - - if (ctx->ignore_all || return_status == FILE_IGNORE) - { - // skip the space allocation error, start file copying - return_status = FILE_CONT; - break; - } - - if (return_status == FILE_ABORT) - { - mc_close (dest_desc); - dest_desc = -1; - mc_unlink (dst_vpath); - dst_status = DEST_NONE; - goto ret; - } - - // return_status == FILE_RETRY -- try allocate space again - } - ctx->eta_secs = 0.0; ctx->bps = 0; @@ -2754,13 +2719,139 @@ copy_file_file (file_op_context_t *ctx, const char *src_path, const char *dst_pa gint64 tv_last_update = ctx->transfer_start; gint64 tv_last_input = 0; gboolean is_first_time = TRUE; + size_t bufsize = 0; - const size_t bufsize = io_blksize (dst_stat); - buf = g_malloc (bufsize); +#ifdef HAVE_FILE_CLONING_BY_RANGE + ssize_t n_copied = -1; + ssize_t (*copy_method) (int, off_t *, int, off_t *, size_t) = NULL; + + off_t src_offset = ctx->do_reget; + // In append and reget modes, the destination fd was positioned at its end above + off_t dst_offset = appending ? dst_stat.st_size : 0; + + void *local_src_fd = NULL; + void *local_dst_fd = NULL; + + // Try to clone the initial chunk to choose a working copy_method + if (try_cloning) + { + bufsize = 1 << 20; + if ((off_t) bufsize > file_size - src_offset) + bufsize = SSIZE_MAX; // don't try to read behind EOF + vfs_class_find_by_handle (src_desc, &local_src_fd); + vfs_class_find_by_handle (dest_desc, &local_dst_fd); +#ifdef HAVE_FICLONERANGE + copy_method = mc_copy_file_range_ficlonerange; + n_copied = copy_method (*(int *) local_src_fd, &src_offset, *(int *) local_dst_fd, + &dst_offset, bufsize); +#ifdef HAVE_COPY_FILE_RANGE + if (n_copied < 0 && errno == EXDEV) +#endif +#endif +#ifdef HAVE_COPY_FILE_RANGE + { + copy_method = mc_copy_file_range_native; + n_copied = copy_method (*(int *) local_src_fd, &src_offset, *(int *) local_dst_fd, + &dst_offset, bufsize); + } +#endif + if (n_copied < 0) + copy_method = NULL; + } + + if (copy_method == NULL) // cloning has failed, fallback to normal copy +#endif + { + bufsize = io_blksize (dst_stat); + buf = g_malloc (bufsize); + + // In normal copy, we may want to preallocate. + // try preallocate space; if fail, try copy anyway. + // Not in append and reget modes: posix_fallocate() extends the file, so the data would + // be appended after the preallocated area. + while (mc_global.vfs.preallocate_space && !appending + && vfs_preallocate (dest_desc, file_size, 0) != 0) + { + if (ctx->ignore_all) + { + // cannot allocate, start the file copying anyway + return_status = FILE_CONT; + break; + } + + return_status = file_error ( + ctx, TRUE, _ ("Cannot preallocate space for target file\n%s"), dst_path); + + if (return_status == FILE_IGNORE_ALL) + ctx->ignore_all = TRUE; + + if (ctx->ignore_all || return_status == FILE_IGNORE) + { + // skip the space allocation error, start file copying + return_status = FILE_CONT; + break; + } + + if (return_status == FILE_ABORT) + { + mc_close (dest_desc); + dest_desc = -1; + mc_unlink (dst_vpath); + dst_status = DEST_NONE; + goto ret; + } + + // return_status == FILE_RETRY -- try allocate space again + } + } while (TRUE) { ssize_t n_read = -1; + gint64 tv_current; + +#ifdef HAVE_FILE_CLONING_BY_RANGE + if (copy_method != NULL) + { + off_t n_rest = file_size - src_offset; + + if (n_rest == 0) + break; + + tv_current = g_get_monotonic_time (); + + // Optimize bufsize until it hurts progress smoothness + if (tv_current - tv_last_input < FILEOP_UPDATE_INTERVAL_US >> 1 + && bufsize < SSIZE_MAX >> 1) + bufsize <<= 1; + + if ((off_t) bufsize > n_rest) + bufsize = SSIZE_MAX; // don't try to read behind EOF + n_copied = copy_method (*(int *) local_src_fd, &src_offset, *(int *) local_dst_fd, + &dst_offset, bufsize); + if (n_copied < 0) + { + return_status = ctx->ignore_all + ? FILE_IGNORE_ALL + : files_error (ctx, TRUE, _ ("Cannot copy file data from\n%s\nto\n%s"), + src_path, dst_path); + if (return_status == FILE_RETRY) + continue; + if (return_status == FILE_IGNORE_ALL) + ctx->ignore_all = TRUE; + goto ret; + } + + if (n_copied == 0) + break; + + file_part = src_offset - ctx->do_reget; + + tv_last_input = tv_current; + + goto chunk_done; + } +#endif // src_read if (mc_ctl (src_desc, VFS_CTL_IS_NOTREADY, 0) == 0) @@ -2778,7 +2869,7 @@ copy_file_file (file_op_context_t *ctx, const char *src_path, const char *dst_pa if (n_read == 0) break; - const gint64 tv_current = g_get_monotonic_time (); + tv_current = g_get_monotonic_time (); if (n_read > 0) { @@ -2826,6 +2917,9 @@ copy_file_file (file_op_context_t *ctx, const char *src_path, const char *dst_pa } } +#ifdef HAVE_FILE_CLONING_BY_RANGE + chunk_done: +#endif ctx->progress_bytes = file_part + ctx->do_reget; const gint64 usecs = tv_current - tv_last_update; diff --git a/tests/lib/vfs/vfs_clone_file.c b/tests/lib/vfs/vfs_clone_file.c index 132c709a1..390df41c7 100644 --- a/tests/lib/vfs/vfs_clone_file.c +++ b/tests/lib/vfs/vfs_clone_file.c @@ -30,16 +30,7 @@ #include #include -#ifdef HAVE_FICLONERANGE -#include // FICLONERANGE -#include // ioctl() -#endif -#if defined(HAVE_COPY_FILE_RANGE) -#include // copy_file_range() -#ifndef COPY_FILE_RANGE_CLONE -#define COPY_FILE_RANGE_CLONE 0 // Linux shim -#endif -#elif defined(HAVE_REFLINK) +#if defined(HAVE_REFLINK) #include // reflink() #elif defined(HAVE_SYS_CLONEFILE_H) #include // clonefile() @@ -57,43 +48,6 @@ static gboolean clone_syscall__call_arguments_are_proper = FALSE; static const char test_filename1[] = "mctestclone1.tst"; static const char test_filename2[] = "mctestclone2.tst"; -#ifdef HAVE_COPY_FILE_RANGE -/* @Mock */ -ssize_t -copy_file_range (int infd, off_t *inoffp, int outfd, off_t *outoffp, size_t len, unsigned int flags) -{ - (void) infd; - (void) inoffp; - (void) outfd; - (void) outoffp; - (void) len; - - clone_syscall__call_count++; - clone_syscall__call_arguments_are_proper = (flags == COPY_FILE_RANGE_CLONE); - - return -1; -} -#endif - -#ifdef HAVE_FICLONERANGE -#ifdef __GLIBC__ -/* @Mock */ -int -ioctl (int fd, unsigned long request, ...) -#else // POSIX, musl -/* @Mock */ -int -ioctl (int fd, int request, ...) -#endif -{ - (void) fd; - - clone_syscall__call_count++; - clone_syscall__call_arguments_are_proper = (request == FICLONERANGE); - return -1; -} -#endif - #ifdef HAVE_SYS_CLONEFILE_H /* @Mock */ int @@ -167,39 +121,6 @@ cleanup_files (vfs_path_t *vpath1, vfs_path_t *vpath2) unlink (test_filename2); } -/* @Test */ -START_TEST (test_vfs_clone_file) -{ - vfs_path_t *vpath1; - vfs_path_t *vpath2; - int fdin; - int fdout; - - // given - clone_syscall__call_count = 0; - clone_syscall__call_arguments_are_proper = FALSE; - prepare_files (&vpath1, &vpath2); - fdin = mc_open (vpath1, O_RDONLY | O_BINARY); - fdout = mc_open (vpath2, O_CREAT | O_WRONLY | O_TRUNC | O_BINARY, 0600); - - // when - vfs_clone_file (fdout, fdin); - - // then -#ifdef HAVE_FILE_CLONING_BY_RANGE - ck_assert (clone_syscall__call_count > 0); - ck_assert (clone_syscall__call_arguments_are_proper); -#else - ck_assert (errno == ENOTSUP); -#endif - - // cleanup - mc_close (fdout); - mc_close (fdin); - cleanup_files (vpath1, vpath2); -} -END_TEST - /* --------------------------------------------------------------------------------------------- */ /* @Test */ @@ -241,7 +162,6 @@ main (void) tcase_add_checked_fixture (tc_core, setup, teardown); // Add new tests here: *************** - tcase_add_test (tc_core, test_vfs_clone_file); tcase_add_test (tc_core, test_vfs_clone_file_by_path); // *********************************** From 43c00c55a84da07ef18eebddade9f6a0bc855ac0 Mon Sep 17 00:00:00 2001 From: Phil Krylov Date: Sun, 4 Oct 2026 23:52:39 +0200 Subject: [PATCH 5/7] tests: add regression tests for mc_copy_file_range_*() Signed-off-by: Phil Krylov --- tests/lib/vfs/Makefile.am | 4 + tests/lib/vfs/mc_copy_file_range.c | 338 +++++++++++++++++++++++++++++ 2 files changed, 342 insertions(+) create mode 100644 tests/lib/vfs/mc_copy_file_range.c diff --git a/tests/lib/vfs/Makefile.am b/tests/lib/vfs/Makefile.am index 90bd55cbb..1d3ec1d38 100644 --- a/tests/lib/vfs/Makefile.am +++ b/tests/lib/vfs/Makefile.am @@ -22,6 +22,7 @@ endif TESTS = \ canonicalize_pathname \ current_dir \ + mc_copy_file_range \ path_cmp \ path_len \ path_manipulations \ @@ -49,6 +50,9 @@ canonicalize_pathname_SOURCES = \ current_dir_SOURCES = \ current_dir.c +mc_copy_file_range_SOURCES = \ + mc_copy_file_range.c + path_cmp_SOURCES = \ path_cmp.c diff --git a/tests/lib/vfs/mc_copy_file_range.c b/tests/lib/vfs/mc_copy_file_range.c new file mode 100644 index 000000000..2bc57bc6a --- /dev/null +++ b/tests/lib/vfs/mc_copy_file_range.c @@ -0,0 +1,338 @@ +/* + lib/vfs - test mc_copy_file_range_*() functionality + + Copyright (C) 2026 + Free Software Foundation, Inc. + + Written by: + Phil Krylov , 2026 + + This file is part of the Midnight Commander. + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#define TEST_SUITE_NAME "/lib/vfs" + +#include "tests/mctest.h" + +#include +#include + +#if defined(HAVE_FICLONERANGE) +#include // FICLONERANGE +#include // ioctl() +#endif + +#if defined(HAVE_COPY_FILE_RANGE) +#include // copy_file_range(), COPY_FILE_RANGE_CLONE +#ifdef __linux__ +#include // uname() +#endif +#endif + +#include "lib/strutil.h" +#include "lib/util.h" +#include "src/vfs/local/local.c" + +/* --------------------------------------------------------------------------------------------- */ + +#ifdef HAVE_FILE_CLONING_BY_RANGE + +static int clone_syscall__call_count = 0; +static gboolean clone_syscall__call_arguments_are_proper = FALSE; + +#ifdef HAVE_FICLONERANGE +/* @ThenReturnValue */ +static int ioctl__ficlonerange__return_value = -1; +#endif + +#ifdef HAVE_COPY_FILE_RANGE +/* @ThenReturnValue */ +static ssize_t copy_file_range__return_value = -1; +#endif + +static const char test_filename1[] = "mctestclone1.tst"; +static const char test_filename2[] = "mctestclone2.tst"; + +#ifdef HAVE_COPY_FILE_RANGE +/* @Mock */ +ssize_t +copy_file_range (int infd, off_t *inoffp, int outfd, off_t *outoffp, size_t len, unsigned int flags) +{ + (void) infd; + (void) inoffp; + (void) outfd; + (void) outoffp; + (void) len; + + clone_syscall__call_count++; +#ifdef COPY_FILE_RANGE_CLONE + clone_syscall__call_arguments_are_proper = (flags == COPY_FILE_RANGE_CLONE); +#else + clone_syscall__call_arguments_are_proper = (flags == 0); +#endif + + return clone_syscall__call_arguments_are_proper ? copy_file_range__return_value : -1; +} + +#ifdef __linux__ +/* @Mock */ +int +uname (struct utsname *buf) +{ + strcpy (buf->release, "5.19.0"); + return 0; +} +#endif +#endif // HAVE_COPY_FILE_RANGE + +#ifdef HAVE_FICLONERANGE +#ifdef __GLIBC__ +/* @Mock */ +int +ioctl (int fd, unsigned long request, ...) +#else // POSIX, musl +/* @Mock */ +int +ioctl (int fd, int request, ...) +#endif +{ + (void) fd; + + clone_syscall__call_count++; + clone_syscall__call_arguments_are_proper = (request == FICLONERANGE); + return request == FICLONERANGE ? ioctl__ficlonerange__return_value : -1; +} +#endif // HAVE_FICLONERANGE + +#endif // HAVE_FILE_CLONING_BY_RANGE + +/* --------------------------------------------------------------------------------------------- */ + +/* @Before */ +static void +setup (void) +{ + str_init_strings (NULL); + + vfs_init (); + vfs_init_localfs (); + vfs_setup_work_dir (); +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @After */ +static void +teardown (void) +{ + vfs_shut (); + str_uninit_strings (); +} + +/* --------------------------------------------------------------------------------------------- */ + +#ifdef HAVE_FILE_CLONING_BY_RANGE + +static void +prepare_files (vfs_path_t **vpath1, vfs_path_t **vpath2, int *src_vfs_fd, int *dst_vfs_fd, + int *src_fd, int *dst_fd) +{ + void *src_fd_ptr = NULL; + void *dst_fd_ptr = NULL; + + unlink (test_filename1); // remove a possible leftover from a previous run + g_file_set_contents (test_filename1, "test", sizeof ("test") - 1, NULL); + unlink (test_filename2); // remove a possible leftover from a previous run + + *vpath1 = vfs_path_from_str (test_filename1); + *vpath2 = vfs_path_from_str (test_filename2); + *src_vfs_fd = mc_open (*vpath1, O_RDONLY | O_BINARY); + *dst_vfs_fd = mc_open (*vpath2, O_CREAT | O_WRONLY | O_TRUNC | O_BINARY, 0600); + vfs_class_find_by_handle (*src_vfs_fd, &src_fd_ptr); + vfs_class_find_by_handle (*dst_vfs_fd, &dst_fd_ptr); + *src_fd = *(int *) src_fd_ptr; + *dst_fd = *(int *) dst_fd_ptr; +} + +static void +cleanup_files (vfs_path_t *vpath1, vfs_path_t *vpath2, int src_vfs_fd, int dst_vfs_fd) +{ + mc_close (src_vfs_fd); + mc_close (dst_vfs_fd); + vfs_path_free (vpath1, TRUE); + vfs_path_free (vpath2, TRUE); + unlink (test_filename1); + unlink (test_filename2); +} + +/* --------------------------------------------------------------------------------------------- */ + +#ifdef HAVE_FICLONERANGE +/* @Test */ +START_TEST (test_mc_copy_file_range_ficlonerange) +{ + vfs_path_t *vpath1; + vfs_path_t *vpath2; + int src_vfs_fd; + int dst_vfs_fd; + int src_fd; + int dst_fd; + off_t in_offset = 0; + off_t out_offset = 0; + + // given + clone_syscall__call_count = 0; + clone_syscall__call_arguments_are_proper = FALSE; + prepare_files (&vpath1, &vpath2, &src_vfs_fd, &dst_vfs_fd, &src_fd, &dst_fd); + + // when + mc_copy_file_range_ficlonerange (src_fd, &in_offset, dst_fd, &out_offset, SSIZE_MAX); + + // then + ck_assert (clone_syscall__call_count > 0); + ck_assert (clone_syscall__call_arguments_are_proper); + + // cleanup + cleanup_files (vpath1, vpath2, src_vfs_fd, dst_vfs_fd); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +/* @Test */ +START_TEST (test_mc_copy_file_range_ficlonerange_success) +{ + vfs_path_t *vpath1; + vfs_path_t *vpath2; + int src_vfs_fd; + int dst_vfs_fd; + int src_fd; + int dst_fd; + off_t in_offset = 0; + off_t out_offset = 0; + ssize_t result; + + // given + clone_syscall__call_count = 0; + ioctl__ficlonerange__return_value = 0; + prepare_files (&vpath1, &vpath2, &src_vfs_fd, &dst_vfs_fd, &src_fd, &dst_fd); + + // when + result = mc_copy_file_range_ficlonerange (src_fd, &in_offset, dst_fd, &out_offset, SSIZE_MAX); + + // then: a successful FICLONERANGE is a successful clone, whatever copy_file_range() would do + ck_assert_int_eq (result, sizeof ("test") - 1); + + // cleanup + ioctl__ficlonerange__return_value = -1; + cleanup_files (vpath1, vpath2, src_vfs_fd, dst_vfs_fd); +} +END_TEST +#endif + +/* --------------------------------------------------------------------------------------------- */ + +#ifdef HAVE_COPY_FILE_RANGE +/* @Test */ +START_TEST (test_mc_copy_file_range_native) +{ + vfs_path_t *vpath1; + vfs_path_t *vpath2; + int src_vfs_fd; + int dst_vfs_fd; + int src_fd; + int dst_fd; + off_t in_offset = 0; + off_t out_offset = 0; + + // given + clone_syscall__call_count = 0; + clone_syscall__call_arguments_are_proper = FALSE; + prepare_files (&vpath1, &vpath2, &src_vfs_fd, &dst_vfs_fd, &src_fd, &dst_fd); + + // when + mc_copy_file_range_native (src_fd, &in_offset, dst_fd, &out_offset, SSIZE_MAX); + + // then + ck_assert (clone_syscall__call_count > 0); + ck_assert (clone_syscall__call_arguments_are_proper); + + // cleanup + cleanup_files (vpath1, vpath2, src_vfs_fd, dst_vfs_fd); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +/* @Test */ +START_TEST (test_mc_copy_file_range_native_success) +{ + vfs_path_t *vpath1; + vfs_path_t *vpath2; + int src_vfs_fd; + int dst_vfs_fd; + int src_fd; + int dst_fd; + off_t in_offset = 0; + off_t out_offset = 0; + ssize_t result; + + // given + clone_syscall__call_count = 0; + copy_file_range__return_value = sizeof ("test") - 1; + prepare_files (&vpath1, &vpath2, &src_vfs_fd, &dst_vfs_fd, &src_fd, &dst_fd); + + // when + result = mc_copy_file_range_native (src_fd, &in_offset, dst_fd, &out_offset, SSIZE_MAX); + + // then: a successful mc_copy_file_range_native() is a successful clone + ck_assert_int_eq (result, sizeof ("test") - 1); + + // cleanup + copy_file_range__return_value = -1; + cleanup_files (vpath1, vpath2, src_vfs_fd, dst_vfs_fd); +} +END_TEST +#endif + +#endif // HAVE_FILE_CLONING_BY_RANGE + +/* --------------------------------------------------------------------------------------------- */ + +int +main (void) +{ + TCase *tc_core; + + tc_core = tcase_create ("Core"); + + tcase_add_checked_fixture (tc_core, setup, teardown); + + // Add new tests here: *************** +#ifdef HAVE_FILE_CLONING_BY_RANGE +#ifdef HAVE_FICLONERANGE + tcase_add_test (tc_core, test_mc_copy_file_range_ficlonerange); + tcase_add_test (tc_core, test_mc_copy_file_range_ficlonerange_success); +#endif +#if defined(HAVE_COPY_FILE_RANGE) + tcase_add_test (tc_core, test_mc_copy_file_range_native); + tcase_add_test (tc_core, test_mc_copy_file_range_native_success); +#endif +#endif + // *********************************** + + return mctest_run_all (tc_core); +} From 20e17aa7f5944165cc14a28edf90606107b48cba Mon Sep 17 00:00:00 2001 From: "Yury V. Zaytsev" Date: Tue, 29 Sep 2026 08:56:00 +0000 Subject: [PATCH 6/7] tests: add copy_file_file() regression tests Assisted-By: Claude Opus 5.5 Signed-off-by: Yury V. Zaytsev --- tests/src/filemanager/Makefile.am | 4 + tests/src/filemanager/copy_file_file.c | 258 +++++++++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 tests/src/filemanager/copy_file_file.c diff --git a/tests/src/filemanager/Makefile.am b/tests/src/filemanager/Makefile.am index 7a902ce0b..823010a7e 100644 --- a/tests/src/filemanager/Makefile.am +++ b/tests/src/filemanager/Makefile.am @@ -17,6 +17,7 @@ endif TESTS = \ cd_to \ + copy_file_file \ examine_cd \ exec_get_export_variables_ext \ ext__exec_make_shell_string \ @@ -29,6 +30,9 @@ check_PROGRAMS = $(TESTS) cd_to_SOURCES = \ cd_to.c +copy_file_file_SOURCES = \ + copy_file_file.c + examine_cd_SOURCES = \ examine_cd.c diff --git a/tests/src/filemanager/copy_file_file.c b/tests/src/filemanager/copy_file_file.c new file mode 100644 index 000000000..b1410b60a --- /dev/null +++ b/tests/src/filemanager/copy_file_file.c @@ -0,0 +1,258 @@ +/* + src/filemanager - tests for copy_file_file() function + + Copyright (C) 2026 + Free Software Foundation, Inc. + + This file is part of the Midnight Commander. + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#define TEST_SUITE_NAME "/src/filemanager" + +#include "tests/mctest.h" + +#include "lib/strutil.h" +#include "lib/util.h" + +#include "src/vfs/local/local.c" + +#include "src/setup.h" // verbose +#include "src/filemanager/layout.h" // nice_rotating_dash +#include "src/filemanager/file.h" +#include "src/filemanager/filegui.h" + +/* --------------------------------------------------------------------------------------------- */ + +/* Big enough to take several chunks of the chunked range copy (which starts at 1 MiB and doubles), + * and deliberately not a multiple of any block size so that the tail goes through the EOF path. */ +#define BIG_SIZE ((3 << 20) + 12345) + +static char *test_dir = NULL; +static char *src_path = NULL; +static char *dst_path = NULL; + +/* --------------------------------------------------------------------------------------------- */ + +static char * +make_data (size_t len, guint32 seed) +{ + char *data; + size_t i; + + data = g_malloc (len); + for (i = 0; i < len; i++) + { + seed = seed * 1103515245U + 12345U; + data[i] = (char) (seed >> 16); + } + return data; +} + +/* --------------------------------------------------------------------------------------------- */ + +static char * +dup_data (const char *data, size_t len) +{ + char *copy; + + copy = g_malloc (len); + memcpy (copy, data, len); + return copy; +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +write_file (const char *path, const char *data, size_t len) +{ + GError *error = NULL; + + ck_assert_msg (g_file_set_contents (path, data, (gssize) len, &error), "%s", + error != NULL ? error->message : ""); +} + +/* --------------------------------------------------------------------------------------------- */ + +static void +assert_file_equals (const char *path, const char *expected, size_t expected_len) +{ + char *actual = NULL; + gsize actual_len = 0; + size_t i; + + ck_assert (g_file_get_contents (path, &actual, &actual_len, NULL)); + ck_assert_uint_eq (actual_len, expected_len); + for (i = 0; i < expected_len && actual[i] == expected[i]; i++) + ; + ck_assert_msg (i == expected_len, "destination differs from expected at offset %zu", i); + g_free (actual); +} + +/* --------------------------------------------------------------------------------------------- */ + +static FileProgressStatus +run_copy (off_t do_reget, gboolean do_append) +{ + file_op_context_t *ctx; + FileProgressStatus ret; + + ctx = file_op_context_new (OP_COPY); + ctx->ask_overwrite = FALSE; + ctx->do_reget = do_reget; + ctx->do_append = do_append; + + ret = copy_file_file (ctx, src_path, dst_path); + + file_op_context_destroy (ctx); + return ret; +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @Before */ +static void +setup (void) +{ + str_init_strings (NULL); + + vfs_init (); + vfs_init_localfs (); + vfs_setup_work_dir (); + + verbose = FALSE; + nice_rotating_dash = FALSE; // no screen to draw on + mc_global.vfs.preallocate_space = FALSE; + + test_dir = g_dir_make_tmp ("mc-test-copy-XXXXXX", NULL); + ck_assert_ptr_nonnull (test_dir); + src_path = g_build_filename (test_dir, "src", (char *) NULL); + dst_path = g_build_filename (test_dir, "dst", (char *) NULL); +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @After */ +static void +teardown (void) +{ + unlink (src_path); + unlink (dst_path); + rmdir (test_dir); + g_free (src_path); + g_free (dst_path); + g_free (test_dir); + + vfs_shut (); + str_uninit_strings (); +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @DataSource("test_copy_ds") */ +static const struct test_copy_ds +{ + gboolean file_cloning; + size_t src_len; + size_t dst_len; // length of pre-existing destination, 0 if it doesn't exist + gboolean do_append; + gboolean reget; // destination is a prefix of source and the copy resumes after it +} test_copy_ds[] = { + // plain copy + { TRUE, 5, 0, FALSE, FALSE }, + { FALSE, 5, 0, FALSE, FALSE }, + { TRUE, BIG_SIZE, 0, FALSE, FALSE }, + { FALSE, BIG_SIZE, 0, FALSE, FALSE }, + // [ Append ]: source must go after the existing destination contents + { TRUE, 5, 7, TRUE, FALSE }, + { FALSE, 5, 7, TRUE, FALSE }, + { TRUE, BIG_SIZE, 4096, TRUE, FALSE }, + { FALSE, BIG_SIZE, 4096, TRUE, FALSE }, + { TRUE, BIG_SIZE, 4096 + 7, TRUE, FALSE }, + { FALSE, BIG_SIZE, 4096 + 7, TRUE, FALSE }, + // [ Reget ]: resume an interrupted copy + { TRUE, BIG_SIZE, 4096, TRUE, TRUE }, + { FALSE, BIG_SIZE, 4096, TRUE, TRUE }, + { TRUE, BIG_SIZE, (1 << 20) + 3, TRUE, TRUE }, + { FALSE, BIG_SIZE, (1 << 20) + 3, TRUE, TRUE }, +}; + +/* @Test(dataSource = "test_copy_ds") */ +START_PARAMETRIZED_TEST (test_copy, test_copy_ds) +{ + char *src; + char *dst = NULL; + char *expected; + size_t expected_len; + FileProgressStatus ret; + + // given + src = make_data (data->src_len, 1); + write_file (src_path, src, data->src_len); + + if (data->dst_len != 0) + { + dst = data->reget ? dup_data (src, data->dst_len) : make_data (data->dst_len, 2); + write_file (dst_path, dst, data->dst_len); + } + + if (data->do_append && !data->reget) + { + expected_len = data->dst_len + data->src_len; + expected = g_malloc (expected_len); + memcpy (expected, dst, data->dst_len); + memcpy (expected + data->dst_len, src, data->src_len); + } + else + { + expected_len = data->src_len; + expected = dup_data (src, expected_len); + } + + mc_global.vfs.file_cloning = data->file_cloning; + + // when + ret = run_copy (data->reget ? (off_t) data->dst_len : 0, data->do_append); + + // then + ck_assert_int_eq (ret, FILE_CONT); + assert_file_equals (dst_path, expected, expected_len); + + g_free (expected); + g_free (dst); + g_free (src); +} +END_PARAMETRIZED_TEST + +/* --------------------------------------------------------------------------------------------- */ + +int +main (void) +{ + TCase *tc_core; + + tc_core = tcase_create ("Core"); + + tcase_add_checked_fixture (tc_core, setup, teardown); + tcase_set_timeout (tc_core, 60); + + // Add new tests here: *************** + mctest_add_parameterized_test (tc_core, test_copy, test_copy_ds); + // *********************************** + + return mctest_run_all (tc_core); +} + +/* --------------------------------------------------------------------------------------------- */ From cacf21c4bec89cc9749d0d25cd29f49561bc3a7c Mon Sep 17 00:00:00 2001 From: Phil Krylov Date: Sun, 27 Sep 2026 23:36:16 +0200 Subject: [PATCH 7/7] Enabled file cloning for temporary files created during VFS copy Signed-off-by: Phil Krylov --- lib/vfs/vfs.c | 8 ++++++- src/filemanager/file.c | 53 +++++++++++++++++++++++++++++++++--------- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/lib/vfs/vfs.c b/lib/vfs/vfs.c index c4af75fc8..0b84bcbc1 100644 --- a/lib/vfs/vfs.c +++ b/lib/vfs/vfs.c @@ -803,7 +803,13 @@ mc_copy_file_range_native (int src_fd, off_t *src_offset, int dest_fd, off_t *de gboolean vfs_cloning_supported (const vfs_path_t *src_vpath, const vfs_path_t *dst_vpath) { - return vfs_file_is_local (src_vpath) && vfs_file_is_local (dst_vpath); + if (!vfs_file_is_local (src_vpath)) + return FALSE; + if (vfs_file_is_local (dst_vpath)) + return TRUE; + if ((vfs_file_class_flags (dst_vpath) & VFSF_USETMP) != 0) + return TRUE; + return FALSE; } /* --------------------------------------------------------------------------------------------- */ diff --git a/src/filemanager/file.c b/src/filemanager/file.c index b297e0b01..b478dd27d 100644 --- a/src/filemanager/file.c +++ b/src/filemanager/file.c @@ -66,6 +66,7 @@ #include "lib/search.h" #include "lib/strutil.h" #include "lib/util.h" +#include "lib/vfs/xdirentry.h" #include "lib/vfs/vfs.h" #include "lib/vfs/utilvfs.h" #include "lib/widget.h" @@ -2617,6 +2618,8 @@ copy_file_file (file_op_context_t *ctx, const char *src_path, const char *dst_pa #endif #ifdef HAVE_FILE_CLONING_BY_RANGE + const gboolean dst_is_local = vfs_file_is_local (dst_vpath); + try_cloning = mc_global.vfs.file_cloning && vfs_cloning_supported (src_vpath, dst_vpath); #endif @@ -2625,10 +2628,17 @@ copy_file_file (file_op_context_t *ctx, const char *src_path, const char *dst_pa open_flags |= O_CREAT | O_EXCL; else if (ctx->do_append) #ifdef HAVE_FILE_CLONING_BY_RANGE + { + // Can't use non-O_APPEND mode for appending on arbitrary VFS implementations. Disable + // cloning in this case. + if (!dst_is_local) + try_cloning = FALSE; + // FICLONERANGE on Linux and copy_file_range(2) support block-aligned ranges for cloning, // but not in O_APPEND mode. Use O_WRONLY + mc_lseek instead as we don't care about // atomicity in our use cases. Non-local VFSes (ftpfs, shell) need O_APPEND to append. open_flags |= try_cloning ? 0 : O_APPEND; + } #else open_flags |= O_APPEND; #endif @@ -2726,24 +2736,35 @@ copy_file_file (file_op_context_t *ctx, const char *src_path, const char *dst_pa ssize_t (*copy_method) (int, off_t *, int, off_t *, size_t) = NULL; off_t src_offset = ctx->do_reget; + // In append and reget modes, the destination fd was positioned at its end above off_t dst_offset = appending ? dst_stat.st_size : 0; - void *local_src_fd = NULL; - void *local_dst_fd = NULL; + int local_src_fd = -1; + int local_dst_fd = -1; // Try to clone the initial chunk to choose a working copy_method if (try_cloning) { + void *src_fsinfo = NULL; + void *dst_fsinfo = NULL; + + // Obtain the source fd. The source is always local. + vfs_class_find_by_handle (src_desc, &src_fsinfo); + local_src_fd = *(int *) src_fsinfo; + + // Obtain the destination fd. The destination is either local or VFSF_USETMP. + vfs_class_find_by_handle (dest_desc, &dst_fsinfo); + local_dst_fd = + dst_is_local ? *(int *) dst_fsinfo : VFS_FILE_HANDLER (dst_fsinfo)->handle; + bufsize = 1 << 20; if ((off_t) bufsize > file_size - src_offset) bufsize = SSIZE_MAX; // don't try to read behind EOF - vfs_class_find_by_handle (src_desc, &local_src_fd); - vfs_class_find_by_handle (dest_desc, &local_dst_fd); + #ifdef HAVE_FICLONERANGE copy_method = mc_copy_file_range_ficlonerange; - n_copied = copy_method (*(int *) local_src_fd, &src_offset, *(int *) local_dst_fd, - &dst_offset, bufsize); + n_copied = copy_method (local_src_fd, &src_offset, local_dst_fd, &dst_offset, bufsize); #ifdef HAVE_COPY_FILE_RANGE if (n_copied < 0 && errno == EXDEV) #endif @@ -2751,11 +2772,21 @@ copy_file_file (file_op_context_t *ctx, const char *src_path, const char *dst_pa #ifdef HAVE_COPY_FILE_RANGE { copy_method = mc_copy_file_range_native; - n_copied = copy_method (*(int *) local_src_fd, &src_offset, *(int *) local_dst_fd, - &dst_offset, bufsize); + n_copied = + copy_method (local_src_fd, &src_offset, local_dst_fd, &dst_offset, bufsize); } #endif - if (n_copied < 0) + if (n_copied >= 0) + { + // We've found a working clone method. + // For a VFSF_USETMP destination, ensure the VFS changed flag is set, as it's + // required for the final file_store() call on mc_close(), and we can't be sure + // every VFS has set the changed flag to TRUE on mc_open() and we're not calling + // mc_write() that would set the flag on the normal copy path. + if (!dst_is_local) + VFS_FILE_HANDLER (dst_fsinfo)->changed = TRUE; + } + else copy_method = NULL; } @@ -2827,8 +2858,8 @@ copy_file_file (file_op_context_t *ctx, const char *src_path, const char *dst_pa if ((off_t) bufsize > n_rest) bufsize = SSIZE_MAX; // don't try to read behind EOF - n_copied = copy_method (*(int *) local_src_fd, &src_offset, *(int *) local_dst_fd, - &dst_offset, bufsize); + n_copied = + copy_method (local_src_fd, &src_offset, local_dst_fd, &dst_offset, bufsize); if (n_copied < 0) { return_status = ctx->ignore_all