On Wed, May 21, 2025 at 12:51:23PM +0000, Alec Brown wrote:
> Add functionality to compare alpha and numeric version segments for kernels.

I think this code applies not only for kernels.

> This can be useful in sorting newer from older kernels.

Where this code come from? Or maybe it is written from scratch. Anyway,
I think whatever it is it should be mentioned in the commit message.

> 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; })

You convert an enum to an int. Why? I think it can be simplified...

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

You can add GRUB_VERCMP_UNKNOWN/GRUB_VERCMP_ERR constant to "vercmp"
enum, return that type and drop ret from arguments. Then probably
GOTO_RETURN macro could be also dropped.

> +{
> +  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");
> +    }

The const in the arguments suggests you do not need these grub_strdup() calls.

> +  str1 = abuf;
> +  str2 = bbuf;
> +
> +  one = str1;
> +  two = str2;

Could not we reduce number of variables here? If not it should be
explained which one is used for what.

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

This could be calculated above and then you do not need to put NUL char
into strings.

> +          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);

grub_strncmp() is probably your friend here.

> +      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)

Same comments like for grub_vercmp().

> +{
> +  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");
> +    }

Do we really need these grub_strdup() calls?

> +  grub_split_package_string (str0, has_name ? &name0 : NULL, &version0, 
> &release0);

s/has_name/(has_name == true)/

> +  grub_split_package_string (str1, has_name ? &name1 : NULL, &version1, 
> &release1);

Ditto.

> +  if (has_name == true)
> +    {
> +      err = grub_vercmp (name0 == NULL ? "" : name0, name1 == NULL ? "" : 
> name1, ret);

This suggests that grub_vercmp() should treat NULLs as empty strings.
Then the code should be nicer and safer.

> +      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);

Ditto.

> +  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);

Ditto.

> +  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);

extern EXPORT_FUNC (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);

Do we really need to export these two functions? If yes then my comment
above applies here too.

Daniel

_______________________________________________
Grub-devel mailing list
Grub-devel@gnu.org
https://lists.gnu.org/mailman/listinfo/grub-devel

Reply via email to