Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/widget/wtools.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ char *input_dialog_help (const char *header, const char *text, const char *help,
char *input_expand_dialog (const char *header, const char *text, const char *history_name,
const char *def_text, input_complete_t completion_flags);

int query_dialog (const char *header, const char *text, int flags, int count, ...);
MC_MOCKABLE int query_dialog (const char *header, const char *text, int flags, int count, ...);
void query_set_sel (int new_sel);

/* Create message box but don't dismiss it yet, not background safe */
Expand Down
27 changes: 18 additions & 9 deletions src/filemanager/file.c
Original file line number Diff line number Diff line change
Expand Up @@ -2762,10 +2762,11 @@ copy_file_file (file_op_context_t *ctx, const char *src_path, const char *dst_pa

// src_read
if (mc_ctl (src_desc, VFS_CTL_IS_NOTREADY, 0) == 0)
while ((n_read = mc_read (src_desc, buf, bufsize)) < 0 && !ctx->ignore_all)
while ((n_read = mc_read (src_desc, buf, bufsize)) < 0)
{
return_status =
file_error (ctx, TRUE, _ ("Cannot read source file\n%s"), src_path);
return_status = ctx->ignore_all
? FILE_IGNORE_ALL
: file_error (ctx, TRUE, _ ("Cannot read source file\n%s"), src_path);
if (return_status == FILE_RETRY)
continue;
if (return_status == FILE_IGNORE_ALL)
Expand Down Expand Up @@ -2923,13 +2924,21 @@ copy_file_file (file_op_context_t *ctx, const char *src_path, const char *dst_pa

if (dst_status == DEST_SHORT_QUERY)
{
// Query to remove short file
if (query_dialog (Q_ ("DialogTitle|Copy"), _ ("Incomplete file was retrieved"), D_ERROR, 2,
_ ("&Delete"), _ ("&Keep"))
== 0)
dst_status = DEST_SHORT_DELETE;
if (ctx->ignore_all)
{
// "Ignore all" answers this question too, but never deletes pre-existing data
dst_status = appending ? DEST_SHORT_KEEP : DEST_SHORT_DELETE;
}
else
dst_status = DEST_SHORT_KEEP;
{
// Query to remove short file
if (query_dialog (Q_ ("DialogTitle|Copy"), _ ("Incomplete file was retrieved"), D_ERROR,
2, _ ("&Delete"), _ ("&Keep"))
== 0)
dst_status = DEST_SHORT_DELETE;
else
dst_status = DEST_SHORT_KEEP;
}
}

if (dst_status == DEST_SHORT_DELETE)
Expand Down
4 changes: 4 additions & 0 deletions tests/src/filemanager/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ endif

TESTS = \
cd_to \
copy_file_file \
examine_cd \
exec_get_export_variables_ext \
ext__exec_make_shell_string \
Expand All @@ -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

Expand Down
230 changes: 230 additions & 0 deletions tests/src/filemanager/copy_file_file.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
/*
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 <https://www.gnu.org/licenses/>.
*/

#define TEST_SUITE_NAME "/src/filemanager"

#include "tests/mctest.h"

#include <errno.h>

#include "src/vfs/local/local.c"

#include "src/filemanager/file.c"

/* --------------------------------------------------------------------------------------------- */

/* Give up injecting read errors after this many calls, so that a regression makes the test
fail instead of hanging forever */
#define READ_ERRORS_MAX 1000

static const char test_content[] = "Midnight Commander copy_file_file() test data\n";

static char *test_dir = NULL;
static char *src_path = NULL;
static char *dst_path = NULL;

static gboolean read_fails = FALSE;
static int read_calls = 0;
static int query_dialog_calls = 0;

/* --------------------------------------------------------------------------------------------- */

/* @Mock */
void
mc_refresh (void)
{
}

/* --------------------------------------------------------------------------------------------- */

/* @Mock */
void
message (int flags, const char *title, const char *text, ...)
{
(void) flags;
(void) title;
(void) text;
}

/* --------------------------------------------------------------------------------------------- */

/* @Mock */
int
query_dialog (const char *header, const char *text, int flags, int count, ...)
{
(void) header;
(void) text;
(void) flags;
(void) count;

query_dialog_calls++;

// "Incomplete file was retrieved": Delete
return 0;
}

/* --------------------------------------------------------------------------------------------- */

/* Fault injection at the VFS layer: fail every read of the local file system with EIO, like
reading a damaged sector does */
static ssize_t
test_local_read (void *data, char *buffer, size_t count)
{
read_calls++;

if (read_fails && read_calls <= READ_ERRORS_MAX)
{
errno = EIO;
return -1;
}

if (read_fails)
return 0;

return local_read (data, buffer, count);
}

/* --------------------------------------------------------------------------------------------- */

/* @Before */
static void
setup (void)
{
str_init_strings (NULL);

vfs_init ();
vfs_init_localfs ();
vfs_setup_work_dir ();

vfs_local_ops->read = test_local_read;

// no UI in tests
nice_rotating_dash = FALSE;
verbose = FALSE;
// force the read()/write() loop instead of reflinking the file
mc_global.vfs.file_cloning = FALSE;
mc_global.vfs.preallocate_space = FALSE;

test_dir = g_dir_make_tmp ("mc-test-copy_file_file-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);
ck_assert (g_file_set_contents (src_path, test_content, -1, NULL));

read_fails = FALSE;
read_calls = 0;
query_dialog_calls = 0;
}

/* --------------------------------------------------------------------------------------------- */

/* @After */
static void
teardown (void)
{
(void) unlink (dst_path);
(void) unlink (src_path);
(void) rmdir (test_dir);
g_free (dst_path);
g_free (src_path);
g_free (test_dir);

vfs_shut ();
str_uninit_strings ();
}

/* --------------------------------------------------------------------------------------------- */

/* @Test */
START_TEST (test_copy_file_file_ok)
{
// given
file_op_context_t *ctx;
FileProgressStatus status;
char *dst_content = NULL;

ctx = file_op_context_new (OP_COPY);

// when
status = copy_file_file (ctx, src_path, dst_path);

// then
ck_assert_int_eq (status, FILE_CONT);
ck_assert (g_file_get_contents (dst_path, &dst_content, NULL, NULL));
mctest_assert_str_eq (dst_content, test_content);
ck_assert_int_eq (query_dialog_calls, 0);

g_free (dst_content);
file_op_context_destroy (ctx);
}
END_TEST

/* --------------------------------------------------------------------------------------------- */

/* Ticket #4631: after "Ignore all", a read error must skip the file instead of retrying the
failing read forever */

/* @Test */
START_TEST (test_copy_file_file_read_error_ignore_all)
{
// given
file_op_context_t *ctx;
FileProgressStatus status;

ctx = file_op_context_new (OP_COPY);
ctx->ignore_all = TRUE;
read_fails = TRUE;

// when
status = copy_file_file (ctx, src_path, dst_path);

// then
ck_assert_int_eq (read_calls, 1);
ck_assert_int_eq (status, FILE_IGNORE_ALL);
ck_assert (ctx->ignore_all);
// the incomplete target file is deleted
ck_assert (!g_file_test (dst_path, G_FILE_TEST_EXISTS));

file_op_context_destroy (ctx);
}
END_TEST

/* --------------------------------------------------------------------------------------------- */

int
main (void)
{
TCase *tc_core;

tc_core = tcase_create ("Core");

tcase_add_checked_fixture (tc_core, setup, teardown);

// Add new tests here: ***************
tcase_add_test (tc_core, test_copy_file_file_ok);
tcase_add_test (tc_core, test_copy_file_file_read_error_ignore_all);
// ***********************************

return mctest_run_all (tc_core);
}

/* --------------------------------------------------------------------------------------------- */
Loading