Add functionality to compare alpha and numeric version segments for kernels. This can be useful in sorting newer from older kernels.
Signed-off-by: Alec Brown <alec.r.br...@oracle.com> --- Makefile.util.def | 16 ++ grub-core/kern/vercmp.c | 316 +++++++++++++++++++++++++++++++++++++++ include/grub/vercmp.h | 35 +++++ tests/vercmp_unit_test.c | 65 ++++++++ 4 files changed, 432 insertions(+) create mode 100644 grub-core/kern/vercmp.c create mode 100644 include/grub/vercmp.h create mode 100644 tests/vercmp_unit_test.c diff --git a/Makefile.util.def b/Makefile.util.def index 038253b37..15be983f8 100644 --- a/Makefile.util.def +++ b/Makefile.util.def @@ -1373,6 +1373,22 @@ program = { ldadd = '$(LIBDEVMAPPER) $(LIBZFS) $(LIBNVPAIR) $(LIBGEOM)'; }; +program = { + testcase = native; + name = vercmp_unit_test; + common = tests/vercmp_unit_test.c; + common = tests/lib/unit_test.c; + common = grub-core/kern/list.c; + common = grub-core/kern/misc.c; + common = grub-core/tests/lib/test.c; + common = grub-core/kern/vercmp.c; + ldadd = libgrubmods.a; + ldadd = libgrubgcry.a; + ldadd = libgrubkern.a; + ldadd = grub-core/lib/gnulib/libgnu.a; + ldadd = '$(LIBDEVMAPPER) $(LIBZFS) $(LIBNVPAIR) $(LIBGEOM)'; +}; + program = { name = grub-menulst2cfg; mansection = 1; diff --git a/grub-core/kern/vercmp.c b/grub-core/kern/vercmp.c new file mode 100644 index 000000000..c2d69d7fd --- /dev/null +++ b/grub-core/kern/vercmp.c @@ -0,0 +1,316 @@ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2025 Free Software Foundation, Inc. + * + * GRUB 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. + * + * GRUB 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 GRUB. If not, see <http://www.gnu.org/licenses/>. + */ +#include <grub/misc.h> +#include <grub/mm.h> +#include <grub/err.h> +#include <grub/vercmp.h> + +#define GOTO_RETURN(x) ({ *ret = (x); goto finish; }) + +/* + * compare alpha and numeric segments of two versions + * return 1: a is newer than b + * 0: a and b are the same version + * -1: a is older than b + */ +grub_err_t +grub_vercmp (const char *a, const char *b, int *ret) +{ + char oldch1, oldch2; + char *abuf, *bbuf; + char *str1, *str2; + char *one, *two; + int rc; + bool isnum; + + if (ret == NULL) + return grub_error (GRUB_ERR_BAD_ARGUMENT, N_("return parameter is not set")); + *ret = 0; + + if (grub_strcmp (a, b) == 0) + return GRUB_ERR_NONE; + + abuf = grub_strdup (a); + if (abuf == NULL) + return grub_error (GRUB_ERR_OUT_OF_MEMORY, "couldn't duplicate string to compare versions"); + + bbuf = grub_strdup (b); + if (bbuf == NULL) + { + grub_free (abuf); + return grub_error (GRUB_ERR_OUT_OF_MEMORY, "couldn't duplicate string to compare versions"); + } + + str1 = abuf; + str2 = bbuf; + + one = str1; + two = str2; + + /* Loop through each version segment of str1 and str2 and compare them. */ + while (*one != '\0' || *two != '\0') + { + while (*one != '\0' && grub_isalnum (*one) == 0 && *one != '~' && *one != '+') + one++; + while (*two != '\0' && grub_isalnum (*two) == 0 && *two != '~' && *two != '+') + two++; + + /* Handle the tilde separator, it sorts before everything else. */ + if (*one == '~' || *two == '~') + { + if (*one != '~') + GOTO_RETURN (GRUB_VERCMP_NEWER); + if (*two != '~') + GOTO_RETURN (GRUB_VERCMP_OLDER); + one++; + two++; + continue; + } + + /* + * Handle the plus separator. Concept is the same as tilde, except that if + * one of the strings ends (base version), the other is considered as the + * higher version. + */ + if (*one == '+' || *two == '+') + { + if (*one == '\0') + GOTO_RETURN (GRUB_VERCMP_OLDER); + if (*two == '\0') + GOTO_RETURN (GRUB_VERCMP_NEWER); + if (*one != '+') + GOTO_RETURN (GRUB_VERCMP_NEWER); + if (*two != '+') + GOTO_RETURN (GRUB_VERCMP_OLDER); + one++; + two++; + continue; + } + + /* If we ran to the end of either, we are finished with the loop. */ + if (*one == '\0' || *two == '\0') + break; + + str1 = one; + str2 = two; + + /* + * Grab the first completely alpha or completely numeric segment. + * Leave one and two pointing to the start of the alpha or numeric + * segment and walk str1 and str2 to end of segment. + */ + if (grub_isdigit (*str1) == 1) + { + while (*str1 != '\0' && grub_isdigit (*str1) == 1) + str1++; + while (*str2 != '\0' && grub_isdigit (*str2) == 1) + str2++; + isnum = true; + } + else + { + while (*str1 != '\0' && grub_isalpha (*str1) == 1) + str1++; + while (*str2 != '\0' && grub_isalpha (*str2) == 1) + str2++; + isnum = false; + } + + /* + * Save the character at the end of the alpha or numeric segment so that + * they can be restored after the comparison. + */ + oldch1 = *str1; + *str1 = '\0'; + oldch2 = *str2; + *str2 = '\0'; + + /* + * This cannot happen, as we previously tested to make sure that + * the first string has a non-null segment. + */ + if (one == str1) + GOTO_RETURN (GRUB_VERCMP_OLDER); /* arbitrary */ + + /* + * Take care of the case where the two version segments are different + * types: one numeric, the other alpha (i.e. empty). Numeric segments are + * always newer than alpha segments. + */ + if (two == str2) + GOTO_RETURN (isnum ? GRUB_VERCMP_NEWER : GRUB_VERCMP_OLDER); + + if (isnum == true) + { + grub_size_t onelen, twolen; + /* + * This used to be done by converting the digit segments to ints using + * atoi() - it's changed because long digit segments can overflow an int - + * this should fix that. + */ + + /* Throw away any leading zeros - it's a number, right? */ + while (*one == '0') + one++; + while (*two == '0') + two++; + + /* Whichever number that has more digits wins. */ + onelen = grub_strlen (one); + twolen = grub_strlen (two); + if (onelen > twolen) + GOTO_RETURN (GRUB_VERCMP_NEWER); + if (twolen > onelen) + GOTO_RETURN (GRUB_VERCMP_OLDER); + } + + /* + * grub_strcmp will return which one is greater - even if the two segments + * are alpha or if they are numeric. Don't return if they are equal + * because there might be more segments to compare. + */ + rc = grub_strcmp (one, two); + if (rc != 0) + GOTO_RETURN (rc < 1 ? GRUB_VERCMP_OLDER : GRUB_VERCMP_NEWER); + + /* Restore the character that was replaced by null above. */ + *str1 = oldch1; + one = str1; + *str2 = oldch2; + two = str2; + } + + /* + * This catches the case where all alpha and numeric segments have compared + * identically but the segment separating characters were different. + */ + if (*one == '\0' && *two == '\0') + GOTO_RETURN (GRUB_VERCMP_SAME); + + /* Whichever version that still has characters left over wins. */ + if (*one == '\0') + GOTO_RETURN (GRUB_VERCMP_OLDER); + else + GOTO_RETURN (GRUB_VERCMP_NEWER); + + finish: + grub_free (abuf); + grub_free (bbuf); + return GRUB_ERR_NONE; +} + +/* + * returns name/version/release + * NULL string pointer returned if nothing is found + */ +void +grub_split_package_string (char *package_string, char **name, + char **version, char **release) +{ + char *package_version, *package_release; + + /* Release */ + package_release = grub_strrchr (package_string, '-'); + + if (package_release != NULL) + *package_release++ = '\0'; + + *release = package_release; + + if (name == NULL) + *version = package_string; + else + { + /* Version */ + package_version = grub_strrchr (package_string, '-'); + + if (package_version != NULL) + *package_version++ = '\0'; + + *version = package_version; + /* Name */ + *name = package_string; + } + + /* Bubble up non-null values from release to name */ + if (name != NULL && *name == NULL) + { + *name = (*version == NULL ? *release : *version); + *version = *release; + *release = NULL; + } + if (*version == NULL) + { + *version = *release; + *release = NULL; + } +} + +/* + * return 1: nvr0 is newer than nvr1 + * 0: nvr0 and nvr1 are the same version + * -1: nvr1 is newer than nvr0 + */ +grub_err_t +grub_split_vercmp (const char *nvr0, const char *nvr1, bool has_name, int *ret) +{ + grub_err_t err = GRUB_ERR_NONE; + char *str0, *name0, *version0, *release0; + char *str1, *name1, *version1, *release1; + + if (ret == NULL) + return grub_error (GRUB_ERR_BAD_ARGUMENT, N_("return parameter is not set")); + + str0 = grub_strdup (nvr0); + if (str0 == NULL) + return grub_error (GRUB_ERR_OUT_OF_MEMORY, "couldn't duplicate BLS filename to compare"); + str1 = grub_strdup (nvr1); + if (str1 == NULL) + { + grub_free (str0); + return grub_error (GRUB_ERR_OUT_OF_MEMORY, "couldn't duplicate BLS filename to compare"); + } + + grub_split_package_string (str0, has_name ? &name0 : NULL, &version0, &release0); + grub_split_package_string (str1, has_name ? &name1 : NULL, &version1, &release1); + + if (has_name == true) + { + err = grub_vercmp (name0 == NULL ? "" : name0, name1 == NULL ? "" : name1, ret); + if (*ret != 0 || err != GRUB_ERR_NONE) + { + grub_free (str0); + grub_free (str1); + return err; + } + } + + err = grub_vercmp (version0 == NULL ? "" : version0, version1 == NULL ? "" : version1, ret); + if (*ret != 0 || err != GRUB_ERR_NONE) + { + grub_free (str0); + grub_free (str1); + return err; + } + + err = grub_vercmp (release0 == NULL ? "" : release0, release1 == NULL ? "" : release1, ret); + + grub_free (str0); + grub_free (str1); + return err; +} diff --git a/include/grub/vercmp.h b/include/grub/vercmp.h new file mode 100644 index 000000000..e588ef530 --- /dev/null +++ b/include/grub/vercmp.h @@ -0,0 +1,35 @@ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2025 Free Software Foundation, Inc. + * + * GRUB 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. + * + * GRUB 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 GRUB. If not, see <http://www.gnu.org/licenses/>. + */ +#ifndef GRUB_VERCMP_H +#define GRUB_VERCMP_H 1 + +enum + { + GRUB_VERCMP_OLDER = -1, + GRUB_VERCMP_SAME = 0, + GRUB_VERCMP_NEWER = 1, + }; + +grub_err_t grub_vercmp (const char *a, const char *b, int *ret); + +void grub_split_package_string (char *package_string, char **name, + char **version, char **release); + +grub_err_t grub_split_vercmp (const char *nvr0, const char *nvr1, bool has_name, int *ret); + +#endif /* ! GRUB_VERCMP_H */ diff --git a/tests/vercmp_unit_test.c b/tests/vercmp_unit_test.c new file mode 100644 index 000000000..9eb6acefd --- /dev/null +++ b/tests/vercmp_unit_test.c @@ -0,0 +1,65 @@ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2025 Free Software Foundation, Inc. + * + * GRUB 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. + * + * GRUB 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 GRUB. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <string.h> +#include <stdlib.h> +#include <grub/test.h> +#include <grub/vercmp.h> + +#define MSG "vercmp test failed" + +static void +vercmp_test (void) +{ + const char *s1 = "name0-1.0.0-1.0.0.el8uek.x86_64"; + const char *s2 = "name0-1.0.0-1.1.0.el8uek.x86_64"; + const char *s3 = "name0-1.1.0-1.0.0.el8uek.x86_64"; + const char *s4 = "name1-1.0.0-1.0.0.el8uek.x86_64"; + const char *s5 = "version0"; + const char *s6 = "version1"; + char *nvr, *name, *version, *release; + int ret; + + nvr = strdup (s1); + + grub_split_package_string (nvr, &name, &version, &release); + grub_test_assert (strcmp (name, "name0") == 0, MSG); + grub_test_assert (strcmp (version, "1.0.0") == 0, MSG); + grub_test_assert (strcmp (release, "1.0.0.el8uek.x86_64") == 0, MSG); + free (nvr); + + grub_split_vercmp (s1, s1, true, &ret); + grub_test_assert (ret == GRUB_VERCMP_SAME, MSG); + grub_split_vercmp (s1, s2, true, &ret); + grub_test_assert (ret == GRUB_VERCMP_OLDER, MSG); + grub_split_vercmp (s1, s3, true, &ret); + grub_test_assert (ret == GRUB_VERCMP_OLDER, MSG); + grub_split_vercmp (s1, s4, true, &ret); + grub_test_assert (ret == GRUB_VERCMP_OLDER, MSG); + grub_split_vercmp (s2, s1, true, &ret); + grub_test_assert (ret == GRUB_VERCMP_NEWER, MSG); + + grub_vercmp (s5, s5, &ret); + grub_test_assert (ret == GRUB_VERCMP_SAME, MSG); + grub_vercmp (s5, s6, &ret); + grub_test_assert (ret == GRUB_VERCMP_OLDER, MSG); + grub_vercmp (s6, s5, &ret); + grub_test_assert (ret == GRUB_VERCMP_NEWER, MSG); +} + +GRUB_UNIT_TEST ("vercmp_unit_test", vercmp_test); -- 2.27.0 _______________________________________________ Grub-devel mailing list Grub-devel@gnu.org https://lists.gnu.org/mailman/listinfo/grub-devel