File 0001-Don-t-depend-on-apache-sshd-test-jars.patch of Package maven-scm

From d6ebbc2783508164f749ce4c47e74c7bb45f5b7e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fridrich=20=C5=A0trba?= <fridrich.strba@bluewin.ch>
Date: Mon, 22 Sep 2025 17:41:31 +0200
Subject: [PATCH] Don't depend on apache-sshd test-jars

---
 .../maven-scm-provider-gittest/pom.xml        |  12 -
 .../git/BogusPasswordAuthenticator.java       |  47 ++
 .../provider/git/CommandExecutionHelper.java  |  91 ++++
 .../provider/git/CommonTestSupportUtils.java  | 504 ++++++++++++++++++
 .../provider/git/CoreTestSupportUtils.java    |  91 ++++
 .../maven/scm/provider/git/EchoShell.java     |  40 ++
 .../scm/provider/git/EchoShellFactory.java    |  41 ++
 .../maven/scm/provider/git/GitSshServer.java  |   2 -
 8 files changed, 814 insertions(+), 14 deletions(-)
 create mode 100644 maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/BogusPasswordAuthenticator.java
 create mode 100644 maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CommandExecutionHelper.java
 create mode 100644 maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CommonTestSupportUtils.java
 create mode 100644 maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CoreTestSupportUtils.java
 create mode 100644 maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/EchoShell.java
 create mode 100644 maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/EchoShellFactory.java

diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/pom.xml b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/pom.xml
index 97a4a84bc..bd894bedb 100644
--- a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/pom.xml
+++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/pom.xml
@@ -50,18 +50,6 @@
       <artifactId>sshd-git</artifactId>
       <version>${minaSshdVersion}</version>
     </dependency>
-    <dependency>
-      <groupId>org.apache.sshd</groupId>
-      <artifactId>sshd-common</artifactId>
-      <version>${minaSshdVersion}</version>
-      <type>test-jar</type>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.sshd</groupId>
-      <artifactId>sshd-core</artifactId>
-      <version>${minaSshdVersion}</version>
-      <type>test-jar</type>
-    </dependency>
     <!-- for creating SSH keypairs dynamically -->
     <dependency>
       <groupId>org.bouncycastle</groupId>
diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/BogusPasswordAuthenticator.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/BogusPasswordAuthenticator.java
new file mode 100644
index 000000000..1bcfa2e1d
--- /dev/null
+++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/BogusPasswordAuthenticator.java
@@ -0,0 +1,47 @@
+/*
+ * 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.apache.maven.scm.provider.git;
+
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+import org.apache.sshd.server.auth.password.PasswordAuthenticator;
+import org.apache.sshd.server.session.ServerSession;
+
+/**
+ * A test {@link PasswordAuthenticator} that accepts an authentication attempt if the username is not {@code null} and
+ * same as password
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class BogusPasswordAuthenticator extends AbstractLoggingBean implements PasswordAuthenticator {
+    public static final BogusPasswordAuthenticator INSTANCE = new BogusPasswordAuthenticator();
+
+    public BogusPasswordAuthenticator() {
+        super();
+    }
+
+    @Override
+    public boolean authenticate(String username, String password, ServerSession session) {
+        boolean result = (username != null) && username.equals(password);
+        if (log.isDebugEnabled()) {
+            log.debug("authenticate({}) {} / {} - success={}", session, username, password, result);
+        }
+
+        return result;
+    }
+}
diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CommandExecutionHelper.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CommandExecutionHelper.java
new file mode 100644
index 000000000..75011879a
--- /dev/null
+++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CommandExecutionHelper.java
@@ -0,0 +1,91 @@
+/*
+ * 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.apache.maven.scm.provider.git;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.sshd.server.command.AbstractCommandSupport;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class CommandExecutionHelper extends AbstractCommandSupport {
+    protected CommandExecutionHelper() {
+        this(null);
+    }
+
+    protected CommandExecutionHelper(String command) {
+        super(command, null);
+    }
+
+    @Override
+    public void run() {
+        String command = getCommand();
+        try {
+            if (command == null) {
+                try (BufferedReader r =
+                        new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8))) {
+                    for (; ; ) {
+                        command = r.readLine();
+                        if (command == null) {
+                            return;
+                        }
+
+                        if (!handleCommandLine(command)) {
+                            return;
+                        }
+                    }
+                }
+            } else {
+                handleCommandLine(command);
+            }
+        } catch (InterruptedIOException e) {
+            // Ignore - signaled end
+        } catch (Exception e) {
+            String message =
+                    "Failed (" + e.getClass().getSimpleName() + ") to handle '" + command + "': " + e.getMessage();
+            try {
+                OutputStream stderr = getErrorStream();
+                stderr.write(message.getBytes(StandardCharsets.US_ASCII));
+            } catch (IOException ioe) {
+                log.warn(
+                        "Failed ({}) to write error message={}: {}",
+                        e.getClass().getSimpleName(),
+                        message,
+                        ioe.getMessage());
+            } finally {
+                onExit(-1, message);
+            }
+        } finally {
+            onExit(0);
+        }
+    }
+
+    /**
+     * @param  command   The command line
+     * @return           {@code true} if continue accepting command
+     * @throws Exception If failed to handle the command line
+     */
+    protected abstract boolean handleCommandLine(String command) throws Exception;
+}
diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CommonTestSupportUtils.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CommonTestSupportUtils.java
new file mode 100644
index 000000000..be4fdda4f
--- /dev/null
+++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CommonTestSupportUtils.java
@@ -0,0 +1,504 @@
+/*
+ * 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.apache.maven.scm.provider.git;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.CodeSource;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.ProtectionDomain;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.sshd.common.cipher.ECCurves;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
+import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.keyprovider.KeyPairProviderHolder;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.security.SecurityUtils;
+import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public final class CommonTestSupportUtils {
+    /**
+     * URL/URI scheme that refers to a file
+     */
+    public static final String FILE_URL_SCHEME = "file";
+    /**
+     * Prefix used in URL(s) that reference a file resource
+     */
+    public static final String FILE_URL_PREFIX = FILE_URL_SCHEME + ":";
+
+    /**
+     * Separator used in URL(s) that reference a resource inside a JAR to denote the sub-path inside the JAR
+     */
+    public static final char RESOURCE_SUBPATH_SEPARATOR = '!';
+
+    /**
+     * Suffix of JAR files
+     */
+    public static final String JAR_FILE_SUFFIX = ".jar";
+
+    /**
+     * URL/URI scheme that refers to a JAR
+     */
+    public static final String JAR_URL_SCHEME = "jar";
+
+    /**
+     * Prefix used in URL(s) that reference a resource inside a JAR
+     */
+    public static final String JAR_URL_PREFIX = JAR_URL_SCHEME + ":";
+
+    /**
+     * Suffix of compile Java class files
+     */
+    public static final String CLASS_FILE_SUFFIX = ".class";
+
+    public static final List<String> TARGET_FOLDER_NAMES = // NOTE: order is important
+            Collections.unmodifiableList(Arrays.asList("target" /* Maven */, "build" /* Gradle */));
+
+    public static final String DEFAULT_TEST_HOST_KEY_PROVIDER_ALGORITHM = KeyUtils.EC_ALGORITHM;
+    public static final int DEFAULT_TEST_HOST_KEY_SIZE = 256;
+    public static final String DEFAULT_TEST_HOST_KEY_TYPE =
+            ECCurves.fromCurveSize(DEFAULT_TEST_HOST_KEY_SIZE).getKeyType();
+
+    // uses a cached instance to avoid re-creating the keys as it is a time-consuming effort
+    private static final AtomicReference<KeyPairProvider> KEYPAIR_PROVIDER_HOLDER = new AtomicReference<>();
+    // uses a cached instance to avoid re-creating the keys as it is a time-consuming effort
+    private static final Map<String, FileKeyPairProvider> PROVIDERS_MAP = new ConcurrentHashMap<>();
+
+    private CommonTestSupportUtils() {
+        throw new UnsupportedOperationException("No instance allowed");
+    }
+
+    /**
+     * @param  clazz              A {@link Class} object
+     * @return                    A {@link URI} to the location of the class bytes container - e.g., the root folder,
+     *                            the containing JAR, etc.. Returns {@code null} if location could not be resolved
+     * @throws URISyntaxException if location is not a valid URI
+     * @see                       #getClassContainerLocationURL(Class)
+     */
+    public static URI getClassContainerLocationURI(Class<?> clazz) throws URISyntaxException {
+        URL url = getClassContainerLocationURL(clazz);
+        return (url == null) ? null : url.toURI();
+    }
+
+    /**
+     * @param  clazz A {@link Class} object
+     * @return       A {@link URL} to the location of the class bytes container - e.g., the root folder, the containing
+     *               JAR, etc.. Returns {@code null} if location could not be resolved
+     */
+    public static URL getClassContainerLocationURL(Class<?> clazz) {
+        ProtectionDomain pd = clazz.getProtectionDomain();
+        CodeSource cs = (pd == null) ? null : pd.getCodeSource();
+        URL url = (cs == null) ? null : cs.getLocation();
+        if (url == null) {
+            url = getClassBytesURL(clazz);
+            if (url == null) {
+                return null;
+            }
+
+            String srcForm = getURLSource(url);
+            if (GenericUtils.isEmpty(srcForm)) {
+                return null;
+            }
+
+            try {
+                url = new URL(srcForm);
+            } catch (MalformedURLException e) {
+                throw new IllegalArgumentException("getClassContainerLocationURL(" + clazz.getName() + ")"
+                        + " Failed to create URL=" + srcForm + " from " + url.toExternalForm()
+                        + ": " + e.getMessage());
+            }
+        }
+
+        return url;
+    }
+
+    /**
+     * @param  uri The {@link URI} value - ignored if {@code null}
+     * @return     The URI(s) source path where {@link #JAR_URL_PREFIX} and any sub-resource are stripped
+     * @see        #getURLSource(String)
+     */
+    public static String getURLSource(URI uri) {
+        return getURLSource((uri == null) ? null : uri.toString());
+    }
+
+    /**
+     * @param  url The {@link URL} value - ignored if {@code null}
+     * @return     The URL(s) source path where {@link #JAR_URL_PREFIX} and any sub-resource are stripped
+     * @see        #getURLSource(String)
+     */
+    public static String getURLSource(URL url) {
+        return getURLSource((url == null) ? null : url.toExternalForm());
+    }
+
+    /**
+     * @param  externalForm The {@link URL#toExternalForm()} string - ignored if {@code null}/empty
+     * @return              The URL(s) source path where {@link #JAR_URL_PREFIX} and any sub-resource are stripped
+     */
+    public static String getURLSource(String externalForm) {
+        String url = externalForm;
+        if (GenericUtils.isEmpty(url)) {
+            return url;
+        }
+
+        url = stripJarURLPrefix(externalForm);
+        if (GenericUtils.isEmpty(url)) {
+            return url;
+        }
+
+        int sepPos = url.indexOf(RESOURCE_SUBPATH_SEPARATOR);
+        if (sepPos < 0) {
+            return adjustURLPathValue(url);
+        } else {
+            return adjustURLPathValue(url.substring(0, sepPos));
+        }
+    }
+
+    /**
+     * @param  url A {@link URL} - ignored if {@code null}
+     * @return     The path after stripping any trailing '/' provided the path is not '/' itself
+     * @see        #adjustURLPathValue(String)
+     */
+    public static String adjustURLPathValue(URL url) {
+        return adjustURLPathValue((url == null) ? null : url.getPath());
+    }
+
+    /**
+     * @param  path A URL path value - ignored if {@code null}/empty
+     * @return      The path after stripping any trailing '/' provided the path is not '/' itself
+     */
+    public static String adjustURLPathValue(final String path) {
+        final int pathLen = (path == null) ? 0 : path.length();
+        if ((pathLen <= 1) || (path.charAt(pathLen - 1) != '/')) {
+            return path;
+        }
+
+        return path.substring(0, pathLen - 1);
+    }
+
+    public static String stripJarURLPrefix(String externalForm) {
+        String url = externalForm;
+        if (GenericUtils.isEmpty(url)) {
+            return url;
+        }
+
+        if (url.startsWith(JAR_URL_PREFIX)) {
+            return url.substring(JAR_URL_PREFIX.length());
+        }
+
+        return url;
+    }
+
+    /**
+     * @param  clazz The request {@link Class}
+     * @return       A {@link URL} to the location of the <code>.class</code> file - {@code null} if location could not
+     *               be resolved
+     */
+    public static URL getClassBytesURL(Class<?> clazz) {
+        String className = clazz.getName();
+        int sepPos = className.indexOf('$');
+        // if this is an internal class, then need to use its parent as well
+        if (sepPos > 0) {
+            sepPos = className.lastIndexOf('.');
+            if (sepPos > 0) {
+                className = className.substring(sepPos + 1);
+            }
+        } else {
+            className = clazz.getSimpleName();
+        }
+
+        return clazz.getResource(className + CLASS_FILE_SUFFIX);
+    }
+
+    public static String getClassBytesResourceName(Class<?> clazz) {
+        return getClassBytesResourceName((clazz == null) ? null : clazz.getName());
+    }
+
+    /**
+     * @param  name The fully qualified class name - ignored if {@code null}/empty
+     * @return      The relative path of the class file byte-code resource
+     */
+    public static String getClassBytesResourceName(String name) {
+        if (GenericUtils.isEmpty(name)) {
+            return name;
+        } else {
+            return name.replace('.', '/') + CLASS_FILE_SUFFIX;
+        }
+    }
+
+    public static Path resolve(Path root, String... children) {
+        if (GenericUtils.isEmpty(children)) {
+            return root;
+        } else {
+            return resolve(root, Arrays.asList(children));
+        }
+    }
+
+    public static Path resolve(Path root, Collection<String> children) {
+        Path path = root;
+        if (!GenericUtils.isEmpty(children)) {
+            for (String child : children) {
+                path = path.resolve(child);
+            }
+        }
+
+        return path;
+    }
+
+    /**
+     * @param  anchor An anchor {@link Class} whose container we want to use as the starting point for the
+     *                &quot;target&quot; folder lookup up the hierarchy
+     * @return        The &quot;target&quot; <U>folder</U> - {@code null} if not found
+     * @see           #detectTargetFolder(Path)
+     */
+    public static Path detectTargetFolder(Class<?> anchor) {
+        Path path = detectTargetFolder(getClassContainerLocationPath(anchor));
+        if (path == null) {
+            String basedir = System.getProperty("basedir");
+            path = detectTargetFolder(Paths.get(basedir, "target"));
+        }
+        return path;
+    }
+
+    /**
+     * @param  clazz                    A {@link Class} object
+     * @return                          A {@link Path} of the location of the class bytes container - e.g., the root
+     *                                  folder, the containing JAR, etc.. Returns {@code null} if location could not be
+     *                                  resolved
+     * @throws IllegalArgumentException If location is not a valid {@link Path} location
+     * @see                             #getClassContainerLocationURI(Class)
+     * @see                             #toPathSource(URI)
+     */
+    public static Path getClassContainerLocationPath(Class<?> clazz) throws IllegalArgumentException {
+        try {
+            URI uri = getClassContainerLocationURI(clazz);
+            return toPathSource(uri);
+        } catch (URISyntaxException | MalformedURLException e) {
+            throw new IllegalArgumentException(e.getClass().getSimpleName() + ": " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Converts a {@link URL} that may refer to an internal resource to a {@link Path} representing is
+     * &quot;source&quot; container (e.g., if it is a resource in a JAR, then the result is the JAR's path)
+     *
+     * @param  url                   The {@link URL} - ignored if {@code null}
+     * @return                       The matching {@link Path}
+     * @throws MalformedURLException If source URL does not refer to a file location
+     * @see                          #toPathSource(URI)
+     */
+    public static Path toPathSource(URL url) throws MalformedURLException {
+        if (url == null) {
+            return null;
+        }
+
+        try {
+            return toPathSource(url.toURI());
+        } catch (URISyntaxException e) {
+            throw new MalformedURLException("toFileSource(" + url.toExternalForm() + ")"
+                    + " cannot (" + e.getClass().getSimpleName() + ")"
+                    + " convert to URI: " + e.getMessage());
+        }
+    }
+
+    /**
+     * Converts a {@link URI} that may refer to an internal resource to a {@link Path} representing is
+     * &quot;source&quot; container (e.g., if it is a resource in a JAR, then the result is the JAR's path)
+     *
+     * @param  uri                   The {@link URI} - ignored if {@code null}
+     * @return                       The matching {@link Path}
+     * @throws MalformedURLException If source URI does not refer to a file location
+     * @see                          #getURLSource(URI)
+     */
+    public static Path toPathSource(URI uri) throws MalformedURLException {
+        String src = getURLSource(uri);
+        if (GenericUtils.isEmpty(src)) {
+            return null;
+        }
+
+        if (!src.startsWith(FILE_URL_PREFIX)) {
+            throw new MalformedURLException("toFileSource(" + src + ") not a '" + FILE_URL_SCHEME + "' scheme");
+        }
+
+        try {
+            return Paths.get(new URI(src));
+        } catch (URISyntaxException e) {
+            throw new MalformedURLException("toFileSource(" + src + ")"
+                    + " cannot (" + e.getClass().getSimpleName() + ")"
+                    + " convert to URI: " + e.getMessage());
+        }
+    }
+
+    /**
+     * @param  anchorFile An anchor {@link Path} we want to use as the starting point for the &quot;target&quot; or
+     *                    &quot;build&quot; folder lookup up the hierarchy
+     * @return            The &quot;target&quot; <U>folder</U> - {@code null} if not found
+     */
+    public static Path detectTargetFolder(Path anchorFile) {
+        for (Path file = anchorFile; file != null; file = file.getParent()) {
+            if (!Files.isDirectory(file)) {
+                continue;
+            }
+
+            String name = Objects.toString(file.getFileName(), "");
+            if (TARGET_FOLDER_NAMES.contains(name)) {
+                return file;
+            }
+        }
+
+        return null;
+    }
+
+    public static KeyPair generateKeyPair(String algorithm, int keySize) throws GeneralSecurityException {
+        KeyPairGenerator gen = SecurityUtils.getKeyPairGenerator(algorithm);
+        if (KeyUtils.EC_ALGORITHM.equalsIgnoreCase(algorithm)) {
+            ECCurves curve = ECCurves.fromCurveSize(keySize);
+            if (curve == null) {
+                throw new InvalidKeySpecException("Unknown curve for key size=" + keySize);
+            }
+            gen.initialize(curve.getParameters());
+        } else {
+            gen.initialize(keySize);
+        }
+
+        return gen.generateKeyPair();
+    }
+
+    public static KeyPairProvider createTestHostKeyProvider(Class<?> anchor) {
+        KeyPairProvider provider = KEYPAIR_PROVIDER_HOLDER.get();
+        if (provider != null) {
+            return provider;
+        }
+
+        Path targetFolder = Objects.requireNonNull(
+                CommonTestSupportUtils.detectTargetFolder(anchor), "Failed to detect target folder");
+        Path file = targetFolder.resolve("hostkey." + DEFAULT_TEST_HOST_KEY_PROVIDER_ALGORITHM.toLowerCase());
+        provider = createTestHostKeyProvider(file);
+
+        KeyPairProvider prev = KEYPAIR_PROVIDER_HOLDER.getAndSet(provider);
+        if (prev != null) { // check if somebody else beat us to it
+            return prev;
+        } else {
+            return provider;
+        }
+    }
+
+    public static KeyPairProvider createTestHostKeyProvider(Path path) {
+        SimpleGeneratorHostKeyProvider keyProvider = new SimpleGeneratorHostKeyProvider();
+        keyProvider.setPath(Objects.requireNonNull(path, "No path"));
+        keyProvider.setAlgorithm(DEFAULT_TEST_HOST_KEY_PROVIDER_ALGORITHM);
+        keyProvider.setKeySize(DEFAULT_TEST_HOST_KEY_SIZE);
+        return validateKeyPairProvider(keyProvider);
+    }
+
+    public static KeyPair getFirstKeyPair(KeyPairProviderHolder holder) {
+        return getFirstKeyPair(Objects.requireNonNull(holder, "No holder").getKeyPairProvider());
+    }
+
+    public static KeyPair getFirstKeyPair(KeyIdentityProvider provider) {
+        Objects.requireNonNull(provider, "No key pair provider");
+        Iterable<KeyPair> pairs;
+        try {
+            pairs = Objects.requireNonNull(provider.loadKeys(null), "No loaded keys");
+        } catch (IOException | GeneralSecurityException e) {
+            throw new RuntimeException(
+                    "Unexpected " + e.getClass().getSimpleName() + ")" + " keys loading exception: " + e.getMessage(),
+                    e);
+        }
+
+        Iterator<KeyPair> iter = Objects.requireNonNull(pairs.iterator(), "No keys iterator");
+        ValidateUtils.checkTrue(iter.hasNext(), "Empty loaded kyes iterator");
+        return Objects.requireNonNull(iter.next(), "No key pair in iterator");
+    }
+
+    private static Path getFile(String resource) {
+        URL url = CommonTestSupportUtils.class.getClassLoader().getResource(resource);
+        try {
+            return Paths.get(url.toURI());
+        } catch (URISyntaxException e) {
+            return Paths.get(url.getPath());
+        }
+    }
+
+    public static FileKeyPairProvider createTestKeyPairProvider(String resource) {
+        Path file = getFile(resource);
+        file = file.toAbsolutePath();
+        String filePath = Objects.toString(file, "");
+        FileKeyPairProvider provider = PROVIDERS_MAP.get(filePath);
+        if (provider != null) {
+            return provider;
+        }
+
+        provider = new FileKeyPairProvider();
+        provider.setPaths(Collections.singletonList(file));
+        provider = validateKeyPairProvider(provider);
+
+        FileKeyPairProvider prev = PROVIDERS_MAP.put(filePath, provider);
+        if (prev != null) { // check if somebody else beat us to it
+            return prev;
+        } else {
+            return provider;
+        }
+    }
+
+    public static <P extends KeyIdentityProvider> P validateKeyPairProvider(P provider) {
+        Objects.requireNonNull(provider, "No provider");
+
+        // get the I/O out of the way
+        Iterable<KeyPair> keys;
+        try {
+            keys = Objects.requireNonNull(provider.loadKeys(null), "No keys loaded");
+        } catch (IOException | GeneralSecurityException e) {
+            throw new RuntimeException(
+                    "Unexpected " + e.getClass().getSimpleName() + ")" + " keys loading exception: " + e.getMessage(),
+                    e);
+        }
+
+        if (keys instanceof Collection<?>) {
+            ValidateUtils.checkNotNullAndNotEmpty((Collection<?>) keys, "Empty keys loaded");
+        }
+
+        return provider;
+    }
+}
diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CoreTestSupportUtils.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CoreTestSupportUtils.java
new file mode 100644
index 000000000..9ed5963a8
--- /dev/null
+++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/CoreTestSupportUtils.java
@@ -0,0 +1,91 @@
+/*
+ * 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.apache.maven.scm.provider.git;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.cipher.BuiltinCiphers;
+import org.apache.sshd.common.cipher.Cipher;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.core.CoreModuleProperties;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator;
+import org.apache.sshd.server.shell.UnknownCommandFactory;
+
+public final class CoreTestSupportUtils {
+    public static final Duration READ_TIMEOUT = getTimeout("read.nio2", Duration.ofSeconds(60));
+
+    private CoreTestSupportUtils() {
+        throw new UnsupportedOperationException("No instance");
+    }
+
+    public static int getFreePort() throws Exception {
+        try (ServerSocket s = new ServerSocket()) {
+            s.setReuseAddress(true);
+            s.bind(new InetSocketAddress((InetAddress) null, 0));
+            return s.getLocalPort();
+        }
+    }
+
+    public static SshServer setupTestServer(Class<?> anchor) {
+        return setupTestServer(SshServer.setUpDefaultServer(), anchor);
+    }
+
+    public static <S extends SshServer> S setupTestServer(S sshd, Class<?> anchor) {
+        List<NamedFactory<Cipher>> cipherFactories = new ArrayList<>(sshd.getCipherFactories());
+        cipherFactories.add(BuiltinCiphers.aes128cbc);
+        sshd.setCipherFactories(cipherFactories);
+        sshd.setKeyPairProvider(CommonTestSupportUtils.createTestHostKeyProvider(anchor));
+        sshd.setPasswordAuthenticator(BogusPasswordAuthenticator.INSTANCE);
+        sshd.setPublickeyAuthenticator(AcceptAllPublickeyAuthenticator.INSTANCE);
+        sshd.setShellFactory(EchoShellFactory.INSTANCE);
+        sshd.setCommandFactory(UnknownCommandFactory.INSTANCE);
+        CoreModuleProperties.NIO2_READ_TIMEOUT.set(sshd, READ_TIMEOUT);
+        return sshd;
+    }
+
+    public static Duration getTimeout(String property, Duration defaultValue) {
+        // Do we have a specific timeout value ?
+        String str = System.getProperty("org.apache.sshd.test.timeout." + property);
+        if (GenericUtils.isNotEmpty(str)) {
+            return Duration.ofMillis(Long.parseLong(str));
+        }
+
+        // Do we have a specific factor ?
+        str = System.getProperty("org.apache.sshd.test.timeout.factor." + property);
+        if (GenericUtils.isEmpty(str)) {
+            // Do we have a global factor ?
+            str = System.getProperty("org.apache.sshd.test.timeout.factor");
+        }
+
+        if (GenericUtils.isNotEmpty(str)) {
+            double factor = Double.parseDouble(str);
+            long dur = Math.round(defaultValue.toMillis() * factor);
+            return Duration.ofMillis(dur);
+        }
+
+        return defaultValue;
+    }
+}
diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/EchoShell.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/EchoShell.java
new file mode 100644
index 000000000..65771a451
--- /dev/null
+++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/EchoShell.java
@@ -0,0 +1,40 @@
+/*
+ * 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.apache.maven.scm.provider.git;
+
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class EchoShell extends CommandExecutionHelper {
+    public EchoShell() {
+        super();
+    }
+
+    @Override
+    protected boolean handleCommandLine(String command) throws Exception {
+        OutputStream out = getOutputStream();
+        out.write((command + "\n").getBytes(StandardCharsets.UTF_8));
+        out.flush();
+
+        return !"exit".equals(command);
+    }
+}
diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/EchoShellFactory.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/EchoShellFactory.java
new file mode 100644
index 000000000..2a8b162ee
--- /dev/null
+++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/EchoShellFactory.java
@@ -0,0 +1,41 @@
+/*
+ * 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.apache.maven.scm.provider.git;
+
+import org.apache.sshd.server.channel.ChannelSession;
+import org.apache.sshd.server.command.Command;
+import org.apache.sshd.server.shell.ShellFactory;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class EchoShellFactory implements ShellFactory {
+    public static final EchoShellFactory INSTANCE = new EchoShellFactory();
+
+    public EchoShellFactory() {
+        super();
+    }
+
+    @Override
+    public Command createShell(ChannelSession channel) {
+        return new EchoShell();
+    }
+}
diff --git a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/GitSshServer.java b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/GitSshServer.java
index a58525907..054990fd3 100644
--- a/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/GitSshServer.java
+++ b/maven-scm-providers/maven-scm-providers-git/maven-scm-provider-gittest/src/main/java/org/apache/maven/scm/provider/git/GitSshServer.java
@@ -40,8 +40,6 @@
 import org.apache.sshd.server.auth.pubkey.KeySetPublickeyAuthenticator;
 import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;
 import org.apache.sshd.server.session.ServerSession;
-import org.apache.sshd.util.test.CommonTestSupportUtils;
-import org.apache.sshd.util.test.CoreTestSupportUtils;
 import org.bouncycastle.openssl.PKCS8Generator;
 import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
 import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
-- 
2.51.0

openSUSE Build Service is sponsored by