The BootLoaderSpec (BLS) defines a scheme where different bootloaders can
share a format for boot items and a configuration directory that accepts
these common configurations as drop-in files.

Signed-off-by: Peter Jones <pjo...@redhat.com>
Signed-off-by: Javier Martinez Canillas <javi...@redhat.com>
Signed-off-by: Will Thompson <w...@endlessm.com>
Signed-off-by: Alec Brown <alec.r.br...@oracle.com>
---
 Makefile.util.def              |   16 +
 docs/grub.texi                 |   29 +
 grub-core/Makefile.core.def    |   10 +
 grub-core/commands/blsuki.c    | 1057 ++++++++++++++++++++++++++++++++
 grub-core/commands/legacycfg.c |    4 +-
 grub-core/commands/menuentry.c |    8 +-
 grub-core/lib/vercmp.c         |  317 ++++++++++
 grub-core/normal/main.c        |    6 +
 include/grub/lib/vercmp.h      |   35 ++
 include/grub/menu.h            |   15 +
 include/grub/normal.h          |    2 +-
 tests/vercmp_unit_test.c       |   65 ++
 12 files changed, 1558 insertions(+), 6 deletions(-)
 create mode 100644 grub-core/commands/blsuki.c
 create mode 100644 grub-core/lib/vercmp.c
 create mode 100644 include/grub/lib/vercmp.h
 create mode 100644 tests/vercmp_unit_test.c

diff --git a/Makefile.util.def b/Makefile.util.def
index 038253b37..a911f2e0a 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/lib/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/docs/grub.texi b/docs/grub.texi
index d9b26fa36..ea7a13953 100644
--- a/docs/grub.texi
+++ b/docs/grub.texi
@@ -6417,6 +6417,7 @@ you forget a command, you can run the command 
@command{help}
 * background_image::            Load background image for active terminal
 * badram::                      Filter out bad regions of RAM
 * blocklist::                   Print a block list
+* blscfg::                      Load Boot Loader Specification menu entries
 * boot::                        Start up your operating system
 * cat::                         Show the contents of a file
 * clear::                       Clear the screen
@@ -6603,6 +6604,34 @@ Print a block list (@pxref{Block list syntax}) for 
@var{file}.
 @end deffn
 
 
+@node blscfg
+@subsection blscfg
+
+@deffn Command blscfg [@option{--path} dir] [@option{--enable-fallback}] 
[@option{--show-default}] [@option{--show-non-default}] [@option{--entry} file]
+Load Boot Loader Specification entries into the GRUB menu.
+
+The @option{--path} option overrides the default path to the directory 
containing
+the BLS entries. If this option isn't used, the default location is
+/loader/entries in @code{$BOOT}. If no BLS entries are found, the
+@option{--enable-fallback} option can be used to check for entries in the 
default
+directory.
+
+The @option{--show-default} option allows the default boot entry to be added 
to the
+GRUB menu from the BLS entries.
+
+The @option{--show-non-default} option allows non-default boot entries to be 
added to
+the GRUB menu from the BLS entries.
+
+The @option{--entry} option allows specific boot entries to be added to the 
GRUB menu
+from the BLS entries.
+
+The @option{--entry}, @option{--show-default}, and @option{--show-non-default} 
options
+are used to filter which BLS entries are added to the GRUB menu. If none are
+used, all entries in the default location or the location specified by 
@option{--path}
+will be added to the GRUB menu.
+@end deffn
+
+
 @node boot
 @subsection boot
 
diff --git a/grub-core/Makefile.core.def b/grub-core/Makefile.core.def
index f70e02e69..f3b776c0a 100644
--- a/grub-core/Makefile.core.def
+++ b/grub-core/Makefile.core.def
@@ -845,6 +845,16 @@ module = {
   common = commands/blocklist.c;
 };
 
+module = {
+  name = blsuki;
+  common = commands/blsuki.c;
+  common = lib/vercmp.c;
+  enable = powerpc_ieee1275;
+  enable = efi;
+  enable = i386_pc;
+  enable = emu;
+};
+
 module = {
   name = boot;
   common = commands/boot.c;
diff --git a/grub-core/commands/blsuki.c b/grub-core/commands/blsuki.c
new file mode 100644
index 000000000..0fb4f870a
--- /dev/null
+++ b/grub-core/commands/blsuki.c
@@ -0,0 +1,1057 @@
+/*
+ *  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/list.h>
+#include <grub/types.h>
+#include <grub/misc.h>
+#include <grub/mm.h>
+#include <grub/err.h>
+#include <grub/dl.h>
+#include <grub/extcmd.h>
+#include <grub/i18n.h>
+#include <grub/fs.h>
+#include <grub/env.h>
+#include <grub/file.h>
+#include <grub/normal.h>
+#include <grub/safemath.h>
+#include <grub/lib/envblk.h>
+#include <grub/lib/vercmp.h>
+
+GRUB_MOD_LICENSE ("GPLv3+");
+
+#define GRUB_BLS_CONFIG_PATH "/loader/entries/"
+
+static const struct grub_arg_option bls_opt[] =
+  {
+    {"path", 'p', 0, "Specify path to find BLS entries.", N_("DIR"), 
ARG_TYPE_PATHNAME},
+    {"enable-fallback", 'f', 0, "Fallback to the default BLS path if --path 
fails to find BLS entries.", 0, ARG_TYPE_NONE},
+    {"show-default", 'd', 0, "Allow the default BLS entry to be added to the 
GRUB menu.", 0, ARG_TYPE_NONE},
+    {"show-non-default", 'n', 0, "Allow the non-default BLS entries to be 
added to the GRUB menu.", 0, ARG_TYPE_NONE},
+    {"entry", 'e', 0, "Allow specific BLS entries to be added to the GRUB 
menu.", N_("FILE"), ARG_TYPE_FILE},
+    {0, 0, 0, 0, 0, 0}
+  };
+
+struct keyval
+{
+  const char *key;
+  char *val;
+};
+
+static grub_blsuki_entry_t *entries = NULL;
+
+#define FOR_BLSUKI_ENTRIES(var) FOR_LIST_ELEMENTS (var, entries)
+
+static grub_err_t
+blsuki_add_keyval (grub_blsuki_entry_t *entry, char *key, char *val)
+{
+  char *k, *v;
+  struct keyval **kvs, *kv;
+  grub_size_t size;
+  int new_n = entry->nkeyvals + 1;
+
+  if (entry->keyvals_size == 0)
+    {
+      size = sizeof (struct keyval *);
+      kvs = grub_malloc (size);
+      if (kvs == NULL)
+       return grub_error (GRUB_ERR_OUT_OF_MEMORY, "couldn't allocate space for 
BLS key values");
+
+      entry->keyvals = kvs;
+      entry->keyvals_size = size;
+    }
+  else if (entry->keyvals_size < new_n * sizeof (struct keyval *))
+    {
+      size = entry->keyvals_size * 2;
+      kvs = grub_realloc (entry->keyvals, size);
+      if (kvs == NULL)
+       return grub_error (GRUB_ERR_OUT_OF_MEMORY, "couldn't reallocate space 
for BLS key values");
+
+      entry->keyvals = kvs;
+      entry->keyvals_size = size;
+    }
+
+  kv = grub_malloc (sizeof (struct keyval));
+  if (kv == NULL)
+    return grub_error (GRUB_ERR_OUT_OF_MEMORY, "couldn't find space for new 
BLS key value");
+
+  k = grub_strdup (key);
+  if (k == NULL)
+    {
+      grub_free (kv);
+      return grub_error (GRUB_ERR_OUT_OF_MEMORY, "couldn't find space for new 
BLS key value");
+    }
+
+  v = grub_strdup (val);
+  if (v == NULL)
+    {
+      grub_free (k);
+      grub_free (kv);
+      return grub_error (GRUB_ERR_OUT_OF_MEMORY, "couldn't find space for new 
BLS key value");
+    }
+
+  kv->key = k;
+  kv->val = v;
+
+  entry->keyvals[entry->nkeyvals] = kv;
+  entry->nkeyvals = new_n;
+
+  return GRUB_ERR_NONE;
+}
+
+/*
+ * Find the value of the key named by keyname.  If there are allowed to be
+ * more than one, pass a pointer to an int set to -1 the first time, and pass
+ * the same pointer through each time after, and it'll return them in sorted
+ * order as defined in the BLS fragment file.
+ */
+static char *
+blsuki_get_val (grub_blsuki_entry_t *entry, const char *keyname, int *last)
+{
+  int idx, start = 0;
+  struct keyval *kv = NULL;
+  char *ret = NULL;
+
+  if (last != NULL)
+    start = *last + 1;
+
+  for (idx = start; idx < entry->nkeyvals; idx++)
+    {
+      kv = entry->keyvals[idx];
+
+      if (grub_strcmp (keyname, kv->key) == 0)
+       {
+         ret = kv->val;
+         break;
+       }
+    }
+
+  if (last != NULL)
+    {
+      if (idx == entry->nkeyvals)
+       *last = -1;
+      else
+       *last = idx;
+    }
+
+  return ret;
+}
+
+static grub_err_t
+blsuki_add_entry (grub_blsuki_entry_t *entry)
+{
+  grub_blsuki_entry_t *e, *last = NULL;
+  grub_err_t err;
+  int rc;
+
+  if (entries == NULL)
+    {
+      grub_dprintf ("blsuki", "Add entry with id \"%s\"\n", entry->filename);
+      entries = entry;
+      return GRUB_ERR_NONE;
+    }
+
+  FOR_BLSUKI_ENTRIES (e)
+    {
+      err = grub_split_vercmp (entry->filename, e->filename, true, &rc);
+      if (err != GRUB_ERR_NONE)
+       return err;
+
+      if (rc == GRUB_VERCMP_SAME)
+       return grub_error (GRUB_ERR_BAD_ARGUMENT, "duplicate file");
+
+      if (rc == GRUB_VERCMP_NEWER)
+       {
+         grub_dprintf ("blsuki", "Add entry with id \"%s\"\n", 
entry->filename);
+         grub_list_push (GRUB_AS_LIST_P (&e), GRUB_AS_LIST (entry));
+         if (entry->next == entries)
+           {
+             entries = entry;
+             entry->prev = NULL;
+           }
+         else
+           last->next = entry;
+         return GRUB_ERR_NONE;
+       }
+      last = e;
+    }
+
+  if (last != NULL)
+    {
+      grub_dprintf ("blsuki", "Add entry with id \"%s\"\n", entry->filename);
+      last->next = entry;
+      entry->prev = &last;
+    }
+
+  return GRUB_ERR_NONE;
+}
+
+static grub_err_t
+bls_read_entry (grub_file_t f, grub_blsuki_entry_t *entry)
+{
+  grub_err_t err = GRUB_ERR_NONE;
+
+  for (;;)
+    {
+      char *buf;
+      char *separator;
+
+      buf = grub_file_getline (f);
+      if (buf == NULL)
+       break;
+
+      while (buf != NULL && buf[0] != '\0' && (buf[0] == ' ' || buf[0] == 
'\t'))
+       buf++;
+      if (buf[0] == '#')
+       {
+         grub_free (buf);
+         continue;
+       }
+
+      separator = grub_strchr (buf, ' ');
+
+      if (separator == NULL)
+       separator = grub_strchr (buf, '\t');
+
+      if (separator == NULL || separator[1] == '\0')
+       {
+         grub_free (buf);
+         break;
+       }
+
+      separator[0] = '\0';
+
+      do
+       {
+         separator++;
+       }
+      while (*separator == ' ' || *separator == '\t');
+
+      err = blsuki_add_keyval (entry, buf, separator);
+      grub_free (buf);
+      if (err != GRUB_ERR_NONE)
+       break;
+    }
+
+  return err;
+}
+
+struct read_entry_info
+{
+  const char *devid;
+  const char *dirname;
+  grub_file_t file;
+};
+
+static int
+blsuki_read_entry (const char *filename,
+                  const struct grub_dirhook_info *dirhook_info __attribute__ 
((__unused__)),
+                  void *data)
+{
+  grub_size_t m = 0, n, ext_len = 5;
+  grub_err_t err;
+  char *p = NULL;
+  grub_file_t f = NULL;
+  grub_blsuki_entry_t *entry;
+  struct read_entry_info *info = (struct read_entry_info *) data;
+
+  grub_dprintf ("blsuki", "filename: \"%s\"\n", filename);
+
+  n = grub_strlen (filename);
+
+  if (info->file != NULL)
+    {
+      f = info->file;
+    }
+  else
+    {
+      if (filename[0] == '.')
+       return 0;
+
+      if (n <= 5)
+       return 0;
+
+      if (grub_strcmp (filename + n - ext_len, ".conf") != 0)
+       return 0;
+
+      p = grub_xasprintf ("(%s)%s/%s", info->devid, info->dirname, filename);
+
+      f = grub_file_open (p, GRUB_FILE_TYPE_CONFIG);
+      grub_free (p);
+      if (f == NULL)
+       goto finish;
+    }
+
+  entry = grub_zalloc (sizeof (*entry));
+  if (entry == NULL)
+    goto finish;
+
+  if (info->file != NULL)
+    {
+      char *slash;
+
+      slash = grub_strrchr (filename, '/');
+      if (slash == NULL)
+       slash = grub_strrchr (filename, '\\');
+
+      if (slash != NULL)
+       {
+         while (*slash == '/' || *slash == '\\')
+           slash++;
+
+         m = slash - filename;
+       }
+      else
+       m = 0;
+    }
+  n -= m;
+
+  entry->filename = grub_strndup (filename + m, n);
+  if (entry->filename == NULL)
+    {
+      grub_free (entry);
+      goto finish;
+    }
+
+  err = bls_read_entry (f, entry);
+
+  if (err == GRUB_ERR_NONE)
+    blsuki_add_entry (entry);
+  else
+    grub_free (entry);
+
+ finish:
+  if (f != NULL)
+    grub_file_close (f);
+
+  return 0;
+}
+
+static char **
+blsuki_make_list (grub_blsuki_entry_t *entry, const char *key, int *num)
+{
+  int last = -1;
+  char *val;
+  int nlist = 0;
+  char **list;
+
+  list = grub_malloc (sizeof (char *));
+  if (list == NULL)
+    return NULL;
+  list[0] = NULL;
+
+  while (1)
+    {
+      char **new;
+
+      val = blsuki_get_val (entry, key, &last);
+      if (val == NULL)
+       break;
+
+      new = grub_realloc (list, (nlist + 2) * sizeof (char *));
+      if (new == NULL)
+       break;
+
+      list = new;
+      list[nlist++] = val;
+      list[nlist] = NULL;
+  }
+
+  if (nlist == 0)
+    {
+      grub_free (list);
+      return NULL;
+    }
+
+  if (num != NULL)
+    *num = nlist;
+
+  return list;
+}
+
+static char *
+blsuki_field_append (bool is_env_var, char *buffer, const char *start, const 
char *end)
+{
+  char *tmp;
+  const char *field;
+  int term = (is_env_var == true) ? 2 : 1;
+  grub_size_t size = 0;
+
+  tmp = grub_strndup (start, end - start + 1);
+  if (tmp == NULL)
+    return NULL;
+
+  field = tmp;
+
+  if (is_env_var == true)
+    {
+      field = grub_env_get (tmp);
+      if (field == NULL)
+       return buffer;
+    }
+
+  if (grub_add (grub_strlen (field), term, &size))
+    return NULL;
+
+  if (buffer == NULL)
+    buffer = grub_zalloc (size);
+  else
+    {
+      if (grub_add (size, grub_strlen (buffer), &size))
+       return NULL;
+      buffer = grub_realloc (buffer, size);
+    }
+
+  if (buffer == NULL)
+    return NULL;
+
+  tmp = buffer + grub_strlen (buffer);
+  tmp = grub_stpcpy (tmp, field);
+
+  if (is_env_var == true)
+      tmp = grub_stpcpy (tmp, " ");
+
+  return buffer;
+}
+
+static char *
+blsuki_expand_val (const char *value)
+{
+  char *buffer = NULL;
+  const char *start = value;
+  const char *end = value;
+  bool is_env_var = false;
+
+  if (value == NULL)
+    return NULL;
+
+  while (*value != '\0')
+    {
+      if (*value == '$')
+       {
+         if (start != end)
+           {
+             buffer = blsuki_field_append (is_env_var, buffer, start, end);
+             if (buffer == NULL)
+               return NULL;
+           }
+
+         is_env_var = true;
+         start = value + 1;
+       }
+      else if (is_env_var == true)
+       {
+         if (grub_isalnum (*value) == 0 && *value != '_')
+           {
+             buffer = blsuki_field_append (is_env_var, buffer, start, end);
+             is_env_var = false;
+             start = value;
+             if (*start == ' ')
+               start++;
+           }
+       }
+
+      end = value;
+      value++;
+    }
+
+  if (start != end)
+    {
+      buffer = blsuki_field_append (is_env_var, buffer, start, end);
+      if (buffer == NULL)
+       return NULL;
+    }
+
+  return buffer;
+}
+
+static char **
+blsuki_early_initrd_list (const char *initrd)
+{
+  int nlist = 0;
+  char **list = NULL;
+  char *separator;
+  char *tmp;
+
+  while ((separator = grub_strchr (initrd, ' ')))
+    {
+      list = grub_realloc (list, (nlist + 2) * sizeof (char *));
+      if (list == NULL)
+       return NULL;
+
+      tmp = grub_strndup (initrd, separator - initrd);
+      if (tmp == NULL)
+       {
+         grub_free (list);
+         return NULL;
+       }
+      list[nlist++] = tmp;
+      list[nlist] = NULL;
+      initrd = separator + 1;
+    }
+
+  list = grub_realloc (list, (nlist + 2) * sizeof (char *));
+  if (list == NULL)
+    return NULL;
+
+  tmp = grub_strndup (initrd, grub_strlen (initrd));
+  if (tmp == NULL)
+    {
+      grub_free (list);
+      return NULL;
+    }
+  list[nlist++] = tmp;
+  list[nlist] = NULL;
+
+  return list;
+}
+
+static void
+bls_create_entry (grub_blsuki_entry_t *entry)
+{
+  int argc = 0;
+  const char **argv = NULL;
+  char *title = NULL;
+  char *clinux = NULL;
+  char *linux_path = NULL;
+  grub_size_t linux_size;
+  char *options = NULL;
+  char **initrds = NULL;
+  char *initrd = NULL;
+  grub_size_t initrd_size;
+  const char *early_initrd = NULL;
+  char **early_initrds = NULL;
+  char *initrd_prefix = NULL;
+  char *prefix = NULL;
+  char *devicetree = NULL;
+  char *dt = NULL;
+  grub_size_t dt_size;
+  char *id = entry->filename;
+  char *dotconf = id;
+  char *hotkey = NULL;
+  char *users = NULL;
+  char **classes = NULL;
+  char **args = NULL;
+  char *src = NULL;
+  char *tmp;
+  const char *sdval = NULL;
+  int i;
+  grub_size_t size = 0;
+  bool add_dt_prefix = false;
+  bool savedefault;
+
+  linux_path = blsuki_get_val (entry, "linux", NULL);
+  if (linux_path == NULL)
+    {
+      grub_dprintf ("blsuki", "Skipping file %s with no 'linux' key.\n", 
entry->filename);
+      goto finish;
+    }
+
+  if (grub_add (sizeof ("linux "), grub_strlen (linux_path), &linux_size))
+    {
+      grub_error (GRUB_ERR_OUT_OF_RANGE, "overflow detected calculating linux 
buffer size");
+      goto finish;
+    }
+  clinux = grub_malloc (linux_size);
+  if (clinux == NULL)
+    goto finish;
+  tmp = clinux;
+  tmp = grub_stpcpy (tmp, "linux");
+  tmp = grub_stpcpy (tmp, " ");
+  tmp = grub_stpcpy (tmp, linux_path);
+
+  /* Strip the ".conf" off the end before we make it our "id" field. */
+  do
+    {
+      dotconf = grub_strstr (dotconf, ".conf");
+    }
+  while (dotconf != NULL && dotconf[5] != '\0');
+  if (dotconf != NULL)
+    dotconf[0] = '\0';
+
+  title = blsuki_get_val (entry, "title", NULL);
+  options = blsuki_expand_val (blsuki_get_val (entry, "options", NULL));
+
+  if (options == NULL)
+    options = blsuki_expand_val (grub_env_get ("default_kernelopts"));
+
+  initrds = blsuki_make_list (entry, "initrd", NULL);
+
+  devicetree = blsuki_expand_val (blsuki_get_val (entry, "devicetree", NULL));
+
+  if (devicetree == NULL)
+    {
+      devicetree = blsuki_expand_val (grub_env_get ("devicetree"));
+      add_dt_prefix = true;
+    }
+
+  hotkey = blsuki_get_val (entry, "grub_hotkey", NULL);
+  users = blsuki_expand_val (blsuki_get_val (entry, "grub_users", NULL));
+  classes = blsuki_make_list (entry, "grub_class", NULL);
+  args = blsuki_make_list (entry, "grub_arg", &argc);
+
+  argc += 1;
+  if (grub_mul (argc + 1, sizeof (char *), &size))
+    {
+      grub_error (GRUB_ERR_OUT_OF_RANGE, N_("overflow detected creating argv 
list"));
+      goto finish;
+    }
+  argv = grub_malloc (size);
+  if (argv == NULL)
+    goto finish;
+  argv[0] = title ? title : linux_path;
+  for (i = 1; i < argc; i++)
+    argv[i] = args[i-1];
+  argv[argc] = NULL;
+
+  early_initrd = grub_env_get ("early_initrd");
+
+  grub_dprintf ("blsuki", "adding menu entry for \"%s\" with id \"%s\"\n",
+               title, id);
+  if (early_initrd != NULL)
+    {
+      early_initrds = blsuki_early_initrd_list (early_initrd);
+      if (early_initrds == NULL)
+       {
+         grub_error (GRUB_ERR_OUT_OF_MEMORY, N_("failed to create early initrd 
list"));
+         goto finish;
+       }
+
+      if (initrds != NULL && initrds[0] != NULL)
+       {
+         initrd_prefix = grub_strrchr (initrds[0], '/');
+         initrd_prefix = grub_strndup (initrds[0], initrd_prefix - initrds[0] 
+ 1);
+       }
+      else
+       {
+         initrd_prefix = grub_strrchr (linux_path, '/');
+         initrd_prefix = grub_strndup (linux_path, initrd_prefix - linux_path 
+ 1);
+       }
+
+      if (initrd_prefix == NULL)
+       {
+         grub_error (GRUB_ERR_OUT_OF_MEMORY, N_("failed to allocate initrd 
prefix buffer"));
+         goto finish;
+       }
+    }
+
+  if (early_initrds != NULL || initrds != NULL)
+    {
+      initrd_size = sizeof ("initrd");
+
+      for (i = 0; early_initrds != NULL && early_initrds[i] != NULL; i++)
+       {
+         if (grub_add (initrd_size, sizeof (" "), &initrd_size) ||
+             grub_add (initrd_size, grub_strlen (initrd_prefix), &initrd_size) 
||
+             grub_add (initrd_size, grub_strlen (early_initrds[i]), 
&initrd_size) ||
+             grub_add (initrd_size, 1, &initrd_size))
+           {
+             grub_error (GRUB_ERR_OUT_OF_RANGE, "overflow detected calculating 
initrd buffer size");
+             goto finish;
+           }
+       }
+
+      for (i = 0; initrds != NULL && initrds[i] != NULL; i++)
+       {
+         if (grub_add (initrd_size, sizeof (" "), &initrd_size) ||
+             grub_add (initrd_size, grub_strlen (initrds[i]), &initrd_size) ||
+             grub_add (initrd_size, 1, &initrd_size))
+           {
+             grub_error (GRUB_ERR_OUT_OF_RANGE, "overflow detected calculating 
initrd buffer size");
+             goto finish;
+           }
+       }
+
+      if (grub_add (initrd_size, 1, &initrd_size))
+       {
+         grub_error (GRUB_ERR_OUT_OF_RANGE, "overflow detected calculating 
initrd buffer size");
+         goto finish;
+       }
+
+      initrd = grub_malloc (initrd_size);
+      if (initrd == NULL)
+       goto finish;
+
+      tmp = grub_stpcpy (initrd, "initrd");
+      for (i = 0; early_initrds != NULL && early_initrds[i] != NULL; i++)
+       {
+         grub_dprintf ("blsuki", "adding early initrd %s\n", early_initrds[i]);
+         tmp = grub_stpcpy (tmp, " ");
+         tmp = grub_stpcpy (tmp, initrd_prefix);
+         tmp = grub_stpcpy (tmp, early_initrds[i]);
+         grub_free (early_initrds[i]);
+       }
+
+      for (i = 0; initrds != NULL && initrds[i] != NULL; i++)
+       {
+         grub_dprintf ("blsuki", "adding initrd %s\n", initrds[i]);
+         tmp = grub_stpcpy (tmp, " ");
+         tmp = grub_stpcpy (tmp, initrds[i]);
+       }
+      tmp = grub_stpcpy (tmp, "\n");
+    }
+
+  if (devicetree != NULL)
+    {
+      if (add_dt_prefix == true)
+       {
+         prefix = grub_strrchr (linux_path, '/');
+         prefix = grub_strndup (linux_path, prefix - linux_path + 1);
+         if (prefix == NULL)
+           {
+             grub_error (GRUB_ERR_OUT_OF_MEMORY, N_("failed to allocate prefix 
buffer"));
+             goto finish;
+           }
+       }
+
+      if (grub_add (sizeof ("devicetree "), grub_strlen (devicetree), 
&dt_size) ||
+         grub_add (dt_size, 1, &dt_size))
+       {
+         grub_error (GRUB_ERR_OUT_OF_RANGE, "overflow detected calculating 
device tree buffer size");
+         goto finish;
+       }
+
+      if (add_dt_prefix == true)
+       {
+         if (grub_add (dt_size, grub_strlen (prefix), &dt_size))
+           {
+             grub_error (GRUB_ERR_OUT_OF_RANGE, "overflow detected calculating 
device tree buffer size");
+             goto finish;
+           }
+       }
+
+      dt = grub_malloc (dt_size);
+      if (dt == NULL)
+       goto finish;
+      tmp = dt;
+      tmp = grub_stpcpy (dt, "devicetree");
+      tmp = grub_stpcpy (tmp, " ");
+      if (add_dt_prefix == true)
+       tmp = grub_stpcpy (tmp, prefix);
+      tmp = grub_stpcpy (tmp, devicetree);
+      tmp = grub_stpcpy (tmp, "\n");
+
+      grub_free (prefix);
+    }
+
+  grub_dprintf ("blsuki", "devicetree %s for id:\"%s\"\n", dt, id);
+
+  sdval = grub_env_get ("save_default");
+  savedefault = ((NULL != sdval) && (grub_strcmp (sdval, "true") == 0));
+  src = grub_xasprintf ("%s%s%s%s\n"
+                       "%s%s",
+                       savedefault ? "savedefault\n" : "",
+                       clinux, options ? " " : "", options ? options : "",
+                       initrd ? initrd : "", dt ? dt : "");
+
+  grub_normal_add_menu_entry (argc, argv, classes, id, users, hotkey, NULL, 
src, 0, entry);
+
+ finish:
+  grub_free (clinux);
+  grub_free (dt);
+  grub_free (initrd);
+  grub_free (initrd_prefix);
+  grub_free (early_initrds);
+  grub_free (devicetree);
+  grub_free (initrds);
+  grub_free (options);
+  grub_free (classes);
+  grub_free (args);
+  grub_free (argv);
+  grub_free (src);
+}
+
+struct find_entry_info
+{
+  const char *dirname;
+  const char *devid;
+  grub_device_t dev;
+  grub_fs_t fs;
+};
+
+static grub_err_t
+blsuki_set_find_entry_info (struct find_entry_info *info, const char *dirname, 
const char *devid)
+{
+  grub_device_t dev;
+  grub_fs_t fs;
+
+  if (info == NULL)
+    return grub_error (GRUB_ERR_BAD_ARGUMENT, "info parameter is not set");
+
+  if (devid == NULL)
+    {
+#ifdef GRUB_MACHINE_EMU
+      devid = "host";
+#else
+      devid = grub_env_get ("root");
+#endif
+      if (devid == NULL)
+       return grub_error (GRUB_ERR_FILE_NOT_FOUND, N_("variable `%s' isn't 
set"), "root");
+    }
+
+  /* Check that we aren't closing and opening the same device. */
+  if (info->dev != NULL && grub_strcmp (info->devid, devid) != 0)
+    {
+      grub_device_close (info->dev);
+      info->dev = NULL;
+    }
+  /* If we are using the same device, then we can skip this step and only set 
the directory. */
+  if (info->dev == NULL)
+    {
+      grub_dprintf ("blsuki", "opening %s\n", devid);
+      dev = grub_device_open (devid);
+      if (dev == NULL)
+       return grub_errno;
+
+      grub_dprintf ("blsuki", "probing fs\n");
+      fs = grub_fs_probe (dev);
+      if (fs == NULL)
+       {
+         grub_device_close (dev);
+         return grub_errno;
+       }
+
+      info->devid = devid;
+      info->dev = dev;
+      info->fs = fs;
+    }
+
+  info->dirname = dirname;
+
+  return GRUB_ERR_NONE;
+}
+
+/* info: the filesystem object the file is on. */
+static void
+blsuki_find_entry (struct find_entry_info *info, bool enable_fallback)
+{
+  struct read_entry_info read_entry_info;
+  grub_fs_t dir_fs = NULL;
+  grub_device_t dir_dev = NULL;
+  int fallback = 0;
+  int r = 0;
+
+ read_fallback:
+  read_entry_info.file = NULL;
+  read_entry_info.dirname = info->dirname;
+
+  grub_dprintf ("blsuki", "scanning dir: %s\n", info->dirname);
+  dir_dev = info->dev;
+  dir_fs = info->fs;
+  read_entry_info.devid = info->devid;
+
+  r = dir_fs->fs_dir (dir_dev, read_entry_info.dirname, blsuki_read_entry,
+                     &read_entry_info);
+  if (r != 0)
+    {
+      grub_dprintf ("blsuki", "blsuki_read_entry returned error\n");
+      grub_errno = GRUB_ERR_NONE;
+    }
+
+  /*
+   * If we aren't able to find BLS entries in the directory given by 
info->dirname,
+   * we can fallback to the default location "/boot/loader/entries/" and see 
if we
+   * can find the files there.
+   */
+  if (entries == NULL && fallback == 0 && enable_fallback == true)
+    {
+      blsuki_set_find_entry_info (info, GRUB_BLS_CONFIG_PATH, NULL);
+      grub_dprintf ("blsuki", "Entries weren't found in %s, fallback to %s\n",
+                   read_entry_info.dirname, info->dirname);
+      fallback = 1;
+      goto read_fallback;
+    }
+}
+
+static grub_err_t
+blsuki_load_entries (char *path, bool enable_fallback)
+{
+  grub_size_t len;
+  static grub_err_t r;
+  const char *devid = NULL;
+  char *dir = NULL;
+  struct find_entry_info info = {
+      .dev = NULL,
+      .fs = NULL,
+      .dirname = NULL,
+  };
+  struct read_entry_info rei = {
+      .devid = NULL,
+      .dirname = NULL,
+  };
+
+  if (path != NULL)
+    {
+      len = grub_strlen (path);
+      if (grub_strcmp (path + len - 5, ".conf") == 0)
+       {
+         rei.file = grub_file_open (path, GRUB_FILE_TYPE_CONFIG);
+         if (rei.file == NULL)
+           return grub_errno;
+
+         /* blsuki_read_entry() closes the file. */
+         return blsuki_read_entry (path, NULL, &rei);
+       }
+      else if (path[0] == '(')
+       {
+         devid = path + 1;
+
+         dir = grub_strchr (path, ')');
+         if (dir == NULL)
+           return grub_error (GRUB_ERR_BAD_ARGUMENT, N_("invalid file name 
`%s'"), path);
+
+         *dir = '\0';
+
+         /* Check if there is more than the devid in the path. */
+         if (dir + 1 < path + len)
+           dir = dir + 1;
+       }
+      else if (path[0] == '/')
+       dir = path;
+    }
+
+  if (dir == NULL)
+    dir = (char *) GRUB_BLS_CONFIG_PATH;
+
+  r = blsuki_set_find_entry_info (&info, dir, devid);
+  if (r == GRUB_ERR_NONE)
+    blsuki_find_entry (&info, enable_fallback);
+
+  if (info.dev != NULL)
+    grub_device_close (info.dev);
+
+  return r;
+}
+
+static bool
+blsuki_is_default_entry (const char *def_entry, grub_blsuki_entry_t *entry, 
int idx)
+{
+  const char *title;
+  int def_idx;
+
+  if (def_entry == NULL)
+    return false;
+
+  if (grub_strcmp (def_entry, entry->filename) == 0)
+    return true;
+
+  title = blsuki_get_val (entry, "title", NULL);
+
+  if (title != NULL && grub_strcmp (def_entry, title) == 0)
+    return true;
+
+  def_idx = (int) grub_strtol (def_entry, NULL, 0);
+  if (grub_errno != GRUB_ERR_NONE)
+    {
+      grub_errno = GRUB_ERR_NONE;
+      return false;
+    }
+
+  if (def_idx == idx)
+    return true;
+
+  return false;
+}
+
+static grub_err_t
+blsuki_create_entries (bool show_default, bool show_non_default, char 
*entry_id)
+{
+  const char *def_entry = NULL;
+  grub_blsuki_entry_t *entry = NULL;
+  int idx = 0;
+
+  def_entry = grub_env_get ("default");
+
+  FOR_BLSUKI_ENTRIES(entry)
+    {
+      if (entry->visible == 1)
+       {
+         idx++;
+         continue;
+       }
+      if ((show_default == true && blsuki_is_default_entry (def_entry, entry, 
idx) == true) ||
+         (show_non_default == true && blsuki_is_default_entry (def_entry, 
entry, idx) == false) ||
+         (entry_id != NULL && grub_strcmp (entry_id, entry->filename) == 0))
+       {
+         bls_create_entry (entry);
+         entry->visible = 1;
+       }
+      idx++;
+    }
+
+  return GRUB_ERR_NONE;
+}
+
+static grub_err_t
+grub_cmd_blscfg (grub_extcmd_context_t ctxt, int argc __attribute__ ((unused)),
+                char **args __attribute__ ((unused)))
+{
+  grub_err_t err;
+  struct grub_arg_list *state = ctxt->state;
+  char *path = NULL;
+  char *entry_id = NULL;
+  bool enable_fallback = false;
+  bool show_default = false;
+  bool show_non_default = false;
+  bool all = true;
+
+  if (state[0].set)
+    path = state[0].arg;
+  if (state[1].set)
+    enable_fallback = true;
+  if (state[2].set)
+    {
+      show_default = true;
+      all = false;
+    }
+  if (state[3].set)
+    {
+      show_non_default = true;
+      all = false;
+    }
+  if (state[4].set)
+    {
+      entry_id = state[4].arg;
+      all = false;
+    }
+  if (all == true)
+    {
+      show_default = true;
+      show_non_default = true;
+    }
+
+  err = blsuki_load_entries (path, enable_fallback);
+  if (err != GRUB_ERR_NONE)
+    return err;
+
+  return blsuki_create_entries (show_default, show_non_default, entry_id);
+}
+
+static grub_extcmd_t bls_cmd;
+
+GRUB_MOD_INIT(blsuki)
+{
+  bls_cmd = grub_register_extcmd ("blscfg", grub_cmd_blscfg, 0,
+                                 N_("[-p|--path] [-f|--enable-fallback] DIR 
[-d|--show-default] [-n|--show-non-default] [-e|--entry] FILE"),
+                                 N_("Import Boot Loader Specification 
snippets."),
+                                 bls_opt);
+}
+
+GRUB_MOD_FINI(blsuki)
+{
+  grub_unregister_extcmd (bls_cmd);
+}
diff --git a/grub-core/commands/legacycfg.c b/grub-core/commands/legacycfg.c
index 3bf9fe2e4..f3c86dc7f 100644
--- a/grub-core/commands/legacycfg.c
+++ b/grub-core/commands/legacycfg.c
@@ -143,7 +143,7 @@ legacy_file (const char *filename)
            args[0] = oldname;
            grub_normal_add_menu_entry (1, args, NULL, NULL, "legacy",
                                        NULL, NULL,
-                                       entrysrc, 0);
+                                       entrysrc, 0, NULL);
            grub_free (args);
            entrysrc[0] = 0;
            grub_free (oldname);
@@ -204,7 +204,7 @@ legacy_file (const char *filename)
        }
       args[0] = entryname;
       grub_normal_add_menu_entry (1, args, NULL, NULL, NULL,
-                                 NULL, NULL, entrysrc, 0);
+                                 NULL, NULL, entrysrc, 0, NULL);
       grub_free (args);
     }
 
diff --git a/grub-core/commands/menuentry.c b/grub-core/commands/menuentry.c
index 720e6d8ea..09749c415 100644
--- a/grub-core/commands/menuentry.c
+++ b/grub-core/commands/menuentry.c
@@ -78,7 +78,7 @@ grub_normal_add_menu_entry (int argc, const char **args,
                            char **classes, const char *id,
                            const char *users, const char *hotkey,
                            const char *prefix, const char *sourcecode,
-                           int submenu)
+                           int submenu, grub_blsuki_entry_t *blsuki)
 {
   int menu_hotkey = 0;
   char **menu_args = NULL;
@@ -188,6 +188,7 @@ grub_normal_add_menu_entry (int argc, const char **args,
   (*last)->args = menu_args;
   (*last)->sourcecode = menu_sourcecode;
   (*last)->submenu = submenu;
+  (*last)->blsuki = blsuki;
 
   menu->size++;
   return GRUB_ERR_NONE;
@@ -286,7 +287,8 @@ grub_cmd_menuentry (grub_extcmd_context_t ctxt, int argc, 
char **args)
                                       users,
                                       ctxt->state[2].arg, 0,
                                       ctxt->state[3].arg,
-                                      ctxt->extcmd->cmd->name[0] == 's');
+                                      ctxt->extcmd->cmd->name[0] == 's',
+                                      NULL);
 
   src = args[argc - 1];
   args[argc - 1] = NULL;
@@ -303,7 +305,7 @@ grub_cmd_menuentry (grub_extcmd_context_t ctxt, int argc, 
char **args)
                                  ctxt->state[0].args, ctxt->state[4].arg,
                                  users,
                                  ctxt->state[2].arg, prefix, src + 1,
-                                 ctxt->extcmd->cmd->name[0] == 's');
+                                 ctxt->extcmd->cmd->name[0] == 's', NULL);
 
   src[len - 1] = ch;
   args[argc - 1] = src;
diff --git a/grub-core/lib/vercmp.c b/grub-core/lib/vercmp.c
new file mode 100644
index 000000000..726e2321b
--- /dev/null
+++ b/grub-core/lib/vercmp.c
@@ -0,0 +1,317 @@
+/*
+ *  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/lib/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;
+
+  grub_dprintf ("blscfg", "%s comparing %s and %s\n", __func__, a, b);
+  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/grub-core/normal/main.c b/grub-core/normal/main.c
index 04d058f55..2d493f21e 100644
--- a/grub-core/normal/main.c
+++ b/grub-core/normal/main.c
@@ -21,6 +21,7 @@
 #include <grub/net.h>
 #include <grub/normal.h>
 #include <grub/dl.h>
+#include <grub/menu.h>
 #include <grub/misc.h>
 #include <grub/file.h>
 #include <grub/mm.h>
@@ -67,6 +68,11 @@ grub_normal_free_menu (grub_menu_t menu)
          grub_free (entry->args);
        }
 
+      if (entry->blsuki)
+       {
+         entry->blsuki->visible = 0;
+       }
+
       grub_free ((void *) entry->id);
       grub_free ((void *) entry->users);
       grub_free ((void *) entry->title);
diff --git a/include/grub/lib/vercmp.h b/include/grub/lib/vercmp.h
new file mode 100644
index 000000000..e588ef530
--- /dev/null
+++ b/include/grub/lib/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/include/grub/menu.h b/include/grub/menu.h
index ee2b5e910..c25a0d16d 100644
--- a/include/grub/menu.h
+++ b/include/grub/menu.h
@@ -20,6 +20,18 @@
 #ifndef GRUB_MENU_HEADER
 #define GRUB_MENU_HEADER 1
 
+struct grub_blsuki_entry
+{
+  struct grub_blsuki_entry *next;
+  struct grub_blsuki_entry **prev;
+  struct keyval **keyvals;
+  grub_size_t keyvals_size;
+  int nkeyvals;
+  char *filename;
+  int visible;
+};
+typedef struct grub_blsuki_entry grub_blsuki_entry_t;
+
 struct grub_menu_entry_class
 {
   char *name;
@@ -60,6 +72,9 @@ struct grub_menu_entry
 
   /* The next element.  */
   struct grub_menu_entry *next;
+
+  /* BLS used to populate the entry */
+  grub_blsuki_entry_t *blsuki;
 };
 typedef struct grub_menu_entry *grub_menu_entry_t;
 
diff --git a/include/grub/normal.h b/include/grub/normal.h
index 218cbabcc..d0150e3c2 100644
--- a/include/grub/normal.h
+++ b/include/grub/normal.h
@@ -145,7 +145,7 @@ grub_normal_add_menu_entry (int argc, const char **args, 
char **classes,
                            const char *id,
                            const char *users, const char *hotkey,
                            const char *prefix, const char *sourcecode,
-                           int submenu);
+                           int submenu, grub_blsuki_entry_t *blsuki);
 
 grub_err_t
 grub_normal_set_password (const char *user, const char *password);
diff --git a/tests/vercmp_unit_test.c b/tests/vercmp_unit_test.c
new file mode 100644
index 000000000..d0dfb1e8b
--- /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/lib/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