File indexing completed on 2024-04-21 05:41:03

0001 /*
0002     SPDX-FileCopyrightText: 2020 Kwon-Young Choi <kwon-young.choi@hotmail.fr>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "mountisoaction.h"
0008 
0009 #include <fcntl.h>
0010 #include <unistd.h>
0011 #include <string.h>
0012 #include <sys/stat.h>
0013 #include <sys/types.h>
0014 #include <errno.h>
0015 
0016 #include <QAction>
0017 #include <QDBusConnection>
0018 #include <QDBusInterface>
0019 #include <QDBusReply>
0020 #include <QDBusUnixFileDescriptor>
0021 #include <QDebug>
0022 #include <QEventLoop>
0023 #include <QIcon>
0024 #include <QMap>
0025 #include <QObject>
0026 #include <QProcess>
0027 #include <QString>
0028 #include <QTimer>
0029 #include <QVariant>
0030 #include <QWidget>
0031 
0032 #include <KLocalizedString>
0033 #include <KPluginFactory>
0034 #include <Solid/Block>
0035 #include <Solid/Device>
0036 #include <Solid/DeviceInterface>
0037 #include <Solid/DeviceNotifier>
0038 #include <Solid/GenericInterface>
0039 #include <Solid/StorageAccess>
0040 #include <Solid/StorageVolume>
0041 
0042 K_PLUGIN_CLASS_WITH_JSON(MountIsoAction, "mountisoaction.json")
0043 
0044 MountIsoAction::MountIsoAction(QObject *parent, const QVariantList &)
0045     : KAbstractFileItemActionPlugin(parent)
0046 {
0047 }
0048 
0049 /**
0050  * Get block device udi ("/org/freedesktop/UDisks2/block_devices/loop0") using
0051  * its backing file name.
0052  *
0053  * Use the Solid framework to iterate through all block devices to check if the
0054  * backing file correspond to the given backingFile.
0055  *
0056  * Warning: The use of GenericInterface makes this function non portable,
0057  * especially to non Unix-like OS.
0058  *
0059  * @backingFile: backing file of the device we want.
0060  *
0061  * @return: device udi of the found block device. If no corresponding device
0062  * was found, return a null QString.
0063  */
0064 const Solid::Device getDeviceFromBackingFile(const QString &backingFile)
0065 {
0066     const QList<Solid::Device> blockDevices =
0067         Solid::Device::listFromQuery(QStringLiteral("[ IS StorageVolume AND IS GenericInterface ]"));
0068 
0069     for (const Solid::Device &device : blockDevices) {
0070         auto genericDevice = device.as<Solid::GenericInterface>();
0071         if (backingFile == genericDevice->property(QStringLiteral("BackingFile")).toString()) {
0072             return device;
0073         }
0074     }
0075     return Solid::Device();
0076 }
0077 
0078 const QList<Solid::Device> getStorageAccessFromDevice(const Solid::Device &device)
0079 {
0080     auto genericInterface = device.as<Solid::GenericInterface>();
0081     // Solid always returns UUID lower-case
0082     const QString uuid = genericInterface->property(QStringLiteral("IdUUID")).value<QString>().toLower();
0083     auto query = QStringLiteral("[ StorageVolume.uuid == '%1' AND IS StorageAccess ]").arg(uuid);
0084     return Solid::Device::listFromQuery(query);
0085 }
0086 
0087 /**
0088  * Callback function for mounting an iso file as a loop device
0089  *
0090  * Uses UDisks2 Manager DBus api to mount the iso file
0091  *
0092  * @file: iso file path to mount
0093  */
0094 void mount(const QString &file)
0095 {
0096     const int fd = open(file.toLocal8Bit().data(), O_RDONLY);
0097     if (fd == -1) {
0098         qWarning() << "Error opening " << file << ": " << strerror(errno);
0099         return;
0100     }
0101     auto qtFd = QDBusUnixFileDescriptor(fd);
0102     int res = close(fd);
0103     if (res == -1) {
0104         qWarning() << "Error closing " << file << ": " << strerror(errno);
0105         return;
0106     }
0107     QMap<QString, QVariant> options;
0108 
0109     QDBusInterface manager(
0110             QStringLiteral("org.freedesktop.UDisks2"),
0111             QStringLiteral("/org/freedesktop/UDisks2/Manager"),
0112             QStringLiteral("org.freedesktop.UDisks2.Manager"),
0113             QDBusConnection::systemBus());
0114     QDBusReply<QDBusObjectPath> reply =
0115         manager.call(QStringLiteral("LoopSetup"), QVariant::fromValue(qtFd), options);
0116 
0117     if (!reply.isValid()) {
0118         qWarning() << "Error mounting " << file << ":" << reply.error().name()
0119                    << reply.error().message();
0120         return;
0121     }
0122 
0123     // Need to wait for UDisks2 to send a signal to Solid to update its database
0124     auto notifier = Solid::DeviceNotifier::instance();
0125 
0126     // The following code can not be put into a slot because the MountIsoAction object is destroyed
0127     // as soon as this function ends
0128     QEventLoop loop;
0129     QTimer timer;
0130     timer.setSingleShot(true);
0131     QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
0132     QObject::connect(notifier, &Solid::DeviceNotifier::deviceAdded, &loop, &QEventLoop::quit);
0133 
0134     int i = 0, maxDeviceRace = 4;
0135     Solid::Device device;
0136     while (i < maxDeviceRace) {
0137         timer.start(5000); // 5s timeout
0138         loop.exec();
0139 
0140         device = Solid::Device(reply.value().path());
0141         if (!device.is<Solid::StorageVolume>()) {
0142             i++;
0143         } else {
0144             break;
0145         }
0146     }
0147 
0148     if (i == maxDeviceRace) {
0149         // Something really wrong happened
0150         return;
0151     }
0152 
0153     auto storageVolume = device.as<Solid::StorageVolume>();
0154     const QString uuid = storageVolume->uuid();
0155 
0156     const QList<Solid::Device> devices = Solid::Device::listFromQuery(
0157         QStringLiteral("[ StorageVolume.uuid == '%1' AND IS StorageAccess ]").arg(uuid));
0158     for (auto dev : devices) {
0159         auto storageAccess = dev.as<Solid::StorageAccess>();
0160         storageAccess->setup();
0161     }
0162 }
0163 
0164 /**
0165  * Callback function for deleting a loop device
0166  *
0167  * Uses UDisks2 DBus api to delete a loop device
0168  *
0169  * @file: iso file to mount
0170  */
0171 void unmount(const Solid::Device &device)
0172 {
0173     const QList<Solid::Device> devices = getStorageAccessFromDevice(device);
0174     for (Solid::Device storageAccessDevice : devices) {
0175         auto storageAccess = storageAccessDevice.as<Solid::StorageAccess>();
0176         if (storageAccess->isAccessible()) {
0177             storageAccess->teardown();
0178         }
0179     }
0180 
0181     // Empty argument required for Loop Delete method to work
0182     QMap<QString, QVariant> options;
0183 
0184     QDBusInterface manager(
0185             QStringLiteral("org.freedesktop.UDisks2"),
0186             device.udi(),
0187             QStringLiteral("org.freedesktop.UDisks2.Loop"),
0188             QDBusConnection::systemBus());
0189     manager.call(QStringLiteral("Delete"), options);
0190 }
0191 
0192 QList<QAction *> MountIsoAction::actions(const KFileItemListProperties &fileItemInfos,
0193                                          QWidget *parentWidget)
0194 {
0195     if (fileItemInfos.urlList().size() != 1 || !fileItemInfos.isLocal()) {
0196         return {};
0197     };
0198 
0199     const QString mimeType = fileItemInfos.mimeType();
0200 
0201     if (mimeType != QLatin1String("application/vnd.efi.iso")
0202             && mimeType != QLatin1String("application/vnd.efi.img")
0203             && mimeType != QLatin1String("application/x-cd-image")
0204             && mimeType != QLatin1String("application/x-raw-disk-image")) {
0205         return {};
0206     }
0207 
0208     auto file = fileItemInfos.urlList().at(0).toLocalFile();
0209 
0210     // Check if dbus can handle file descriptor
0211     auto connection = QDBusConnection::sessionBus();
0212     QDBusConnection::ConnectionCapabilities capabilities = connection.connectionCapabilities();
0213     if (!(capabilities & QDBusConnection::UnixFileDescriptorPassing)) {
0214         return {};
0215     }
0216 
0217     const Solid::Device device = getDeviceFromBackingFile(file);
0218 
0219     if (!device.isValid()) {
0220         const QIcon icon = QIcon::fromTheme(QStringLiteral("media-mount"));
0221         const QString title = i18nc("@action:inmenu Action to mount a disk image", "Mount");
0222 
0223         QAction *action = new QAction(icon, title, parentWidget);
0224 
0225         connect(action, &QAction::triggered, this, [file]() { mount(file); });
0226         return { action };
0227     } else {
0228         // fileItem is mounted on device
0229         const QIcon icon = QIcon::fromTheme(QStringLiteral("media-eject"));
0230         const QString title = i18nc("@action:inmenu Action to unmount a disk image", "Unmount");
0231 
0232         QAction *action = new QAction(icon, title, parentWidget);
0233 
0234         connect(action, &QAction::triggered, this, [device]() { unmount(device); });
0235         return { action };
0236     }
0237 
0238     return {};
0239 }
0240 
0241 #include "mountisoaction.moc"
0242 
0243 #include "moc_mountisoaction.cpp"