Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package plexus-sec-dispatcher4 for 
openSUSE:Factory checked in at 2026-11-06 19:58:44
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/plexus-sec-dispatcher4 (Old)
 and      /work/SRC/openSUSE:Factory/.plexus-sec-dispatcher4.new.3759290 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "plexus-sec-dispatcher4"

Fri Nov  6 19:58:44 2026 rev:4 rq:1395845 version:4.3.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/plexus-sec-dispatcher4/plexus-sec-dispatcher4.changes
    2026-09-28 10:39:33.219607982 +0200
+++ 
/work/SRC/openSUSE:Factory/.plexus-sec-dispatcher4.new.3759290/plexus-sec-dispatcher4.changes
       2026-11-06 20:06:51.845590007 +0100
@@ -1,0 +2,23 @@
+Fri Nov  6 10:44:58 UTC 2026 - Fridrich Strba <[email protected]>
+
+- Update to version 4.3.0
+  * New features and improvements
+    + Try to remove sensitive parts from memory more aggressively
+    + Add Git Credential Helper as master password source
+  * Documentation updates
+    + Stop pinning a version in the README snippet
+  * Maintenance
+    + Clean up declared dependencies
+  * Build
+    + Point the site URL at the published site so site:stage works
+    + Allow running the release drafter by hand
+  * Dependency updates
+    + Bump org.eclipse.sisu:org.eclipse.sisu.inject from 1.1.0 to
+      1.1.1
+    + Bump release-drafter/release-drafter from 7.7.0 to 7.10.0
+    + Bump version.slf4j from 2.0.18 to 2.0.20
+    + Bump org.codehaus.modello:modello-maven-plugin from 2.8.0 to
+      2.8.1
+    + Bump github/codeql-action from 4.37.7 to 4.37.8
+
+-------------------------------------------------------------------

Old:
----
  sec-dispatcher-4.2.0.tar.gz

New:
----
  sec-dispatcher-4.3.0.tar.gz

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

Other differences:
------------------
++++++ plexus-sec-dispatcher4.spec ++++++
--- /var/tmp/diff_new_pack.iw1Rc9/_old  2026-11-06 20:06:52.995638202 +0100
+++ /var/tmp/diff_new_pack.iw1Rc9/_new  2026-11-06 20:06:52.998638328 +0100
@@ -15,18 +15,19 @@
 # Please submit bugfixes or comments via https://bugs.opensuse.org/
 #
 
+
 %global fragment_name sec-dispatcher
 %global base_name plexus-%{fragment_name}
 %global version_suffix 4
 Name:           %{base_name}%{version_suffix}
-Version:        4.2.0
+Version:        4.3.0
 Release:        0
 Summary:        Plexus Security Dispatcher Component
 License:        Apache-2.0
 Group:          Development/Libraries/Java
 URL:            https://github.com/codehaus-plexus/%{base_name}
 Source0:        
https://github.com/codehaus-plexus/plexus-sec-dispatcher/archive/refs/tags/%{fragment_name}-%{version}.tar.gz
-Source1:        http://www.apache.org/licenses/LICENSE-2.0.txt
+Source1:        https://www.apache.org/licenses/LICENSE-2.0.txt
 Source100:      %{name}-build.xml
 BuildRequires:  ant
 BuildRequires:  atinject

++++++ _scmsync.obsinfo ++++++
--- /var/tmp/diff_new_pack.iw1Rc9/_old  2026-11-06 20:06:53.113643147 +0100
+++ /var/tmp/diff_new_pack.iw1Rc9/_new  2026-11-06 20:06:53.124643608 +0100
@@ -1,6 +1,6 @@
-mtime: 1790293737
-commit: 55fe35cb379d9d790d590f942ee5a1e2c79408d2670a964f98470b2fea43cae1
+mtime: 1793962123
+commit: 34ad73c25001af1e3e21c9246b2ff32301cf607b6218db8404e97db4dbaca196
 url: https://src.opensuse.org/java-packages/plexus-sec-dispatcher4
-revision: 55fe35cb379d9d790d590f942ee5a1e2c79408d2670a964f98470b2fea43cae1
+revision: 34ad73c25001af1e3e21c9246b2ff32301cf607b6218db8404e97db4dbaca196
 projectscmsync: https://src.opensuse.org/java-packages/_ObsPrj
 

++++++ build.specials.obscpio ++++++

++++++ build.specials.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/.gitignore new/.gitignore
--- old/.gitignore      1970-01-01 01:00:00.000000000 +0100
+++ new/.gitignore      2026-11-06 11:48:43.000000000 +0100
@@ -0,0 +1 @@
+.osc

++++++ plexus-sec-dispatcher4-build.xml ++++++
--- /var/tmp/diff_new_pack.iw1Rc9/_old  2026-11-06 20:06:53.463657815 +0100
+++ /var/tmp/diff_new_pack.iw1Rc9/_new  2026-11-06 20:06:53.469658067 +0100
@@ -10,7 +10,7 @@
 
   <property name="project.groupId" value="org.codehaus.plexus"/>
   <property name="project.artifactId" value="plexus-sec-dispatcher"/>
-  <property name="project.version" value="4.2.0"/>
+  <property name="project.version" value="4.3.0"/>
   <property name="project.url" 
value="https://github.com/codehaus-plexus/plexus-sec-dispatcher"/>
   <property name="project.name" value="Plexus Security Dispatcher Component"/>
   <property name="project.organization.name" value="Sonatype, Inc."/>

++++++ sec-dispatcher-4.2.0.tar.gz -> sec-dispatcher-4.3.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/.github/workflows/codeql.yml 
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/.github/workflows/codeql.yml
--- old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/.github/workflows/codeql.yml 
2026-08-19 21:22:50.000000000 +0200
+++ new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/.github/workflows/codeql.yml 
2026-11-06 11:38:06.000000000 +0100
@@ -59,7 +59,7 @@
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/[email protected]
+      uses: github/codeql-action/init@v4
       with:
         languages: ${{ matrix.language }}
         build-mode: ${{ matrix.build-mode }}
@@ -87,6 +87,6 @@
         exit 1
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/[email protected]
+      uses: github/codeql-action/analyze@v4
       with:
         category: "/language:${{matrix.language}}"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/.github/workflows/maven.yml 
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/.github/workflows/maven.yml
--- old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/.github/workflows/maven.yml  
2026-08-19 21:22:50.000000000 +0200
+++ new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/.github/workflows/maven.yml  
2026-11-06 11:38:06.000000000 +0100
@@ -17,7 +17,7 @@
 
 name: GitHub CI
 
-on: [push, pull_request]
+on: [push, pull_request, workflow_dispatch]
 
 jobs:
   build:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/.github/workflows/release-drafter.yml
 
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/.github/workflows/release-drafter.yml
--- 
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/.github/workflows/release-drafter.yml
        2026-08-19 21:22:50.000000000 +0200
+++ 
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/.github/workflows/release-drafter.yml
        2026-11-06 11:38:06.000000000 +0100
@@ -3,10 +3,11 @@
   push:
     branches:
       - master
+  workflow_dispatch:
 jobs:
   update_release_draft:
     runs-on: ubuntu-latest
     steps:
-      - uses: release-drafter/[email protected]
+      - uses: release-drafter/[email protected]
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/README.md 
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/README.md
--- old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/README.md    2026-08-19 
21:22:50.000000000 +0200
+++ new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/README.md    2026-11-06 
11:38:06.000000000 +0100
@@ -23,7 +23,6 @@
 <dependency>
   <groupId>org.codehaus.plexus</groupId>
   <artifactId>plexus-sec-dispatcher</artifactId>
-  <version>4.1.0</version>
 </dependency>
 ```
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/pom.xml 
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/pom.xml
--- old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/pom.xml      2026-08-19 
21:22:50.000000000 +0200
+++ new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/pom.xml      2026-11-06 
11:38:06.000000000 +0100
@@ -10,7 +10,7 @@
   </parent>
 
   <artifactId>plexus-sec-dispatcher</artifactId>
-  <version>4.2.0</version>
+  <version>4.3.0</version>
 
   <name>Plexus Security Dispatcher Component</name>
   <description>This library provides encryption/decryption functionality with 
pluggable ciphers and password providers</description>
@@ -18,7 +18,7 @@
   <scm>
     
<connection>scm:git:https://github.com/codehaus-plexus/plexus-sec-dispatcher.git</connection>
     <developerConnection>${project.scm.connection}</developerConnection>
-    <tag>sec-dispatcher-4.2.0</tag>
+    <tag>sec-dispatcher-4.3.0</tag>
     
<url>https://github.com/codehaus-plexus/plexus-sec-dispatcher/tree/${project.scm.tag}/</url>
   </scm>
 
@@ -30,16 +30,17 @@
   <distributionManagement>
     <site>
       <id>github:gh-pages</id>
-      <url>${project.scm.developerConnection}</url>
+      <!-- the published site; scm-publish takes its target from pubScmUrl, 
and site:deploy is skipped -->
+      <url>https://codehaus-plexus.github.io/plexus-sec-dispatcher/</url>
     </site>
   </distributionManagement>
 
   <properties>
     <javaVersion>17</javaVersion>
-    
<project.build.outputTimestamp>2026-08-19T19:22:39Z</project.build.outputTimestamp>
+    
<project.build.outputTimestamp>2026-11-06T10:37:51Z</project.build.outputTimestamp>
 
-    <version.sisu>1.1.0</version.sisu>
-    <version.slf4j>2.0.18</version.slf4j>
+    <version.sisu>1.1.1</version.sisu>
+    <version.slf4j>2.0.20</version.slf4j>
   </properties>
 
   <dependencies>
@@ -55,12 +56,6 @@
       <version>1</version>
       <scope>provided</scope>
     </dependency>
-    <dependency>
-      <groupId>org.eclipse.sisu</groupId>
-      <artifactId>org.eclipse.sisu.inject</artifactId>
-      <version>${version.sisu}</version>
-      <scope>provided</scope>
-    </dependency>
 
     <dependency>
       <groupId>org.junit.jupiter</groupId>
@@ -113,7 +108,7 @@
       <plugin>
         <groupId>org.codehaus.modello</groupId>
         <artifactId>modello-maven-plugin</artifactId>
-        <version>2.8.0</version>
+        <version>2.8.1</version>
         <configuration>
           <version>4.0.0</version>
           <models>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/AESGCMNoPadding.java
 
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/AESGCMNoPadding.java
--- 
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/AESGCMNoPadding.java
      2026-08-19 21:22:50.000000000 +0200
+++ 
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/cipher/AESGCMNoPadding.java
      2026-11-06 11:38:06.000000000 +0100
@@ -27,13 +27,14 @@
 import javax.crypto.spec.SecretKeySpec;
 import javax.inject.Named;
 import javax.inject.Singleton;
+import javax.security.auth.DestroyFailedException;
 
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.security.spec.InvalidKeySpecException;
-import java.security.spec.KeySpec;
+import java.util.Arrays;
 import java.util.Base64;
 
 import org.codehaus.plexus.components.secdispatcher.CipherException;
@@ -57,15 +58,19 @@
             byte[] salt = getRandomNonce(SALT_LENGTH_BYTE);
             byte[] iv = getRandomNonce(IV_LENGTH_BYTE);
             SecretKey secretKey = 
getAESKeyFromPassword(password.toCharArray(), salt);
-            Cipher cipher = Cipher.getInstance(CIPHER_ALG);
-            cipher.init(Cipher.ENCRYPT_MODE, secretKey, new 
GCMParameterSpec(TAG_LENGTH_BIT, iv));
-            byte[] cipherText = 
cipher.doFinal(clearText.getBytes(StandardCharsets.UTF_8));
-            byte[] cipherTextWithIvSalt = ByteBuffer.allocate(iv.length + 
salt.length + cipherText.length)
-                    .put(iv)
-                    .put(salt)
-                    .put(cipherText)
-                    .array();
-            return Base64.getEncoder().encodeToString(cipherTextWithIvSalt);
+            try {
+                Cipher cipher = Cipher.getInstance(CIPHER_ALG);
+                cipher.init(Cipher.ENCRYPT_MODE, secretKey, new 
GCMParameterSpec(TAG_LENGTH_BIT, iv));
+                byte[] cipherText = 
cipher.doFinal(clearText.getBytes(StandardCharsets.UTF_8));
+                byte[] cipherTextWithIvSalt = ByteBuffer.allocate(iv.length + 
salt.length + cipherText.length)
+                        .put(iv)
+                        .put(salt)
+                        .put(cipherText)
+                        .array();
+                return 
Base64.getEncoder().encodeToString(cipherTextWithIvSalt);
+            } finally {
+                destroyQuietly(secretKey);
+            }
         } catch (Exception e) {
             throw new CipherException("Failed encrypting", e);
         }
@@ -83,10 +88,14 @@
             byte[] cipherText = new byte[buffer.remaining()];
             buffer.get(cipherText);
             SecretKey secretKey = 
getAESKeyFromPassword(password.toCharArray(), salt);
-            Cipher cipher = Cipher.getInstance(CIPHER_ALG);
-            cipher.init(Cipher.DECRYPT_MODE, secretKey, new 
GCMParameterSpec(TAG_LENGTH_BIT, iv));
-            byte[] plainText = cipher.doFinal(cipherText);
-            return new String(plainText, StandardCharsets.UTF_8);
+            try {
+                Cipher cipher = Cipher.getInstance(CIPHER_ALG);
+                cipher.init(Cipher.DECRYPT_MODE, secretKey, new 
GCMParameterSpec(TAG_LENGTH_BIT, iv));
+                byte[] plainText = cipher.doFinal(cipherText);
+                return new String(plainText, StandardCharsets.UTF_8);
+            } finally {
+                destroyQuietly(secretKey);
+            }
         } catch (Exception e) {
             throw new CipherException("Failed decrypting", e);
         }
@@ -100,8 +109,36 @@
 
     private static SecretKey getAESKeyFromPassword(char[] password, byte[] 
salt)
             throws NoSuchAlgorithmException, InvalidKeySpecException {
-        SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_FACTORY);
-        KeySpec spec = new PBEKeySpec(password, salt, PBE_ITERATIONS, 
PBE_KEY_SIZE);
-        return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), 
KEY_ALGORITHM);
+        PBEKeySpec spec = new PBEKeySpec(password, salt, PBE_ITERATIONS, 
PBE_KEY_SIZE);
+        try {
+            SecretKeyFactory factory = 
SecretKeyFactory.getInstance(KEY_FACTORY);
+            SecretKey originalKey = factory.generateSecret(spec);
+            byte[] encodedKey = originalKey.getEncoded();
+            try {
+                return new SecretKeySpec(encodedKey, KEY_ALGORITHM);
+            } finally {
+                destroyQuietly(encodedKey);
+                destroyQuietly(originalKey);
+            }
+        } finally {
+            spec.clearPassword();
+            // also clear the passed password array to avoid leaving sensitive 
data in memory
+            Arrays.fill(password, '\0');
+        }
+    }
+
+    private static void destroyQuietly(byte[] data) {
+        if (data != null) {
+            Arrays.fill(data, (byte) 0x00);
+        }
+    }
+
+    private static void destroyQuietly(SecretKey key) {
+        try {
+            key.destroy();
+        } catch (DestroyFailedException e) {
+            // Ignore exception during key destruction as not all SecretKey 
implementations support destruction
+            // https://bugs.openjdk.org/browse/JDK-8389121
+        }
     }
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GitCredentialHelperMasterSource.java
 
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GitCredentialHelperMasterSource.java
--- 
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GitCredentialHelperMasterSource.java
     1970-01-01 01:00:00.000000000 +0100
+++ 
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/src/main/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GitCredentialHelperMasterSource.java
     2026-11-06 11:38:06.000000000 +0100
@@ -0,0 +1,267 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.plexus.components.secdispatcher.internal.sources;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import org.codehaus.plexus.components.secdispatcher.MasterSourceMeta;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
+
+/**
+ * Password source that uses Git Credential Helpers.
+ * <p>
+ * Git credential helpers have a common interface for retrieving credentials.
+ * This master source allows using any git credential helper to retrieve 
passwords.
+ * <p>
+ * Config: {@code git-credential:helper-name?url=protocol://host/path}
+ * <p>
+ * Examples:
+ * <ul>
+ *   <li>{@code git-credential:cache?url=https://maven.apache.org/master}</li>
+ *   <li>{@code git-credential:store?url=https://maven.apache.org/master}</li>
+ *   <li>{@code 
git-credential:/usr/local/bin/git-credential-osxkeychain?url=https://maven.apache.org/master}</li>
+ * </ul>
+ *
+ * @see <a href="https://git-scm.com/docs/gitcredentials";>Git Credentials</a>
+ * @see <a href="https://git-scm.com/doc/credential-helpers";>Git Credential 
Helpers</a>
+ */
+@Singleton
+@Named(GitCredentialHelperMasterSource.NAME)
+public final class GitCredentialHelperMasterSource extends 
PrefixMasterSourceSupport implements MasterSourceMeta {
+    public static final String NAME = "git-credential";
+
+    public GitCredentialHelperMasterSource() {
+        super(NAME + ":");
+    }
+
+    @Override
+    public String description() {
+        return "Git Credential Helper (helper name and URL should be edited)";
+    }
+
+    @Override
+    public Optional<String> configTemplate() {
+        return Optional.of(NAME + ":helper-name?url=protocol://host/path");
+    }
+
+    @Override
+    protected String doHandle(String transformed) throws 
SecDispatcherException {
+        String helperName;
+        String url;
+
+        // Parse configuration: helper-name?url=protocol://host/path
+        int queryIndex = transformed.indexOf('?');
+        if (queryIndex < 0) {
+            throw new SecDispatcherException(
+                    "Invalid git-credential configuration. Expected format: 
git-credential:helper-name?url=protocol://host/path");
+        }
+
+        helperName = transformed.substring(0, queryIndex);
+        String query = transformed.substring(queryIndex + 1);
+
+        if (!query.startsWith("url=")) {
+            throw new SecDispatcherException(
+                    "Invalid git-credential configuration. Expected URL 
parameter: url=protocol://host/path");
+        }
+
+        url = query.substring(4);
+
+        try {
+            return retrievePassword(helperName, url);
+        } catch (IOException | InterruptedException e) {
+            throw new SecDispatcherException(
+                    String.format(
+                            "Failed to retrieve password from git credential 
helper '%s': %s",
+                            helperName, e.getMessage()),
+                    e);
+        }
+    }
+
+    @Override
+    protected SecDispatcher.ValidationResponse doValidateConfiguration(String 
transformed) {
+        HashMap<SecDispatcher.ValidationResponse.Level, List<String>> report = 
new HashMap<>();
+        boolean isValid = false;
+
+        try {
+            int queryIndex = transformed.indexOf('?');
+            if (queryIndex < 0) {
+                
report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new 
ArrayList<>())
+                        .add(
+                                "Invalid configuration format. Expected: 
git-credential:helper-name?url=protocol://host/path");
+                return new 
SecDispatcher.ValidationResponse(getClass().getSimpleName(), false, report, 
List.of());
+            }
+
+            String helperName = transformed.substring(0, queryIndex);
+            String query = transformed.substring(queryIndex + 1);
+
+            if (!query.startsWith("url=")) {
+                
report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new 
ArrayList<>())
+                        .add("Invalid configuration. Expected URL parameter: 
url=protocol://host/path");
+                return new 
SecDispatcher.ValidationResponse(getClass().getSimpleName(), false, report, 
List.of());
+            }
+
+            String url = query.substring(4);
+
+            // Validate URL format
+            try {
+                new URI(url);
+            } catch (URISyntaxException e) {
+                
report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new 
ArrayList<>())
+                        .add(String.format("Invalid URL format: %s", 
e.getMessage()));
+                return new 
SecDispatcher.ValidationResponse(getClass().getSimpleName(), false, report, 
List.of());
+            }
+
+            // Try to execute the helper to see if it's available
+            String helperCommand = buildHelperCommand(helperName);
+            try {
+                Process process = new ProcessBuilder(helperCommand, 
"get").start();
+                // Close stdin to prevent the helper from waiting for input
+                process.getOutputStream().close();
+
+                if (!process.waitFor(2, TimeUnit.SECONDS)) {
+                    process.destroyForcibly();
+                    
report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.WARNING, k -> new 
ArrayList<>())
+                            .add(String.format(
+                                    "Git credential helper '%s' did not 
respond in time. It may still work.",
+                                    helperName));
+                    isValid = true; // Still consider it valid, just warn
+                } else if (process.exitValue() == 127 || process.exitValue() 
== 126) {
+                    
report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new 
ArrayList<>())
+                            .add(String.format("Git credential helper '%s' not 
found or not executable", helperName));
+                } else {
+                    
report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.INFO, k -> new 
ArrayList<>())
+                            .add(String.format("Git credential helper '%s' is 
available", helperName));
+                    isValid = true;
+                }
+            } catch (IOException e) {
+                
report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new 
ArrayList<>())
+                        .add(String.format(
+                                "Failed to execute git credential helper '%s': 
%s", helperName, e.getMessage()));
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                
report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new 
ArrayList<>())
+                        .add("Validation was interrupted");
+            }
+        } catch (Exception e) {
+            
report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new 
ArrayList<>())
+                    .add(String.format("Validation error: %s", 
e.getMessage()));
+        }
+
+        return new 
SecDispatcher.ValidationResponse(getClass().getSimpleName(), isValid, report, 
List.of());
+    }
+
+    private String retrievePassword(String helperName, String url) throws 
IOException, InterruptedException {
+        String helperCommand = buildHelperCommand(helperName);
+
+        ProcessBuilder pb = new ProcessBuilder(helperCommand, "get");
+        Process process = pb.start();
+
+        // Write credential request to helper's stdin
+        try (PrintWriter writer = new PrintWriter(new 
OutputStreamWriter(process.getOutputStream()))) {
+            URI uri = new URI(url);
+            if (uri.getScheme() != null) {
+                writer.println("protocol=" + uri.getScheme());
+            }
+            if (uri.getHost() != null && !uri.getHost().isEmpty()) {
+                if (uri.getPort() != -1) {
+                    writer.println("host=" + uri.getHost() + ":" + 
uri.getPort());
+                } else {
+                    writer.println("host=" + uri.getHost());
+                }
+            }
+            if (uri.getPath() != null && !uri.getPath().isEmpty()) {
+                writer.println("path=" + uri.getPath());
+            }
+            writer.println(); // Blank line signals end of input
+            writer.flush();
+        } catch (URISyntaxException e) {
+            throw new IOException("Invalid URL format: " + e.getMessage(), e);
+        }
+
+        // Read response from helper's stdout
+        String password = null;
+        try (BufferedReader reader = new BufferedReader(new 
InputStreamReader(process.getInputStream()))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                if (line.startsWith("password=")) {
+                    password = line.substring(9);
+                    break;
+                }
+            }
+        }
+
+        if (!process.waitFor(30, TimeUnit.SECONDS)) {
+            process.destroyForcibly();
+            throw new IOException("Git credential helper timed out");
+        }
+
+        int exitCode = process.exitValue();
+        if (exitCode != 0) {
+            String errorOutput = readProcessError(process);
+            throw new IOException(
+                    String.format("Git credential helper exited with code %d. 
Error: %s", exitCode, errorOutput));
+        }
+
+        if (password == null || password.isEmpty()) {
+            throw new IOException("Git credential helper did not return a 
password");
+        }
+
+        return password;
+    }
+
+    static String buildHelperCommand(String helperName) {
+        // If helper name contains a path separator, use it as-is (absolute or 
relative path)
+        // Otherwise, prefix with "git-credential-"
+        if (helperName.contains("/") || helperName.contains("\\")) {
+            return helperName;
+        }
+        return "git-credential-" + helperName;
+    }
+
+    private String readProcessError(Process process) {
+        try (BufferedReader reader = new BufferedReader(new 
InputStreamReader(process.getErrorStream()))) {
+            StringBuilder sb = new StringBuilder();
+            String line;
+            while ((line = reader.readLine()) != null) {
+                if (sb.length() > 0) {
+                    sb.append("; ");
+                }
+                sb.append(line);
+            }
+            return sb.toString();
+        } catch (IOException e) {
+            return "(failed to read error output)";
+        }
+    }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GitCredentialHelperMasterSourceTest.java
 
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GitCredentialHelperMasterSourceTest.java
--- 
old/plexus-sec-dispatcher-sec-dispatcher-4.2.0/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GitCredentialHelperMasterSourceTest.java
 1970-01-01 01:00:00.000000000 +0100
+++ 
new/plexus-sec-dispatcher-sec-dispatcher-4.3.0/src/test/java/org/codehaus/plexus/components/secdispatcher/internal/sources/GitCredentialHelperMasterSourceTest.java
 2026-11-06 11:38:06.000000000 +0100
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.plexus.components.secdispatcher.internal.sources;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Set;
+
+import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
+import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class GitCredentialHelperMasterSourceTest {
+
+    @TempDir
+    static Path tempDir;
+
+    static Path mockHelperPath;
+
+    @BeforeAll
+    static void setup() throws IOException {
+        // Create a mock git credential helper script
+        // On Windows, create a batch file; on Unix-like systems, create a 
shell script
+        if (System.getProperty("os.name").toLowerCase().contains("win")) {
+            mockHelperPath = tempDir.resolve("mock-git-credential-helper.bat");
+            String batchScript = "@echo off\r\n"
+                    + "if \"%1\"==\"get\" (\r\n"
+                    + "  REM Read input until empty line\r\n"
+                    + "  :loop\r\n"
+                    + "  set /p line=\r\n"
+                    + "  if not defined line goto output\r\n"
+                    + "  goto loop\r\n"
+                    + "  :output\r\n"
+                    + "  echo protocol=https\r\n"
+                    + "  echo host=maven.apache.org\r\n"
+                    + "  echo username=testuser\r\n"
+                    + "  echo password=testPassword123\r\n"
+                    + ")\r\n";
+            Files.writeString(mockHelperPath, batchScript);
+        } else {
+            mockHelperPath = tempDir.resolve("mock-git-credential-helper");
+            String script = "#!/bin/sh\n"
+                    + "if [ \"$1\" = \"get\" ]; then\n"
+                    + "  # Read input (we don't actually parse it in this 
simple mock)\n"
+                    + "  while IFS= read -r line; do\n"
+                    + "    [ -z \"$line\" ] && break\n"
+                    + "  done\n"
+                    + "  # Return mock credentials\n"
+                    + "  echo \"protocol=https\"\n"
+                    + "  echo \"host=maven.apache.org\"\n"
+                    + "  echo \"username=testuser\"\n"
+                    + "  echo \"password=testPassword123\"\n"
+                    + "fi\n";
+
+            Files.writeString(mockHelperPath, script);
+            // Make it executable on Unix-like systems
+            Files.setPosixFilePermissions(
+                    mockHelperPath,
+                    Set.of(
+                            PosixFilePermission.OWNER_READ,
+                            PosixFilePermission.OWNER_WRITE,
+                            PosixFilePermission.OWNER_EXECUTE));
+        }
+    }
+
+    @Test
+    void testMetadata() {
+        GitCredentialHelperMasterSource source = new 
GitCredentialHelperMasterSource();
+
+        assertNotNull(source.description());
+        assertTrue(source.description().contains("Git Credential Helper"));
+
+        assertTrue(source.configTemplate().isPresent());
+        assertEquals(
+                "git-credential:helper-name?url=protocol://host/path",
+                source.configTemplate().get());
+    }
+
+    @Test
+    void testHandleReturnsNullForNonMatchingPrefix() throws 
SecDispatcherException {
+        GitCredentialHelperMasterSource source = new 
GitCredentialHelperMasterSource();
+
+        assertNull(source.handle("env:SOME_VAR"));
+        assertNull(source.handle("file:/path/to/file"));
+        assertNull(source.handle("other-prefix:value"));
+    }
+
+    @Test
+    void testHandleThrowsExceptionForMissingUrlParameter() {
+        GitCredentialHelperMasterSource source = new 
GitCredentialHelperMasterSource();
+
+        SecDispatcherException exception =
+                assertThrows(SecDispatcherException.class, () -> 
source.handle("git-credential:helper-name"));
+
+        assertTrue(exception.getMessage().contains("Expected format"));
+    }
+
+    @Test
+    void testHandleThrowsExceptionForInvalidUrlParameter() {
+        GitCredentialHelperMasterSource source = new 
GitCredentialHelperMasterSource();
+
+        SecDispatcherException exception = assertThrows(
+                SecDispatcherException.class, () -> 
source.handle("git-credential:helper-name?invalid=value"));
+
+        assertTrue(exception.getMessage().contains("Expected URL parameter"));
+    }
+
+    @Test
+    @DisabledOnOs(
+            value = OS.WINDOWS,
+            disabledReason = "Windows batch files don't handle closed stdin 
gracefully in validation tests")
+    void testHandleWithMockHelper() throws SecDispatcherException {
+        GitCredentialHelperMasterSource source = new 
GitCredentialHelperMasterSource();
+
+        String config = mockHelperPath.toString() + 
"?url=https://maven.apache.org/master";;
+        String password = source.handle("git-credential:" + config);
+
+        assertEquals("testPassword123", password);
+    }
+
+    @Test
+    void testValidateConfigurationWithInvalidFormat() {
+        GitCredentialHelperMasterSource source = new 
GitCredentialHelperMasterSource();
+
+        SecDispatcher.ValidationResponse response = 
source.validateConfiguration("git-credential:invalid-format");
+
+        assertNotNull(response);
+        assertFalse(response.isValid());
+        
assertTrue(response.getReport().containsKey(SecDispatcher.ValidationResponse.Level.ERROR));
+    }
+
+    @Test
+    void testValidateConfigurationWithInvalidUrl() {
+        GitCredentialHelperMasterSource source = new 
GitCredentialHelperMasterSource();
+
+        SecDispatcher.ValidationResponse response =
+                
source.validateConfiguration("git-credential:helper?url=invalid url with 
spaces");
+
+        assertNotNull(response);
+        assertFalse(response.isValid());
+        
assertTrue(response.getReport().containsKey(SecDispatcher.ValidationResponse.Level.ERROR));
+    }
+
+    @Test
+    void testValidateConfigurationWithNonMatchingPrefix() {
+        GitCredentialHelperMasterSource source = new 
GitCredentialHelperMasterSource();
+
+        SecDispatcher.ValidationResponse response = 
source.validateConfiguration("env:SOME_VAR");
+
+        assertNull(response);
+    }
+
+    @Test
+    @DisabledOnOs(
+            value = OS.WINDOWS,
+            disabledReason = "Windows batch files don't handle closed stdin 
gracefully in validation tests")
+    void testValidateConfigurationWithMockHelper() {
+        GitCredentialHelperMasterSource source = new 
GitCredentialHelperMasterSource();
+
+        String config = mockHelperPath.toString() + 
"?url=https://maven.apache.org/master";;
+        SecDispatcher.ValidationResponse response = 
source.validateConfiguration("git-credential:" + config);
+
+        assertNotNull(response);
+        assertTrue(response.isValid());
+        
assertTrue(response.getReport().containsKey(SecDispatcher.ValidationResponse.Level.INFO));
+    }
+
+    @Test
+    void testBuildHelperCommandWithShortName() {
+        String result = 
GitCredentialHelperMasterSource.buildHelperCommand("cache");
+        assertEquals("git-credential-cache", result);
+
+        result = GitCredentialHelperMasterSource.buildHelperCommand("store");
+        assertEquals("git-credential-store", result);
+    }
+
+    @Test
+    void testBuildHelperCommandWithPath() {
+        String result = 
GitCredentialHelperMasterSource.buildHelperCommand("/usr/local/bin/git-credential-osxkeychain");
+        assertEquals("/usr/local/bin/git-credential-osxkeychain", result);
+
+        result = 
GitCredentialHelperMasterSource.buildHelperCommand("./relative/path/helper");
+        assertEquals("./relative/path/helper", result);
+    }
+}

Reply via email to