Fix file version sort.

Use filevercmp() instead of str_verscmp().
Source code of filevercmp() is taken from Gnulib.

Signed-off-by: Andrew Borodin <aborodin@vmail.ru>
This commit is contained in:
Andrew Borodin 2019-02-20 15:39:13 +03:00
parent 2163009270
commit a2a5fa017e
6 changed files with 557 additions and 2 deletions

View File

@ -538,6 +538,28 @@ const char *str_detect_termencoding (void);
int str_verscmp (const char *s1, const char *s2); 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 /* return how many lines and columns will text occupy on terminal
*/ */
void str_msg_term_size (const char *text, int *lines, int *columns); void str_msg_term_size (const char *text, int *lines, int *columns);

View File

@ -1,6 +1,7 @@
noinst_LTLIBRARIES = libmcstrutil.la noinst_LTLIBRARIES = libmcstrutil.la
libmcstrutil_la_SOURCES = \ libmcstrutil_la_SOURCES = \
filevercmp.c \
replace.c \ replace.c \
strescape.c \ strescape.c \
strutil8bit.c \ strutil8bit.c \

232
lib/strutil/filevercmp.c Normal file
View File

@ -0,0 +1,232 @@
/*
Copyright (C) 1995 Ian Jackson <iwj10@cus.cam.ac.uk>
Copyright (C) 2001 Anthony Towns <aj@azure.humbug.org.au>
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 <https://www.gnu.org/licenses/>.
*/
#include <config.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#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;
}
/* --------------------------------------------------------------------------------------------- */

View File

@ -354,7 +354,7 @@ sort_vers (file_entry_t * a, file_entry_t * b)
if (ad == bd || panels_options.mix_all_files) if (ad == bd || panels_options.mix_all_files)
{ {
return str_verscmp (a->fname, b->fname) * reverse; return filevercmp (a->fname, b->fname) * reverse;
} }
else else
{ {

View File

@ -16,7 +16,8 @@ endif
TESTS = \ TESTS = \
replace__str_replace_all \ replace__str_replace_all \
parse_integer \ parse_integer \
str_verscmp str_verscmp \
filevercmp
check_PROGRAMS = $(TESTS) check_PROGRAMS = $(TESTS)
@ -28,3 +29,6 @@ parse_integer_SOURCES = \
str_verscmp_SOURCES = \ str_verscmp_SOURCES = \
str_verscmp.c str_verscmp.c
filevercmp_SOURCES = \
filevercmp.c

View File

@ -0,0 +1,296 @@
/*
lib/strutil - tests for lib/strutil/fileverscmp function.
Copyright (C) 2019
Free Software Foundation, Inc.
Written by:
Andrew Borodin <aborodin@vmail.ru>, 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 <http://www.gnu.org/licenses/>.
*/
#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;
}
/* --------------------------------------------------------------------------------------------- */