File indexing completed on 2024-04-14 04:52:21

0001 /*
0002  * SPDX-FileCopyrightText: 2022 Kai Uwe Broulik <kde@broulik.de>
0003  * SPDX-License-Identifier: GPL-2.0-or-later
0004  */
0005 
0006 #include "afcdevice.h"
0007 
0008 #include "afc_debug.h"
0009 
0010 #include "afcapp.h"
0011 #include "afcspringboard.h"
0012 #include "afcutils.h"
0013 
0014 #include <QDir>
0015 #include <QFileInfo>
0016 #include <QSaveFile>
0017 #include <QScopeGuard>
0018 #include <QStandardPaths>
0019 
0020 #include <libimobiledevice/installation_proxy.h>
0021 
0022 using namespace KIO;
0023 
0024 static const char s_lockdownLabel[] = "kio_afc";
0025 
0026 AfcDevice::AfcDevice(const QString &id)
0027     : m_id(id)
0028 {
0029     idevice_new(&m_device, id.toUtf8().constData());
0030     if (!m_device) {
0031         qCWarning(KIO_AFC_LOG) << "Failed to create idevice for" << id;
0032         return;
0033     }
0034 
0035     lockdownd_client_t lockdowndClient = nullptr;
0036     auto ret = lockdownd_client_new(m_device, &lockdowndClient, s_lockdownLabel);
0037     if (ret != LOCKDOWN_E_SUCCESS) {
0038         qCWarning(KIO_AFC_LOG) << "Failed to create lockdown client for" << id << ret;
0039         return;
0040     }
0041 
0042     ScopedLockdowndClientPtr lockdowndClientPtr(lockdowndClient);
0043 
0044     char *name = nullptr;
0045     auto lockdownRet = lockdownd_get_device_name(lockdowndClientPtr.data(), &name);
0046     if (lockdownRet != LOCKDOWN_E_SUCCESS) {
0047         qCWarning(KIO_AFC_LOG) << "Failed to get device name for" << id << lockdownRet;
0048     } else {
0049         m_name = QString::fromUtf8(name);
0050         free(name);
0051     }
0052 
0053     plist_t deviceClassEntry = nullptr;
0054     lockdownRet = lockdownd_get_value(lockdowndClientPtr.data(), nullptr /*global domain*/, "DeviceClass", &deviceClassEntry);
0055     if (lockdownRet != LOCKDOWN_E_SUCCESS) {
0056         qCWarning(KIO_AFC_LOG) << "Failed to get device class for" << id << lockdownRet;
0057     } else {
0058         char *deviceClass = nullptr;
0059         plist_get_string_val(deviceClassEntry, &deviceClass);
0060         m_deviceClass = QString::fromUtf8(deviceClass);
0061         free(deviceClass);
0062     }
0063 }
0064 
0065 AfcDevice::~AfcDevice()
0066 {
0067     if (m_afcClient) {
0068         afc_client_free(m_afcClient);
0069         m_afcClient = nullptr;
0070     }
0071 
0072     if (m_device) {
0073         idevice_free(m_device);
0074         m_device = nullptr;
0075     }
0076 }
0077 
0078 idevice_t AfcDevice::device() const
0079 {
0080     return m_device;
0081 }
0082 
0083 QString AfcDevice::id() const
0084 {
0085     return m_id;
0086 }
0087 
0088 bool AfcDevice::isValid() const
0089 {
0090     return m_device && !m_name.isEmpty();
0091 }
0092 
0093 QString AfcDevice::name() const
0094 {
0095     return m_name;
0096 }
0097 
0098 QString AfcDevice::deviceClass() const
0099 {
0100     return m_deviceClass;
0101 }
0102 
0103 QString AfcDevice::cacheLocation() const
0104 {
0105     return QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/kio_afc/") + m_id;
0106 }
0107 
0108 QString AfcDevice::appIconCachePath(const QString &bundleId) const
0109 {
0110     return cacheLocation() + QLatin1String("/%1.png").arg(bundleId);
0111 }
0112 
0113 WorkerResult AfcDevice::handshake()
0114 {
0115     if (!m_handshakeSuccessful) {
0116         lockdownd_client_t lockdownClient = nullptr;
0117         // libimobiledevice doesn't properly allow doing a handshake on an existing instance
0118         // Instead, create a new one, and when it works, swap the one created in the constructor with this one
0119         auto ret = lockdownd_client_new_with_handshake(m_device, &lockdownClient, s_lockdownLabel);
0120         if (ret != LOCKDOWN_E_SUCCESS) {
0121             qCWarning(KIO_AFC_LOG) << "Failed to create lockdownd client with handshake on" << id() << "- make sure the device is unlocked" << ret;
0122             return AfcUtils::Result::from(ret);
0123         }
0124 
0125         m_lockdowndClient.reset(lockdownClient);
0126         m_handshakeSuccessful = true;
0127     }
0128 
0129     return WorkerResult::pass();
0130 }
0131 
0132 WorkerResult AfcDevice::client(const QString &appId, AfcClient::Ptr &client)
0133 {
0134     auto result = handshake();
0135     if (!result.success()) {
0136         return result;
0137     }
0138 
0139     if (m_lastClient && m_lastClient->appId() == appId) {
0140         client = m_lastClient;
0141         return WorkerResult::pass();
0142     }
0143 
0144     Q_ASSERT(m_lockdowndClient);
0145 
0146     AfcClient::Ptr clientPtr(new AfcClient(this));
0147     result = clientPtr->init(m_lockdowndClient.data(), appId);
0148     if (!result.success()) {
0149         return result;
0150     }
0151 
0152     m_lastClient = clientPtr;
0153     client = clientPtr;
0154     return WorkerResult::pass();
0155 }
0156 
0157 AfcApp AfcDevice::app(const QString &bundleId)
0158 {
0159     auto it = m_apps.constFind(bundleId);
0160     if (it != m_apps.constEnd()) {
0161         return *it;
0162     }
0163 
0164     // Refresh cache
0165     QVector<AfcApp> appsList;
0166     if (!apps(appsList).success()) {
0167         return AfcApp();
0168     }
0169 
0170     // See if we know it now
0171     it = m_apps.constFind(bundleId);
0172     if (it != m_apps.constEnd()) {
0173         return *it;
0174     }
0175 
0176     return AfcApp();
0177 }
0178 
0179 WorkerResult AfcDevice::apps(QVector<AfcApp> &apps)
0180 {
0181     auto result = handshake();
0182     if (!result.success()) {
0183         return result;
0184     }
0185 
0186     lockdownd_service_descriptor_t service = nullptr;
0187     auto ret = lockdownd_start_service(m_lockdowndClient.data(), INSTPROXY_SERVICE_NAME, &service);
0188     if (ret != LOCKDOWN_E_SUCCESS) {
0189         qCWarning(KIO_AFC_LOG) << "Failed to start instproxy for getting apps" << ret;
0190         return AfcUtils::Result::from(ret, m_id);
0191     }
0192 
0193     auto serviceCleanup = qScopeGuard([service] {
0194         lockdownd_service_descriptor_free(service);
0195     });
0196 
0197     instproxy_client_t instProxyClient = nullptr;
0198     auto instRet = instproxy_client_new(m_device, service, &instProxyClient);
0199     if (instRet != INSTPROXY_E_SUCCESS) {
0200         qCWarning(KIO_AFC_LOG) << "Failed to create instproxy instance" << instRet;
0201         return AfcUtils::Result::from(instRet);
0202     }
0203 
0204     auto instProxyCleanup = qScopeGuard([instProxyClient] {
0205         instproxy_client_free(instProxyClient);
0206     });
0207 
0208     auto opts = instproxy_client_options_new();
0209     auto optsCleanup = qScopeGuard([opts] {
0210         instproxy_client_options_free(opts);
0211     });
0212     instproxy_client_options_add(opts, "ApplicationType", "User", nullptr);
0213 
0214     // Browse apps.
0215     plist_t appsPlist = nullptr;
0216     instRet = instproxy_browse(instProxyClient, opts, &appsPlist);
0217     if (instRet != INSTPROXY_E_SUCCESS) {
0218         qCWarning(KIO_AFC_LOG) << "Failed to browse apps via instproxy" << instRet;
0219         return AfcUtils::Result::from(instRet);
0220     }
0221 
0222     auto appsPlistCleanup = qScopeGuard([appsPlist] {
0223         plist_free(appsPlist);
0224     });
0225 
0226     m_apps.clear();
0227     apps.clear();
0228 
0229     const int count = plist_array_get_size(appsPlist);
0230     m_apps.reserve(count);
0231     apps.reserve(count);
0232     for (int i = 0; i < count; ++i) {
0233         plist_t appPlist = plist_array_get_item(appsPlist, i);
0234         AfcApp app(appPlist);
0235         if (!app.isValid()) {
0236             continue;
0237         }
0238 
0239         const QString iconPath = appIconCachePath(app.bundleId());
0240         if (QFileInfo::exists(iconPath)) {
0241             app.m_iconPath = iconPath;
0242         }
0243 
0244         m_apps.insert(app.bundleId(), app);
0245         apps.append(app);
0246     }
0247 
0248     return WorkerResult::pass();
0249 }
0250 
0251 WorkerResult AfcDevice::fetchAppIcon(AfcApp &app)
0252 {
0253     QVector<AfcApp> apps{app};
0254 
0255     const auto result = fetchAppIcons(apps);
0256     if (!result.success()) {
0257         return result;
0258     }
0259 
0260     app.m_iconPath = apps.first().m_iconPath;
0261     return result;
0262 }
0263 
0264 WorkerResult AfcDevice::fetchAppIcons(QVector<AfcApp> &apps)
0265 {
0266     QStringList appIconsToFetch;
0267 
0268     for (const AfcApp &app : std::as_const(apps)) {
0269         if (app.iconPath().isEmpty()) {
0270             appIconsToFetch.append(app.bundleId());
0271         }
0272     }
0273 
0274     if (appIconsToFetch.isEmpty()) {
0275         // Nothing to do.
0276         return WorkerResult::pass();
0277     }
0278 
0279     qCDebug(KIO_AFC_LOG) << "About to fetch app icons for" << appIconsToFetch;
0280 
0281     AfcSpringBoard springBoard(m_device, m_lockdowndClient.data());
0282     if (!springBoard.result().success()) {
0283         return springBoard.result();
0284     }
0285 
0286     QDir cacheDir(cacheLocation());
0287     if (!cacheDir.mkpath(QStringLiteral("."))) { // Returns true if it already exists.
0288         qCWarning(KIO_AFC_LOG) << "Failed to create icon cache directory" << cacheLocation();
0289         return WorkerResult::fail(ERR_CANNOT_MKDIR, cacheLocation());
0290     }
0291 
0292     WorkerResult result = WorkerResult::pass();
0293     for (const QString &bundleId : appIconsToFetch) {
0294         QByteArray data;
0295 
0296         const auto fetchIconResult = springBoard.fetchAppIconData(bundleId, data);
0297         if (!fetchIconResult.success()) {
0298             result = fetchIconResult;
0299             continue;
0300         }
0301 
0302         if (data.isEmpty()) {
0303             result = WorkerResult::fail(ERR_CANNOT_READ); // NO_CONTENT is "success, but no content"
0304             continue;
0305         }
0306 
0307         // Basic sanity check whether we got a PNG file.
0308         if (!data.startsWith(QByteArrayLiteral("\x89PNG\x0d\x0a\x1a\x0a"))) {
0309             qCWarning(KIO_AFC_LOG) << "Got bogus app icon data for" << bundleId << data.left(20) << "...";
0310             result = WorkerResult::fail(ERR_CANNOT_READ);
0311             continue;
0312         }
0313 
0314         const QString path = appIconCachePath(bundleId);
0315 
0316         QSaveFile iconFile(path);
0317         if (!iconFile.open(QIODevice::WriteOnly)) {
0318             qCWarning(KIO_AFC_LOG) << "Failed to open icon cache file for writing" << path << iconFile.errorString();
0319             result = WorkerResult::fail(ERR_CANNOT_OPEN_FOR_WRITING, path);
0320             continue;
0321         }
0322 
0323         iconFile.write(data);
0324 
0325         if (!iconFile.commit()) {
0326             qCWarning(KIO_AFC_LOG) << "Failed to save icon cache of size" << data.count() << "to" << path;
0327             result = WorkerResult::fail(ERR_CANNOT_WRITE, path);
0328             continue;
0329         }
0330 
0331         // Update internal cache.
0332         auto &app = m_apps[bundleId];
0333         app.m_iconPath = path;
0334 
0335         // Update app list argument.
0336         auto it = std::find_if(apps.begin(), apps.end(), [&bundleId](const AfcApp &app) {
0337             return app.bundleId() == bundleId;
0338         });
0339         Q_ASSERT(it != apps.end());
0340         it->m_iconPath = path;
0341     }
0342 
0343     return result;
0344 }