diff --git a/lib/strutil.h b/lib/strutil.h index 9bbff6ca3..a091c25aa 100644 --- a/lib/strutil.h +++ b/lib/strutil.h @@ -538,6 +538,28 @@ const char *str_detect_termencoding (void); int str_verscmp (const char *s1, const char *s2); +/* Compare version strings: + + This function compares strings s1 and s2: + 1) By PREFIX in the same way as strcmp. + 2) Then by VERSION (most similarly to version compare of Debian's dpkg). + Leading zeros in version numbers are ignored. + 3) If both (PREFIX and VERSION) are equal, strcmp function is used for + comparison. So this function can return 0 if (and only if) strings s1 + and s2 are identical. + + It returns number > 0 for s1 > s2, 0 for s1 == s2 and number < 0 for s1 < s2. + + This function compares strings, in a way that if VER1 and VER2 are version + numbers and PREFIX and SUFFIX (SUFFIX defined as (\.[A-Za-z~][A-Za-z0-9~]*)*) + are strings then VER1 < VER2 implies filevercmp (PREFIX VER1 SUFFIX, + PREFIX VER2 SUFFIX) < 0. + + This function is intended to be a replacement for strverscmp. + */ +int filevercmp (const char *s1, const char *s2); + + /* return how many lines and columns will text occupy on terminal */ void str_msg_term_size (const char *text, int *lines, int *columns); diff --git a/lib/strutil/Makefile.am b/lib/strutil/Makefile.am index 07973c61b..5936a3664 100644 --- a/lib/strutil/Makefile.am +++ b/lib/strutil/Makefile.am @@ -1,6 +1,7 @@ noinst_LTLIBRARIES = libmcstrutil.la libmcstrutil_la_SOURCES = \ + filevercmp.c \ replace.c \ strescape.c \ strutil8bit.c \ diff --git a/lib/strutil/filevercmp.c b/lib/strutil/filevercmp.c new file mode 100644 index 000000000..a94fb6625 --- /dev/null +++ b/lib/strutil/filevercmp.c @@ -0,0 +1,232 @@ +/* + Copyright (C) 1995 Ian Jackson + Copyright (C) 2001 Anthony Towns + Copyright (C) 2008-2018 Free Software Foundation, Inc. + + This program 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. + + This program 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 . + */ + +#include + +#include +#include +#include +#include + +#include "lib/strutil.h" + +/*** global variables ****************************************************************************/ + +/*** file scope macro definitions ****************************************************************/ + +/*** file scope type declarations ****************************************************************/ + +/*** file scope variables ************************************************************************/ + +/* --------------------------------------------------------------------------------------------- */ +/*** file scope functions ************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +/* Match a file suffix defined by this regular expression: /(\.[A-Za-z~][A-Za-z0-9~]*)*$/ + * + * @str pointer to string to scan. + * + * @return pointer to the matching suffix, or NULL if not found. + * Upon return, @str points to terminating NUL. + */ +static const char * +match_suffix (const char **str) +{ + const char *match = NULL; + gboolean read_alpha = FALSE; + + while (**str != '\0') + { + if (read_alpha) + { + read_alpha = FALSE; + if (!g_ascii_isalpha (**str) && **str != '~') + match = NULL; + } + else if (**str == '.') + { + read_alpha = TRUE; + if (match == NULL) + match = *str; + } + else if (!g_ascii_isalnum (**str) && **str != '~') + match = NULL; + (*str)++; + } + + return match; +} + +/* --------------------------------------------------------------------------------------------- */ + +/* verrevcmp helper function */ +static int +order (unsigned char c) +{ + if (g_ascii_isdigit (c)) + return 0; + if (g_ascii_isalpha (c)) + return c; + if (c == '~') + return -1; + return (int) c + UCHAR_MAX + 1; +} + +/* --------------------------------------------------------------------------------------------- */ + +/* Slightly modified verrevcmp function from dpkg + * + * This implements the algorithm for comparison of version strings + * specified by Debian and now widely adopted. The detailed + * specification can be found in the Debian Policy Manual in the + * section on the 'Version' control field. This version of the code + * implements that from s5.6.12 of Debian Policy v3.8.0.1 + * https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version + * + * @s1 first string to compare + * @s1_len length of @s1 + * @s2 second string to compare + * @s2_len length of @s2 + * + * @return an integer less than, equal to, or greater than zero, if @s1 is <, == or > than @s2. + */ +static int +verrevcmp (const char *s1, size_t s1_len, const char *s2, size_t s2_len) +{ + size_t s1_pos = 0; + size_t s2_pos = 0; + + while (s1_pos < s1_len || s2_pos < s2_len) + { + int first_diff = 0; + + while ((s1_pos < s1_len && !g_ascii_isdigit (s1[s1_pos])) + || (s2_pos < s2_len && !g_ascii_isdigit (s2[s2_pos]))) + { + int s1_c = 0; + int s2_c = 0; + + if (s1_pos != s1_len) + s1_c = order (s1[s1_pos]); + if (s2_pos != s2_len) + s2_c = order (s2[s2_pos]); + + if (s1_c != s2_c) + return (s1_c - s2_c); + + s1_pos++; + s2_pos++; + } + + while (s1[s1_pos] == '0') + s1_pos++; + while (s2[s2_pos] == '0') + s2_pos++; + + while (g_ascii_isdigit (s1[s1_pos]) && g_ascii_isdigit (s2[s2_pos])) + { + if (first_diff == 0) + first_diff = s1[s1_pos] - s2[s2_pos]; + + s1_pos++; + s2_pos++; + } + + if (g_ascii_isdigit (s1[s1_pos])) + return 1; + if (g_ascii_isdigit (s2[s2_pos])) + return -1; + if (first_diff != 0) + return first_diff; + } + + return 0; +} + +/* --------------------------------------------------------------------------------------------- */ +/*** public functions ****************************************************************************/ +/* --------------------------------------------------------------------------------------------- */ + +/* Compare version strings. + * + * @s1 first string to compare + * @s2 second string to compare + * + * @return an integer less than, equal to, or greater than zero, if @s1 is <, == or > than @s2. + */ +int +filevercmp (const char *s1, const char *s2) +{ + const char *s1_pos, *s2_pos; + const char *s1_suffix, *s2_suffix; + size_t s1_len, s2_len; + int simple_cmp, result; + + /* easy comparison to see if strings are identical */ + simple_cmp = strcmp (s1, s2); + if (simple_cmp == 0) + return 0; + + /* special handle for "", "." and ".." */ + if (*s1 == '\0') + return -1; + if (*s2 == '\0') + return 1; + if (DIR_IS_DOT (s1)) + return -1; + if (DIR_IS_DOT (s2)) + return 1; + if (DIR_IS_DOTDOT (s1)) + return -1; + if (DIR_IS_DOTDOT (s2)) + return 1; + + /* special handle for other hidden files */ + if (*s1 == '.' && *s2 != '.') + return -1; + if (*s1 != '.' && *s2 == '.') + return 1; + if (*s1 == '.' && *s2 == '.') + { + s1++; + s2++; + } + + /* "cut" file suffixes */ + s1_pos = s1; + s2_pos = s2; + s1_suffix = match_suffix (&s1_pos); + s2_suffix = match_suffix (&s2_pos); + s1_len = (s1_suffix != NULL ? s1_suffix : s1_pos) - s1; + s2_len = (s2_suffix != NULL ? s2_suffix : s2_pos) - s2; + + /* restore file suffixes if strings are identical after "cut" */ + if ((s1_suffix != NULL || s2_suffix != NULL) && (s1_len == s2_len) + && strncmp (s1, s2, s1_len) == 0) + { + s1_len = s1_pos - s1; + s2_len = s2_pos - s2; + } + + result = verrevcmp (s1, s1_len, s2, s2_len); + + return result == 0 ? simple_cmp : result; +} + +/* --------------------------------------------------------------------------------------------- */ diff --git a/src/filemanager/dir.c b/src/filemanager/dir.c index 925942fc9..6474aa2f2 100644 --- a/src/filemanager/dir.c +++ b/src/filemanager/dir.c @@ -354,7 +354,7 @@ sort_vers (file_entry_t * a, file_entry_t * b) if (ad == bd || panels_options.mix_all_files) { - return str_verscmp (a->fname, b->fname) * reverse; + return filevercmp (a->fname, b->fname) * reverse; } else { diff --git a/tests/lib/strutil/Makefile.am b/tests/lib/strutil/Makefile.am index a8ec2dd30..52c1f1198 100644 --- a/tests/lib/strutil/Makefile.am +++ b/tests/lib/strutil/Makefile.am @@ -16,7 +16,8 @@ endif TESTS = \ replace__str_replace_all \ parse_integer \ - str_verscmp + str_verscmp \ + filevercmp check_PROGRAMS = $(TESTS) @@ -28,3 +29,6 @@ parse_integer_SOURCES = \ str_verscmp_SOURCES = \ str_verscmp.c + +filevercmp_SOURCES = \ + filevercmp.c diff --git a/tests/lib/strutil/filevercmp.c b/tests/lib/strutil/filevercmp.c new file mode 100644 index 000000000..9faeb3e19 --- /dev/null +++ b/tests/lib/strutil/filevercmp.c @@ -0,0 +1,296 @@ +/* + lib/strutil - tests for lib/strutil/fileverscmp function. + + Copyright (C) 2019 + Free Software Foundation, Inc. + + Written by: + Andrew Borodin , 2019 + + 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/strutil" + +#include "tests/mctest.h" + +#include "lib/strutil.h" + +/* --------------------------------------------------------------------------------------------- */ + +/* @Before */ +static void +setup (void) +{ +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @After */ +static void +teardown (void) +{ +} + +/* --------------------------------------------------------------------------------------------- */ + +static int +sign (int n) +{ + return ((n < 0) ? -1 : (n == 0) ? 0 : 1); +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @DataSource("filevercmp_test_ds1") */ +/* Testcases are taken from Gnulib */ +/* *INDENT-OFF* */ +static const struct filevercmp_test_struct +{ + const char *s1; + const char *s2; + int expected_result; +} filevercmp_test_ds1[] = +{ + { "", "", 0 }, + { "a", "a", 0 }, + { "a", "b", -1 }, + { "b", "a", 1 }, + { "a0", "a", 1 }, + { "00", "01", -1 }, + { "01", "010", -1 }, + { "9", "10", -1 }, + { "0a", "0", 1 } +}; +/* *INDENT-ON* */ + + +/* @Test(dataSource = "filevercmp_test_ds1") */ +/* *INDENT-OFF* */ +START_TEST (filevercmp_test1) +/* *INDENT-ON* */ +{ + /* given */ + int actual_result; + const struct filevercmp_test_struct *data = &filevercmp_test_ds1[_i]; + + /* when */ + actual_result = filevercmp (data->s1, data->s2); + + /* then */ + mctest_assert_int_eq (sign (actual_result), sign (data->expected_result)); +} +/* *INDENT-OFF* */ +END_TEST +/* *INDENT-ON* */ + +/* --------------------------------------------------------------------------------------------- */ + +/* @DataSource("filevercmp_test_ds2") */ +/* Testcases are taken from Gnulib */ +static const char *filevercmp_test_ds2[] = { + "", + ".", + "..", + ".0", + ".9", + ".A", + ".Z", + ".a~", + ".a", + ".b~", + ".b", + ".z", + ".zz~", + ".zz", + ".zz.~1~", + ".zz.0", + "0", + "9", + "A", + "Z", + "a~", + "a", + "a.b~", + "a.b", + "a.bc~", + "a.bc", + "b~", + "b", + "gcc-c++-10.fc9.tar.gz", + "gcc-c++-10.fc9.tar.gz.~1~", + "gcc-c++-10.fc9.tar.gz.~2~", + "gcc-c++-10.8.12-0.7rc2.fc9.tar.bz2", + "gcc-c++-10.8.12-0.7rc2.fc9.tar.bz2.~1~", + "glibc-2-0.1.beta1.fc10.rpm", + "glibc-common-5-0.2.beta2.fc9.ebuild", + "glibc-common-5-0.2b.deb", + "glibc-common-11b.ebuild", + "glibc-common-11-0.6rc2.ebuild", + "libstdc++-0.5.8.11-0.7rc2.fc10.tar.gz", + "libstdc++-4a.fc8.tar.gz", + "libstdc++-4.10.4.20040204svn.rpm", + "libstdc++-devel-3.fc8.ebuild", + "libstdc++-devel-3a.fc9.tar.gz", + "libstdc++-devel-8.fc8.deb", + "libstdc++-devel-8.6.2-0.4b.fc8", + "nss_ldap-1-0.2b.fc9.tar.bz2", + "nss_ldap-1-0.6rc2.fc8.tar.gz", + "nss_ldap-1.0-0.1a.tar.gz", + "nss_ldap-10beta1.fc8.tar.gz", + "nss_ldap-10.11.8.6.20040204cvs.fc10.ebuild", + "z", + "zz~", + "zz", + "zz.~1~", + "zz.0", + "#.b#" +}; + +const size_t filevercmp_test_ds2_len = G_N_ELEMENTS (filevercmp_test_ds2); + +/* @Test(dataSource = "filevercmp_test_ds2") */ +/* *INDENT-OFF* */ +START_TEST (filevercmp_test2) +/* *INDENT-ON* */ +{ + const char *i = filevercmp_test_ds2[_i]; + size_t _j; + + for (_j = 0; _j < filevercmp_test_ds2_len; _j++) + { + const char *j = filevercmp_test_ds2[_j]; + int result; + + result = filevercmp (i, j); + + if (result < 0) + ck_assert_int_eq (!!((size_t) _i < _j), 1); + else if (0 < result) + ck_assert_int_eq (!!(_j < (size_t) _i), 1); + else + ck_assert_int_eq (!!(_j == (size_t) _i), 1); + } +} +/* *INDENT-OFF* */ +END_TEST +/* *INDENT-ON* */ + + +/* @DataSource("filevercmp_test_ds3") */ +/* Ticket #3959 */ +static const char *filevercmp_test_ds3[] = { + "application-1.10.tar.gz", + "application-1.10.1.tar.gz" +}; + +const size_t filevercmp_test_ds3_len = G_N_ELEMENTS (filevercmp_test_ds3); + +/* @Test(dataSource = "filevercmp_test_ds3") */ +/* *INDENT-OFF* */ +START_TEST (filevercmp_test3) +/* *INDENT-ON* */ +{ + const char *i = filevercmp_test_ds3[_i]; + size_t _j; + + for (_j = 0; _j < filevercmp_test_ds3_len; _j++) + { + const char *j = filevercmp_test_ds3[_j]; + int result; + + result = filevercmp (i, j); + + if (result < 0) + ck_assert_int_eq (!!((size_t) _i < _j), 1); + else if (0 < result) + ck_assert_int_eq (!!(_j < (size_t) _i), 1); + else + ck_assert_int_eq (!!(_j == (size_t) _i), 1); + } +} +/* *INDENT-OFF* */ +END_TEST +/* *INDENT-ON* */ + + +/* @DataSource("filevercmp_test_ds4") */ +/* Ticket #3905 */ +static const char *filevercmp_test_ds4[] = { + "firefox-58.0.1+build1.tar.gz", + "firefox-59.0~b14+build1.tar.gz", + "firefox-59.0.1+build1.tar.gz" +}; + +const size_t filevercmp_test_ds4_len = G_N_ELEMENTS (filevercmp_test_ds4); + +/* @Test(dataSource = "filevercmp_test_ds4") */ +/* *INDENT-OFF* */ +START_TEST (filevercmp_test4) +/* *INDENT-ON* */ +{ + const char *i = filevercmp_test_ds4[_i]; + size_t _j; + + for (_j = 0; _j < filevercmp_test_ds4_len; _j++) + { + const char *j = filevercmp_test_ds4[_j]; + int result; + + result = filevercmp (i, j); + + if (result < 0) + ck_assert_int_eq (!!((size_t) _i < _j), 1); + else if (0 < result) + ck_assert_int_eq (!!(_j < (size_t) _i), 1); + else + ck_assert_int_eq (!!(_j == (size_t) _i), 1); + } +} +/* *INDENT-OFF* */ +END_TEST +/* *INDENT-ON* */ + +/* --------------------------------------------------------------------------------------------- */ + +int +main (void) +{ + int number_failed; + + Suite *s = suite_create (TEST_SUITE_NAME); + TCase *tc_core = tcase_create ("Core"); + SRunner *sr; + + tcase_add_checked_fixture (tc_core, setup, teardown); + + /* Add new tests here: *************** */ + mctest_add_parameterized_test (tc_core, filevercmp_test1, filevercmp_test_ds1); + tcase_add_loop_test (tc_core, filevercmp_test2, 0, filevercmp_test_ds2_len); + tcase_add_loop_test (tc_core, filevercmp_test3, 0, filevercmp_test_ds3_len); + tcase_add_loop_test (tc_core, filevercmp_test4, 0, filevercmp_test_ds4_len); + /* *********************************** */ + + suite_add_tcase (s, tc_core); + sr = srunner_create (s); + srunner_set_log (sr, "filevercmp.log"); + srunner_run_all (sr, CK_ENV); + number_failed = srunner_ntests_failed (sr); + srunner_free (sr); + return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +} + +/* --------------------------------------------------------------------------------------------- */