Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package gearlever for openSUSE:Factory 
checked in at 2025-11-09 21:10:10
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/gearlever (Old)
 and      /work/SRC/openSUSE:Factory/.gearlever.new.1980 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "gearlever"

Sun Nov  9 21:10:10 2025 rev:16 rq:1316678 version:3.4.7

Changes:
--------
--- /work/SRC/openSUSE:Factory/gearlever/gearlever.changes      2025-10-14 
18:10:08.059401732 +0200
+++ /work/SRC/openSUSE:Factory/.gearlever.new.1980/gearlever.changes    
2025-11-09 21:12:38.695047040 +0100
@@ -1,0 +2,9 @@
+Sun Nov  9 13:54:04 UTC 2025 - Jaime Marquínez Ferrándiz 
<[email protected]>
+
+- Update to 3.4.7:
+  * Fix updating from Codeberg and GitLab
+  * Fixed unescaped \ char
+- Update to 3.4.6:
+  * Improved desktop file support: added support for desktop actions
+
+-------------------------------------------------------------------

Old:
----
  gearlever-3.4.5.tar.gz

New:
----
  gearlever-3.4.7.tar.gz

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

Other differences:
------------------
++++++ gearlever.spec ++++++
--- /var/tmp/diff_new_pack.2zEqwp/_old  2025-11-09 21:12:39.287071800 +0100
+++ /var/tmp/diff_new_pack.2zEqwp/_new  2025-11-09 21:12:39.287071800 +0100
@@ -18,7 +18,7 @@
 
 %define appid it.mijorus.gearlever
 Name:           gearlever
-Version:        3.4.5
+Version:        3.4.7
 Release:        0
 Summary:        Manage AppImages
 License:        GPL-3.0-or-later

++++++ gearlever-3.4.5.tar.gz -> gearlever-3.4.7.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/gearlever-3.4.5/data/it.mijorus.gearlever.appdata.xml.in 
new/gearlever-3.4.7/data/it.mijorus.gearlever.appdata.xml.in
--- old/gearlever-3.4.5/data/it.mijorus.gearlever.appdata.xml.in        
2025-10-09 10:35:37.000000000 +0200
+++ new/gearlever-3.4.7/data/it.mijorus.gearlever.appdata.xml.in        
2025-10-30 12:31:49.000000000 +0100
@@ -20,6 +20,16 @@
         <p>An utility to manage AppImages with ease! Gear lever will organize 
and manage AppImage files for you, generate desktop entries and app metadata, 
update apps in-place or keep multiple versions side-by-side.</p>
     </description>
     <releases>
+        <release type="stable" version="3.4.7" date="2025-11-30:00:00Z">
+            <description>
+                <p>- Bug fix</p>
+            </description>
+        </release>
+        <release type="stable" version="3.4.6" date="2025-11-29:00:00Z">
+            <description>
+                <p>- Improved desktop file support: added support for desktop 
actions</p>
+            </description>
+        </release>
         <release type="stable" version="3.4.5" date="2025-10-09:00:00Z">
             <description>
                 <p>- Fixed an issue happening on systems with more than one 
user</p>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gearlever-3.4.5/meson.build 
new/gearlever-3.4.7/meson.build
--- old/gearlever-3.4.5/meson.build     2025-10-09 10:35:37.000000000 +0200
+++ new/gearlever-3.4.7/meson.build     2025-10-30 12:31:49.000000000 +0100
@@ -1,5 +1,5 @@
 project('gearlever',
-          version: '3.4.5',
+          version: '3.4.7',
     meson_version: '>= 0.59.0',
   default_options: [ 'warning_level=2',
                    ],
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gearlever-3.4.5/src/AppDetails.py 
new/gearlever-3.4.7/src/AppDetails.py
--- old/gearlever-3.4.5/src/AppDetails.py       2025-10-09 10:35:37.000000000 
+0200
+++ new/gearlever-3.4.7/src/AppDetails.py       2025-10-30 12:31:49.000000000 
+0100
@@ -123,6 +123,7 @@
         self.env_variables_widgets = []
         self.env_variables_group_container = None
         self.save_vars_btn: Optional[Gtk.Button] = None
+        self.command_line_arguments_row = None
 
         # Update url entry
         self.update_url_group: Optional[Adw.PreferencesGroup] = None
@@ -195,7 +196,8 @@
 
         if self.app_list_element.installed_status is InstalledStatus.INSTALLED:
             # Exec arguments
-            gtk_list.append(self.create_show_exec_args_row())
+            self.command_line_arguments_row = self.create_show_exec_args_row()
+            gtk_list.append(self.command_line_arguments_row)
 
             # A custom link to a website
             gtk_list.append(self.create_edit_custom_website_row())
@@ -251,6 +253,10 @@
     @_async
     def load(self, load_completed_callback: Optional[Callable] = None):
         self.show_row_spinner(True)
+
+        if self.command_line_arguments_row:
+            self.command_line_arguments_row.remove_css_class('error')
+
         icon = Gtk.Image(icon_name='application-x-executable-symbolic')
         generation = self.provider.get_appimage_type(self.app_list_element)
 
@@ -269,10 +275,10 @@
 
     @_async
     def install_file(self, el: AppImageListElement):
-        try:
-            self.provider.install_file(el)
-        except Exception as e:
-            logging.error(str(e))
+        self.provider.install_file(el)
+        # try:
+        # except Exception as e:
+        #     logging.error(str(e))
 
         self.update_installation_status()
 
@@ -691,9 +697,14 @@
     def on_cmd_arguments_changed(self, widget):
         text = widget.get_text().strip()
         text = text.replace('\n', '')
+        widget.remove_css_class('error')
 
-        self.app_list_element.exec_arguments = shlex.split(text)
-        self.provider.update_desktop_file(self.app_list_element)
+        self.app_list_element.exec_arguments = text
+
+        try:
+            self.provider.update_desktop_file(self.app_list_element)
+        except:
+            widget.add_css_class('error')
 
     # Returns the configuration from the json for this specific app
     def get_config_for_app(self) -> dict:
@@ -865,7 +876,7 @@
         row = Adw.EntryRow(
             title=(_('Command line arguments')),
             selectable=False,
-            text=' '.join(self.app_list_element.exec_arguments)
+            text=self.app_list_element.exec_arguments
         )
 
         row_img = Gtk.Image(icon_name='gearlever-cmd-args', 
pixel_size=self.ACTION_ROW_ICON_SIZE)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gearlever-3.4.5/src/main.py 
new/gearlever-3.4.7/src/main.py
--- old/gearlever-3.4.5/src/main.py     2025-10-09 10:35:37.000000000 +0200
+++ new/gearlever-3.4.7/src/main.py     2025-10-30 12:31:49.000000000 +0100
@@ -160,6 +160,9 @@
     if not os.path.exists(LOG_FOLDER):
         os.makedirs(LOG_FOLDER)
 
+    if not os.path.exists(TMP_DIR):
+        os.makedirs(TMP_DIR)
+
     # Clear log file if it's too big
     log_file_size = 0
     if os.path.exists(LOG_FILE): 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gearlever-3.4.5/src/models/UpdateManager.py 
new/gearlever-3.4.7/src/models/UpdateManager.py
--- old/gearlever-3.4.5/src/models/UpdateManager.py     2025-10-09 
10:35:37.000000000 +0200
+++ new/gearlever-3.4.7/src/models/UpdateManager.py     2025-10-30 
12:31:49.000000000 +0100
@@ -489,10 +489,12 @@
             if paths[1] != 'api' or paths[2] != 'v4' or paths[5] != 'packages':
                 return False
 
-        return {
-            'username': paths[4],
-            'filename': paths[9],
-        }
+            return {
+                'username': paths[4],
+                'filename': paths[9],
+            }
+
+        return False
 
     def can_handle_link(url: str):
         return GitlabUpdater.get_url_data(url) != False
@@ -638,11 +640,13 @@
             if len(paths) != 7:
                 return False
 
-        return {
-            'username': paths[1],
-            'repo': paths[2],
-            'filename': paths[6],
-        }
+            return {
+                'username': paths[1],
+                'repo': paths[2],
+                'filename': paths[6],
+            }
+
+        return False
 
     def can_handle_link(url: str):
         return CodebergUpdater.get_url_data(url) != False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/gearlever-3.4.5/src/providers/AppImageProvider.py 
new/gearlever-3.4.7/src/providers/AppImageProvider.py
--- old/gearlever-3.4.5/src/providers/AppImageProvider.py       2025-10-09 
10:35:37.000000000 +0200
+++ new/gearlever-3.4.7/src/providers/AppImageProvider.py       2025-10-30 
12:31:49.000000000 +0100
@@ -4,7 +4,7 @@
 import shutil
 import filecmp
 import shlex
-from xdg import DesktopEntry
+from xdg.DesktopEntry import DesktopEntry
 
 import dataclasses
 from ..lib.constants import APP_ID, TMP_DIR
@@ -22,7 +22,7 @@
 
 class ExtractedAppImage():
     extraction_folder: str
-    desktop_entry: Optional[DesktopEntry.DesktopEntry]
+    desktop_entry: Optional[DesktopEntry]
     appimage_file: Gio.File
     desktop_file: Optional[Gio.File]
     icon_file: Optional[Gio.File]
@@ -47,8 +47,8 @@
     trusted: bool = False
     is_updatable_from_url = False
     env_variables: List[str] = dataclasses.field(default_factory=lambda: [])
-    exec_arguments: List[str] = dataclasses.field(default_factory=lambda: [])
-    desktop_entry: Optional[DesktopEntry.DesktopEntry] = None
+    exec_arguments: str = ''
+    desktop_entry: Optional[DesktopEntry] = None
     update_logic: Optional[AppImageUpdateLogic] = None
     architecture: Optional[AppImageArchitecture] = None
     updating_from: Optional[any] = None # AppImageListElement
@@ -99,7 +99,7 @@
 
             try:
                 if os.path.isfile(gfile.get_path()) and 
get_giofile_content_type(gfile) == 'application/x-desktop':
-                    entry = 
DesktopEntry.DesktopEntry(filename=gfile.get_path())
+                    entry = DesktopEntry(filename=gfile.get_path())
                     exec_location = entry.getTryExec()
                     exec_command_data = 
extract_terminal_arguments(entry.getExec())
 
@@ -121,7 +121,7 @@
                                 desktop_entry=entry,
                                 trusted=True,
                                 external_folder=(not exec_in_defalut_folder),
-                                exec_arguments=exec_command_data['arguments'],
+                                
exec_arguments=shlex.join(exec_command_data['arguments']),
                                 env_variables=exec_command_data['env_vars'],
                             )
 
@@ -346,69 +346,82 @@
             if not os.path.exists(self.user_desktop_files_path):
                 os.makedirs(self.user_desktop_files_path)
 
-            dest_desktop_file_path = 
f'{os.path.join(self.user_desktop_files_path, prefixed_filename)}.desktop'
+            dest_desktop_file_path = 
os.path.join(self.user_desktop_files_path, prefixed_filename) + '.desktop'
             dest_desktop_file_path = dest_desktop_file_path.replace(' ', '_')
 
             # Get default exec arguments
             exec_arguments = 
shlex.split(extracted_appimage.desktop_entry.getExec())[1:]
-            el.exec_arguments = exec_arguments
+            el.exec_arguments = shlex.join(exec_arguments)
 
+            desktop_file_content = ''
             with open(extracted_appimage.desktop_file.get_path(), 'r') as 
dskt_file:
                 desktop_file_content = dskt_file.read()
-                desktop_file_content = re.sub(r'^TryExec=.*$', "", 
desktop_file_content, flags=re.MULTILINE)
-                desktop_file_content = re.sub(r'^Icon=.*$', "", 
desktop_file_content, flags=re.MULTILINE)
-                desktop_file_content = re.sub(r'^X-AppImage-Version=.*$', "", 
desktop_file_content, flags=re.MULTILINE)
-
-                # replace executable path
-                exec_command = ['Exec=' + 
shlex.join([dest_appimage_file.get_path(), *exec_arguments])]
-                # replace try exec executable path
-                exec_command.append(f'TryExec=' + 
dest_appimage_file.get_path())
 
-                if dest_appimage_icon_file:
-                    
exec_command.append(f"Icon={dest_appimage_icon_file.get_path()}")
+            escaped_exec_filepath = 
self._escape_exec_argument(dest_appimage_file.get_path())
+            icon_key = 'Icon=applications-other'
+            if dest_appimage_icon_file:
+                icon_key = dest_appimage_icon_file.get_path()
+            
+            for g in extracted_appimage.desktop_entry.groups():
+                if g == DesktopEntry.defaultGroup:
+                    exec_key = extracted_appimage.desktop_entry.get('Exec', 
group=g)
+                    exec_kg_arguments = shlex.split(exec_key)[1:]
+
+                    exec_line = ' '.join([
+                        escaped_exec_filepath, 
+                        shlex.join(exec_kg_arguments)
+                    ])
+
+                    desktop_file_content = 
self._desktop_file_replace_key(desktop_file_content, g, 'Exec', exec_line)
+                    desktop_file_content = 
self._desktop_file_replace_key(desktop_file_content, g, 'TryExec', 
dest_appimage_file.get_path())
+                    desktop_file_content = 
self._desktop_file_replace_key(desktop_file_content, g, 'Icon', icon_key)
                 else:
-                    exec_command.append(f'Icon=applications-other')
+                    if extracted_appimage.desktop_entry.hasKey('Icon', 
group=g):
+                        desktop_file_content = 
self._desktop_file_replace_key(desktop_file_content, g, 'Icon', icon_key)
 
-                desktop_file_content = re.sub(
-                    r'^Exec=.*$',
-                    '\n'.join(exec_command),
-                    desktop_file_content,
-                    flags=re.MULTILINE
-                )
+                    if extracted_appimage.desktop_entry.hasKey('Exec', 
group=g):
+                        exec_key = 
extracted_appimage.desktop_entry.get('Exec', group=g)
+                        exec_kg_arguments = shlex.split(exec_key)[1:]
+                        exec_line = ' '.join([
+                            escaped_exec_filepath, 
+                            shlex.join(exec_kg_arguments)
+                        ])
 
-                # generate a new app name
-                final_app_name = 
extracted_appimage.appimage_file.get_basename()
-                if extracted_appimage.desktop_entry:
-                    final_app_name = extracted_appimage.desktop_entry.getName()
-                    desktop_file_content += f'\nX-AppImage-Version={version}'
+                        desktop_file_content = self._desktop_file_replace_key(
+                            desktop_file_content, g, 'Exec', exec_line)
 
-                    if el.update_logic is AppImageUpdateLogic.KEEP:
-                        final_app_name += f' ({version})'
+            # generate a new app name
+            final_app_name = extracted_appimage.appimage_file.get_basename()
+            if extracted_appimage.desktop_entry:
+                final_app_name = extracted_appimage.desktop_entry.get('Name', 
group=DesktopEntry.defaultGroup)
 
-                        desktop_file_content = re.sub(
-                            r'^Name\[(.*?)\]=.*$',
-                            '',
-                            desktop_file_content,
-                            flags=re.MULTILINE
-                        )
-
-                final_app_name = final_app_name.strip()
-                desktop_file_content = re.sub(
-                    r'^Name=.*$',
-                    f"Name={final_app_name}",
-                    desktop_file_content,
-                    flags=re.MULTILINE
-                )
+                if el.update_logic is AppImageUpdateLogic.KEEP:
+                    final_app_name += f' ({version})'
+
+            desktop_file_content = self._desktop_file_replace_key(
+                desktop_file_content,
+                group_name=DesktopEntry.defaultGroup,
+                key='X-AppImage-Version',
+                replacement=version
+            )
+
+            final_app_name = final_app_name.strip()
+            desktop_file_content = self._desktop_file_replace_key(
+                desktop_file_content,
+                group_name=DesktopEntry.defaultGroup,
+                key='Name',
+                replacement=final_app_name
+            )
 
-                # finally, write the new .desktop file
-                if (not os.path.exists(self.user_desktop_files_path)) and 
os.path.exists(self.user_local_share_path):
-                    os.mkdir(self.user_desktop_files_path)
+            # finally, write the new .desktop file
+            if (not os.path.exists(self.user_desktop_files_path)) and 
os.path.exists(self.user_local_share_path):
+                os.mkdir(self.user_desktop_files_path)
 
-                with open(dest_desktop_file_path, 'w+') as 
desktop_file_python_dest:
-                    desktop_file_python_dest.write(desktop_file_content)
+            with open(dest_desktop_file_path, 'w+') as 
desktop_file_python_dest:
+                desktop_file_python_dest.write(desktop_file_content)
 
             if os.path.exists(dest_desktop_file_path):
-                el.desktop_entry = 
DesktopEntry.DesktopEntry(filename=dest_desktop_file_path)
+                el.desktop_entry = 
DesktopEntry(filename=dest_desktop_file_path)
                 el.desktop_file_path = dest_desktop_file_path
                 el.installed_status = InstalledStatus.INSTALLED
 
@@ -513,67 +526,47 @@
             shutil.rmtree(self.extraction_folder)
             os.makedirs(self.extraction_folder)
 
-    def update_exec_arguments(self, el:AppImageListElement, arg_string: str):
-        arg_string = arg_string.replace("\n", "")
-
+    def update_desktop_file(self, el: AppImageListElement):
         if not el.desktop_file_path:
             raise Exception('desktop_file_path not specified')
-    
-        desktop_file_content = ''
-        entry = DesktopEntry.DesktopEntry(filename=el.desktop_file_path)
-        with open(el.desktop_file_path, 'r') as desktop_file:
-            desktop_file_content = desktop_file.read()
-            exec_command = shlex.split(entry.getExec())[0]
-            exec_command += f' {arg_string}'
 
-            # replace executable path
-            desktop_file_content = re.sub(
-                r'^Exec=.*$',
-                f"Exec={exec_command}",
-                desktop_file_content,
-                flags=re.MULTILINE
-            )
+        entry = DesktopEntry(filename=el.desktop_file_path)
+        tryexec_command = entry.getTryExec()
+        exec_arguments = el.exec_arguments
+        env_vars = ' '.join(el.env_variables)
+
+        if exec_arguments:
+            exec_arguments = f' {exec_arguments}'
+
+        if env_vars:
+            env_vars = f'env {env_vars} '
+
+        exec_command = ''.join([
+            env_vars,
+            self._escape_exec_argument(tryexec_command),
+            exec_arguments
+        ])
 
-        with open(el.desktop_file_path, 'w') as desktop_file:
-            desktop_file.write(desktop_file_content)         
-
-    def update_desktop_file(self, el: AppImageListElement):
-        if not el.desktop_file_path:
-            raise Exception('desktop_file_path not specified')
-    
         desktop_file_content = ''
-        entry = DesktopEntry.DesktopEntry(filename=el.desktop_file_path)
         with open(el.desktop_file_path, 'r') as desktop_file:
             desktop_file_content = desktop_file.read()
 
-            tryexec_command = entry.getTryExec()
-            exec_arguments = ' '.join(el.exec_arguments)
-            env_vars = ' '.join(el.env_variables)
-
-            if exec_arguments:
-                exec_arguments = f' {exec_arguments}'
-
-            if env_vars:
-                env_vars = f'env {env_vars} '
-
-            exec_command = ''.join([
-                env_vars,
-                shlex.quote(tryexec_command),
-                exec_arguments
-            ])
-
-            # replace executable path
-            desktop_file_content = re.sub(
-                r'^Exec=.*$',
-                f"Exec={exec_command}",
-                desktop_file_content,
-                flags=re.MULTILINE
-            )
+        # replace executable path
+        desktop_file_content = self._desktop_file_replace_key(
+            desktop_file_content,
+            group_name=DesktopEntry.defaultGroup,
+            key='Exec',
+            replacement=exec_command
+        )
 
-        with open(el.desktop_file_path, 'w') as desktop_file:
+        tmp_desktop_file = os.path.join(TMP_DIR, get_random_string() + 
'.desktop')
+        with open(tmp_desktop_file, 'w+') as desktop_file:
             desktop_file.write(desktop_file_content)
 
-        el.desktop_entry = 
DesktopEntry.DesktopEntry(filename=el.desktop_file_path)
+        terminal.host_sh(['desktop-file-validate', '--no-hints', 
'--no-warn-deprecated', tmp_desktop_file])
+
+        shutil.move(tmp_desktop_file, el.desktop_file_path)
+        el.desktop_entry = DesktopEntry(filename=el.desktop_file_path)
 
     def update_from_url(self, manager, el: AppImageListElement, status_cb: 
callable) -> AppImageListElement:
         try:
@@ -600,6 +593,45 @@
         return list_element
 
     # Private methods
+    def _escape_exec_argument(self, arg: str) -> str:
+        """
+        Escape an input string according to the Exec key quoting rules
+        from the freedesktop.org Desktop Entry Specification.
+
+        Escapes:
+        - Double quote (")
+        - Backtick (`)
+        - Dollar sign ($)
+        - Backslash (\\)
+
+        Returns the escaped string, enclosed in double quotes if needed.
+        """
+        reserved_chars = set(' \t\n"\'\\><~|&;$*?#()`')
+
+        def escape_inner(s: str) -> str:
+            s = s.replace("\\", "\\\\")  # Escape backslash first
+            s = s.replace('"', r'\"')
+            s = s.replace('`', r'\`')
+            s = s.replace('$', r'\$')
+            return s
+
+        if any(ch in reserved_chars for ch in arg):
+            return f'"{escape_inner(arg)}"'
+        else:
+            return arg
+
+    def _desktop_file_replace_key(self, content: str, group_name: str, key: 
str, replacement: str):
+        pattern = rf'(\[{group_name}\][\s\S]*?)^{key}=.*$'
+
+        if re.search(pattern, content, flags=re.MULTILINE):
+            replacement = rf'\1{key}={replacement}'
+        else:
+            pattern = rf'\[{group_name}\].*$'
+            replacement = f'[{group_name}]\n{key}={replacement}'
+
+
+        return re.sub(pattern, replacement, content, flags=re.MULTILINE, 
count=1)
+
     def _run_filepath(self, el: AppImageListElement):
         is_nixos = re.search(r"^NAME=NixOS$", get_osinfo(), re.MULTILINE) != 
None
 
@@ -708,7 +740,6 @@
                 appimage_offset = terminal.sandbox_sh(['get_appimage_offset', 
file.get_path()])
 
                 try:
-                    terminal.sandbox_sh(['unsquashfs', '-o', appimage_offset, 
'-l', file.get_path()])
                     terminal.sandbox_sh(['unsquashfs', '-o', appimage_offset, 
'-d', squashfs_root_folder, file.get_path()])
                 except Exception as e:
                     logging.error('Extraction with unsquashfs failed')
@@ -732,7 +763,7 @@
 
         icon_file: Optional[Gio.File] = None
         desktop_file: Optional[Gio.File] = None
-        desktop_entry: Optional[DesktopEntry.DesktopEntry] = None
+        desktop_entry: Optional[DesktopEntry] = None
 
         # hash file
         md5_hash = get_file_hash(file)
@@ -763,7 +794,7 @@
 
                 desktop_entry_icon = None
                 if desktop_file:
-                    desktop_entry = 
DesktopEntry.DesktopEntry(desktop_file.get_path())
+                    desktop_entry = DesktopEntry(desktop_file.get_path())
                     desktop_entry_icon = desktop_entry.getIcon()
                     desktop_entry_icon = re.sub(r"\.(png|svg)$", '', 
desktop_entry_icon)
 

Reply via email to