[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"
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.