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); +} + +/* --------------------------------------------------------------------------------------------- */