File indexing completed on 2024-04-21 03:54:45

0001 /*
0002     SPDX-FileCopyrightText: 2003 Waldo Bastian <bastian@kde.org>
0003     SPDX-FileCopyrightText: 2007, 2009 David Faure <faure@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "desktopexecparsertest.h"
0009 
0010 #include <QTest>
0011 
0012 QTEST_GUILESS_MAIN(DesktopExecParserTest)
0013 
0014 #include <QStandardPaths>
0015 
0016 #include <desktopexecparser.h>
0017 
0018 #include <KApplicationTrader>
0019 #include <KConfigGroup>
0020 #include <KProcess>
0021 #include <KService>
0022 #include <KSharedConfig>
0023 #include <KShell>
0024 #include <KSycoca>
0025 
0026 void DesktopExecParserTest::initTestCase()
0027 {
0028     QStandardPaths::setTestModeEnabled(true);
0029 
0030     KSycoca::setupTestMenu();
0031 
0032     qputenv("PATH", QByteArray(qgetenv("PATH") + QFile::encodeName(QDir::listSeparator() + QCoreApplication::applicationDirPath())));
0033 
0034     // testProcessDesktopExec works only if your terminal application is set to "xterm"
0035     KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("General"));
0036     cg.writeEntry("TerminalApplication", "true --test");
0037 
0038     // We just want to test if the command is properly constructed
0039     m_pseudoTerminalProgram = QStandardPaths::findExecutable(QStringLiteral("true"));
0040     QVERIFY(!m_pseudoTerminalProgram.isEmpty());
0041 
0042     // Determine the full path of sh - this is needed to make testProcessDesktopExecNoFile()
0043     // pass on systems where QStandardPaths::findExecutable("sh") is not "/bin/sh".
0044     m_sh = QStandardPaths::findExecutable(QStringLiteral("sh"));
0045     if (m_sh.isEmpty()) {
0046         m_sh = QStringLiteral("/bin/sh");
0047     }
0048 }
0049 
0050 void DesktopExecParserTest::testExecutableName_data()
0051 {
0052     QTest::addColumn<QString>("execLine");
0053     QTest::addColumn<QString>("expectedPath");
0054     QTest::addColumn<QString>("expectedName");
0055 
0056     QTest::newRow("/usr/bin/ls") << "/usr/bin/ls"
0057                                  << "/usr/bin/ls"
0058                                  << "ls";
0059     QTest::newRow("/path/to/wine \"long argument with path\"") << "/path/to/wine \"long argument with path\""
0060                                                                << "/path/to/wine"
0061                                                                << "wine";
0062     QTest::newRow("/path/with/a/sp\\ ace/exe arg1 arg2") << "/path/with/a/sp\\ ace/exe arg1 arg2"
0063                                                          << "/path/with/a/sp ace/exe"
0064                                                          << "exe";
0065     QTest::newRow("\"progname\" \"arg1\"") << "\"progname\" \"arg1\""
0066                                            << "progname"
0067                                            << "progname";
0068     QTest::newRow("'quoted' \"arg1\"") << "'quoted' \"arg1\""
0069                                        << "quoted"
0070                                        << "quoted";
0071     QTest::newRow(" 'leading space'   arg1") << " 'leading space'   arg1"
0072                                              << "leading space"
0073                                              << "leading space";
0074     QTest::newRow("if_command") << "if test -e /tmp/foo; then kwrite ; else konsole ; fi"
0075                                 << ""
0076                                 << ""; // "if" isn't a known executable, so this is good...
0077 }
0078 
0079 void DesktopExecParserTest::testExecutableName()
0080 {
0081     QFETCH(QString, execLine);
0082     QFETCH(QString, expectedPath);
0083     QFETCH(QString, expectedName);
0084     QCOMPARE(KIO::DesktopExecParser::executableName(execLine), expectedName);
0085     QCOMPARE(KIO::DesktopExecParser::executablePath(execLine), expectedPath);
0086 }
0087 
0088 // static const char *bt(bool tr) { return tr?"true":"false"; }
0089 static void checkDesktopExecParser(const char *exec, const char *term, const char *sus, const QList<QUrl> &urls, bool tf, const QString &b)
0090 {
0091     QFile out(QStringLiteral("kruntest.desktop"));
0092     if (!out.open(QIODevice::WriteOnly)) {
0093         abort();
0094     }
0095     QByteArray str(
0096         "[Desktop Entry]\n"
0097         "Type=Application\n"
0098         "Name=just_a_test\n"
0099         "Icon=~/icon.png\n");
0100     str += QByteArray(exec) + '\n';
0101     str += QByteArray(term) + '\n';
0102     str += QByteArray(sus) + '\n';
0103     out.write(str);
0104     out.close();
0105 
0106     KService service(QDir::currentPath() + "/kruntest.desktop");
0107     /*qDebug() << QString().sprintf(
0108         "processDesktopExec( "
0109         "service = {\nexec = %s\nterminal = %s, terminalOptions = %s\nsubstituteUid = %s, user = %s },"
0110         "\nURLs = { %s },\ntemp_files = %s )",
0111         service.exec().toLatin1().constData(), bt(service.terminal()), service.terminalOptions().toLatin1().constData(), bt(service.substituteUid()),
0112        service.username().toLatin1().constData(), KShell::joinArgs(urls.toStringList()).toLatin1().constData(), bt(tf));
0113     */
0114     KIO::DesktopExecParser parser(service, urls);
0115     parser.setUrlsAreTempFiles(tf);
0116     QCOMPARE(KShell::joinArgs(parser.resultingArguments()), b);
0117 
0118     QFile::remove(QStringLiteral("kruntest.desktop"));
0119 }
0120 
0121 void DesktopExecParserTest::testProcessDesktopExec()
0122 {
0123     QList<QUrl> l0;
0124     static const char *const execs[] = {"Exec=date -u", "Exec=echo $PWD"};
0125     static const char *const terms[] = {"Terminal=false", "Terminal=true\nTerminalOptions=-T \"%f - %c\""};
0126     static const char *const sus[] = {"X-KDE-SubstituteUID=false", "X-KDE-SubstituteUID=true\nX-KDE-Username=sprallo"};
0127     static const char *const results[] = {
0128         "/bin/date -u", // 0
0129         "/bin/sh -c 'echo $PWD '", // 1
0130         "/bin/true --test -T ' - just_a_test' -e /bin/date -u", // 2
0131         "/bin/true --test -T ' - just_a_test' -e /bin/sh -c 'echo $PWD '", // 3
0132         /* kdesu */ " -u sprallo -c '/bin/date -u'", // 4
0133         /* kdesu */ " -u sprallo -c '/bin/sh -c '\\''echo $PWD '\\'''", // 5
0134         "/bin/true --test -T ' - just_a_test' -e su sprallo -c '/bin/date -u'", // 6
0135         "/bin/true --test -T ' - just_a_test' -e su sprallo -c '/bin/sh -c '\\''echo $PWD '\\'''", // 7
0136     };
0137 
0138     // Find out the full path of the shell which will be used to execute shell commands
0139     KProcess process;
0140     process.setShellCommand(QLatin1String(""));
0141     const QString shellPath = process.program().at(0);
0142 
0143     // Arch moved /bin/date to /usr/bin/date...
0144     const QString datePath = QStandardPaths::findExecutable(QStringLiteral("date"));
0145 
0146     for (int su = 0; su < 2; su++) {
0147         for (int te = 0; te < 2; te++) {
0148             for (int ex = 0; ex < 2; ex++) {
0149                 int pt = ex + te * 2 + su * 4;
0150                 QString exe;
0151                 if (pt == 4 || pt == 5) {
0152                     exe = QFile::decodeName(KDE_INSTALL_FULL_LIBEXECDIR_KF "/kdesu");
0153                     if (!QFile::exists(exe)) {
0154                         qWarning() << "kdesu not found, skipping test";
0155                         continue;
0156                     }
0157                 }
0158                 const QString result = QString::fromLatin1(results[pt])
0159                                            .replace(QLatin1String("/bin/true"), m_pseudoTerminalProgram)
0160                                            .replace(QLatin1String("/bin/sh"), shellPath)
0161                                            .replace(QLatin1String("/bin/date"), datePath);
0162                 checkDesktopExecParser(execs[ex], terms[te], sus[su], l0, false, exe + result);
0163             }
0164         }
0165     }
0166 }
0167 
0168 void DesktopExecParserTest::testProcessDesktopExecNoFile_data()
0169 {
0170     /* clang-format off */
0171     QTest::addColumn<QString>("execLine");
0172     QTest::addColumn<QList<QUrl>>("urls");
0173     QTest::addColumn<bool>("tempfiles");
0174     QTest::addColumn<QString>("expected");
0175 
0176     QList<QUrl> l0;
0177     QList<QUrl> l1;
0178     l1 << QUrl(QStringLiteral("file:/tmp"));
0179     QList<QUrl> l2;
0180     l2 << QUrl(QStringLiteral("http://localhost/foo"));
0181     QList<QUrl> l3;
0182     l3 << QUrl(QStringLiteral("file:/local/some file")) << QUrl(QStringLiteral("http://remotehost.org/bar"));
0183     QList<QUrl> l4;
0184     l4 << QUrl(QStringLiteral("http://login:password@www.kde.org"));
0185 
0186     // A real-world use case would be kate.
0187     // But I picked ktrash6 since it's installed by kio
0188     QString ktrash = QStandardPaths::findExecutable(QStringLiteral("ktrash6"));
0189     QVERIFY(!ktrash.isEmpty());
0190     QString ktrashQuoted = KShell::quoteArg(ktrash);
0191 
0192     QString kioexec = QCoreApplication::applicationDirPath() + "/kioexec";
0193     if (!QFileInfo::exists(kioexec)) {
0194         kioexec = KDE_INSTALL_FULL_LIBEXECDIR_KF "/kioexec";
0195     }
0196     QVERIFY(QFileInfo::exists(kioexec));
0197     QString kioexecQuoted = KShell::quoteArg(kioexec);
0198 
0199     QTest::newRow("%U l0") << "ktrash6 %U" << l0 << false << ktrashQuoted;
0200     QTest::newRow("%U l1") << "ktrash6 %U" << l1 << false << ktrashQuoted + " /tmp";
0201     QTest::newRow("%U l2") << "ktrash6 %U" << l2 << false << ktrashQuoted + " http://localhost/foo";
0202     QTest::newRow("%U l3") << "ktrash6 %U" << l3 << false << ktrashQuoted + " '/local/some file' http://remotehost.org/bar";
0203 
0204     // QTest::newRow("%u l0") << "ktrash6 %u" << l0 << false << ktrashQuoted; // gives runtime warning
0205     QTest::newRow("%u l1") << "ktrash6 %u" << l1 << false << ktrashQuoted + " /tmp";
0206     QTest::newRow("%u l2") << "ktrash6 %u" << l2 << false << ktrashQuoted + " http://localhost/foo";
0207     // QTest::newRow("%u l3") << "ktrash6 %u" << l3 << false << ktrashQuoted; // gives runtime warning
0208 
0209     QTest::newRow("%F l0") << "ktrash6 %F" << l0 << false << ktrashQuoted;
0210     QTest::newRow("%F l1") << "ktrash6 %F" << l1 << false << ktrashQuoted + " /tmp";
0211     QTest::newRow("%F l2") << "ktrash6 %F" << l2 << false << kioexecQuoted + " 'ktrash6 %F' http://localhost/foo";
0212     QTest::newRow("%F l3") << "ktrash6 %F" << l3 << false << kioexecQuoted + " 'ktrash6 %F' 'file:///local/some file' http://remotehost.org/bar";
0213 
0214     QTest::newRow("%F l1 tempfile") << "ktrash6 %F" << l1 << true << kioexecQuoted + " --tempfiles 'ktrash6 %F' file:///tmp";
0215     QTest::newRow("%f l1 tempfile") << "ktrash6 %f" << l1 << true << kioexecQuoted + " --tempfiles 'ktrash6 %f' file:///tmp";
0216 
0217     QTest::newRow("sh -c ktrash6 %F")
0218         << R"(sh -c "ktrash6 "'\"'"%F"'\"')"
0219         << l1
0220         << false
0221         << m_sh + R"( -c 'ktrash6 \"/tmp\"')";
0222 
0223     // This was originally with kmailservice5, but that relies on it being installed
0224     QTest::newRow("ktrash6 %u l1") << "ktrash6 %u" << l1 << false << ktrashQuoted + " /tmp";
0225     QTest::newRow("ktrash6 %u l4") << "ktrash6 %u" << l4 << false << ktrashQuoted + " http://login:password@www.kde.org";
0226 
0227     /* clang-format on */
0228 }
0229 
0230 void DesktopExecParserTest::testProcessDesktopExecNoFile()
0231 {
0232     QFETCH(QString, execLine);
0233     KService service(QStringLiteral("dummy"), execLine, QStringLiteral("app"));
0234     QFETCH(QList<QUrl>, urls);
0235     QFETCH(bool, tempfiles);
0236     QFETCH(QString, expected);
0237     KIO::DesktopExecParser parser(service, urls);
0238     parser.setUrlsAreTempFiles(tempfiles);
0239     const QStringList args = parser.resultingArguments();
0240     QVERIFY2(!args.isEmpty(), qPrintable(parser.errorMessage()));
0241     QCOMPARE(KShell::joinArgs(args), expected);
0242 }
0243 
0244 extern KSERVICE_EXPORT int ksycoca_ms_between_checks;
0245 
0246 void DesktopExecParserTest::testKtelnetservice()
0247 {
0248     const QString ktelnetDesk = QFINDTESTDATA(QStringLiteral("../src/schemehandlers/telnet/ktelnetservice6.desktop"));
0249     QVERIFY(!ktelnetDesk.isEmpty());
0250 
0251     // KApplicationTrader in KIO::DesktopExecParser::hasSchemeHandler() needs the .desktop file to be installed
0252     const QString destDir = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation);
0253     QVERIFY(QDir().mkpath(destDir));
0254     QFile::remove(destDir + QLatin1String("/ktelnetservice6.desktop"));
0255     QVERIFY(QFile::copy(ktelnetDesk, destDir + QLatin1String("/ktelnetservice6.desktop")));
0256 
0257     ksycoca_ms_between_checks = 0; // need it to check the ksycoca mtime
0258 
0259     KService::Ptr service = KService::serviceByStorageId(QStringLiteral("ktelnetservice6.desktop"));
0260     QVERIFY(service);
0261 
0262     QString ktelnetExec = QStandardPaths::findExecutable(QStringLiteral("ktelnetservice6"));
0263     // if KIO is installed we'll find <bindir>/ktelnetservice6, otherwise KIO::DesktopExecParser will
0264     // use the executable from Exec= line
0265     if (ktelnetExec.isEmpty()) {
0266         ktelnetExec = service->exec().remove(QLatin1String(" %u"));
0267     }
0268     QVERIFY(!ktelnetExec.isEmpty());
0269 
0270     const QString expected = KShell::quoteArg(ktelnetExec) + QLatin1String(" %1://root@10.1.1.1");
0271     const QStringList protocols({QStringLiteral("ssh"), QStringLiteral("telnet"), QStringLiteral("rlogin")});
0272     for (const QString &protocol : protocols) {
0273         const QList<QUrl> urls({QUrl(QStringLiteral("%1://root@10.1.1.1").arg(protocol))});
0274         KIO::DesktopExecParser parser(*service, urls);
0275         QCOMPARE(KShell::joinArgs(parser.resultingArguments()), expected.arg(protocol));
0276     }
0277 }
0278 
0279 #include "moc_desktopexecparsertest.cpp"