[plasma/ksshaskpass] /: Split prompt into its own static library

Kai Uwe Broulik <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit d3c16e7589099097d5e59c452d5d2574a685d4cd by Kai Uwe Broulik.
Committed on 17/08/2026 at 14:09.
Pushed by broulik into branch 'master'.

Split prompt into its own static library

So we can reuse it for unit testing.

M  +2    -10   CMakeLists.txt
A  +23   -0    src/CMakeLists.txt
M  +2    -202  src/main.cpp
A  +194  -0    src/prompt.cpp     [License: GPL(v2.0+)]
A  +27   -0    src/prompt.h     [License: GPL(v2.0+)]

https://invent.kde.org/plasma/ksshaskpass/-/commit/d3c16e7589099097d5e59c452d5d2574a685d4cd

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6785ec7..c459a85 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -21,6 +21,7 @@ include(ECMQtDeclareLoggingCategory)
 include(KDEClangFormat)
 include(KDEGitCommitHooks)
 include(ECMDeprecationSettings)
+include(ECMQtDeclareLoggingCategory)
 
 find_package(Qt6  ${QT_MIN_VERSION} REQUIRED COMPONENTS Core)
 
@@ -42,16 +43,7 @@ ecm_set_disabled_deprecation_versions(QT 6.8.1
     KF 6.27.0
 )
 
-set(ksshaskpass_SRCS src/main.cpp)
- 
-add_executable(ksshaskpass ${ksshaskpass_SRCS})
-target_compile_definitions(ksshaskpass PRIVATE -DPROJECT_VERSION="${PROJECT_VERSION}")
-target_link_libraries(ksshaskpass
-    KF6::CoreAddons
-    KF6::I18n
-    KF6::WidgetsAddons
-    Qt6Keychain::Qt6Keychain
-)
+add_subdirectory(src)
 
 # add clang-format target for all our real source files
 file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES *.cpp *.h)
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000..2ec6914
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,23 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# SPDX-FileCopyrightText: 2026 Kai Uwe Broulik <[email protected]>
+
+add_library(ksshaskpass_prompt OBJECT prompt.cpp prompt.h)
+
+target_include_directories(ksshaskpass_prompt PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+
+ecm_qt_declare_logging_category(ksshaskpass_prompt HEADER debug.h
+                                               IDENTIFIER LOG_KSSHASKPASS
+                                               CATEGORY_NAME ksshaskpass
+                                               DESCRIPTION "ksshaskpass")
+
+target_link_libraries(ksshaskpass_prompt PUBLIC Qt6::Core)
+
+add_executable(ksshaskpass main.cpp)
+target_compile_definitions(ksshaskpass PRIVATE -DPROJECT_VERSION="${PROJECT_VERSION}")
+target_link_libraries(ksshaskpass
+    KF6::CoreAddons
+    KF6::I18n
+    KF6::WidgetsAddons
+    Qt6Keychain::Qt6Keychain
+    ksshaskpass_prompt
+)
diff --git a/src/main.cpp b/src/main.cpp
index a901d59..0f35f61 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -15,217 +15,17 @@
 #include <QApplication>
 #include <QCommandLineParser>
 #include <QInputDialog>
-#include <QLoggingCategory>
 #include <QPointer>
 #include <QRegularExpression>
 #include <QTextStream>
 
 #include <qt6keychain/keychain.h>
 
-Q_LOGGING_CATEGORY(LOG_KSSHASKPASS, "ksshaskpass")
+#include "debug.h"
+#include "prompt.h"
 
 constexpr const char *PROMPT_TYPE_ENV_VAR = "SSH_ASKPASS_PROMPT";
 
-// Standard prompt types defined by openssh.
-enum class PromptType {
-    Confirm,
-    Entry,
-    None,
-};
-
-// Implemented UI display types.
-enum class DisplayType {
-    Password,
-    ClearText,
-    Confirm,
-    ConfirmCancel,
-    UnknownSshHost
-};
-
-static void parsePrompt(PromptType promptType, const QString &prompt, QString &identifier, bool &ignoreKeychain, DisplayType &displayType)
-{
-    if (promptType == PromptType::Confirm) {
-        displayType = DisplayType::Confirm;
-        ignoreKeychain = true;
-        return;
-    }
-
-    if (promptType == PromptType::None) {
-        displayType = DisplayType::ConfirmCancel;
-        ignoreKeychain = true;
-        return;
-    }
-
-    // "Entry" prompt type: password or text input. We parse several known prompts from openssh and git (which have no i18n)
-    // to extract credential names and determine whether to use cleartext.
-    QRegularExpressionMatch match;
-
-    // openssh sshconnect2.c
-    // Case: password for authentication on remote ssh server
-    match = QRegularExpression(QStringLiteral("^(.*@.*)'s password: $")).match(prompt);
-    if (match.hasMatch()) {
-        identifier = match.captured(1);
-        displayType = DisplayType::Password;
-        ignoreKeychain = false;
-        return;
-    }
-
-    // Case: password for authentication on remote ssh server, also supports PAM format
-    match = QRegularExpression(QStringLiteral("^\\((.*@.*)\\) Password: $")).match(prompt);
-    if (match.hasMatch()) {
-        identifier = match.captured(1);
-        displayType = DisplayType::Password;
-        ignoreKeychain = false;
-        return;
-    }
-
-    // openssh sshconnect2.c
-    // Case: password change request
-    match = QRegularExpression(QStringLiteral("^(Enter|Retype) (.*@.*)'s (old|new) password: $")).match(prompt);
-    if (match.hasMatch()) {
-        identifier = match.captured(2);
-        displayType = DisplayType::Password;
-        ignoreKeychain = true;
-        return;
-    }
-
-    // openssh sshconnect2.c and sshconnect1.c
-    // Case: asking for passphrase for a certain keyfile
-    match = QRegularExpression(QStringLiteral("^Enter passphrase for( RSA)? key '(.*)': $")).match(prompt);
-    if (match.hasMatch()) {
-        identifier = match.captured(2);
-        displayType = DisplayType::Password;
-        ignoreKeychain = false;
-        return;
-    }
-
-    // openssh ssh-add.c
-    // Case: asking for passphrase for a certain keyfile for the first time => we should try a password from the keychain
-    match = QRegularExpression(QStringLiteral("^Enter passphrase for (.*?)( \\(will confirm each use\\))?: $")).match(prompt);
-    if (match.hasMatch()) {
-        identifier = match.captured(1);
-        displayType = DisplayType::Password;
-        ignoreKeychain = false;
-        return;
-    }
-
-    // openssh ssh-add.c
-    // Case: re-asking for passphrase for a certain keyfile => probably we’ve tried a password from the keychain, no point
-    // in trying it again
-    match = QRegularExpression(QStringLiteral("^Bad passphrase, try again for (.*?)( \\(will confirm each use\\))?: $")).match(prompt);
-    if (match.hasMatch()) {
-        identifier = match.captured(1);
-        displayType = DisplayType::Password;
-        ignoreKeychain = true;
-        return;
-    }
-
-    // openssh ssh-pkcs11.c
-    // Case: asking for PIN for some token label
-    match = QRegularExpression(QStringLiteral("Enter PIN for '(.*)': $")).match(prompt);
-    if (match.hasMatch()) {
-        identifier = match.captured(1);
-        displayType = DisplayType::Password;
-        ignoreKeychain = false;
-        return;
-    }
-
-    // openssh ssh-agent.c
-    // Case: asking to provide the PIN of the security key device
-    // match after "for" is key type, match after "key" is SHA digest of key
-    match = QRegularExpression(QStringLiteral("^Enter PIN( and confirm user presence)? for (.*?) key (.*?): $")).match(prompt);
-    if (match.hasMatch()) {
-        identifier = QStringLiteral("PIN:") + match.captured(3);
-        displayType = DisplayType::Password;
-        ignoreKeychain = true;
-        return;
-    }
-
-    // google-authenticator-libpam pam_google_authenticator.c
-    // Case: OTP verification code request from remote ssh server through PAM module
-    match = QRegularExpression(QStringLiteral("Verification code: $")).match(prompt);
-    if (match.hasMatch()) {
-        identifier = QString();
-        displayType = DisplayType::ClearText;
-        ignoreKeychain = true;
-        return;
-    }
-
-    // git credential.c
-    // Case: asking for username by git without specifying any other information
-    match = QRegularExpression(QStringLiteral("^Username: $")).match(prompt);
-    if (match.hasMatch()) {
-        identifier = QString();
-        displayType = DisplayType::ClearText;
-        ignoreKeychain = true;
-        return;
-    }
-
-    // git credential.c
-    // Case: asking for password by git without specifying any other information
-    match = QRegularExpression(QStringLiteral("^Password: $")).match(prompt);
-    if (match.hasMatch()) {
-        identifier = QString();
-        displayType = DisplayType::Password;
-        ignoreKeychain = true;
-        return;
-    }
-
-    // git credential.c
-    // Case: asking for username by git for some identifier
-    match = QRegularExpression(QStringLiteral("^Username for '(.*)': $")).match(prompt);
-    if (match.hasMatch()) {
-        identifier = match.captured(1);
-        displayType = DisplayType::ClearText;
-        ignoreKeychain = false;
-        return;
-    }
-
-    // git credential.c
-    // Case: asking for password by git for some identifier
-    match = QRegularExpression(QStringLiteral("^Password for '(.*)': $")).match(prompt);
-    if (match.hasMatch()) {
-        identifier = match.captured(1);
-        displayType = DisplayType::Password;
-        ignoreKeychain = false;
-        return;
-    }
-
-    // Case: username extraction from git-lfs
-    match = QRegularExpression(QStringLiteral("^Username for \"(.*?)\"$")).match(prompt);
-    if (match.hasMatch()) {
-        identifier = match.captured(1);
-        displayType = DisplayType::ClearText;
-        ignoreKeychain = false;
-        return;
-    }
-
-    // Case: password extraction from git-lfs
-    match = QRegularExpression(QStringLiteral("^Password for \"(.*?)\"$")).match(prompt);
-    if (match.hasMatch()) {
-        identifier = match.captured(1);
-        displayType = DisplayType::Password;
-        ignoreKeychain = false;
-        return;
-    }
-
-    // Case: unknown SSH host
-    match = QRegularExpression(QStringLiteral("^The authenticity of host '([^']+)(?: \\(([^)]+)\\))?' can't be established\\.\n"
-                                              "([A-Z0-9_-]+) key fingerprint is ([A-Za-z0-9:+/=]+)\\.\n"
-                                              "This key is not known by any other names\\.\n"
-                                              "Are you sure you want to continue connecting \\(yes/no/\\[fingerprint\\]\\)\\?\\s*$"))
-                .match(prompt);
-    if (match.hasMatch()) {
-        displayType = DisplayType::UnknownSshHost;
-        ignoreKeychain = true;
-        return;
-    }
-
-    // Nothing matched; either it was called by some sort of a script with a custom prompt (i.e. not ssh-add), or
-    // strings we're looking for were broken. Issue a warning and continue without identifier.
-    qCWarning(LOG_KSSHASKPASS) << "Unable to parse phrase" << prompt;
-}
-
 static void execQKeychainJobBlocking(QKeychain::Job &job)
 {
     QEventLoop loop;
diff --git a/src/prompt.cpp b/src/prompt.cpp
new file mode 100644
index 0000000..2573768
--- /dev/null
+++ b/src/prompt.cpp
@@ -0,0 +1,194 @@
+/*
+ *   SPDX-FileCopyrightText: 2006 Hans van Leeuwen <[email protected]>
+ *   SPDX-FileCopyrightText: 2008-2010 Armin Berres <[email protected]>
+ *   SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "prompt.h"
+#include "debug.h"
+
+#include <QRegularExpression>
+
+void parsePrompt(PromptType promptType, const QString &prompt, QString &identifier, bool &ignoreKeychain, DisplayType &displayType)
+{
+    if (promptType == PromptType::Confirm) {
+        displayType = DisplayType::Confirm;
+        ignoreKeychain = true;
+        return;
+    }
+
+    if (promptType == PromptType::None) {
+        displayType = DisplayType::ConfirmCancel;
+        ignoreKeychain = true;
+        return;
+    }
+
+    // "Entry" prompt type: password or text input. We parse several known prompts from openssh and git (which have no i18n)
+    // to extract credential names and determine whether to use cleartext.
+    QRegularExpressionMatch match;
+
+    // openssh sshconnect2.c
+    // Case: password for authentication on remote ssh server
+    match = QRegularExpression(QStringLiteral("^(.*@.*)'s password: $")).match(prompt);
+    if (match.hasMatch()) {
+        identifier = match.captured(1);
+        displayType = DisplayType::Password;
+        ignoreKeychain = false;
+        return;
+    }
+
+    // Case: password for authentication on remote ssh server, also supports PAM format
+    match = QRegularExpression(QStringLiteral("^\\((.*@.*)\\) Password: $")).match(prompt);
+    if (match.hasMatch()) {
+        identifier = match.captured(1);
+        displayType = DisplayType::Password;
+        ignoreKeychain = false;
+        return;
+    }
+
+    // openssh sshconnect2.c
+    // Case: password change request
+    match = QRegularExpression(QStringLiteral("^(Enter|Retype) (.*@.*)'s (old|new) password: $")).match(prompt);
+    if (match.hasMatch()) {
+        identifier = match.captured(2);
+        displayType = DisplayType::Password;
+        ignoreKeychain = true;
+        return;
+    }
+
+    // openssh sshconnect2.c and sshconnect1.c
+    // Case: asking for passphrase for a certain keyfile
+    match = QRegularExpression(QStringLiteral("^Enter passphrase for( RSA)? key '(.*)': $")).match(prompt);
+    if (match.hasMatch()) {
+        identifier = match.captured(2);
+        displayType = DisplayType::Password;
+        ignoreKeychain = false;
+        return;
+    }
+
+    // openssh ssh-add.c
+    // Case: asking for passphrase for a certain keyfile for the first time => we should try a password from the keychain
+    match = QRegularExpression(QStringLiteral("^Enter passphrase for (.*?)( \\(will confirm each use\\))?: $")).match(prompt);
+    if (match.hasMatch()) {
+        identifier = match.captured(1);
+        displayType = DisplayType::Password;
+        ignoreKeychain = false;
+        return;
+    }
+
+    // openssh ssh-add.c
+    // Case: re-asking for passphrase for a certain keyfile => probably we’ve tried a password from the keychain, no point
+    // in trying it again
+    match = QRegularExpression(QStringLiteral("^Bad passphrase, try again for (.*?)( \\(will confirm each use\\))?: $")).match(prompt);
+    if (match.hasMatch()) {
+        identifier = match.captured(1);
+        displayType = DisplayType::Password;
+        ignoreKeychain = true;
+        return;
+    }
+
+    // openssh ssh-pkcs11.c
+    // Case: asking for PIN for some token label
+    match = QRegularExpression(QStringLiteral("Enter PIN for '(.*)': $")).match(prompt);
+    if (match.hasMatch()) {
+        identifier = match.captured(1);
+        displayType = DisplayType::Password;
+        ignoreKeychain = false;
+        return;
+    }
+
+    // openssh ssh-agent.c
+    // Case: asking to provide the PIN of the security key device
+    // match after "for" is key type, match after "key" is SHA digest of key
+    match = QRegularExpression(QStringLiteral("^Enter PIN( and confirm user presence)? for (.*?) key (.*?): $")).match(prompt);
+    if (match.hasMatch()) {
+        identifier = QStringLiteral("PIN:") + match.captured(3);
+        displayType = DisplayType::Password;
+        ignoreKeychain = true;
+        return;
+    }
+
+    // google-authenticator-libpam pam_google_authenticator.c
+    // Case: OTP verification code request from remote ssh server through PAM module
+    match = QRegularExpression(QStringLiteral("Verification code: $")).match(prompt);
+    if (match.hasMatch()) {
+        identifier = QString();
+        displayType = DisplayType::ClearText;
+        ignoreKeychain = true;
+        return;
+    }
+
+    // git credential.c
+    // Case: asking for username by git without specifying any other information
+    match = QRegularExpression(QStringLiteral("^Username: $")).match(prompt);
+    if (match.hasMatch()) {
+        identifier = QString();
+        displayType = DisplayType::ClearText;
+        ignoreKeychain = true;
+        return;
+    }
+
+    // git credential.c
+    // Case: asking for password by git without specifying any other information
+    match = QRegularExpression(QStringLiteral("^Password: $")).match(prompt);
+    if (match.hasMatch()) {
+        identifier = QString();
+        displayType = DisplayType::Password;
+        ignoreKeychain = true;
+        return;
+    }
+
+    // git credential.c
+    // Case: asking for username by git for some identifier
+    match = QRegularExpression(QStringLiteral("^Username for '(.*)': $")).match(prompt);
+    if (match.hasMatch()) {
+        identifier = match.captured(1);
+        displayType = DisplayType::ClearText;
+        ignoreKeychain = false;
+        return;
+    }
+
+    // git credential.c
+    // Case: asking for password by git for some identifier
+    match = QRegularExpression(QStringLiteral("^Password for '(.*)': $")).match(prompt);
+    if (match.hasMatch()) {
+        identifier = match.captured(1);
+        displayType = DisplayType::Password;
+        ignoreKeychain = false;
+        return;
+    }
+
+    // Case: username extraction from git-lfs
+    match = QRegularExpression(QStringLiteral("^Username for \"(.*?)\"$")).match(prompt);
+    if (match.hasMatch()) {
+        identifier = match.captured(1);
+        displayType = DisplayType::ClearText;
+        ignoreKeychain = false;
+        return;
+    }
+
+    // Case: password extraction from git-lfs
+    match = QRegularExpression(QStringLiteral("^Password for \"(.*?)\"$")).match(prompt);
+    if (match.hasMatch()) {
+        identifier = match.captured(1);
+        displayType = DisplayType::Password;
+        ignoreKeychain = false;
+        return;
+    }
+
+    // Case: unknown SSH host
+    match = QRegularExpression(QStringLiteral("^The authenticity of host '([^']+)(?: \\(([^)]+)\\))?' can't be established\\.\n"
+                                              "([A-Z0-9_-]+) key fingerprint is ([A-Za-z0-9:+/=]+)\\.\n"
+                                              "This key is not known by any other names\\.\n"
+                                              "Are you sure you want to continue connecting \\(yes/no/\\[fingerprint\\]\\)\\?\\s*$"))
+                .match(prompt);
+    if (match.hasMatch()) {
+        displayType = DisplayType::UnknownSshHost;
+        ignoreKeychain = true;
+        return;
+    }
+
+    // Nothing matched; either it was called by some sort of a script with a custom prompt (i.e. not ssh-add), or
+    // strings we're looking for were broken. Issue a warning and continue without identifier.
+    qCWarning(LOG_KSSHASKPASS) << "Unable to parse phrase" << prompt;
+}
diff --git a/src/prompt.h b/src/prompt.h
new file mode 100644
index 0000000..682ffa6
--- /dev/null
+++ b/src/prompt.h
@@ -0,0 +1,27 @@
+/*
+ *   SPDX-FileCopyrightText: 2006 Hans van Leeuwen <[email protected]>
+ *   SPDX-FileCopyrightText: 2008-2010 Armin Berres <[email protected]>
+ *   SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#pragma once
+
+class QString;
+
+// Standard prompt types defined by openssh.
+enum class PromptType {
+    Confirm,
+    Entry,
+    None,
+};
+
+// Implemented UI display types.
+enum class DisplayType {
+    Password,
+    ClearText,
+    Confirm,
+    ConfirmCancel,
+    UnknownSshHost
+};
+
+void parsePrompt(PromptType promptType, const QString &prompt, QString &identifier, bool &ignoreKeychain, DisplayType &displayType);
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.