[kde-linux/package-compatibility-helper] /: Add DOS program support, batch file handling and a Windows binfmt tree
Hadi Chokr <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit d8ee3f3a725ed3d7163af7d2cf899f7c3642c0ec by Hadi Chokr. Committed on 25/07/2026 at 09:51. Pushed by silverhadch into branch 'master'. Add DOS program support, batch file handling and a Windows binfmt tree Classify executables by their MZ/NE/PE headers instead of trusting the MIME type. Real-mode DOS programs are routed to a DOS emulator (DOSBox Staging, via the new dosbox.conf); PE and NE executables, MSI packages, batch files and shortcuts stay with Wine. The app database is still consulted for DOS programs, so a native alternative is offered before falling back to emulation. Register the batch and NE executable MIME types, which previously never triggered the helper at all. Add a "windows" tree with a binfmt rule on the MZ magic, so executing any DOS or Windows binary directly from a shell also opens the helper. Signed-off-by: Hadi Chokr <[email protected]> C +4 -4 data/apps/dosbox.conf [from: data/apps/wine.conf - 053% similarity] M +2 -1 data/apps/wine.conf A +81 -0 src/BatchCompatibilityHelper.cpp [License: LGPL(3+eV) LGPL(v3.0) LGPL(v2.0)] A +37 -0 src/BatchCompatibilityHelper.h [License: LGPL(3+eV) LGPL(v3.0) LGPL(v2.0)] M +2 -0 src/CMakeLists.txt M +45 -7 src/CompatibilityHelperFactory.cpp M +2 -1 src/CompatibilityHelperFactory.h M +79 -0 src/ICompatibilityHelper.cpp M +23 -0 src/ICompatibilityHelper.h M +52 -0 src/PackageUtils.cpp M +17 -0 src/PackageUtils.h M +18 -7 src/WindowsCompatibilityHelper.cpp M +3 -1 src/WindowsCompatibilityHelper.h M +28 -0 src/contents/ui/Main.qml M +1 -1 src/org.kde.package-compatibility-helper.desktop A +9 -0 trees/windows/usr/lib/binfmt.d/windows.conf.in A +4 -0 trees/windows/usr/libexec/windows-binfmt.in https://invent.kde.org/kde-linux/package-compatibility-helper/-/commit/d8ee3f3a725ed3d7163af7d2cf899f7c3642c0ec diff --git a/data/apps/wine.conf b/data/apps/dosbox.conf similarity index 53% copy from data/apps/wine.conf copy to data/apps/dosbox.conf index 5866211..26f4b9f 100644 --- a/data/apps/wine.conf +++ b/data/apps/dosbox.conf @@ -1,10 +1,10 @@ # SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL # SPDX-FileCopyrightText: 2026 Hadi Chokr <[email protected]> [App] -Id=org.winehq.Wine -Name=Wine -Icon=wine-symbolic -MimeTypes=application/x-ms-dos-executable,application/x-msi,application/x-ms-shortcut,application/vnd.microsoft.portable-executable,application/x-msdownload +Id=io.github.dosbox-staging +Name=DOSBox Staging +Icon=io.github.dosbox-staging +MimeTypes=application/x-ms-dos-executable,application/x-dosexec [Remote] Name=flathub diff --git a/data/apps/wine.conf b/data/apps/wine.conf index 5866211..ade778d 100644 --- a/data/apps/wine.conf +++ b/data/apps/wine.conf @@ -4,7 +4,8 @@ Id=org.winehq.Wine Name=Wine Icon=wine-symbolic -MimeTypes=application/x-ms-dos-executable,application/x-msi,application/x-ms-shortcut,application/vnd.microsoft.portable-executable,application/x-msdownload +# DOS executables are handled by dosbox.conf. +MimeTypes=application/x-msi,application/x-ms-shortcut,application/vnd.microsoft.portable-executable,application/x-msdownload,application/x-ms-ne-executable,application/x-bat,application/x-msdos-batch [Remote] Name=flathub diff --git a/src/BatchCompatibilityHelper.cpp b/src/BatchCompatibilityHelper.cpp new file mode 100644 index 0000000..8d769b0 --- /dev/null +++ b/src/BatchCompatibilityHelper.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +// SPDX-FileCopyrightText: 2026 Hadi Chokr <[email protected]> + +#include "BatchCompatibilityHelper.h" +#include "CompatibilityToolInstaller.h" + +#include <KLocalizedString> + +BatchCompatibilityHelper::BatchCompatibilityHelper(const QUrl &filePath, QObject *parent) + : ICompatibilityHelper(filePath, parent) +{ + // Wine (the primary tool) is attached by the factory. Load DOSBox as the + // secondary tool for the rare case of a genuine MS-DOS batch file. + if (CompatibilityToolInstaller *dosbox = CompatibilityToolInstaller::load(u"dosbox"_s, this)) { + setSecondaryCompatibilityToolInstaller(dosbox); + } +} + +QString BatchCompatibilityHelper::windowTitle() const +{ + return m_filePath.fileName(); +} + +QString BatchCompatibilityHelper::heading() const +{ + return i18n("Batch scripts are not natively supported on %1", distroName()); +} + +QString BatchCompatibilityHelper::icon() const +{ + return u"application-x-ms-dos-executable"_s; +} + +QString BatchCompatibilityHelper::description() const +{ + QString desc = i18n( + "This is a batch script. It could be either a modern Windows batch file or an old MS-DOS batch " + "file, and the two cannot be told apart automatically, so please choose how to run it."); + + if (hasCompatibilityTool() && hasSecondaryCompatibilityTool()) { + desc += u"<br><br>"_s; + // %1 is Wine, %2 is DOSBox. + desc += i18n( + "If you don't know what DOS is, or the software was written within roughly the last 30 years, " + "choose <b>%1</b>. Only choose <b>%2</b> if you know this is an MS-DOS batch file.", + compatibilityToolName(), + secondaryCompatibilityToolName()); + } + + return desc; +} + +bool BatchCompatibilityHelper::hasNativeApp() const +{ + return false; +} + +QString BatchCompatibilityHelper::nativeAppActionText() const +{ + return QString(); +} + +QString BatchCompatibilityHelper::nativeAppActionIcon() const +{ + return QString(); +} + +QString BatchCompatibilityHelper::nativeAppName() const +{ + return QString(); +} + +QString BatchCompatibilityHelper::nativeAppRef() const +{ + return QString(); +} + +bool BatchCompatibilityHelper::isNativeAppInstalled() const +{ + return false; +} diff --git a/src/BatchCompatibilityHelper.h b/src/BatchCompatibilityHelper.h new file mode 100644 index 0000000..d54e6d5 --- /dev/null +++ b/src/BatchCompatibilityHelper.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +// SPDX-FileCopyrightText: 2026 2026 Hadi Chokr <[email protected]> + +#pragma once + +#include "ICompatibilityHelper.h" + +using namespace Qt::Literals::StringLiterals; + +// Helper for .bat files, which are indistinguishable between old MS-DOS batch +// scripts and modern Windows batch scripts. Rather than guess, it offers both +// compatibility tools: Wine as the primary (default) tool and DOSBox as the +// secondary one, with a description explaining which to pick. +// +// The primary (Wine) installer is attached by CompatibilityHelperFactory via +// the usual MIME-type lookup; the DOSBox installer is loaded here. +class BatchCompatibilityHelper : public ICompatibilityHelper +{ + Q_OBJECT + +public: + explicit BatchCompatibilityHelper(const QUrl &filePath, QObject *parent = nullptr); + ~BatchCompatibilityHelper() override = default; + + QString windowTitle() const override; + QString heading() const override; + QString icon() const override; + QString description() const override; + bool hasNativeApp() const override; + QString nativeAppActionText() const override; + QString nativeAppActionIcon() const override; + +private: + QString nativeAppName() const override; + QString nativeAppRef() const override; + bool isNativeAppInstalled() const override; +}; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f305eb7..11013c4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -15,6 +15,7 @@ target_sources(packagecompatibilityhelper_static PUBLIC ICompatibilityHelper.cpp CompatibilityHelperFactory.cpp WindowsCompatibilityHelper.cpp + BatchCompatibilityHelper.cpp RpmCompatibilityHelper.cpp DebCompatibilityHelper.cpp GenericCompatibilityHelper.cpp @@ -57,6 +58,7 @@ add_executable(package-compatibility-helper main.cpp) target_link_libraries(package-compatibility-helper PUBLIC packagecompatibilityhelper_static packagecompatibilityhelper_staticplugin) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/app_db.json DESTINATION ${KDE_INSTALL_DATADIR}/${PROJECT_NAME}) install(FILES ${CMAKE_SOURCE_DIR}/data/apps/wine.conf + ${CMAKE_SOURCE_DIR}/data/apps/dosbox.conf DESTINATION ${KDE_INSTALL_DATADIR}/${PROJECT_NAME}/apps) install(TARGETS package-compatibility-helper ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/CompatibilityHelperFactory.cpp b/src/CompatibilityHelperFactory.cpp index e06bc0f..004ea8c 100644 --- a/src/CompatibilityHelperFactory.cpp +++ b/src/CompatibilityHelperFactory.cpp @@ -2,15 +2,18 @@ // SPDX-FileCopyrightText: 2025 Thomas Duckworth <[email protected]> #include "CompatibilityHelperFactory.h" +#include "BatchCompatibilityHelper.h" #include "CompatibilityToolInstaller.h" #include "DebCompatibilityHelper.h" #include "GenericCompatibilityHelper.h" #include "ICompatibilityHelper.h" +#include "PackageUtils.h" #include "RpmCompatibilityHelper.h" #include "WindowsCompatibilityHelper.h" #include "directories.h" #include <QMimeDatabase> +#include <QMimeType> ICompatibilityHelper *CompatibilityHelperFactory::create(const QUrl &filePath) { @@ -19,14 +22,42 @@ ICompatibilityHelper *CompatibilityHelperFactory::create(const QUrl &filePath) } QMimeDatabase mimeDb; - QString mimeTypeName = mimeDb.mimeTypeForFile(filePath.toLocalFile()).name(); + const QMimeType mimeType = mimeDb.mimeTypeForFile(filePath.toLocalFile()); + const QString mimeTypeName = mimeType.name(); - CompatibilityToolInstaller *installer = CompatibilityToolInstaller::findForMimeType(mimeTypeName); + // Not MZ binaries, matched by name. + static const QStringList windowsAuxMimeTypes = { + u"application/x-msi"_s, + u"application/x-ms-shortcut"_s, + u"application/x-bat"_s, + u"application/x-msdos-batch"_s, + }; + + // All MZ executables (DOS, NE, PE) inherit these two, regardless of + // shared-mime-info version; inherits() also resolves aliases. + const bool isWindowsExecutable = mimeType.inherits(u"application/x-ms-dos-executable"_s) || mimeType.inherits(u"application/x-msdownload"_s); + + // The MIME type used to look up the compatibility tool config. + QString toolMimeType = mimeTypeName; ICompatibilityHelper *helper = nullptr; - if (mimeTypeName == u"application/x-ms-dos-executable"_s || mimeTypeName == u"application/x-msi"_s || mimeTypeName == u"application/x-ms-shortcut"_s - || mimeTypeName == u"application/vnd.microsoft.portable-executable"_s || mimeTypeName == u"application/x-msdownload"_s) { - helper = createWindowsCompatibilityHelper(QUrl::fromLocalFile(WINDOWSCOMPATIBILITYHELPER_DB_PATH), filePath); + if (isWindowsExecutable || windowsAuxMimeTypes.contains(mimeTypeName)) { + const WindowsBinaryKind kind = classifyWindowsBinary(filePath.toLocalFile(), mimeTypeName); + + if (kind == WindowsBinaryKind::Batch) { + // A .bat file cannot be told apart from a DOS batch file, so offer + // both Wine and DOSBox. Wine is the primary (highlighted) tool via + // the PE MIME type below; BatchCompatibilityHelper loads DOSBox as + // the secondary tool itself. + toolMimeType = u"application/vnd.microsoft.portable-executable"_s; + helper = createBatchCompatibilityHelper(filePath); + } else { + // DOS programs get the DOS emulator; PE and NE executables, MSI + // packages, .cmd/shortcut files get Wine. + const bool isDosProgram = kind == WindowsBinaryKind::DosProgram; + toolMimeType = isDosProgram ? u"application/x-ms-dos-executable"_s : u"application/vnd.microsoft.portable-executable"_s; + helper = createWindowsCompatibilityHelper(QUrl::fromLocalFile(WINDOWSCOMPATIBILITYHELPER_DB_PATH), filePath, isDosProgram); + } } else if (mimeTypeName == u"application/x-rpm"_s) { helper = createRpmCompatibilityHelper(filePath); } else if (mimeTypeName == u"application/vnd.debian.binary-package"_s || mimeTypeName == u"application-x-deb"_s) { @@ -38,6 +69,8 @@ ICompatibilityHelper *CompatibilityHelperFactory::create(const QUrl &filePath) helper = createAppImageCompatibilityHelper(filePath); }*/ + CompatibilityToolInstaller *installer = CompatibilityToolInstaller::findForMimeType(toolMimeType); + // This returns when no compatible helper was found for the given file type. // At this point, the program should exit. if (!helper && installer) { @@ -55,9 +88,14 @@ ICompatibilityHelper *CompatibilityHelperFactory::create(const QUrl &filePath) return helper; } -ICompatibilityHelper *CompatibilityHelperFactory::createWindowsCompatibilityHelper(const QUrl &databaseFilePath, const QUrl &openedExePath) +ICompatibilityHelper *CompatibilityHelperFactory::createWindowsCompatibilityHelper(const QUrl &databaseFilePath, const QUrl &openedExePath, bool isDosProgram) +{ + return new WindowsCompatibilityHelper(databaseFilePath, openedExePath, isDosProgram); +} + +ICompatibilityHelper *CompatibilityHelperFactory::createBatchCompatibilityHelper(const QUrl &filePath) { - return new WindowsCompatibilityHelper(databaseFilePath, openedExePath); + return new BatchCompatibilityHelper(filePath); } ICompatibilityHelper *CompatibilityHelperFactory::createRpmCompatibilityHelper(const QUrl &filePath) diff --git a/src/CompatibilityHelperFactory.h b/src/CompatibilityHelperFactory.h index f7a38cf..d4d976d 100644 --- a/src/CompatibilityHelperFactory.h +++ b/src/CompatibilityHelperFactory.h @@ -13,7 +13,8 @@ public: static ICompatibilityHelper *create(const QUrl &filePath); private: - static ICompatibilityHelper *createWindowsCompatibilityHelper(const QUrl &databaseFilePath, const QUrl &openedExePath); + static ICompatibilityHelper *createWindowsCompatibilityHelper(const QUrl &databaseFilePath, const QUrl &openedExePath, bool isDosProgram); + static ICompatibilityHelper *createBatchCompatibilityHelper(const QUrl &filePath); static ICompatibilityHelper *createRpmCompatibilityHelper(const QUrl &filePath); static ICompatibilityHelper *createDebCompatibilityHelper(const QUrl &filePath); static ICompatibilityHelper *createGenericCompatibilityHelper(const QUrl &filePath); diff --git a/src/ICompatibilityHelper.cpp b/src/ICompatibilityHelper.cpp index fbc1ac1..a7c6c6e 100644 --- a/src/ICompatibilityHelper.cpp +++ b/src/ICompatibilityHelper.cpp @@ -139,6 +139,11 @@ void ICompatibilityHelper::setCompatibilityToolInstaller(CompatibilityToolInstal m_compatibilityToolInstaller = installer; } +void ICompatibilityHelper::setSecondaryCompatibilityToolInstaller(CompatibilityToolInstaller *installer) +{ + m_secondaryCompatibilityToolInstaller = installer; +} + bool ICompatibilityHelper::installOnly() const { return m_installOnly; @@ -221,3 +226,77 @@ void ICompatibilityHelper::compatibilityToolInstallFinished() const m_compatibilityToolInstaller->runPostInstall(); openApp(m_compatibilityToolInstaller->appId(), {m_filePath}); } + +bool ICompatibilityHelper::hasSecondaryCompatibilityTool() const +{ + return m_secondaryCompatibilityToolInstaller != nullptr; +} + +QString ICompatibilityHelper::secondaryCompatibilityToolName() const +{ + return m_secondaryCompatibilityToolInstaller != nullptr ? m_secondaryCompatibilityToolInstaller->displayName() : QString(); +} + +QObject *ICompatibilityHelper::secondaryCompatibilityToolInstaller() const +{ + return m_secondaryCompatibilityToolInstaller; +} + +bool ICompatibilityHelper::isSecondaryCompatibilityToolInstalled() const +{ + if (!m_secondaryCompatibilityToolInstaller) { + return false; + } + return m_secondaryCompatibilityToolInstaller->isInstalled(); +} + +bool ICompatibilityHelper::secondaryCompatibilityToolInstalled() const +{ + return isSecondaryCompatibilityToolInstalled(); +} + +QString ICompatibilityHelper::secondaryCompatibilityToolActionText() const +{ + if (!hasSecondaryCompatibilityTool()) { + return QString(); + } + if (isSecondaryCompatibilityToolInstalled()) { + return i18n("Run with %1", secondaryCompatibilityToolName()); + } + return i18n("Install %1", secondaryCompatibilityToolName()); +} + +QString ICompatibilityHelper::secondaryCompatibilityToolActionIcon() const +{ + if (!m_secondaryCompatibilityToolInstaller) { + return QString(); + } + if (isSecondaryCompatibilityToolInstalled() && hasIcon(m_secondaryCompatibilityToolInstaller->appId())) { + return m_secondaryCompatibilityToolInstaller->appId(); + } + return m_secondaryCompatibilityToolInstaller->icon(); +} + +void ICompatibilityHelper::launchSecondaryCompatibilityTool() const +{ + if (!hasSecondaryCompatibilityTool()) { + qWarning() << "Invalid operation: No secondary compatibility tool is available for this file type."; + return; + } + if (!isSecondaryCompatibilityToolInstalled()) { + qWarning() << "Invalid operation: The secondary compatibility tool is not installed."; + return; + } + // Deliberately not calling takeOverMimeTypes() here: the secondary tool is + // the non-default choice, so it should not claim the MIME type handler. + openApp(m_secondaryCompatibilityToolInstaller->appId(), {m_filePath}); +} + +void ICompatibilityHelper::secondaryCompatibilityToolInstallFinished() const +{ + if (!m_secondaryCompatibilityToolInstaller) { + return; + } + m_secondaryCompatibilityToolInstaller->runPostInstall(); + openApp(m_secondaryCompatibilityToolInstaller->appId(), {m_filePath}); +} diff --git a/src/ICompatibilityHelper.h b/src/ICompatibilityHelper.h index 7295b4b..947a16d 100644 --- a/src/ICompatibilityHelper.h +++ b/src/ICompatibilityHelper.h @@ -48,6 +48,17 @@ class ICompatibilityHelper : public QObject Q_PROPERTY(bool compatibilityToolInstalled READ compatibilityToolInstalled CONSTANT) // The Flatpak installer configuration for the compatibility tool. Q_PROPERTY(QObject *compatibilityToolInstaller READ compatibilityToolInstaller CONSTANT) + + // A second, optional compatibility tool offered alongside the primary one. + // Used when a file type could be handled by either of two tools and the two + // cannot be told apart automatically, e.g. a .bat file (Wine or DOSBox). + // Defaults to unset, so ordinary helpers are unaffected. + Q_PROPERTY(bool hasSecondaryCompatibilityTool READ hasSecondaryCompatibilityTool CONSTANT) + Q_PROPERTY(QString secondaryCompatibilityToolActionText READ secondaryCompatibilityToolActionText CONSTANT) + Q_PROPERTY(QString secondaryCompatibilityToolActionIcon READ secondaryCompatibilityToolActionIcon CONSTANT) + Q_PROPERTY(bool secondaryCompatibilityToolInstalled READ secondaryCompatibilityToolInstalled CONSTANT) + Q_PROPERTY(QObject *secondaryCompatibilityToolInstaller READ secondaryCompatibilityToolInstaller CONSTANT) + Q_PROPERTY(bool installOnly READ installOnly CONSTANT) // The text to show the user for the action to open the documentation. @@ -75,10 +86,16 @@ public: virtual QString compatibilityToolActionIcon() const; bool compatibilityToolInstalled() const; QObject *compatibilityToolInstaller() const; + virtual bool hasSecondaryCompatibilityTool() const; + QString secondaryCompatibilityToolActionText() const; + QString secondaryCompatibilityToolActionIcon() const; + bool secondaryCompatibilityToolInstalled() const; + QObject *secondaryCompatibilityToolInstaller() const; QString documentationActionText() const; QString documentationActionIcon() const; void setCompatibilityToolInstaller(CompatibilityToolInstaller *installer); + void setSecondaryCompatibilityToolInstaller(CompatibilityToolInstaller *installer); bool installOnly() const; void setInstallOnly(bool installOnly); @@ -89,6 +106,9 @@ public: Q_INVOKABLE virtual void launchCompatibilityTool() const; // Runs the post-install hook and opens the file with the tool. Q_INVOKABLE void compatibilityToolInstallFinished() const; + // Same as the two above, but for the optional secondary tool. + Q_INVOKABLE void launchSecondaryCompatibilityTool() const; + Q_INVOKABLE void secondaryCompatibilityToolInstallFinished() const; // Opens the documentation link. // This is a generic action, so it doesn't need to be overridden in subclasses. Q_INVOKABLE void documentationAction() const; @@ -111,8 +131,10 @@ protected: // Indicates if the compatibility tool is already installed on the system. virtual bool isCompatibilityToolInstalled() const; + bool isSecondaryCompatibilityToolInstalled() const; QString compatibilityToolName() const; + QString secondaryCompatibilityToolName() const; // Returns the documentation URL, which can be overridden per each mime type. virtual QUrl documentationUrl() const; @@ -143,5 +165,6 @@ protected: private: CompatibilityToolInstaller *m_compatibilityToolInstaller = nullptr; + CompatibilityToolInstaller *m_secondaryCompatibilityToolInstaller = nullptr; bool m_installOnly = false; }; diff --git a/src/PackageUtils.cpp b/src/PackageUtils.cpp index e561788..cfafb66 100644 --- a/src/PackageUtils.cpp +++ b/src/PackageUtils.cpp @@ -3,9 +3,13 @@ #include <QDebug> #include <QDomDocument> +#include <QFile> #include <QProcess> #include <QRegularExpression> #include <QUrl> +#include <QtEndian> + +#include <cstring> #include "PackageUtils.h" @@ -160,3 +164,51 @@ bool extractMetainfoFiles(const QUrl &archivePath, QStringList &metainfoFilesCon return true; } + +WindowsBinaryKind classifyWindowsBinary(const QString &localFilePath, const QString &mimeTypeName) +{ + if (mimeTypeName == u"application/x-bat"_s || mimeTypeName == u"application/x-msdos-batch"_s) { + // .cmd was introduced with Windows NT and is only ever run by cmd.exe, + // so it is unambiguously a Windows batch script. + if (localFilePath.endsWith(u".cmd"_s, Qt::CaseInsensitive)) { + return WindowsBinaryKind::WindowsBatch; + } + // A .bat file is plain text with no magic number and has meant both a + // DOS and a Windows batch script for its whole history. The two share + // almost all their syntax, so they cannot be told apart reliably. + return WindowsBinaryKind::Batch; + } + if (mimeTypeName == u"application/x-msi"_s) { + return WindowsBinaryKind::Msi; + } + if (mimeTypeName == u"application/x-ms-shortcut"_s) { + return WindowsBinaryKind::Shortcut; + } + + QFile file(localFilePath); + if (!file.open(QIODevice::ReadOnly)) { + return WindowsBinaryKind::NotWindows; + } + + const QByteArray dosHeader = file.read(0x40); + if (dosHeader.size() < 0x40 || dosHeader.at(0) != 'M' || dosHeader.at(1) != 'Z') { + return WindowsBinaryKind::NotWindows; + } + + // e_lfanew points to the NE/PE/LE header, if any. + const auto *rawHeader = reinterpret_cast<const uchar *>(dosHeader.constData()); + const quint32 newHeaderOffset = qFromLittleEndian<quint32>(rawHeader + 0x3C); + if (newHeaderOffset < 0x40 || quint64(newHeaderOffset) + 4 > quint64(file.size()) || !file.seek(newHeaderOffset)) { + return WindowsBinaryKind::DosProgram; + } + + const QByteArray newHeader = file.read(4); + if (newHeader.size() >= 4 && std::memcmp(newHeader.constData(), "PE\0\0", 4) == 0) { + return WindowsBinaryKind::WindowsPE; + } + if (newHeader.size() >= 2 && newHeader.at(0) == 'N' && newHeader.at(1) == 'E') { + return WindowsBinaryKind::Win16; + } + + return WindowsBinaryKind::DosProgram; +} diff --git a/src/PackageUtils.h b/src/PackageUtils.h index 27b9667..12d978f 100644 --- a/src/PackageUtils.h +++ b/src/PackageUtils.h @@ -1,6 +1,8 @@ // SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL // SPDX-FileCopyrightText: 2025 Thomas Duckworth <[email protected]> +#pragma once + #include <QDebug> #include <QString> @@ -12,3 +14,18 @@ void matchFlatpakFromMetainfo(QStringList metainfoFilesContent, QString &nativeA // Extract metainfo files in a package format-agnostic way, using bsdtar. bool extractMetainfoFiles(const QUrl &archivePath, QStringList &metainfoFilesContent); + +// Classifies DOS/Windows files by their MZ/NE/PE headers, since the MIME +// types reported for MZ executables vary between systems. +enum class WindowsBinaryKind { + NotWindows, + DosProgram, // Real-mode MZ, incl. LE/LX + Win16, // NE + WindowsPE, + Batch, // Ambiguous .bat: could be a DOS or a Windows batch script. + WindowsBatch, // .cmd: an NT-only extension, so always a Windows batch script. + Msi, + Shortcut, +}; + +WindowsBinaryKind classifyWindowsBinary(const QString &localFilePath, const QString &mimeTypeName); diff --git a/src/WindowsCompatibilityHelper.cpp b/src/WindowsCompatibilityHelper.cpp index 4c259a2..4d2cc23 100644 --- a/src/WindowsCompatibilityHelper.cpp +++ b/src/WindowsCompatibilityHelper.cpp @@ -19,8 +19,9 @@ #include <QRegularExpression> #include <QStandardPaths> -WindowsCompatibilityHelper::WindowsCompatibilityHelper(const QUrl &databaseFilePath, const QUrl &openedExePath, QObject *parent) +WindowsCompatibilityHelper::WindowsCompatibilityHelper(const QUrl &databaseFilePath, const QUrl &openedExePath, bool isDosProgram, QObject *parent) : ICompatibilityHelper(openedExePath, parent) + , m_isDosProgram(isDosProgram) { m_nativeAppName = m_filePath.fileName(); m_hasNativeApp = false; @@ -105,6 +106,8 @@ QString WindowsCompatibilityHelper::heading() const } else { return i18n("Install %1 from %2 instead", m_alternativeAppName, appStoreName()); } + } else if (m_isDosProgram) { + return i18n("DOS programs are not natively supported on %1", distroName()); } else { return i18n("Windows applications are not natively supported on %1", distroName()); } @@ -139,14 +142,22 @@ QString WindowsCompatibilityHelper::description() const if (hasCompatibilityTool() && !hasNativeApp()) { desc += u"<br><br>"_s; - if (isCompatibilityToolInstalled()) { - desc += i18n("Alternatively, you can run the Windows version using %1. ", compatibilityToolName()); + if (m_isDosProgram) { + if (isCompatibilityToolInstalled()) { + desc += i18n("Alternatively, you can run this DOS program in the %1 emulator.", compatibilityToolName()); + } else { + desc += i18n("Alternatively, you can install %1 to run DOS programs.", compatibilityToolName()); + } } else { - desc += i18n("Alternatively, you can install %1 to run Windows applications. ", compatibilityToolName()); + if (isCompatibilityToolInstalled()) { + desc += i18n("Alternatively, you can run the Windows version using %1. ", compatibilityToolName()); + } else { + desc += i18n("Alternatively, you can install %1 to run Windows applications. ", compatibilityToolName()); + } + desc += i18n( + "This is not recommended for most users, as running Windows applications through a compatibility layer can have bugs, poor performance, and " + "poor system integration."); } - desc += i18n( - "This is not recommended for most users, as running Windows applications through a compatibility layer can have bugs, poor performance, and poor " - "system integration."); } return desc; diff --git a/src/WindowsCompatibilityHelper.h b/src/WindowsCompatibilityHelper.h index d80dee8..c7ff907 100644 --- a/src/WindowsCompatibilityHelper.h +++ b/src/WindowsCompatibilityHelper.h @@ -17,7 +17,7 @@ class WindowsCompatibilityHelper : public ICompatibilityHelper Q_OBJECT public: - explicit WindowsCompatibilityHelper(const QUrl &databaseFilePath, const QUrl &openedExePath, QObject *parent = nullptr); + explicit WindowsCompatibilityHelper(const QUrl &databaseFilePath, const QUrl &openedExePath, bool isDosProgram = false, QObject *parent = nullptr); ~WindowsCompatibilityHelper() override = default; QString windowTitle() const override; @@ -53,4 +53,6 @@ private: bool m_hasNativeApp = false; // e.g. if the user opens ie11.exe, this will be true as the Flatpak alternative is Microsoft Edge. bool m_needsAlternativeApp = false; + // True for real-mode DOS programs, which are run in a DOS emulator instead of Wine. + bool m_isDosProgram = false; }; diff --git a/src/contents/ui/Main.qml b/src/contents/ui/Main.qml index 4797129..7f9e3fe 100644 --- a/src/contents/ui/Main.qml +++ b/src/contents/ui/Main.qml @@ -54,6 +54,19 @@ Kirigami.ApplicationWindow { } } + Component { + id: secondaryInstallPageComponent + + InstallPage { + installer: PackageCompatibilityHelper.secondaryCompatibilityToolInstaller + onDeclined: root.pageStack.pop() + onSucceeded: { + PackageCompatibilityHelper.secondaryCompatibilityToolInstallFinished() + root.close() + } + } + } + Component { id: installPageMetricsComponent @@ -147,6 +160,21 @@ Kirigami.ApplicationWindow { } } + QQC2.Button { + id: secondaryCompatibilityToolActionButton + visible: PackageCompatibilityHelper.hasSecondaryCompatibilityTool && !PackageCompatibilityHelper.hasNativeApp + icon.name: PackageCompatibilityHelper.secondaryCompatibilityToolActionIcon + text: PackageCompatibilityHelper.secondaryCompatibilityToolActionText + onClicked: { + if (PackageCompatibilityHelper.secondaryCompatibilityToolInstalled) { + PackageCompatibilityHelper.launchSecondaryCompatibilityTool() + root.close() + } else { + root.pageStack.push(secondaryInstallPageComponent, { prepareOnCompleted: true }) + } + } + } + QQC2.Button { id: nativeAppActionButton visible: PackageCompatibilityHelper.hasNativeApp diff --git a/src/org.kde.package-compatibility-helper.desktop b/src/org.kde.package-compatibility-helper.desktop index 4ff5041..d241eb9 100644 --- a/src/org.kde.package-compatibility-helper.desktop +++ b/src/org.kde.package-compatibility-helper.desktop @@ -9,4 +9,4 @@ Icon=apper Type=Application Terminal=false NoDisplay=true -MimeType=application/x-ms-dos-executable;application/x-msi;application/x-ms-shortcut;application/vnd.microsoft.portable-executable;application/x-msdownload;application/x-rpm;application/vnd.debian.binary-package;application/java-archive; +MimeType=application/x-ms-dos-executable;application/x-dosexec;application/x-msi;application/x-ms-shortcut;application/vnd.microsoft.portable-executable;application/x-msdownload;application/x-ms-ne-executable;application/x-bat;application/x-msdos-batch;application/x-rpm;application/vnd.debian.binary-package;application/java-archive; diff --git a/trees/windows/usr/lib/binfmt.d/windows.conf.in b/trees/windows/usr/lib/binfmt.d/windows.conf.in new file mode 100644 index 0000000..426e308 --- /dev/null +++ b/trees/windows/usr/lib/binfmt.d/windows.conf.in @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: NONE +:PackageCompatibilityHelper-windows:M::MZ::@KDE_INSTALL_FULL_LIBEXECDIR@/windows-binfmt:F +# Batch files are plain text and have no magic, so match by extension. +# binfmt_misc extension matching is case-sensitive, hence one rule per casing. +:PackageCompatibilityHelper-bat:E::bat::@KDE_INSTALL_FULL_LIBEXECDIR@/windows-binfmt:F +:PackageCompatibilityHelper-BAT:E::BAT::@KDE_INSTALL_FULL_LIBEXECDIR@/windows-binfmt:F +:PackageCompatibilityHelper-cmd:E::cmd::@KDE_INSTALL_FULL_LIBEXECDIR@/windows-binfmt:F +:PackageCompatibilityHelper-CMD:E::CMD::@KDE_INSTALL_FULL_LIBEXECDIR@/windows-binfmt:F diff --git a/trees/windows/usr/libexec/windows-binfmt.in b/trees/windows/usr/libexec/windows-binfmt.in new file mode 100755 index 0000000..e6b6b42 --- /dev/null +++ b/trees/windows/usr/libexec/windows-binfmt.in @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: NONE +exec @KDE_INSTALL_FULL_BINDIR@/package-compatibility-helper "$1"