diff --git a/configure.ac b/configure.ac
index 5a5155f9e0..1d343f3f46 100644
--- a/configure.ac
+++ b/configure.ac
@@ -697,6 +697,7 @@ tests/lib/vfs/mc.charsets
tests/lib/widget/Makefile
tests/src/Makefile
tests/src/filemanager/Makefile
+tests/src/viewer/Makefile
tests/src/editor/Makefile
tests/src/editor/edit_complete_word_cmd_test_data.txt
tests/src/vfs/Makefile
diff --git a/lib/util.c b/lib/util.c
index 5ad50ab025..ac42512b9c 100644
--- a/lib/util.c
+++ b/lib/util.c
@@ -977,7 +977,7 @@ list_append_unique (GList *list, char *text)
* If there is no stored data, return line 1 and col 0.
*/
-void
+MC_MOCKABLE void
load_file_position (const vfs_path_t *filename_vpath, long *line, long *column, off_t *offset,
GArray **bookmarks)
{
diff --git a/lib/widget/wtools.c b/lib/widget/wtools.c
index 92f1b27522..d554d3a464 100644
--- a/lib/widget/wtools.c
+++ b/lib/widget/wtools.c
@@ -376,7 +376,7 @@ create_message (int flags, const char *title, const char *text, ...)
/* --------------------------------------------------------------------------------------------- */
/** Show message box, background safe */
-void
+MC_MOCKABLE void
message (int flags, const char *title, const char *text, ...)
{
char *p;
diff --git a/src/util.c b/src/util.c
index 6d164f4e96..599d41f4b2 100644
--- a/src/util.c
+++ b/src/util.c
@@ -81,7 +81,7 @@ check_for_default (const vfs_path_t *default_file_vpath, const vfs_path_t *file_
* @param file file name. Can be NULL.
*/
-void
+MC_MOCKABLE void
file_error_message (const char *format, const char *filename)
{
const char *error_string = unix_error_string (errno);
diff --git a/src/viewer/display.c b/src/viewer/display.c
index 1284b10f3b..20107a49b8 100644
--- a/src/viewer/display.c
+++ b/src/viewer/display.c
@@ -234,7 +234,7 @@ mcview_update (WView *view)
/* --------------------------------------------------------------------------------------------- */
/** Displays as much data from view->dpy_start as fits on the screen */
-void
+MC_MOCKABLE void
mcview_display (WView *view)
{
if (view->mode_flags.hex)
@@ -246,7 +246,7 @@ mcview_display (WView *view)
/* --------------------------------------------------------------------------------------------- */
-void
+MC_MOCKABLE void
mcview_compute_areas (WView *view)
{
WRect view_area;
@@ -302,7 +302,7 @@ mcview_compute_areas (WView *view)
/* --------------------------------------------------------------------------------------------- */
-void
+MC_MOCKABLE void
mcview_update_bytes_per_line (WView *view)
{
int cols = view->data_area.cols;
diff --git a/src/viewer/lib.c b/src/viewer/lib.c
index 5de9313c96..3b0139abd7 100644
--- a/src/viewer/lib.c
+++ b/src/viewer/lib.c
@@ -256,7 +256,7 @@ mcview_done (WView *view)
/* --------------------------------------------------------------------------------------------- */
-void
+MC_MOCKABLE void
mcview_set_codeset (WView *view)
{
const char *cp_id = NULL;
@@ -290,7 +290,7 @@ mcview_select_encoding (WView *view)
/* --------------------------------------------------------------------------------------------- */
-void
+MC_MOCKABLE void
mcview_show_error (WView *view, const char *format, const char *filename)
{
if (mcview_is_in_panel (view))
diff --git a/src/viewer/mcviewer.c b/src/viewer/mcviewer.c
index bef8977bc0..4344ce85ab 100644
--- a/src/viewer/mcviewer.c
+++ b/src/viewer/mcviewer.c
@@ -398,8 +398,7 @@ mcview_load (WView *view, const char *command, const char *file, int start_line,
if (fd1 == -1)
{
- mcview_close_datasource (view);
- mcview_show_error (view, _ ("Cannot open\n%s\nin parse mode\n%s"), file);
+ // VFS decompression failed -- display raw file content
}
else
{
diff --git a/tests/src/Makefile.am b/tests/src/Makefile.am
index abba5ad1f2..62c49148d1 100644
--- a/tests/src/Makefile.am
+++ b/tests/src/Makefile.am
@@ -1,6 +1,6 @@
PACKAGE_STRING = "/src"
-SUBDIRS = . filemanager vfs
+SUBDIRS = . filemanager vfs viewer
if USE_INTERNAL_EDIT
SUBDIRS += editor
diff --git a/tests/src/viewer/Makefile.am b/tests/src/viewer/Makefile.am
new file mode 100644
index 0000000000..43e323a9d8
--- /dev/null
+++ b/tests/src/viewer/Makefile.am
@@ -0,0 +1,24 @@
+PACKAGE_STRING = "/src/viewer"
+
+AM_CPPFLAGS = \
+ $(GLIB_CFLAGS) \
+ -I$(top_srcdir) \
+ -I$(top_srcdir)/lib/vfs \
+ -DTEST_SHARE_DIR=\"$(abs_srcdir)/../fixtures\" \
+ @CHECK_CFLAGS@
+
+LIBS = @CHECK_LIBS@ \
+ $(top_builddir)/src/libinternal.la \
+ $(top_builddir)/lib/libmc.la
+
+if ENABLE_MCLIB
+LIBS += $(GLIB_LIBS)
+endif
+
+TESTS = \
+ mcview__load_zip_magic
+
+check_PROGRAMS = $(TESTS)
+
+mcview__load_zip_magic_SOURCES = \
+ mcview__load_zip_magic.c
diff --git a/tests/src/viewer/mcview__load_zip_magic.c b/tests/src/viewer/mcview__load_zip_magic.c
new file mode 100644
index 0000000000..123c811fbf
--- /dev/null
+++ b/tests/src/viewer/mcview__load_zip_magic.c
@@ -0,0 +1,337 @@
+/*
+ src/viewer - tests for mcview_load() with PK/ZIP magic bytes
+
+ Copyright (C) 2025
+ 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/viewer"
+
+#include "tests/mctest.h"
+
+#include
+
+#include "lib/strutil.h"
+#include "lib/vfs/vfs.h"
+#include "src/vfs/local/local.c"
+#include "src/util.h"
+
+#include "src/viewer/internal.h"
+
+/* --------------------------------------------------------------------------------------------- */
+/* Stubs for symbols pulled in transitively from editor/diffviewer/tty.
+ * These are never actually called in our tests, but the linker needs them
+ * because libinternal.a bundles all of mc's subsystems together. */
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wmissing-prototypes"
+
+extern int mc_skin_color__cache[64];
+int mc_skin_color__cache[64];
+
+extern void *mc_editor_plugin_list;
+void *mc_editor_plugin_list = NULL;
+
+extern int we_are_strstrstrbackground;
+int we_are_strstrstrbackground = 0;
+
+void tty_lowlevel_setcolor (MC_UNUSED int color) {}
+void mc_editor_plugin_add (MC_UNUSED void *p) {}
+void mc_editor_plugins_load (void) {}
+int button_get_width (MC_UNUSED void *b) { return 0; }
+
+#pragma GCC diagnostic pop
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @CapturedValue */
+static int mcview_show_error__call_count;
+
+/* mock: mcview_compute_areas -- no-op in test (depends on widget geometry) */
+void
+mcview_compute_areas (MC_UNUSED WView *view)
+{
+}
+
+/* mock: mcview_update_bytes_per_line -- no-op in test (depends on display areas) */
+void
+mcview_update_bytes_per_line (MC_UNUSED WView *view)
+{
+}
+
+/* mock: mcview_set_codeset -- no-op in test (depends on charset subsystem) */
+void
+mcview_set_codeset (MC_UNUSED WView *view)
+{
+}
+
+/* mock: mcview_display -- no-op in test (depends on tty subsystem) */
+void
+mcview_display (MC_UNUSED WView *view)
+{
+}
+
+/* mock: mcview_show_error -- capture call count instead of showing dialog */
+void
+mcview_show_error (MC_UNUSED WView *view, MC_UNUSED const char *format, MC_UNUSED const char *filename)
+{
+ mcview_show_error__call_count++;
+}
+
+/* mock: file_error_message -- no-op in test */
+void
+file_error_message (MC_UNUSED const char *format, MC_UNUSED const char *filename)
+{
+}
+
+/* mock: load_file_position -- no-op in test */
+void
+load_file_position (MC_UNUSED const vfs_path_t *filename_vpath, MC_UNUSED long *line,
+ MC_UNUSED long *column, MC_UNUSED off_t *offset, MC_UNUSED GArray **bookmarks)
+{
+}
+
+/* mock: message -- no-op in test */
+void
+message (MC_UNUSED int flags, MC_UNUSED const char *title, MC_UNUSED const char *text, ...)
+{
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+static char *
+create_test_file (const unsigned char *data, size_t size)
+{
+ char *tmp_path = NULL;
+ GError *error = NULL;
+ int fd;
+
+ fd = g_file_open_tmp ("mc-test-viewer-XXXXXX", &tmp_path, &error);
+ if (fd == -1)
+ {
+ if (error != NULL)
+ g_error_free (error);
+ return NULL;
+ }
+
+ if (size > 0)
+ {
+ write (fd, data, size);
+ }
+
+ close (fd);
+ return tmp_path;
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+static WView test_view;
+
+/* @Before */
+static void
+setup (void)
+{
+ str_init_strings (NULL);
+ vfs_init ();
+ vfs_init_localfs ();
+ vfs_setup_work_dir ();
+
+ memset (&test_view, 0, sizeof (test_view));
+ mcview_init (&test_view);
+ test_view.mode_flags.magic = TRUE;
+
+ mcview_show_error__call_count = 0;
+}
+
+/* @After */
+static void
+teardown (void)
+{
+ mcview_close_datasource (&test_view);
+ vfs_path_free (test_view.filename_vpath, TRUE);
+ test_view.filename_vpath = NULL;
+ vfs_path_free (test_view.workdir_vpath, TRUE);
+ test_view.workdir_vpath = NULL;
+ g_free (test_view.command);
+ test_view.command = NULL;
+
+ vfs_shut ();
+ str_uninit_strings ();
+}
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @Test: file with PK/ZIP magic bytes should load successfully as DS_FILE */
+START_TEST (test_zip_magic_file_loads_as_ds_file)
+{
+ // given -- a file that starts with PK\x03\x04 (ZIP local file header)
+ const unsigned char zip_header[] = {
+ 'P', 'K', 0x03, 0x04, 0x14, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ };
+ char *tmp_path = create_test_file (zip_header, sizeof (zip_header));
+ ck_assert_msg (tmp_path != NULL, "Failed to create temp file");
+
+ // when
+ gboolean result = mcview_load (&test_view, NULL, tmp_path, 0, 0, 0);
+
+ // then -- should succeed and set DS_FILE datasource
+ ck_assert_msg (result == TRUE, "mcview_load should return TRUE for ZIP-magic file");
+ ck_assert_int_eq (test_view.datasource, DS_FILE);
+ ck_assert_int_eq (mcview_show_error__call_count, 0);
+
+ // cleanup
+ unlink (tmp_path);
+ g_free (tmp_path);
+}
+END_TEST
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @Test: reloading ZIP-magic file should not crash (regression for segfault on second F3) */
+START_TEST (test_zip_magic_file_reload_no_crash)
+{
+ // given -- a file with ZIP magic
+ const unsigned char zip_header[] = {
+ 'P', 'K', 0x03, 0x04, 0x14, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ };
+ char *tmp_path = create_test_file (zip_header, sizeof (zip_header));
+ ck_assert_msg (tmp_path != NULL, "Failed to create temp file");
+
+ // when -- load, cleanup, reinit, load again (simulates two F3 presses)
+ gboolean result1 = mcview_load (&test_view, NULL, tmp_path, 0, 0, 0);
+ ck_assert_msg (result1 == TRUE, "First mcview_load should return TRUE");
+ ck_assert_int_eq (test_view.datasource, DS_FILE);
+
+ // simulate viewer close + reinit (like mcview_done + mcview_init)
+ mcview_close_datasource (&test_view);
+ vfs_path_free (test_view.filename_vpath, TRUE);
+ test_view.filename_vpath = NULL;
+ vfs_path_free (test_view.workdir_vpath, TRUE);
+ test_view.workdir_vpath = NULL;
+ g_free (test_view.command);
+ test_view.command = NULL;
+ mcview_init (&test_view);
+ test_view.mode_flags.magic = TRUE;
+
+ // second load -- should not crash
+ gboolean result2 = mcview_load (&test_view, NULL, tmp_path, 0, 0, 0);
+
+ // then
+ ck_assert_msg (result2 == TRUE, "Second mcview_load should return TRUE");
+ ck_assert_int_eq (test_view.datasource, DS_FILE);
+ ck_assert_int_eq (mcview_show_error__call_count, 0);
+
+ // cleanup
+ unlink (tmp_path);
+ g_free (tmp_path);
+}
+END_TEST
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @Test: normal file (no magic) should load as DS_FILE */
+START_TEST (test_normal_file_loads_as_ds_file)
+{
+ // given -- a plain text file
+ const unsigned char text_data[] = "Hello, World!\nThis is a test file.\n";
+ char *tmp_path = create_test_file (text_data, sizeof (text_data) - 1);
+ ck_assert_msg (tmp_path != NULL, "Failed to create temp file");
+
+ // when
+ gboolean result = mcview_load (&test_view, NULL, tmp_path, 0, 0, 0);
+
+ // then
+ ck_assert_msg (result == TRUE, "mcview_load should return TRUE for normal file");
+ ck_assert_int_eq (test_view.datasource, DS_FILE);
+ ck_assert_int_eq (mcview_show_error__call_count, 0);
+
+ // cleanup
+ unlink (tmp_path);
+ g_free (tmp_path);
+}
+END_TEST
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @Test: nonexistent file should fail to load */
+START_TEST (test_nonexistent_file_fails)
+{
+ // when
+ gboolean result = mcview_load (&test_view, NULL, "/nonexistent/file/path", 0, 0, 0);
+
+ // then
+ ck_assert_msg (result == FALSE, "mcview_load should return FALSE for nonexistent file");
+ ck_assert_int_eq (test_view.datasource, DS_NONE);
+}
+END_TEST
+
+/* --------------------------------------------------------------------------------------------- */
+
+/* @Test: gzip magic should also load successfully when VFS decompression fails */
+START_TEST (test_gzip_magic_file_loads_as_ds_file)
+{
+ // given -- a file that starts with gzip magic (\x1f\x8b) but is not actually valid gzip
+ const unsigned char gzip_header[] = {
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ };
+ char *tmp_path = create_test_file (gzip_header, sizeof (gzip_header));
+ ck_assert_msg (tmp_path != NULL, "Failed to create temp file");
+
+ // when
+ gboolean result = mcview_load (&test_view, NULL, tmp_path, 0, 0, 0);
+
+ // then
+ ck_assert_msg (result == TRUE, "mcview_load should return TRUE for gzip-magic file");
+ ck_assert_int_eq (test_view.datasource, DS_FILE);
+ ck_assert_int_eq (mcview_show_error__call_count, 0);
+
+ // cleanup
+ unlink (tmp_path);
+ g_free (tmp_path);
+}
+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_zip_magic_file_loads_as_ds_file);
+ tcase_add_test (tc_core, test_zip_magic_file_reload_no_crash);
+ tcase_add_test (tc_core, test_normal_file_loads_as_ds_file);
+ tcase_add_test (tc_core, test_nonexistent_file_fails);
+ tcase_add_test (tc_core, test_gzip_magic_file_loads_as_ds_file);
+ /* *********************************** */
+
+ return mctest_run_all (tc_core);
+}
+
+/* --------------------------------------------------------------------------------------------- */