Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package rdi-installer for openSUSE:Factory 
checked in at 2026-10-08 11:56:31
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/rdi-installer (Old)
 and      /work/SRC/openSUSE:Factory/.rdi-installer.new.3698736 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "rdi-installer"

Thu Oct  8 11:56:31 2026 rev:2 rq:1383089 version:1.1.0+git20261007.d929392

Changes:
--------
--- /work/SRC/openSUSE:Factory/rdi-installer/rdi-installer.changes      
2026-09-29 17:55:53.154468547 +0200
+++ /work/SRC/openSUSE:Factory/.rdi-installer.new.3698736/rdi-installer.changes 
2026-10-08 11:58:52.959716397 +0200
@@ -1,0 +2,9 @@
+Wed Oct 07 16:25:07 UTC 2026 - Thorsten Kukuk <[email protected]>
+
+- Update to version 1.1.0+git20261007.d929392:
+  * Release version 1.1.0
+  * image: add rdii version to menu entry
+  * Don't show sysext images in the filter by default
+  * Added filter for selecting online images (#24)
+
+-------------------------------------------------------------------

Old:
----
  rdi-installer-1.0.0+git20260923.8f88a3f.tar.xz

New:
----
  rdi-installer-1.1.0+git20261007.d929392.tar.xz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ rdi-installer.spec ++++++
--- /var/tmp/diff_new_pack.PTjw3j/_old  2026-10-08 11:58:54.503780957 +0200
+++ /var/tmp/diff_new_pack.PTjw3j/_new  2026-10-08 11:58:54.505781041 +0200
@@ -16,7 +16,7 @@
 #
 
 Name:           rdi-installer
-Version:        1.0.0+git20260923.8f88a3f
+Version:        1.1.0+git20261007.d929392
 Release:        0
 Summary:        Utility to write disk images to hard disk
 License:        MIT

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.PTjw3j/_old  2026-10-08 11:58:54.605785222 +0200
+++ /var/tmp/diff_new_pack.PTjw3j/_new  2026-10-08 11:58:54.618785766 +0200
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param 
name="url">https://github.com/thkukuk/rdi-installer.git</param>
-<param 
name="changesrevision">8f88a3f728ee268b5fe1e76ca0aa59b4d011a522</param></service></servicedata>
+<param 
name="changesrevision">d929392abddf784c686ede2114e11b0997102138</param></service></servicedata>
 (No newline at EOF)
 

++++++ rdi-installer-1.0.0+git20260923.8f88a3f.tar.xz -> 
rdi-installer-1.1.0+git20261007.d929392.tar.xz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rdi-installer-1.0.0+git20260923.8f88a3f/include/nc-dialogs.h 
new/rdi-installer-1.1.0+git20261007.d929392/include/nc-dialogs.h
--- old/rdi-installer-1.0.0+git20260923.8f88a3f/include/nc-dialogs.h    
2026-09-23 14:47:14.000000000 +0200
+++ new/rdi-installer-1.1.0+git20261007.d929392/include/nc-dialogs.h    
2026-10-07 18:23:15.000000000 +0200
@@ -42,4 +42,8 @@
 
 extern int choose_entry(int row, const char *options[], int num_options,
                         int start, const char *title, const char *help_text);
+extern void render_scrollable_list(int row, int max_visible, const char 
*options[],
+                                   int num_options, int selected, int 
scroll_offset);
+extern int list_viewport_height(int row);
+extern int scroll_offset_for_selection(int selected, int scroll_offset, int 
max_visible);
 extern void init_ncurses(const char *title);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rdi-installer-1.0.0+git20260923.8f88a3f/lib/nc-dialogs.c 
new/rdi-installer-1.1.0+git20261007.d929392/lib/nc-dialogs.c
--- old/rdi-installer-1.0.0+git20260923.8f88a3f/lib/nc-dialogs.c        
2026-09-23 14:47:14.000000000 +0200
+++ new/rdi-installer-1.1.0+git20261007.d929392/lib/nc-dialogs.c        
2026-10-07 18:23:15.000000000 +0200
@@ -669,6 +669,94 @@
   refresh();
 }
 
+// Number of list rows that fit below `row`, reserving the last line for the
+// footer drawn by print_global_header_footer(). Shared by every scrollable
+// list widget so their viewport height and scroll-offset clamping agree.
+int
+list_viewport_height(int row)
+{
+  int max_y, max_x;
+  getmaxyx(stdscr, max_y, max_x);
+  (void)max_x;
+
+  int max_visible = max_y - row - 1;
+  return (max_visible < 1) ? 1 : max_visible;
+}
+
+// Returns an updated scroll_offset that keeps 'selected' within the visible
+// window [scroll_offset, scroll_offset + max_visible), moving the window by
+// the minimum amount needed (sticky scrolling) rather than recentering it
+// every frame. Shared by every scrollable list widget.
+int
+scroll_offset_for_selection(int selected, int scroll_offset, int max_visible)
+{
+  if (selected < scroll_offset)
+    return selected;
+  if (selected >= scroll_offset + max_visible)
+    return selected - max_visible + 1;
+  return scroll_offset;
+}
+
+// Renders a scrollable list widget: the visible viewport slice of `options`
+// (clipped/padded to fit), the selection highlight, and a scrollbar thumb
+// when the list overflows the viewport. Shared by choose_entry() and any
+// other widget that lays out a scrollable list below its own header row.
+// `max_visible` is taken from the caller (via list_viewport_height()) rather
+// than recomputed here, since every caller already needs it beforehand to
+// drive scroll_offset_for_selection().
+void
+render_scrollable_list(int row, int max_visible, const char *options[],
+                       int num_options, int selected, int scroll_offset)
+{
+  int max_y, max_x;
+  getmaxyx(stdscr, max_y, max_x);
+  (void)max_y;
+
+  // Leave room for the scrollbar column so long entries can't overwrite it
+  bool show_scrollbar = num_options > max_visible;
+  int text_width = (show_scrollbar ? max_x - 2 : max_x) - 2 - 3;
+  if (text_width < 1) text_width = 1;
+
+  // Render visible options
+  for (int i = 0; i < max_visible; i++)
+    {
+      int item_idx = scroll_offset + i;
+      int y = row + i;
+
+      // Clear line to avoid leftover artifacts from previously rendered text
+      move(y, 0);
+      clrtoeol();
+
+      if (item_idx < num_options)
+        {
+          if (item_idx == selected)
+            {
+              attron(COLOR_PAIR(CP_SELECTED) | A_BOLD);
+              mvprintw(y, 2, "-> %.*s", text_width, options[item_idx]);
+              attroff(COLOR_PAIR(CP_SELECTED) | A_BOLD);
+            }
+          else
+            {
+              attron(COLOR_PAIR(CP_UNSELECTED));
+              mvprintw(y, 2, "   %.*s", text_width, options[item_idx]);
+              attroff(COLOR_PAIR(CP_UNSELECTED));
+            }
+        }
+    }
+
+  // Render scrollbar on right margin if list exceeds viewport
+  if (show_scrollbar)
+    {
+      int sb_x = max_x - 2;
+      int thumb_pos = (selected * (max_visible - 1)) / (num_options - 1);
+
+      for (int i = 0; i < max_visible; i++)
+        {
+          mvaddch(row + i, sb_x, (i == thumb_pos) ? ACS_CKBOARD : '|');
+        }
+    }
+}
+
 int
 choose_entry(int row, const char *options[], int num_options, int start,
              const char *title, const char *help_text)
@@ -685,62 +773,11 @@
 
   while (1)
     {
-      int max_y, max_x;
-      getmaxyx(stdscr, max_y, max_x);
-
-      // Reserve the last line for the footer drawn by 
print_global_header_footer()
-      int max_visible = max_y - row - 1;
-      if (max_visible < 1) max_visible = 1;
-
-      // Adjust scroll offset to keep 'selected' visible
-      if (selected < scroll_offset)
-        scroll_offset = selected;
-      else if (selected >= scroll_offset + max_visible)
-        scroll_offset = selected - max_visible + 1;
-
-      // Leave room for the scrollbar column so long entries can't overwrite it
-      bool show_scrollbar = num_options > max_visible;
-      int text_width = (show_scrollbar ? max_x - 2 : max_x) - 2 - 3;
-      if (text_width < 1) text_width = 1;
-
-      // Render visible options
-      for (int i = 0; i < max_visible; i++)
-        {
-          int item_idx = scroll_offset + i;
-          int y = row + i;
-
-          // Clear line to avoid leftover artifacts from previously rendered 
text
-          move(y, 0);
-          clrtoeol();
-
-          if (item_idx < num_options)
-            {
-              if (item_idx == selected)
-                {
-                  attron(COLOR_PAIR(CP_SELECTED) | A_BOLD);
-                  mvprintw(y, 2, "-> %.*s", text_width, options[item_idx]);
-                  attroff(COLOR_PAIR(CP_SELECTED) | A_BOLD);
-                }
-              else
-                {
-                  attron(COLOR_PAIR(CP_UNSELECTED));
-                  mvprintw(y, 2, "   %.*s", text_width, options[item_idx]);
-                  attroff(COLOR_PAIR(CP_UNSELECTED));
-                }
-            }
-        }
+      int max_visible = list_viewport_height(row);
 
-      // Render scrollbar on right margin if list exceeds viewport
-      if (show_scrollbar)
-        {
-          int sb_x = max_x - 2;
-          int thumb_pos = (selected * (max_visible - 1)) / (num_options - 1);
+      scroll_offset = scroll_offset_for_selection(selected, scroll_offset, 
max_visible);
 
-          for (int i = 0; i < max_visible; i++)
-            {
-              mvaddch(row + i, sb_x, (i == thumb_pos) ? ACS_CKBOARD : '|');
-            }
-        }
+      render_scrollable_list(row, max_visible, options, num_options, selected, 
scroll_offset);
 
       refresh();
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/rdi-installer-1.0.0+git20260923.8f88a3f/meson.build 
new/rdi-installer-1.1.0+git20261007.d929392/meson.build
--- old/rdi-installer-1.0.0+git20260923.8f88a3f/meson.build     2026-09-23 
14:47:14.000000000 +0200
+++ new/rdi-installer-1.1.0+git20261007.d929392/meson.build     2026-10-07 
18:23:15.000000000 +0200
@@ -14,7 +14,7 @@
                   'werror=true',
                  'warning_level=2'],
   license : ['MIT', 'GPL-2.0-or-later'],
-  version : '1.0.0',
+  version : '1.1.0',
 )
 
 conf = configuration_data()
@@ -123,6 +123,7 @@
 rdi_installer_c = ['src/rdi-installer.c',
                    'src/rdii-menu.c', 'src/rdii-ssh-hostkey.c',
                    'src/rdii-menu-disk.c', 'src/rdii-menu-image.c',
+                   'src/rdii-select-online-image.c',
                    'src/rdii-menu-sysinfo.c', 'src/rdii-menu-installation.c',
                    'src/rdii-menu-mdraid.c', 'src/rdii-autoinstall.c']
 executable('rdi-installer',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rdi-installer-1.0.0+git20260923.8f88a3f/src/rdii-menu-image.c 
new/rdi-installer-1.1.0+git20261007.d929392/src/rdii-menu-image.c
--- old/rdi-installer-1.0.0+git20260923.8f88a3f/src/rdii-menu-image.c   
2026-09-23 14:47:14.000000000 +0200
+++ new/rdi-installer-1.1.0+git20261007.d929392/src/rdii-menu-image.c   
2026-10-07 18:23:15.000000000 +0200
@@ -16,6 +16,7 @@
 #include "logger.h"
 #include "nc-dialogs.h"
 #include "rdii-menu.h"
+#include "rdii-select-online-image.h"
 #include "download.h"
 
 #define OFFSET 2
@@ -208,25 +209,6 @@
 
 #define _cleanup_entries_ _cleanup_(free_entriesp)
 
-static bool
-is_supported_image(const char *name)
-{
-  const char *exts[] = {
-    ".img",     ".raw",
-    ".img.gz",  ".raw.gz",
-    ".img.bz2", ".raw.bz2",
-    ".img.xz",  ".raw.xz",
-    ".img.zst", ".raw.zst"
-  };
-  const size_t num_exts = sizeof(exts) / sizeof(exts[0]);
-
-  for (size_t i = 0; i < num_exts; i++)
-    if (endswith(name, exts[i]))
-      return true;
-
-  return false;
-}
-
 // Sort directories first (with .. the very first), then alphabetically
 static int
 compare_entries(const void *a, const void *b)
@@ -430,184 +412,6 @@
   return -ENOSYS;
 }
 
-/*
- *  select an image from a remote SHA256SUMS listing
- */
-
-/*
-  Parse SHA256SUMS file and return the names of all supported images.
-  Returns the number of found entries (>= 0), or a negative errno code.
-*/
-static int
-parse_sha256sums(const char *path, char ***ret_names)
-{
-  _cleanup_fclose_ FILE *fp = NULL;
-  _cleanup_free_ char *line = NULL;
-  size_t linecap = 0;
-  ssize_t linelen;
-  char **names = NULL;
-  int capacity = 0;
-  int count = 0;
-
-  MSG_FUNC("path='%s'", path);
-
-  fp = fopen(path, "r");
-  if (!fp)
-    return -errno;
-
-  while ((linelen = getline(&line, &linecap, fp)) > 0)
-    {
-      char *nl = strchr(line, '\n');
-      if (nl)
-       *nl = '\0';
-
-      // A valid line is "<64 hex char hash><separator><filename>"
-      if (linelen < 66)
-       continue;
-
-      char *name = strchr(line, ' ');
-      while (*name == ' ')
-       ++name;
-
-      if (isempty(name) || !is_supported_image(name))
-       continue;
-
-      if (count >= capacity)
-       {
-         capacity = capacity ? capacity * 2 : 16;
-         char **new_names = realloc(names, capacity * sizeof(char *));
-         if (!new_names)
-           {
-             for (int i = 0; i < count; i++)
-               free(names[i]);
-             free(names);
-             return -ENOMEM;
-           }
-         names = new_names;
-       }
-
-      names[count] = strdup(name);
-      if (!names[count])
-       {
-         for (int i = 0; i < count; i++)
-           free(names[i]);
-         free(names);
-         return -ENOMEM;
-       }
-      count++;
-    }
-
-  *ret_names = names;
-
-  MSG_INFO("Found %i supported image(s) in '%s'", count, path);
-
-  return count;
-}
-
-static int
-get_url_from_list(char **ret)
-{
-  _cleanup_free_ char *sha256sums_url = NULL;
-  _cleanup_free_ char *sha256sums_asc_url = NULL;
-  _cleanup_free_ char *sha256sums_fn = NULL;
-  _cleanup_free_ char *sha256sums_asc_fn = NULL;
-  char **names = NULL;
-  int num_names;
-  int selected;
-  int r = 0;
-
-  MSG_FUNC();
-
-  if (asprintf(&sha256sums_url, "%s/SHA256SUMS", rdii_download_server) < 0)
-    return -ENOMEM;
-  if (asprintf(&sha256sums_asc_url, "%s/SHA256SUMS.asc", rdii_download_server) 
< 0)
-    return -ENOMEM;
-  if (asprintf(&sha256sums_fn, "%s/SHA256SUMS", rdii_tmp_dir) < 0)
-    return -ENOMEM;
-  if (asprintf(&sha256sums_asc_fn, "%s/SHA256SUMS.asc", rdii_tmp_dir) < 0)
-    return -ENOMEM;
-
-  r = curl_download_file(sha256sums_url, sha256sums_fn);
-  if (r != 0)
-    {
-      MSG_ERROR("Error downloading SHA256SUMS file:",
-               r < 0 ? strerror(-r) : curl_easy_strerror(r));
-      show_error_popup("Error downloading SHA256SUMS file:",
-                      r < 0 ? strerror(-r) : curl_easy_strerror(r), NULL);
-      return -EIO;
-    }
-
-  r = curl_download_file(sha256sums_asc_url, sha256sums_asc_fn);
-  if (r != 0)
-    {
-      MSG_ERROR("Error downloading SHA256SUMS.asc file:",
-               r < 0 ? strerror(-r) : curl_easy_strerror(r));
-      if (!show_warning_popup("Error downloading SHA256SUMS.asc file:",
-                             r < 0 ? strerror(-r) : curl_easy_strerror(r),
-                             "Continue without signature verification?"))
-       return -ECANCELED;
-    }
-  else
-    {
-      char *error_msg = NULL;
-      if (!verify_signature(sha256sums_fn, sha256sums_asc_fn, &error_msg))
-       {
-         MSG_WARN("Cannot verify SHA256SUMS signature: %s", error_msg);
-         if (!show_warning_popup("Cannot verify signature.", error_msg,
-                                 "Continue without signature verification?"))
-           {
-             MSG_ERROR("Canceld");
-             return -ECANCELED;
-           }
-       }
-    }
-
-  num_names = parse_sha256sums(sha256sums_fn, &names);
-  if (num_names < 0)
-    {
-      MSG_ERROR("Error parsing SHA256SUMS file: %s",
-               strerror(-num_names));
-      show_error_popup("Error parsing SHA256SUMS file:",
-                      strerror(-num_names), NULL);
-      return num_names;
-    }
-  if (num_names == 0)
-    {
-      MSG_INFO("No supported images found in %s", rdii_download_server);
-      show_error_popup("No supported images found in SHA256SUMS file.",
-                       NULL, NULL);
-      return -ENOENT;
-    }
-
-  _cleanup_free_ char *header = NULL;
-  const char *help_text = "Select raw disk image which has to be installed on 
the target system.\n"
-    "The name of the default download server can be changed by the value of 
rdii.download_server, set "
-    "by the kernel cmdline during boot or by a configuration file.\n";
-
-  if (asprintf(&header, "Select image from download server %s", 
rdii_download_server) < 0)
-    {
-      for (int i = 0; i < num_names; i++)
-       free(names[i]);
-      free(names);
-      return -ENOMEM;
-    }
-
-  selected = choose_entry(4, (const char **)names, num_names, 0,
-                          header, help_text);
-  if (selected < 0)
-    r = selected;
-  else if (asprintf(ret, "%s/%s", rdii_download_server, names[selected]) < 0)
-    r = -ENOMEM;
-  else
-    r = 0;
-
-  for (int i = 0; i < num_names; i++)
-    free(names[i]);
-  free(names);
-
-  return r;
-}
-
 void
 select_installation_source(const char *prefill, char **ret)
 {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rdi-installer-1.0.0+git20260923.8f88a3f/src/rdii-select-online-image.c 
new/rdi-installer-1.1.0+git20261007.d929392/src/rdii-select-online-image.c
--- old/rdi-installer-1.0.0+git20260923.8f88a3f/src/rdii-select-online-image.c  
1970-01-01 01:00:00.000000000 +0100
+++ new/rdi-installer-1.1.0+git20261007.d929392/src/rdii-select-online-image.c  
2026-10-07 18:23:15.000000000 +0200
@@ -0,0 +1,694 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "config.h"
+
+#include <errno.h>
+#include <ctype.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <sys/utsname.h>
+#include <curl/curl.h>
+#include <ncurses.h>
+
+#include "basics.h"
+#include "logger.h"
+#include "nc-dialogs.h"
+#include "rdii-menu.h"
+#include "rdii-select-online-image.h"
+#include "download.h"
+
+typedef struct {
+  const char *token; // Pattern bounded by '.' or '-' (or start/end of string)
+  const char *arch;  // Standardized architecture name
+} ArchMap;
+
+// Data structure for the architecture-specific items
+typedef struct {
+  char *name;
+  const char *arch; // always one of arch_map[].arch or the "unknown" literal, 
never heap-owned
+} Image;
+
+typedef struct {
+  Image *data;
+  size_t size;
+  size_t capacity;
+} ImageList;
+
+static const ArchMap arch_map[] = {
+  {"x86-64", "x86-64"},
+  {"x86_64", "x86-64"},
+  {"amd64",  "x86-64"},
+  {"x64",    "x86-64"},
+  {"x86",    "x86"},
+  {"i386",   "x86"},
+  {"i486",   "x86"},
+  {"i586",   "x86"},
+  {"i686",   "x86"},
+  {"arm64",  "arm64"},
+  {"aarch64","arm64"},
+  {"s390x",  "s390x"},
+  {"s390",   "s390"}
+};
+
+// Dynamic list of options for the filter box
+static char **arch_options = NULL;
+static int arch_options_count = 0;
+static int current_arch_idx = 0;
+
+// Initialize the list
+static int
+init_list(ImageList *list, size_t initial_capacity)
+{
+  list->size = 0;
+  list->capacity = initial_capacity;
+  list->data = malloc(list->capacity * sizeof(Image));
+  if (!list->data)
+    {
+      perror("Failed to allocate image list");
+      return -ENOMEM;
+    }
+  return 0;
+}
+
+// Add an element to the list (allocating memory for strings)
+static int
+add_image(ImageList *list, const char *name, const char *arch)
+{
+  // Resize the array if it reaches capacity
+  if (list->size >= list->capacity)
+    {
+      list->capacity *= 2;
+      Image *new_data = realloc(list->data, list->capacity * sizeof(Image));
+      if (!new_data)
+        {
+          perror("Failed to reallocate image list");
+          return -ENOMEM;
+        }
+      list->data = new_data;
+    }
+
+  // name needs its own copy (it points into the caller's line buffer); arch
+  // is always one of arch_map[].arch or the "unknown" literal (see
+  // extract_arch_bounded()), so it's safe - and avoids a pointless
+  // allocation - to store that constant pointer directly.
+  char *name_dup = strdup(name);
+  if (!name_dup)
+    {
+      perror("Failed to duplicate image entry");
+      return -ENOMEM;
+    }
+
+  list->data[list->size].name = name_dup;
+  list->data[list->size].arch = arch;
+
+  list->size++;
+  return 0;
+}
+
+// Free all allocated memory within the list
+static void
+free_list(ImageList *list)
+{
+  for (size_t i = 0; i < list->size; i++)
+    free(list->data[i].name);
+  free(list->data);
+  list->data = NULL;
+  list->size = 0;
+  list->capacity = 0;
+}
+
+// Clean up dynamically allocated memory
+static void
+free_arch_options(void)
+{
+  if (!arch_options)
+    return;
+  for (int i = 0; i < arch_options_count; i++)
+    free(arch_options[i]);
+  free(arch_options);
+  arch_options = NULL;
+  arch_options_count = 0;
+}
+
+// Adapts free_arch_options() to the _cleanup_() attribute, which always
+// passes the address of the annotated variable regardless of its type.
+static void
+free_arch_options_cleanup(void *unused)
+{
+  (void)unused;
+  free_arch_options();
+}
+
+bool
+is_supported_image(const char *name)
+{
+  const char *exts[] = {
+    ".img",     ".raw",
+    ".img.gz",  ".raw.gz",
+    ".img.bz2", ".raw.bz2",
+    ".img.xz",  ".raw.xz",
+    ".img.zst", ".raw.zst"
+  };
+  const size_t num_exts = sizeof(exts) / sizeof(exts[0]);
+
+  for (size_t i = 0; i < num_exts; i++)
+    if (endswith(name, exts[i]))
+      return true;
+
+  return false;
+}
+
+// sysext images are only meant to be picked explicitly (via the "all"
+// filter), not offered under "matching" or a specific architecture, since
+// they are overlay images rather than bootable system images.
+static bool
+is_sysext_image(const char *name)
+{
+  const char *exts[] = {
+    ".sysext.raw",
+    ".sysext.raw.gz",
+    ".sysext.raw.bz2",
+    ".sysext.raw.xz",
+    ".sysext.raw.zst"
+  };
+  const size_t num_exts = sizeof(exts) / sizeof(exts[0]);
+
+  for (size_t i = 0; i < num_exts; i++)
+    if (endswith(name, exts[i]))
+      return true;
+
+  return false;
+}
+
+// A token match is only accepted if it isn't glued to surrounding
+// alphanumeric characters, e.g. "x86" must not match inside "x8600". '_' is
+// treated as a boundary (not alphanumeric-glue) since it's a common
+// filename separator and several arch_map tokens (e.g. "x86_64") contain it
+// themselves.
+static bool
+is_arch_boundary(char c)
+{
+  return c == '\0' || !isalnum((unsigned char)c);
+}
+
+// Finds the architecture token that appears earliest in `filename`, rather
+// than the first one in arch_map's declaration order, so classification
+// depends on filename content rather than table ordering (relevant if a
+// filename were ever to contain more than one recognized token). Ties at
+// the same position (e.g. "x86" and "x86_64" both start at the same offset
+// in "...-x86_64...") are broken by preferring the longer, more specific
+// token, rather than depending on which one arch_map happens to list first.
+static const char *
+extract_arch_bounded(const char *filename)
+{
+  size_t count = sizeof(arch_map) / sizeof(arch_map[0]);
+  const char *best_arch = "unknown";
+  size_t best_pos = SIZE_MAX;
+  size_t best_len = 0;
+
+  for (size_t i = 0; i < count; i++)
+    {
+      const char *token = arch_map[i].token;
+      size_t token_len = strlen(token);
+      const char *p = filename;
+
+      while ((p = strstr(p, token)) != NULL)
+        {
+          char before = (p == filename) ? '\0' : p[-1];
+          char after = p[token_len];
+
+          if (is_arch_boundary(before) && is_arch_boundary(after))
+            {
+              size_t pos = (size_t)(p - filename);
+              if (pos < best_pos || (pos == best_pos && token_len > best_len))
+                {
+                  best_pos = pos;
+                  best_len = token_len;
+                  best_arch = arch_map[i].arch;
+                }
+              break;
+            }
+
+          p++;
+        }
+    }
+
+  return best_arch;
+}
+
+// Normalize a `uname -m` style machine name to the same canonical
+// architecture names used as arch_map values, reusing arch_map itself so
+// the filename-token and uname-value alias lists can't drift apart.
+static const char *
+normalize_machine_arch(const char *machine)
+{
+  if (!machine)
+    return "unknown-host";
+
+  size_t count = sizeof(arch_map) / sizeof(arch_map[0]);
+  for (size_t i = 0; i < count; i++)
+    if (streq(machine, arch_map[i].token))
+      return arch_map[i].arch;
+
+  // Fall back to the raw uname() value rather than the literal "unknown":
+  // that string is also what extract_arch_bounded() reports for images
+  // whose filename doesn't match any known token, and having both sides
+  // collapse to the same literal would make an unrelated image on an
+  // unrecognized host architecture look like a "matching" filter hit.
+  return machine;
+}
+
+// Returns the canonical architecture name of the running system.
+static const char *
+get_system_arch(void)
+{
+  static char system_arch[32] = "";
+
+  if (system_arch[0] == '\0')
+    {
+      struct utsname uts;
+      const char *arch = (uname(&uts) == 0) ? 
normalize_machine_arch(uts.machine) : "unknown-host";
+      strncpy(system_arch, arch, sizeof(system_arch) - 1);
+      system_arch[sizeof(system_arch) - 1] = '\0';
+    }
+
+  return system_arch;
+}
+
+// Helper to check if a string already exists in an array of strings
+static bool
+string_exists(char **arr, int count, const char *str)
+{
+  for (int i = 0; i < count; i++)
+    if (streq(arr[i], str))
+      return true;
+  return false;
+}
+
+/*
+ *  select an image from a remote SHA256SUMS listing
+ */
+
+/*
+  Parse SHA256SUMS file and return the names of all supported images.
+  Returns the number of found entries (>= 0), or a negative errno code.
+*/
+static int
+parse_sha256sums(const char *path, ImageList *ret_images)
+{
+  _cleanup_fclose_ FILE *fp = NULL;
+  _cleanup_free_ char *line = NULL;
+  size_t linecap = 0;
+  ssize_t linelen;
+  int count = 0;
+
+  MSG_FUNC("path='%s'", path);
+
+  current_arch_idx = 0;
+
+  arch_options = malloc(2 * sizeof(char *));
+  if (!arch_options)
+    {
+      perror("Failed to allocate architecture filter list");
+      return -ENOMEM;
+    }
+  arch_options[0] = strdup("matching");
+  arch_options[1] = strdup("all");
+  if (!arch_options[0] || !arch_options[1])
+    {
+      free(arch_options[0]);
+      free(arch_options[1]);
+      free(arch_options);
+      arch_options = NULL;
+      perror("Failed to allocate architecture filter list");
+      return -ENOMEM;
+    }
+  arch_options_count = 2;
+
+  fp = fopen(path, "r");
+  if (!fp)
+    return -errno;
+
+  while ((linelen = getline(&line, &linecap, fp)) > 0)
+    {
+      char *nl = strchr(line, '\n');
+      if (nl)
+        *nl = '\0';
+
+      // A valid line is "<64 hex char hash><separator><filename>"
+      if (linelen < 66)
+        continue;
+
+      char *name = strchr(line, ' ');
+      if (!name)
+        continue;
+      while (*name == ' ')
+        ++name;
+
+      if (isempty(name) || !is_supported_image(name))
+        continue;
+
+      const char *arch = extract_arch_bounded(name);
+      int ret = add_image(ret_images, name, arch);
+      if (ret == 0)
+        {
+          count++;
+          // If it's a new unique architecture, add it
+          if (!string_exists(arch_options, arch_options_count, arch))
+            {
+              char **new_options = realloc(arch_options, (arch_options_count + 
1) * sizeof(char *));
+              if (!new_options)
+                {
+                  perror("Failed to reallocate architecture filter list");
+                  return -ENOMEM;
+                }
+              arch_options = new_options;
+              arch_options[arch_options_count] = strdup(arch);
+              if (!arch_options[arch_options_count])
+                {
+                  perror("Failed to duplicate architecture name");
+                  return -ENOMEM;
+                }
+              arch_options_count++;
+            }
+        }
+      else
+        return ret;
+    }
+
+  MSG_INFO("Found %i supported image(s) in '%s'", count, path);
+
+  return count;
+}
+
+// Helper to determine if an item matches the active filter
+static bool
+item_matches_filter(const char *image_name, const char *image_arch, int 
arch_idx)
+{
+  const char *target_arch = arch_options[arch_idx];
+
+  if (streq(target_arch, "all"))
+    // "all architectures" selected: show complete array of ALL_ITEMS
+    return true;
+
+  if (is_sysext_image(image_name))
+    // sysext images are only shown with "all", never under "matching" or a
+    // specific architecture filter
+    return false;
+
+  if (streq(target_arch, "matching"))
+    // "matching" selected: show only images compatible with this system's 
architecture
+    return streq(image_arch, get_system_arch());
+
+  // Specific filter selected: show matching items only
+  return streq(image_arch, target_arch);
+}
+
+// Row at which the scrollable image list starts; shared with choose_image()
+// so it can compute the same viewport height for scroll-offset tracking.
+static const int list_y_start = 6;
+
+// Draws the parts of the screen that only change when the architecture
+// filter changes: header/footer/title (which clear() the whole screen) and
+// the combobox. Kept separate from draw_list() so plain navigation keys
+// don't force a full-screen clear-and-redraw on every keystroke.
+static void
+draw_chrome(int current_arch, const char *title)
+{
+  print_global_header_footer("F1: Help | Left/Right: Change Filter", 
SELECTION);
+  print_title((title ? title : ""));
+
+  // Draw Combobox / Filter Selection
+  mvprintw(4, 2, "Architecture: ");
+  attron(A_REVERSE | A_BOLD);
+  mvprintw(4, 23, " [ %-17s v] ", arch_options[current_arch]);
+  attroff(A_REVERSE | A_BOLD);
+
+  mvprintw(4, 48, "(LEFT/RIGHT: change filter)");
+
+  mvprintw(5, 2, "Available Images:");
+}
+
+static void
+draw_list(size_t current_item, size_t scroll_offset, int max_visible,
+         const char **names, size_t total_matching)
+{
+  if (total_matching == 0)
+    mvprintw(list_y_start + 1, 6, "< No image found >");
+  else
+    render_scrollable_list(list_y_start, max_visible, names, 
(int)total_matching,
+                           (int)current_item, (int)scroll_offset);
+
+  refresh();
+}
+
+// (Re)builds `filtered` with the images matching the current architecture
+// filter, along with `names`, a `filtered`-sized array of pointers into
+// `filtered`'s Image.name fields used for rendering. Only needs to run when
+// the filter changes or on first entry, since the underlying image_list is
+// immutable while choose_image() is running.
+static int
+rebuild_filtered_list(const ImageList *image_list, ImageList *filtered,
+                      const char ***names)
+{
+  free(filtered->data);
+  free(*names);
+  *filtered = (ImageList){ .data = NULL, .size = 0 };
+  *names = NULL;
+
+  if (image_list->size == 0)
+    return 0;
+
+  filtered->data = malloc(image_list->size * sizeof(*image_list->data));
+  if (!filtered->data)
+    {
+      MSG_ERROR("Failed to allocate filtered image list");
+      return -ENOMEM;
+    }
+  filtered->capacity = image_list->size;
+
+  for (size_t i = 0; i < image_list->size; i++)
+    if (item_matches_filter(image_list->data[i].name, 
image_list->data[i].arch, current_arch_idx))
+      filtered->data[filtered->size++] = image_list->data[i];
+
+  if (filtered->size == 0)
+    return 0;
+
+  *names = malloc(filtered->size * sizeof(**names));
+  if (!*names)
+    {
+      MSG_ERROR("Failed to allocate name list for rendering");
+      free(filtered->data);
+      *filtered = (ImageList){ .data = NULL, .size = 0 };
+      return -ENOMEM;
+    }
+
+  for (size_t i = 0; i < filtered->size; i++)
+    (*names)[i] = filtered->data[i].name;
+
+  return 0;
+}
+
+// Selects an image interactively. On success returns 0 and sets *ret_name to
+// the chosen entry (owned by image_list, not to be freed by the caller); on
+// ESC returns -ECANCELED; on allocation failure returns -ENOMEM, distinct
+// from a user-initiated cancel.
+static int
+choose_image(ImageList *image_list, const char *title, char **ret_name)
+{
+  size_t selected = 0;
+  size_t scroll_offset = 0;
+  int last_arch_idx = -1;
+  ImageList filtered = { .data = NULL, .size = 0 };
+  const char **names = NULL;
+  const char *help_text = "Select raw disk image which has to be installed on 
the target system.\n"
+    "The name of the default download server can be changed by the value of 
rdii.download_server, set "
+    "by the kernel cmdline during boot or by a configuration file.\n";
+
+  MSG_FUNC("number of images=%zu", image_list->size);
+
+  *ret_name = NULL;
+
+  while (1)
+    {
+      // Only rebuild the filtered list (and its name projection), and
+      // redraw the chrome (which does a full-screen clear), when the
+      // architecture filter changed - not on every navigation keystroke.
+      if (current_arch_idx != last_arch_idx)
+        {
+          int rc = rebuild_filtered_list(image_list, &filtered, &names);
+          if (rc != 0)
+            return rc;
+          last_arch_idx = current_arch_idx;
+          selected = 0;
+          scroll_offset = 0;
+          draw_chrome(current_arch_idx, title);
+        }
+
+      // Keep 'selected' visible in the viewport, sharing choose_entry()'s
+      // sticky (hysteresis-based) scrolling instead of recentering every frame
+      int max_visible = list_viewport_height(list_y_start);
+      scroll_offset = (size_t)scroll_offset_for_selection((int)selected, 
(int)scroll_offset, max_visible);
+
+      draw_list(selected, scroll_offset, max_visible, names, filtered.size);
+
+      int ch = getch();
+
+      if (ch == 27) // ESC
+        {
+          MSG_INFO("Canceled with ESC");
+          free(filtered.data);
+          free(names);
+          return -ECANCELED;
+        }
+      else if (ch == KEY_UP)
+        {
+          if (filtered.size > 0)
+            selected = (selected - 1 + filtered.size) % filtered.size;
+        }
+      else if (ch == KEY_DOWN)
+        {
+          if (filtered.size > 0)
+            selected = (selected + 1) % filtered.size;
+        }
+      else if (ch == KEY_PPAGE) // Page Up
+        {
+          size_t step = (size_t)max_visible;
+          selected = (selected < step) ? 0 : selected - step;
+        }
+      else if (ch == KEY_NPAGE) // Page Down
+        {
+          size_t step = (size_t)max_visible;
+          selected = (filtered.size == 0) ? 0 :
+            (selected + step >= filtered.size) ? filtered.size - 1 : selected 
+ step;
+        }
+      else if (ch == KEY_LEFT)
+        {
+          current_arch_idx = (current_arch_idx > 0)
+            ? current_arch_idx - 1
+            : arch_options_count - 1;
+        }
+      else if (ch == KEY_RIGHT)
+        {
+          current_arch_idx = (current_arch_idx < arch_options_count - 1)
+            ? current_arch_idx + 1
+            : 0;
+        }
+      else if (ch == KEY_F1)
+        show_help_dialog(title, help_text);
+      else if ((ch == '\n' || ch == KEY_ENTER) && filtered.size > 0)
+        {
+          *ret_name = filtered.data[selected].name;
+          MSG_INFO("Selected entry %zu (Name: %s)", selected, *ret_name);
+          free(filtered.data);
+          free(names);
+          return 0;
+        }
+    }
+}
+
+// Joins `base` and `path` with exactly one '/' between them, regardless of
+// whether `base` already ends with a slash.
+static int
+build_url(char **ret, const char *base, const char *path)
+{
+  size_t len = strlen(base);
+  bool ends_with_slash = len > 0 && base[len - 1] == '/';
+
+  return (asprintf(ret, "%s%s%s", base, ends_with_slash ? "" : "/", path) < 0)
+    ? -ENOMEM : 0;
+}
+
+int
+get_url_from_list(char **ret)
+{
+  _cleanup_free_ char *sha256sums_url = NULL;
+  _cleanup_free_ char *sha256sums_asc_url = NULL;
+  _cleanup_free_ char *sha256sums_fn = NULL;
+  _cleanup_free_ char *sha256sums_asc_fn = NULL;
+  _cleanup_(free_list) ImageList image_list = { .data = NULL, .size = 0, 
.capacity = 0 };
+  _cleanup_(free_arch_options_cleanup) int arch_options_guard = 0;
+  int num_images;
+  char *selected_image = NULL;
+  int r = 0;
+
+  MSG_FUNC();
+
+  r = init_list(&image_list, 2);
+  if (r != 0)
+    return r;
+
+  if (build_url(&sha256sums_url, rdii_download_server, "SHA256SUMS") != 0)
+    return -ENOMEM;
+  if (build_url(&sha256sums_asc_url, rdii_download_server, "SHA256SUMS.asc") 
!= 0)
+    return -ENOMEM;
+  if (build_url(&sha256sums_fn, rdii_tmp_dir, "SHA256SUMS") != 0)
+    return -ENOMEM;
+  if (build_url(&sha256sums_asc_fn, rdii_tmp_dir, "SHA256SUMS.asc") != 0)
+    return -ENOMEM;
+
+  r = curl_download_file(sha256sums_url, sha256sums_fn);
+  if (r != 0)
+    {
+      MSG_ERROR("Error downloading SHA256SUMS file: %s",
+                r < 0 ? strerror(-r) : curl_easy_strerror(r));
+      show_error_popup("Error downloading SHA256SUMS file:",
+                       r < 0 ? strerror(-r) : curl_easy_strerror(r), NULL);
+      return -EIO;
+    }
+
+  r = curl_download_file(sha256sums_asc_url, sha256sums_asc_fn);
+  if (r != 0)
+    {
+      MSG_ERROR("Error downloading SHA256SUMS.asc file: %s",
+                r < 0 ? strerror(-r) : curl_easy_strerror(r));
+      if (!show_warning_popup("Error downloading SHA256SUMS.asc file:",
+                              r < 0 ? strerror(-r) : curl_easy_strerror(r),
+                              "Continue without signature verification?"))
+        return -ECANCELED;
+    }
+  else
+    {
+      char *error_msg = NULL;
+      if (!verify_signature(sha256sums_fn, sha256sums_asc_fn, &error_msg))
+        {
+          MSG_WARN("Cannot verify SHA256SUMS signature: %s", error_msg);
+          if (!show_warning_popup("Cannot verify signature.", error_msg,
+                                  "Continue without signature verification?"))
+            {
+              MSG_ERROR("Canceld");
+              return -ECANCELED;
+            }
+        }
+    }
+
+  num_images = parse_sha256sums(sha256sums_fn, &image_list);
+  if (num_images < 0)
+    {
+      MSG_ERROR("Error parsing SHA256SUMS file: %s",
+                strerror(-num_images));
+      show_error_popup("Error parsing SHA256SUMS file:",
+                       strerror(-num_images), NULL);
+      return num_images;
+    }
+  if (num_images == 0)
+    {
+      MSG_INFO("No supported images found in %s", rdii_download_server);
+      show_error_popup("No supported images found in SHA256SUMS file.",
+                        NULL, NULL);
+      return -ENOENT;
+    }
+
+  _cleanup_free_ char *header = NULL;
+
+  if (asprintf(&header, "Select image from download server %s", 
rdii_download_server) < 0)
+    return -ENOMEM;
+
+  r = choose_image(&image_list, header, &selected_image);
+  if (r == 0)
+    r = build_url(ret, rdii_download_server, selected_image);
+
+  return r;
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/rdi-installer-1.0.0+git20260923.8f88a3f/src/rdii-select-online-image.h 
new/rdi-installer-1.1.0+git20261007.d929392/src/rdii-select-online-image.h
--- old/rdi-installer-1.0.0+git20260923.8f88a3f/src/rdii-select-online-image.h  
1970-01-01 01:00:00.000000000 +0100
+++ new/rdi-installer-1.1.0+git20261007.d929392/src/rdii-select-online-image.h  
2026-10-07 18:23:15.000000000 +0200
@@ -0,0 +1,8 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <stdbool.h>
+
+extern int get_url_from_list(char **ret);
+extern bool is_supported_image(const char *name);

Reply via email to