diff --git a/.gitignore b/.gitignore index 2915975..08f5d7b 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ compile_commands.json build/* .idea +.vscode diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b8b2cc..7a0ec52 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,9 @@ cmake_minimum_required(VERSION 3.5) +# Add the tests subdirectory +add_subdirectory(tests) + # Do not print deprecated warnings for Qt5 or KF5 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-deprecated-declarations") @@ -36,53 +39,65 @@ set(CMAKE_INSTALL_RPATH $ORIGIN/../lib) # 'launch' binary in different paths add_executable(launch src/launch.cpp - src/dbmanager.h - src/dbmanager.cpp - src/applicationinfo.h - src/applicationinfo.cpp - src/appdiscovery.h - src/appdiscovery.cpp + src/DbManager.h + src/DbManager.cpp + src/ApplicationInfo.h + src/ApplicationInfo.cpp + src/AppDiscovery.h + src/AppDiscovery.cpp src/extattrs.h src/extattrs.cpp src/launcher.h src/launcher.cpp - src/applicationselectiondialog.h src/applicationselectiondialog.cpp src/applicationselectiondialog.ui + src/ApplicationSelectionDialog.h + src/ApplicationSelectionDialog.cpp + src/ApplicationSelectionDialog.ui + src/Executable.cpp + src/Executable.h ) add_executable(open src/launch.cpp - src/dbmanager.h - src/dbmanager.cpp - src/applicationinfo.h - src/applicationinfo.cpp - src/appdiscovery.h - src/appdiscovery.cpp + src/DbManager.h + src/DbManager.cpp + src/ApplicationInfo.h + src/ApplicationInfo.cpp + src/AppDiscovery.h + src/AppDiscovery.cpp src/extattrs.h src/extattrs.cpp src/launcher.h src/launcher.cpp - src/applicationselectiondialog.h src/applicationselectiondialog.cpp src/applicationselectiondialog.ui + src/ApplicationSelectionDialog.h + src/ApplicationSelectionDialog.cpp + src/ApplicationSelectionDialog.ui + src/Executable.cpp + src/Executable.h ) add_executable(xdg-open src/launch.cpp - src/dbmanager.h - src/dbmanager.cpp - src/applicationinfo.h - src/applicationinfo.cpp - src/appdiscovery.h - src/appdiscovery.cpp + src/DbManager.h + src/DbManager.cpp + src/ApplicationInfo.h + src/ApplicationInfo.cpp + src/AppDiscovery.h + src/AppDiscovery.cpp src/extattrs.h src/extattrs.cpp src/launcher.h src/launcher.cpp - src/applicationselectiondialog.h src/applicationselectiondialog.cpp src/applicationselectiondialog.ui + src/ApplicationSelectionDialog.h + src/ApplicationSelectionDialog.cpp + src/ApplicationSelectionDialog.ui + src/Executable.cpp + src/Executable.h ) add_executable(bundle-thumbnailer src/bundle-thumbnailer.cpp - src/dbmanager.h - src/dbmanager.cpp + src/DbManager.h + src/DbManager.cpp src/extattrs.h src/extattrs.cpp ) diff --git a/src/appdiscovery.cpp b/src/AppDiscovery.cpp similarity index 98% rename from src/appdiscovery.cpp rename to src/AppDiscovery.cpp index 4f0745a..07019ca 100644 --- a/src/appdiscovery.cpp +++ b/src/AppDiscovery.cpp @@ -1,11 +1,11 @@ -#include "appdiscovery.h" +#include "AppDiscovery.h" #include #include #include #include -#include "dbmanager.h" +#include "DbManager.h" AppDiscovery::AppDiscovery(DbManager *db) { diff --git a/src/AppDiscovery.h b/src/AppDiscovery.h new file mode 100644 index 0000000..61b6363 --- /dev/null +++ b/src/AppDiscovery.h @@ -0,0 +1,52 @@ +#ifndef APPDISCOVERY_H +#define APPDISCOVERY_H + +#include + +#include "DbManager.h" + +/** + * @file AppDiscovery.h + * @class AppDiscovery + * @brief A class for discovering and handling application locations. + * + * This class is responsible for discovering well-known application locations and + * finding applications within those locations. + */ +class AppDiscovery +{ +public: + /** + * Constructor. + * + * @param db A pointer to the DbManager instance for database handling. + */ + AppDiscovery(DbManager *db); + + /** + * Destructor. + */ + ~AppDiscovery(); + + /** + * Retrieve a list of well-known application locations. + * + * @return A QStringList containing well-known application locations. + */ + QStringList wellKnownApplicationLocations(); + + /** + * Find and process applications within specified locations. + * + * This function searches for applications within the provided locations and + * handles each discovered application using the associated DbManager instance. + * + * @param locationsContainingApps A list of locations to search for applications. + */ + void findAppsInside(QStringList locationsContainingApps); + +private: + DbManager *dbman; /**< A pointer to the DbManager instance. */ +}; + +#endif // APPDISCOVERY_H diff --git a/src/applicationinfo.cpp b/src/ApplicationInfo.cpp similarity index 84% rename from src/applicationinfo.cpp rename to src/ApplicationInfo.cpp index 58a1998..5db9ad8 100644 --- a/src/applicationinfo.cpp +++ b/src/ApplicationInfo.cpp @@ -1,4 +1,4 @@ -#include "applicationinfo.h" +#include "ApplicationInfo.h" #include #include #include @@ -26,41 +26,36 @@ ApplicationInfo::~ApplicationInfo() { } // Returns the name of the most nested bundle a file is in, // or an empty string if the file is not in a bundle -QString ApplicationInfo::bundlePath(QString path) +QString ApplicationInfo::bundlePath(const QString &path) { - QDir(path).cleanPath(path); + QString ourPath = path; + QDir(path).cleanPath(ourPath); // Remove trailing slashes - while (path.endsWith("/")) { - path.remove(path.length() - 1, 1); + while (ourPath.endsWith("/")) { + ourPath.remove(path.length() - 1, 1); } - if (path.endsWith(".app")) { - return path; - } else if (path.contains(".app/")) { - QStringList parts = path.split(".app"); + if (ourPath.endsWith(".app")) { + return ourPath; + } else if (ourPath.contains(".app/")) { + QStringList parts = ourPath.split(".app"); parts.removeLast(); return parts.join(".app"); - } else if (path.endsWith(".AppDir")) { - return path; - } else if (path.contains(".AppDir/")) { - QStringList parts = path.split(".AppDir"); + } else if (ourPath.endsWith(".AppDir")) { + return ourPath; + } else if (ourPath.contains(".AppDir/")) { + QStringList parts = ourPath.split(".AppDir"); parts.removeLast(); return parts.join(".AppDir"); - } else if (path.endsWith(".AppImage")) { - return path; - } else if (path.endsWith(".desktop")) { - return path; + } else if (ourPath.endsWith(".AppImage")) { + return ourPath; + } else if (ourPath.endsWith(".desktop")) { + return ourPath; } else { return ""; } } -// Returns the name of the bundle -QString ApplicationInfo::bundleName(unsigned long long id) -{ - return ""; -} - -QString ApplicationInfo::applicationNiceNameForPath(QString path) +QString ApplicationInfo::applicationNiceNameForPath(const QString &path) { QString applicationNiceName; QString bp = bundlePath(path); diff --git a/src/ApplicationInfo.h b/src/ApplicationInfo.h new file mode 100644 index 0000000..6ab174f --- /dev/null +++ b/src/ApplicationInfo.h @@ -0,0 +1,94 @@ +#ifndef APPLICATIONINFO_H +#define APPLICATIONINFO_H + +#include + +/* + * https://en.wikipedia.org/wiki/Rule_of_three_(computer_programming) + * Currently being used in: + * Menu (master) + * launch (copy) + */ + +class ApplicationInfo +{ +public: + /** + * Constructor. + * + * Creates an instance of the ApplicationInfo class. + */ + explicit ApplicationInfo(); + + /** + * Destructor. + * + * Cleans up resources associated with the ApplicationInfo instance. + */ + ~ApplicationInfo(); + + /** + * Get the most nested bundle path of a file. + * + * This function returns the name of the most nested bundle a file is in, + * or an empty string if the file is not in a bundle. + * + * @param path The path of the file to check. + * @return The bundle path or an empty string. + */ + static QString bundlePath(const QString &path); + + /** + * Get a human-readable application name for a given path. + * + * This function returns a nice name for the application based on its path. + * + * @param path The path of the application. + * @return The application nice name. + */ + static QString applicationNiceNameForPath(const QString &path); + + /** + * Get the bundle path for a given process ID. + * + * This function returns the bundle path for a process ID, based on the + * LAUNCHED_BUNDLE environment variable set by the 'launch' command. + * + * @param pid The process ID. + * @return The bundle path. + */ + static QString bundlePathForPId(unsigned int pid); + + /** + * Get the bundle path for a given window ID. + * + * This function returns the bundle path associated with a window ID. + * + * @param id The window ID. + * @return The bundle path. + */ + static QString bundlePathForWId(unsigned long long id); + + /** + * Get the path for a given window ID. + * + * This function returns the path associated with a window ID. + * + * @param id The window ID. + * @return The path. + */ + static QString pathForWId(unsigned long long id); + + /** + * Get a human-readable application name for a given window ID. + * + * This function returns a nice name for the application associated with + * a window ID. + * + * @param id The window ID. + * @return The application nice name. + */ + static QString applicationNiceNameForWId(unsigned long long id); +}; + +#endif // APPLICATIONINFO_H diff --git a/src/applicationselectiondialog.cpp b/src/ApplicationSelectionDialog.cpp similarity index 99% rename from src/applicationselectiondialog.cpp rename to src/ApplicationSelectionDialog.cpp index 62aa2bb..870ea7c 100644 --- a/src/applicationselectiondialog.cpp +++ b/src/ApplicationSelectionDialog.cpp @@ -1,5 +1,5 @@ -#include "applicationselectiondialog.h" -#include "ui_applicationselectiondialog.h" +#include "ApplicationSelectionDialog.h" +#include "ui_ApplicationSelectionDialog.h" #include #include "extattrs.h" @@ -7,7 +7,7 @@ #include #include #include "launcher.h" -#include "dbmanager.h" +#include "DbManager.h" #include diff --git a/src/applicationselectiondialog.h b/src/ApplicationSelectionDialog.h similarity index 97% rename from src/applicationselectiondialog.h rename to src/ApplicationSelectionDialog.h index 43d694a..2ade769 100644 --- a/src/applicationselectiondialog.h +++ b/src/ApplicationSelectionDialog.h @@ -3,7 +3,7 @@ #include #include -#include "dbmanager.h" +#include "DbManager.h" namespace Ui { class ApplicationSelectionDialog; diff --git a/src/applicationselectiondialog.ui b/src/ApplicationSelectionDialog.ui similarity index 100% rename from src/applicationselectiondialog.ui rename to src/ApplicationSelectionDialog.ui diff --git a/src/dbmanager.cpp b/src/DbManager.cpp similarity index 99% rename from src/dbmanager.cpp rename to src/DbManager.cpp index dc16afc..04dc2f9 100644 --- a/src/dbmanager.cpp +++ b/src/DbManager.cpp @@ -1,4 +1,4 @@ -#include "dbmanager.h" +#include "DbManager.h" #include #include #include diff --git a/src/dbmanager.h b/src/DbManager.h similarity index 100% rename from src/dbmanager.h rename to src/DbManager.h diff --git a/src/Executable.cpp b/src/Executable.cpp new file mode 100644 index 0000000..2a377f8 --- /dev/null +++ b/src/Executable.cpp @@ -0,0 +1,86 @@ +#include "Executable.h" +#include +#include +#include +#include +#include +#include +#include + +bool Executable::isExecutable(const QString& path) { + QFileInfo fileInfo(path); + return fileInfo.isExecutable(); +} + +bool Executable::hasShebang(const QString& path) { + QFile file(path); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qWarning() << "Failed to open file:" << path; + return false; + } + + QTextStream in(&file); + QString firstLine = in.readLine(); + file.close(); + + return firstLine.startsWith("#!"); +} + +bool Executable::isElf(const QString& path) { + QMimeDatabase mimeDatabase; + QMimeType mimeType = mimeDatabase.mimeTypeForFile(path); + + if (mimeType.isValid() && mimeType.name().startsWith("application/x-executable")) { + qDebug() << "File is an ELF executable."; + return true; + } else { + qDebug() << "File is not an ELF executable."; + return false; + } +} + +bool Executable::askUserToMakeExecutable(const QString& path) { + if (!isExecutable(path)) { + QString message = tr("The file is not executable:\n%1\n\nDo you want to make it executable?\n\nYou should only do this if you trust this file.") + .arg(path); + QMessageBox::StandardButton response = QMessageBox::question(nullptr, tr("Make Executable"), message, + QMessageBox::Yes | QMessageBox::No); + + if (response == QMessageBox::Yes) { + QProcess process; + QStringList arguments; + arguments << "-A" << "-E" << "chmod" << "+x" << path; + + process.setProgram("sudo"); + process.setArguments(arguments); + + process.start(); + if (process.waitForFinished() && process.exitCode() == 0) { + // QMessageBox::information(nullptr, tr("Success"), tr("File is now executable.")); + return true; + } else { + QMessageBox::warning(nullptr, tr("Error"), tr("Failed to make the file executable.")); + return false; + } + } else { + // QMessageBox::information(nullptr, tr("Info"), tr("File was not made executable.")); + return false; + } + } else { + // QMessageBox::information(nullptr, tr("Info"), tr("The file is already executable.")); + return true; + } +} + +bool Executable::hasShebangOrIsElf(const QString& path) { + if (hasShebang(path)) { + qDebug() << tr("File has a shebang."); + return true; + } else if (isElf(path)) { + qDebug() << tr("File is an ELF."); + return true; + } else { + qDebug() << tr("File does not have a shebang or is not an ELF."); + return false; + } +} diff --git a/src/Executable.h b/src/Executable.h new file mode 100644 index 0000000..307b755 --- /dev/null +++ b/src/Executable.h @@ -0,0 +1,60 @@ +#ifndef EXECUTABLE_H +#define EXECUTABLE_H + +#include +#include + +/** + * @file Executable.h + * @class Executable + * @brief A class to provide utility methods related to ELF executables and interpreted scripts. + */ +class Executable : public QObject { + Q_OBJECT +public: + /** + * Check if a file is executable. + * + * @param path The path to the file. + * @return True if the file is executable, false otherwise. + */ + static bool isExecutable(const QString& path); + + /** + * Check if a file has a shebang line. + * + * @param path The path to the file. + * @return True if the file has a shebang, false otherwise. + */ + static bool hasShebang(const QString& path); + + /** + * Check if a file has a shebang line or is an ELF executable. + * + * @param path The path to the file. + * @return True if the file has a shebang or is an ELF, false otherwise. + */ + static bool hasShebangOrIsElf(const QString& path); + + /** + * Check if a file is an ELF executable. + * + * @param path The path to the file. + * @return True if the file is an ELF, false otherwise. + */ + static bool isElf(const QString& path); + + /** + * @brief Ask the user if they want to make a file executable and perform the action if requested. + * + * This method displays a dialog asking the user if they want to make the specified file executable. + * If the user agrees, the file's permissions are modified accordingly. + * + * @param path The path to the file. + * @return True if the file is now executable or already executable, false otherwise. + * If an error occurs during permission modification, false is returned as well. + */ + static bool askUserToMakeExecutable(const QString& path); +}; + +#endif // EXECUTABLE_H diff --git a/src/appdiscovery.h b/src/appdiscovery.h deleted file mode 100644 index 4bb2033..0000000 --- a/src/appdiscovery.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef APPDISCOVERY_H -#define APPDISCOVERY_H - -#include - -#include "dbmanager.h" - -class AppDiscovery -{ -public: - AppDiscovery(DbManager *db); - ~AppDiscovery(); - QStringList wellKnownApplicationLocations(); - void findAppsInside(QStringList locationsContainingApps); - -private: - DbManager *dbman; -}; - -#endif // APPDISCOVERY_H diff --git a/src/applicationinfo.h b/src/applicationinfo.h deleted file mode 100644 index 4e3dcbe..0000000 --- a/src/applicationinfo.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef APPLICATIONINFO_H -#define APPLICATIONINFO_H - -#include - -/* - * https://en.wikipedia.org/wiki/Rule_of_three_(computer_programming) - * Currently being used in: - * Menu (master) - * launch (copy) - */ - -class ApplicationInfo -{ - -public: - explicit ApplicationInfo(); - ~ApplicationInfo(); - QString bundlePath(QString path); - QString bundleName(unsigned long long id); - QString applicationNiceNameForPath(QString path); - QString bundlePathForPId(unsigned int pid); - QString bundlePathForWId(unsigned long long id); - QString pathForWId(unsigned long long id); - QString applicationNiceNameForWId(unsigned long long id); -}; - -#endif // APPLICATIONINFO_H diff --git a/src/bundle-thumbnailer.cpp b/src/bundle-thumbnailer.cpp index cfbf871..6e1724c 100644 --- a/src/bundle-thumbnailer.cpp +++ b/src/bundle-thumbnailer.cpp @@ -3,7 +3,7 @@ #include #include -#include "dbmanager.h" +#include "DbManager.h" int main(int argc, char *argv[]) { diff --git a/src/launcher.cpp b/src/launcher.cpp index b5bb944..1e4b71b 100644 --- a/src/launcher.cpp +++ b/src/launcher.cpp @@ -1,10 +1,11 @@ #include "launcher.h" -#include "applicationselectiondialog.h" +#include "ApplicationSelectionDialog.h" #include #include #include #include #include +#include "Executable.h" Launcher::Launcher() : db(new DbManager()) { } @@ -184,9 +185,21 @@ QStringList Launcher::executableForBundleOrExecutablePath(QString bundleOrExecut } else { executableAndArgs = execStringAndArgs; } - } else if (info.isExecutable() && !info.isDir()) { - qDebug() << "# Found executable" << bundleOrExecutablePath; - executableAndArgs = QStringList({ bundleOrExecutablePath }); + } else if (!info.isDir()) { + if(Executable::hasShebangOrIsElf(bundleOrExecutablePath)) { + if(info.isExecutable()) { + qDebug() << "# Found executable" << bundleOrExecutablePath; + executableAndArgs = QStringList({ bundleOrExecutablePath }); + } else { + qDebug() << "# Found non-executable" << bundleOrExecutablePath; + bool success = Executable::askUserToMakeExecutable(bundleOrExecutablePath); + if (!success) { + exit(1); + } else { + executableAndArgs = QStringList({ bundleOrExecutablePath }); + } + } + } } } return executableAndArgs; @@ -194,12 +207,15 @@ QStringList Launcher::executableForBundleOrExecutablePath(QString bundleOrExecut QString Launcher::pathWithoutBundleSuffix(QString path) { - // FIXME: This is very lazy; TODO: Do it properly - return path.replace(".AppDir", "") - .replace(".app", "") - .replace(".desktop", "") - .replace(".AppImage", "") - .replace(".appimage", ""); + // List of common bundle suffixes to remove + QStringList bundleSuffixes = { ".AppDir", ".app", ".desktop", ".AppImage", ".appimage" }; + + QString cleanedPath = path; + for (const QString &suffix : bundleSuffixes) { + cleanedPath = cleanedPath.remove(QRegularExpression(suffix, QRegularExpression::CaseInsensitiveOption)); + } + + return cleanedPath; } int Launcher::launch(QStringList args) @@ -377,11 +393,10 @@ int Launcher::launch(QStringList args) if (args.length() < 1 && env.contains("LAUNCHED_BUNDLE") && (firstArg != "Menu")) { qDebug() << "# Checking for existing windows"; const auto &windows = KWindowSystem::windows(); - ApplicationInfo *ai = new ApplicationInfo(); bool foundExistingWindow = false; for (WId wid : windows) { - QString runningBundle = ai->bundlePathForWId(wid); + QString runningBundle = ApplicationInfo::bundlePathForWId(wid); if (runningBundle == env.value("LAUNCHED_BUNDLE")) { // Check if the user ID which the application is running under is the same user ID as is the current user // This is to avoid bringing to the front windows of other users (e.g., if we want to run as root) @@ -426,7 +441,6 @@ int Launcher::launch(QStringList args) qDebug() << "# Did not find existing windows for LAUNCHED_BUNDLE" << env.value("LAUNCHED_BUNDLE"); } - ai->~ApplicationInfo(); } else if (args.length() > 1) { qDebug() << "# Not checking for existing windows because arguments were " "passed to the application"; @@ -438,9 +452,7 @@ int Launcher::launch(QStringList args) p.start(); // Tell Menu that an application is being launched - ApplicationInfo *ai = new ApplicationInfo(); - QString bPath = ai->bundlePath(p.program()); - ai->~ApplicationInfo(); + QString bPath = ApplicationInfo::bundlePath(p.program()); // Blocks until process has started if (!p.waitForStarted()) { diff --git a/src/launcher.h b/src/launcher.h index 3a4fdcd..fe7e70d 100644 --- a/src/launcher.h +++ b/src/launcher.h @@ -28,9 +28,9 @@ #include -#include "dbmanager.h" -#include "applicationinfo.h" -#include "appdiscovery.h" +#include "DbManager.h" +#include "ApplicationInfo.h" +#include "AppDiscovery.h" #include "extattrs.h" class QDetachableProcess : public QProcess diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..5992b33 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.15) + +project("test") + +# Find the Qt5 package +find_package(Qt5 REQUIRED COMPONENTS Test Gui Widgets) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../src/) + +# Find the Qt5 package and its components +find_package(Qt5 REQUIRED COMPONENTS Test Gui Widgets) + +# Set up your project sources and headers +set(SOURCES + testExecutable.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../src/Executable.h + ${CMAKE_CURRENT_SOURCE_DIR}/../src/Executable.cpp + ) + +# Add the executable for your tests +add_executable(${PROJECT_NAME} ${SOURCES}) + +# Link against Qt libraries +target_link_libraries(${PROJECT_NAME} PRIVATE Qt5::Test Qt5::Gui Qt5::Widgets) + +# Define a CTest test +add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}_tests) \ No newline at end of file diff --git a/tests/CTestTestfile.cmake b/tests/CTestTestfile.cmake new file mode 100644 index 0000000..2e131d2 --- /dev/null +++ b/tests/CTestTestfile.cmake @@ -0,0 +1,2 @@ +# Define a CTest test +add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) diff --git a/tests/testExecutable.cpp b/tests/testExecutable.cpp new file mode 100644 index 0000000..245be05 --- /dev/null +++ b/tests/testExecutable.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include "Executable.h" + +class TestExecutable : public QObject { + Q_OBJECT + +private slots: + void testIsExecutable() { + QVERIFY(Executable::isExecutable("/usr/bin/env")); + QVERIFY(!Executable::isExecutable("/etc/os-release")); + } + + void testHasShebang() { + QVERIFY(Executable::hasShebang("/usr/bin/bg")); + QVERIFY(!Executable::hasShebang("/etc/os-release")); + } + + void testHasShebangOrIsElf() { + QVERIFY(Executable::hasShebangOrIsElf("/usr/bin/bg")); + QVERIFY(Executable::hasShebangOrIsElf("/usr/bin/env")); + QVERIFY(!Executable::hasShebangOrIsElf("/etc/os-release")); + } + + }; + +QTEST_APPLESS_MAIN(TestExecutable) + +#include "testExecutable.moc"