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 }