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

Reply via email to